ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

RAG响应生成实战:证据筛选、排序与提示词结构设计

RAG响应生成实战:证据筛选、排序与提示词结构设计 1. 从「检索到」到「答得好」响应生成到底在解决什么问题做过检索增强生成RAG系统的人大概都有过这种体验检索模块召回了一堆看起来相关的文档片段向量相似度分数也挺漂亮但最终丢给大模型之后生成的答案要么答非所问要么把好几篇文档的内容搅在一起要么干脆忽略了检索到的关键信息自顾自地凭「记忆」回答。用户看到的结果就是——明明知识库里有的东西它就是说不对。这个问题的核心其实不在检索而在响应生成这一步。检索负责「找到证据」响应生成负责「拿着证据把答案组装出来」。这两件事的技术难度完全不在一个量级上。检索做得好顶多是召回率高一点低一点响应生成做得不好整个系统的可信度直接崩塌。我拿一个实际场景来说明。假设你做了一个企业内部知识助手员工问「年假没休完能不能折现」检索模块召回了三段内容一段是员工手册里关于年假结转的条款一段是财务制度里关于薪酬发放的说明还有一段是某次内部通知里提到的特殊时期年假处理办法。这三段内容都跟「年假」相关但只有第一段和第三段真正能回答「折现」这个问题。如果响应生成环节处理不当模型很可能把财务制度里关于「工资发放周期」的内容也揉进去生成一段看起来合理但实际错误的回答。所以响应生成要解决的核心问题是如何在有限的上下文窗口内把检索到的多源、异构、可能互相矛盾的证据组织成一段准确、完整、可追溯的答案。这里面涉及证据的筛选与排序、上下文的组织方式、提示词的结构设计、生成过程中的约束控制以及最终答案的格式规范。适合谁来参考这篇文章如果你正在搭建 RAG 系统或者已经有一套检索流程但生成质量不稳定或者你是一个应用开发者需要在自己的产品里集成大模型问答能力那接下来的内容应该能帮你少踩一些坑。我会从整体设计思路讲到具体的实操细节包括提示词模板、证据排序策略、冲突处理机制、以及我在实际项目中总结出来的一些排查技巧。2. 响应生成的整体设计思路与方案选型2.1 为什么不能直接把检索结果拼起来丢给模型最朴素的做法是把检索到的 top-k 文档片段直接拼接成一个长字符串前面加一句「请根据以下内容回答问题」然后丢给大模型。这种做法在 demo 阶段看起来能用但一到生产环境就会暴露出一堆问题。第一个问题是上下文窗口的浪费。检索回来的片段里往往有大量冗余信息比如导航栏文字、页脚、重复的段落。这些内容占据了宝贵的 token 空间却对回答没有任何帮助。尤其是在多轮对话场景下历史对话已经占了一部分窗口留给证据的空间就更紧张了。第二个问题是证据之间的优先级不明确。模型拿到一堆平铺的文本它不知道哪段更重要、哪段更权威、哪段是过时的。结果就是它可能把一条三年前的旧规定和一条上个月刚发布的新规定同等对待生成一个自相矛盾的答案。第三个问题是缺乏引用锚点。用户看到答案之后如果想知道这个结论是从哪来的系统没法给出明确的出处。这在企业知识库、法律咨询、医疗辅助等场景下是致命的——不可追溯的答案等于没有答案。所以响应生成的设计目标很明确在有限的上下文预算内把最相关、最权威、最新的证据以结构化的方式呈现给模型并引导模型基于这些证据生成可追溯的答案。2.2 三种主流方案的成本与效果对比在实际项目中响应生成的方案大致可以分成三个层次我做一个对比方案层次核心做法优点缺点适用场景朴素拼接检索结果直接拼接 简单指令实现简单零额外成本质量不稳定无法处理冲突Demo、原型验证结构化组装证据分块标注 排序 模板化提示词质量可控可追溯需要设计模板和排序逻辑大多数生产级 RAG 系统多阶段生成先筛选证据再生成草稿再校验修正质量最高能处理复杂冲突延迟高成本高高价值场景法律、医疗、金融我个人的经验是结构化组装方案是性价比最高的选择。它在质量和成本之间取得了很好的平衡而且实现难度适中大部分团队都能在一到两周内跑通。多阶段生成方案虽然效果好但每次问答要调用两到三次大模型延迟和成本都会翻倍只有在答案错误代价极高的场景下才值得上。2.3 证据组织的核心原则让模型「有据可依」而不是「自由发挥」结构化组装的核心思想是把每一条证据当作一个独立的、带标签的单元来处理而不是揉成一团。具体来说每条证据需要携带以下元信息来源标识这条证据来自哪个文档、哪个章节、哪个段落时间戳这条证据是什么时候发布的用于判断时效性权威等级这条证据来自官方文档、内部通知还是用户评论相关度分数检索阶段给出的相似度分数这些元信息在提示词中以结构化的方式呈现给模型模型就能根据这些线索来判断哪些证据应该优先采用、哪些证据之间存在冲突、哪些证据可能已经过时。提示不要指望模型自己从纯文本中推断出这些元信息。你必须在提示词里显式地告诉它否则它只会把所有文本当作同等重要的内容来处理。3. 核心细节解析证据筛选、排序与提示词结构设计3.1 证据筛选不是所有召回的内容都值得放进上下文检索模块通常会返回 top-k 个结果k 一般设成 5 到 20。但这里面真正有用的可能只有 2 到 3 条。如果全部塞进上下文不仅浪费 token还会引入噪声干扰模型的判断。我的做法是做一个两级筛选。第一级是分数阈值过滤设定一个相似度分数的下限低于这个阈值的直接丢掉。这个阈值需要根据你的嵌入模型和实际数据来调我一般会先跑一批测试问题观察正确证据的分数分布然后取一个能覆盖 90% 正确证据的值作为阈值。第二级是去重和合并。同一个文档的不同段落可能都被召回了这时候需要判断它们是互补的还是重复的。如果是重复的保留信息量最大的那一段如果是互补的可以合并成一个证据块但要在元信息里标注它们来自同一文档。还有一个容易被忽略的点是证据的多样性。如果 top-5 结果全部来自同一个文档的相邻段落那实际上你只得到了一条证据。这种情况下我会在筛选阶段引入一个多样性惩罚项对来自同一来源的证据进行降权确保最终进入上下文的证据来自不同的文档或章节。3.2 证据排序位置即权重大模型对上下文中的位置是有偏好的。大量实验表明模型对开头和结尾的内容关注度最高中间部分容易被忽略。这个现象在长上下文中尤其明显。所以证据的排序策略很关键。我的做法是最相关的证据放在最前面。这是模型注意力最集中的位置确保核心证据被优先看到。次相关的证据放在最后面。利用模型对结尾的关注度形成「首尾呼应」的效果。辅助性证据放在中间。这些证据起补充作用即使被忽略也不会影响核心答案的正确性。如果证据之间存在时间顺序或逻辑顺序比如一个流程的多个步骤那就按自然顺序排列不要强行按相关度打乱。模型对有序内容的处理能力远强于对乱序内容的处理能力。3.3 提示词结构把「指令」和「证据」彻底分开提示词的结构设计直接决定了生成质量。我见过很多项目把指令和证据混在一起写比如「根据以下内容回答问题xxx。内容yyy」。这种写法在简单场景下能用但一旦证据变多、问题变复杂模型就容易混淆。我推荐的结构是三段式[系统指令区] 你是一个基于证据回答问题的助手。你的回答必须严格基于下方提供的证据。 如果证据不足以回答问题请明确说明「根据现有信息无法回答」。 不要编造证据中不存在的信息。 [证据区] 证据 id1 source员工手册第3章 date2024-01 authority高 年假未休完的部分可以在次年第一季度申请折现... /证据 证据 id2 source财务制度第5章 date2023-06 authority中 薪酬发放周期为每月15日... /证据 [问题区] 年假没休完能不能折现这种结构的好处是模型能清晰地看到每条证据的边界和元信息不容易把不同证据的内容混淆。同时系统指令区明确告诉模型「证据不足时要承认」这能大幅降低幻觉的发生率。注意证据的 id 和 source 字段不是摆设。在生成答案时你可以要求模型在每句话后面标注引用的证据 id这样就能实现答案级别的可追溯性。3.4 冲突处理当两条证据互相矛盾时怎么办这是响应生成中最棘手的问题。检索回来的证据里经常会出现新旧规定不一致、不同部门说法不同、官方文档和用户评论矛盾的情况。我的处理策略是在提示词中显式地定义冲突处理规则而不是让模型自己猜。具体规则包括时间优先如果两条证据内容冲突优先采用发布时间更晚的那条权威优先如果时间相近优先采用权威等级更高的那条显式标注如果无法判断哪条更可靠在答案中同时呈现两种说法并说明存在冲突这些规则要写在系统指令区并且用具体的例子说明。比如当证据之间存在冲突时按以下优先级处理 1. 发布时间更晚的证据优先 2. 如果时间相近权威等级更高的证据优先 3. 如果无法判断在答案中说明「存在不同说法」并分别列出实测下来这种显式规则能解决 80% 以上的冲突场景。剩下的 20% 通常是证据本身质量太差需要在检索阶段就过滤掉。4. 实操过程从证据到答案的完整组装流程4.1 第一步证据的格式化与元信息注入检索模块返回的原始结果通常是一个列表每个元素包含文档 id、段落文本、相似度分数等字段。在进入响应生成之前需要把这些原始结果转换成结构化的证据块。我一般会写一个格式化函数输入是检索结果列表输出是符合提示词模板要求的证据字符串。这个函数需要做几件事为每条证据生成一个唯一的 id可以用文档 id 段落序号从文档元数据中提取来源、时间、权威等级等信息对证据文本做清洗去掉多余的空白字符和特殊符号如果证据文本过长做截断处理保留最相关的部分def format_evidence(retrieval_results, max_length500): evidence_blocks [] for idx, result in enumerate(retrieval_results): text result[text].strip() if len(text) max_length: text text[:max_length] ... block f证据 id{idx1} source{result[source]} block fdate{result[date]} authority{result[authority]}\n block text \n/证据 evidence_blocks.append(block) return \n\n.join(evidence_blocks)这段代码看起来简单但有几个细节需要注意。max_length的设置要根据你的上下文预算来定。如果总预算是一万 token检索证据占一半那五条证据每条平均一千 token对应大约六七百个中文字符。截断的时候尽量在句子边界处截断不要把一个完整的句子切成两半。4.2 第二步上下文预算的分配与动态调整上下文窗口是有限的资源需要合理分配。我的分配策略是系统指令区固定占用 200 到 300 token证据区占总预算的 50% 到 60%问题区固定占用 100 到 200 token历史对话多轮场景占总预算的 20% 到 30%预留输出空间至少留 500 token 给模型生成答案如果证据太多导致超预算就需要动态调整。我的做法是按相关度从高到低依次加入证据直到预算用完为止。同时设置一个最少证据数比如 2 条确保不会因为预算问题导致模型完全没有依据。在多轮对话场景下历史对话的预算需要动态管理。如果当前问题跟历史对话关联不大可以只保留最近一两轮如果关联紧密就需要保留更多历史。这个判断可以交给一个轻量级的分类模型来做也可以用简单的关键词匹配来实现。4.3 第三步生成参数的设置与调优大模型的生成参数对答案质量有很大影响。以下是我在实际项目中总结出来的参数设置经验参数推荐值说明temperature0.1 - 0.3响应生成需要准确性温度不宜过高top_p0.8 - 0.9配合低温度使用保持一定的表达多样性max_tokens500 - 1000根据答案预期长度设置不宜过短frequency_penalty0.3 - 0.5抑制重复内容尤其在证据较多时presence_penalty0.1 - 0.2轻微惩罚避免模型反复引用同一条证据temperature 是最关键的参数。我试过 0.7 的温度生成的答案确实更「流畅」但经常会出现证据里没有的信息。降到 0.2 之后答案的忠实度明显提升代价是表达稍微生硬一些。对于知识问答场景忠实度远比流畅度重要所以我一般会设在 0.1 到 0.2 之间。4.4 第四步答案的引用标注与后处理生成完答案之后还需要做一步后处理把答案中的引用标注提取出来跟原始证据做关联。这样前端就能在展示答案的同时把引用的来源也显示出来。如果提示词里要求模型在每句话后面标注[证据1]这样的引用那后处理就很简单用正则表达式提取即可。但有时候模型会忘记标注或者标注的格式不统一。这时候可以用一个轻量级的后处理模型来做引用对齐或者用简单的字符串匹配来补全。import re def extract_citations(answer): pattern r\[证据(\d)\] citations re.findall(pattern, answer) return list(set(citations)) def link_citations(answer, evidence_map): def replace(match): evidence_id match.group(1) if evidence_id in evidence_map: return f[证据{evidence_id}]({evidence_map[evidence_id]}) return match.group(0) return re.sub(r\[证据(\d)\], replace, answer)后处理还包括格式规范化。比如模型有时候会用 markdown 格式输出有时候用纯文本需要统一。还有答案的长度控制如果模型生成了过于冗长的内容可以做一个摘要压缩。5. 常见问题与排查技巧实录5.1 模型忽略证据凭「记忆」回答这是最常见的问题。明明证据里写得很清楚模型却给出了一个跟证据无关的答案。原因通常有三个第一系统指令不够强硬。如果指令只是说「请参考以下内容」模型可能觉得参考不参考都行。改成「你的回答必须严格基于以下证据不得使用证据之外的信息」效果会好很多。第二证据的位置太靠后。如果证据放在很长的历史对话之后模型的注意力已经被分散了。把证据放在系统指令之后、问题之前能显著提升模型对证据的关注度。第三证据的格式不够醒目。如果证据跟普通文本混在一起模型可能没意识到那是需要特别关注的内容。用 XML 标签或 markdown 分隔线把证据区明确标出来效果会好很多。5.2 答案正确但引用了错误的证据这种情况通常发生在多条证据内容相似的时候。模型可能引用了证据 2但实际上正确答案来自证据 1。解决方法是在证据块中增加区分度比如在元信息里加入更详细的来源描述或者在证据文本前加一句简短的摘要帮助模型区分不同证据。还有一个技巧是在提示词中要求模型先判断哪条证据最相关再基于该证据回答。这种「先思考再回答」的模式能提升引用准确性代价是生成时间稍微长一点。5.3 证据之间存在冲突时答案自相矛盾前面提到了冲突处理的规则但实际执行中还是会遇到问题。最常见的是模型虽然看到了冲突处理规则但在生成时忘记了。解决方法是在问题区再次提醒比如「注意如果证据之间存在冲突请按系统指令中的规则处理」。如果冲突特别复杂可以考虑在证据区直接标注冲突关系。比如证据 id1 source旧版手册 date2022-01 authority中 conflict2 ... /证据 证据 id2 source新版手册 date2024-01 authority高 conflict1 ... /证据这样模型一眼就能看出这两条证据是冲突的处理起来更有针对性。5.4 上下文超长导致生成质量下降当证据很多、历史对话很长时上下文可能接近模型的上限。这时候模型的生成质量会明显下降表现为遗漏关键信息、重复内容增多、逻辑混乱等。我的应对策略是分层压缩。首先压缩历史对话只保留跟当前问题最相关的几轮其次压缩证据对每条证据做摘要只保留核心信息最后如果还是超长就减少证据数量优先保留相关度最高的几条。提示不要等到超长了才处理。在组装上下文之前就应该根据预算做好规划预留足够的余量。5.5 常见问题速查表问题现象可能原因排查方向解决方法模型忽略证据指令不够强硬 / 证据位置靠后检查提示词结构和证据位置强化指令调整证据位置引用错误证据证据区分度不够检查证据元信息是否充分增加来源描述要求先判断再回答答案自相矛盾冲突处理规则未生效检查冲突规则是否明确显式标注冲突关系问题区再次提醒生成质量下降上下文超长检查 token 使用量分层压缩减少证据数量答案过于简略max_tokens 设置过小检查生成参数增大 max_tokens调整提示词答案重复啰嗦frequency_penalty 过低检查生成参数提高 frequency_penalty6. 进阶技巧让响应生成更稳、更快、更可控6.1 用少样本示例锚定输出格式如果你对答案的格式有严格要求比如必须包含「结论」「依据」「补充说明」三个部分那在提示词里加一两个少样本示例是最有效的方法。示例不需要很长但必须覆盖你期望的输出结构。示例输出格式 结论年假未休完可以折现。 依据根据员工手册第3章未休年假可在次年第一季度申请折现。[证据1] 补充说明具体折现比例需参照财务制度相关规定。[证据2]这种格式化的输出不仅便于前端展示也便于后续的自动化处理。实测下来加了少样本示例之后输出格式的合规率能从 60% 提升到 95% 以上。6.2 流式输出与首字延迟的平衡用户对响应速度的感知主要取决于首字延迟而不是完整答案的生成时间。所以流式输出是提升体验的关键。但流式输出跟引用标注有点冲突——如果答案还没生成完引用信息就不完整。我的做法是先流式输出答案正文等生成完毕后再异步补充引用信息。前端可以先展示答案然后在答案下方逐步显示引用来源。这样既保证了首字延迟又保证了引用的完整性。6.3 缓存策略相同问题不必重复生成如果系统中有大量重复或相似的问题可以考虑加一层缓存。缓存的 key 可以用问题的语义哈希而不是原始文本。这样即使用户换了种问法只要语义相同就能命中缓存。缓存的过期时间需要根据知识库的更新频率来定。如果知识库每天更新缓存可以设成几小时如果知识库很少变动缓存可以设成几天。缓存命中时直接返回之前生成的答案和引用信息延迟能从秒级降到毫秒级。6.4 答案质量的自评估机制在生产环境中你需要一个机制来监控生成质量。我的做法是在生成答案的同时让模型给自己打个分。具体来说在提示词的最后加一句「请评估你的答案与证据的一致性用 1 到 5 分表示1 分表示完全不一致5 分表示完全一致」。这个自评分虽然不完全准确但能帮你快速定位低质量答案。如果某个问题的自评分持续偏低说明检索或提示词可能有问题需要进一步排查。7. 我在实际项目中的几点体会响应生成这个环节说起来就是「把证据组装成答案」但真正做起来细节非常多。我踩过最大的坑是过度依赖模型的自主判断能力。早期我总觉得只要把证据给到模型它自然能分辨哪些有用哪些没用。实际测试下来模型在证据筛选和冲突处理上的表现远不如预期。后来我把更多逻辑放到了提示词和预处理环节用显式的规则来引导模型效果才稳定下来。另一个体会是不要追求一次到位。响应生成的优化是一个迭代过程。先跑通基本流程然后根据 bad case 逐步调整提示词、排序策略、生成参数。我一般会维护一个测试集包含几十个典型问题每次调整后都跑一遍看整体准确率和引用准确率的变化。还有一个容易被忽略的点是证据的清洗。检索回来的文本往往包含大量噪声比如 HTML 标签、导航文字、广告内容。这些噪声如果不清理会严重干扰模型的判断。我现在的做法是在检索之后、组装之前加一个清洗步骤用规则加轻量级模型的方式过滤掉明显无关的内容。这一步看起来不起眼但对最终质量的影响非常大。最后分享一个小技巧在提示词中给模型一个「不知道」的选项。很多模型在面对不完整的证据时会倾向于强行编造一个答案而不是承认自己不知道。如果你在指令中明确说「如果证据不足以回答问题请回答‘根据现有信息无法回答’」模型承认不知道的概率会大幅提升。这对于知识问答场景的可信度至关重要——一个说「不知道」的系统远比一个胡说八道的系统有价值。
RELATED READING

延伸阅读

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