
LangChain CRAG 深度剖析检索纠错到底怎么纠的幻觉检测每次都查吗普通 RAG 是检索 → 生成一条路走到底——检索回来的文档可能根本不相关模型照样硬着头皮编答案。Corrective RAGCRAG号称能自我纠错但纠错到底纠的是什么是改答案还是改检索幻觉检测是不是每次都调 LLM本文拆开 langchainrust 的 CRAG 实现逐行讲清楚整条纠错链路。一、CRAG 全流程6 步状态机CRAG 的核心逻辑是一个状态机6 步走完1. 检索(retrieve) → 向量检索拿回 N 篇文档 2. 评分(grade_documents) → LLM 给每篇文档打相关度分 3. 纠错(correct) → 平均分低于阈值? 触发纠错 4. 过滤(filter) → 扔掉低于阈值的文档 5. 生成(generate) → 基于过滤后的文档生成答案 6. 幻觉检测(hallucination) → LLM 检查答案有没有依据下面逐步拆解。二、评分LLM 怎么判断文档相不相关评分不是算余弦相似度是让 LLM 读文档后判断。评分 PromptYou are a document relevance grader. Given a user query and a document, determine if the document is relevant. Query: {query} Document: {document_content} Respond in this exact format: Relevance: [relevant/irrelevant] Score: [0.0 to 1.0] Reasoning: [brief explanation]LLM 返回类似Relevance: relevant, Score: 0.9, Reasoning: 文档直接描述了...代码再解析。解析规则4 种情况LLM 回了什么得分说明有明确数字Score: 0.750.75直接取最可靠含relevant不含irrelevant0.8默认相关分含irrelevant0.2默认不相关分啥都没有模糊响应0.4故意低于阈值标记is_ambiguous这里有个坑模糊响应给 0.4 是 v0.5.1 才调的。早期版本给 0.5恰好等于旧阈值也是 0.5导致 LLM 说不清时——纠不纠全看运气。改成 0.4 后模糊就触发纠错保守策略宁多纠不漏纠。评分是并行的4 篇文档不是串行评是用join_all并行调 4 次 LLMletfutures:Vec_documents.iter().map(|doc|self.grade(query,doc)).collect();letresultsjoin_all(futures).await;// 4 次同时发出延迟 ≈ 1 次 LLM 调用的时间不是 4 倍。三、纠错纠的不是答案是检索这是 CRAG 最核心的设计。当平均分低于阈值默认 0.6触发correct()ifstate.avg_scoreself.grade_threshold{// 默认 0.6self.correct(mutstate).await?;}纠错不是改答案是换一种方式重新检索。correct()做四件事3.1 生成 3 个备选查询让 LLM 把原问题换个问法生成 3 个变体Generate 3 alternative versions of the following query to improve retrieval coverage. Original query: {query} Each alternative should approach the information need from a different angle.比如原问题Rust 和 Go 的区别LLM 可能生成Rust 与 Go 语言特性对比Go 和 Rust 性能差异分析Rust Go 适用场景选择为什么要换问法向量检索有角度偏差——同一个意思换个说法可能检索不到。多角度提问能提高召回。3.2 用备选查询并行检索 去重3 个查询各检索 4 篇 → 最多 12 篇 → 按前 200 字符判重合并letretrieve_futures:Vec_alternatives.iter().map(|q|self.retriever.retrieve(q,self.retrieve_k)).collect();letall_resultsjoin_all(retrieve_futures).await;// 3 路并行letmutseen_contentHashSet::new();fordocinall_results{ifseen_content.insert(doc.content[..200]){// 按内容去重merged_docs.push(doc);}}3.3 联网搜索兜底可选如果配了 web 搜索工具还会去网上搜一次补料ifletSome(tool)self.web_fallback{letweb_resulttool.run(state.current_query.clone()).await?;state.web_resultsSome(web_result);}3.4 重新评分纠错后还会再评一轮分确保新文档的质量self.grade_documents(state).await?;// 对新文档重新打分不是换回来就直接用要验证新检索的文档确实更好。四、过滤宁可没文档不留烂文档纠错后低于阈值的文档直接扔掉letfiltered:Vec(Document,f64)state.documents.iter().zip(state.grade_scores.iter()).filter(|(_,score)|scoreself.grade_threshold).collect();// 全不达标 → 宁可空也不留烂文档误导生成letsource_docsiffiltered.is_empty(){Vec::new()}else{filtered};这是个重要设计宁可没文档生成我不知道也不要用烂文档生成看似合理实则胡编的答案。五、生成评分理由也喂给模型生成答案时不只喂文档还把评分时 LLM 说的为什么这篇相关也塞进 promptletcontextbuild_context(source_docs,state.web_results,...);letreasoning_textformat_reasoning(source_docs,reasoning);// 评分理由letuser_messagebuild_generate_prompt(context,reasoning_text,state.query);这样生成模型能理解每篇文档的价值更好地组织答案。另外有个Token 预算截断build_context()按分数从高到低排超预算的低分文档直接丢letmutsorted:Vec_docs.iter().collect();sorted.sort_by(|a,b|b.1.partial_cmp(a.1)...);// 按分降序for(doc,_score)insorted{iftoken_countentry_tokensmax{continue;// 超预算直接跳过}...}六、幻觉检测每次都查且最好用另一个模型生成答案后最后一步是幻觉检测——检查答案是不是编的。什么是幻觉LLM 没有可靠资料时会一本正经地胡编。问张三的生日文档没写它可能编一个1985 年 3 月 12 日。这就是幻觉。怎么检测把文档 答案一起喂给 LLM让它以怀疑态度核查You are a skeptical fact-checking assistant. Be skeptical. Is the above answer fully grounded in and supported by the provided context? - grounded if every claim is directly supported - not grounded if any information is not found in or contradicted by the context解析 LLM 回复ifwords 包含not grounded或unsupported{state.groundedfalse;// 判定编的}elseifwords 包含grounded或yes{state.groundedtrue;// 判定有据}else{state.groundedfalse;// 模糊 → 默认当编的保守}三个关键设计点① 为什么用独立 LLM生成答案和检测幻觉默认用同一个 LLM。问题是模型倾向于认同自己的输出——自己写的自己看当然觉得对这就是自检偏差。所以可以注入独立 LLM 做检测letagentCorrectiveRAGAgent::new(main_llm,retriever).with_grader_llm(checker_llm);// 用另一个模型检测代码里letgrader_llmself.grader_llm.unwrap_or(self.llm);// 优先独立 LLM没配就用主 LLM向后兼容。② 检测失败不中断检测本身调用失败网络断了、限流了不会让整个流程崩而是降级标记grounded falsematchgrader_llm.chat_with_system(...).await{Ok(r)r,Err(_){state.groundedfalse;// 标记未验证不报错returnOk(());}}答案照常返回只是标了未验证。③ 结果怎么用CRAGResult.grounded告诉调用方答案有没有经过验证letresult:CRAGResultagent.invoke(问题).await?;if!result.grounded{// 答案可能不靠谱提示用户或走别的策略}七、灵魂拷问每次都调 LLM 吗调几次这是性能和成本的核心问题。数一下invoke()一次的 LLM 调用步骤调 LLM次数1. 检索❌向量检索不调 LLM2. 评分✅每篇文档 1 次4 篇 4 次并行3. 纠错触发时✅生成备选查询 1 次 重评分 ~4 次并行 web 1 次4. 过滤❌纯逻辑5. 生成✅1 次6. 幻觉检测✅1 次如果开启最少几次文档质量好不纠错评分 4 次 生成 1 次 幻觉检测 1 次 6 次最多几次触发纠错 联网搜索评分 4 次 生成备选查询 1 次 重评分 4 次 web 1 次 生成 1 次 幻觉检测 1 次 ≈ 12 次没有缓存代码里没有上次评分过就跳过的缓存机制每次invoke()都完整跑一遍。但有两个优化评分并行join_all4 次同时发不是串行等备选查询检索并行3 路同时搜所以实际延迟 ≈ 几个 LLM 往返不是十几次串行相加。八、完整链路一图流用户提问 │ ▼ [1] 检索 ─── 向量检索 N 篇文档 │ ▼ [2] 评分 ─── LLM 并行给每篇打分4 次并行调用 │ ▼ 平均分 0.6? ├─ 否 ───────────────────────────────────┐ │ │ ├─ 是 → [3] 纠错 │ │ ├─ 生成 3 个备选查询1 次 LLM │ │ ├─ 并行检索 去重3 路并行 │ │ ├─ 联网搜索兜底可选1 次 │ │ └─ 重新评分~4 次 LLM 并行 │ │ │ ▼ ▼ [4] 过滤 ─── 扔掉低分文档宁可空不留烂 │ ▼ [5] 生成 ─── 文档 评分理由 → LLM 生成答案1 次 │ ▼ [6] 幻觉检测 ── 文档 答案 → LLM 怀疑式核查1 次 │ └─ 优先用独立 LLM避免自检偏差 │ └─ 失败不中断标记 groundedfalse │ ▼ 输出 CRAGResult ├─ answer: 答案 ├─ grounded: 是否有据幻觉检测结论 ├─ sources: 来源文档 ├─ grade_scores: 各文档评分 └─ grade_reasoning: 评分理由九、一句话总结CRAG 的纠错不是改答案是改检索——评分发现检索回来的文档不行就换个问法重新搜、去网上搜搜到更好的文档后再生成。最后还用最好独立的LLM 验一遍答案是不是胡编的。代价是 LLM 调用次数多6~12 次但换来了可观测的质量信号grounded字段和更高的答案可靠性。十、什么时候该用 CRAG适合对答案准确性要求高、容忍延迟的场景客服质检、医疗法律咨询检索质量不稳定文档库杂、向量模型一般需要质量信号做后续决策groundedfalse时转人工不适合实时聊天6~12 次 LLM 调用延迟太高成本敏感的大规模批处理调用量翻倍简单 FAQ普通 RAG 足够CRAG 是杀鸡用牛刀仓库github.com/atliliw/langchainrust