1. 为什么每次测到生成层就卡壳
做Agent和RAG系统测试也有段时间了,我发现在整个链路里,检索层(Retrieval)和生成层(Generation)的测试难度完全不在一个量级。检索层的问题通常很直观——召回少了、排错了、相似度分数不对,拿数据一对比就能定位。但生成层不一样,它像一个黑盒,你输入一堆东西进去,它吐出一段文本,中间发生了什么很难直接观测。
最典型的困惑就是标题里说的"两个输入搞不清"。很多人第一次接触RAG的生成层时,会理所当然地认为:生成层的输入不就是"用户的问题"吗?模型根据问题生成答案,这有什么好研究的?等到真正开始做测试、写用例、排查badcase时才发现,事情没那么简单。生成层接收的从来不是一个孤立的用户问题,而是一整套组装好的提示词(Prompt),里面既有检索回来的上下文片段,也有系统指令、历史对话、用户查询等多个组成部分。
这个认知差直接导致了一系列测试问题:不知道该怎么设计用例,不知道badcase该归咎于哪个环节,不知道调prompt和调检索参数到底谁先谁后。我在团队里带过不少做RAG测试的新同学,几乎每个人都会在这个地方卡上两三天,反复看代码、反复改配置,最后才慢慢摸到门道。这篇文章就把我梳理清楚的东西完整写出来,给正在被同样问题困扰的人一个参照。
我默认读这篇文章的人已经对RAG的基本概念有了解——知道它有索引、检索、生成三段式结构,也在实际项目里跑通过最简单的流程。如果你还没到这个程度,建议先拿一条公开数据集把最基础的RAG链路跑通,再回来看这篇,否则很多细节会显得抽象。
2. 生成层的"两个输入"到底指什么
2.1 第一个输入:检索来的上下文
先说说第一个输入——检索上下文。RAG系统的核心思想就是不指望模型肚里的知识够用,而是先从外部知识库里捞一批相关资料,再让模型基于这些资料回答问题。这批"捞上来的资料"经过截断、去重、拼接之后,会变成一段文本,塞进生成层的提示词里。
这里有个关键细节:检索上下文不是简单地原文照搬。预处理环节通常做了很多事——文档切块时的重叠策略决定了相邻块边界的完整性;截断策略决定了超出模型窗口的部分是丢弃还是压缩;排序去重决定了哪些片段能进入最终提示词、以什么顺序排列。这些因素都会影响生成层最终看到的上下文长什么样。
在测试时,我见过最多的情况是:badcase报出来,所有人第一反应是"是不是检索没召回到正确答案",于是跑去看召回结果、算召回率、比对相似度。但有时候检索结果明明不错——正确答案就排在Top3里——生成结果还是不对。这种时候问题往往就出在生成层的上下文组装上:可能截断太狠把关键句切掉了,可能多个片段拼接时失去了原有逻辑顺序,让模型"看不懂"上下文之间的衔接关系。
这个输入在调试时最容易被忽略,但不夸张地说,RAG系统80%以上的生成质量瓶颈都藏在这段文本的组装细节里。
2.2 第二个输入:用户的查询指令
第二个输入是用户查询,但这里要强调一下:它不只是"用户在对话框里输入的那句话"本身。在真实的Agent和RAG系统中,用户查询在到达生成层之前,通常已经经过了一层改写或扩展。
最常见的改写形式是Multi-Turn Query Rewriting——多轮对话中,用户问了一句"那它呢?",单看这句话根本不知道在问什么,系统会把历史对话浓缩成一句完整的话,比如"刚才说的那本书的作者,它的另一部作品的出版年份是多少",再丢给生成层。有的系统还会做查询扩展(Query Expansion),把用户一句话拆成多个角度的子查询,分别去检索、再合并结果,用来提高召回。
所以测试生成层时,不能只看最终吐出来的回答对不对,还得关注"喂给生成层的用户查询长什么样"。有时候问题不在模型生成能力,而是前面的查询改写把用户意图带偏了。一个典型的badcase:用户问"帮我总结一下这份报告的政策建议",查询改写模块可能把它转成了一句语义偏窄的疑问句,导致检索和生成都跟着偏。
2.3 把两个输入的关系画清楚
我把这两个输入的关系总结成一句话:检索上下文负责"提供素材",用户查询负责"限定任务"。生成层所有的工作,都是围绕"如何把素材组织成能满足任务要求的内容"展开的。
做了大量测试后才意识到,很多人搞不清这两个输入,根本原因是把生成层当成了一個"函数:输入问题,输出答案",而没有把它当成"一个上下文组装器 + 一个文本生成器"。前者是产品视角,后者才是工程和测试视角。测试需要的是后者,因为你只有清楚了内部结构,才能对一个badcase做出准确的归因。
两个输入在提示词里的位置也很讲究。常见的RAG提示词结构是这样组织的:
系统指令(规定角色和回答规范) [知识库片段1] [知识库片段2] ... 用户查询(经过改写后的完整问题) 生成指令(有时候会说明回答格式要求,如分点、要引用来源)有的实现会把生成指令和系统指令合并成一大段,有的会把检索片段放在偏前、有的放在偏后,不同框架的Prompt模板长得都不一样。但无论怎么排,基本的逻辑都是:上下文和查询是分开的两块,而不是混在一起。测试人员拿到一个badcase后,如果能快速从日志里把这两块分别提取出来,排查效率会大幅提升。
3. 生成层测试容易踩的坑:我自己踩过的三个
3.1 上下文被静默截断
这是我在实际项目里遇到最多、也最让人头大的一类问题。某次测试,系统回答一个关于产品参数的问题,检索阶段明明召回了完整的规格文档段落,但生成的回答里偏偏少了关键的单位信息。查了半天,最后发现是知识库的文本切片器在处理超长文档时,把原标题和正文切成了两块,检索只召回了正文块,标题里"功率单位"这几个字没进入生成层的上下文。
当时排查的过程很有意思:第一反应是调生成参数,把温度调低、加大上下文窗口,全部无效。最后是翻生成层的调用日志,把完整提示词打出来,才看到根本问题——上下文文本在组装前就已经不完整了,生成层连"看到"那个关键信息的机会都没有。这个教训让我之后做RAG测试时多了一个固定动作:凡是生成badcase,第一步必看完整提示词,而不是先怀疑模型能力。
这类问题很难通过调参解决,必须回到检索和切片环节去调整。但从生成层的测试角度看,你需要建立一条排查链路:完整提示词 → 上下文缺失 → 切片/检索/截断策略。这条链路会让排查变得非常快。
3.2 查询改写后的语义偏移
另一个高频坑跟前面说的查询改写有关。某次测试多轮对话场景,用户先问了一款产品的价格,再追问"和A款比呢?",本意是问两款产品价格差异。但系统把第二轮改写成了"产品A的价格和产品B的价格哪个更便宜",看起来好像差不多,但实际检索时直接命中了大量产品介绍类文档,生成结果变成了两款的详细介绍堆叠,完全没回答比较关系。这就是典型的改写语义偏移——改写后的查询在字面上与原来意思接近,但检索和生成的行为却因此变了。后续把改写逻辑固定为"保留原始的对比意图标记",这类badcase才开始收敛。
做生成层测试时,如果发现badcase像是在答非所问,不要急着怪模型,先去检查查询改写模块的输出。尤其要多轮对话场景,一次改写错误,后面每轮都会把错误放大。
3.3 提示词模板的结构型错误对生成行为的隐性影响
提示词模板里的措辞细节,能显著影响生成层的输出行为,这一点在测试中需要特别留意。举个例子:模板里写"请根据以下内容回答问题",模型就会倾向于只从给定内容里找答案,超出范围的宁可说不知道;但如果模板里写的是"你可以参考以下内容",模型就会自由发挥得多,甚至在上下文信息不足时开始用自己训练时的记忆补全。这两种措辞在测试数据上的表现差异非常大。
有一次我们在调整了一个模板词的表述后,回答风格的分布完全变了——严谨了,但"拒答率"也上去了。所以测试生成层时,模板本身也应当是测试对象。测试用例设计时,应该把模板的不同措辞风格作为一个对比维度,验证它们在不同领域、不同问题类型下的表现差异。而不是盲目相信"提示词模板只要写一次就一直好用"。
4. 生成层测试的方法论:从黑盒到白盒
4.1 建立"提示词日志"机制
做生成层测试的第一件事,不是设计测试用例,而是先把可见性建立起来。黑盒状态下,你只能看到输入和输出,中间的提示词组装过程是一个黑箱——但在工程实践中,我们应该在白盒层面上把提示词暴露出来,才能做更细致的分析。我在项目中推动的第一件事,就是让生成层在调用模型前,把完整组装好的提示词、检索片段及其来源的文档ID、截断标记、查询改写前后的文本,全部写进结构化日志。
有了这些日志,测试工作就有了抓手。任何badcase都可以从日志里快速还原模型的"视角"——它当时看到了什么上下文、收到了什么指令、上下文来自哪些文档——从而准确判断错误发生在检索、改写、组装还是模型本身这个环节。这个机制是后续所有测试策略的基础。
4.2 分模块设计用例矩阵
建立了可见性后,就可以系统性地设计测试用例了。我习惯把生成层测试拆成几个独立维度,每个维度做一组专项用例:
| 测试维度 | 核心问题 | 典型用例 |
|---|---|---|
| 上下文完整性 | 关键信息是否完整进入提示词 | 构造包含关键单位和前置条件的文档,观察回答是否遗漏 |
| 上下文顺序性 | 多个片段拼接后逻辑是否通顺 | 设计跨多个切片的完整论述,检查生成是否连贯 |
| 查询改写正确性 | 多轮改写是否保持原意图 | 构造省略型、指代型多轮对话,检查改写结果 |
| 模板稳定性 | 措辞变化对生成风格的影响 | 同一问题换不同模板措辞,对比回答风格 |
| 边界情况 | 上下文为空、查询为空、内容冲突 | 空检索、多文档矛盾等极端场景下的生成表现 |
每个维度不需要很多用例,但覆盖要全。这套矩阵的价值在于:当你面对一个badcase时,可以快速判断它属于哪个维度,进而确定修复方向。团队里的新同学拿到这套矩阵后,基本不会再对着一个badcase发呆。
4.3 用"对比生成"快速定位根因
定位badcase根因时,有一个非常高效的手段:做对比生成实验。含义是,保持其他环节不变,只修改一个变量,看生成结果怎么变。
举一个实际例子:某badcase是模型没有用上下文里的正确数字回答,而是自己编了一个接近但错误的数字。我用对比生成做了三组实验:
- 第一组:只保留正确的上下文片段,去掉其他干扰片段,模型答对了。这证明模型本身有使用上下文的能力。
- 第二组:保留全部上下文,但把用户查询从模糊问法改成更精确的问法,模型答对了。这说明问题出在查询改写不够精确,而不是上下文缺失。
- 第三组:全部保持原样,只降低温度参数,模型依然答错。这说明无关参数不是根因。
三个实验做完,根因清楚:查询改写模块把用户的精确问题转成了模糊表述,导致生成层对多个上下文片段的注意力分配出现了问题。修复方向明确落在改写逻辑上,后续改动也顺利通过了回归。这种对比生成实验,从测试角度来说,就是把生成层的归因从"玄学"变成了"可定位"。需要强调的是,这类方法更适用于测试分析场景,生产环境通常不需要频繁做——但作为开发或测试人员定位问题的方法,非常有效。
5. 一个完整的排查复盘:从badcase到根因确认
5.1 问题现象
某次版本测试中,系统收到一个用户问题:"这款手机支持5G网络吗?"系统从知识库中召回了手机说明文档、上市新闻稿和一篇评测文章三个来源的内容。实际生成的回答里出现了前后矛盾的话——先说支持5G,后面又说"仅支持4G网络"。这类问题在RAG测试中非常典型,属于多来源内容冲突导致的生成矛盾,也是在生产环境中容易被用户感知并投诉的高频问题。
5.2 排查链路回放
拿到这个badcase后,我没有直接去改提示词模板,而是按建立的排查方法一步步来:
第一步,拉生成层日志,看完整提示词。日志显示检索回了三个来源的文本:文档A来自官方规格页,明确写了"支持5G网络,频段n41/n78";文档B来自产品新闻稿,内容没有涉及网络制式;文档C来自一篇第三方评测文章,写的是"海外版本不支持5G,仅支持4G网络"。
第二步,对比生成参数。模板没有变化,查询改写也没有介入(单轮对话且问题已经很精确),所以问题锁定在上下文本身——多个来源的信息存在冲突。
第三步,分析上下文组装顺序。日志显示三个片段按检索分数排列,官方规格页排在最前,评测文章排在最后。模型读到前文"支持5G",再读到后文"仅支持4G",输出时选择了"都提一下",因为没有指令告诉它遇到冲突时该如何取舍。
第四步,用对比生成验证。把评测文章那个片段从上下文中移除,生成的回答变成一致的支持5G;把官方规格页放最后、评测放前面,回答又倒向"不支持"。这确认了是上下文冲突导致生成矛盾。
5.3 修复与验证
修复方案很简单:在提示词模板里加了一条生成指令——"如果检索到的多个信息源存在冲突,请优先采信来自官方来源的信息,并如实告知用户其他说法存在差异"。加上这条指令后,同样的上下文输入,生成的回答变成了:"该产品官方规格显示支持5G网络,但部分评测指出海外版本可能存在差异。"既处理了冲突,也没有隐瞒信息。
对比一下修复前后的效果差异:
| 项目 | 修复前 | 修复后 |
|---|---|---|
| 回答一致性 | 前后矛盾,既说支持又说不支持 | 明确采信官方信息,同时披露差异 |
| 对冲突的处理 | 模型自行决定,无规则可循 | 有明确优先策略,行为可预期 |
| 调试复杂度 | 每次冲突都需要人工分析上下文 | 日志中可快速定位冲突来源并验证策略 |
这个案例给了我一个重要启发:生成层的很多badcase,表面看是模型"智商不够",实际上是提示词模板缺少了对冲突场景的规则约束。测试人员应该在用例设计时主动构造这类冲突场景,而不是等到线上出了问题才补测试。
6. 理顺之后:我现在的测试工作流
经过一段时间的梳理和实践,我现在面对生成层的测试工作已经变成了一条相对固定的工作流。这里直接分享给有同样需求的人:
第一步,配置日志,确保能看到完整提示词和检索来源信息。没有这一步,后续工作等于盲人摸象。
第二步,用分维度用例矩阵做基线测试,跑一遍覆盖"上下文完整性、查询改写、模板稳定性、冲突处理"四类的用例,形成基线结果。
第三步,每次出badcase,先归因再动手——完整提示词 → 上下文检查 → 查询改写检查 → 模板约束检查 → 模型本身。不要一上来就调温度、换模型,那样只会让问题变得更难定位。
第四步,修复后必须跑回归用例,且要跑多个维度。原因很简单:改查询改写逻辑可能影响多轮对话表现,改模板约束可能影响整体回答风格。只跑当前badcase的回归,很容易让上一轮修好的问题复发。
这个工作流谈不上多高级,但胜在稳定可靠。团队里跟着这套方法做的人,上手速度普遍比之前快了不少,这就是我分享出来的最大动力。
最后分享一个自己在测试生成层时的小技巧:如果你和我在同样的Pipeline下做RAG测试,建议在日志里给每个生成的回答都打上一个"上下文是否含正确答案"的标记。做法很简单——用检索到的Top K片段做一次自动化的答案包含性检查(比如用字符串匹配或LLM做判定),看生成的回答有没有包含上下文中的关键信息。这个标记可以快速区分"模型没看到正确答案"和"模型看到了但没用对"两类问题。一旦把这两类区分开,你排查badcase的速度会快非常多。这个技巧我在多个项目里都试过,效果稳定,值得一试。