ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

为何我把问答需求都收敛到RAG:工程实践与踩坑记录

为何我把问答需求都收敛到RAG:工程实践与踩坑记录

为什么我最后把所有问答需求都收敛到了RAG这条路上

半年前我接手了一个内部知识库问答的项目,需求很朴素:把产品文档、运维故障库和客服话术整合成一个入口,让同事用大白话提问、系统直接给出可引用的答案。结果第一版做出来效果惨不忍睹——模型一本正经地编造了不存在的配置项,还煞有介事地标注了页码。

被幻觉折腾了几轮之后,我做了个比较极端的决定:所有问答场景全部换成RAG架构来跑,也就是笔记标题里写的"all-in-rag"。这里的all-in不是指技术栈全家桶,而是把"先检索再生成"这条链路当成默认选择,哪怕某些场景看起来用提示词硬答也能凑合。

这篇笔记是给自己留的实践记录,包含这几周里反复验证过的切分参数、检索层调优、踩坑排查和评估方法。如果你正在搭RAG系统,或者搭完之后发现效果忽好忽坏,这份记录应该能帮你少走一些弯路。

1. 为什么决定All in RAG:被幻觉整怕之后的选择

1.1 从"硬答"到"翻资料再答"的思路转变

最开始我倾向于在系统提示词里塞满约束,比如"只回答基于给定文档的内容,不要编造"。这种做法的局限很快就暴露了:约束写得再严格,模型面对不太确定的问题时仍然倾向于用"看起来合理的回答"填补空白。尤其是内部文档里大量存在互相矛盾的版本,模型根本分不清哪个是当前生效的规范,干脆自己挑一个逻辑顺的。

后来我把问题换了一种拆法:不指望模型"记得"文档内容,而是让它每次都先去知识库里"翻资料"。这一步看起来只是流程上加了个检索环节,实际上是改变了整个系统的不确定性来源——模型不再需要对知识负责,只需要对检索回来的一段上下文负责。

1.2 为什么不选微调路线

有一个反复被问到的选择:既然回答总是不准,直接把文档拿去做领域微调不就行了吗?我在动手之前确实认真比较过。微调的本质是把知识压缩进模型参数里,适合的是"稳定不变的表达习惯、输出格式、判断规则"这一类定制;而知识库问答的特点是内容频繁更新、边界模糊、错误代价高。公司这周的版本规范和下周的可能完全不同,如果每次文档一改就重新训练一轮,成本和时间都扛不住。

RAG的优势恰恰在于知识外挂:文档变了,只需重建对应索引,模型本体完全不动。代价是多了一跳检索的延迟、多了一块向量存储的维护成本,以及一套需要持续校准的检索流程。对于"答案必须能追溯到原始文档"的项目来说,这笔账怎么算都划算。

1.3 RAG不是银弹:什么时候我仍然会劝退

但我必须诚实记录一句:RAG不是所有问题的答案。如果知识总量只有几十条、问法高度固定、答案本身也很短,直接用传统搜索加模板更省事。RAG的价值在于"问题开放式、知识量大、对溯源有硬要求"的场景。纯粹为了追赶技术潮流硬上RAG,反而会把简单问题复杂化。

我在项目里划了一条分界线:凡是能枚举出标准答案的,直接走配置表;凡是需要从大量文本中综合、归纳、对比的,才交给RAG。这条分界线帮我省了不少无用功。

2. 从文档到向量:RAG前置链路里最容易被低估的三件事

2.1 文档接入:格式杂才是第一道坎

很多人以为RAG项目最难的环节是模型选择,实际动手才发现,把一堆乱七八糟的文档干净地送进索引系统才是耗时大头。我碰到过的格式包括:Markdown、PDF里的老式扫描件、各类表格、带批注的Word、甚至还有从某个旧系统导出来的纯文本。

我的处理原则是:入口处宁可多写解析器,也不要相信"这次格式都统一了"。每个格式都要有独立的清洗流程,而且要保存清洗后的中间态纯文本,方便后续排查。踩过的坑是PDF里多栏排版被解析成一段交叉文字、表格被拆得粉碎,这些如果不在前期发现,后面所有环节都会连环出错。

具体做法是给每个文档来源登记一个解析器,输出固定的结构化字段:文档标识、章节路径、页面范围、正文文本。这样切分、向量化甚至故障回放都有据可查。

2.2 切分策略:chunk尺寸并不是越小越好

切分是RAG链路里争议最大、也最看手感的一步。我先给结论:不存在一个通用最优chunk大小,它取决于文档粒度、模型上下文长度和答案形态三者之间的匹配。

我用一个小实验来说明,把一份产品手册按256字、512字、1024字三档分别切分,每个chunk之间重叠64字,然后拿同样一批问题测试检索命中情况。结果很有意思:短chunk对"某个参数是什么意思"这类精确查询召回更好,但答非所问的情况也变多,因为上下文切得太碎,生成时缺少前后文支撑;长chunk对"整个流程怎么操作"这类综合问题有明显优势,但如果chunk超过模型上下文窗口的三分之一,生成阶段能塞进去的片段数量就太少。

我最后采用的方案是按语义块为主、兼顾长度上限:优先按文档的一级二级章节边界切,单块超过1200字再强制截断。等于说先保证语义完整,再用长度做上限约束。这样做比纯按固定字数切更适合技术文档类语料。

2.3 向量化:一个容易忽略的一致性陷阱

向量化环节有一个细节非常容易踩坑:索引用模型和查询用模型必须保持一致。某个阶段我为了图方便,索引时用的是一套中文语义向量模型,查询时换了另一套更"强大"的模型,结果检索质量明显下降,所有向量分布空间都不在同一坐标系里,相似度计算完全没有意义。

另外,文档里的术语和口语化提问之间天然存在措辞差异。我的处理办法是索引端的文本保留原样,查询端做一层轻量改写,比如"这玩意儿怎么关掉"这种问法,先由大模型改写为"如何关闭某功能"再进向量检索。这个过程会在后面的章节详细展开。

3. 检索层的一次次校准:召回率、精度和排序的拉扯

3.1 从单路向量检索到混合检索

第一版系统只用了向量检索,效果在常规问题上还行,但在产品名缩写、型号代号这些场景下频繁翻车。原因不复杂:向量模型对"精确拼写"不敏感,PC-104和PC-104X在向量空间里距离很近,但对业务来说完全是两个东西。

我后来改成了混合检索:一路走向量召回语义相近内容,一路走关键词(就是传统倒排索引)召回精确匹配内容,两路结果合并后再统一进重排序环节。这样"语义上像"和"字面上同"都被覆盖到了。调整量最大的地方是两路的召回数量配比,我常用的配置是向量路召回30条、关键词路召回20条,合并去重后进重排序阶段。

3.2 重排序:不要迷信向量相似度排序

另一个经验是:向量检索算出的相似度分数只能用来排序候选,不能直接当作最终答案的置信度。我测试过多次,向量相似度最高的片段有时候反而是噪音——因为它在整体语义上接近问题,但具体到答案的要点却偏离了。

现在的流程是检索出TopK候选之后,再过一个专门的重排序模型,把候选片段和原始问题重新做一次精细匹配打分,然后按重排后的顺序拼接上下文送进生成模型。重排序会增加几十毫秒的开销,但换来的是答案质量的稳定提升,尤其在多个候选片段都"沾边"但只有一个是真正答案的场景,效果差异非常明显。

3.3 检索参数的经验区间

我把项目里反复试出来的参数区间记录在这里,方便自己有据可查:

参数经验值范围说明
chunk长度400-1200字短chunk偏精确,长chunk偏综合
chunk重叠32-96字太小容易丢上下文,太大浪费存储
向量召回数20-50条过小漏召回,过大加重排序负担
关键词召回数10-30条与向量路数量大致相当
重排后送入生成的片段数3-6条需要同时满足上下文窗口与信息冗余度
相似度阈值0.45-0.6低于0.45的片段基本可判定为不相关

这些参数的调整顺序也很重要。我自己的经验是先固定切分和向量化的环节,只调检索数量,确定一个相对合适的范围后,再回头微调chunk重叠。如果一上来就同时动好几个旋钮,出了问题根本定位不到是哪一环引起的。

4. 踩坑记录:检索为空、答案串台、知识过期

4.1 "明明入库了却查不到"的三种成因

这是我排查次数最多的一类问题,现象是完全相同的问法,某条文档内容却始终没有被检索到。三种成因值得单独记录。

第一种是切分后内容被清洗规则误伤。我遇到过解析器把空字符替换规则写得太激进,导致所有"RN"这种序列被当成换行符处理,整段配置说明被拦腰截断。排查方法很笨但有效:把切分后的中间结果导出成文本,按文档标识抽查几条,肉眼过一遍就知道内容有没有变形。

第二种是向量化模型的字典里缺少领域专属词。中文技术文档里的生僻组合词在通用向量模型里可能被拆得七零八落,检索时自然匹配不上。这个问题的解法是给索引端做领域词表扩充,必要时单独训练一个针对该领域的向量模型适配层。

第三种是查询改写阶段把关键词改没了。原本明确的产品型号被改写成了泛指描述,精确匹配的路就废了。后来我在查询改写时给模型加了强制约束:保持原问题里的实体词不能动,只允许补充同义词和展开描述。

4.2 多个片段互相打架时怎么办

知识库内容够大之后,必然出现多个文档片段对同一问题的说法不一致。这在文档版本并存的阶段尤其常见。生成模型拿到互相冲突的上下文,往往选择"取平均"式的糅合,输出一段每个字都来自文档、但整体观点和每一份原始文档都对不上的回答。

我的处理办法是三重防护。第一重,在切分时记录每个chunk的来源文档标识和生效时间;第二重,送入生成模型前按来源文档的时间戳给片段分组,同一来源的片段优先连续拼接,避免不同版本交叉出现;第三重,在提示词里明确要求"如果上下文中存在冲突说法,输出时优先采纳时间戳较新且标注了生效状态的来源,并在引用中标注对应的文档标识"。这套规则上线之后,糅合矛盾答案的情况大幅减少。

4.3 更新文档后的老数据清理

刚开始做系统时我只顾着加新文档,完全没考虑删除和覆盖。结果某条旧规范已经下线,但它留在向量库里的旧向量仍然被检索到,回答引用了已废弃的内容。这个坑比检索为空隐蔽得多,因为答案看起来完全正常,只是内容过时了。

现在的做法是在入库时给每个文档片段打上状态标签,逻辑删除后并不立刻物理删除,而是在检索结果过滤阶段直接把非生效状态的片段剔除。这样既避免检索阶段出现废弃内容,又保留了历史追溯能力。增量更新的触发采用"文档有变更就重建对应批次的索引",避免每次全量重建,节约大量计算时间。

5. 给RAG装上评估尺子:不评估等于盲调

5.1 先建一套小规模黄金测试集

在RAG系统上做调优最怕的就是自我感觉良好。某个检索参数改了之后,拿三条问题测一下觉得变好了,结果上线后整体反而变差。为了不做这种盲调,我花了大概一周时间手工整理了一份黄金测试集,规模不大,大约一百条问答对,但覆盖了分诊类型的多样性:单点事实查询、流程步骤归纳、对比差异、否定式问法、带错别字的口语提问。

这套测试集的价值不体现在数量上,而体现在"每个问题都明确标注了引用的标准答案出处"。只有这样,当系统输出路径变化时,才能精确判断是召回环节漏了正确文档,还是生成环节引用了错误片段。

5.2 关注三个核心指标

我日常盯的指标有三个。

召回率:标准答案对应文档是否出现在检索候选列表里。这个指标是整个系统的上限,召回都没找对,生成再怎么优化都是白费。评估方法是人工核对每条问题的TopK候选列表里有没有出现正确文档。

引用正确性:生成答案引用的文档标识与实际支撑依据是否匹配。这个指标专门抓"答案内容看起来对但其实引用了不相关文档"的情况。

答案可接受率:由评审人员给生成答案打分,判断回复是否满足用户需求。这个指标比较主观,适合抽检而非全量。

这三个指标各管一段,召回率管检索,引用正确性管生成约束,可接受率管最终体验。任何改动上线前都要在这三个指标上跑一遍回归。

5.3 每次改动都留基线记录

后来我发现,RAG调优最怕的不是效果差,而是"改来改去不知道改回了哪个版本"。所以现在每次调整参数、替换模型、变更切分规则之后,我都会跑一份完整的测试集评估结果,并附上一句话记录这次改动的原因和预期效果。时间久了之后,这些记录本身就是一套完整的调参地图,明确指出哪些路走过没用,哪些参数组合是真正有效的。

6. 下一步:从"检索-生成"到"带决策的RAG"

6.1 让查询改写从可选变成必需

RAG系统到了后期,检索质量的天花板往往不在向量模型,而在查询端。同一个问题用不同方式问,检索结果可以差别很大。我现在的做法是把查询改写作为一个独立的显式步骤:原始问题先进一个轻量模型,拆解出核心实体和意图,生成2-3个不同措辞的检索请求,分别走检索后合并候选再重排。

这个改动直接解决了两类以前无解的问题:一类是问题太长导致检索被无关词带偏;另一类是隐含实体没有在问题里出现,比如用户问"怎么申请外网权限",但文档里写的是"开通网络访问申请流程",只有通过改写补充同义表达才能把两边对上。

6.2 让系统自己决定从哪个知识库找

当知识库从一个扩展成多个(产品文档库、运维故障库、客服话术库),路由策略就变得很关键。所有库一把抓的做法既浪费检索资源,也会让不同来源的答案互相干扰。我尝试的方案是加一层"库路由"判断:先用一个轻量分类模型判断问题属于哪个领域,再只路由到对应的一个或两个库执行检索,库路由拿不准的问题才走全部库全量召回。

这个设计带来的好处非常直接:检索噪音少了、响应速度快了、不同库之间的答案冲突也少了。当然它引入了一个新风险就是路由判断错误会直接导致检索不到正确文档,所以路由层的置信度阈值我设得比较保守,宁可多路由一个库也不漏掉正确答案。

6.3 增量更新和缓存

最后记录两个降本增效的小改动。第一是增量索引的触发机制由定时全量改为文档变更监听,解析、切分、向量化只处理有变动的文档批次,大幅降低计算资源的开销和管理成本。第二是给高频问题加了一层答案缓存:完全相同或高度相似的问题命中缓存后直接返回之前的结果,不再走一遍完整检索和生成链路。实测下来大概有三成左右的重复问法能命中缓存,节省的延迟非常可观。


一些最终的笔记

记录到这里,其实还有好多细节没有展开,比如不同向量模型在同一批测试集上的效果对比、重排序模型的选型依据和阈值标定等等,这些内容等后续更新再补进来。目前这套RAG链路已经稳定跑了半个多月,最直观的改善就是"一本正经胡说"的比例明显下降,同事的反馈也从"这个回答看不太懂"变成了"这个回答能找到出处"。

我个人的体会是:RAG系统的复杂度是一点一点长出来的,不存在一步到位的完美方案。先把基础链路跑通,再围绕检索质量和评估体系逐步精修,比一开始就引入复杂的agent编排要稳妥得多。如果这份笔记能带给你哪怕一个可复用的排查思路,那就是它最大的价值了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进