基于LangGraph从零构建Agentic RAG:路由、评分与反思机制实战
2026/9/24 23:36:34 网站建设 项目流程

先别急着上 RAG,我们先把话说清楚:传统 RAG 的毛病不在检索,而在“没有判断”。用户问什么就检索什么,检索到什么就拼进 Prompt,结果是啥就生成啥——整个过程是个单行道,错了也不知道,问了上下文也不接。这次我用 LangGraph 从零搭一个会路由、会改写、会评分、会反思重检索的 Agentic RAG,把“检索-生成”这条死链变成“决策-行动-验证-修正”的闭环。代码全部给出,思路和踩坑也一并说透。

这篇东西适合两类人:一类是已经做过 Naive RAG,被“检索质量差、多轮对话接不上、答非所问”折腾过的人;另一类是只听说过 LangGraph,想知道这玩意儿跟 LangChain 到底啥区别、怎么用来做正经事的人。目标是你看完之后,能自己把这套架构落到项目里。

1. 死板 RAG 死在哪,Agentic RAG 到底解决了什么

1.1 传统 RAG 的三个硬伤

我见过太多把 RAG 当“救世主”的项目,上线两周就发现效果跟 demo 差一倍。问题不是 embedding、不是向量库,而是流程本身有硬伤。

第一,无差别检索。不管用户问的是“你叫什么名字”还是“帮我看看 2025 年 Q3 财报里关于现金流的部分”,系统都先去向量库捞一遍。可有些问题压根不需要检索本地知识库——比如查天气、查实时新闻,本地库里根本没有;有些问题只需要常识就能回答,检索反而引入噪音。传统 RAG 没有“先判断一下该不该检索”这个环节。

第二,检索错了没有纠错机制。向量检索的 Top-K 结果里混进一两个不相关的文档,太常见了。传统 RAG 的做法是照单全收,全部塞给大模型。模型如果被干扰,就会答出“看似合理实则错误”的内容。更麻烦的是,它不会自己发现问题——没有验证环节,没有反思机制。

第三,多轮对话里一问就懵。用户上一句问“你们公司的退款政策是什么”,下一句问“那如果超过 30 天呢”,传统 RAG 会把第二句原文拿去检索,“超过 30 天”这种指代不明的 query,向量检索效果直接崩。你得自己去做 query 改写、上下文拼接,而这些工作量在传统流水线里完全没有官方解法。

1.2 Agentic RAG 的核心:把“检索”变成“决策循环”

Agentic RAG 的思路是把 RAG 从一条线性流水线改造成一个“决策循环”。它不再回答“检索→生成”这种固定指令,而是让语言模型充当智能体的核心,自己决定下一步干什么。

具体到我这套方案里,循环长这样:

  • 路由节点先判断:这个问题该走 Web 搜索,还是该走本地知识库检索
  • 如果走本地检索,先做 query 改写,把多轮上下文、指代词还原成完整的独立问题
  • 检索之后不急着生成,先让一个“评审员”给每篇文档打分,过滤掉不相关的
  • 如果过滤完一篇都不剩,说明检索失败,触发反思逻辑:重新改写 query,再来一轮检索
  • 生成答案之后还可以再加一层质量校验,检查答案是否基于证据、是否回答了用户问题
  • 全部通过,才把答案返回给用户

这套东西的学名叫 Corrective RAG / Self-RAG,核心就一句话:系统对自己输出的质量要有感知,感知到不行就自己纠正。

1.3 技术选型:为什么选 LangGraph 而不是裸 LangChain

LangChain 本身也能串 RAG 流程,LCEL 用|符号把组件串起来,写 demo 非常爽。但它有几个硬伤:没有原生循环结构,条件分支写起来非常别扭,状态管理靠你自己塞进 context。Agentic RAG 的核心恰恰是“循环”和“条件跳转”,用 LangChain 的链式写法只能写出 if-else 套 if-else 的屎山。

LangGraph 解决的就是这个问题。它是“有状态、可循环、可条件跳转”的图编排框架,本质上是个图状态机。你可以把整个流程画成一个图:节点是函数或工具,边是流转关系,条件边是根据状态决定下一步走哪、是否回溯。状态由一个全局 State 对象维护,每个节点读状态、改状态,然后往下传。

我做这个需求的时候对比过 LangChain 和 LangGraph 的代码量:同样的“检索评分不过就重写 query 再检索”,LangChain 要额外写状态类、循环逻辑、手动管理上下文,能跑,但代码可读性很差;LangGraph 里只需要定义好节点,然后用add_conditional_edges连一条回到改写节点的边,多级反思循环几行搞定。

提示:LangGraph 不是 LangChain 的替代品,而是它的编排层。LangGraph 的节点内部照样用 LangChain 的 Retriever、Tool、PromptTemplate,两者是配合关系,不是竞争关系。这也是社区里问“LangChain 和 LangGraph 区别”的标准答案:LangChain 管的是“用什么组件”,LangGraph 管的是“组件怎么流转”。

2. 整体方案设计:一个会“反思”的图

2.1 状态定义:GraphState 是贯穿全流程的“黑板书”

LangGraph 里最重要的概念就是 State。你可以把它想象成一块挂在墙上的黑板,每个节点在上面读信息、写信息,下一个节点接着读。Agentic RAG 的所有决策依据都在这块黑板上。

我这套架构的 State 定义如下:

from typing import TypedDict, Annotated, List, Literal from langgraph.graph.message import add_messages class GraphState(TypedDict): # 多轮对话消息,用 add_messages 注解让 LangGraph 自动合并历史 messages: Annotated[list, add_messages] # 当前需要处理的问题,可能被改写 question: str # 用户最初输入的原问题,用于最终生成时还原语境 original_question: str # 检索到的文档列表 documents: List[str] # 最终生成答案 generation: str # 路由结果:web_search 或 retrieve search_type: str # 记录反思/重写次数,防止死循环 iteration: int # 记录是否已经重写过一次 rewritten: bool

messages前面加了Annotated[list, add_messages],这是 LangGraph 做状态合并的关键,加了之后每次节点返回新消息时不是覆盖,而是追加。多轮对话的上下文就是靠这个累积起来的。

iteration字段我现在就埋下,后面做反思循环的兜底,防止节点在“重写-检索-评分不过-再重写”里无限转圈。

2.2 节点与条件边:画出完整的控制流

我先把图的结构画出来,不画箭头图了,直接文字描述,你脑子跟着走一遍:

  1. 入口是route节点,输入用户问题,输出路由决策
  2. 条件边判断:决策是web_search就走 Web 搜索节点,搜完直接进generate生成答案;决策是retrieve就走本地知识库路线
  3. 本地路线先过rewrite_query节点,做多轮上下文改写,生成完整 query
  4. retrieve节点拿着改写后的 query 去向量库检索,取回 Top-K 文档
  5. grade_documents节点对每篇文档打分,标注 yes/no 是否相关,过滤掉 no 的
  6. 条件边判断:过滤后还有文档,就进generate生成答案;一篇都没剩下,就说明检索失败,回到rewrite_query重新改写、重新检索
  7. generate节点拿着过滤后的合格文档生成最终答案

注意第 6 步,这就是“反思”和“纠错”落地的关键点。传统 RAG 检索到垃圾就当垃圾吃了,这里我们会让系统自己发现“这些文档不行”,然后调整策略再来一轮。第二次改写时我会让模型知道“上次检索没找到,请换一种表达方式”,而不是用同样的话再检索一遍。

2.3 这套设计比传统流水线多了哪些“智能”

我把关键差异列成表格,你感受一下:

能力维度传统 RAGAgentic RAG
问题分类无差别全走向量检索路由节点判断走 Web 还是走本地库
多轮上下文需外部拼接历史query 改写节点主动补全指代
检索质量Top-K 照单全收文档级评分,逐篇过滤
错误恢复无,答错就错评分全不过时重写 query 再检索
可观测性黑盒每个节点状态可查,可用graph.get_state()调试
可扩展性改流程要改代码加节点、加条件边即可扩展

这套设计本质上把大模型从“生成器”升级成了“控制器”。生成答案只是其中一个小节点,真正的智能体现在路由判断、质量评审、纠错决策这些环节上——每个决策点都是一个 LLM 调用,但每个调用的职责都非常单一,所以不会失控。

3. 手把手实现:核心节点代码逐段拆解

3.1 环境准备与依赖

老规矩,先把依赖装好。Python 3.10 以上,建议用 3.11。我自己的项目用 uv 管理依赖,你直接用 pip 也行。

pip install langgraph langchain langchain-openai langchain-community langchain-chroma python-dotenv

版本我建议直接装最新的,LangGraph 0.2+ 的 API 才算稳定。如果你用StateGraph的初始化方式,注意 0.2 版本后它的用法有变化,我下面代码按新版写。

模型配置用环境变量管理:

import os from dotenv import load_dotenv load_dotenv() from langchain_openai import ChatOpenAI, OpenAIEmbeddings # 主模型:负责生成最终答案,温度低一点保证事实性 llm = ChatOpenAI( model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), temperature=0.1, ) # 快速模型:负责路由、改写、评分,这些任务对创造力要求低,用快模型省钱省时间 fast_llm = ChatOpenAI( model=os.getenv("FAST_MODEL", "gpt-4o-mini"), temperature=0, ) embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

提示:路由、改写、评分这类“小决策”任务,我统一用fast_llm跑,只有最终答案用主模型。实测如果全部用 GPT-4o,一次反思循环的成本能高一倍多。这个习惯建议你从一开始就养成,Agentic 系统里决策点是耗 token 大户。

3.2 构建本地知识库:先把 RAG 的“弹药”备好

为了演示完整性,我先快速建一个本地知识库。假设你有一批公司政策文档,用 Chroma 当向量库。

from langchain_chroma import Chroma from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter # 加载文档,这里以 txt 为例,pdf/csv 用对应 loader loader = TextLoader("./knowledge_base/company_policy.txt", encoding="utf-8") docs = loader.load() # 切分:按段落切,chunk_size 取 500 是经验值 # 太小容易丢上下文,太大会让评分和生成都变慢 splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?", " "], ) chunks = splitter.split_documents(docs) vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db", ) retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

k=4是我试过的平衡点。k 太大会混入大量噪音,评审员压力大;k 太小又可能漏掉关键文档。实际项目中你可以根据文档库的规模和评分结果动态调,我给新手先用 4。

3.3 路由节点:让系统先想清楚“该不该检索”

路由节点是整个 Agentic RAG 的第一道闸门。它的职责只有一个:判断这个 query 应该走 Web 搜索通道,还是走本地知识库通道。

def route_question(state: GraphState) -> dict: question = state["question"] prompt = f"""你是一个智能路由助手。根据用户的问题,判断应该使用哪种检索方式。 用户问题:{question} 可选通道: - web_search:问题涉及实时信息(天气、新闻、股价)、最新事件、本地知识库之外的常识性问题 - retrieve:问题可以在本地知识库中找到答案,比如公司政策、产品文档、内部资料 你只需输出一个词:web_search 或 retrieve。不要输出任何其他内容。""" response = fast_llm.invoke(prompt) search_type = response.content.strip().lower() # 做一个格式兜底,防止模型输出多余内容 if "web_search" in search_type: search_type = "web_search" elif "retrieve" in search_type: search_type = "retrieve" else: search_type = "retrieve" print(f"[路由] 判断结果: {search_type}") return {"search_type": search_type}

这个节点最关键的地方是 Prompt 里给出了明确的判断标准。我见过很多人写路由 Prompt 就一句“请判断用户意图”,模型理解不一致,输出格式也千奇百怪。把标准写清楚、把输出格式限制死,决策质量会高很多。

路由之后的条件边是这么连的:

def decide_route(state: GraphState) -> str: return state["search_type"] # 在构建图时使用 graph.add_conditional_edges( "route", decide_route, { "web_search": "web_search", "retrieve": "rewrite_query", }, )

条件边的机制:decide_route返回一个字符串,LangGraph 用这个字符串去匹配第三个参数字典的 key,然后决定下一步走哪个节点。这里的retrieve对应本地检索路线,但我们不直接去retrieve,而是先去rewrite_query做一轮上下文改写。

3.4 查询改写:解决多轮对话的“指代错乱”

多轮对话里最经典的坑是:“那退款政策呢?” 这种 query 不给上下文,谁也检索不明白。改写节点要做的就是把这种指代不清的问题还原成完整表述。

def rewrite_query(state: GraphState) -> dict: question = state["question"] messages = state.get("messages", []) # 把最近几轮对话拼出来作为上下文 history_text = "\n".join( f"{'用户' if i % 2 == 0 else '助手'}: {m.content}" for i, m in enumerate(messages[-4:]) # 只看最近两轮,太多反而干扰 ) prompt = f"""你是对话理解助手。根据对话历史,将用户最新问题改写为可以独立检索的完整问题。 要求: 1. 补全指代词(他、它、这个、那个等)的具体含义 2. 保留原问题所有关键信息 3. 如果原问题已经很完整,直接输出原问题 4. 只输出改写后的问题,不要解释 对话历史: {history_text} 用户最新问题:{question} 改写后的问题:""" rewritten = fast_llm.invoke(prompt).content.strip() print(f"[改写] {question} -> {rewritten}") return {"question": rewritten}

我设置只看最近两轮对话,这是个经验值。看多了之后,历史里的噪音会干扰改写判断,而且 token 消耗也会上升。在多数企业知识库场景,用户最近一两轮的问题就足够补全上下文了。

3.5 检索与 Web 搜索节点

本地检索节点本身很朴素,就是拿改写后的 query 去打向量库。但注意,我把检索结果先放进documents字段,而不是直接拼进 Prompt——因为后面还有评分节点要逐篇审。

def retrieve(state: GraphState) -> dict: question = state["question"] docs = retriever.invoke(question) doc_contents = [doc.page_content for doc in docs] print(f"[检索] 获取到 {len(doc_contents)} 篇文档") return {"documents": doc_contents}

Web 搜索节点我用 Tavily Search API,它是目前 LangChain 生态里集成最顺手的搜索工具,返回结构化结果,免费额度够做开发和测试。

from langchain_community.tools.tavily_search import TavilySearchResults web_search_tool = TavilySearchResults(max_results=5) def web_search(state: GraphState) -> dict: question = state["question"] results = web_search_tool.invoke({"query": question}) # 把搜索结果拼接成文档格式 docs = [] for r in results: content = r.get("content", "") docs.append(content) print(f"[Web搜索] 获取到 {len(docs)} 条结果") return {"documents": docs, "search_type": "web_search"}

3.6 文档相关性评分:给检索结果当“质检员”

这是整套系统最核心的节点。检索完不直接生成,先让一个评审 LLM 逐篇判断文档和问题的相关性。这一步把传统 RAG 的“静默错误”变成了“显式判断”。

def grade_documents(state: GraphState) -> dict: question = state["question"] documents = state["documents"] filtered_docs = [] for doc in documents: prompt = f"""你是文档相关性评审员。判断以下文档是否与用户问题相关。 用户问题:{question} 文档内容: {doc[:800]} # 评分不需要看全文档,截断省 token 如果文档包含可以回答用户问题的关键信息,输出 "yes";否则输出 "no"。 只输出一个词。""" response = fast_llm.invoke(prompt).content.strip().lower() print(f"[评分] {'相关' if response == 'yes' else '不相关'}") if response == "yes": filtered_docs.append(doc) # 如果过滤后一篇不剩,把当前轮次加一,触发反思条件边 if len(filtered_docs) == 0: print("[反思] 所有文档均不相关,触发重新改写...") return { "documents": [], "iteration": state.get("iteration", 0) + 1, "rewritten": True, } return {"documents": filtered_docs}

注意这里我对原文做了截断.doc[:800],评论任务根本不需要全文,看开头部分就足够判断了。这个优化能让评分节点成本降一半。

评分之后的条件边是反思循环的开关:

def decide_after_grade(state: GraphState) -> str: if len(state.get("documents", [])) == 0: # 已经重写过一轮还是没找到,说明知识库可能真没有 if state.get("iteration", 0) >= 2: return "generate" # 兜底:硬着头皮生成,但告诉用户可能没有答案 return "rewrite_query" # 回去改写,换个姿势再检索 return "generate" # 有文档,生成答案

这里iteration >= 2是防死循环的保险丝。反思是好事,但无限反思就是灾难。我已经数不清有多少次调 Bug 时看到 agent 在同一个循环里转了几十圈,token 烧得哗哗的。给反思加次数限制,是 Agentic 系统上线的必备操作。

3.7 生成答案节点

生成节点用的是主模型,把过滤后的文档拼进 Prompt,要求模型严格基于文档回答,不知道就说不知道。

def generate(state: GraphState) -> dict: question = state["question"] documents = state.get("documents", []) # 文档可能为空(反思次数用完仍没找到),这时直接告诉用户没找到 if not documents: response = ("我尝试检索了知识库,但没有找到与你的问题明确相关的内容。" "建议你换一种表述方式,或者补充更多关键词。") return {"generation": response} docs_text = "\n\n---\n\n".join(documents) prompt = f"""你是一个严谨的问答助手。请仅根据以下参考资料回答用户问题。 规则: 1. 如果参考资料中包含答案,请准确回答,并在答案中标明信息来源文档 2. 如果参考资料中不包含答案,请直接说"根据现有资料无法回答",不要编造 3. 回答要简洁、准确,不要重复参考资料中的无关内容 参考资料: {docs_text} 用户问题:{question} 你的回答:""" response = llm.invoke(prompt).content print(f"[生成] 输出长度 {len(response)} 字") return {"generation": response}

这里有个细节:如果反思轮次用完了还是一篇文档都没有,我不会硬编答案,而是返回“没有找到相关内容”。对 RAG 系统来说,诚实地承认不知道,比一本正经地胡说八道要好一百倍。

3.8 组装整张图:编译可用

所有节点定义好后,组装成图:

from langgraph.graph import StateGraph, END def build_agentic_rag_graph(): graph = StateGraph(GraphState) # 注册所有节点 graph.add_node("route", route_question) graph.add_node("web_search", web_search) graph.add_node("rewrite_query", rewrite_query) graph.add_node("retrieve", retrieve) graph.add_node("grade_documents", grade_documents) graph.add_node("generate", generate) # 入口:路由判断 graph.set_entry_point("route") # 路由条件边 graph.add_conditional_edges( "route", decide_route, { "web_search": "web_search", "retrieve": "rewrite_query", }, ) # Web 搜索路线:搜完直接生成 graph.add_edge("web_search", "generate") # 本地检索路线 graph.add_edge("rewrite_query", "retrieve") graph.add_edge("retrieve", "grade_documents") # 评分后的条件边:这是反思循环的核心 graph.add_conditional_edges( "grade_documents", decide_after_grade, { "rewrite_query": "rewrite_query", # 不满意的又一次改写+检索 "generate": "generate", }, ) # 生成完结束 graph.add_edge("generate", END) return graph.compile() agentic_rag = build_agentic_rag_graph()

到这里,一个能自动转圈的 Agentic RAG 已经跑起来了。编译后的对象可以像函数一样调用:

result = agentic_rag.invoke({ "question": "公司的退款政策是什么?", "messages": [], "iteration": 0, }) print(result["generation"])

4. 全流程实测与参数调优心得

4.1 三种典型场景实测记录

我拿三组不同的问题测了一下,把现场输出贴出来供参考。

第一组,本地知识库问题:问面“公司的年假政策是怎么规定的?”

路由判断是retrieve。改写环节因为问题本身完整,没有改动。检索拿到 4 篇文档,评分时只有 2 篇判为相关,过滤后的内容包括“年假天数按工龄分三档”等关键信息。生成结果直接引用这两篇文档的信息,答得干脆。这个案例里最能体现价值的是:如果没有评分节点,另外 2 篇不相关文档会被强塞进 Prompt,答案的可信度会打折。

第二组,多轮指代问题。用户先说“我想了解弹性工作制”,然后接着问“那它的申请流程呢?”

如果没有改写节点,第二句话直接检索,效果大概率稀碎。加了改写之后,模型输出的改写结果是“弹性工作制度的申请流程是什么”,检索结果立刻精准。再往后,如果用户说“审批一般多久”,前面两轮上下文还在,改写节点会把它补全成“弹性工作制的审批一般需要多久”。

第三组,故意刁难型问题。我拿一个知识库里完全不存在的主题去问:“公司的宠物殡葬服务政策是什么?”

路由正确走了retrieve。检索到的 4 篇文档里,评分全员判 no。于是触发反思环,系统带着“上次没找到,请换一种说法”的指令重新改写,结果改成了“宠物丧葬 相关制度”,又检索一轮,还是全 no。迭代次数到 2,兜底逻辑启动,生成节点返回“没有找到相关内容”。整个过程自动完成,没有死循环,没有编造答案,行为完全符合预期。

4.2 参数调优:哪些旋钮决定了系统智商

Agentic RAG 的上限取决于模型能力,但下限取决于参数调得好不好。我总结几个影响最大的参数。

温度。路由和评分节点必须把温度设为 0,这两个节点需要的是确定性输出,一点随机性都不该有。改写节点可以给 0.1-0.2,太低了改写缺乏灵活度,太高了容易乱编。生成节点给 0.1 左右,既保证事实性,又不至于完全机械。

迭代上限。反思循环里的max_iteration我建议设成 2 或 3。设 1 等于没反思,出一次问题就硬着头皮上。设 5 以上,一旦知识库确实没有答案,系统就会白白转好几圈烧钱。2 是“给了你一次纠错机会”的合理值。

检索 Top-K。K 值直接影响评分节点的负担。K=4 是我测下来比较稳的,如果知识库很杂、噪音多,降到 3 更稳;如果知识库很干净、文档切分质量高,可以升到 5-6。

4.3 知识库切分的隐藏学问

很多人调了半天 Prompt 没效果,最后发现是文档切分的问题。我再分享一个经验:切分器的separators顺序很重要,我建议带上中文标点。

splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?", ";", " "], )

这是因为 RecursiveCharacterTextSplitter 会按顺序尝试分隔符,如果你不写中文标点,一个段落几百字都会被硬切一刀,很可能切断关键语义。加了中文句号后,切分点会优先选在语义完整的位置。

5. 常见问题与避坑实录

5.1 死循环:反思的次数失控了

这是 Agentic RAG 上线初期最经典的事故。症状是调用链不停地转圈,每次调用都是评分不过、回去改写、检索、再评分,用户的提问被搁置几分钟,token 消耗像流水一样失控。

排查方法:第一,检查decide_after_grade里的兜底条件是否覆盖所有路径。我见过有人写了if iteration >= 2: return "generate",但忘记在rewrite_query节点里递增iteration,导致条件永远不满足。第二,用 LangGraph 自带的调试能力,graph.get_state(config)可以查看每个节点执行后的状态,直接看iteration字段有没有变化。

我的建议是:在grade_documents返回的时候无条件递增iteration,而不是只在不相关的时候递增。这样即使其他路径出问题,保险丝也能起作用。

5.2 小模型路由输出格式不稳定

路由节点用fast_llm时容易遇到一个问题:模型偶尔不老老实实输出一个词,而是输出一整句话,比如“根据您的询问,我认为应该使用 retrieve 通道”。

解决办法是在解析时做容错。代码里我用了 substring 匹配的方式而不是精确匹配,if "web_search" in search_type而不是if search_type == "web_search"。这个看似不起眼的处理,能吃掉 90% 的格式问题。

如果还不行,就在 Prompt 里加一句“这是一个极其简单的二选一任务,直接输出词,不要礼貌,不要解释”。语气写得强硬一点,模型会更听话。

5.3 多轮对话还是接不上:你大概率忘了传历史

很多人把 LangGraph 代码抄过去了,路由、评分、反思都跑通了,但多轮对话依然接不上。问题往往出在调用方式上——每次invoke只传了question,没传messages

正确用法是把整个对话历史传进 state:

# 第一次调用 state1 = agentic_rag.invoke({"question": "什么是弹性工作制?", "messages": []}) # 第二次调用,把上一轮消息追加进 messages messages = [ {"role": "user", "content": "什么是弹性工作制?"}, {"role": "assistant", "content": state1["generation"]}, ] state2 = agentic_rag.invoke({"question": "申请流程是什么?", "messages": messages})

如果你做 Web 应用,会话历史一般存在 Redis 或数据库里,每次请求时把历史捞出来拼进messages字段即可。

5.4 检索评分误杀率太高:好文档被当成垃圾

评分节点把相关文档判成不相关,这也很常见。原因一般有两个:一是 Prompt 里的相关标准写得太严,二是截断后关键信息恰好被切掉了。

解决办法:第一,评分 Prompt 里把“相关”定义放宽一点,改成“文档内容可以作为回答该问题的参考依据”。第二,截断长度从 800 增加到 1500,牺牲一点 token 成本,换取判断准确率。第三,实在不行可以把system换成更强、更聪明的模型,比如从 min 换成标准版,评分准确性会立刻上一个台阶。

5.5 扣子(Coze)是不是用 LangGraph 实现的

这个问题在社区里被问得挺多,我也被读者私信问过几次。我给个保守的回答:扣子这类低代码工作流平台,产品形态上确实和 LangGraph 非常像——画布上拖节点、连边、设置条件跳转,这本身就是“可视化 Agent 编排”。但你不能说它就是 LangGraph 做的,字节跳动自研工作流引擎的概率远大于直接套用开源框架。

对开发者的实际意义在于:如果你在扣子上玩过工作流,理解 LangGraph 的 node/edge/条件边会非常快,因为套路完全一样。反过来,你在 LangGraph 上趟过的坑(死循环、状态管理、条件冲突)在扣子上也会以相似的形态出现。概念是通用的,工具只是载体。

5.6 LangChain 和 LangGraph 到底怎么分工

再总结一次,因为这个问题我看到太多人搞混了。LangChain 提供的是组件库:LLM 封装、Prompt 模板、Retriever、Tool、Output Parser。LangGraph 提供的是编排能力:状态机、条件跳转、循环、持久化、人工审核。

在实际项目里,你不必二选一。我自己的习惯是:LangGraph 搭骨架,节点内部全用 LangChain 的组件。比如retriever是 LangChain 的,llm是 LangChain 的,web_search_tool是 LangChain 的,但它们的流转关系完全由 LangGraph 控制。这就够了。

最后再分享一点经验

这套 Agentic RAG 我从 0.1 版本迭代到现在,回头看最有价值的设计不是反思循环,是“评审节点”的存在。它把 RAG 从“尽全力从噪声里找答案”变成了“先确认什么是噪声,再回答问题”。很多看起来是检索质量的问题,本质上是行程缺乏质量意识——先把检索结果当成待验证的假设,用一次廉价的 LLM 调用快速验证,再决定走哪条路,这个思路能迁移到很多 Agent 场景里。

代码里我用的是最简单的字符串状态和条件边,LangGraph 底层还有 Checkpointer、Human-in-the-loop、Message 级别的状态管理,等你的场景需要人工审批、断点续跑的时候再升级也不迟。先把手里的模型、向量库、检索器拼出一个能自我纠错的闭环,比什么都强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询