
1. 从LangGraph的定位说起它到底解决什么问题第一次接触LangGraph的人十有八九是被LangChain的链式调用折腾过一轮之后才找过来的。我最初也是这个路径用LangChain搭了一个RAG问答跑通demo很爽但一旦想加个“如果检索结果不相关就换个query重试”的逻辑整个链条就开始变得别扭。LCEL的管道符写起来优雅可它本质上是线性的遇到需要循环、分支、状态回传的场景就得靠各种hack去绕。LangGraph的核心价值就在这儿它把Agent的执行过程建模成一张有向图节点是计算单元边是控制流整个图的运行状态由一个共享的State对象承载。这个模型听起来简单但它解决的是LLM应用里最棘手的一类问题——多步骤、有条件跳转、需要人工介入、需要循环重试的复杂编排。举个具体的例子。假设你要做一个客服Agent流程是这样的先判断用户问题类型如果是退款就走退款流程如果是技术问题就走技术支持流程技术问题里如果第一轮没解决就升级到人工。用LangChain的Chain来写你得嵌套一堆RunnableBranch代码可读性急剧下降。用LangGraph你画一张图一个分类节点两条条件边分别指向退款节点和技术支持节点技术支持节点再连一个条件边指向“解决”或“升级人工”。每个节点就是一个函数输入State输出State的更新。逻辑一目了然。LangGraph和LangChain的关系也值得说清楚这是热搜里问得最多的。LangChain是一个大而全的框架提供了模型封装、工具、记忆、检索等大量组件LangGraph是LangChain生态里的一个编排层专注于有状态、多步骤的Agent流程控制。你可以只用LangGraph不用LangChain的Chain也可以把LangChain的组件塞进LangGraph的节点里。它们不是替代关系而是不同层次的工具。我个人的习惯是简单的单轮调用用LangChain的LCEL就够了一旦涉及循环、分支、多Agent协作直接上LangGraph。这篇文章适合谁看如果你已经用LangChain或直接调API做过一些LLM应用现在想把手上的demo升级成能处理复杂逻辑的生产级Agent那LangGraph就是你要找的东西。如果你还没接触过LLM开发建议先把OpenAI API的基本调用和Prompt工程过一遍再回来。下面我会从设计思路、核心概念、实操步骤到踩坑经验完整走一遍。2. LangGraph的核心概念拆解与设计思路2.1 StateGraph一切从状态开始LangGraph最核心的抽象是StateGraph。你可以把它理解成一个“带状态的流程图”。传统流程图里数据在节点之间通过参数传递每个节点是独立的函数。LangGraph不一样它维护一个全局的State每个节点读取State的一部分返回要更新的字段框架负责合并。这个设计的好处是什么它让节点之间的通信变得极其自然。比如你在节点A里往State的messages列表追加了一条消息节点B直接就能读到不需要显式传参。对于多轮对话、工具调用链这种场景状态共享是刚需。State的定义通常用TypedDict或者Pydantic模型。我一般用TypedDict因为轻量。关键是要给每个字段指定reducer也就是“当多个节点都更新同一个字段时怎么合并”。默认是覆盖但像消息列表这种你需要用add_messages这个reducer来追加而不是覆盖。from typing import Annotated, TypedDict from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] user_intent: str retry_count: int这里messages用了add_messages意味着任何节点返回的messages都会被追加到列表里而不是替换。user_intent和retry_count没有指定reducer默认就是覆盖。这个细节很关键我见过不少人因为忘了加reducer导致消息被覆盖调试半天。2.2 节点与边计算与控制的分离节点就是普通的Python函数签名是(state) - dict。返回的dict里包含要更新的State字段。节点可以是同步的也可以是异步的LangGraph都支持。边分两种普通边和条件边。普通边就是A执行完直接到B。条件边是A执行完后根据一个路由函数的返回值决定去B还是C。路由函数接收State返回一个字符串这个字符串对应你在add_conditional_edges里注册的映射。def route_by_intent(state: AgentState) - str: if state[user_intent] refund: return refund_node return tech_support_node graph.add_conditional_edges(classify, route_by_intent, { refund_node: refund, tech_support_node: tech_support })这种设计把“做什么”和“下一步去哪”彻底分开了。节点只管计算路由只管决策。好处是每个部分都可以单独测试。我写LangGraph的时候路由函数基本都会单独写单元测试因为它是整个流程的骨架错了整个图就跑偏。2.3 Checkpointer让Agent拥有记忆LangGraph另一个杀手级特性是内置的持久化机制。通过Checkpointer每次图执行完一个节点State都会被保存。这意味着你可以让Agent记住多轮对话的上下文在人工介入后从断点恢复实现“时间旅行”回到之前的某个状态重新执行Checkpointer有内存版和数据库版。开发阶段用MemorySaver就够了生产环境一般用SqliteSaver或PostgresSaver。配置方式是在编译图的时候传入from langgraph.checkpoint.memory import MemorySaver memory MemorySaver() app graph.compile(checkpointermemory)然后调用的时候传一个thread_id同一个thread_id的多次调用会共享状态。这个机制对于构建有记忆的Agent来说比自己在外面维护一个字典要靠谱得多。2.4 为什么是图而不是链这个问题我被问过很多次。链式调用Chain的本质是线性的A→B→C→结束。图Graph的本质是网络可以有环、有分支、有汇合。LLM应用的真实需求往往不是线性的工具调用需要循环调工具→看结果→再调多Agent协作需要分支和汇合人工审核需要中断和恢复。这些用链来表达都很别扭用图就很自然。LangGraph的图模型还有一个隐含优势它是可序列化的。整个图的结构、每个节点的配置、State的schema都可以导出成JSON。这对于调试、可视化、甚至动态生成Agent流程都很有价值。我在项目里就做过根据配置文件动态构建图的事情不同客户配不同的流程代码完全不用改。3. 从零搭建一个LangGraph Agent的完整实操3.1 环境准备与依赖安装先把环境弄干净。我习惯用conda建一个独立环境避免和系统Python打架。conda create -n langgraph-demo python3.11 conda activate langgraph-demo pip install langgraph langchain-openai langchain-core版本方面LangGraph迭代很快建议锁一个较新的版本。我写这篇文章时用的是langgraph0.2.xAPI和0.1.x有一些变化比如START和END的导入路径。如果你看的是老教程注意对照官方文档确认。API Key通过环境变量传不要硬编码在代码里export OPENAI_API_KEYyour-key-here如果你用的是其他模型LangChain提供了统一的接口换成ChatAnthropic或ChatGLM都行LangGraph本身不绑定模型。3.2 定义State和工具我们做一个“智能客服”Agent能查订单、能退款、能回答一般问题。先定义Statefrom typing import Annotated, TypedDict from langgraph.graph.message import add_messages class CustomerServiceState(TypedDict): messages: Annotated[list, add_messages] order_id: str intent: str resolved: bool然后定义工具。工具就是普通的Python函数用tool装饰器包一下from langchain_core.tools import tool tool def query_order(order_id: str) - str: 根据订单号查询订单状态 # 实际项目里这里查数据库 mock_db {12345: 已发货, 67890: 待付款} return mock_db.get(order_id, 订单不存在) tool def process_refund(order_id: str) - str: 对指定订单发起退款 return f订单{order_id}退款已提交3个工作日内到账工具的描述docstring很重要LLM就是靠这个来决定调哪个工具的。描述要写清楚“这个工具做什么、什么时候用、参数是什么”。3.3 构建图结构现在开始搭图。我一般先把节点函数写好再连边。from langgraph.graph import StateGraph, START, END from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini) llm_with_tools llm.bind_tools([query_order, process_refund]) def classify_intent(state: CustomerServiceState): 判断用户意图 last_msg state[messages][-1] prompt f判断以下用户消息的意图只返回refund、query或other{last_msg.content} result llm.invoke(prompt) return {intent: result.content.strip()} def handle_query(state: CustomerServiceState): 处理查询类请求 response llm_with_tools.invoke(state[messages]) return {messages: [response]} def handle_refund(state: CustomerServiceState): 处理退款请求 response llm_with_tools.invoke(state[messages]) return {messages: [response]} def general_response(state: CustomerServiceState): 一般问题直接回答 response llm.invoke(state[messages]) return {messages: [response]}路由函数def route_intent(state: CustomerServiceState) - str: intent state.get(intent, other) if intent refund: return refund elif intent query: return query return general组装图builder StateGraph(CustomerServiceState) builder.add_node(classify, classify_intent) builder.add_node(query, handle_query) builder.add_node(refund, handle_refund) builder.add_node(general, general_response) builder.add_edge(START, classify) builder.add_conditional_edges(classify, route_intent, { query: query, refund: refund, general: general }) builder.add_edge(query, END) builder.add_edge(refund, END) builder.add_edge(general, END) graph builder.compile()跑一下result graph.invoke({ messages: [(user, 我的订单12345到哪了)], order_id: , intent: , resolved: False }) print(result[messages][-1].content)这个例子虽然简单但已经包含了LangGraph的所有核心要素State、节点、条件边、START/END。你可以在这个骨架上加循环比如工具调用后回到LLM节点、加人工审核节点、加Checkpointer。3.4 加入工具调用循环上面的例子其实有个问题handle_query里LLM可能会返回一个tool_call但图没有处理这个tool_call就结束了。真实的Agent需要“LLM决定调工具→执行工具→把结果喂回LLM→LLM再决定”这个循环。LangGraph提供了ToolNode来简化这个流程from langgraph.prebuilt import ToolNode, tools_condition builder.add_node(tools, ToolNode([query_order, process_refund])) # 在query和refund节点后面加条件边 builder.add_conditional_edges(query, tools_condition) builder.add_conditional_edges(refund, tools_condition) builder.add_edge(tools, query) # 工具执行完回到querytools_condition是内置的路由函数它会检查最后一条消息里有没有tool_calls有就去tools节点没有就结束。ToolNode会自动执行工具并把结果包装成ToolMessage追加到messages里。这个循环是Agent的核心。我踩过的一个坑是循环没有终止条件。如果LLM一直调工具不停图会无限执行。LangGraph有recursion_limit参数可以限制最大步数默认是25。生产环境一定要设这个值并且做好超限后的兜底处理。4. 进阶技巧与生产环境注意事项4.1 人工介入Human-in-the-loop的正确姿势很多业务场景需要人工审核比如退款金额超过一定阈值要人工确认。LangGraph通过interrupt机制支持这个。在节点里调用interrupt()会暂停图的执行把当前State保存到Checkpointer然后返回给调用方。调用方拿到中断信息后可以展示给人工人工确认后通过Command(resume...)恢复执行。from langgraph.types import interrupt, Command def refund_with_approval(state: CustomerServiceState): if needs_approval(state): decision interrupt({question: 是否批准这笔退款, order_id: state[order_id]}) if decision reject: return {messages: [(assistant, 退款被拒绝)]} return {messages: [(assistant, 退款已处理)]}恢复的时候app.invoke(Command(resumeapprove), config{configurable: {thread_id: 1}})这里的关键是必须配置Checkpointer否则中断后状态就丢了。另外thread_id要一致不然恢复不到正确的状态。4.2 多Agent协作的两种模式LangGraph支持多Agent常见的有两种模式Supervisor模式一个主管Agent负责分派任务给子Agent子Agent执行完把结果汇报给主管。图结构是主管节点连条件边到各个子Agent子Agent连边回主管。Swarm模式Agent之间直接交接没有中心节点。每个Agent可以决定把控制权交给另一个Agent。适合流程比较动态的场景。我个人的经验是Supervisor模式更好调试因为控制流是集中的出问题容易定位。Swarm模式更灵活但容易出现Agent之间互相踢皮球的情况。选哪种取决于你的业务复杂度。4.3 流式输出与可观测性LangGraph支持多种流式模式values每个节点后的完整State、updates每个节点的增量更新、messagesLLM的token流。前端要展示“Agent正在思考”的效果用messages模式最合适。for chunk in app.stream(inputs, stream_modemessages): print(chunk)可观测性方面LangGraph和LangSmith是天然集成的。设置LANGCHAIN_TRACING_V2true和LANGCHAIN_API_KEY所有执行轨迹都会上报到LangSmith可以看到每个节点的输入输出、耗时、token消耗。调试复杂图的时候这个功能救命。5. 常见问题排查与避坑指南5.1 状态更新不生效最常见的原因是忘了加reducer。比如你定义messages: list节点返回{messages: [new_msg]}默认行为是覆盖原来的消息全没了。正确做法是Annotated[list, add_messages]。另一个原因是节点返回了State里没有的字段。LangGraph会忽略未定义的字段不报错但也不生效。建议用TypedDict并在开发阶段开启严格模式。5.2 条件边路由错误路由函数的返回值必须和add_conditional_edges里注册的key完全匹配包括大小写。我见过有人返回Refund但注册的是refund结果图直接报错。建议把路由的返回值定义成常量避免手写字符串。5.3 循环无法终止前面提过设recursion_limit。另外在路由函数里加逻辑判断比如retry_count超过3次就强制走结束分支。不要指望LLM自己会停它没有“我调太多次了该停了”这个概念。5.4 Checkpointer相关的坑用SqliteSaver的时候注意数据库文件的路径要可写。用PostgresSaver的时候记得先跑建表语句。另外不同版本的LangGraph Checkpointer的表结构可能不一样升级版本后如果报错先检查是否需要迁移。还有一个隐蔽的坑thread_id如果重复使用会读到旧的状态。测试的时候记得每次用新的thread_id或者手动清理。问题现象可能原因排查方法消息被覆盖缺少add_messages reducer检查State定义路由报错返回值与注册key不匹配打印路由函数返回值图无限执行缺少终止条件设置recursion_limit中断后无法恢复未配置Checkpointer检查compile参数工具不被调用工具描述不清晰优化docstring5.5 性能优化经验LangGraph本身的开销很小瓶颈通常在LLM调用和工具执行上。几个优化方向并行执行如果多个节点之间没有依赖可以用SendAPI并行分发。比如同时查多个数据源。缓存LLM调用加缓存相同输入直接返回。LangChain有set_llm_cache。精简StateState里不要放太大的对象Checkpointer每次都会序列化整个State。大文件、图片这些放外部存储State里只存引用。我在实际项目里遇到过一次性能问题State里存了一个很大的检索结果列表每次Checkpointer序列化都要几百毫秒。后来改成只存文档ID需要的时候再查性能提升明显。6. 一些个人体会LangGraph刚出来的时候我觉得它就是把LangChain的AgentExecutor重写了一遍没什么新意。但用久了发现图模型带来的表达力提升是质变的。以前写Agent像是在拼乐高只能按说明书拼现在像是在画流程图想怎么连就怎么连。学习曲线方面如果你熟悉状态机和图论的基本概念上手很快。如果不熟建议先花半小时了解一下“有向图”和“状态机”是什么不用深入知道节点、边、状态转移这几个词就够了。最后分享一个调试技巧把图可视化出来。LangGraph提供了get_graph().draw_mermaid()方法虽然我这里不能用mermaid图表但你可以在本地跑一下把生成的图贴到支持mermaid的编辑器里看。复杂的图一眼就能看出结构问题比看代码快得多。这个框架还在快速迭代API时不时会变。我的建议是锁定一个版本把官方文档的Quick Start和How-to部分过一遍然后直接上手写。看再多教程不如自己搭一个能跑的Agent踩几个坑理解就深刻了。