
1. 从缺料就出门买说起RAG 到底缺了什么做过 RAG 项目的人大概都有过这种体验用户问了一个问题检索模块老老实实返回了 Top-K 文档大模型也老老实实基于这些文档生成了一段看起来挺像样的回答但仔细一读——答非所问或者干脆是模型在编。更尴尬的是有些问题明明知识库里根本没有对应内容系统却硬要凑出一段话来用户被误导了还不自知。这个问题的根源其实不在检索算法本身也不在生成模型的能力而在于整个链路里缺了一个关键角色判断信息够不够的那道关卡。打个比方。你家里做饭发现酱油没了。这时候你有两个选择一是翻遍厨房所有柜子看看是不是藏在哪个角落二是确认家里确实没有直接出门去超市买。RAG 系统现在的毛病就是——它只会翻柜子翻不到就随便拿瓶醋糊弄你从来不会说家里真没有我出门买一趟。缺料就出门买这个说法对应的就是 RAG 系统里一个非常实用的设计模式当本地知识库检索结果不足以支撑回答时主动触发外部查询web_query去互联网上补充信息而不是硬答。这个模式在 LangGraph 这类支持状态机和条件分支的框架里实现起来特别自然核心就是加一个评估节点EvaluateSchema用结构化的 JSON 输出来判断够不够然后决定下一步走向。这篇文章我会把整套思路拆开讲清楚为什么要做这个判断、判断的标准怎么定、LangGraph 里怎么编排这个分支、EvaluateSchema 的 JSON 结构怎么设计、web_query 节点怎么接、以及我在实际跑的时候踩过的那些坑。适合已经搭过基础 RAG、想让它更聪明一点的开发者也适合刚接触 LangGraph 想找个真实场景练手的朋友。2. 为什么检索到了不等于能回答2.1 检索分数高不代表内容对得上很多人做 RAG 的第一反应是调 Top-K 和相似度阈值。K 调大一点阈值调低一点总能捞到相关内容吧实测下来这个思路在简单问答上还行一旦问题稍微复杂或者知识库覆盖不全就会出问题。原因在于向量相似度衡量的是语义接近程度不是能否回答问题。举个例子用户问LangGraph 里怎么做条件分支知识库里有一篇讲LangGraph 状态图基础概念的文档里面提到了节点和边但没具体讲条件分支怎么写。这篇文档的向量相似度可能很高因为关键词重叠多但它回答不了这个问题。检索器只看分数它不知道回答了没有。这就是第一个认知误区把检索当成找相关文档而不是找能回答问题的证据。前者是信息检索的思路后者才是 RAG 该有的思路。2.2 大模型的硬答倾向就算你告诉模型只根据提供的上下文回答不知道就说不知道它还是会硬答。这不是模型不听话而是它的训练目标就是生成连贯的、有帮助的回答。当上下文里只有半截信息时它会本能地用预训练知识去补全补出来的东西对不对它自己也不知道。我做过一个对比测试同一个问题知识库里只有部分信息。不加评估节点时模型有大约六成概率会编出一个看似合理的答案加了评估节点、明确告诉它信息不足之后它会老老实实说当前资料不足以回答建议查询外部信息。差别非常明显。2.3 缺料判断的本质三个维度那信息够不够到底怎么判断我总结下来是三个维度覆盖度检索到的内容是否覆盖了问题的所有关键要素。比如问LangGraph 和 LangChain 的区别如果只检索到 LangGraph 的介绍没提 LangChain覆盖度就不够。具体度内容是泛泛而谈还是针对具体问题。上面那个条件分支的例子就是具体度不够。一致性多篇文档之间有没有矛盾。如果两篇文档说法冲突模型会无所适从这也算信息不够。这三个维度光靠向量分数是判断不出来的必须让模型读一遍检索结果然后给出结构化判断。这就是 EvaluateSchema 存在的意义。3. EvaluateSchema 的设计让模型输出可编程的 JSON3.1 为什么必须是结构化输出如果让模型自由发挥它可能会说嗯这些资料看起来差不多够了但可能还差点意思。这种模糊表述没法编程处理——你没法用 if 判断差不多和差点意思。所以评估节点的输出必须是严格结构化的 JSON字段固定、取值明确程序拿到之后能直接做分支判断。这也是为什么关键词里出现了 JSON 和 EvaluateSchema——评估这件事本质上是一个分类打分任务不是生成任务。3.2 字段设计我用的 EvaluateSchema 大概长这样Pydantic 定义LangGraph 里可以直接用from pydantic import BaseModel, Field from typing import Literal, List class EvaluateSchema(BaseModel): sufficient: bool Field( description检索到的资料是否足以回答用户问题 ) confidence: float Field( description判断的置信度0到1之间, ge0.0, le1.0 ) missing_aspects: List[str] Field( default_factorylist, description如果资料不足列出缺失的关键信息点 ) reason: str Field( description做出该判断的简要理由 ) next_action: Literal[generate, web_query] Field( description建议的下一步动作 )几个字段的作用sufficient是核心开关布尔值直接决定走哪条分支。confidence是保险丝。如果模型判断够但置信度只有 0.4你可以选择保守一点仍然去查外部。missing_aspects是给 web_query 用的。它告诉外部查询节点到底缺什么这样查询词才能精准而不是把原问题原封不动丢给搜索引擎。reason是给人看的调试的时候特别有用能看出模型为什么这么判断。next_action是显式的动作指令虽然理论上可以从 sufficient 推导但让模型自己说出来能减少程序逻辑的歧义。3.3 提示词怎么写才不跑偏结构化输出能不能稳定七成看提示词。我踩过的坑是一开始提示词写得太客气模型经常和稀泥明明资料不够也说够。后来改成强约束效果好很多。我的提示词骨架是这样的你是一个严格的资料评估员。你的任务不是回答问题而是判断 给定的检索资料是否足以回答用户问题。 判断标准 1. 资料必须覆盖问题的所有关键要素缺一不可 2. 资料必须包含具体信息不能只是泛泛提及 3. 如果资料之间存在矛盾视为不足 4. 宁可判为不足也不要勉强判为充足 用户问题{question} 检索资料{context} 请严格按照 EvaluateSchema 输出 JSON。关键在最后那句宁可判为不足。RAG 系统里假阴性明明够却说不够的代价远小于假阳性明明不够却说够。前者最多多查一次外部后者会直接产出错误答案。所以评估的倾向性要往严格那边偏。3.4 用 with_structured_output 绑定LangChain 里绑定结构化输出很简单from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0) evaluator llm.with_structured_output(EvaluateSchema)temperature0是必须的评估任务要的是稳定不是创意。with_structured_output会自动把 Pydantic 模型转成 JSON Schema 塞进提示词并解析返回结果省了很多手工解析的活。注意不同模型对结构化输出的支持程度不一样。有些小模型会返回带 markdown 代码块的 JSON解析会失败。上线前一定要用真实数据压测别只看 demo 跑通就完事。4. LangGraph 里的分支编排从检索到出门买4.1 状态设计LangGraph 的核心是状态State在节点之间流转。这个场景的状态大概需要这些字段from typing import TypedDict, List, Optional class RAGState(TypedDict): question: str context: List[str] evaluation: Optional[dict] web_results: Optional[List[str]] answer: Optional[str] retry_count: intretry_count是个容易被忽略但很重要的字段。没有它系统可能在评估不足→查外部→还是不足→再查之间死循环。加上计数超过阈值就强制生成哪怕信息不全也要给个交代。4.2 节点划分整个图我分成五个节点retrieve从本地知识库检索填充 context。evaluate调用评估模型产出 EvaluateSchema写入 evaluation。web_query根据 missing_aspects 构造查询去外部拿资料追加到 context。generate基于最终 context 生成回答。fallback信息实在不够时的兜底回复。4.3 条件边的写法LangGraph 的条件边conditional edge是这套逻辑的关键。评估节点之后根据sufficient和retry_count决定走向def route_after_evaluate(state: RAGState) - str: ev state[evaluation] if ev[sufficient]: return generate if state[retry_count] 2: return fallback return web_query graph.add_conditional_edges( evaluate, route_after_evaluate, { generate: generate, web_query: web_query, fallback: fallback, } )这段逻辑看着简单但有几个细节值得说retry_count 的阈值设 2 还是 3我实测下来 2 比较合适。查两次外部还搞不定的问题多半是问题本身太偏或者表述有问题再查也是浪费 token。web_query 之后要不要再评估一次要。外部资料拿回来之后应该重新走一遍 evaluate确认这次够了没有。所以 web_query 的边要指回 evaluate同时 retry_count 加一。fallback 不是失败。它是诚实的表现。告诉用户这个问题我暂时答不了建议你换个问法或者提供更多背景比编一个答案强得多。4.4 完整图的组装from langgraph.graph import StateGraph, END builder StateGraph(RAGState) builder.add_node(retrieve, retrieve_node) builder.add_node(evaluate, evaluate_node) builder.add_node(web_query, web_query_node) builder.add_node(generate, generate_node) builder.add_node(fallback, fallback_node) builder.set_entry_point(retrieve) builder.add_edge(retrieve, evaluate) builder.add_conditional_edges(evaluate, route_after_evaluate, {...}) builder.add_edge(web_query, evaluate) builder.add_edge(generate, END) builder.add_edge(fallback, END) graph builder.compile()注意web_query的边指回evaluate形成一个小循环。这就是 LangGraph 相比传统链式调用的优势——它天然支持这种带条件的回环不用自己写 while 循环和状态管理。5. web_query 节点怎么出门买才买得准5.1 查询词构造是成败关键web_query 最容易犯的错就是把用户原问题直接丢给搜索引擎。用户问LangGraph 条件分支怎么写你拿这句话去搜搜回来的多半是泛泛的教程还是解决不了具体问题。正确做法是用 missing_aspects 构造精准查询。评估节点已经告诉你缺什么了比如 missing_aspects 是 [条件分支的具体 API 用法, add_conditional_edges 的参数说明]那查询词就围绕这些点来构造def build_query(question: str, missing: List[str]) - str: if not missing: return question return f{question} .join(missing[:2])只取前两个缺失点是因为查询词太长反而会稀释关键词权重。搜索引擎和向量检索都吃这一套。5.2 外部结果的处理拿回来的网页内容不能直接塞进 context得先清洗去重外部结果和本地检索结果可能重叠重复内容会浪费 context 窗口。截断单篇网页动辄几千字全塞进去会挤爆窗口。我一般每篇截前 800 字保留最相关的部分。标注来源给外部内容加个标记比如[web]生成的时候模型能区分哪些是本地知识、哪些是外部补充回答时可以注明。def web_query_node(state: RAGState) - RAGState: query build_query(state[question], state[evaluation][missing_aspects]) raw search_web(query, top_k3) cleaned [f[web] {truncate(r, 800)} for r in dedup(raw)] return { **state, context: state[context] cleaned, retry_count: state[retry_count] 1, }5.3 外部查询的成本控制出门买是有成本的——延迟、token、API 调用费。不能评估一说不确定就无脑出门。我的做法是加两道闸置信度闸sufficientFalse但confidence 0.5时说明模型自己也不确定这种情况先不查外部直接走 fallback 让用户澄清。频率闸同一个会话里外部查询次数做个上限。防止用户连续问偏门问题把预算烧光。这两道闸的具体阈值得根据你的业务场景调。客服场景可以宽松点内部知识助手可以严格点。6. 实测中那些文档不会告诉你的坑6.1 评估模型的过度自信我用过几个不同规模的模型做评估发现一个规律模型越大越容易说够了。大模型见多识广看到半截信息就觉得自己能补全于是判 sufficientTrue。反而是小模型更谦虚经常说不够。解决办法有两个一是提示词里反复强调严格标准二是用 few-shot给几个看似够其实不够的例子。我加了三个 few-shot 例子之后误判率明显下降。6.2 JSON 解析失败的兜底结构化输出不是 100% 可靠的。网络抖动、模型抽风、token 超限都可能导致返回的不是合法 JSON。这时候不能直接崩得有兜底try: ev evaluator.invoke(prompt) except Exception: ev EvaluateSchema( sufficientFalse, confidence0.0, missing_aspects[], reason评估失败保守处理, next_actionweb_query, )评估失败时默认走 web_query是宁可多查一次的思路。总比默认走 generate 然后编答案强。6.3 循环里的状态污染web_query 之后回到 evaluate这时候 context 里既有本地资料又有外部资料。评估的时候要注意别把外部资料也当成本地知识库来评估——它本来就是补充的评估标准应该放宽一点。我的做法是在评估提示词里区分标注告诉模型哪些是外部补充。6.4 延迟叠加一次完整的本地检索→评估→外部查询→再评估→生成链路比普通 RAG 长不少。实测下来P95 延迟大概会翻倍。如果对响应速度敏感可以考虑把评估和检索并行或者用流式输出先给用户一个正在查询的反馈别让用户干等。6.5 别把评估做成第二遍生成有个反模式要避免评估节点里让模型详细分析资料、逐条比对。这样评估本身就成了一个生成任务又慢又贵。评估应该是轻量的分类任务模型扫一眼资料给个判断就行。提示词里明确说简要判断不要展开分析能省不少 token。7. 什么场景值得上这套什么场景别折腾这套缺料就出门买的机制不是万能的它有明确的适用边界。适合的场景知识库覆盖不全但用户问题范围很广的通用助手。对答案准确性要求高宁可说不知道也不能编的领域比如技术支持、医疗咨询注意合规。问题复杂度高单次检索经常不够的深度问答。不适合的场景知识库覆盖完整、问题范围固定的垂直场景。这种场景评估节点纯属多余直接检索生成就行。对延迟极度敏感的实时交互。多一次评估和外部查询延迟扛不住。外部信息不可信或不可用的环境。没有出门买的渠道这套机制就退化成评估兜底价值有限。我个人的经验是先上基础 RAG观察一段时间统计答非所问和编造答案的比例。如果这个比例超过 10%再考虑加评估节点。别一上来就搞复杂架构很多问题其实是检索质量或提示词的问题不是缺评估。8. 几个可以立刻动手的优化点如果你已经决定上这套机制这几个点可以马上落地第一评估提示词里加反例。给模型看几个资料看似相关但实际答不了的例子比讲一堆道理管用。第二missing_aspects 限制数量。让它最多列三个多了反而抓不住重点查询词也会变散。第三给 web_query 加超时。外部查询不可控设个 3 秒超时超了就跳过别让整个链路卡死。第四记录每次评估的 reason。这些日志是优化提示词的黄金素材。跑一周之后回头看能发现模型判断的规律针对性调整。第五retry_count 阈值做成配置项。不同场景需求不一样别写死在代码里。这套东西我前后迭代了大概三版从最初的评估经常误判到现在的基本稳定最大的体会是评估节点的质量八成取决于提示词和 few-shot两成取决于模型选型。别指望换个更强的模型就能解决所有问题提示词打磨才是正道。另外LangGraph 的状态机模型确实适合这类带分支和回环的场景。如果你还在用传统的链式调用硬写 if-else建议试试 LangGraph代码会清爽很多调试也方便——每个节点的输入输出都能单独看出问题好定位。