ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LangGraph实战:为RAG添加判断力节点,解决信息不足硬答问题

LangGraph实战:为RAG添加判断力节点,解决信息不足硬答问题 1. 从“缺料就出门买”说起RAG 到底缺了什么做过 RAG 项目的人大概都有过这种体验用户问了一个问题检索模块吭哧吭哧从向量库里捞回来三五段文本大模型拿到这些文本之后一本正经地开始编。编得还挺像那么回事格式工整、语气笃定但内容跟事实差了十万八千里。更气人的是你明明知道知识库里没有这个信息但模型不会告诉你“我不知道”它只会硬答。这个问题的根源不在于大模型不够聪明而在于整个 RAG 流程里缺了一个关键角色判断“信息够不够”的那个环节。传统的 RAG 流程是线性的——用户提问、向量检索、拼接上下文、丢给大模型生成。这条链路里没有任何一步在问“检索回来的这些东西真的足以回答这个问题吗”如果没有是不是应该换个方式再找找比如去网上搜一圈我最近在做一个基于 LangGraph 的 RAG 项目时就遇到了这个瓶颈。知识库里存的是一些内部技术文档和产品说明但用户的问题经常跑到知识库覆盖范围之外。一开始我的做法很简单检索不到就检索不到让模型自己看着办。结果就是各种胡编乱造用户体验极差。后来我想了一个办法给 RAG 装上一个“出门买料”的能力。当本地知识库的信息不够时不是硬答而是触发一次 web_query去外部搜索补充信息然后再基于补充后的上下文来生成回答。这个思路听起来简单但落地的时候有几个关键问题需要解决怎么判断信息够不够判断的逻辑放在哪一步web_query 的结果怎么和本地检索结果融合LangGraph 的状态图怎么设计这篇文章就把我这段时间的实战经验完整拆一遍。如果你也在做 RAG 项目或者正在用 LangGraph 搭建 Agent 工作流尤其是遇到过“检索结果不够但模型硬答”的问题那这篇内容应该能帮你少走一些弯路。我会从整体设计思路讲到具体的 EvaluateSchema 定义、JSON 结构化输出的处理、LangGraph 节点编排再到实际踩过的坑和排查技巧尽量把每个环节都讲透。2. 整体设计为什么要在 RAG 里加一个“判断力”节点2.1 传统 RAG 的线性困境先说说传统 RAG 的问题到底出在哪。大部分 RAG 系统的架构是这样的用户输入问题Embedding 模型把问题转成向量向量数据库做相似度检索取 Top-K 个文档片段拼成一个 Prompt 模板最后交给大模型生成答案。这条链路是单向的、无反馈的。问题在于向量检索的“相似度”和“能不能回答问题”是两回事。一段文本可能和问题在语义上很接近但它包含的信息并不足以回答这个问题。举个例子用户问“LangGraph 的 checkpointer 怎么配置”知识库里有一段文本讲的是“LangGraph 支持状态持久化”语义相似度很高会被检索出来但这段文本没有讲具体怎么配置 checkpointer。模型拿到这段文本要么说一些泛泛的话要么就开始编具体的配置参数。这就是我所说的“信息够不够”的问题。传统 RAG 没有能力识别这种情况因为它没有判断环节。2.2 加入判断节点后的流程变化我在项目里引入的核心改动是在检索之后、生成之前插入一个评估节点。这个节点的职责很明确拿到用户问题和检索回来的文档片段判断这些信息是否足以回答用户的问题。判断结果是一个结构化的 JSON 对象包含两个关键字段is_sufficient布尔值表示信息是否充足和reason字符串说明判断理由。如果is_sufficient为 true流程继续走正常的生成路径。如果为 false流程会分支到 web_query 节点触发一次外部搜索把搜索结果和原始检索结果合并然后再走生成路径。这个设计的好处在于它把“我不知道”变成了一个显式的、可编程的状态而不是让模型去猜。模型不需要在生成阶段纠结“我到底该不该编”因为评估节点已经帮它做了这个决定。2.3 为什么选 LangGraph 而不是自己写 if-else有人可能会问这个逻辑用 if-else 不就行了吗检索完判断一下不够就搜索够了就生成为什么要用 LangGraph这个问题我一开始也想过。如果你的流程就是“检索→判断→搜索→生成”这一条线确实用 if-else 也能实现。但实际项目中流程往往会比这复杂得多。比如web_query 之后可能还需要再做一次评估确认补充的信息是否足够如果仍然不够可能需要走一个降级路径直接告诉用户“当前知识库无法回答这个问题”再比如你可能需要在评估节点里加入重试逻辑第一次判断不确定的时候换个 Prompt 再试一次。这些分支和循环用 if-else 写会非常混乱而 LangGraph 的状态图模型天然适合这种场景。每个节点是一个处理单元边定义了节点之间的流转条件整个流程一目了然。而且 LangGraph 内置了状态管理和检查点机制调试和回溯都很方便。提示如果你的 RAG 流程只有一条直线、没有任何分支那确实没必要上 LangGraph。但只要涉及到条件分支、循环重试、多路合并LangGraph 的优势就会非常明显。2.4 核心状态设计在 LangGraph 里整个流程的状态用一个 TypedDict 或 Pydantic 模型来定义。我的项目里状态包含以下几个关键字段question用户原始问题retrieved_docs本地知识库检索回来的文档列表evaluation评估节点的输出是一个 EvaluateSchema 对象web_resultsweb_query 返回的搜索结果final_context最终拼接到 Prompt 里的上下文answer模型生成的最终回答这个状态会在各个节点之间传递每个节点读取自己需要的字段写入自己产生的字段。LangGraph 会自动管理状态的合并和传递不需要手动处理。3. 核心细节EvaluateSchema 与 JSON 结构化输出3.1 为什么需要结构化输出评估节点的核心任务是让大模型判断“信息够不够”。如果让模型自由输出它可能会说“根据检索结果来看信息基本充足但有一些细节可能需要补充”——这种回答对人来说可以理解但对程序来说没法用。程序需要的是一个明确的布尔值来决定下一步走哪个分支。所以评估节点的输出必须是结构化的。我定义了一个 EvaluateSchema用 Pydantic 模型来描述from pydantic import BaseModel, Field class EvaluateSchema(BaseModel): is_sufficient: bool Field( description检索到的信息是否足以回答用户问题 ) reason: str Field( description判断理由说明为什么信息充足或不充足 ) missing_aspects: list[str] Field( default_factorylist, description如果信息不充足列出缺失的关键信息点 )这个 Schema 有三个字段。is_sufficient是核心判断结果reason用于调试和日志记录missing_aspects是一个可选的列表用来记录具体缺了什么信息。这个字段在后续的 web_query 环节很有用——它可以指导搜索关键词的生成。3.2 用 with_structured_output 约束模型输出LangChain 提供了with_structured_output方法可以直接把 Pydantic 模型绑定到 LLM 上让模型输出符合 Schema 的 JSON。用法大概是这样from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0) evaluator_llm llm.with_structured_output(EvaluateSchema)绑定之后调用evaluator_llm.invoke(prompt)返回的就是一个 EvaluateSchema 实例而不是纯文本。这比手动解析 JSON 要可靠得多因为底层会利用模型的 function calling 或 JSON mode 来保证输出格式。不过这里有个坑需要注意不是所有模型都支持 structured output。如果你用的是开源模型或者某些 API 兼容层可能不支持 function calling这时候with_structured_output会报错。遇到这种情况退而求其次的方案是在 Prompt 里明确要求模型输出 JSON然后用JsonOutputParser来解析。但这种方式可靠性会差一些需要加一些容错逻辑。3.3 评估 Prompt 的设计要点评估节点的 Prompt 设计直接决定了判断的准确性。我试过好几版 Prompt最后稳定下来的版本大概包含这几个部分第一明确角色和任务。告诉模型它是一个信息评估员任务是判断给定的文档片段是否足以回答用户问题。第二给出判断标准。什么算“充足”我的定义是文档中必须包含回答问题的所有关键信息点不需要模型进行推测或补充。如果文档只提供了部分信息或者需要模型自己推理才能得出结论都算不充足。第三给出输出格式的说明。虽然用了 structured output但在 Prompt 里再强调一遍输出字段的含义能提高判断质量。EVALUATE_PROMPT 你是一个信息评估专家。你的任务是判断以下检索到的文档片段 是否足以回答用户的问题。 判断标准 - 如果文档包含了回答问题的所有关键信息不需要额外推测则判定为充足 - 如果文档只包含部分信息或者需要模型自行推理补充则判定为不充足 - 如果文档与问题完全不相关则判定为不充足 用户问题{question} 检索到的文档 {docs} 请给出你的判断。3.4 JSON 解析的容错处理即使使用了 structured output在实际运行中仍然可能遇到解析失败的情况。比如模型返回的 JSON 格式有细微问题或者某些字段缺失。我的做法是在评估节点外面包一层 try-except解析失败时默认返回is_sufficientFalse让流程走 web_query 分支。这样即使评估环节出了问题最坏情况也只是多搜一次不会导致整个流程崩溃。注意默认走 web_query 而不是默认走生成这是一个重要的设计决策。宁可多搜一次也不要让模型在信息不足的情况下硬答。这个取舍在大多数场景下都是合理的。4. LangGraph 状态图编排节点、边与条件路由4.1 节点划分我的 LangGraph 流程包含四个核心节点retrieve 节点负责从本地向量库检索相关文档evaluate 节点判断检索结果是否充足web_query 节点当信息不足时触发外部搜索generate 节点基于最终上下文生成回答每个节点都是一个 Python 函数接收当前状态返回状态更新。LangGraph 会自动把返回值合并到全局状态里。from langgraph.graph import StateGraph, END from typing import TypedDict class RAGState(TypedDict): question: str retrieved_docs: list[str] evaluation: dict web_results: list[str] final_context: str answer: str def retrieve_node(state: RAGState) - dict: docs vectorstore.similarity_search(state[question], k5) return {retrieved_docs: [d.page_content for d in docs]} def evaluate_node(state: RAGState) - dict: result evaluator_llm.invoke( EVALUATE_PROMPT.format( questionstate[question], docs\n---\n.join(state[retrieved_docs]) ) ) return {evaluation: result.model_dump()} def web_query_node(state: RAGState) - dict: query state[question] missing state[evaluation].get(missing_aspects, []) if missing: query query .join(missing) results web_search_tool.invoke(query) return {web_results: results} def generate_node(state: RAGState) - dict: context_parts state[retrieved_docs] state.get(web_results, []) context \n---\n.join(context_parts) answer llm.invoke( GENERATE_PROMPT.format( questionstate[question], contextcontext ) ) return {answer: answer.content, final_context: context}4.2 条件路由的设计节点之间的流转由条件边控制。核心的条件判断在 evaluate 节点之后def route_after_evaluate(state: RAGState) - str: if state[evaluation][is_sufficient]: return generate else: return web_query graph.add_conditional_edges( evaluate, route_after_evaluate, { generate: generate, web_query: web_query } )这个路由逻辑很直白信息充足就直接生成不充足就去搜索。web_query 节点执行完之后直接连到 generate 节点不再做二次评估。这样做是为了控制延迟——多一次评估就多一次 LLM 调用在大多数场景下一次 web_query 已经能补足信息了。4.3 状态图的完整组装把所有节点和边组装起来workflow StateGraph(RAGState) workflow.add_node(retrieve, retrieve_node) workflow.add_node(evaluate, evaluate_node) workflow.add_node(web_query, web_query_node) workflow.add_node(generate, generate_node) workflow.set_entry_point(retrieve) workflow.add_edge(retrieve, evaluate) workflow.add_conditional_edges( evaluate, route_after_evaluate, {generate: generate, web_query: web_query} ) workflow.add_edge(web_query, generate) workflow.add_edge(generate, END) app workflow.compile()编译之后调用app.invoke({question: ...})就能跑完整个流程。LangGraph 会自动处理状态传递和节点调度。4.4 web_query 工具的实现web_query 节点需要一个搜索工具。我用的是一个通用的搜索 API 封装核心逻辑是接收查询字符串返回搜索结果列表。这里不展开具体 API 的细节重点说一下查询构造的策略。直接拿用户原始问题去搜索效果往往一般。因为用户问题可能是口语化的、包含上下文的而搜索引擎更喜欢关键词明确的查询。我的做法是把用户问题和评估节点输出的missing_aspects拼接起来组成一个更精确的搜索查询。比如用户问“LangGraph 怎么做条件路由”评估发现缺少具体代码示例missing_aspects 是[代码示例, add_conditional_edges 用法]那搜索查询就变成“LangGraph 条件路由 代码示例 add_conditional_edges 用法”。这样搜出来的结果针对性会强很多。5. 实操过程从零搭建一个带判断力的 RAG5.1 环境准备与依赖安装先把基础环境搭起来。我用的 Python 版本是 3.11主要依赖包括 langchain、langgraph、langchain-openai、chromadb 和 pydantic。安装命令pip install langchain langgraph langchain-openai chromadb pydantic如果你用的是其他向量数据库把 chromadb 换成对应的客户端库就行。Embedding 模型我用的是 OpenAI 的 text-embedding-3-small性价比高效果也够用。5.2 知识库准备与向量化知识库的内容我放在一个 docs 目录下每个文档是一个 markdown 文件。加载和向量化的代码from langchain_community.document_loaders import DirectoryLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma loader DirectoryLoader(./docs, glob**/*.md) documents loader.load() splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50 ) chunks splitter.split_documents(documents) embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vectorstore Chroma.from_documents(chunks, embeddings, persist_directory./chroma_db)chunk_size 设成 500 是我试出来的经验值。太小了信息不完整太大了检索精度下降。chunk_overlap 设 50 是为了避免关键信息被切断。5.3 评估节点的完整实现评估节点的完整代码包括 Prompt 模板、LLM 绑定和容错处理from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field class EvaluateSchema(BaseModel): is_sufficient: bool Field(description信息是否充足) reason: str Field(description判断理由) missing_aspects: list[str] Field(default_factorylist, description缺失的信息点) evaluator_llm ChatOpenAI( modelgpt-4o-mini, temperature0 ).with_structured_output(EvaluateSchema) def evaluate_node(state: RAGState) - dict: docs_text \n---\n.join(state[retrieved_docs]) if not docs_text.strip(): return { evaluation: { is_sufficient: False, reason: 未检索到任何文档, missing_aspects: [state[question]] } } try: result evaluator_llm.invoke( EVALUATE_PROMPT.format( questionstate[question], docsdocs_text ) ) return {evaluation: result.model_dump()} except Exception as e: return { evaluation: { is_sufficient: False, reason: f评估失败: {str(e)}, missing_aspects: [] } }这里有两个细节值得注意。第一如果检索结果为空直接返回不充足不需要调用 LLM省一次调用。第二异常处理里默认返回不充足保证流程不会因为评估失败而中断。5.4 生成节点的 Prompt 设计生成节点的 Prompt 需要明确告诉模型上下文里既有本地知识库的内容也有外部搜索的结果请综合这些信息来回答。同时要强调如果上下文里确实没有相关信息就如实说“根据现有信息无法回答”不要编造。GENERATE_PROMPT 请基于以下上下文信息回答用户问题。 要求 1. 只使用上下文中提供的信息不要编造 2. 如果上下文信息不足以回答问题请如实说明 3. 回答要简洁准确引用具体内容 上下文 {context} 用户问题{question} 回答5.5 完整流程的调用与测试组装好之后跑一个测试result app.invoke({ question: LangGraph 的 checkpointer 怎么配置, retrieved_docs: [], evaluation: {}, web_results: [], final_context: , answer: }) print(result[answer]) print(评估结果:, result[evaluation])如果知识库里有相关内容评估节点会返回is_sufficientTrue直接生成。如果没有会触发 web_query搜索结果会拼接到上下文里再生成回答。通过打印evaluation字段可以看到判断的理由和缺失的信息点方便调试。6. 常见问题与排查技巧实录6.1 评估节点总是判断“不充足”这是最常见的问题。可能的原因有几个一是 Prompt 里的判断标准太严格模型倾向于保守判断二是检索回来的文档质量确实不高信息不完整三是 chunk_size 设置不合理关键信息被切散了。排查方法先把评估节点的reason字段打印出来看看模型给的理由是什么。如果理由是“文档只提到了概念但没有具体步骤”那说明检索结果确实不够需要优化知识库或调整 chunk 策略。如果理由是“文档与问题无关”那可能是 Embedding 检索出了问题检查一下相似度阈值和 Top-K 设置。我的经验是把判断标准从“必须包含所有关键信息”放宽到“包含回答问题的核心信息即可”能显著减少误判。毕竟 RAG 的目标是辅助生成不是要求文档完美覆盖所有细节。6.2 web_query 返回的结果质量差外部搜索的结果质量取决于搜索查询的构造。如果直接拿用户原始问题去搜效果往往不好。我的优化策略是用评估节点输出的missing_aspects来构造搜索查询把缺失的信息点作为关键词拼进去。另外可以在搜索工具里加一个结果过滤逻辑只保留和问题相关性高的前几条结果避免噪声太多。还有一个技巧如果 web_query 返回的结果为空或者质量太差可以在生成节点的 Prompt 里加一个降级指令让模型基于已有的本地检索结果尽量回答同时明确告知用户“部分信息可能不完整”。6.3 JSON 解析失败的排查虽然用了 structured output但偶尔还是会遇到解析失败。常见原因包括模型返回的 JSON 被截断输出 token 超限、字段类型不匹配比如is_sufficient返回了字符串 true 而不是布尔值、模型没有按照 Schema 输出。排查步骤第一打印原始返回内容看看模型到底输出了什么。第二检查 max_tokens 设置是否足够。第三如果用的是 JsonOutputParser 而不是 structured output检查 Prompt 里的格式说明是否清晰。第四在解析失败时加一个重试逻辑换一个更明确的 Prompt 再试一次。6.4 流程延迟过高加入评估节点和 web_query 之后整个流程的延迟会增加。评估节点多了一次 LLM 调用web_query 多了一次网络请求。如果对延迟敏感可以考虑几个优化用更小的模型做评估比如 gpt-4o-mini 而不是 gpt-4o、把评估和检索并行化先检索同时评估上一轮的结果、对 web_query 的结果做缓存。提示评估节点的 LLM 调用可以用 temperature0 来保证判断的稳定性同时选择响应速度快的模型。评估任务本身不复杂小模型完全够用。6.5 常见问题速查表问题现象可能原因排查方法解决方案评估总是返回不充足Prompt 标准过严 / 检索质量差打印 reason 字段放宽判断标准 / 优化 chunk 策略web_query 结果质量差搜索查询构造不合理检查查询字符串用 missing_aspects 拼接关键词JSON 解析失败输出截断 / 格式不符打印原始输出增加 max_tokens / 加重试逻辑流程延迟高多次 LLM 调用统计各节点耗时用小模型评估 / 加缓存生成节点仍然编造Prompt 约束不够检查生成 Prompt强化“不要编造”的指令7. 几个我踩过的坑和实战心得第一个坑是关于评估节点的位置。我一开始把评估节点放在检索之前想先判断问题需不需要检索。后来发现这样不行因为不检索就不知道知识库里有什么评估缺乏依据。正确的做法是先检索再基于检索结果做评估。第二个坑是关于 web_query 的触发条件。我最初的设计是只要评估不充足就触发搜索但实际运行中发现有些问题本身就很模糊评估节点判断不充足是因为问题不清楚而不是信息不够。这种情况下搜索也搜不到有用的东西。后来我加了一个判断如果missing_aspects为空说明评估节点自己也说不清缺什么这时候不触发搜索直接走生成让模型基于现有信息尽量回答。第三个坑是关于状态字段的命名。LangGraph 的状态是全局共享的字段命名要尽量明确避免歧义。我一开始用docs表示检索结果后来发现和 web 搜索结果容易混淆改成了retrieved_docs和web_results清晰很多。最后一个心得评估节点的 Prompt 值得反复打磨。这个节点的判断质量直接决定了整个流程的效果。我前后改了五六版 Prompt每版都拿一批测试问题跑一遍看判断准确率。建议你也建一个小的测试集包含“信息充足”和“信息不足”两类问题用来验证评估节点的表现。这个方案后续还可以继续扩展。比如在 web_query 之后再加一个二次评估节点确认补充的信息是否足够或者加入多轮搜索逻辑第一次搜不到就换个关键词再搜再或者把评估结果缓存起来同样的问题不用重复判断。这些扩展都可以在 LangGraph 的状态图里平滑地加进去不会破坏现有的流程结构。
返回列表