一、调参不能靠"看起来还行"
前几篇我们做了文档解析、切块、混合检索、重排序。但一个灵魂问题:你怎么知道改完之后变好了?
"我把 similarityThreshold 从 0.7 调到 0.75,感觉回答准了点。"——"感觉"不是证据。
没有量化评估,你今天改的"优化"可能明天就被另一个"感觉"推翻,RAG 系统在反复横跳中越来越烂。
RAGAS 就是把"感觉"变成分数的工具。 它用 LLM 当裁判,自动给每一轮问答打 4 个指标,你改任何参数都能看到曲线变化。
二、RAGAS 四大指标讲人话
| Context Precision | 召回的文档排得对不对 | 检索 | 相关文档排前面,噪声靠后 |
| Context Recall | 该召回的有没有漏 | 检索 | 答案需要的知识都在 context 里 |
| Faithfulness | 答案有没有编造 | 生成 | 每句话都能在 context 里找到依据 |
| Answer Relevancy | 答案对不对题 | 生成 | 直接回答用户问题,不跑题 |
关键点:Faithfulness 和 Context Recall 才是业务最在意的——一个回答再流畅,如果编造了合同条款(低 Faithfulness),就是事故。
三、评估数据集:你得先有"标准答案"
RAGAS 不是凭空打分,它需要一份评估集:每一条包含「问题 + 标准答案(ground truth)+ 上下文」。
package com.example.rag.eval;
import java.util.List;
/**
* 评估用例:最小可用结构
* 生产环境至少准备 50~100 条覆盖核心场景的用例
*/
public record EvalCase(
String question, // 用户问题
String referenceAnswer, // 人工标注的标准答案(用于算 Answer Relevancy / Recall)
List<String> groundTruthCtx // 该问题"应该"召回的文档片段(用于算 Context Recall)
) {}
package com.example.rag.eval;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.io.ClassPathResource;
import java.io.InputStream;
import java.util.List;
/**
* 从 YAML/JSON 加载评估集(按 Day 69 的 Prompt 版本管理思路,评估集也要纳入版本控制)
*/
public class EvalDatasetLoader {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static List<EvalCase> load(String path) throws Exception {
try (InputStream is = new ClassPathResource(path).getInputStream()) {
return MAPPER.readValue(is, new TypeReference<List<EvalCase>>() {});
}
}
}
四、评分器:用 LLM 当裁判
RAGAS 的核心是"LLM-as-Judge"——用一个判定模型,针对每条用例的「问题/回答/上下文」给出结构化评分。
package com.example.rag.eval;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Faithfulness 评分器:判断答案是否忠于上下文(有没有幻觉)
* 用结构化输出把 AI 的"是/否/依据"变成可计算的数字
*/
@Component
public class FaithfulnessScorer {
private final ChatClient judge; // 建议用便宜的模型当裁判(如 qwen-plus),省钱
public FaithfulnessScorer(ChatClient.Builder builder) {
this.judge = builder.build();
}
// 提示词:让裁判逐句核对,输出"支撑句"列表
private static final String PROMPT = """
你是一个严格的RAG质量评审。
给定【用户问题】【参考上下文】【模型回答】,请判断回答是否完全基于上下文、没有编造。
逐句检查回答,列出每句话在上下文中能找到依据的支撑句。
最后输出 JSON:{"faithful": true/false, "claims": [支撑句列表], "hallucinations": [编造句子列表]}
【用户问题】{question}
【参考上下文】{context}
【模型回答】{answer}
""";
public double score(String question, String context, String answer) {
String json = judge.prompt()
.user(u -> u.text(PROMPT)
.param("question", question)
.param("context", context)
.param("answer", answer))
.call().content();
// 解析 JSON:faithful=true 给 1.0,否则 0.0(生产中可细分到"编造句占比")
return json.contains("\\"faithful\\": true") ? 1.0 : 0.0;
}
}
Faithfulness 的实现逻辑:让裁判把回答拆成"claim",逐个检查 context 里有没有依据。所有 claim 都有依据 → 1.0;有一条编造 → 0.0(或更细:有依据 claim 数 / 总 claim 数)。我建议生产用占比而非非黑即白,曲线更平滑。
五、Context Recall:该召回的有没有漏
package com.example.rag.eval;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
/**
* Context Recall:判断"标准答案所依赖的知识"是否都在召回的 context 里
* 这一步用 groundTruth + 召回context 比对,不依赖模型回答
*/
@Component
public class ContextRecallScorer {
private final ChatClient judge;
public ContextRecallScorer(ChatClient.Builder builder) { this.judge = builder.build(); }
public double score(String question, String referenceAnswer, String retrievedContext) {
String prompt = """
给定【用户问题】【标准答案】【召回的上下文】,判断标准答案中需要的事实,
有多少能在召回上下文里找到。输出 JSON:{"tp": 命中事实数, "fn": 漏掉事实数}
""";
String json = judge.prompt().user(u -> u.text(prompt)
.param("question", question)
.param("referenceAnswer", referenceAnswer)
.param("retrievedContext", retrievedContext)).call().content();
// Recall = TP / (TP + FN)
int tp = extract(json, "tp"), fn = extract(json, "fn");
return (tp + fn) == 0 ? 0.0 : (double) tp / (tp + fn);
}
private int extract(String json, String key) {
// 简化解析:生产用 JSON 库
int i = json.indexOf("\\"" + key + "\\":");
if (i < 0) return 0;
return Integer.parseInt(json.substring(i + key.length() + 3).trim().split("[,\\\\s}]")[0]);
}
}
六、一键跑出评估报告
把上面四个评分器串起来,对每个用例跑一遍,汇总成一张表:
package com.example.rag.eval;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class RagEvaluator {
private final FaithfulnessScorer faithfulness;
private final ContextRecallScorer recall;
// … Precision / AnswerRelevancy 类似
public EvalReport run(List<EvalCase> cases, RagPipeline pipeline) {
List<Row> rows = cases.stream().map(c -> {
// 1. 跑你的 RAG 管线,拿到回答和召回的 context
RagResult r = pipeline.answer(c.question());
// 2. 四项打分
double f = faithfulness.score(c.question(), r.context(), r.answer());
double rec = recall.score(c.question(), c.referenceAnswer(), r.context());
return new Row(c.question(), f, rec, r.answer());
}).toList();
// 3. 汇总均值
return new EvalReport(
avg(rows, Row::faithfulness),
avg(rows, Row::contextRecall)
);
}
private double avg(List<Row> rows, java.util.function.ToDoubleFunction<Row> f) {
return rows.stream().mapToDouble(f).average().orElse(0.0);
}
public record Row(String question, double faithfulness, double contextRecall, String answer) {}
public record EvalReport(double faithfulness, double contextRecall) {}
}
用法:改一次 similarityThreshold,跑一遍 RagEvaluator.run(),记下分数;再改一次,再跑。两次分数对比,就知道这次调参是"真优化"还是"自嗨"。
我自己的经验阈值(技术文档问答):
| Faithfulness | ≥ 0.85 | ≥ 0.95 |
| Context Recall | ≥ 0.75 | ≥ 0.90 |
| Context Precision | ≥ 0.70 | ≥ 0.85 |
| Answer Relevancy | ≥ 0.80 | ≥ 0.92 |
七、建议
评估集最小 50 条,且要覆盖"难例"。光放简单问题,分数全是 0.95 没意义。把线上真实翻车的 query 沉淀进评估集,分数才反映真实质量。
裁判模型用便宜的。Faithfulness 这种判定不需要 GPT-4o,qwen-plus / deepseek 足够,评估跑 100 条能省几十块。
把评估接进 CI。每次改检索参数/换 Embedding 模型,自动跑评估并对比基线,低于阈值就阻断合并——把"凭感觉"变成"门禁"。
没有评估的 RAG 优化,都是盲人摸象。RAGAS 给你一双眼睛。
你无法优化你无法测量的东西。
下篇预告:Day 76 我们跳出"问答",进入 AI Agent——用 LangChain4j 这个 Java 生态首选框架,把"工具调用 + 记忆 + 多步推理"串起来,让 AI 不只是答,而是"做"。
往期回顾:
- 数据层 × 中间件AI化篇:MySQL主从复制与读写分离:延迟排查/故障切换
- 分布式事务解决方案演进
- JDBC、Hibernate、MyBatis、JPA持久层技术的四次进化




