一年内我先后参与了3个企业级RAG落地项目的设计与交付行业不同、技术栈也有差异但最后踩过的坑出奇一致。一个很反直觉的现象是Demo阶段越顺利的项目上线后的问题反而越扎手——因为Demo把检索能跑通当成了检索能用好而生产环境真正考验的根本不是跑不跑得通而是它能不能稳定、正确、可解释地服务真实流量。这篇文章不聊PPT只聊我在这些项目里真实遇到的问题、排查过程和沉淀下来的方案希望能帮准备上RAG的人少走一段弯路。1. Demo只会走最优路径生产环境满地都是异常路径1.1 Demo的默认假设在生产里几乎都不成立先给Demo方案画个像。一个典型的RAG Demo是这样准备几百页产品文档切成512字或者1024字的块塞进向量库搭一个检索问答链路拿三五个精心挑选的问题问一遍回答有模有样PPT上放几个截图演示结束。这个流程我见过太多次包括我们自己早期也是这么干的。问题在于Demo只验证了理想管道的连通性它默认了几个前提用户的问题表述足够规范、知识库里没有冲突信息和噪声、并发量极小、失败了重试一次就行。而生产环境的真实情况我列个对比表维度Demo的默认假设生产环境的真实情况用户提问精心准备的、表述完整的问句口语化、漏字、错别字、中英混合知识库规模几十篇干净文档几十万文档含扫描件、PPT、Excel、历史遗留知识冲突基本不存在同一问题答案在不同部门文档里可能打架调用模式单用户串行多租户并发流量有尖峰失败处理重试一次需要熔断、降级、兜底、可观测效果评估人工看一眼可量化的指标加持续回归这张表不是凭空写的是三个项目里一条条填出来的。1.2 用户query的脏远超预期我印象很深的是第二个项目做企业内部制度文档问答。上线前我们专门收集了一周的真实用户问题来验收系统结果发现真实query和Demo时用的query完全是两回事。Demo时我们问差旅报销的审批流程是什么真实用户问的是出差回来钱什么时候打到我卡上我们问年假可以累积吗用户问的是去年的假还能不能用了。语义上是同一个问题词面上几乎不重叠。这直接导致按词面匹配的检索方式无以为继纯向量检索也经常召回一堆不相关的东西因为用户问法里的关键实体和文档里的关键实体对不上。说白了Demo里那些精心设计的query恰好和文档原句高度重合系统当然答得漂亮。真实世界的query千奇百怪才是检验RAG真正实力的考题。1.3 单次调用到持续服务是质变第三个项目上线第一天多个内部子系统同时调用问答服务的embedding接口同一个嵌入模型被几十个并发请求打到CPU满载部分请求直接超时。Demo阶段根本不会有人用压测工具去测一个RAG链路但线上它就是实实在在的故障。后来我们花了两周时间做缓存、做降级、做模型服务独立部署才让链路稳定下来。这个经历让我明白一个道理Demo是验证能不能生产是验证扛不扛得住这两件事之间隔着一整套工程体系。2. 三个项目里最让我印象深刻的四次生产事故2.1 项目Aembedding服务OOM原因是模型按一个文档库一个实例部署第一个项目里我们按部门做了好几个知识库图省事每个知识库都独立部署了一个embedding模型服务。结果某个部门的大促活动知识库流量冲上来之后模型服务直接OOM重启。排查过程比较曲折。先是怀疑向量库连接池问题查了半天发现连接池一切正常又怀疑网关问题看日志也没什么异常最后看监控才发现是多个模型服务实例把GPU显存和CPU内存吃满了其中一个实例被OOM killer杀掉后依赖它的服务全开始报错。教训是两层。第一embedding模型服务必须做资源隔离和限流不能用同一个实例去扛所有知识库的并发第二高频相同问题一定要做缓存否则每个用户进来都重新算一遍文本向量再好的机器也扛不住。我们后面统一改成共享embedding服务 query语义缓存 分库路由的结构OOM问题再没有出现过。2.2 项目BTopK召回的数字很好看真正回答时却答非所问这是最扎心的一次。我们用一份包含500条测试问题的评测集跑离线指标Hit Rate前10个召回结果里包含标准答案做到了85%以上觉得可以上线了。结果灰度期间用户投诉率飙升典型表现是问XX产品支不支持蓝牙系统回答里带着一大段产品参数说明之类的无关文字最后模棱两可说支持但请参考说明书。后来人工分析才发现我们的标准答案是从文档原句里摘出来的测试问题也是围绕这些原句改写的所以检索看起来很准。可真实用户的问法跟文档语句差距很大TopK召回的结果根本不在正确答案附近。这就是典型的评测集偏置——你测什么就优化什么你的测试问题分布是窄的系统暴露的问题也只会出现在那个窄区间。这件事逼着我们做了两个改动。一是引入BM25和向量检索的混合召回用词面匹配去补纯语义匹配的短板二是引入重排阶段把召回的前50到100条塞给cross-encoder模型重新打分只取前5到10条进生成。改动之后灰度投诉率明显下降。Demo里问一句就答对常常是幸存者偏差只有把评测问题的分布做广做杂才能暴露召回的假阳性。2.3 项目C知识库更新之后新旧答案同时出现第三个项目是产品售后知识库厂商会不定期发布新版手册。第一次更新时我们写了个脚本把新文档灌进向量库但没有清理同名的旧文档。结果用户问同一个问题一会儿答新版的内容一会儿答旧版的内容非常尴尬。更危险的是有些旧文档包含已停产的型号参数会被当成当前信息推荐给用户。这个问题光靠向量库覆盖写入解决不了——旧chunk和新chunk在语义上高度相似很容易同时被召回。我们的解决办法是在每个chunk的元数据上挂版本号和生效日期检索时强制过滤掉已经失效的版本。再配合一个简单的文档版本对照表更新文档时先把旧版本标记为已失效再灌入新版本。这套机制看起来不酷但生产上极其有用。2.4 共性问题没有评估体系的RAG改进等于闭眼开车三个项目挣扎到中期我越来越清晰地意识到上面这些事故之所以发生根本原因是团队在Demo阶段没有建立一套可回归的评估体系。做Demo的时候改一个prompt、换一种chunk大小全靠肉眼判断效果可在生产环境改动一个参数可能影响几百个线上query的表现。没有一套自动化评测你根本不知道自己是改好了还是改坏了。所以我把评估单独留了一整章来讲——对我来说它是企业级RAG和Demo级RAG的分水岭。3. 重新设计检索链路从能查到到查得对、查得快、查得稳经历过几次事故之后我把RAG的检索链路重新梳理了一遍。下面这套结构基本成了后来三个项目共享的模板不一定适合所有场景但至少是经过生产流量验证的。3.1 入库不是切块-embedding-灌库三步那么简单很多教程会把人引到一种误区先把文档切成512或1024字符的chunk然后embedding然后灌入向量库完事。这套流程做Demo一点问题没有生产上会遇到两类很现实的问题一是表格、代码、附图被切开后语义不完整二是同一个chunk可能对应多个用户问法。我们的做法是对入库材料做一次面向检索的结构化预处理。具体来说先用版面解析工具把PDF/Word里的标题层级、表格、列表结构提取出来尽量保留结构信息结构感知切块而不是按固定字符数硬切标题层级完整、表格整体保留、代码块不拆开给每个chunk生成一组规范问句作为附加索引。也就是让LLM把每个chunk改写成用户可能会问的问题用这些问句去做embedding和匹配。这个思路类似Query改写但方向是反的——不是改用户的query而是扩大文档侧的语义覆盖面。第三步看起来多花不少token但效果非常明显。因为用户query往往和文档原句不是同一句话我们把文档侧可被匹配的语义铺得更宽本质上是在缓解词汇鸿沟问题。3.2 混合检索向量加BM25用RRF合并纯向量检索的问题我在项目B已经讲过词面完全不重叠的query容易召回失败纯词面检索则对同义改写无能为力。生产上最稳的做法是把两者结合向量检索负责语义相近BM25负责词面精确命中然后用RRFReciprocal Rank Fusion合并排序。RRF的公式很简单对每个候选文档把它在两路结果中的排名取倒数再加权求和def rrf_score(ranks, k60): return sum(1 / (k rank) for rank in ranks if rank is not None)其中k是一个平滑常数通常取60。这个算法没有参数需要训练实现也只要十几行但稳定性相当好。我后来也试过用learning-to-rank模型做融合效果提升不大运维成本却高了一大截最后线上主力仍然是RRF。3.3 Reranker从召回TopK到精排TopN召回阶段的目标是别漏重排阶段的目标是别错。我们用cross-encoder做rerank把Top50的chunk逐对喂进模型计算query和doc的匹配分数重新排序取Top10。这个环节直接把线上应答质量提升了一个台阶因为向量和BM25都是基于表示的匹配而cross-encoder是交互式匹配对细粒度的相关性判断能力更强。代价是耗时变高一个query可能要花几百毫秒在rerank上。我们的做法是限制rerank的输入规模只对Top50做重排而不是全量重排。另外rerank模型要单独调优别直接拿来一个公共模型就上最好用一批领域内的相关/不相关样本做微调效果差异很大。3.4 在线链路的缓存、降级与兜底这一块是Demo方案最不关心的但生产环境最离不开的。缓存按语义加关键词双重hash做query缓存。完全一样的query直接命中缓存相似query用向量相似度做语义缓存。缓存命中率做上去之后embedding服务和LLM调用量能降40%以上。降级当embedding服务或向量库响应超时系统自动走BM25加规则抽取的降级链路虽然回答质量会下降但至少不会给用户一个报错页面。兜底当检索结果的相关性分数低于阈值时系统明确告诉用户当前知识库没有找到相关资料而不是强行编一个回答。后端生成阶段还加了一个faithfulness校验——用LLM检查回答内容是否都能由检索到的原文支撑不满足就拒绝回答。3.5 延迟的量化标准别只看平均耗时上线初期我们只看平均延迟结果被平均值骗得很惨。后来改成看p95和p99才发现最慢的那5%请求能拖到8秒以上原因往往是大chunk拼接超出上下文限制、或者rerank排队。我们当时定的一个比较现实的基线是在混合检索加rerank全链路下p95延迟控制在3秒以内p99控制在8秒以内如果超过这个范围先优先砍rerank规模或调低重排层数而不是去升级GPU。这里有个容易被忽略的点延迟和召回质量是互相制约的。你召回越多、重排越多延迟就越高。所以一定要先定延迟预算再在预算内做召回和重排的配置而不是先做功能再调速度。4. 真正的护城河在数据侧知识治理决定了RAG的上限有一句话在RAG圈子里被反复说但很少有人彻底执行RAG的上限由数据决定而不是由模型决定。真相确实是这样的。模型再聪明检索结果如果是错的它只能一本正经地胡说。4.1 结构感知切块比固定512字符高明得多固定长度切块是RAG的经典反模式。它的问题很直观一篇文档的某个小节可能只有两三百字讲的是一个完整概念你把它和一个毫不相干的小节并到一个chunk里另一些长表格又会被拦腰截断。生产上我们最终采用的是标题加列表加表格感知的切块器优先按文档结构切遇到长表格整块保留遇到代码块强制不拆分。chunk长度的上下限可以放宽到300到1500字但核心原则是每个chunk在语义上自洽。一个常见的问题是chunk重叠度设多少。我们试过0、10%、20%三档最后线上用的是10%到15%。重叠太少会切断语义重叠太多会造成大量冗余chunk既浪费存储又容易引入噪声。这个值最好根据你自己的文档结构跑评测集对比不要照抄网上的配置。4.2 元数据不是可有可无是检索过滤的地基早前我们灌向量库时只给每个chunk写了文档名加chunk序号后来做知识更新和权限控制时才发现远远不够。生产级知识库给chunk打元数据至少要覆盖文档标题、文档ID、来源部门、更新日期、版本号、生效状态、可见范围。这些字段不只是给人看的它们要参与检索过滤。比如权限控制检索阶段就根据用户角色过滤掉无权访问的chunk而不是等答案生成之后再判断能不能看后者很容易出现文档内容已经泄露到上下文里的情况日志里也可能留下敏感内容的痕迹这在合规审计上是说不过去的。4.3 知识更新的幂等性与灰度知识库不可能一直不变。每次更新我都担心两件事旧知识删不干净、新知识还没生效。后来我们建了一个文档版本对照表每次发布新版文档时先把该文档ID下所有旧chunk标记为失效再灌入新chunk查询时过滤条件里带上生效状态有效。对于高风险的制度类、产品参数类知识还支持按文档维度的灰度更新——先在小范围内生效验证检索和回答正常后再全量放开。这一步能防止文档里有个小错误结果全量上线被所有用户看到的尴尬。4.4 权限与合规企业级RAG绕不开的硬约束只要企业知识库里存在部门内部资料管理层财务口径这类内容权限控制就是硬需求。检索阶段必须带用户权限标签常见做法是把文档的可见范围映射为部门ID/角色ID的集合向量数据库查询时用metadata filter把无权访问的chunk抛掉。数据出境问题也要提前想。部分客户明确要求所有内容必须在自有机房内处理那就不能调外部embedding API、外部LLM API。这个约束会直接决定技术选型是私有化部署的开源模型还是混合云方案。千万别等架构做完了再被合规部门一票否决。4.5 数据质量评估用检索命中率加冲突率评价知识库数据治理做得好不好可以用两个指标直观衡量检索命中率标准问题集里有多少query能在Top10召回中找到正确chunk。如果这个数字不达标优先查知识库的覆盖度和切块方式而不是调模型。知识冲突率同一query召回结果中互相矛盾的高置信chunk占比。这个指标过高说明文档之间存在版本冲突或口径冲突需要在数据侧解决。这两个指标配合人工抽检能把数据侧的问题和模型侧的问题分开。我一直觉得能把问题定性到具体层面就已经解决了一大半麻烦。5. 构建可回归的评估体系让RAG越改越稳最后一个章节我想认真讲讲评估。这不是为了写论文而是为了一个最朴素的目的让团队敢去改系统。没有可回归的评估每一次调参都是赌博。5.1 评估集怎么来从真实日志里捞而不是自己编自己拍脑袋编的问题集有一个致命缺陷——它们总是偏向那些能答上来的问题。正确做法是从上线后的真实日志里抽取高频、长尾、失败和成功的问题混合起来做成评测集。我们当时的做法是上线第一周先记录所有用户query人工标注出一批有标准答案的问题再按业务场景补充一批未见过的query最终组成一个200到500条的评测集。这个数量不算多但比100条自编问题集有效得多因为它覆盖了真实用户的表述习惯。5.2 指标怎么选Hit Rate、MRR、Faithfulness、Answer Relevance常用指标我直接按经验解释一下Hit RateK前K个检索结果里是否包含标准答案。这是检索召回能力的底线指标K取5或10都可以。MRRMean Reciprocal Rank标准答案在检索结果里的排名有多靠前。它比Hit Rate更敏感适合观察微调之后排名是升了还是降了。Faithfulness忠实度生成结果能否由检索原文支撑。用LLM打分或者NLI模型都可以它反映的是有没有胡说。Answer Relevance答案相关性回答是否贴合用户问题不跑偏。这个指标和Faithfulness要一起看一个管对不对一个管有没有用。这些指标的绝对值没有统一标准我习惯跟自己的baseline做对比而不是跟别人的数字比。重点是每次改动之后评测集的分数不能掉最好还能涨一点。5.3 把评估塞进开发流程调参之前先跑回归我现在的习惯是任何涉及检索、prompt、切块策略的改动都要先跑一遍全量评测集对比baseline结果。可以做成一个简单的脚本任务也可以在CI/CD流水线里加一个step。模型或参数改动如果导致Hit Rate下降超过2个百分点或者出现新的Faithfulness失败案例就禁止合并。这个习惯对我们的帮助非常大。项目后期基本杜绝了改了A问题、冒出B问题的循环因为每次改动的影响都能量化。它叮嘱不了所有风险至少让团队对系统状态有数。5.4 线上监控与反馈回路把差评变成训练资产评测集是离线视角线上还需要一套反馈回路。我们的做法很简单在问答页面加了回答是否有帮助的反馈按钮后台把负面反馈的query、当时检索到的chunk、LLM的完整回答一起落日志。每周抽一批负面样本回填到评测集里。这样评测集不是静态的它会随着线上真实问题不断生长RAG系统也就能在这个循环里越改越稳。到项目后期我们基本可以做到每一次线上负面反馈在下一周就会变成评测集里的一个用例系统的短板会被持续而系统地补上。文章写到这我想说的核心经验其实就一条做RAG别追求一次答对要追求每次改动都有数。Demo阶段可以靠直觉和运气生产阶段必须靠数据和流程。上面这些结论是三个项目里一个一个坑填出来的希望能让正在从Demo走向生产的你少走一段弯路。后面我估计还会遇到新问题到时候再接着分享。