1. 先盘问题:RAG为什么总卡在“能用”和“好用”之间
这两年凡是做大模型落地的人,几乎都绕不开RAG(检索增强生成)。说白了就是先把你自己的知识库切块、向量化,用户提问时先从库里检索相关片段,再把片段拼进Prompt里让大模型作答。这个思路本身不复杂,跑通一个demo也很快:几个文档、一个向量库、一个模型接口,半天能出效果。可一旦真正接进业务系统,问题就全冒出来了——用户问“报销标准”,检索结果却返回一堆差旅制度;模型一本正经地把旧政策编进回答里;同一个问题换个问法答案就完全对不上。
我见过太多团队卡在这条坎上。demo做得挺漂亮,一上线就被业务方吐槽“人工智障”。老板催着优化,但大家除了调调切块大小、改改Prompt,好像也找不到更体系化的办法。这篇文章把我这一年多在多个RAG项目里沉淀下来的思路整理一遍,核心就一句话:想要从“能用”走到“好用”,必须建立一套系统化的评估、迭代、治理方法,而不是靠零散的“灵光一现”。
先说清楚“能用”和“好用”的分界线。在我看来,“能用”意味着回答大部分时候靠谱,但出了错你没法解释为什么错,也找不到规律;“好用”意味着有明确的质量指标、有bad case归因路径、每次优化都能看见数据变化,并且系统的错误边界是清晰的——它答不上来的时候至少会老实说“不知道”。绝大多数团队都卡在前者,因为大家习惯凭感觉调参,却忘了RAG是一条完整链路,任何一个环节出问题,最终都表现为“答案不对”。
这条链路大致分成检索和生成两段。检索段负责“找得对”,生成段负责“答得准”。找得不对,后面的模型再怎么强也白搭;答得不准,前面的检索做得再好,用户也只会觉得系统不行。两段都需要精细化治理,而且顺序很重要:先把检索搞好,再做生成优化,最后才是交互体验。下面按这个顺序来讲。
2. 检索侧的三个隐形杀手:切块粗糙、召回不全、排序失准
2.1 切块策略不是“一刀切”的字符数游戏
很多教程会告诉你:用固定长度切块,300到500字,重叠50到100字,完事。这套默认参数在纯文本、主题单一的资料上确实能跑,但真实知识库永远是混合体——有规章制度、技术文档、表格、公告、FAQ,甚至还有扫描件转文字后的乱序文本。固定长度切块最典型的问题,是把一个完整的制度条款拦腰截断,导致向量化之后语义不完整;或者把一个表格拆得七零八落,检索时根本找不回整表内容。
我现在的经验是:切块必须先看文档结构,再定策略。规章制度按条目切,每条保留条款编号和适用范围;技术文档按章节层级切,章节标题和正文绑定;表格单独处理,转成Markdown表格后再入库,并且在块内容前补一句表格摘要,帮助向量模型理解这张表讲的是什么;对话记录按“轮次+主题”聚合,避免把一问一答拆成两半。如果文档本身有清晰的Markdown标题结构,优先按结构切;只有纯文本长文才退回固定长度策略。
另一个容易被忽视的点是“父子块”设计。子块是真正送去检索的最小单元,父块则是更大范围的上下文。检索命中子块后,把父块同时喂给大模型,防止“只见树木不见森林”。比如一份技术方案,某段落在讲部署步骤,单看子块不知道它属于哪个项目,把父块带上后模型才能理解语境。这个设计对长文和嵌套结构文档特别有用,强烈建议一上来就铺好。
2.2 只靠向量检索,本质上是在赌语义
向量检索擅长处理“语义相近”的问题,比如“差旅费能报多少”和“出差报销额度是多少”能召回同一批文档。但它在精确匹配上很弱:设备型号、单据编号、人名、政策条款号这类必须逐字命中才能召回的内容,向量模型经常给出“看似相关、实际不对”的结果。用户问“Q3-2024-011号工单的处理进度”,向量检索很可能召回一堆关于工单制度的文章,唯独找不到真正那条工单记录。
解决办法是混合检索:向量召回+关键词召回(BM25或ES语法查询)并行,取并集再送入重排序阶段。很多人听到“混合检索”就觉得要上多复杂的系统,其实落地成本很低:向量库里查一遍,倒排索引里查一遍,两边结果合并去重就够了。难点在于权重调节——不同业务场景里关键词和向量的贡献比例差异很大。我的做法是先各取top50,合并后靠Rerank统一排序,而不是手动调两个召回来源的加权分,这样能少踩很多平衡性的坑。
元数据过滤也是检索侧性价比极高的手段。每篇文档入库时打上产品线、部门、发布日期、文档类型等标签,检索时先根据用户问题和会话上下文做硬过滤。比如用户问“最新的离职流程”,就限死在“制度流程+近期发布日期”范围内;用户问“客服退款政策”,就直接过滤掉非客服线文档。这个动作能去掉大量无关噪音,比单纯优化向量模型更直接。
2.3 Rerank不是可选项,是“能用到好用”的分水岭
向量检索返回的top_k结果里,通常只有前几个是精准的,后面混着大量“语义沾边但内容没用”的片段。如果不做重排序直接全塞给大模型,模型容易被不相关上下文带偏,甚至把两篇不同主题的内容杂交成一份四不像答案。Rerank的作用,就是用一个更强的交叉编码器模型,把用户问题与每一条候选文档做深度交互打分,重新排出真正有用的顺序。
我之前在某知识库项目上做过对比:纯向量检索时,回答“引用内容与问题相关”的比例不到六成;加入Rerank后,同样的top_k数量,相关率明显提升,而且幻觉率同步下降。原因是模型看到的前几条文档质量变高了,编造的空间自然被压缩。
做Rerank有几个实操注意点。第一,先扩大召回的范围,再压缩:向量和关键词各取50条,Rerank只保留最相关的5到10条,这样既保住了召回率,又控制了喂给模型的上限。第二,Rerank模型有延迟成本,通常几十到几百毫秒不等,对查询量大的线上服务要做缓存和异步优化,不能每个请求都全量重排。第三,定期用真实bad case微调Rerank的候选集大小,有些场景top10够用,有些场景要留给top20才有机会把正确答案捞回来。
2.4 查询理解:用户不会按你的知识库说话
用户提问天然带噪音:口语化表达、缺主语、指代不清、一个问题里藏着好几个子问题。如果原样拿用户输入去做检索,效果一定不稳定。查询改写是检索侧最值得投入的一环,但它又最容易被人忽略,因为它不在主链路上,出了错也不容易一眼看出来。
比较实用的做法有三类。第一是同义扩展:根据知识库里的高频词,把用户问题中的口语词映射成库内标准说法,比如“社保”映射到“五险一金”,“年终奖”映射到“年度绩效奖金”。第二是指代消解:多轮对话里用户说“那它的流程是什么”,“它”指代的是上一轮提到的对象,要把指代换成具体名词再检索。第三是子问题拆分:用户问“新员工入职要准备什么材料,以及转正考核有哪些指标”,这个复合问题应该拆成两个独立检索,再分别拿结果拼接。
查询改写本身可以用大模型来做,但要注意控制成本和延迟。我习惯用一个轻量级模型或规则模板先处理常见改写,只有命中复杂模式时才调用大模型。另外,改写要保留原始问题副本,方便出了问题回溯“是改写错了还是检索错了”。
3. 评估体系:没有数据,你根本不知道改的是好是坏
3.1 别只盯着“准确率”一个数字
RAG的评估必须拆维度,单一准确率毫无意义。我在项目里常用五个维度:检索命中率、上下文利用率、答案忠实度、答案相关度、格式合规率。检索命中率看正确答案有没有进入候选;上下文利用率看喂给模型的片段里有多少被真正用上;答案忠实度衡量回答是否严格基于给定资料、有没有编造;答案相关度看回答是否针对用户问题;格式合规率用于需要结构化输出的场景。
忠实度是最值得盯的指标。它不等于“正确”,一个忠实度高的回答可能不够全面,但它至少不会编造。判断方法也直观:把回答里的每个关键陈述,逐一对照检索到的上下文,能找到依据就算忠实,找不到就是幻觉。这一步自动化比较难,我目前是结合自动评估和人工抽检来做,后面细说。
3.2 测试集是地基,宁可做得糙也不能没有
很多团队一上来就拿着系统去线上试,出了错再回头查日志,改进毫无章法。正确做法是先构建一个固定的评估测试集,覆盖核心业务场景。规模不用大,100到200条足够起步,但结构要合理:四成是高频真实问题,四成是从文档里反推出来的“已知答案”问题,两成是模糊、多义、无标准答案的问题。
每个测试用例至少标注三样东西:用户会怎么问(多写几个变体)、期望回答里的关键要点、对应答案的依据来源(哪篇文档哪一节)。这条标注工作非常费人力,但它是整个改进闭环的基准线。没有测试集,你每次“优化”都只是在掷骰子。
3.3 回归测试:每次改动都跑一遍同一批题
系统改进最怕的就是“按下葫芦浮起瓢”。换了切块策略,某个问题修好了,另一类问题全崩了。固定测试集的价值就在这里——任何改动上线前,先跑一遍测试集,对比各项指标和bad case数量。我习惯把上一版结果存成基线,新的改动跑出来逐项对比,只有核心指标不降、目标指标提升才允许上线。
有一点要特别注意:生成类模型有随机性,同一套代码同一批测试集跑两次,结果未必完全一致。所以对比时不要拿单次结果下结论,至少跑两三次取平均值,确保看到的是真实变化。测试集里的检索部分不受随机性影响,可以单次跑;生成部分必须重复多次。
3.4 自动化评估能省力,但别全信
现在有开源的RAG评估框架,例如RAGAS,能自动算忠实度、答案相关度等指标,用来做初筛和日常回归很方便。它在“发现明显问题”这件事上很靠谱,但在“判断回答是否真好”上还不够,因为自动评估也是用一个模型来当裁判,模型的判断同样会受Prompt、上下文影响。
我的用法是双层过滤:先用自动化指标跑全量测试集,筛出分数异常低的bad case;再由人工逐条审这些bad case,做归因。人工审的时候重点看三件事:是检索没找到、还是找到了但排序靠后、还是模型没正确理解上下文。归因错了,后面的优化动作一定跑偏。这条经验是踩过坑才记牢的——有段时间我以为是切块问题,反复调了快两周,后来才发现是查询改写在中间环节把关键实体吞掉了。
4. 生成侧治理:让大模型“照着材料说话”
4.1 Prompt约束不是写作文,是立边界
检索做得再准,如果Prompt里没有明确约束,大模型依然会自由发挥。我见过太多团队在Prompt里只写“根据以下资料回答问题,如果不知道请说不知道”,这远远不够。一个严谨的生成Prompt至少要包含三层约束:身份与任务边界、资料使用规则、输出格式要求。
资料使用规则是重点。要把“只能基于给定资料作答”写成强制性指令,并且明确说明“资料中找不到明确依据时,必须回答‘根据现有资料无法确认’,禁止推断和补充”。同时要让模型在回答里标注内容来源编号,比如[1]、[2]对应检索片段的序号。这样做有两个好处:用户能点开来源核对,降低不信任感;开发者在bad case分析时能快速定位是哪个片段误导了模型。
4.2 结构化输出:把不可控的回答变成可控的字段
交互界面上如果只是让模型吐一段自然语言,出了问题很难做后置拦截。我强烈建议要求模型输出结构化结果,最简单的是JSON格式,包含answer字段、references字段(用到的片段编号列表)、hit字段(是否找到依据)、uncertainty字段(模型对回答把握程度高还是低)。
这个设计带来的直接收益是:你可以用规则在后置环节做二次校验。比如references为空但answer写了长篇大论,说明模型在无凭无据地编造;hit为false但answer语气非常肯定,同样是高风险信号,直接拦截返回“暂无法回答”比放一句错话出去好得多。成本就是多了解析一次JSON,但换来的可控性非常值。
4.3 引用溯源:好用感很大程度上来自“能核对”
用户对RAG系统的容忍度,通常取决于它有没有提供依据。同样一句“根据规定,孕妇产检假为98天”,没有来源的时候用户会怀疑,有来源能点进去看到具体条款,信任感完全不一样。而且引用溯源对开发者也很有价值:它能形成“答案—依据—原文”的完整链路,排查问题时一眼就能看出是检索错了还是模型理解偏了,不用再对着整段回答瞎猜。
做引用溯源时要注意,模型不是真的“理解”来源再引用——它只是根据编号规则把对应的编号填进回答。所以引用是否合理,必须单独检查。我见过不少回答把一段不相关的片段编号硬塞进去,原因是检索结果本身就有噪音,模型无法区分哪些有效哪些无效。这种情况压不下来,就要回到检索侧优化,先把喂进来的材料搞干净。
4.4 模糊问题先澄清,别硬着头皮答
用户的问题经常是模糊的:“报销流程是什么”可能问差旅报销、采购报销、员工福利报销;“离职要办什么手续”可能问主动离职、被辞退、试用期离职。模型如果硬答,大概率答偏。更好的做法是设计澄清策略:当用户问题有歧义,或者检索结果的置信度整体偏低时,让模型先列出可能的理解方向,问用户确认后再回答。
澄清策略最怕滥用。如果每个问题都反问一句,用户体验会变得极其糟糕。我是这样控制的:只有当检索得分很低、且问题本身存在多个实体类型时,才触发澄清;如果用户已经在问题里给出了足够明确的条件,就直接作答。澄清时一次性给出几种可能的选项,让用户点选或回复序号,比开放式的“请补充说明”更高效。
5. 持续迭代:把优化从“一次性攻坚”变成“日常习惯”
5.1 bad case样本库是改进的燃料
优化RAG最值钱的东西不是模型参数,不是Prompt模板,而是线上真实用户的反馈和失败案例。系统上线后一定要做两件事:全量日志记录——query、检索结果、Rerank排序、最终答案、用户是否点踩,全部落库;用户反馈入口——至少在答案底部放“有帮助/没帮助”两个按钮。
每天从日志里捞“无帮助”的case,给它们打标签:检索无关、答案错误、答案模糊、引用缺失、格式不对、答非所问。第一周你可能要花一两个小时做这件事,但标签打满半个月之后,你的改进优先级会非常清晰——某个标签占比明显偏高,说明问题集中在特定环节和特定文档类型上,直接下手解决就行。这个样本库做起来之前,所有优化都像盲人摸象。
5.2 每周一次“坏样本评审会”,按共因排优先级
单看零散的bad case容易头痛医头、脚痛医脚。我习惯每周拉一次评审:把本周新收集的bad case按标签聚合,找共性原因。比如发现上周“售后政策过期”类的错误占比窜到三成,细查下来是某个旧文档一直没下架,而且内容没有标注生效日期,导致模型反复引用。这是个数据问题,更新文档后该类case直接消失大半。
排优先级时记住二八原则:集中修掉影响面最大的两三个共因,胜过零散修十个个案。很多团队失败在“什么都想改”,改切块、换模型、调Prompt一起来,最后连哪项改动带来提升都说不清。一次只动一个变量,对比测试集结果,记录变更日志,这个习惯能避免大量返工。
5.3 改动上线前先做影子对比
RAG链路里的参数调整——top_k、切块大小、Prompt语气、Rerank候选数——每一项都可能引发线上效果波动。直接改线上配置是高风险操作,我建议用影子模式:新配置和旧配置并行跑一段时间,同一批真实请求同时送入两套链路,但只有旧配置的结果返回给用户,新配置只记录答案和评估分数,用来对比。
这个做法避免了“拿真实用户当小白鼠”的尴尬,也帮团队养成了用数据评估改动的习惯。等到新配置在离线测试集和影子对比上双双稳定优于旧配置,再小流量切线上。这套流程看上去繁琐,但对连续优化几个月甚至更久的项目来说,是防止效果倒退最可靠的屏障。
5.4 机制大于热情:让改进不依赖某个人的感觉
最后说说团队层面的体会。RAG优化很容易变成“某个工程师自己埋头调Prompt、调参数”的个人行为,短期内可能有效,但人一离职或转去做别的事,整个系统就停滞了。要让它可持续,就得把改进机制固化下来:测试集有人维护,bad case仓库有人打标,指标看板定时更新,变更记录写清楚每版改了什么、为什么改、效果如何。
这样即使接手的人换了,也能快速理解系统状态。从我经历过的项目来看,凡是建立了这套“数据—迭代—验证”闭环的团队,两到三个月后效果提升都非常可观;凡是靠个人热情驱动、临时抱佛脚的团队,大概率半年了还在原地打转。
6. 一次真实改进过程的复盘:从12%幻觉率到5%以内
6.1 初始状态:能答,但不能细问
某公司的内部制度问答系统,知识库一万两千多篇文档,覆盖人事、财务、行政、IT等多个条线。上线时大家觉得“已经能用了”:常见问题能答出来,回答速度也快。但业务方做了一轮真实用户抽样后,反馈很扎心:约52%的回答称得上“有用”,15%的回答存在明显编造,尤其被追问“依据是什么”时经常前言不搭后语。团队当时的改进方式是让开发同学手动调Prompt,但调了两周,效果没有任何可感知的变化。
6.2 第一轮改造:把检索的先做好
第一件事是停下来建测试集。团队从半年的用户日志里筛出高频问题180条,另从重点文档中构造了50条“已知答案”问答题,一共230条基线。第一轮改动只聚焦检索侧:文档切块从固定500字改为按结构切,表格单独转Markdown并加摘要;引入混合检索,向量和BM25并行召回,合并后用Rerank统一排序,最终只保留top8送入模型。
这一轮改完,测试集上“检索命中率”明显上升,模型生成的答案明显比之前“有依据”。但人工审bad case时发现问题并没有完全消失,编造率虽然下降,但遇到不相干的片段时模型还是会强行缝合。这让我们确信:检索侧优化是必要条件,但只做检索还不够。
6.3 第二轮改造:把生成侧管起来
第二轮专门治理生成侧。Prompt重写,加入了“只能依据给定资料、无依据必须声明”的强硬指令;输出改成JSON结构化字段,增加references列表;后置增加规则校验,发现references为空却输出长回答时,直接拦截为“未找到相关依据”。
同时上线了引用溯源功能,每条回答末尾展示依据文档标题和原文链接。这一轮的效果是幻觉率从9%降到5%左右,用户点踩率有了可感知的下降。但新的问题浮出水面:有相当一部分模糊问题,模型答得“忠实但没用”——它确实没编造,可用户也没得到想要的答案,因为问题本身就带着歧义。
6.4 第三轮改造:补上澄清与改写
第三轮集中处理交互理解。在链路里加入查询改写模块,对口语化问题和指代不明做改写;引入意图判断,对多义问题触发澄清话术,给用户提供几个可选项。比如“报销流程”不再直接按默认制度答,而是问“您要查差旅报销、采购报销、还是日常费用报销”,用户点击后再调对应文档检索。
这一轮的效果体现在“用户满意度”上,抽样问卷里有用的回答比例从57%升到73%。系统不再是“三棍子打不出一个准屁”的死板问答机,更像一个会先确认需求再办事的助手。当然长尾低频问题、知识库本身缺失的内容仍然处理不好,但那属于“资料质量治理”的范畴,已经不是RAG链路本身能解决的了。
6.5 复盘总结:有效的是闭环,而不是某一次大招
回头看这三轮改造,没有任何一步是“惊天神技”,全是基础动作的组合:切块改得更符合文档结构了,检索从单一向量换成混合召回加Rerank,Prompt从“自由发挥”变成了“严格守规矩”,交互上加了澄清。真正让效果持续提升的,是每一轮都跑同一批测试集、每次改动都有数据对比、每个bad case都能归因到具体环节。这个过程不浪漫,但极其有效。
我个人最深的体会是:RAG优化没有银弹,任何人告诉你“换个模型、换个向量库就能解决所有问题”,基本都是在忽悠。好用是靠持续、系统、甚至有点笨拙的迭代堆出来的。如果你现在正卡在“能用但不好用”的尴尬期,我的建议很简单——先别急着调参,花两周时间把你的测试集和bad case仓库建起来,那才是所有优化的起点。