先直接讲结论LangGraph对我来说是今年在AI Agent项目里用得最顺手的一个编排框架。它不是又一个套壳封装而是把LangChain时代的“链式思维”彻底改成“图式思维”来设计Agent工作流。早期用LangChain搭Agent最痛苦的就是流程写死想加个分支判断要不拆好几个Chain拼接要不直接塞进Prompt靠模型自觉后来遇到LangGraph才意识到问题出在抽象层级选错了。如果你现在正在做一个需要多步骤推理、中途可能要反复调用工具、甚至需要人工介入确认的智能体应用LangGraph这套基于图结构的编排思路大概率能帮你省掉很多重复设计。这篇实战指南我会从为什么选图模型、几个核心概念怎么理解、最小可运行示例怎么搭、状态和检查点怎么设计到最常见的坑和排查技巧完整过一遍我自己实操积累的东西。适合两类人看一是LangChain用了一段时间但觉得Chain方式做复杂Agent太别扭的开发者二是刚接触LangGraph想知道这套东西和普通工作流引擎到底差在哪的新手。看完基本上能从零跑通一个带工具调用和人工确认的Agent也能避开我踩过的大部分坑。1. 为什么是LangGraph从链式调用到图式编排1.1 链式结构的天花板先把老问题摆出来。用LangChain写普通的问答、摘要、SQL生成这类单次调用的任务Chain完全够用跑起来也简洁。但一旦进入Agent场景问题就来了Agent的本质是一个带循环思考和工具调用的运行循环模型判断“我是否需要再查一次数据”这种决策在代码里的表达用Chain强行做会变成什么样子我最早期用LangChain实现一个需要“查库存 - 算价格 - 确认订单”的客服Agent代码风格基本是这样的先定义一个工具函数列表然后初始化一个AgentExecutor让模型在Prompt的约束下反复调用工具直到它自己觉得可以输出最终答案。这种方式的问题是控制力极弱。用户说一句“我要买那个便宜点的”模型可能连续调用五次库存查询或者完全无视库存数据直接编一个价格。你只能通过疯狂调Prompt措辞、调模型温度、调工具描述来“感化”它没有任何结构性手段强制它按步骤走。更麻烦的是很多真实业务场景根本不允许模型自由发挥。比如订单确认这个节点你需要在生成订单之前强制插入一个用户确认步骤“订单是XXX确认下单吗”这类必须被执行的流程节点在自由Agent循环里没法保证一定出现。那时候我特别希望有一个东西既能保留Agent的循环智能又能让我像画流程图一样把节点固定下来LangGraph正好就是这个东西。1.2 图模型到底改了什么LangGraph并不是彻底推翻LangChain它更像是把LangChain组件模型、工具、提示词重新组织成一张有向图。核心思想是一个Agent任务被拆分成若干节点Node节点和节点之间的连接关系被称为边Edge整张图维护一份共享的状态State每个节点执行完就更新状态然后根据现状决定下一步走哪条边。这个模型带来的变化是本质性的。第一流程可见了所有逻辑分支被显式画出而不是藏在Prompt里。第二可控性变强了我可以强制某个节点必须执行也可以设置条件边Conditional Edge让模型或代码来决定流向。第三循环成为一等公民图可以走回前面的节点这是Chain做不到的。第四支持断点、持久化和人工介入这非常契合真业务里的审核、确认、暂停场景。拿刚才那个订单Agent举例用LangGraph我可以明确画出接收用户需求 - 调用库存工具查可选商品 - 计算价格并展示 - 等待用户确认 - 生成订单。如果用户不满意价格还能从“计算价格”节点往回跳。这套结构对模型智能的依赖大大降低因为每一步都在图的控制下。我自己的体会是LangGraph把Agent从“玄学召唤模型智能”变成了“工程化编排节点逻辑”后者才是能上线、能调试、能背锅的方案。2. 核心概念拆解先把图模型装进脑子2.1 图的构成节点、边和状态学LangGraph首要任务不是看API文档而是理解它对你整个业务是怎么建模的。任何一个LangGraph应用都逃不开三个核心概念State、Node、Edge。State是整张图的数据中枢它是一个规范化的数据类在Python里通常是TypedDict定义了所有节点之间需要共享的数据字段。你可以简单理解为一个全图可见的“数据背包”每个节点往里塞自己产出的数据也会读取前序节点塞进去的数据。比如我的订单AgentState至少会有这几个字段user_input用户原话、retrieved_products查到的商品列表、order_confirmed布尔值、final_result最终输出。Node就是图上的一个处理单元本质是一个接收当前State、处理后返回新的State字段或状态的函数。节点里可以干任何事调用大模型、调外部接口、跑一段业务规则、甚至只是做个格式转换。在我的项目里节点一般按照“单任务”来划分比如“查询库存”一个节点、“生成报价”一个节点、“生成订单”一个节点保证每个节点代码都在100行以内方便测试和替换。Edge用来描述节点之间的流转方向分为普通边和条件边。普通边是“执行完A必须执行B”条件边是“执行完A之后根据判断决定去B还是去C”。条件判断可以是一段纯Python逻辑也可以是大模型返回的结构化结果。这就让我能把“用户是否确认”“查询结果是否为空”这类判断故障明确写出来。2.2 节点与边的运行规则刚开始画图的时候最容易犯的错是“把边画太满”觉得必须每个节点之间都连起来才行。实际上LangGraph的图运行时只做一件事从入口点开始执行每执行完一个节点就把这个节点的返回值合并进全局State然后看当前节点的下一个边是什么继续走下去。没有走的边不会被执行不是图上有连线就必然经过。这里把LangGraph的运行规则理解透很重要因为调试错误经常就是因为想当然地认为某个节点会执行结果实际运行时被条件边跳过了。我可以总结成三条运行规则状态是全局唯一的任何节点返回的字段都会覆盖更新到State中且遵循后写优先生效除非你配置了自定义Reducer。每条边只在上游节点结束之后触发一次判断条件边返回的是目标节点的名称字符串跳转后从目标节点继续执行。整张图被执行时像一个状态机不断在“执行节点 - 合并状态 - 走向下一条边”中循环直到到达END节点才停止。这个机制带来的好处是因果链条非常清晰。我在排查线上Agent问题时只需要把每次节点返回的状态打日志整条用户请求的处理链路一目了然这在Chain那种黑盒AgentExecutor里是完全做不到的。所以先花一小时把这个模型在脑子里搭清楚后面调代码会顺很多。3. 环境搭建与最小可运行示例3.1 安装与版本选择LangGraph是纯Python包和LangChain生态一起用最顺手但也可以只单独用LangGraph配合自己的大模型API。安装命令很简单pip install langgraph langchain-core langchain-openai版本选择上我建议优先使用0.2.x及以上版本0.1版本时期的API变动比较频繁很多教程代码跑会报错就是因为版本差异。如果需要在图里使用LangChain生态里现成的工具库再补一个langchain-community。我自己的做法是项目一开始就同时安装langgraph-cli这个命令行工具对后面启动服务来说很有用pip install langgraph-cli装完之后可以在Python里验证版本号import langgraph print(langgraph.__version__)实测下来LangGraph对Python版本要求不苛刻3.9到3.12我都在不同项目里用过没有遇到兼容性障碍。不过如果你的Python环境是3.8以下还是先升级环境再玩这个不然会有很多typing语法上的坑。3.2 一个最小Agent让图先跑起来我建议不要一开始就上复杂例子先搞一个最简单的“两步图”跑通对建立手感很有帮助。为了演示可控性我让这个图包含一个判断逻辑输入一个数字如果大于等于0就走“正数处理”节点否则走“负数处理”节点。直接上完整代码from typing import TypedDict, Literal from langgraph.graph import StateGraph, END class MyState(TypedDict): input_num: int result: str def handle_positive(state: MyState): return {result: f这是正数{state[input_num]}} def handle_negative(state: MyState): return {result: f这是负数{state[input_num]}} def route_by_sign(state: MyState) - Literal[handle_positive, handle_negative]: if state[input_num] 0: return handle_positive return handle_negative # 构建图结构 builder StateGraph(MyState) builder.add_node(handle_positive, handle_positive) builder.add_node(handle_negative, handle_negative) # 设置入口和条件边 builder.set_entry_point(input) # 这里我们会用一个虚拟入口节点写到这里我发现需要说明一下上面这个例子为了简化没有单独建一个入口节点。LangGraph的入口点指向的是第一个执行节点所以可以稍微调整一下代码把入口点直接指向一个“判断节点”from typing import TypedDict, Literal from langgraph.graph import StateGraph, END class MyState(TypedDict): input_num: int result: str def route_by_sign(state: MyState) - Literal[handle_positive, handle_negative]: if state[input_num] 0: return handle_positive return handle_negative def handle_positive(state: MyState): return {result: f这是正数{state[input_num]}} def handle_negative(state: MyState): return {result: f这是负数{state[input_num]}} builder StateGraph(MyState) builder.add_node(route_by_sign, route_by_sign) builder.add_node(handle_positive, handle_positive) builder.add_node(handle_negative, handle_negative) builder.set_entry_point(route_by_sign) builder.add_conditional_edges(route_by_sign, route_by_sign) builder.add_edge(handle_positive, END) builder.add_edge(handle_negative, END) app builder.compile() # 测试 result app.invoke({input_num: -5}) print(result)这段代码跑通之后你会对“条件边”有一个直观感觉route_by_sign这个节点里没有调用任何大模型就是一段纯逻辑它根据状态里的字段决定图往哪走。这种确定性控制就是LangGraph和纯模型驱动Agent的最大区别。回调输出结果大概是这样的{input_num: -5, result: 这是负数-5}状态里保留了所有经过节点的数据这对调试来说非常宝贵。你不需要猜模型到底在想什么直接看State字段变化就够了。4. 状态管理与数据流转认准StateSchema4.1 状态定义与字段规约StateSchema是整个图的“数据库表结构”它的设计直接决定了图里所有节点的接口契约。如果一张图里各节点之间数据依赖很混乱多半是StateSchema没设计好。我一般遵循几条原则第一条字段粒度宁可细一些也不要大而全。比如用户需求、工具调用结果、对话历史、最终答案这些我会拆成独立的字段不要塞在一个大JSON字段里。字段拆得细节点之间依赖关系就清晰调试时也能定位到具体的字段是怎么污染的。第二条区分数据是由大模型生成的还是由业务代码生成的。由大模型生成的数据字段类型尽量用字符串或对象不要用列表因为模型输出的列表格式不稳定由业务代码生成的可以直接用复杂类型比如库存查询结果列表、订单对象等。第三条对需要追加而不是覆盖的字段显式指定Reducer。比如对话历史这个字段它需要把每一轮的新消息追加到旧消息后面而不是用新消息覆盖旧消息。LangGraph的State默认是覆盖式更新如果你直接返回{messages: [new_message]}历史消息会被清空。正确做法是给这个字段配置一个Reducer函数from typing import TypedDict, Annotated from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] user_intent: str tool_results: dict上面这个Annotated[list, add_messages]表示每次节点返回新的消息列表时自动把旧列表和新列表拼接起来。类似的自定义Reducer也可以写比如把多个节点产出的字典字段合并成一个总字典。4.2 跨节点传递数据时的常见问题我刚用LangGraph的前两周基本每周都要踩一次状态字段相关的坑。第一个坑是节点函数返回的字段和StateSchema不一致运行时不报错只是这个返回值被静默丢弃。比如State里定义了tool_result节点粗心返回了tool_reslut拼写错了图照常跑结果所有后续节点读这个字段都读不到值需要排查半天。所以节点返回值的键名一定要和StateSchema保持严格一致最好在节点函数内部做一个断言检查。第二个坑是把大对象全量塞进State。比如我把整个数据库查询结果DataFrame直接放进State一执行图内存直接飙升图一复杂状态还会被多次复制性能会非常难看。实战中我只会把结构化摘要、关键ID、排序后的TopN结果放进State原始数据需要用时在节点内通过服务层按需获取。第三个坑是并发更新冲突。如果你用了LangGraph的多个分支并发执行后续版本支持SendAPI做动态并行多个分支同时更新同一字段可能会出现“最后一个写入者胜出”的竞态问题。解决办法是对这个字段配置自定义Reducer合并逻辑比如用取最大值、取均值或拼接列表。特别是计数器、汇总得分这类字段一定要预设好合并规则。状态跨节点的流转是LangGraph的心智核心我自己的代码规范是所有节点函数第一行声明本节点依赖哪些State字段最后一行声明会更新哪些字段写在注释里。这样跑久了不会乱回头看也友好。5. 条件分支与工具调用图的可控性核心5.1 用条件边实现路由上一节的数字判断属于最简单的条件边实际项目里条件边判断的复杂度和场景远比这个夸张。我遇到过几类比较典型的路由内容分类路由用户的输入先过一个分类节点根据分类结果跳转到不同的处理流程。质量与重试路由模型生成的结果经过一个验证节点如果不通过跳回生成节点重新生成带重试次数上限。兜底话术路由当所有检索结果为空时走一条专门应对空场景的节点而不是直接用模型硬答。条件边在LangGraph里实现非常直白就是写一个接收State、返回目标节点名的函数。比如质量路由def route_by_quality(state: MyState) - Literal[generate_answer, regenerate, fallback]: score state[answer_score] if score 0.8: return generate_answer if score 0.5: return regenerate return fallback然后把这个函数传给add_conditional_edges图中就多了一个动态决策点。实际部署时条件边的好处还体现在A/B测试和控制台上。因为每次路由的结果都会记录在执行轨迹里我可以把质量路由的走向做成监控指标比如“兜底路由占比”高就说明当前的生成或检索环节有问题该调参调参该换策略换策略。这种可观测性比传统自由式Agent强太多。5.2 工具调用与Agent循环工具调用是Agent应用的重头戏LangGraph里天然支持在节点内循环调用工具。基本的套路是这样from langchain_core.messages import AIMessage, ToolMessage, HumanMessage from langchain_core.tools import tool from langgraph.prebuilt import ToolNode # 定义一个简单工具 tool def get_product_price(product_id: str) - str: 根据商品ID查询当前价格 # 这里替换为真实的库存/价格服务 price_map {A100: 1999元, B200: 2999元} return price_map.get(product_id, 商品不存在) # 构建一个带模型调用的节点 def call_model(state: AgentState): messages state[messages] # 这里替换为你的模型服务封装 from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0) llm_with_tools llm.bind_tools([get_product_price]) response llm_with_tools.invoke(messages) return {messages: [response]} # 构建图 workflow StateGraph(AgentState) workflow.add_node(agent, call_model) workflow.add_node(tools, ToolNode([get_product_price])) workflow.set_entry_point(agent) # 核心如果模型返回的是工具调用指令就跳去执行工具 def should_continue(state: AgentState) - Literal[tools, END]: last_message state[messages][-1] if isinstance(last_message, AIMessage) and last_message.tool_calls: return tools return END workflow.add_conditional_edges(agent, should_continue, {tools: tools, END: END}) workflow.add_edge(tools, agent) # 工具执行完回到agent继续判断这段代码非常典型建议重点吸收。它的逻辑链条很清晰模型节点判断当前对话是否需要工具如果AIMessage里带了tool_calls字段就走工具节点执行工具执行完把结果作为ToolMessage塞回状态然后回到模型节点继续推理直到模型认为不需要再调用工具直接输出答案。这个循环其实就是LangGraph自动帮我们跑起来的。用图的好处是我可以在这个循环里插入一道“人工检查节点”比如在工具执行完回来之后先看下工具返回的数据是否合理再决定是否交给模型处理。这是很多真实落地场景的刚需。6. 检查点与持久化让记忆不丢失6.1 Checkpointer的作用与配置Agent应用一旦做长对话就需要一个能跨多轮保存状态的能力。LangGraph很早就内置了检查点Checkpoint机制它会把每一步执行后的State快照保存到持久化后端中这样即使服务重启也能恢复整张图的状态继续跑未完成的任务。检查点在LangGraph里有两种使用场景最重要的一个是人工介入。比如写一个“用户确认订单”的流程先执行到确认节点然后暂停图把当前状态存下来等用户回复确认或取消后再从暂停的地方继续。这个能力在业务里的价值极高靠链式结构做非常麻烦LangGraph几乎是原生就支持了。使用检查点是两步走的事情。首先在编译图时传入一个Checkpointerfrom langgraph.checkpoint.memory import MemorySaver memory MemorySaver() app builder.compile(checkpointermemory)然后执行时不再用invoke而是用带线程ID的invoke线程ID就是会话标识config {configurable: {thread_id: order-12345}} result app.invoke({input_num: 5}, config)有了线程ID之后LangGraph会把每次执行的状态快照都保存在这个线程下。下次用同一个线程ID再次调用图就可以基于上次的状态继续走而不是从零开始。6.2 断点续跑与人工审核场景断点续跑和人工审核是两个高频用法我结合一个实际场景说透。假设我们做一个内容发布助手流程是用户输入草稿 - 模型生成优化稿 - 人工审核节点 - 审核通过后发布。这个流程里“人工审核”是一个阻塞节点本地逻辑无法自动通过需要等外部用户点“通过”按钮。用LangGraph做这个特别自然。在模型生成节点之后加一个无条件边指向一个“审核暂停节点”该节点的功能就是中断图并等待外部信号。官方API里提供了interrupt相关的支持可以用Command配合interrupt函数来实现from langgraph.types import interrupt, Command def human_review_node(state: MyState): review_result interrupt({ review_request: state[draft_content], tips: 请审核以下内容点击通过或驳回 }) # 外部传入的信号会赋给review_result if review_result.get(approved): return {audit_status: approved} return {audit_status: rejected, reject_reason: review_result.get(reason, )}执行到interrupt时图的执行会暂停并抛出图表快照前端可以通过API读取出待审核详情。等审核人在页面上点击后前端再带着审核结果调用同一个线程继续执行config {configurable: {thread_id: thread-1}} app.invoke(Command(resume{approved: True}), config)此时图就会从interrupt的位置继续往下走。这种模式对产品化、对需要“人机协同”的业务来说体验非常顺。我在好几个项目里都靠这个能力解决了“模型不能自己下单”“模型不能自己发文章”这类安全约束问题审核链路完全保留下来了。7. 常见问题与排查技巧实录7.1 问题速查表实战一段时间后我把常见问题整理成了一个速查表给团队做分享时大家反馈都挺有用这里直接放出来常见问题典型原因解决方式状态字段更新无效返回值键名拼写错误或字段未在StateSchema中定义核对键名运行时打印StateKeys消息历史不增长字段未配置Reducer被覆盖更新用Annotated配合add_messages图跳转不符合预期条件边函数返回的节点名和实际不匹配检查条件边返回值与add_node注册名工具调用不生效模型返回消息没有正确透传到ToolNode确保AIMessage.tool_calls字段存在且被检查图中断后无法续跑没有配置Checkpointer编译图时传入MemorySaver或持久化后端并发执行状态错乱多个分支写入同一字段且未配置合并逻辑为共享字段指定自定义Reducer内存占用过高State塞入大对象或未及时清理历史消息控制State字段体量适时裁剪消息列表修改代码后图行为不变可能是缓存了旧的编译图重新执行编译或重启服务进程7.2 排查思路与调试技巧最关键的排查思路其实就一条把图执行过程中的每一步都可视化。LangGraph的调试体验比普通Agent框架好很多但我发现很多新手不知道善用内置的回调追踪。LangGraph兼容LangChain的回调机制可以挂一个LangGraphTracer之类的工具。但我的建议是最简单粗暴的打印法在每个节点函数入口和出口各加一行日志记录当前节点名、状态字段摘要、将要去往的边。图本身就20行代码加日志不会增加多少复杂度但对排查问题帮助巨大。另外app.get_state(config)和app.get_state_history(config)这两个API很值得记住。图出问题时用get_state看一下当前线程的最新状态用get_state_history回溯历史能看到每一步的状态快照。我在排查一个“订单状态莫名跳错”的问题时就是用状态历史逐帧回放发现是条件边函数在状态里读到了上一次遗留的旧字段导致走了错误分支。这种情况如果只看接口报错是永远查不出来的。还有一个调试技巧给图写一个“模拟测试模式”把节点里的大模型调用替换成固定的假响应这一步可以完全跑通图的路径逻辑先确认代码没毛病再引入真实模型变量。我强烈建议所有LangGraph项目都做这一步能省下大量因为模型输出不稳定导致的疑难杂症排查时间。8. 一些真实的项目体会最近我把一个小型客服系统的核心模块从原来的AgentExecutor迁移到了LangGraph上改动量不算小但收益很直观原来要维护一大堆Prompt分支去“引导”模型行为现在只需要在图上画出分支模型只在节点内部做判断和生成功能整个系统的可解释性和稳定度都上了一个台阶。这里也分享一个我的个人习惯画图之前先在纸上画一遍业务流程图标清楚哪些节点是模型节点、哪些是规则节点、哪些是人工节点、哪些需要暂停等待。这个“先画图再写代码”的习惯帮我省掉了大概一半以上的返工时间。LangGraph的图就是执行蓝图想不清楚就写代码最后一定是边写边改、状态字段满天飞。如果你本来就在用LangChain建议找一个中等复杂度的Agent模块试试迁移。不用贪大从一个小流程开始感受到图式编排带来的掌控感之后自然就知道哪些场景该用它了。
阅读完成 · 觉得有帮助?