第2386篇:RAG中的迭代优化——持续改善检索质量的工程闭环
大约 6 分钟
第2386篇:RAG中的迭代优化——持续改善检索质量的工程闭环
适读人群:负责RAG系统持续迭代的AI工程师 | 阅读时长:约18分钟 | 核心价值:建立RAG检索质量的度量-分析-改进闭环,让系统随着时间持续变好
上线半年后的RAG系统,往往有一个奇怪的现象:团队每天都在做优化,但用户满意度几乎没有提升。
仔细分析下来,原因通常是:优化没有方向,靠感觉在做。"这个Prompt看起来更好"、"重新切片一下应该会好",但没有量化指标来验证。
真正有效的优化是数据驱动的:先找到最影响质量的问题所在,然后针对性优化,优化后用数据验证效果。
建立质量基线
不知道现在在哪,就不知道优化有没有效果。
@Service
public class RAGQualityBaseline {
/**
* 建立评估数据集
*
* 这是最重要的一步,没有这个什么都是空话
*
* 评估集要求:
* 1. 覆盖不同类型的问题(简单/复杂、有答案/无答案)
* 2. 有明确的预期答案(Ground Truth)
* 3. 定期更新(反映最新的用户需求)
* 4. 至少100条,最好300条以上
*/
@Data
@Builder
public static class EvaluationCase {
private String question;
private String expectedAnswer; // 预期答案(人工标注)
private List<String> keyFacts; // 答案中必须包含的关键事实
private String sourceDocId; // 答案应来自的文档(可选)
private QuestionType questionType; // 问题类型
private DifficultyLevel difficulty; // 难度
}
/**
* 执行基线评估
*/
public BaselineReport evaluate(List<EvaluationCase> evalSet, RAGService ragService) {
List<CaseResult> results = new ArrayList<>();
for (EvaluationCase evalCase : evalSet) {
long startTime = System.currentTimeMillis();
// 执行RAG
RAGResult ragResult = ragService.answer(evalCase.getQuestion());
long latency = System.currentTimeMillis() - startTime;
// 评估各维度
CaseResult result = CaseResult.builder()
.question(evalCase.getQuestion())
.ragAnswer(ragResult.getAnswer())
.expectedAnswer(evalCase.getExpectedAnswer())
.latency(latency)
// 检索相关性:检索到的文档是否相关
.retrievalRelevance(evaluateRetrievalRelevance(
evalCase, ragResult.getRetrievedDocs()))
// 答案完整性:关键事实是否都覆盖了
.answerCompleteness(evaluateAnswerCompleteness(
evalCase.getKeyFacts(), ragResult.getAnswer()))
// 答案忠实性:答案是否基于检索到的文档
.answerFaithfulness(evaluateAnswerFaithfulness(
ragResult.getAnswer(), ragResult.getRetrievedDocs()))
// 整体质量(综合评分)
.overallScore(calculateOverallScore(/* 三项指标 */))
.build();
results.add(result);
}
return buildBaselineReport(results);
}
/**
* 答案完整性评估
* 检查预期的关键事实是否都出现在答案中
*/
private double evaluateAnswerCompleteness(List<String> keyFacts, String answer) {
if (keyFacts == null || keyFacts.isEmpty()) return 1.0;
long coveredFacts = keyFacts.stream()
.filter(fact -> answerContainsFact(answer, fact))
.count();
return (double) coveredFacts / keyFacts.size();
}
private boolean answerContainsFact(String answer, String fact) {
// 精确包含
if (answer.contains(fact)) return true;
// 语义相似(使用LLM判断)
String prompt = """
答案是否包含了以下事实?只回答"是"或"否"。
答案:%s
事实:%s
""".formatted(answer, fact);
String response = chatClient.prompt(prompt).call().content().trim();
return response.startsWith("是");
}
}错误分析:找到改进的方向
@Service
public class RAGErrorAnalyzer {
/**
* 对评估结果做分类分析
* 找出最常见的失败模式
*/
public ErrorAnalysisReport analyze(List<CaseResult> results) {
// 只分析失败的案例
List<CaseResult> failedCases = results.stream()
.filter(r -> r.getOverallScore() < 0.7)
.collect(Collectors.toList());
// 按失败类型分类
Map<FailureType, List<CaseResult>> byFailureType = new HashMap<>();
for (CaseResult failedCase : failedCases) {
FailureType type = classifyFailure(failedCase);
byFailureType.computeIfAbsent(type, k -> new ArrayList<>()).add(failedCase);
}
return ErrorAnalysisReport.builder()
.totalFailed(failedCases.size())
.totalEvaluated(results.size())
.failureRate((double) failedCases.size() / results.size())
.byFailureType(byFailureType)
.topFailureType(getTopFailureType(byFailureType))
.recommendations(generateRecommendations(byFailureType))
.build();
}
/**
* 故障分类:弄清楚是哪个环节出了问题
*/
private FailureType classifyFailure(CaseResult failedCase) {
// 检索相关性低:检索阶段出了问题
if (failedCase.getRetrievalRelevance() < 0.5) {
return FailureType.RETRIEVAL_MISS; // 没有检索到相关文档
}
// 检索相关性好,但答案质量低:生成阶段出了问题
if (failedCase.getRetrievalRelevance() >= 0.7 &&
failedCase.getAnswerCompleteness() < 0.5) {
return FailureType.GENERATION_INCOMPLETE; // 生成时遗漏了信息
}
// 答案不忠实于检索结果:幻觉问题
if (failedCase.getAnswerFaithfulness() < 0.5) {
return FailureType.HALLUCINATION; // AI编造了没有依据的内容
}
// 检索到了但文档质量差
if (failedCase.getRetrievalRelevance() >= 0.5 &&
failedCase.getAnswerCompleteness() < 0.5) {
return FailureType.POOR_SOURCE_QUALITY; // 检索到的文档内容质量差
}
return FailureType.UNKNOWN;
}
/**
* 基于失败类型生成改进建议
*/
private List<Recommendation> generateRecommendations(
Map<FailureType, List<CaseResult>> byFailureType) {
List<Recommendation> recommendations = new ArrayList<>();
if (byFailureType.containsKey(FailureType.RETRIEVAL_MISS)) {
int count = byFailureType.get(FailureType.RETRIEVAL_MISS).size();
recommendations.add(Recommendation.builder()
.priority(count > 10 ? Priority.HIGH : Priority.MEDIUM)
.failureType(FailureType.RETRIEVAL_MISS)
.title("改善检索召回率")
.suggestions(List.of(
"检查这些问题对应的文档是否已入库",
"优化文档的切片策略(可能切片太大或太小)",
"考虑增加混合检索(BM25 + 向量)",
"优化查询改写,用更好的查询词"
))
.build()
);
}
if (byFailureType.containsKey(FailureType.HALLUCINATION)) {
recommendations.add(Recommendation.builder()
.priority(Priority.HIGH) // 幻觉问题优先级最高
.failureType(FailureType.HALLUCINATION)
.title("降低幻觉率")
.suggestions(List.of(
"在Prompt中强化'只基于提供的文档回答'的指令",
"增加置信度评估,低置信度时不生成答案",
"减少检索文档数(文档越多,越容易发生幻觉)",
"使用更保守的Temperature设置"
))
.build()
);
}
return recommendations;
}
}改进循环的工程实现
@Service
public class RAGImprovementCycle {
/**
* 持续改进循环
*
* 每次迭代:
* 1. 运行评估
* 2. 分析失败模式
* 3. 选择优先级最高的改进点
* 4. 实施改进(在测试环境)
* 5. 验证改进效果
* 6. 如果有改善,推广到生产
*/
public ImprovementReport runImprovementCycle(
RAGService currentRAG, List<EvaluationCase> evalSet) {
// 当前基线
BaselineReport baseline = qualityBaseline.evaluate(evalSet, currentRAG);
log.info("Current baseline: overall score = {}", baseline.getOverallScore());
// 错误分析
ErrorAnalysisReport errorAnalysis = errorAnalyzer.analyze(baseline.getResults());
// 找到最高优先级的改进点
Recommendation topRecommendation = errorAnalysis.getTopRecommendation();
if (topRecommendation == null) {
return ImprovementReport.noImprovementNeeded(baseline);
}
log.info("Top improvement area: {} (affected {} cases)",
topRecommendation.getTitle(),
topRecommendation.getAffectedCount());
// 执行改进实验
ExperimentResult experiment = runExperiment(topRecommendation, evalSet);
return ImprovementReport.builder()
.baselineScore(baseline.getOverallScore())
.improvement(experiment.getImprovement())
.appliedRecommendation(topRecommendation)
.experimentResult(experiment)
.build();
}
/**
* A/B测试:对比改进前后的效果
*/
private ExperimentResult runExperiment(Recommendation recommendation,
List<EvaluationCase> evalSet) {
// 创建改进后的RAG实例
RAGService improvedRAG = applyImprovement(recommendation);
// 在相同的评估集上运行
BaselineReport improvedReport = qualityBaseline.evaluate(evalSet, improvedRAG);
BaselineReport currentReport = qualityBaseline.evaluate(evalSet, currentRAG);
double improvement = improvedReport.getOverallScore() - currentReport.getOverallScore();
return ExperimentResult.builder()
.baselineScore(currentReport.getOverallScore())
.improvedScore(improvedReport.getOverallScore())
.improvement(improvement)
.isSignificant(Math.abs(improvement) > 0.03) // 超过3%认为显著
.shouldApply(improvement > 0 && improvement > 0.02)
.build();
}
}在线质量监控
评估集是离线的,还需要在线实时监控。
@Service
public class OnlineQualityMonitor {
/**
* 实时质量指标收集
* 每次RAG响应后,自动评估一些可量化的指标
*/
public void recordResponseMetrics(String queryId, RAGResponse response) {
ResponseMetrics metrics = ResponseMetrics.builder()
.queryId(queryId)
.retrievalCount(response.getRetrievedDocs().size())
.topDocScore(getTopScore(response.getRetrievedDocs()))
.responseLength(response.getAnswer().length())
.hasDisclaimer(containsUncertaintyDisclaimer(response.getAnswer()))
.latencyMs(response.getLatencyMs())
.build();
metricsRepository.save(metrics);
// 推送到监控面板
metricsPublisher.publish("rag.response", metrics);
}
/**
* 低质量响应的实时预警
*
* 如果最近1小时的平均top_doc_score低于阈值,可能说明有问题
*/
@Scheduled(fixedRate = 300000) // 每5分钟检查
public void checkQualityAlerts() {
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
QualityStats stats = metricsRepository.getAggregateStats(oneHourAgo);
if (stats.getAverageTopDocScore() < 0.5) {
alertService.sendAlert(
AlertLevel.WARNING,
String.format("RAG检索质量下降:过去1小时平均相似度%.2f(阈值0.5)",
stats.getAverageTopDocScore())
);
}
if (stats.getLowConfidenceRate() > 0.3) {
alertService.sendAlert(
AlertLevel.WARNING,
String.format("低置信度响应比例过高:%.1f%%",
stats.getLowConfidenceRate() * 100)
);
}
}
}迭代周期建议
建立一个有效的RAG迭代机制,我推荐这样的节奏:
- 每天:看在线监控指标,有没有明显异常
- 每周:运行一次评估集,对比上周基线,看有没有退步
- 每月:做一次深度错误分析,制定下个月的改进优先级
- 每季度:更新评估集(加入最近用户问的新类型问题),重新校准基线
没有这个节奏,RAG系统很容易陷入"自以为在优化,但实际上没变化甚至变差了"的困境。
