
1. 从 LangChain 到 LangGraph为什么单靠链式调用撑不起一个真正的 Agent1.1 一个让我彻底转向 LangGraph 的真实场景去年下半年我接了一个内部工单系统的智能化改造需求听起来不复杂用户用自然语言描述问题系统自动判断类型、查知识库、必要时调用外部接口拿数据、最后生成回复。我第一反应就是用 LangChain 的 LCEL 把这条链路串起来prompt | llm | parser一套组合拳打下来demo 半天就跑通了当时觉得挺美。结果上线测试第一天就翻车了。用户问了一个需要先查订单状态如果状态异常再查物流物流也异常才触发人工的问题。用 LCEL 写我得把这三层判断全部塞进一个 prompt 里让模型一次性输出决策模型稍微一飘要么该查的不查要么把三个分支全走一遍。更麻烦的是用户中途补充了一句算了帮我直接转人工这时候整个链已经跑了一半没有任何机制能打断并跳转。那一刻我才真正理解为什么社区里那么多人说LangChain 负责组件LangGraph 负责编排。LCEL 本质是一条有向无环的流水线数据从 A 流到 B 流到 C中间不能回头、不能分叉后合并、不能暂停等人。而真实的 Agent 是什么是一个带状态的循环决策系统思考、行动、观察、再思考可能循环十几次可能中途被打断可能同时跑几条并行分支最后汇总。LangGraph 就是为这个场景生的。它把 Agent 的执行过程建模成一张图Graph节点是计算单元边是流转逻辑全局还有一个可持久化的State。你可以在这张图上做条件分支、做循环、做中断恢复、做多智能体协作。这篇文章我就把 LangChain 和 LangGraph 的关系、LangGraph 的核心抽象StateGraph、Node、Edge、Checkpointer、以及一个能直接抄作业的实战案例完整讲一遍适合已经会写基础 LangChain 链、想往 Agent 方向进阶的开发者。1.2 LangChain 和 LangGraph 到底是不是替代关系先把一个最常见的误解澄清掉LangGraph 不是 LangChain 的替代品而是它的上层编排框架。这两个东西解决的是不同层次的问题。LangChain 提供的是零件库各种 LLM 封装、Prompt 模板、输出解析器、Retriever、Tool 定义、Memory 组件。你写 LCEL 的时候用的是这些零件拼一条直线管道。LangGraph 提供的是装配图纸它不管你每个节点内部用什么实现它只管这些节点怎么连、什么时候走哪条边、状态怎么在节点间传递和累积。打个比方LangChain 像是给你一堆乐高积木LCEL 是让你按说明书拼一个固定造型LangGraph 则是给你一张可以自由设计的电路板每个焊点节点里放什么元件随你但板子上的走线边和电流方向状态流转由你精确控制。所以LangChain 和 LangGraph 都过时了吗这种热搜问题我的答案是LCEL 在简单线性场景下依然好用没必要为了用 LangGraph 而用但只要你的需求里出现了循环条件跳转人工介入多 Agent 协作这四个词中的任何一个就该上 LangGraph 了。它们不是二选一而是LangGraph 内部大量复用 LangChain 的组件——你在 LangGraph 的节点函数里照样可以调ChatOpenAI、照样可以用Retriever只是外层的控制流换了一套更强大的引擎。1.3 一张表看懂 LCEL 和 LangGraph 的能力边界我把两者最核心的差异整理成一张对照表这张表基本能覆盖你选型时 90% 的纠结维度LCELLangChain 表达式语言LangGraph状态图编排执行模型有向无环图DAG单向流动有向图支持环、条件分支、并行状态管理隐式靠管道传递显式全局 State 可读写可持久化循环能力不支持需要外部 while 包裹原生支持边可以指回上游节点人工介入很难需要自己拆断点原生 interrupt可暂停可恢复断点续跑不支持Checkpointer 持久化随时恢复多 Agent 协作需要手写调度逻辑天然支持子图即子 Agent调试可视化靠日志图结构可视化节点级追踪学习曲线平缓会函数式编程就能上手陡峭要理解图、状态、reducer这张表里最关键的一行是循环能力。很多人第一次意识到 LangGraph 必要性就是因为想实现 ReAct 那种思考-行动-观察的循环用 LCEL 写会非常别扭而 LangGraph 里就是加一条从should_continue节点指回agent节点的边而已。2. LangGraph 的四大核心抽象State、Node、Edge、Checkpointer2.1 State整个图的共享内存也是新手最容易踩坑的地方LangGraph 里最核心的概念是State状态。你可以把它理解成整张图运行期间的共享内存或者全局变量池每个节点都能读到它、都能往里写东西。它通常定义成一个TypedDict字段就是你想在节点间传递的数据。from typing import TypedDict, Annotated from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] user_id: str order_status: str retry_count: int这里有个新手必踩的坑State 字段默认是覆盖式更新也就是节点返回{order_status: shipped}就会把原来的值直接替换掉。但像messages这种对话历史你肯定希望是追加而不是覆盖所以要用Annotated[list, add_messages]给它挂一个reducer归约函数。add_messages是 LangGraph 内置的作用是把新消息追加到列表末尾而不是替换整个列表。我见过太多人第一次写 LangGraph发现对话历史莫名其妙只剩最后一条排查半天就是忘了加 reducer。这个点官方文档讲得比较散我这里强调一下凡是需要累积的字段消息列表、日志、中间结果集合都要挂 reducer凡是需要覆盖的字段当前状态标记、计数器用默认行为就行。reducer 还能自定义。比如你想让某个字段做数值累加可以写def add_int(left: int, right: int) - int: return left right class State(TypedDict): total_tokens: Annotated[int, add_int]这样每个节点返回{total_tokens: 100}就会累加到总数上而不是覆盖。这个技巧在做 token 统计、成本核算的时候特别有用。2.2 Node一个节点就是一个函数但返回值有讲究Node节点就是图上的一个计算单元本质上就是一个 Python 函数接收当前 State返回一个字典表示要更新的字段。def classify_intent(state: AgentState) - dict: last_msg state[messages][-1].content # 这里可以调 LLM 做意图分类 intent llm.invoke(f判断意图{last_msg}).content return {intent: intent}节点函数有几个实操要点第一返回值必须是字典键必须是 State 里定义过的字段。返回未定义的键会被忽略有些版本会报错返回已定义但没挂 reducer 的字段就是覆盖。第二节点可以是同步函数也可以是异步函数。如果你的节点里有 IO 操作调 API、查数据库强烈建议写成async def然后用graph.ainvoke()调用并发性能差别很大。第三节点内部可以调 LLM、可以调工具、可以做任何事LangGraph 不关心你节点里干什么它只关心你返回什么。这种关注点分离的设计让 LangGraph 非常灵活——你甚至可以在节点里调用另一个 LangGraph 子图这就是多 Agent 协作的基础。第四节点命名要有意义因为调试和可视化的时候你看到的就是节点名。我习惯用动词开头比如classify_intent、retrieve_docs、call_tool、generate_answer一眼能看出这个节点在干嘛。2.3 Edge普通边、条件边、入口出口控制流的全部秘密Edge边决定了节点之间的流转关系LangGraph 里有几种边普通边用add_edge(A, B)表示 A 执行完无条件走 B。入口用set_entry_point(A)或add_edge(START, A)指定从哪个节点开始。出口用add_edge(B, END)表示 B 执行完就结束。真正强大的是条件边用add_conditional_edges定义def should_continue(state: AgentState) - str: last_msg state[messages][-1] if last_msg.tool_calls: return call_tool return end graph.add_conditional_edges( agent, should_continue, { call_tool: tool_node, end: END } )这段代码就是 ReAct 循环的核心agent 节点输出后判断有没有工具调用有就去tool_node没有就结束。而tool_node执行完又有一条边指回agent形成循环。这就是 LCEL 做不到、LangGraph 轻松做到的事情。条件边函数的返回值必须是字符串然后通过第三个参数的映射字典决定实际走哪条边。这个映射字典的好处是条件函数只负责判断不负责知道有哪些节点解耦得很干净。2.4 Checkpointer让 Agent 拥有记忆和断点续跑能力Checkpointer检查点保存器是 LangGraph 相比 LCEL 最被低估的能力。它会在每个节点执行后把当前 State 快照保存下来。这带来三个直接好处一是对话记忆。用MemorySaver或数据库 Checkpointer同一个thread_id的多轮对话能自动带上历史不用你自己维护 message 列表。二是断点续跑。图跑到一半崩了或者需要人工审批暂停恢复时从最后一个 checkpoint 继续不用从头再来。三是时间旅行调试。你可以回放任意一个历史 checkpoint看看当时 State 长什么样排查问题极其方便。from langgraph.checkpoint.memory import MemorySaver checkpointer MemorySaver() app graph.compile(checkpointercheckpointer) config {configurable: {thread_id: user-123}} app.invoke({messages: [(user, 你好)]}, config)生产环境别用MemorySaver它是纯内存的进程重启就没了。要用SqliteSaver或PostgresSaver做持久化。这个点后面实战部分我会详细讲。3. 手把手搭一个带工具调用和人工审批的 Agent3.1 需求拆解与技术选型我们来做一个真实可用的场景一个订单查询 Agent。用户用自然语言问订单相关问题Agent 能调用工具查订单状态、查物流如果发现订单异常比如物流停滞超过 3 天自动暂停等待人工审批是否给用户补偿。这个场景覆盖了 LangGraph 的四大核心能力循环多轮工具调用、条件分支正常/异常、人工介入interrupt、状态持久化checkpointer。做完这个你对 LangGraph 的理解就到位了。技术栈langgraphlangchain-openailanggraph-checkpoint-sqlite。安装pip install langgraph langchain-openai langgraph-checkpoint-sqlite3.2 定义 State 和工具先定义 State。注意messages要挂add_messagesreducerneed_human_approval是覆盖式的布尔标记from typing import TypedDict, Annotated from langgraph.graph.message import add_messages class OrderState(TypedDict): messages: Annotated[list, add_messages] order_id: str logistics_days: int need_human_approval: bool approved: bool然后定义工具。工具用 LangChain 的tool装饰器这样能自动生成 schema 给 LLMfrom langchain_core.tools import tool tool def query_order(order_id: str) - dict: 根据订单号查询订单状态 # 实际项目里这里查数据库 return {order_id: order_id, status: shipped, amount: 299} tool def query_logistics(order_id: str) - dict: 查询订单物流信息返回停滞天数 return {order_id: order_id, stalled_days: 5} tools [query_order, query_logistics]3.3 构建图节点、边、条件路由现在开始搭图。核心节点有三个agentLLM 决策、tools执行工具、human_approval人工审批。from langgraph.graph import StateGraph, START, END from langgraph.prebuilt import ToolNode from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini).bind_tools(tools) def agent_node(state: OrderState) - dict: response llm.invoke(state[messages]) return {messages: [response]} def check_logistics_node(state: OrderState) - dict: # 从工具返回结果里提取停滞天数 days 0 for msg in state[messages]: if hasattr(msg, content) and stalled_days in str(msg.content): days 5 # 简化处理 return { logistics_days: days, need_human_approval: days 3 } def human_approval_node(state: OrderState) - dict: # 这里会被 interrupt 打断等人工输入 return {approved: True}路由函数决定 agent 之后往哪走def route_after_agent(state: OrderState) - str: last state[messages][-1] if last.tool_calls: return tools return check_logistics def route_after_check(state: OrderState) - str: if state.get(need_human_approval): return human_approval return END组装图builder StateGraph(OrderState) builder.add_node(agent, agent_node) builder.add_node(tools, ToolNode(tools)) builder.add_node(check_logistics, check_logistics_node) builder.add_node(human_approval, human_approval_node) builder.add_edge(START, agent) builder.add_conditional_edges(agent, route_after_agent, { tools: tools, check_logistics: check_logistics }) builder.add_edge(tools, agent) # 关键工具执行完回到 agent形成循环 builder.add_conditional_edges(check_logistics, route_after_check, { human_approval: human_approval, END: END }) builder.add_edge(human_approval, END)注意builder.add_edge(tools, agent)这一行它就是循环的来源。工具执行完把结果塞回 messagesagent 再读一遍决定下一步。这就是 ReAct 模式的图化表达。3.4 编译、持久化与人工中断编译时挂上 checkpointer 和 interruptfrom langgraph.checkpoint.sqlite import SqliteSaver with SqliteSaver.from_conn_string(checkpoints.db) as checkpointer: app builder.compile( checkpointercheckpointer, interrupt_before[human_approval] )interrupt_before[human_approval]的意思是执行到human_approval节点之前暂停把控制权交还给调用方。调用方可以检查 State、做人工决策然后决定是否继续。调用方式config {configurable: {thread_id: order-001}} result app.invoke( {messages: [(user, 帮我查一下订单 A123 的物流)], order_id: A123}, config ) # 如果触发了 interrupt检查状态 state app.get_state(config) if state.next: # 有下一个待执行节点说明被中断了 print(需要人工审批当前物流停滞天数, state.values[logistics_days]) # 人工确认后继续 app.invoke(None, config) # 传 None 表示从断点继续app.invoke(None, config)这个用法很关键——传None表示我不提供新输入从上次断点继续跑。这是 LangGraph 人工介入的标准姿势。4. 实战中踩过的坑与排查速查表4.1 那些官方文档没明说的注意事项坑一State 字段没挂 reducer 导致消息丢失。前面提过但值得再强调。判断标准很简单这个字段是累积语义还是覆盖语义累积就挂 reducer。坑二条件边函数返回值不在映射字典里。比如你返回了tool但映射字典里写的是toolsLangGraph 会直接报错。我习惯把映射字典的键和路由函数的返回值用同一组常量避免手滑。坑三循环没有终止条件导致死循环。ReAct 循环一定要有最大轮数限制。可以在 State 里加retry_count每次 agent 节点自增超过阈值强制走 END。我一般设 10 轮超过就返回抱歉我需要转人工。坑四Checkpointer 的 thread_id 管理混乱。每个独立会话必须用不同的thread_id否则历史会串。生产环境建议用用户ID 会话ID组合。坑五interrupt 之后 State 没更新。人工审批节点如果只是确认记得在恢复时把approved之类的标记写进去否则后续节点读不到审批结果。4.2 常见问题速查表现象可能原因排查方向对话历史只剩最后一条messages 没挂 add_messages检查 State 定义图跑起来直接结束入口没设或边连错打印 graph.get_graph() 看结构条件边报 KeyError路由返回值不在映射里对齐返回值和映射键死循环缺终止条件加 retry_count 上限恢复后重复执行thread_id 不一致确认 config 一致工具调用结果读不到ToolNode 输出格式没解析对打印 messages 看结构内存持续增长用了 MemorySaver 且没清理换持久化 Checkpointer4.3 我个人的调试习惯我调试 LangGraph 有个固定套路先用app.get_graph().draw_mermaid()把图结构打印出来虽然本文不放图但你自己调试时这个 API 很好用确认节点和边连对了然后在每个节点函数开头加一行print(f[{node_name}] state{state})看状态流转最后用app.get_state_history(config)回放历史 checkpoint定位是哪一步出的问题。这套流程走下来90% 的问题能在 10 分钟内定位。LangGraph 的调试体验比手写 Agent 循环好太多因为它的状态是显式的、可回放的而不是藏在某个闭包里。5. 从单 Agent 到多 AgentLangGraph 的进阶玩法5.1 子图即子 Agent组合方式比想象中简单当你把单个 Agent 玩明白之后多 Agent 协作就是水到渠成的事。LangGraph 里最优雅的多 Agent 实现方式是子图Subgraph把一个编译好的图当成另一个图的节点。# 先编译一个查询 Agent子图 query_agent query_builder.compile() # 再把它作为一个节点塞进主图 main_builder.add_node(query_agent, query_agent)主图负责路由分发子图负责具体任务各司其职。这种设计的好处是每个子 Agent 可以独立开发、独立测试、独立部署最后在主图里组装。我做过一个客服系统主图路由到售前咨询售后处理技术支持三个子图每个子图内部又是完整的 ReAct 循环维护起来非常清爽。5.2 多 Agent 协作的两种主流模式监督者模式Supervisor一个 supervisor 节点负责决策下一步交给哪个 Agent各个 worker Agent 执行完把结果汇报回 supervisor。适合任务边界清晰、需要统一调度的场景。网络模式NetworkAgent 之间可以互相调用没有中心调度。适合 Agent 数量少、协作关系松散的场景。LangGraph 官方有个langgraph-supervisor和langgraph-swarm库封装了这两种模式可以直接用。我个人的经验是Agent 数量少于 3 个用网络模式超过 3 个用监督者模式。因为 Agent 一多没有中心调度很容易乱套调试起来也痛苦。5.3 关于扣子是不是 LangGraph 实现的这类问题经常有人问某个低代码平台是不是基于 LangGraph 做的。我的看法是这类平台底层大概率用了类似的图编排思想但具体实现是不是 LangGraph 不重要重要的是理解图编排这套范式本身。你学会了 StateGraph 的思维模型换任何框架都能快速上手因为核心概念是相通的状态、节点、边、检查点。工具会变范式不会。6. 学习路径与选型建议6.1 什么阶段该学 LangGraph我的建议是先把 LangChain 的 LCEL 写熟能独立完成 RAG 问答和简单工具调用再学 LangGraph。因为 LangGraph 的节点里大量用到 LangChain 组件基础不牢直接上 LangGraph 会很痛苦。学习顺序我推荐LCEL 基础 → Tool 定义与调用 → 手写一个 ReAct 循环理解原理→ LangGraph StateGraph → Checkpointer → 人工介入 → 多 Agent。每一步都动手写代码别只看文档。LangGraph 官方文档质量不错但例子偏简建议配合实际项目练。6.2 选型决策树给你一个简单的判断流程需求是纯线性处理翻译、摘要、单轮问答→ 用 LCEL 就够需求有循环多轮工具调用、反思重试→ 上 LangGraph需求有人工审批或断点续跑 → 必须 LangGraph需求有多 Agent 协作 → LangGraph 子图或 supervisor 模式。别为了追新而过度设计。我见过有人用 LangGraph 写一个单轮翻译功能图里就俩节点纯属杀鸡用牛刀。工具是拿来解决问题的不是拿来炫技的。6.3 一个我常用的最小可运行模板最后分享一个我每次开新项目都会复制的最小模板帮你省去搭骨架的时间from typing import TypedDict, Annotated from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from langgraph.checkpoint.memory import MemorySaver from langchain_openai import ChatOpenAI class State(TypedDict): messages: Annotated[list, add_messages] llm ChatOpenAI(modelgpt-4o-mini) def chatbot(state: State) - dict: return {messages: [llm.invoke(state[messages])]} builder StateGraph(State) builder.add_node(chatbot, chatbot) builder.add_edge(START, chatbot) builder.add_edge(chatbot, END) app builder.compile(checkpointerMemorySaver()) config {configurable: {thread_id: test-1}} while True: user_input input(你) if user_input quit: break for event in app.stream({messages: [(user, user_input)]}, config): for node_output in event.values(): print(AI, node_output[messages][-1].content)这个模板跑通之后往里面加工具、加条件边、加 interrupt就是完整的 Agent 了。我实际用下来从零到能跑通一个带工具调用的 Agent熟悉的话半小时内能搞定。踩过几次坑之后我最大的体会是LangGraph 的学习成本主要不在 API而在思维方式的转变——从写一条流水线变成设计一张状态机。一旦这个弯转过来你会发现以前用 LCEL 写得磕磕绊绊的需求用 LangGraph 表达起来自然得多。尤其是那个add_edge(tools, agent)形成的循环第一次看到它跑起来的时候我是真觉得这才是 Agent 该有的样子。