ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LangChain、LangGraph与Deep Agents:构建可上线复杂Agent系统的三层架构实践

LangChain、LangGraph与Deep Agents:构建可上线复杂Agent系统的三层架构实践

1. 从"能跑通"到"能上线":这套组合拳到底在解决什么问题

第一次接触 LangChain 的时候,我跟大多数人一样,觉得这东西就是个"把大模型调用封装得更优雅"的库。写个LLMChain,套个PromptTemplate,再接个OutputParser,跑通了就觉得自己会了。直到真正拿它去做一个需要多轮决策、需要调用外部工具、需要在失败时自动重试的项目,才发现单靠 LangChain 那套链式结构,代码会迅速膨胀成一团乱麻——条件分支嵌套、状态传递靠全局变量、调试的时候根本不知道是哪一步出了问题。

后来 LangGraph 出来了,再后来 Deep Agents 这个概念也被提了出来。我把这三样东西放在一起用了一段时间,踩了不少坑,也总结出了一些真正能落地的经验。这篇文章就是把这些东西掰开揉碎讲清楚:LangChain 负责什么、LangGraph 补上了什么、Deep Agents 又在这个基础上加了什么,以及在实际项目里怎么把它们组合起来用。

先说结论性的判断,方便你决定要不要往下读:

  • LangChain是基础层,解决的是"和大模型对话、解析输出、调用工具"这些原子操作的标准接口问题。它的价值在于生态和抽象,不在于它本身有多复杂。
  • LangGraph是编排层,解决的是"多个步骤之间怎么流转、状态怎么管理、出错怎么回退"的问题。它把 Agent 的执行过程建模成一张有向图,每个节点是一个操作,边是流转条件。
  • Deep Agents是模式层,它不是一个具体的库,而是一种架构思路——把复杂任务拆解成"规划、执行、反思、修正"的循环,让 Agent 具备长程任务的处理能力。

这三者的关系不是替代,而是叠加。你可以只用 LangChain 写个简单的问答机器人,也可以用 LangGraph 编排一个多 Agent 协作系统,而 Deep Agents 的思路则决定了你这个系统能不能处理那些需要几十步才能完成的任务。

这篇文章适合谁看?如果你已经写过至少一个能跑通的大模型应用,但对"怎么让它稳定、可维护、能处理复杂任务"感到困惑,那这篇就是写给你的。如果你完全没接触过 LangChain,建议先花半天时间把官方 Quickstart 跑一遍,再回来看这篇,效果会好很多。

下面我会按照"设计思路 → 核心细节 → 实操过程 → 问题排查"这个顺序展开,每一部分都会给出具体的代码示例和参数说明。代码基于 Python,版本以我写这篇文章时的稳定版为准,如果你用的是其他版本,个别 API 可能有差异,注意对照官方文档。

2. 整体架构设计:为什么是这三层而不是别的组合

2.1 先搞清楚每一层到底在管什么

很多人一开始会把 LangChain 和 LangGraph 混着用,结果写出来的代码既不像链式调用,也不像图编排,四不像。要避免这个问题,得先把每一层的职责边界划清楚。

LangChain 的职责边界:它管的是"单次操作"。一次模型调用、一次工具执行、一次输出解析,这些都是 LangChain 的范畴。它提供的ChatModel、Tool、OutputParser、Retriever这些抽象,本质上都是在定义"一个操作长什么样"。你用它来写一个 RAG 的检索环节,或者写一个需要调用计算器的工具节点,都很合适。但如果你试图用 LangChain 的Chain去表达"先检索、再判断相关性、不相关就重新检索、相关就生成答案"这种带循环的逻辑,就会很别扭。

LangGraph 的职责边界:它管的是"多个操作之间怎么连"。状态怎么定义、节点之间怎么跳转、条件边怎么写、中断怎么恢复,这些是 LangGraph 的核心。它不关心你每个节点内部是用 LangChain 写的还是手写的,它只关心输入状态和输出状态。这就意味着你可以把 LangChain 的那些组件当作 LangGraph 节点里的"零件"来用,而不是让 LangChain 的 Chain 去承载整个流程。

Deep Agents 的职责边界:它管的是"任务怎么拆、怎么跟踪、怎么修正"。这不是一个代码层面的东西,而是一种设计模式。一个 Deep Agent 通常包含几个关键角色:规划器负责把大任务拆成子任务,执行器负责逐个完成,反思器负责检查结果是否达标,修正器负责在失败时调整策略。这套东西可以用 LangGraph 来实现,也可以用别的框架,但用 LangGraph 来实现是最自然的,因为它的图结构天然适合表达这种循环。

我画不出图(这里也不允许画),但你可以这样理解三者的关系:LangChain 是砖块,LangGraph 是钢筋骨架,Deep Agents 是建筑图纸。没有图纸,你也能用砖块和钢筋搭个棚子;但没有钢筋,你只能用砖块垒个矮墙;而没有砖块,钢筋骨架也立不起来。

2.2 为什么不用纯 LangChain 硬扛复杂流程

我试过。早期做一个需要"检索 → 判断 → 可能重新检索 → 生成 → 自检 → 可能重新生成"的问答系统,我用 LangChain 的SequentialChain加上一堆if-else硬写。结果是:

  • 状态传递靠一个不断膨胀的字典,每个 Chain 都要手动把需要的字段挑出来传进去,漏一个就报错。
  • 重试逻辑写在 Chain 外面,用while循环包着,一旦嵌套两层以上,代码就没法看了。
  • 调试的时候,日志打出来是一长串嵌套调用,根本分不清哪一层是哪一步。
  • 想加一个"人工审核"的中断点,发现得把整个流程拆开重写。

后来换成 LangGraph,同样的逻辑,代码量少了大概三分之一,而且结构清晰得多。每个节点就是一个函数,输入输出都是明确定义的状态,条件边用简单的函数表达,中断点用interrupt机制就能实现。这不是说 LangGraph 有多神奇,而是它把"流程控制"这件事从业务逻辑里抽出来了,让你能分别关注。

2.3 Deep Agents 思路带来的关键转变

在没有引入 Deep Agents 思路之前,我写的 Agent 基本都是"一步到位"的:给一个任务,它调用一次或几次工具,然后给出答案。这种模式在简单任务上没问题,但遇到需要多步推理、需要根据中间结果调整策略的任务,就很容易失败。

Deep Agents 的核心转变在于:把"完成任务"变成一个"迭代改进"的过程。具体来说,它引入了几个关键机制:

  • 显式规划:在开始执行之前,先让模型生成一个任务分解列表,而不是直接开始做。这个规划步骤本身可以是一个 LangGraph 节点。
  • 状态跟踪:用一个结构化的状态对象记录"已经完成了哪些子任务、当前在做什么、遇到了什么问题",而不是靠模型的上下文记忆。
  • 反思循环:每个子任务完成后,让模型检查结果是否满足要求,不满足就触发修正。
  • 终止条件:明确什么情况下算完成、什么情况下算失败,避免无限循环。

这些机制单独看都不复杂,但组合起来就能让 Agent 处理那些需要十几步甚至几十步才能完成的任务。我实测下来,在同样的模型能力下,加了这套机制的 Agent 在复杂任务上的成功率比"一步到位"的版本高出不少,具体数字因任务而异,但趋势是明显的。

2.4 技术选型的几个关键决策点

在实际搭建的时候,有几个决策点需要提前想清楚,不然后面改起来很痛苦。

第一个决策:状态用什么结构。LangGraph 的状态可以是一个TypedDict,也可以是一个 Pydantic 模型。我的建议是,如果状态字段不多且类型简单,用TypedDict就够了;如果状态里有嵌套结构、需要校验、或者字段很多,用 Pydantic 模型会更清晰。另外要注意,LangGraph 的状态更新默认是"覆盖"语义,如果你需要"追加"语义(比如往列表里加东西),需要用Annotated配合 reducer 函数。

第二个决策:节点粒度怎么定。一个节点可以是一个简单的模型调用,也可以是一整个子流程。粒度太细,图会变得很复杂,节点之间的边会很多;粒度太粗,又失去了图编排的灵活性。我的经验是,一个节点对应一个"语义上完整"的操作,比如"检索文档"是一个节点,"判断相关性"是另一个节点,"生成答案"又是一个节点。不要把"调用模型"和"解析输出"拆成两个节点,除非解析逻辑本身很复杂需要单独处理。

第三个决策:用不用预置的 Agent 类型。LangGraph 提供了一些预置的 Agent 构造函数,比如create_react_agent。这些预置类型在简单场景下很好用,但一旦你需要自定义状态、自定义节点逻辑,就会觉得受限。我的建议是,先用预置类型快速验证想法,确认可行后再改成手写图。不要一上来就手写,那样会浪费很多时间在样板代码上。

第四个决策:工具怎么组织。如果工具数量少(少于 10 个),可以直接全部塞给模型,让它自己选。如果工具数量多,建议做分组或者用检索的方式动态选择工具,否则模型的工具选择准确率会下降。LangChain 的Tool抽象支持给工具写详细的描述,这个描述的质量直接影响模型的选择准确率,值得花时间打磨。

3. 核心细节拆解:状态、节点、边、工具这四样东西怎么用对

3.1 状态定义:别小看这个 TypedDict

状态是 LangGraph 里最基础也最容易出错的地方。我见过不少人把状态定义成一个巨大的字典,什么字段都往里塞,结果节点之间互相污染,调试的时候根本不知道哪个字段是谁改的。

一个清晰的状态定义应该遵循几个原则:

  • 字段名要能自解释。不要用data、result这种模糊的名字,用retrieved_docs、relevance_score、final_answer这种一看就知道是什么的名字。
  • 区分"输入字段"和"中间字段"。输入字段是外部传进来的,中间字段是节点产生的。虽然 LangGraph 不强制区分,但在文档里标注清楚有助于维护。
  • 列表类型的字段要用 reducer。如果你有一个messages字段需要不断追加,直接定义成list会导致每次更新都覆盖,必须用Annotated[list, add_messages]这样的写法。

下面是一个我常用的状态定义模板,你可以直接参考:

from typing import Annotated, TypedDict from langgraph.graph.message import add_messages class AgentState(TypedDict): # 输入字段 user_query: str # 消息历史,用 add_messages reducer 实现追加语义 messages: Annotated[list, add_messages] # 规划相关 plan: list[str] current_step: int # 执行相关 retrieved_docs: list[dict] tool_results: list[dict] # 反思相关 reflection: str is_complete: bool # 错误处理 error_count: int last_error: str

这里有几个细节值得说。add_messages是 LangGraph 提供的一个 reducer,它会智能地处理消息的追加和更新(比如根据消息 ID 去重)。如果你自己写 reducer,要注意处理各种边界情况,不如直接用官方的。error_count和last_error这两个字段是我后来加的,用来做错误重试控制,没有它们的话,一旦某个节点反复失败,图会陷入死循环。

注意:状态字段不是越多越好。每加一个字段,就意味着所有节点都要考虑这个字段的存在。我建议在初期只加必要的字段,等确实需要了再加。加字段的时候要检查所有节点,确保它们不会因为新字段而行为异常。

3.2 节点函数:输入输出都是状态,别搞特殊

LangGraph 的节点就是一个函数,输入是状态,输出是要更新的状态字段。听起来简单,但实际写的时候有几个坑。

第一个坑:返回部分状态还是完整状态。LangGraph 允许你只返回要更新的字段,比如return {"retrieved_docs": docs},它会自动合并到现有状态里。这比返回完整状态方便得多,但要注意,如果你返回的字段类型和状态定义不一致,会在运行时才报错,而不是定义时报错。所以建议在节点函数里加类型注解,让静态检查工具帮你发现问题。

第二个坑:节点里的异常处理。如果节点函数抛异常,整个图会中断。有些异常是你希望中断的(比如配置错误),有些是你希望捕获并重试的(比如网络超时)。我的做法是在节点内部用 try-except 捕获可重试的异常,更新error_count和last_error字段,然后通过条件边决定是重试还是放弃。这样图本身不会中断,但你能控制重试逻辑。

第三个坑:节点里的模型调用怎么组织。如果你在节点里直接用 LangChain 的ChatModel,要注意每次调用都会创建新的连接。在高频调用的场景下,建议把模型实例作为节点函数的参数传入,或者用单例模式管理。LangGraph 支持通过config传递依赖,这是一个比较优雅的做法。

下面是一个典型的节点函数示例,展示了规划节点的写法:

def planning_node(state: AgentState) -> dict: """根据用户查询生成任务计划""" llm = get_llm() # 获取模型实例 prompt = f"""你是一个任务规划助手。请把下面的任务拆解成3-5个可执行的子任务。 任务:{state['user_query']} 请以JSON数组格式返回子任务列表,每个子任务是一个字符串。""" try: response = llm.invoke(prompt) plan = parse_json_response(response.content) return {"plan": plan, "current_step": 0} except Exception as e: return {"error_count": state.get("error_count", 0) + 1, "last_error": str(e)}

这个节点做了三件事:构造提示词、调用模型、解析结果。解析失败时更新错误计数而不是抛异常,这样图可以继续走条件边来决定下一步。

3.3 条件边:流程控制的真正核心

如果说节点是图的"肉",那条件边就是图的"骨"。LangGraph 的条件边通过一个函数来决定下一步走哪个节点,这个函数的返回值是目标节点的名字。

写条件边函数的时候,有几个经验:

  • 函数要纯粹。条件边函数不应该有副作用,只根据状态做判断。如果你需要在判断前做一些计算,把计算放到节点里,条件边只读结果。
  • 返回值要明确。返回的字符串必须是图里真实存在的节点名,否则运行时会报错。建议用常量或者枚举来管理节点名,避免拼写错误。
  • 要有默认分支。条件边函数应该覆盖所有可能的情况,包括异常情况。如果所有条件都不满足,应该有一个兜底的分支,而不是返回 None 导致图卡住。

下面是一个典型的条件边示例,用于决定规划完成后是继续执行还是直接结束:

def should_continue(state: AgentState) -> str: """决定下一步走向""" # 如果错误太多,放弃 if state.get("error_count", 0) >= 3: return "handle_failure" # 如果任务已完成,结束 if state.get("is_complete", False): return "end" # 如果还有未完成的步骤,继续执行 if state.get("current_step", 0) < len(state.get("plan", [])): return "execute_step" # 所有步骤完成,进入反思 return "reflect"

这个函数覆盖了四种情况,每种都返回一个明确的节点名。注意end是 LangGraph 的特殊节点名,表示图执行结束。

3.4 工具集成:描述比实现更重要

在 Agent 系统里,工具是模型和外部世界交互的接口。LangChain 的Tool抽象让定义工具变得很简单,但很多人忽略了工具描述的重要性。

模型选择工具的依据主要是工具的名称和描述。如果描述写得含糊,模型就会选错工具,或者在不该用工具的时候用工具。我踩过的坑包括:两个功能相似的工具描述太接近,模型随机选;工具描述里没写清楚参数格式,模型传错参数;工具描述里没写清楚适用场景,模型在不合适的场景下调用。

一个好的工具描述应该包含:

  • 功能说明:这个工具是做什么的,一句话说清楚。
  • 适用场景:什么情况下应该用这个工具,什么情况下不应该用。
  • 参数说明:每个参数的类型、含义、格式要求。
  • 返回值说明:返回什么格式的数据,模型应该怎么理解。

下面是一个工具定义的示例:

from langchain_core.tools import tool @tool def search_knowledge_base(query: str, top_k: int = 5) -> list[dict]: """在知识库中搜索相关文档。 适用场景:当你需要查找事实性信息、文档内容、或者需要引用来源时使用。 不适用场景:当你需要计算、或者需要实时数据时,不要用这个工具。 参数: query: 搜索关键词,应该是自然语言描述的问题或关键词组合。 top_k: 返回的文档数量,默认5,范围1-20。 返回: 一个字典列表,每个字典包含 'content'(文档内容)和 'score'(相关性分数)。 """ # 实际实现 return do_search(query, top_k)

这个描述看起来有点啰嗦,但实测下来,模型选择工具的准确率比简短描述高出不少。花在写描述上的时间,会在调试阶段省回来。

提示:如果你的工具数量超过 15 个,考虑做工具分组。比如把"搜索类"工具放一组,"计算类"工具放一组,先让模型选组,再在组内选工具。这样能显著降低模型的选择难度。

4. 实操过程:从零搭一个带反思循环的 Deep Agent

4.1 环境准备与依赖安装

先把环境搭起来。我用的是 Python 3.11,LangChain 和 LangGraph 的版本以你安装时的最新稳定版为准。依赖不多,但要注意版本兼容性。

pip install langchain langchain-core langgraph langchain-openai

如果你用的是其他模型提供商,把langchain-openai换成对应的包。我建议在虚拟环境里装,避免和系统里的其他包冲突。

装完之后,先跑一个最小示例验证环境没问题:

from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, END from typing import TypedDict class SimpleState(TypedDict): input: str output: str def process_node(state: SimpleState) -> dict: llm = ChatOpenAI(model="gpt-4o-mini") result = llm.invoke(state["input"]) return {"output": result.content} graph = StateGraph(SimpleState) graph.add_node("process", process_node) graph.set_entry_point("process") graph.add_edge("process", END) app = graph.compile() result = app.invoke({"input": "你好"}) print(result["output"])

这段代码能跑通,说明环境没问题。如果报错,大概率是 API Key 没配好,或者模型名称写错了。

4.2 定义状态和节点:把规划、执行、反思拆开

接下来定义完整的状态和节点。我按照 Deep Agents 的思路,把流程拆成四个核心节点:规划、执行、反思、修正。

from typing import Annotated, TypedDict from langgraph.graph.message import add_messages class DeepAgentState(TypedDict): user_query: str messages: Annotated[list, add_messages] plan: list[str] current_step: int step_results: list[dict] reflection: str is_complete: bool error_count: int last_error: str max_steps: int

这里加了max_steps字段,用来防止无限循环。这是一个很重要的保护机制,后面会详细说。

规划节点的实现前面已经给过示例,这里补充一下执行节点和反思节点。

执行节点的职责是:根据当前步骤,决定调用哪个工具,执行工具,记录结果。

def execute_node(state: DeepAgentState) -> dict: """执行当前步骤""" current_step = state["current_step"] plan = state["plan"] if current_step >= len(plan): return {"is_complete": True} step_desc = plan[current_step] llm = get_llm_with_tools() # 获取绑定了工具的模型 # 构造执行提示 prompt = f"""当前任务:{step_desc} 已有结果:{state.get('step_results', [])} 请决定是否需要调用工具,如果需要,调用合适的工具。""" try: response = llm.invoke(prompt) # 处理工具调用 if response.tool_calls: tool_results = execute_tool_calls(response.tool_calls) new_result = {"step": current_step, "desc": step_desc, "result": tool_results} else: new_result = {"step": current_step, "desc": step_desc, "result": response.content} return { "step_results": state.get("step_results", []) + [new_result], "current_step": current_step + 1, "messages": [response] } except Exception as e: return { "error_count": state.get("error_count", 0) + 1, "last_error": str(e) }

反思节点的职责是:检查到目前为止的结果是否满足要求,如果不满足,给出修正建议。

def reflect_node(state: DeepAgentState) -> dict: """反思当前进展""" llm = get_llm() prompt = f"""原始任务:{state['user_query']} 已完成的步骤和结果: {format_step_results(state.get('step_results', []))} 请评估:这些结果是否足以回答原始任务? 如果足够,返回 "COMPLETE"。 如果不够,指出缺少什么,并给出下一步建议。""" response = llm.invoke(prompt) reflection = response.content if "COMPLETE" in reflection.upper(): return {"reflection": reflection, "is_complete": True} else: # 把反思结果作为新的步骤加入计划 new_plan = state["plan"] + [reflection] return {"reflection": reflection, "plan": new_plan}

这两个节点的关键区别在于:执行节点关注"做",反思节点关注"检查"。分开之后,每个节点的逻辑都很清晰,调试的时候也容易定位问题。

4.3 组装图:条件边怎么写才不出错

节点定义好了,接下来把它们组装成图。这一步的关键是条件边的设计。

from langgraph.graph import StateGraph, END def build_deep_agent(): graph = StateGraph(DeepAgentState) # 添加节点 graph.add_node("plan", planning_node) graph.add_node("execute", execute_node) graph.add_node("reflect", reflect_node) graph.add_node("handle_failure", handle_failure_node) # 设置入口 graph.set_entry_point("plan") # 规划后进入执行 graph.add_edge("plan", "execute") # 执行后的条件分支 graph.add_conditional_edges( "execute", route_after_execute, { "continue": "execute", # 还有步骤,继续执行 "reflect": "reflect", # 步骤完成,进入反思 "fail": "handle_failure" # 错误太多,处理失败 } ) # 反思后的条件分支 graph.add_conditional_edges( "reflect", route_after_reflect, { "complete": END, # 任务完成 "continue": "execute", # 需要继续 "fail": "handle_failure" # 错误太多 } ) # 失败处理节点结束 graph.add_edge("handle_failure", END) return graph.compile()

条件边函数route_after_execute和route_after_reflect的实现:

def route_after_execute(state: DeepAgentState) -> str: # 错误检查 if state.get("error_count", 0) >= 3: return "fail" # 步骤检查 if state.get("current_step", 0) < len(state.get("plan", [])): return "continue" # 步骤都完成了,进入反思 return "reflect" def route_after_reflect(state: DeepAgentState) -> str: if state.get("error_count", 0) >= 3: return "fail" if state.get("is_complete", False): return "complete" # 检查是否超过最大步数 if len(state.get("plan", [])) >= state.get("max_steps", 20): return "fail" return "continue"

这里有几个细节值得注意。route_after_execute里先检查错误,再检查步骤,最后才进入反思。这个顺序很重要,因为如果错误太多,继续执行只会浪费资源。route_after_reflect里加了最大步数检查,防止反思节点不断往计划里加新步骤导致无限循环。

注意:条件边函数的返回值必须是字典里定义的键,不能是节点名本身。这是一个容易搞混的地方。字典的键是条件边函数的返回值,值是目标节点名。

4.4 运行与调试:怎么知道每一步在干什么

图组装好之后,运行起来很简单:

app = build_deep_agent() result = app.invoke({ "user_query": "帮我分析一下最近三个月的销售数据,找出增长最快的产品类别", "plan": [], "current_step": 0, "step_results": [], "is_complete": False, "error_count": 0, "max_steps": 20 }) print(result["step_results"])

但调试的时候,光看最终结果是不够的。你需要知道每一步的输入输出是什么。LangGraph 提供了流式执行的能力,可以逐步查看状态变化:

for event in app.stream(initial_state): for node_name, node_output in event.items(): print(f"节点:{node_name}") print(f"输出:{node_output}") print("---")

这样你就能看到每个节点执行后的状态更新。如果某个节点的输出不符合预期,就能快速定位。

另外,LangGraph 支持把执行过程可视化。虽然这里不能画图,但你可以用app.get_graph().draw_mermaid()生成一个 Mermaid 格式的图描述,然后粘贴到支持 Mermaid 的工具里查看。这个功能在调试复杂图的时候很有用。

4.5 参数调优:几个影响效果的关键参数

在实际运行中,有几个参数对效果影响很大,值得单独调。

第一个是最大步数max_steps。设得太小,复杂任务做不完;设得太大,失败的任务会浪费很多资源。我的经验是,根据任务的平均复杂度来设,一般 15-25 之间比较合适。如果任务特别复杂,可以设到 50,但要配合超时机制。

第二个是错误重试阈值。我在条件边里用的是 3 次,这个数字不是固定的。如果工具调用容易失败(比如依赖外部 API),可以设高一点;如果错误通常是逻辑问题,设低一点更好,避免浪费时间。

第三个是反思的触发频率。我现在的设计是每完成所有步骤后反思一次。但有些任务适合每完成一个步骤就反思一次,这样能更早发现问题。代价是模型调用次数增加,成本上升。你可以根据任务的重要性和预算来权衡。

第四个是模型的温度参数。规划节点和反思节点建议用较低的温度(0.1-0.3),保证输出的稳定性;执行节点可以稍微高一点(0.3-0.7),保留一些灵活性。这个不是绝对的,需要根据实际效果调整。

5. 常见问题与排查技巧实录

5.1 图跑不起来?先检查这几个地方

问题一:节点名拼写错误。这是最常见的错误。条件边函数返回的字符串必须和add_node时用的名字完全一致,包括大小写。我建议把节点名定义成常量,比如NODE_PLAN = "plan",然后在所有地方引用常量,避免手写字符串。

问题二:状态字段类型不匹配。如果状态定义里current_step是int,但某个节点返回了字符串,会在运行时才报错。解决办法是在节点函数上加类型注解,并用 mypy 之类的工具做静态检查。

问题三:忘记设置入口点。set_entry_point必须调用,否则图不知道从哪里开始。这个错误比较明显,但新手容易忘。

问题四:条件边没有覆盖所有情况。如果条件边函数在某些状态下返回了未定义的键,图会报错。解决办法是在条件边函数里加一个兜底分支,或者用assert确保返回值在预期范围内。

5.2 图跑起来了但结果不对?排查思路

症状一:Agent 陷入循环。表现为图一直执行不结束,或者反复执行同样的步骤。原因通常是条件边逻辑有问题,或者反思节点不断往计划里加新步骤。排查方法是打印每次循环的状态,看current_step和plan的变化。解决办法是加最大步数限制,并检查反思节点的逻辑。

症状二:工具调用失败。表现为执行节点报错,或者工具返回空结果。原因可能是工具描述不清楚导致模型传错参数,或者工具本身有问题。排查方法是单独测试工具函数,确认它能正常工作;然后检查模型生成的工具调用参数是否符合预期。

症状三:反思节点不生效。表现为任务明明没完成,但反思节点返回了 COMPLETE。原因可能是反思提示词写得不够严格,或者模型能力不足。解决办法是优化提示词,明确列出完成的标准;如果模型能力有限,可以考虑用更强的模型来做反思。

症状四:状态字段被意外覆盖。表现为某个字段的值在某个节点后突然变了。原因通常是节点返回了不该返回的字段,或者 reducer 配置有问题。排查方法是检查每个节点的返回值,确保只返回要更新的字段。

5.3 常见问题速查表

问题现象可能原因排查方法解决方案
图不执行未设置入口点检查set_entry_point添加入口点设置
节点名报错拼写不一致对比节点名和条件边返回值用常量管理节点名
无限循环条件边逻辑错误打印状态变化加最大步数限制
工具调用失败描述不清或参数错误单独测试工具优化工具描述
反思不生效提示词不严格检查反思输出优化提示词或用更强模型
状态被覆盖节点返回多余字段检查节点返回值只返回要更新的字段
类型错误状态定义与返回值不符加类型注解用静态检查工具
执行超时任务太复杂或模型太慢查看日志拆分任务或换模型

5.4 几个我踩过的坑和对应的技巧

坑一:把模型实例放在节点函数里创建。一开始我每个节点都ChatOpenAI(...)一下,结果发现每次调用都要重新建立连接,速度很慢。后来改成用模块级的单例,或者通过config传递,速度快了不少。

坑二:状态里存了不可序列化的对象。LangGraph 在某些场景下需要序列化状态(比如做检查点),如果状态里有数据库连接、文件句柄这类对象,会报错。解决办法是只存必要的数据,把连接管理放在节点外部。

坑三:条件边函数里有副作用。我一开始在条件边函数里做了一些日志记录和状态更新,结果发现这些操作会被执行多次(因为条件边可能被多次调用)。后来把所有副作用都移到节点里,条件边只做纯判断。

坑四:忽略了 token 消耗。Deep Agents 的反思循环会显著增加模型调用次数,token 消耗比普通 Agent 高不少。我建议在开发阶段就加上 token 统计,了解每个节点的消耗情况,避免上线后成本失控。

坑五:没有做超时控制。如果某个工具调用卡住了,整个图会一直等。解决办法是给工具调用加超时,超时后当作错误处理,触发重试或失败分支。

提示:在开发阶段,建议把每个节点的输入输出都打到日志里,包括完整的提示词和模型返回。这样出问题的时候能快速定位。上线后可以降低日志级别,但关键节点还是要保留日志。

6. 一些关于扩展和优化的个人体会

这套东西搭起来之后,能做的事情比我想象的多。我后来在它基础上加了几个扩展,效果不错,这里简单说一下思路。

扩展一:多 Agent 协作。把执行节点拆成多个专门的 Agent,比如一个负责搜索、一个负责计算、一个负责写作,用一个协调节点来分配任务。LangGraph 的图结构天然支持这种模式,每个 Agent 是一个子图,协调节点根据任务类型路由到不同的子图。

扩展二:人工审核中断点。用 LangGraph 的interrupt机制,在关键步骤前暂停,等人工确认后再继续。这在需要高准确率的场景下很有用,比如涉及金额计算或者对外发送内容的时候。

扩展三:持久化和恢复。LangGraph 支持检查点机制,可以把图的状态存到数据库里,中断后从上次的状态恢复。这对于长时间运行的任务很有价值,避免因为意外中断而从头开始。

扩展四:动态工具加载。根据任务类型动态加载不同的工具集,而不是一开始就把所有工具都绑给模型。这样能减少模型的困惑,提高工具选择的准确率。

这些扩展每一个都值得单独写一篇,这里只是提一下方向。如果你已经把基础版本跑通了,可以挑一个感兴趣的试试。

最后分享一个我在调优过程中总结的小技巧:不要试图一次性把所有节点都做到完美。先把最简单的版本跑通,确认流程没问题,然后逐个节点优化。每次只改一个地方,改完就测试,确认有效再改下一个。这样虽然看起来慢,但实际上比一次性大改要快得多,因为出问题的时候容易定位。

另外,模型的选择对效果影响很大。同样的图结构,换个模型可能效果差很多。如果发现某个节点效果不好,先别急着改逻辑,试试换个模型,有时候会有惊喜。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进