一次 RAGAS 评估,揪出了 30% 的查询 Bug
项目上线前,我跑了一次 RAGAS 评估。20 条测试集,6 条返回"您尚未提出具体问题"。
我第一反应是 RAGAS 有问题。加了日志才发现——问题真的拼进 Prompt 了,但 LLM 就是"看不见"。
这篇文章记录完整的排查过程,以及为什么"感觉回答不错"是最危险的自我欺骗。
一、上线前跑了一次评估,30% 的查询是废的
RAG 项目做到一半,功能都通了。手测了几个问题——"LangChain 是什么"、"RAG 和 Rerank 的区别",答得都挺好。当时觉得差不多可以收了。
后来想,还是按 RAGAS 跑一遍评估吧,至少有个量化数据。
20 条测试集跑下来,四指标出来了:
| 指标 | 值 |
|---|---|
| faithfulness | 0.58 |
| answer_relevancy | 0.44 |
| context_precision | 0.84 |
| context_recall | 0.75 |
context_precision 0.84 还行,context_recall 0.75 也说得过去。
但 answer_relevancy 只有 0.44——这个数字太低了。
翻了一下逐条结果,发现一个问题:20 条里 6 条的回答,全是同一句话。
您尚未提出具体问题。请补充您需要咨询的具体问题……
这不对吧。6 个完全不同的问题,怎么会有同一个答案呢?这已经不是"回答质量差"的情况了,是系统根本没识别出我问的问题
二、加日志:问题明明拼进去了,为什么 LLM 看不见
第一反应:是不是 RAGAS 传参错了?
把评估用的 JSON 翻出来看了一眼,question 字段都在,也没有 null 值啊。那就肯定出现在系统内部的问题。
于是单独写了测试接口,直接源码复制,改了一下接口地址,加了几行日志:
public String ask(String question) {
log.info("=== 收到问题: [{}]", question);
String context = buildContext(topDocs);
log.info("=== Context 长度: {}", context.length());
String fullPrompt = "参考资料:\n" + context + "\n\n问题:" + question;
log.info("=== 完整Prompt尾部: ...{}",
fullPrompt.substring(Math.max(0, fullPrompt.length() - 200)));
// ...
}
跑一次出问题的查询:"LangGraph 的核心总结?"
日志输出:
=== 收到问题: [LangGraph 的核心总结?]
=== Context 长度: 5208
=== 完整Prompt尾部: ...+ 数据飞轮
工具治
理
50+ 工具最大瓶颈是上下文膨胀...
上下文
压缩
问题:LangGraph 的核心总结?
问题确实拼进去了,就在最后一行。
那为什么 LLM 还说"您尚未提出具体问题"?
三、根因:5000 字资料淹没了 1 行问题
盯着这段 Prompt 看了半天,终于意识到问题在哪:
参考资料:
<5208 字的资料...>
问题:LangGraph 的核心总结?
问题在于边界不清晰。
从 LLM 的视角看,整个 Prompt 非常像"用户给我一堆资料"。最后那行"问题:xxx"和前面的资料是连续的文本,没有任何分隔标记——LLM 会把它也当成资料的一部分。
再加上 5000 多字的资料占满了上下文,1 行问题的注意力权重被彻底稀释。
所以 LLM 真的以为用户只上传了资料没提问,回了句"您尚未提出具体问题"。
这不是模型的锅,是我 Prompt 设计的问题。
我在转换问题的时候,把答案和问题,放到了一个question里面,导致 LLM 没有识别出来我的真正问题
四、修复:给 Prompt 加"边界",让 LLM 分清资料和问题
修复思路很直接——把资料和问题用显式分隔符框起来,并在 system prompt 里说明。
改动 1:user message 加分隔符
String userMessage = """
=== 参考资料开始 ===
%s
=== 参考资料结束 ===
### 用户问题开始 ###
%s
### 用户问题结束 ###
请回答上面【用户问题】中的提问。
""".formatted(context, question);
三处关键改动:
===和###是分节标记——LLM 训练数据里很常见,它能识别这是"结构边界"- 结尾再强调一次"请回答上面【用户问题】" ——保险机制,防止 LLM 忽略
- 资料和问题之间留空行——视觉上强化边界
改动 2:system prompt 明确规则
private final String SYSTEM_PROMPT = """
你是专业的 Java 技术助手,负责基于知识库回答用户问题。
规则:
1. 参考资料通过 === 分隔符包裹,只使用其中的信息回答
2. 用户问题通过 ### 分隔符标注,位于参考资料之后
3. 只使用参考资料中的信息回答,不要编造
4. 如果参考资料中没有相关信息,明确告知用户"知识库中未找到相关信息"
5. 只输出纯文本格式
6. 在回答末尾标注信息来源
""";
system prompt 里明确告诉 LLM"参考资料在 === 内,问题在 ### 内" ——这句话让它主动去找边界。
五、效果:answer_relevancy 从 0.44 到 0.75
改完重跑 RAGAS:
| 指标 | 修复前 | 修复后 | 变化 |
|---|---|---|---|
| faithfulness | 0.58 | 0.82 | +0.24 |
| answer_relevancy | 0.44 | 0.75 | +70% |
| context_precision | 0.84 | 0.72 | -0.12 |
| context_recall | 0.75 | 0.79 | +0.04 |
answer_relevancy 从 0.44 涨到 0.75,提升 70%。
faithfulness 也涨了 0.24——说明之前 LLM 因为"看不见问题"而在瞎答,现在基于问题回答,幻觉也少了。
修复后跑一次"AutoGen 是什么" ,日志:
=== 收到问题: [AutoGen 是什么?]
=== Context 长度: 4872
=== 完整Prompt尾部: ...### 用户问题开始 ###
AutoGen 是什么?
### 用户问题结束 ###
请回答上面【用户问题】中的提问。
回答正常,归纳了 AutoGen 的核心哲学、三大特性、分层架构,末尾还标注了来源。
六、反思:不做量化评估,你永远不知道 30% 在裸奔
回头看这次 Bug,最值得说的不是"我怎么修的",而是为什么我之前没发现。
手测的时候,我测了 5 个问题,都答得挺好。
- "LangChain 是什么" ✅
- "RAG 和 Rerank 的区别" ✅
- "MCP 是什么" ✅
- "SSE 和 WebSocket 的区别" ✅
- "什么是纳什均衡" ✅
5 个问题全部通过——所以我以为项目没问题。
但 RAGAS 用 20 条问题系统性地跑,就暴露了 30% 的失败率。
这就是量化评估的价值。
手测的本质是抽样——你测的恰好是"正常路径",就以为全对。但真实流量里,问题类型千奇百怪,一定有触发 Bug 的样本。
RAGAS 的 20 条测试集,覆盖了事实型、综合型、术语解释、拒答型、口语化提问——只有系统性覆盖,才能暴露问题。
所以做 RAG,别靠"感觉回答不错"来判断质量。
跑一遍 RAGAS,看四指标,比读一百遍回答有用得多。
附:改动涉及的完整代码
// WebSseEmitterService.java
private final String SYSTEM_PROMPT = """
你是专业的 Java 技术助手,负责基于知识库回答用户问题。
规则:
1. 参考资料通过 === 分隔符包裹,只使用其中的信息回答
2. 用户问题通过 ### 分隔符标注,位于参考资料之后
3. 只使用参考资料中的信息回答,不要编造
4. 如果参考资料中没有相关信息,明确告知用户"知识库中未找到相关信息"
5. 只输出纯文本格式
6. 在回答末尾标注信息来源
""";
public EvalResult evalOne(String question, String groundTruth){
log.info("===收到问题:[{}]", question);
// 复用现有链路
RetrieveResult retrieveResult = retrieveAndRerank(question);
List<String> contexts = retrieveResult.getTopDocs().stream().map(Document::getText).toList();
// 拼接处改造
String userMessage = """
=== 参考资料开始 ===
%s
=== 参考资料结束 ===
### 用户问题开始 ###
%s
### 用户问题结束 ###
请回答上面【用户问题】中的提问。
""".formatted(buildContext(retrieveResult.getTopDocs()), question);
String answer = chatClient.prompt().system(SYSTEM_PROMPT).user(userMessage).call().content();
return new EvalResult(question, groundTruth, answer, contexts);
}
项目已开源: github.com/shuguang-li…
相关文章: