第2311篇:Corrective RAG——检测到检索质量差时的自动修正机制
大约 7 分钟
第2311篇:Corrective RAG——检测到检索质量差时的自动修正机制
适读人群:RAG系统工程师、AI质量保障工程师 | 阅读时长:约16分钟 | 核心价值:掌握CRAG的核心机制,构建能自动识别检索失败并采取补救措施的鲁棒RAG系统
我们的RAG系统有一个"必死"场景:用户问的问题涉及最近三个月的事情。我们的向量库是两个月前建的,里面根本没有相关内容。在这种情况下,系统的表现是:检索到一些"相关但不准确"的文档,然后LLM基于这些陈旧文档生成了一个看似完整但实际上是错误的回答。
最可怕的不是"回答不出来",而是"自信地给出错误答案"。
Corrective RAG(CRAG)的出发点就是:不要假设检索一定成功,要主动检测检索质量,并在质量不佳时启动修正流程。
检索失败的模式分类
不是所有的检索失败都一样,要分类处理:
完全不相关(Irrelevant):检索到的文档和问题完全没有交集。最差的情况,需要最积极的修正。
部分相关(Ambiguous):文档涉及同一主题但角度不对,或信息过时。需要补充检索。
高度相关(Relevant):文档直接回答了问题。正常流程,无需修正。
检索质量评估器
@Component
public class RetrievalQualityEvaluator {
private final ChatClient evaluationClient;
private static final String EVALUATION_PROMPT = """
你是一个严格的RAG质量评估专家。
评估维度(每项0-100分):
1. 主题相关性:文档主题是否与问题相关?
2. 信息充分性:文档是否包含回答问题所需的具体信息?
3. 时效性:对于需要最新信息的问题,文档是否足够新?
4. 一致性:多个文档之间是否信息一致(有无明显矛盾)?
综合评级:
- SUFFICIENT:可以直接用于生成回答(三项均≥70)
- AMBIGUOUS:文档部分有用,建议补充检索(有项目50-70)
- INSUFFICIENT:文档无法支撑回答(有项目<50)
输出格式(JSON):
{
"overallGrade": "SUFFICIENT/AMBIGUOUS/INSUFFICIENT",
"topicRelevance": 0-100,
"informationSufficiency": 0-100,
"timeliness": 0-100,
"consistency": 0-100,
"issues": ["具体问题描述"],
"suggestedAction": "建议的修正行动"
}
""";
public RetrievalQualityReport evaluate(String question,
List<RetrievedDocument> documents) {
if (documents.isEmpty()) {
return RetrievalQualityReport.insufficient("未检索到任何文档");
}
String docsContent = documents.stream()
.enumerate()
.map(e -> "文档%d:\n%s".formatted(e.getKey() + 1, e.getValue().content()))
.collect(Collectors.joining("\n\n---\n\n"));
String response = evaluationClient.prompt()
.system(EVALUATION_PROMPT)
.user("问题:%s\n\n检索到的文档:\n%s".formatted(question, docsContent))
.call()
.content();
return parseReport(response);
}
/**
* 快速评估:基于向量相似度分数的简单判断
* 不调用LLM,用于快速路径
*/
public RetrievalGrade quickEvaluate(List<RetrievedDocument> documents) {
if (documents.isEmpty()) {
return RetrievalGrade.INSUFFICIENT;
}
double avgScore = documents.stream()
.mapToDouble(RetrievedDocument::similarityScore)
.average()
.orElse(0.0);
double maxScore = documents.stream()
.mapToDouble(RetrievedDocument::similarityScore)
.max()
.orElse(0.0);
if (maxScore >= 0.85 && avgScore >= 0.75) {
return RetrievalGrade.SUFFICIENT;
} else if (maxScore >= 0.65) {
return RetrievalGrade.AMBIGUOUS;
} else {
return RetrievalGrade.INSUFFICIENT;
}
}
}查询重写器:换个角度再试一次
当检索质量不佳时,先尝试重写查询再检索:
@Component
public class QueryRewriter {
private final ChatClient chatClient;
private static final String REWRITE_PROMPT = """
用户的原始查询在知识库中没有找到好的匹配文档。
请对这个查询进行重写,尝试用不同的角度、词汇或拆解方式重新表达。
重写策略:
1. 同义词替换:用不同的词语表达相同含义
2. 问题拆解:将复杂问题拆成多个简单子问题
3. 关键词提炼:从问题中提取最核心的关键词
4. 泛化/具体化:根据情况适当扩大或缩小范围
输出3个不同的重写版本(JSON数组):
["重写版本1", "重写版本2", "重写版本3"]
""";
public List<String> rewrite(String originalQuery, String retrievalIssue) {
String response = chatClient.prompt()
.system(REWRITE_PROMPT)
.user("原始查询:%s\n\n检索问题:%s".formatted(originalQuery, retrievalIssue))
.call()
.content();
return parseRewrittenQueries(response);
}
/**
* 对问题进行结构化拆解
* 适用于复合问题
*/
public List<String> decompose(String complexQuery) {
String response = chatClient.prompt()
.system("将复合问题拆解为独立的子问题,每个子问题可以单独检索回答。输出JSON数组。")
.user(complexQuery)
.call()
.content();
return parseRewrittenQueries(response);
}
}知识精炼:从检索结果中提取精华
就算检索质量不理想,也可以从文档中提炼有用信息:
@Component
public class KnowledgeRefiner {
private final ChatClient chatClient;
private static final String REFINE_PROMPT = """
从以下文档中,提取与用户问题相关的所有有用信息片段。
要求:
1. 只保留与问题直接相关的内容
2. 去除与问题无关的背景信息
3. 对矛盾信息,保留更新/更可信的来源
4. 明确标注每条信息的来源文档编号
如果文档中完全没有有用信息,输出 "NO_USEFUL_INFO"。
否则,以精炼后的信息列表形式输出,格式:
- [来源文档X] 提炼后的信息内容
""";
public RefinedKnowledge refine(String question, List<RetrievedDocument> documents) {
String docsContent = buildDocsContent(documents);
String response = chatClient.prompt()
.system(REFINE_PROMPT)
.user("问题:%s\n\n文档内容:\n%s".formatted(question, docsContent))
.call()
.content();
if (response.trim().equals("NO_USEFUL_INFO")) {
return RefinedKnowledge.empty();
}
return RefinedKnowledge.of(response.trim());
}
}完整的CRAG执行器
@Service
public class CorrectiveRAGExecutor {
private final VectorSearchService vectorSearch;
private final RetrievalQualityEvaluator qualityEvaluator;
private final QueryRewriter queryRewriter;
private final WebSearchService webSearch;
private final KnowledgeRefiner knowledgeRefiner;
private final ChatClient generationClient;
public CRAGResult execute(String question) {
// 第一步:初始检索
List<RetrievedDocument> initialDocs = vectorSearch.search(question, 5);
// 第二步:快速质量判断(不调LLM,基于向量得分)
RetrievalGrade quickGrade = qualityEvaluator.quickEvaluate(initialDocs);
// 高质量直接走标准RAG,避免额外开销
if (quickGrade == RetrievalGrade.SUFFICIENT) {
String answer = generateWithDocs(question, initialDocs);
return CRAGResult.standard(answer, initialDocs);
}
// 第三步:深度质量评估(调用LLM)
RetrievalQualityReport report = qualityEvaluator.evaluate(question, initialDocs);
log.info("检索质量: {}, 问题: {}", report.overallGrade(), report.issues());
return switch (report.overallGrade()) {
case SUFFICIENT -> {
String answer = generateWithDocs(question, initialDocs);
yield CRAGResult.standard(answer, initialDocs);
}
case AMBIGUOUS -> handleAmbiguous(question, initialDocs, report);
case INSUFFICIENT -> handleInsufficient(question, report);
};
}
/**
* 处理模糊情况:补充检索后合并知识
*/
private CRAGResult handleAmbiguous(String question,
List<RetrievedDocument> initialDocs,
RetrievalQualityReport report) {
log.info("检索质量模糊,执行补充检索");
// 知识精炼:从初始文档中提取有用部分
RefinedKnowledge refinedFromInitial = knowledgeRefiner.refine(question, initialDocs);
// 查询重写,补充检索
List<String> rewrittenQueries = queryRewriter.rewrite(question,
String.join(", ", report.issues()));
List<RetrievedDocument> additionalDocs = new ArrayList<>();
for (String rewrittenQuery : rewrittenQueries.subList(0, Math.min(2, rewrittenQueries.size()))) {
additionalDocs.addAll(vectorSearch.search(rewrittenQuery, 3));
}
// 去重
additionalDocs = deduplicateDocs(additionalDocs, initialDocs);
// 合并知识
String combinedContext = buildCombinedContext(refinedFromInitial, additionalDocs);
String answer = generateWithContext(question, combinedContext);
return CRAGResult.corrected(answer, initialDocs, additionalDocs, "AMBIGUOUS_CORRECTED");
}
/**
* 处理质量不足的情况:扩展到外部知识源
*/
private CRAGResult handleInsufficient(String question, RetrievalQualityReport report) {
log.warn("内部知识库检索质量不足,扩展到外部搜索");
// 策略1:查询重写后重试内部检索
List<String> rewrittenQueries = queryRewriter.rewrite(question,
String.join(", ", report.issues()));
List<RetrievedDocument> retrySocs = new ArrayList<>();
for (String q : rewrittenQueries) {
retrySocs.addAll(vectorSearch.search(q, 3));
}
RetrievalGrade retryGrade = qualityEvaluator.quickEvaluate(retrySocs);
if (retryGrade == RetrievalGrade.SUFFICIENT) {
// 重写查询后找到了好文档
String answer = generateWithDocs(question, retrySocs);
return CRAGResult.corrected(answer, List.of(), retrySocs, "QUERY_REWRITE_SUCCESS");
}
// 策略2:Web搜索
if (webSearch.isAvailable()) {
try {
List<WebSearchResult> webResults = webSearch.search(question, 5);
String webContext = webResults.stream()
.map(WebSearchResult::snippet)
.collect(Collectors.joining("\n\n"));
String answer = generateWithContext(question, webContext);
return CRAGResult.corrected(answer, List.of(), List.of(), "WEB_SEARCH");
} catch (Exception e) {
log.error("Web搜索失败", e);
}
}
// 策略3:降级——告知用户知识库限制
String fallbackAnswer = generateWithLimitation(question);
return CRAGResult.degraded(fallbackAnswer, "知识库无相关信息");
}
private String generateWithDocs(String question, List<RetrievedDocument> docs) {
String context = docs.stream()
.map(RetrievedDocument::content)
.collect(Collectors.joining("\n\n---\n\n"));
return generateWithContext(question, context);
}
private String generateWithContext(String question, String context) {
return generationClient.prompt()
.system("基于以下参考信息回答问题:\n\n" + context)
.user(question)
.call()
.content();
}
private String generateWithLimitation(String question) {
return generationClient.prompt()
.system("""
你的知识库中没有关于这个问题的相关信息。
请直接告知用户你无法提供准确答案,并建议他们通过其他渠道获取信息。
不要编造信息。
""")
.user(question)
.call()
.content();
}
}指标监控:衡量CRAG的效果
上线CRAG后,要有指标来衡量它是否真的在起作用:
@Component
public class CRAGMetricsCollector {
private final MeterRegistry meterRegistry;
public void recordResult(CRAGResult result) {
// 记录各类型处理比例
Counter.builder("crag.result.type")
.tag("type", result.resultType())
.register(meterRegistry)
.increment();
// 记录检索质量分布
if (result.qualityGrade() != null) {
Counter.builder("crag.retrieval.quality")
.tag("grade", result.qualityGrade().name())
.register(meterRegistry)
.increment();
}
// 记录修正是否改善了最终结果(需要用户反馈)
if (result.wasCorrected()) {
log.info("CRAG修正触发: originalGrade={}, correctionType={}",
result.originalGrade(), result.correctionType());
}
}
}我们上线CRAG的最大收益是:对"知识库里没有的问题",从之前的"自信地给错答案"变成了"明确告知用户无法回答或跳转到Web搜索"。这一变化让用户的信任度明显提升——人们能接受系统说"我不知道",但无法接受系统信口开河。
