我最近把一个 RAG 系统从“能回答”调到了“回答得靠谱”,整个过程踩了不少坑,也摸出了一些可复用的方法论。平时聊 RAG 优化,网上最多的说法是“换个更好的 Embedding 模型”“加大 top_k”,好像改个参数就能起飞,但实际跑过之后你会发现,回复效果差往往是多个环节叠加导致的,二八原则在这里特别明显:真正让效果质变的,往往是数据清洗、切分策略、检索融合和生成侧的约束,而不是某个单点的参数魔改。这篇就来梳理我在一个实际知识库问答项目里的全链路优化全流程,从问题定位、数据预处理、检索策略、生成约束到评估回归,每一环我都会说清楚为什么这么做、怎么做、做完效果怎样。
这篇内容适合正在做 RAG 落地的工程师,也可以给产品和技术负责人做方案参考。涉及的思路和代码片段都可以直接抄作业,但也建议你先跑一遍自己的 badcase,再针对性调整,因为不同领域的文档结构差异非常大,盲目照搬参数很容易翻车。
1. 先搞清楚RAG系统“烂”在哪:从失败样本反推优化方向
1.1 三种高频翻车现场
接手这个项目的时候,我们的 RAG 系统已经能跑通“用户提问-检索-生成”的完整链路,但内测反馈非常集中:回答内容经常出现“答非所问”“信息过时”“胡说八道”。我把这些问题翻来覆去看了几遍,发现它们表面上是生成模型的问题,实际上根因五花八门。
第一类典型场景是用户问“XX功能的计费规则是什么”,系统检索回来的内容全是产品介绍页,包含了大量“XX功能支持包年包月”这类泛泛而谈的信息,大模型只能基于这些没营养的内容强行凑答案,最后给出一堆正确的废话。这类问题本质上是检索召回不精准,文档里真正写定价表格的段落根本没有被召回到。
第二类典型场景是用户问一个复合问题,比如“如果我同时购买A套餐和B套餐,能否叠加优惠,退款时怎么计算”,系统只召回了一个套餐的介绍,模型就把单套餐的逻辑当作通用规则来回答,最后答案与官方规则完全相反。这类问题本质上是检索召回不全,相关切片分散在多篇文档里,但系统只取到了其中一小块。
第三类典型场景是用户问的信息在知识库里根本没有,比如“C功能什么时候上线”,系统没有识别出知识库覆盖不了,而是基于幻觉生成了一段“预计将在下季度上线”的假消息。这类问题的责任主要在生成环节,缺少必要的拒答机制和知识边界判断。
我把这些案例整理成一个坏case库,并给每个问题标上现象、猜测根因、涉及模块三个阶段,后续每一步优化都用这批样本来验证效果。这一步虽然耗时,但是整个项目里最值钱的投资,没有样本库,后面的优化全是拍脑袋。
1.2 目标不是“看起来答得对”,而是“能落地的质量指标”
RAG 系统的回复质量不能只靠人眼感受,必须有一组可量化、可回归的指标。我在项目里用了四个维度,组成了评估的基本框架:
| 指标 | 含义 | 评测方式 |
|---|---|---|
| 忠实度 | 答案是否完全基于检索到的文档,没有编造 | 人工打分 + 句子级归因检查 |
| 相关性 | 答案是否直接回答了用户的问题 | 对照黄金答案计算语义相似度 |
| 完整性 | 复合问题中的所有子问题是否都被覆盖 | 人工逐点核对 |
| 引用准确率 | 答案里标注的引用来源是否真实对应文档内容 | 程序化校验引用编号 |
这里要特别强调引用准确率,很多团队会忽略这一项,只看答案文字是否流畅。我在实测中发现,大模型非常喜欢“自然地”把多个信息片段融合在一起,然后给一个看似合理的引用编号,但它引用的那条文档里其实根本没有这句话。这个指标不抓,用户一旦去点引用核对,立刻会对你整个系统的信任度打对折。
确定指标体系之后,我拉了一批测试集,大概 200 条来自真实用户日志的问题,覆盖了产品规则、故障排查、使用教程、政策说明四类场景。之后每做一次优化,就在这个固定测试集上跑回归,记录四个维度得分的变化。我会在最后专门讲评估体系怎么搭建和固化,这里先不展开,但请你记住:没有固定测试集和量化指标之前,任何“效果变好了”的判断都是主观错觉。
2. 文档侧的脏数据与结构噪声:很多检索问题其实从这里开始
2.1 清洗优先级清单:不是所有文本都适合直接切分
很多人做 RAG 会直接拿现成文档丢进切分器,这是在给后面的检索埋雷。我处理的这批知识库文档来自多个渠道,有从官网帮助中心导出的 HTML、有产品团队交付的 Markdown、还有一堆历史遗留下来的 PDF。实际清洗时我发现,真正的重灾区是以下几类内容:
- 页眉页脚和导航重复文本:每个页面都带着“产品帮助中心|联系我们”字样,切分后这些片段大量重复,检索时会被模型当成高权重噪声。
- 代码块与表格被错误换行:PDF 导出的表格经常在单元格中间断行,切分后语义完全破碎。
- 版本信息混杂:同一条规则在新旧版本文档里说法不一致,模型经常捡到旧版本。
- 无意义符号和乱码:比如“© 2024 某某公司 保留所有权利”混在正文中间。
清洗的逻辑是:先把非正文噪声剥掉,再做切分,而不是切分之后再清洗。我用 Python 写了一个预处理管道,先按文档格式分类,HTML 就用 BeautifulSoup 提取正文区,PDF 就先按页抽文本再用规则拼回段落,Markdown 相对好处理但也要过滤掉“最近更新”这类时间戳文本。
这一步做完之后,最直接的变化是:检索结果的精准度明显提升,因为被噪声文本污染的高频片段少了。这里给一个实际经验值:我处理的文档集清洗前后切片总数减少了大约 18%,但单个切片的信息密度提升了不止一倍,检索命中率靠这一步就有了肉眼可见的提升。对于版本混杂问题,清洗阶段没法彻底解决,我会在元数据设计那一节用一个较简单的方式处理。
2.2 切分参数不是玄学:块大小、重叠窗口与语义边界
切分策略是 RAG 数据侧优化的核心环节,也是最容易被忽视的部分。我的经验是:切分单位应该尽量保持语义完整,而不是机械地按字数切割。如果切出来的一个片段是“计费方式:按量付费。如果用户选择包年”,下一段是“包月的价格见下表”,那这个语义信息就被撕裂了。虽然重叠窗口能缓解这个问题,但治标不治本。
我在项目里对比了三种切分方式:
- 固定长度切分:实现简单,但极易切断段落和列表。
- 递归字符切分:先按段落、再按句、最后按字符兜底,能显著减少语义断裂。
- 基于结构感知的切分:识别标题层级、列表项和代码块边界,用文档自身的结构来决定切分点。
最终我采用的是第三种思路,利用文档里的 Markdown 标题层级和列表结构作为切分边界,配合向量模型的 token 上限来限制每块的最大长度。切分参数上,我测过 256、512、1024 三档块大小。实测下来,和这个项目场景比较匹配的是中小块加一定重叠的做法:块大小控制在 512 字符左右,重叠窗口设置 80-120 字符,既保证片段内容不至于太碎,又能覆盖到跨切片的上下文。但这个值仅供参者参考,如果你的文档是密集的表格型或者长文档叙述型,最优值会不同,还是要拿自己的 badcase 去测。
还有一个很容易犯的错:没有把“一级标题+二级标题”这样的上下文信息带进每个切片。我在早期版本里直接把文档按标题切完就灌库,切出来的小片段内容本身是完整的,但模型不知道这个片段属于哪个章节、解决什么问题。检索时遇到比较模糊的 query,这个信息差就是致命的。后来我在每个切片的文本前面注入章节目录链路,比如“[产品规则>计费>退费规则] 退费条件:……”,检索效果又上了一个台阶,这一招强烈建议直接用上。
2.3 元数据设计:给检索模型画重点
只靠文本切片本身是不够的,RAG 系统的检索能力很大程度上取决于你给每条切片附加了哪些元数据。我在这个项目里给每个切片都打上了这些标签:
- 唯一 ID 和版本号:用于追溯引用来源。
- 文档标题和章节路径:方便生成侧拼接引用信息。
- 文档类型:产品规则、操作教程、FAQ、故障代码表等。
- 更新时间:用于后续的时效性过滤。
- 关键词标签:少量人工规则补充,比如把“退款”“退费”“取消订单”归为同一主题。
这套元数据最大的价值体现在两块。第一,生成侧可以用它组装引用列表,大模型能直接输出带有可点击来源的答案,而不是空泛地编一个段落;第二,检索侧可以做硬过滤,比如当用户明确问“退款规则”时,优先只召回文档类型为“产品规则”的切片,把“操作教程”这类相似但不直接相关的文档排除掉。
版本问题上我做了个较务实的妥协:因为老版本规则仍然在生效期内,用户有可能会问旧规则,所以我没有粗暴地只保留最新版,而是把所有切片带上版本号,并在元数据里标记“是否当前生效版本”。检索时优先召回当前版本切片,只有当前版本覆盖不足时才允许旧版本进入候选集。这套“版本优先 + 兜底”的做法不完美,但比无脑全量召回稳定得多,也推荐你在处理多版本文档时采用类似的策略。
3. 检索侧的重构:从单一向量检索到混合召回再加一轮重排
3.1 Embedding 选型与向量化细节
先聊 Embedding。刚启动时团队内部有人建议直接用某个重量级大模型来做向量化,理由是“大模型理解能力强”,但实测下来效果并没好到哪儿去,成本和耗时却翻了好几倍。我在这个项目的做法是:在开源向量模型里做对比,而不是盲目追大。当时初步筛选了三个候选,用同一批测试问题跑召回率,最终选定的是一款中文场景表现稳定的开源模型,维度适中,推理速度快,和我们的文档领域契合度也够。
这里有一个容易被忽略的点:做向量化的文本和检索 query 之间,要在格式上对齐。比如我们的知识库文档是正式书面语,而用户真实查询经常是口语化的短句,比如“买多了能退钱不”。两者直接去算相似度,匹配质量会很难看。我采取的改进办法是,在向量化之前对 query 做一次轻量改写,把口语问题扩展成包含业务术语的查询语句,改写完再走 embedding。这一招的效果立竿见影,检索命中率的提升非常明显。
另外,因为现有模型还在持续更新,我会在这个项目里明确锁死向量化模型的版本,并且所有切片都记录当时的 embedding 模型 ID。这个经验是从一次事故里得来的:某天研发顺手升级了向量化模型但没重建索引,结果旧索引的向量和新模型的向量空间对不上,检索质量突然雪崩,排查了很久才发现是版本不匹配。所以,升级模型后务必全量重建向量索引,并且把模型版本写入元数据,这一点值得刻在流程里。
3.2 混合检索:关键词召回负责“稳”,向量召回负责“懂”
纯向量检索在长尾 query 上容易翻车。比如用户问具体的产品编码“XK-2307”,或者问一个冷门的配置项名字,向量模型可能因为训练集里没见过就匹配不准,但 BM25 关键词检索对这种精确匹配非常稳。所以我的方案不是二选一,而是混合检索:向量召回和关键词召回同时跑,最后合并候选集。
实现上我用的是比较标准的做法:先分别用 BM25 和向量检索各取出 top 50,取并集后进入重排序环节。关键词检索这里我用到了两个加法:一是对 query 做简单分词后加同义词扩展,比如“退钱”扩展出“退款”“退费”;二是对文档中的编码、型号类字段做精确匹配权重提升。实践下来,混合召回的召回率比纯向量检索高出不少,尤其是在标题和编码类问题上的优势非常明显。
混合之后的合并逻辑有一个细节值得注意:两路召回结果在打分尺度上完全不统一,不能直接按原始分数排序取 top。需要一个归一化或者融合的步骤,这部分我放在下一节一起讲。
3.3 重排序的必要性:给召回结果做一次“精加工”
重排序是整个检索链路里投入产出比很高的一环。混合召回拿到的候选集可能有几十条,但真正和用户问题强相关的可能只有几条。如果直接把 top 几十条全塞给大模型,不仅浪费 token,还会干扰生成质量。所以我在召回之后加了一层重排序模型,把候选集重新计算相关性得分。
我比较了两个路线:一个是轻量级的 RRF 分数融合,另一个是使用跨编码器的重排序模型。RRF 实现简单、不需要额外模型,但它的本质只是不同排序位置的加权合并,对于语义相关性的提升有限。跨编码器重排序模型则可以真正把“query”和“document”配对输入模型做深度语义匹配,效果更好。我最后采用的是跨编码器重排,让重排后的 top 3 进入生成环节。
这一步之后,我观察到一个很有价值的现象:不是所有问题都适合取固定 top 数量。有些问题是明确的“单文档事实问题”,比如“某某功能的限制是什么”,取一个高相关切片就够了;但“套餐对比”“流程差异”这类问题,需要多篇文档的多个切片才能拼出完整答案。所以我在重排之后做了一个简单的判断:如果重排结果的前两条相关性得分差距过大,就只保留该条;如果前几条得分相近,则保留更多。动态 top 策略比固定 top_k 在完整性指标上提升更明显。这个调整不算复杂,但收益不错。
4. 生成侧调优:把检索结果变成靠谱回答的最后一道关卡
4.1 Prompt 结构的三个层次:身份、约束与知识隔离
生成侧的调优,我第一刀就切在 Prompt 上。项目初版 Prompt 写得非常简单,基本就是“你是客服助手,请根据资料回答问题”,模型自由发挥的余地太大,导致幻觉频发。后来我把 Prompt 改成严格的层次化结构,实测下来效果差异非常明显:
- 身份与行为指令:定义模型是“知识库问答助手”,严格基于提供的资料回答,不知道就说不知道。
- 引用规则:要求答案中的每个事实性陈述都标注来源编号,对应上下文片段。
- 知识边界规则:如果用户问题超出资料范围,明确回复“知识库中没有该信息”而不是猜测。
- 回答格式要求:分点、突出关键结论、复述问题后再作答,方便用户核对。
这里我特别想提醒一点:Prompt 里不要堆砌过多的否定句式。很多人怕模型胡说,就连续写“不要编造”“不要猜测”“不要答非所问”,反而会干扰模型的指令遵循。更好的写法是把正向要求和负向约束分开,让模型先明确“我要怎么做”,再补一句“不满足条件时如何回应”,这样的结构更清晰,模型遵循起来更稳定。
另外,我们做了一个有点反直觉的调整:在 Prompt 里明确告诉模型,如果检索到的资料不足以回答问题,可以拒答。初版为了防止回答空泛,我们强制模型必须给答案,结果模型就硬编。后来我们允许拒答,并设计了一个标准的“拒答话术”,反而提升了整体效果。原因很简单,用户对“无法回答”的容忍度远高于“一本正经地胡编”,而且拒答率下降说明检索侧的优化是有效的,这个信号对我们做迭代很有帮助。
4.2 上下文输入的组装与噪声抑制
生成侧的效果不仅取决于 Prompt 怎么写,还取决于你把哪些内容塞给了大模型。我审视过项目早期的日志,发现模型经常被一些“相关但无用”的切片带偏。比如用户问“接口调用频率限制”,检索侧召回了“API 使用概述”和“常见错误码”两个片段,模型就把错误码的一段话混进了答案里,造成不准确。
我在组装上下文时做了三件事,效果很直接。第一,相关切片按分数从高到低排列,并且用分隔符隔开,让模型清楚知道哪段信息优先级更高。第二,我会在每段资料前面加上该切片所属章节的路径,让模型能分辨不同来源的归属。第三,设定生成用的最大 token 上限,避免把候选集一口气全塞进去,打分低的片段宁可不要,也不能让它们拖垮答案质量。
关于“给模型吃多少资料合适”,我的一个实测判断是:如果重排序后的 top 1 切片已经能覆盖问题的主要答案,那给 1-3 条就够了;如果问题明显需要多文档对比,那就动态扩到 5-6 条。关键是宁可少而精,不要多而杂,一大堆低质量片段混在里面,效果甚至不如只给一条高质量的。这一点在长文档领域尤其明显。
4.3 输出后处理:引用校验与格式规范化
生成完成之后,还有一个我后来补上的环节:输出后处理。大模型生成的内容里,引用编号经常出现错位,比如它引用了 [2] 结尾,但那个内容其实来自于第 5 个切片。所以我在生成端接了一个简单的校验层,把答案中的引用编号和实际提供给模型的上下文顺序做匹配校验,顺序对不上就尝试修正,修正不了就丢弃引用号而不是保留错误的引用。
这个处理在技术实现上并不难,但价值非常大。一个带正确引用的答案,用户敢点进去核对,信任感完全不同;而一个引用错位的答案,哪怕内容是对的,也会让用户觉得系统不可靠。
后处理还可以顺带做格式规范化,比如把长段落拆成 bullet 列表、把“您”这类称谓统一等。这些细节不直接影响准确率,但直接影响用户对系统专业度的感受。我在项目复盘时和团队说过一句话:准确率决定用户是否留下,格式和引用决定用户是否信任。两者缺一不可。
5. RAG 评估与回归体系:把效果固化下来,而不是靠记忆迭代
5.1 评测集的设计:从真实日志中筛问题,而不是拍脑袋出题
我在前面提过,这个项目从一开始就建了坏 case 库,后面又扩展成了一套比较正式的评测集。评测集的设计原则是:尽量用真实用户在真实场景里的提问,而不是研发自己编的一堆感觉会有人问的问题。自己编的问题容易带偏见,比如大家都爱出“某个功能怎么用”这类好答的问题,但真实用户的问题往往更别扭、更口语化、隐含条件更多。
我的做法是把历史用户日志里的 query 拉出来,先聚类,再从每个类别里按比例抽代表性样本。覆盖维度包括:产品规则类、故障排查类、比较类(A和B有什么不同)、否定类(为什么不能用C)、时效类(D规则现在还有效吗)。每个类别至少 30 条,总共攒了 200 到 300 条左右。数量不一定要特别大,但覆盖面要够,否则回归结果很容易失真。
评测集的标注质量决定了它的参考价值。我的做法是让熟悉业务的人给这 200 多条问题写“黄金答案”,并对检索结果标注“哪个切片是真正的答案来源”。这项工作很费人力,但价值不是一次性的,后续每一次迭代都要依赖它来做效果验证。如果公司预算允许,建议优化 RAG 的团队一定要舍得在这个环节投入人力,这是整个项目里回报率最高的投资之一。
5.2 回归测试怎么跑:检索环节和生成环节分开看
评估要分层,把检索和生成分开来看,压缩定位问题的时间。我搭了一个简单的回归流程,两步走:
第一步,检索回归。拿评测集里的每个问题去跑检索,看答案所对应的黄金切片是否出现在召回列表或最终候选集里。如果黄金切片没被召回到,那不管生成模型多强都不可能答对。这一步用代码自动跑就行,指标就是“黄金切片命中率”。
第二步,生成回归。在检索结果已经能满足基本召回的前提下,用固定的 Prompt 和参数跑完整问答,然后比较生成答案和黄金答案的语义相似度,以及忠实度人工评分。如果检索命中没问题但生成分低,问题就锁定在 Prompt 或生成策略上。
这个分层思路帮我避过了一个大坑。某次优化之后,用户反馈说回答质量下降了,我第一反应是 Prompt 被改坏了,后来一跑回归发现生成评分没降,反而是某个文档被重新清洗后,黄金切片不再被检索到,检索层出了问题。如果只测端到端效果,我可能要花好几天才能定位到这个层面。
5.3 每次优化都留一条“证据链”
最后我想分享一个工作习惯:每次优化都留下完整的证据链。做了什么改动、改了什么文件、评测集上的各项指标变化、回滚方法、当时为什么做这个决定,全部记录整理成 memo,更新在这个持续更新的优化文档里。
比如我记录过这么几次印象深刻的优化:第一次是把混合检索加进来,召回命中率提升了约 8 个百分点;第二次是重排序接进来,相关性指标和端到端忠实度都明显改善;第三次是优化了元数据里的文档类型过滤,故障排查类的回答质量提升最明显。也记录过失败案例,比如某次尝试把块大小从 512 改到 1024,结果完整度上升但准确度下降,于是果断回滚,并在记录里写明原因和判断标准。
我自己的体会是:RAG 优化是一个持续迭代的过程,没有所谓的“一步到位”。每一次改动都是在多个指标之间做取舍,今天觉得好的方案,加了新数据之后可能就不灵了。但只要有评测集、有分层回归、有证据链,你就永远知道下一步该往哪里走,而不是跟着感觉打转。这套方法论比任何一个具体的参数都值钱,坚持执行下去,系统的回复效果会稳定而持续地变好。
最后再分享一个实用小技巧:当你觉得检索结果已经不错、但生成答案还是不行的时候,把问题的检索片段拿出来自己读一遍,看看如果你是客服,你会怎么用这些片段回答用户。这个简单动作,通常能快速暴露 Prompt 里缺了哪条关键指令。