首页 / 资讯中心 / 文章详情

RAG答案质量差?问题可能不在检索,而在证据驱动的响应生成

RAG答案质量差?问题可能不在检索,而在证据驱动的响应生成 ★ FEATURED ARTICLE
我接手某个知识库问答项目时遇到过一个非常典型的场景检索层的召回率已经堆到很高的水平相关片段基本都进了上下文可产品上线后用户投诉反而变多了。有人说答案“感觉不对”有人截图说两个句子自相矛盾还有人追问“这句话到底从哪来的”。当时我的第一反应是换模型、调温度折腾了一圈没有起色。后来把坏案例摊开看才发现问题不在检索也不完全在模型能力而在于 LLM 拿到证据之后怎么把零散片段组装成一份像样的答案。这个环节通常叫响应生成说直白一点就是证据驱动的答案组装。今天我想把这个阶段完整拆一遍覆盖证据预处理、组装策略、提示词设计、评测调优四个层面适合正在做 RAG、做知识库问答、做智能客服以及被“检索很好但答案很烂”折磨过的朋友。1. 先搞清楚拿到证据和答好题之间差着一条鸿沟1.1 很多 RAG 项目真正翻车的地方不在检索而在组装业内有个挺普遍的误解只要能把对的资料找回来放进上下文模型自然会写出正确答案。这个想法就像“把一堆资料放到实习生桌上他就能写出一份漂亮的调研报告”一样过于乐观。检索做得好的意思是资料在桌上而组装是这个实习生看懂资料、挑选信息、组织逻辑、标注出处、给出结论的全过程。中间任何一步掉了链子答案质量都会崩。我自己在项目里分开统计过检索成功率和最终答案好评率发现检索成功率高不代表答案好评率高。很多时候证据明明齐全答案依旧三流要么把多段原文生硬拼在一起要么抓错了重点回答旁枝末节要么对矛盾证据视而不见。这说明答案质量的瓶颈后移到了“拿到证据之后”这一步而这一步往往是最少被认真打磨的。1.2 证据进入上下文后模型内部到底发生了什么想理解组装为什么难你得先知道证据进入上下文之后模型本质上在做一件什么事。基于 Transformer 的解码器架构在处理长序列时注意力会被大量片段稀释位置靠中间的内容很容易被忽略这就是常说的“lost in the middle”现象。很多做 RAG 的人都验证过把关键证据放在中间位置模型经常漏掉放在开头或结尾回答质量明显上升。另一个容易被忽略的点是模型默认行为并不是“综合所有证据”。它更像一个取巧的写手常常抓住上下文里某一段字面最顺的片段展开或者把多段内容按顺序复述一遍而不是真正做归纳、对比、取舍。如果你不给它明确的组装规则它的默认输出就是“局部最强片段扩写”或“多段拼接复述”这两种都不叫高质量答案。1.3 高质量答案的三个硬指标在谈组装技巧之前先把“高质量”定义清楚。我通常用三个硬指标衡量一份答案忠实性Groundedness答案里的每一句关键信息都要能在提供给模型的证据里找到支撑。这是底线不允许模型自己脑补。完整性Coverage用户问题的所有子要点都要覆盖到证据里和问题相关的关键信息不能漏掉一半。可用性Usability答案要有条理、有结论用户读完能直接拿来用而不是看一段资料搬运。工程上我还额外看一个指标可追溯性。也就是答案里的引用编号是否真实有效、能否对应到具体证据。这个指标直接影响用户信任度也是后面评测章节的重点。这三个指标是一个三角形经常要互相妥协。证据不足时完整性要让位于忠实性宁可说不知道也不编造。我在实际项目中把忠实性视为一票否决项一旦出现幻觉其余两项再好也算失败。2. 证据进窗口前的预处理组装质量从入口就被决定了很多人以为组装是提示词的事其实在证据真正进入模型窗口之前有几个预处理环节就已经悄悄决定了最终答案的上限。这些预处理做不好再好的提示词也救不回来。2.1 排序不是“相关”就够要排成“适合引用”的顺序常规检索系统按向量相似度召回排序结果直接反映的是“语义相似”不完全等于“生成友好度”。我用中文问答场景举个例子A 证据是百科式背景介绍B 证据是直接回答问题的规则条款。按相似度A 可能排在最前面模型读到的第一个片段却是背景于是回答绕了半天才进入正题用户体验很差。正确的做法是在向量召回之后用交叉编码器或其他重排器以“该片段是否直接支撑问题答案”为目标二次打分。这个操作投入很小收益却很直接。我自己习惯在重排打分里再加一个小权重给时效性知识库文档更新时间越新得分加成越高。比如某个接口的调用方式在新版本文档里已经改了旧文档却排在前面模型很容易输出过时答案。2.2 去重与去噪重复证据会把模型逼疯知识库里的内容常常存在大量近似重复同一份公告被多个页面转载、多篇文档描述同一套流程。这些近似重复的证据放进上下文后会产生两个问题。一是注意力被稀释真正有差异的信息被淹没二是模型倾向“复读”把重复出现的表达当重点答案读起来啰嗦。去重我用的是 embedding 余弦相似度。以常见的开源向量模型为例余弦相似度超过 0.8 的两个片段基本可以判定为近似重复我会只保留其中一个。不过这个阈值千万别直接照搬不同 embedding 模型对中文语义的敏感度差很多。我在项目里做标准流程是随机抽 100 组召回结果中的段落对人工标注“重复/不重复”画一条 PR 曲线定阈值。虽然辛苦但一劳永逸。2.3 冲突证据的标记把难题交给答案层之前先分出优先级知识库文档之间内容打架是常态新版本覆盖旧版本、产品手册和论坛讨论互相矛盾、不同部门口径不一致。如果预处理阶段不给证据做优先级标记模型在组装时就会陷入一个尴尬处境它不知道应该信谁最后往往选了上下文中最后出现的一条或者干脆强行编出一个折中说法。我的做法是在证据实际拼接进上下文之前给每一条证据附加结构化元数据直接写在片段前面。比如这样的形式[证据3][来源:某产品手册 v2.1, 更新时间: 2024-05] 新接口的完整参数列表是... [证据4][来源:某论坛帖子, 更新时间: 2023-10] 旧接口参数说明...模型看到这两条再配合提示词里“冲突时以更新时间新、来源可信度高者优先”的指令就能做出合理判断并且在答案里主动说明版本差异。这件事与其让模型在密密麻麻的文本里自己搜不如预处理阶段就替它标好路标。2.4 上下文填装策略开头和结尾最要命同样一批证据摆放顺序不同生成质量能差出一大截。这是由 Transformer 对位置敏感的特性决定的。我在实测中总结出的填装顺序是把最核心、最直接回答问题的证据放在最开头把次要佐证放中间把“如果上述资料都不足以回答请直接说明”这类兜底提示放在末尾。中间位置不是不能放内容而是不适合放最关键的内容。如果证据总量超过窗口上限截断策略也有讲究。我不建议简单从尾部砍文本而是先按重要性对证据排序从尾部开始丢弃相关性最低的证据。这样即便窗口装不下全部留在里面的也尽量是精华。还有一个进阶技巧在放入窗口之前先用一个轻量模型把每条证据压缩成三句话以内的摘要保留关键数字和结论再充值进上下文。这相当于给模型一张更整洁的办公桌。2.5 一个可落地的证据预处理管线把上面这些串成一个可复用的处理函数大概是这个逻辑def build_model_context(question, retrieved_docs, reranker, embedder, max_tokens6000): # 1. 重排以“是否直接支撑答案”为目标二次打分 reranked reranker.rerank(question, retrieved_docs) # 2. 去重按 embedding 余弦相似度过滤近似片段 deduped_docs [] for doc in reranked: vec embedder.encode(doc.text) if all(cosine(vec, old_vec) 0.80 for old_vec in deduped_vecs): deduped_docs.append(doc) deduped_vecs.append(vec) # 3. 排序与元数据注入 final_docs sorted(deduped_docs, keylambda d: (d.rerank_score, d.source_weight)) pack_items [f[证据{i}][来源:{d.source}] {d.text} for i, d in enumerate(final_docs)] # 4. 按重要性从头填充超过上限则丢弃尾部 packed_text for item in pack_items: if len(tokens(packed_text)) len(tokens(item)) max_tokens: break packed_text item \n return packed_text, final_docs这段代码只是示意但你在真实项目里需要把它当成正式模块来维护而不是每次在预测脚本里顺手写几行拼接逻辑。证据预处理和生成策略是两套代码应该分开管理、单独优化。3. 选择组装模式不同问题要用不同的生成策略证据准备好了接下来就是生成策略。很多人写提示词时一视同仁无论什么类型的问题都用同一套“根据资料回答”。这是不对的。根据问题的结构我应该选择不同的组装模式或者说不同的“写作脚本”。3.1 直答模式证据只用来验算不背在背上针对单跳事实性问题比如“某产品的退款政策是几天内无理由”最合适的组装模式是直答模型读证据然后直接给出答案。这种模式下证据的作用是“验算”不是为了展示。直答模式的提示词特征是强调简短、直接、不要复述资料只回答问题本身不要复述资料不要补充背景。如果资料中有可以直接回答 问题的句子用你的话提炼出来并在句末用[编号]标注来源。这种模式的实际难点不是“答不出来”而是“答得臃肿”。模型经常把证据里相关的几段内容全倒出来用户需要自己找结论。直答模式的高质量就体现在用户看完答案心里有数不需要往下再翻。3.2 摘要合成模式把多份证据拧成一份通顺答案当问题需要综合多个文档的分散信息时比如“这个产品和支持系统怎么联动”单一证据说不清楚需要用摘要合成模式。这里我有一个强烈建议不要直接让模型“看完所有材料后总结”一步到位的做法很容易漏信息。我更推荐两步法。第一步让模型先对每一条证据单独提取“与问题相关的一句话要点”第二步再把这些要点汇总、去重、组织成答案。这其实就是 Map-Reduce 的思路。两步分开走每一步的任务都更单纯模型不容易被中间某段强势证据带偏。我在实践中两步法对召回完整度平均能提升一截具体的数字不同场景差异很大但方向是一致的。3.3 条件回答模式证据不足时“说不知道”也是高质量这是最被低估的一种组装模式。真实用户问题里总有相当一部分是知识库里没有答案的。面对这种情况硬编一个答案是最差的因为它制造幻觉但直接说“不知道”也很差因为用户会认为系统没用。高质量的条件回答应该是说明现有资料不足并指出要回答这个问题还缺哪些信息。提示词里我通常会这样写如果资料不足以回答用户问题不要编造。请回答 “现有资料不足以给出完整答案”然后列出你觉得缺失的具体信息 比如“缺少最新的退换货时效说明”。这种“带信息量的不知道”反而能建立信任因为它诚实且指出了下一步动作。3.4 链式推理模式多跳问题要靠证据串联多跳问题的特点是单条证据只能支撑推理的某一步中间结论要从证据 A 得出再带着结论去证据 B 里找答案。应对这类问题组装策略要显式地设计成链条。我实操中的做法是把证据按推理步骤分组提示词要求模型先处理第一组产出中间结论再带着中间结论处理第二组。比如第一步仅依据[证据1][证据2]回答“该模块的调用入口是什么” 得到中间结论 A。 第二步结合中间结论 A从[证据3][证据4]中查找“该入口对应的 鉴权参数”得到最终结论。这种显式链条能显著减少一种典型失败模型跳过中间推理直接拿最后一段证据硬答导致逻辑链断裂。给模型搭好阶梯它才愿意一步步走。3.5 四种模式的适用对照组装模式适用问题特征核心检查点提示词关键特征直答模式单跳、事实明确简洁、直接、引用到点要求提炼作答不重复资料摘要合成模式多文档、信息分散无遗漏、去重、结构化分步提取要点再合并条件回答模式证据不足、不确定不编造、明确指出缺口允许“证据不足” 列出缺失项链式推理模式多跳、需要逻辑链逐步推导、中间结论准确按步骤分组消费证据我在实际项目里不是固定选一种而是先对问题类型做一次轻量分类再动态切换组装模式。这个分类本身可以用模型做也可以靠指令规则兜底。成本不高但对最终质量的提升非常明显。4. 把组装逻辑写进指令给模型的“排版规范”预处理和组装模式定好之后真正把逻辑落到模型头上靠的是提示词。这一节聊提示词设计里最容易被忽略的几个细节。4.1 先立规矩证据与答案的边界关系很多提示词只告诉模型“根据资料回答”却没说清楚资料和答案的边界关系。结果模型把资料里的背景介绍也当成回答的一部分答案冗长且没有重点。我一般会在系统指令里明确规则资料仅用于回答用户当前问题不得把资料中的科普背景、无关案例一并输出。答案中的每个关键断言都必须能在对应编号的证据中找到出处。如果你的知识储备与资料冲突以资料为准。这些规则看似简单但逐条写清楚比笼统一句“基于资料回答”有效得多。4.2 引用标注与引用校验让每句话都有出处高质量答案的特色之一是“可追溯”。我喜欢在提示词里强制要求引用标注取这样的格式在句末用方括号数字标注来源例如“退款时效为7天[1]”。 禁止出现证据列表中不存在的编号。这个要求对模型来说不难但实际输出时偶尔还是会出问题。所以我在系统侧增加了引用后处理脚本用白名单过滤掉不存在的编号import re def verify_citations(answer, valid_ids): cited_ids set(map(int, re.findall(r\[(\d)\], answer))) invalid cited_ids - set(valid_ids) return len(invalid) 0, invalid一旦发现无效引用我会触发一次修正生成把“你引用了不存在的编号请重新作答”作为反馈传给模型。这种方法比单纯依赖模型自律可靠得多。4.3 冲突处理指令怎么写才能不产生幻觉关于证据冲突提示词里的标准话术是如果资料之间存在矛盾以时间更新、来源更权威的资料为准 并在答案中单独用“注意”段落向用户说明关于XX现有资料 存在不一致当前采纳的是较新版本的说明。这个指令有两个作用一是给模型一个明确取舍规则不会随机选一条二是让答案显式暴露不确定性。实测下来用户对这种“明说不一致”的答案信任度反而更高因为系统没有装懂。4.4 一份可复制的完整组装提示词模板把上述要点放在一起我常用的一个完整模板长这样你是一名资深分析师。请基于以下资料回答用户问题。 规则 1. 只使用资料中的信息作答任何资料之外的断言都视为错误。 2. 结论先行然后给出依据。依据在句末用[编号]标注编号对应下方证据列表。 3. 严禁引用证据列表中不存在的编号。 4. 若资料之间存在矛盾以更新时间更近、来源更权威者为准 并在“注意”部分向用户说明不一致之处。 5. 若资料不足以回答请明确回答“现有资料不足以回答” 并列出缺失的具体信息。不要编造。 证据列表 [证据0][来源:产品手册v2.1, 更新于2024-05] ... [证据1][来源:官方FAQ, 更新于2024-03] ... 用户问题 ...这个模板看起来长但每一条都有对应的事故教训。它基本覆盖了忠实性、可用性、可追溯性三个维度是我目前在项目里反复打磨后的稳定版本。4.5 实测中的意外模型记不住编号怎么办分享一个我踩过的坑当证据数量超过八条时模型经常“不敢”引用靠后的编号所有标注都集中在[1][2]哪怕答案内容明显来自第 6 条证据。后来我总结出两个缓解办法。一是控制证据条数优先通过预处理把证据压缩到五条以内让模型能稳定记住编号。二是在提示词里加一句“证据编号很重要引用时必须准确匹配”并且把证据列表的编号放在每一条最前面用明显标记隔开。配合引用校验后处理这个问题基本能压住。另一个相关问题是“过度引用”模型为了安全每句话后面都标[1][2][3]看似严谨实际约等于没标。这种我靠提示词里强调“只在与该句内容直接相关的证据后标注不要堆叠无关引用”来约束。5. 落地评测与调优如何判断组装质量并迭代看了前面的方法论你可能会觉得“每一步都有道理但怎么知道我改对了没有”这一节讲实操层面的评测和迭代方法。这部分最容易被忽视但恰恰是决定组装质量能否持续提升的关键。5.1 自动指标忠实度、覆盖率、引用准确率怎么测组装质量的自动评测我一般看三个指标忠实度用一个 NLI 模型判断答案的每个句子是否被证据蕴含。落地上我会把答案按句切开逐句判断“这句话能否由证据推出”。低于阈值的句子就是嫌疑幻觉。覆盖率把证据中与问题相关的关键信息点抽出来检查答案是否覆盖。实际操作中我会用 LLM 先从证据里提取信息点列表再逐项检查答案是否有对应内容。引用准确率引用编号对应的证据是否真的支撑了该句内容。这个指标和忠实度有重叠但更细线上它是一个独立的质检信号。这些指标都有成熟工具可以做但它们只能粗分善恶。ROUGE 这类基于字面重叠的指标在生成式答案上基本不可用不要拿来做忠实度基线。5.2 Bad Case 抽检五种典型失败模式自动化指标之外我坚持定期人工抽检坏案例并把它们归类。五年项目下来组装层的失败模式基本集中在五种失败类型典型表现常驻原因初步解法幻觉型答案出现证据完全没有的信息提示词边界不足加强忠实性规则、降低温度堆砌型多段原文拼贴没有结论缺少组装模式选择切到摘要合成两步法偏航型答非所问排序问题优化重排、检查证据相关度冲突无视型矛盾证据各取所需缺少冲突指令注入元数据、冲突处理规则引用错配型标注编号与内容不匹配后处理缺失加引用校验与修正为了方便分类我建议线上每次请求都把“证据列表 模型完整输出 引用校验结果”落进日志。否则出了问题只能靠用户截图事后分析无从谈起。这一步非常无聊但没有它所有评测都是盲人摸象。5.3 从失败反推检索问题和组装问题的分界线调优时第一时间要判断问题出在哪个层。我的思路是把线上某次请求的证据列表单独拿出来人工看一眼——如果人拿着这些证据能写出好答案而模型没写出来问题在组装层应该调提示词、组装模式或预处理如果人看着证据也答不出来那就是检索层漏召回别在生成层硬扛。这个判断说起来简单但很多团队不做结果在生成层反复调耗了很久才发现证据本身是垃圾。建议在项目一开始就建立这个判断习惯可以省下无数无效调参时间。我自己在项目里发现真正需要回检索层处理的案例往往比想象中少得多。很多“答案差”的案例换一种组装模式或补充冲突规则之后效果立刻不一样了。整个 RAG 链路里组装阶段可能就是性价比最高的优化对象值得多花时间。5.4 长期迭代建议让证据组装成为一项可治理的工程最后说几个长期迭代的建议。第一准备一个固定评测集比如一百条真实用户问题覆盖单跳、多跳、冲突、证据不足四类场景。每次调整预处理逻辑或提示词后都跑一遍这个集看指标有没有回退。第二提示词要纳入版本管理每次改动记录一句话理由不要靠心算“上次改成什么样”。第三把引用准确率当作一个独立指标每天盯它崩了往往说明预处理排序或证据质量动了。第四一次只改一个变量。我见过最惨痛的教训就是同时换了模型、改了提示词、又动了去重阈值结果效果好得奇怪但完全无法定位是哪个改动起了作用。我自己现在做这类项目会把组装阶段当成一套独立系统来治理有输入输出契约、有日志、有自动质检、有版本回滚。这听起来工程化得很重但在线上问题真正爆发时这套体系能帮你半小时定位问题而不是靠猜。
阅读完成 · 觉得有帮助?
咨询建站