做 RAG 应用的朋友应该都有过这种体验:知识库明明塞了几百份文档,问个具体问题,答案却要么答非所问,要么一本正经地编出文档里根本没有的内容。我前几个月接手了一个内部知识库问答系统,线上反馈最多的就是“搜不到”“答不对”“瞎编”,当时第一反应是换更大的模型,后来发现模型换了好几个,问题原封不动。折腾一圈才意识到,RAG 链路里任何一个环节掉链子,最后都会以“答案不准”的形式暴露出来,而这些问题绝大多数是可以定位、可以量化、可以修掉的。
这篇文章我不会讲太多理论框架,重点是我在优化 RAG 应用提升问答准确度这件事上实打实踩过的坑、验证过的方案,以及最后沉淀下来的一套调优流程。内容面向正在做 RAG 落地的开发者,尤其适合那些知识库问答已经跑通、但准确度一直提不上去的团队。文章会按“定位病灶—检索优化—重排精排—生成约束—评测迭代”这条线展开,每一步都会给出可复现的配置和参数参考,希望能帮你省掉几周自己摸索的时间。
1. 先搞清楚 RAG 问答不准的病灶在哪
1.1 RAG 链路的问题不会只出一环
RAG 的标准链路其实就四段:问句进来之后先做召回,从向量库里捞一批相关片段;然后做重排,把相关度最高的内容排到前面;接着把选中的片段拼进 Prompt,喂给大模型;最后大模型基于这些素材生成答案。看起来不复杂,但每一段都有自己的坑,而且这四个环节的问题会互相掩盖。比如检索结果里确实有正确答案,但重排把它排到了第 8 位,生成时上下文超长被截断了,答案就变成了一堆废话——这时候你很难一眼判断到底是谁的锅。
我自己的经验是,遇到回答不准,第一件事不是改 Prompt,而是先把“检索质量”和“生成质量”拆开评估。做法很简单:单独把检索结果打出来,人眼判断前 10 条里有没有包含能直接回答问题的片段。如果前 5 条就有,说明检索基本合格,问题大概率在生成侧;如果前 10 条都没有,那你 Prompt 写得再好也白搭,因为模型手上根本没有正确答案,它只能靠脑补。这个二分法能帮你快速缩小排查范围,避免在错误的环节上反复试。
1.2 用一个小案例定位问题归属
我挑一个实际遇到过的例子来说明。用户问的是“公司年假制度里,入职满一年可以休几天”,知识库里其实有一份详细的《考勤与休假管理制度》PDF,里面明确写了“连续工作满 12 个月后,可享受每年 5 个工作日的带薪年假”。但线上系统的回答却是“根据公司制度,年假天数由部门主管根据绩效确定”,听起来像那么回事,实际上完全是错的。
排查过程分两步。先把用户问题拿去跑检索,看召回片段——结果发现召回的前 10 条全是从另一份《员工手册》里来的,里面确实提到了“年假与绩效挂钩”的模糊表述,而真正写清楚天数的制度文档因为分块太粗,被一个长达 3000 字的超大块裹住了,向量相似度被稀释,压根没被召回来。这属于典型的检索侧问题。事后我把那个文档按条款粒度重新切块,并做了关键词权重补偿,同样的问题再问一次,正确答案排到了第 2 条。这个案例说明两件事:一是分块策略对召回的影响比很多人想象的大得多,二是“答案不对”不一定是模型的错,先看检索结果再下结论。
2. 检索侧调优:准确度的第一道闸门
2.1 Embedding 模型选型要考虑语料场景
检索侧的第一件事是选对 Embedding 模型。我见过不少团队直接用 OpenAI 的 text-embedding-ada-002 之类的 API 模型跑中文知识库,结果检索效果稀烂,追问半天才发现是 embedding 对中文长文本的语义理解不足。选型时先看你的语料是什么语言、什么领域。如果以中文为主,我建议优先试开源的 bge-large-zh-v1.5、bge-m3 这类中文优化过的模型;如果你的语料偏专业垂直领域(比如法律、医疗、工业手册),还要额外做领域微调或者用领域数据生成的 query-document 对做相似度校准。
另一个被忽略的点是“查询侧”和“文档侧”的表示差异。用户问题通常很短,文档片段通常很长,两者直接做余弦相似度,天然不对称。很多 embedding 模型支持为 query 和 passage 分别设置指令前缀(在构造向量时加不同的 prompt 前缀,比如给 query 加“为检索生成此查询的表示”,给文档加“为检索生成此文档的表示”),bge 系列就用这种方式显著拉高了对短句子的检索效果。我测试过同一个知识库,不开这个前缀的召回命中率大概在 68%,开了之后直接到 82%,改动成本几乎为零,强烈建议你查一下自己用的模型是否支持。
2.2 分块策略决定召回粒度的上限
分块是整个 RAG 里最“手艺人”的环节。拿前面说的年假案例,把整份制度文档按固定字数 500 字切块,最后得到的块可能是“上半段讲休假申请流程、下半段讲年假折算”,语义被切得稀碎,向量表征一个都不像。固定窗口分块适合通用网页文本,但不适合条款式、结构化强的文档。
我现在做知识库时默认按结构分块:先把文档转成 Markdown,用标题层级(#、## 等)划出最小语义单元,再把属于同一个二级标题下的小节合并成块,块长控制在 300 到 800 字之间。这样切出来的每个块都有相对完整的论点,向量能表达清楚,被召回后也能直接当成上下文喂给模型。分块完必须做的验证是拿一批真实用户问题去打检索,看“黄金片段”(人眼确认包含答案的那段)能不能出现在前 5 条。如果经常出现在第 10 名开外,优先考虑是不是这个块的边界切错了,而不是急着换 embedding 模型。
另外,块与块之间的重叠也有讲究。我用的是 20% 到 30% 的滑动重叠,主要是防止某个关键句正好落在切分边界上导致两边都不像。但重叠越大,向量库里的重复内容越多,检索噪音也越大,这个需要实测平衡。我的建议是先不重叠跑一版,看召回 effect 再决定要不要加。
2.3 元数据过滤与混合检索补齐短板
纯向量检索有一个天然短板:它只看语义相似度,不看“这个片段来自什么文档、什么日期、什么部门”。比如你问“最新的报销标准是多少”,如果知识库里有 2022 版和 2024 版两份规定,向量检索很可能把两个版本的片段都召回,模型就傻眼了。解决办法是给每个块打上元数据标签(版本号、发布日期、文档类型、适用范围),在检索时用过滤器强制只召回最新版本。这一步的效果非常明显,我见过只加一个日期过滤,问答准确率就能提升十几个百分点。
另一个补短板的手段是混合检索:把向量召回和 BM25 关键词召回的结果做一个融合。向量擅长语义匹配,BM25 擅长精确关键词命中。很多垂直场景里用户问的是专有名词或编号,比如“AP-320 型压力表的检定周期”,BM25 能精准锁定包含 AP-320 的片段,而向量检索可能因为整体语义接近拉进来一堆不相关型号。我线上用的方案是向量召回前 50 条、BM25 召回前 30 条,用 RRF(Reciprocal Rank Fusion)做融合,公式是每个文档的分数等于各列表中排名倒数的累加,再兼顾两者权重。融合之后 TopK 的准确率比单用向量检索大概高 10% 到 15%。
2.4 查询改写别忽略用户真实意图
还有一个经常被遗忘的检索侧细节是查询改写。用户输入的原始问题往往很短,比如“年假到底怎么算?”“退款政策是什么?”这种短句直接做向量检索,表征出来的是一个泛化的“年假”“退款”概念,而不是具体的问题。更靠谱的做法是在进入检索前加一个轻量级改写步骤:让大模型把用户问题扩展成 2 到 3 个更具体的检索 query,每个 query 分别去检索,最后合并结果去重。
我用过的改写策略是模板式指令,效果稳定。Prompt 大致是:根据用户问题,生成最多 3 个用于知识库检索的短查询,要求包含专有名词和关键限制条件。比如“年假到底怎么算”会生成“入职满一年年假天数”“年假计算规则”“未休年假补偿标准”三个子查询,命中率明显比原始短句高。注意改写这一步不要让模型自由发挥太多,否则生成一堆发散问法,检索噪音反而变大,限定 3 个以内比较稳妥。
3. 召回之后的精排:别让 TopK 拖后腿
3.1 为什么粗召回后必须加一层精排
向量检索或者混合检索召回的 TopK 结果是按相似度排的,但这个相似度和“能不能直接回答用户问题”之间并不等价。我遇到过一个很典型的情况:用户问“工伤认定的申请时限是多久”,向量召回把科普定义、工伤预防措施、申请材料清单都排到了前面,而真正写着“单位应在事故伤害发生之日起 30 日内提出申请”的那一条排在了第 6 位。用 Top5 直接喂给模型,答案就漏了。
这就是重排(Rerank)存在的意义。召回阶段目标是“宁可多捞,不能漏网”,所以 TopK 可以拉高到 50 甚至 100;重排阶段目标是把“真正能回答问题”的片段排到最前面,同时把不相关的挤出去。我用的是一个独立的 Rerank 模型,它会拿用户问题和每个候选片段做深度交叉编码,输出的分数比向量相似度更贴近“问答相关度”。实际效果很直观:同一个问题,不加 Rerank 时答案命中率在七成左右,加了之后能稳定到八成五以上。
3.2 常见 rerank 模型与部署策略
目前常用的方案有两类:第一类是开源模型,比如 bge-reranker-base、bge-reranker-v2-m3,中文效果不错,单个片段推理耗时控制在几十毫秒级别,对生产可接受;第二类是一些商业 API 提供的 Rerank 服务,效果通常更好,但按量计费、数据要过一遍外部接口,隐私敏感场景要谨慎。我自己的偏好是先用开源模型跑通流程,确认链路没问题再考虑是否换更强的模型。Rerank 模型一般比 Embedding 模型大,显存占用也高,部署时建议单独起一个服务,别和向量检索服务混在一台机器上抢资源。
重排的时候要控制候选数量。候选太多会增加耗时,候选太少可能一开始就漏了答案。我的参数是:召回阶段取前 80 条,重排后取前 6 条作为输入模型的上下文片段。这个 80/6 的组合是在“召回覆盖率”和“上下文洁净度”之间反复调出来的。如果你发现检索质量本身很差,前 80 条全是无关内容,那加大候选数也没用,反而应该回到上游修分块和 embedding。
3.3 上下文长度的资源分配策略
重排之后,真正喂给模型的是前 N 条片段,但这里还有个隐藏问题:上下文窗口是有限的。模型窗口很大,不代表你把所有重排结果都塞进去效果就好——不相关的内容塞多了,模型会被带偏;相关的内容被挤到窗口边缘甚至截掉,答案照样不准。我建议给上下文设定一个硬上限,比如 2500 到 3000 个 token 的片段预算,重排完成后按分数从高到低往预算里塞,塞不下的宁可直接丢弃。具体操作时可以在 Prompt 里把每个片段前面加一个编号,让模型输出答案时尽量引用编号,方便排查到底哪条片段起了作用。
我还会做一个简单的位置优先级处理:重排分数最高的片段不一定放在 Prompt 最容易引起模型关注的位置。大模型对上下文开头和结尾的信息更敏感,中间部分容易被忽略。所以我把高分段片段放开头和结尾,中分段放中间,低分段直接不放。这个操作调完之后,模型漏用关键片段的情况减少了不少,而且是零成本改动。
4. 生成侧约束:让模型只做“事实的搬运工”
4.1 用 Prompt 把模型的默认行为改过来
检索和重排做得再好,生成侧不配合,准确度也上不去。大模型的默认行为是“有知识就先答,管你是不是知识库里的”。所以 Prompt 里最重要的不是格式化输出,而是强约束它的信息边界。我的做法是在 system prompt 里显示写明:只允许根据上下文内容回答问题,不得使用训练数据中的知识;如果上下文无法回答,请直接回答“知识库中未找到相关信息”;不能对上下文内容做任何推理引申或补充说明。
这里有一个反直觉的细节:如果只写“只能根据上下文回答”,模型偶尔还是会嘴硬。把“请基于以下文档片段回答,这些片段是唯一的信息来源。如果你不确定,就说不知道。”这句明确加上,效果会好很多。用“不知道”兜底尽管牺牲了一部分“回答率”,但有实际价值——它把模型的幻觉从“一本正经地编”变成了“诚实说不知道”,这在企业知识库场景里是正确的价值取向:宁可答不上来,也不能用错误信息误导用户。
4.2 给上下文加上“位置身份卡”和引用要求
为了让模型更明确哪些内容是给它用的素材,我会在拼接上下文时给每个片段加上一个身份标记,例如“片段来源:文档标题+章节名”,再要求回答时引用来源编号。这不仅是流程规范,更是一种隐性约束:一旦要求模型“说明信息来自哪一段”,模型编造的成本就变高了,它会倾向于从已有片段中找答案,而不是凭空发挥。
引用做法是:在每个片段前标注[1]、[2]这样的编号,Prompt 里明确写“回答时请以 [编号] 的形式引用你使用的片段”。我对比过开不开引用约束的差异:开了之后,模型输出的内容与上下文的忠实度更高,原因是它被迫逐条核对信息出处,而不是笼统地“概括一下大意”。当然这会多消耗一点 token,但换来的是可溯源的答案,值得。
4.3 兜底机制:设置置信度阈值和主动拒答
即使 Prompt 写得再严格,模型依然有可能“自信地答错”。我的做法是在生成后加一个轻量的分类判断:把问题和生成的答案、检索到的上下文一起再次交给模型或一个较小的分类模型,判断“答案是否完全基于上下文”。如果分类器认为答案和上下文不一致,就拦截掉这条回答,直接返回“知识库中没有足够的信息”。这个兜底机制类似一道质检,虽然增加了一次模型调用,但对准确度要求高的场景非常值得。
置信度阈值的设置取决于业务容忍度。内部知识库问答我会把阈值调得偏严,宁可拒答率更高;面向客户的服务则要平衡,拒答太多用户体验差,这时候可以退而求其次,让模型给出“可能的答案”并明确标注置信度低的提示,而不是直接拒绝。灵活调整拦截策略比一个固定规则跑到底更贴合实际。
5. 评测集与回归:把优化从玄学变成工程
5.1 没有评测集,一切调优都是手感
做 RAG 优化最容易犯的一个错误是:随手拿几个问题试一下,感觉“效果好像变好了”,然后上线。这种主观判断完全不可靠,因为你根本不知道是参数波动还是真实改进。我吃过这个亏:某次改完分块,用手边 5 个问题测,4 个变好了,自信满满上线,结果线上准确率反而掉了,后来一查是那 5 个问题恰好都是新分块方式擅长的类型。
解决这个问题只有一个笨办法:建评测集。一开始不需要很大,50 到 100 条就够了。每条包含三个部分:真实用户问题、标准答案(或者至少是“答案要点”)、对应文档和分块路径。评测集来源要从线上日志里扒真实问题,不要自己脑补问题,因为自己脑补的问题往往太理想化。扒完之后分类标注,比如“制度类”“价格类”“操作类”,这样每次优化后还能看不同类别的涨跌情况,定位更精细。
5.2 四个核心指标怎么盯
评测不能只看“回答对不对”这一个二值指标,那样太粗糙。我常用四个指标:
第一个是检索命中率,考察标准答案所在的片段有没有出现在召回前 10 条里。这个指标关注的是上游有没有“漏”。第二个是答案忠实度,判断生成答案的内容是否完全基于检索到的上下文,有没有引入外部知识或凭空发挥。这个可以用人工评,也可以用模型辅助打分。第三个是答案有用性,考察答案是否真的回答到了用户问题,很多答案忠实但答偏了,有用性就会差。第四个是端到端准确率,即最终答案是否与标准答案核心要点一致。
每个优化动作做完,我至少跑一遍这四个指标,记录变化。比如改分块策略,先看检索命中率有没有提升;如果命中率提升了但端到端准确率没变,那问题就出在生成侧,再回去调 Prompt。这套流程跑熟了之后,优化过程从“我觉得”变成了“数据显示”,决策速度快了很多。
5.3 回归测试与线上灰度
评测集建好之后,最怕的是“修好一个地方,弄坏另一个地方”。比如为了提升短问题召回,给所有 query 加了改写,结果长问题的改写反而搅乱了原有语义,这类回归问题不跑测试根本发现不了。所以我每次改完配置,都会跑一遍完整评测集,把四项指标和上一次的结果做对比,只要有任一项下降超过 5%,这个改动就要重新评估,不能直接上线。
线上环境再补一个灰度策略:先切 10% 流量跑两天,对比线上日志里的用户反馈数据和之前基线的差异。如果线上效果验证没问题,再逐步放开到 50%、100%。这套流程看着繁琐,但能挡住很多“评测集过了但线上崩了”的意外。评测集毕竟是离线快照,覆盖不了所有线上场景,灰度是最便宜的安全带。
6. 高频问题排查与实战避坑
6.1 常见问题速查表
我把这段时间在优化 RAG 过程中遇到的高频问题整理成了一张速查表,方便你对照排查:
| 症状 | 常见原因 | 优先排查动作 | 参考解法 |
|---|---|---|---|
| 检索结果里根本没有正确答案 | 分块太粗、embedding 和语料不匹配 | 打印召回前 10 条,人工核对黄金片段 | 按结构重切块、换中文 embedding、加混合检索 |
| 答案片片段正确但最终回答不对 | 生成侧约束不足、上下文被无关片段污染 | 检查 prompt 是否限制“只能基于上下文” | 强化 system prompt、加引用要求、收紧上下文预算 |
| 回答过于笼统,抓不住关键点 | 高分段片段没被模型注意,或上下文太长 | 查看模型实际使用的片段编号 | 调整片段位置优先级、压缩输入片段数量 |
| 频繁说“不知道” | 召回片段不足、prompt 太严格 | 检查召回数量和片段质量 | 放宽召回数、优化重排策略、调整拒答阈值 |
| 同一问题有时对有时错 | 检索排序不稳定、不同问法召回差异大 | 看日志里每次召回的片段集合 | 增加查询改写、做结果集稳定化、减少随机性 |
6.2 更换模型后的坑
很多团队在优化 RAG 时会顺手把大模型从 7B 换成 70B,或者从旧版换新版,然后发现效果“莫名变差”。这通常不是模型变差了,而是新模型的输出风格变了:它更爱发挥、更愿意补充背景知识,导致忠实度下降。遇到这种情况,不要急着否定新模型,先检查生成侧 prompt 是否需要跟着调整。新模型对 system prompt 的遵从度往往有差异,需要微调措辞。
另外,换 embedding 模型之后,老向量库里的向量和新 query 向量之间的匹配很容易出问题。如果无法一次性全量重建向量库,至少要保证查询侧和文档侧用同一个模型和同一个版本,否则新旧向量分布在不同的语义空间里,检索质量必然受损。这一点很多人会漏掉,我建议任何 embedding 模型升级都配一个完整的重建流程,不要做增量混用。
6.3 采集线上真实反馈持续迭代
RAG 应用的优化没有终点,线上线下用户会不断带来新的问法、新的文档、新的边界情况。我的习惯是在线上问答系统里加一个“答案是否有用”的反馈按钮,同时定期从日志里抽“用户改写了问题再次提问”的 case——那意味着第一次回答大概率没满足需求。把这些 case 补充进评测集,每周更新一次,评测集的规模会越来越大,系统对真实问题的覆盖也越做越稳。
这个持续迭代的环节其实比一次性调优更重要。你把基础链路调到位之后,接下来所有的提升都来自对真实问题的回应速度。评测集是你手里一个越用越准的标尺,别怕它增加工作量,它替你省下的排查时间远大于你标注这些问题花掉的时间。
6.4 一些配置参数的参考基线
最后分享一套我当前线上稳定跑着的指标,供你作为起点参考,但不要照抄,最好按你自身语料实测调整:分块采用基于标题结构的半自动切分,块长 400 到 600 字,重叠 15%;召回阶段用向量加 BM25 混合检索,从 80 条里用 rerank 挑出 6 条喂给模型;上下文片段预算控制在 2500 token 以内,最高分片段放在 Prompt 首尾;Prompt 中强制标注来源编号并要求“不确定就回答不知道”,加一道置信度质检兜底。这套基线在中文内部知识库场景下评测集的端到端准确率大约在 85% 左右,距离上线可用还有优化空间,但已经能扛住日常问答流量。
我个人在实际操作中最大的体会是,RAG 优化最怕“东一榔头西一棒子”,今天调分块、明天换模型、后天改提示词,每次改动都很合理,但因为没有做 A/B 对比和评测回归,最后根本不知道哪个动作起了作用。如果让我给你一个建议,那就是先花三天时间把评测集建起来,把四个指标跑通,再开始调任何参数。有了这个底座,后面每一步优化都是可累积的,而不是原地打转。