
刚接触LangGraph那会儿我正在做一个需要“自己决定调用哪个工具”的AI智能体。做之前我觉得不复杂模型不行就多写几个if else工具多了加个函数字典。等真上手才发现一旦进入“多轮对话 工具调用 状态记忆”的组合代码会迅速失控流程边界模糊历史消息越攒越多想回退某一步都无从下手。换到LangGraph之后我才理清这类问题的根源——AI智能体开发本质上不是写一个个孤立的函数而是在编排一张“有状态、能循环决策、可打断可恢复”的执行图。这篇文章想把LangGraph的零基础落地方案完整拆解一遍从环境准备到可调工具的Agent实现再到常见坑的排查尽量做到你看完能直接抄作业。1. Agent开发复杂在哪为什么需要LangGraph1.1 手写Agent时最常见的三种失控现场我先用失败案例帮你建立直觉。第一个常见问题是流程变“面条”。我第一次做智能体时先让模型输出意图再根据意图走不同的函数。一开始只有“查天气”和“查新闻”两条分支代码还算清爽。后来加“订闹钟”“查日历”“发邮件”函数嵌套、条件判断、返回值处理全都搅在一起。模型一旦格式输出不稳定函数的入参就接不上轻则返回空值重则整个流程炸掉。第二个问题是状态管理靠全局变量硬扛。多轮对话要求记住用户上一轮提到的地点、时间、偏好。我用了一个dict存上下文但并发来几个用户之后dict就乱了。A用户的历史被B用户覆盖模型回答驴唇不对马嘴。更可怕的是一旦某个步骤异常整个会话的中间状态全丢想追查问题只能靠打印日志。第三个问题是循环逻辑没法优雅表达。真实的Agent不是一条直线跑到底的。模型可能说“我需要调用天气工具”拿到工具结果后还要再判断“要不要再调一次”如果工具返回异常还要决定是换一种问法还是直接兜底。手写这种循环只能递归调用自己边界条件稍微想错一个就直接无限递归或者提前退出。这三个问题本质上是同一件事缺少一个显式的流程编排层。LangGraph把Agent执行过程建模成图节点就是具体动作边就是流转规则共享状态是跨节点传递数据的“黑board”。你不需要自己维护while循环和全局缓存框架帮你控制“下一步去哪、状态如何合并、还能不能回头”。1.2 链式LangChain和LangGraph的差异一张图带来的关键能力很多新手会问“我之前用LangChain的chain能实现不少功能为什么还要学LangGraph”我的理解是chain是“串联”适合输入到输出路径相对固定的场景比如“先检索再总结”。而Agent需要“根据模型的决策动态选择下一步”这种能力在纯chain里实现很别扭要么靠if/else包外壳要么靠外部循环驱动。LangGraph给的是一个“有向图”抽象。节点是你写好的任意Python函数。边分两类一类是普通边A执行完必定去B另一类是条件边A执行完根据某个返回值决定去B还是去C。这个条件边就是Agent“自主决策”的发动机。你甚至可以在图上画一条从“工具节点”回到“模型节点”的回边让Agent反复地“想一下、调一下工具、再想一下”直到它认为可以给用户最终答复。另外一个重要能力是检查点checkpointer。LangGraph允许你把每一步执行中间状态保存下来。保存的不只是消息列表还包括当前到哪个节点、输入输出是什么、状态数据长什么样。这样挂了可以恢复、出错可以调试还能天然实现“会话记忆”。迁移时只要换一个检查点存储后端就能从内存态升级到数据库持久化。这些能力都是“图 状态 检查点”组合带来的单靠chain根本没法自然做到。2. 零基础快速起步环境准备与最小可运行项目2.1 安装依赖与API配置在动手写图之前先准备好基础环境。我推荐用Python 3.10以上的版本因为LangGraph对较新类型语法支持更好。创建虚拟环境后执行pip install langgraph langchain-core langchain-openai这里的langgraph是核心框架langchain-core提供消息、工具、模型调用的标准接口langchain-openai则是用来对接“兼容OpenAI格式”的大模型服务的适配器。如果你不想绑定具体模型服务商也可以先装一个模拟模型库或者在后面用假模型做测试但那样体会不到真实效果。我的建议是直接用一个付费或免费的模型API反正LangGraph的命令规范是统一的换模型时只改模型名和base_url流程代码不用动。模型服务通常需要两个信息API密钥和模型名称。安全起见不要把这些信息硬编码进项目。在项目根目录创建.env文件然后利用你惯用的环境变量加载方式读取。示例export MODEL_API_KEY你的密钥 export MODEL_NAME你的模型名代码里我习惯这样读取import os from langchain_openai import ChatOpenAI llm ChatOpenAI( api_keyos.getenv(MODEL_API_KEY), modelos.getenv(MODEL_NAME, default-model), )有人会问为什么这里要刻意提“兼容OpenAI格式”因为现在很多本地部署模型或国产模型服务都实现了同一套HTTP接口规范。你只要把base_url指过去LangGraph的调用代码完全不用改。千万不要为了“选一个最好的模型”卡在这里入门阶段随便用一个能跑通的就足够了。2.2 手写第一个两节点图体验StateGraph的完整流程先做一个没有工具、没有循环的最小案例让整体概念落地。它做一件事收下用户输入交给模型返回回答。代码如下from typing import TypedDict, Annotated from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages # 状态对象Agent执行过程中所有节点共享的数据结构 class AgentState(TypedDict): messages: Annotated[list, add_messages] def call_model(state: AgentState): response llm.invoke(state[messages]) # 返回值会追加到 state[messages]而不是覆盖 return {messages: [response]} # 新建图对象 builder StateGraph(AgentState) # 添加节点 builder.add_node(model, call_model) # 添加边起点 - model节点 - 终点 builder.add_edge(START, model) builder.add_edge(model, END) # 编译成可执行的应用 graph builder.compile() # 调用一次 result graph.invoke({ messages: [(human, 你好介绍一下你自己)] }) print(result[messages][-1].content)这段代码虽然简单但包含了LangGraph的五个核心元素StateGraph、State、Node、Edge、compile。第一次跑通后建议你改一改messages的历史格式比如塞两条之前的内容再调一次观察模型能不能记住上文。你能直观感受到“消息列表”是整个图的共享血液每个节点都可以往里面追加新消息。注意这里的状态合并逻辑很关键因为messages字段声明了add_messages reducer语言所以每次节点返回{messages: [...]}时框架不是覆盖旧消息而是把新旧消息拼接成一条更长的新列表。如果不写Annotated[list, add_messages]后一个节点返回的消息会覆盖前面的历史多轮对话信息就丢了。新手第一天踩的坑绝大部分都跟这个状态合并语义有关。3. 核心实操一步步构建能调用工具的AI智能体3.1 定义带Reducer的状态与工具函数从这个章节开始我们要实现一个真正的最小可用的“ReAct风格”智能体模型先判断需不需要调用工具如果需要框架自动执行工具执行完工具再把结果交给模型生成最终回复。整个过程用图来表示就是START - agent节点 - 条件边 - tools节点 - 回到agent节点 - ... - END先定义状态比刚才那个例子多了一层历史消息的合并逻辑from typing import TypedDict, Annotated from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages]接下来定义工具。我常用langchain_core.tools.tool装饰器它能把普通Python函数自动包装成可被模型识别的工具对象。注意函数名、函数docstring一定要写清楚因为它们会被塞进模型提示词中直接影响模型是否能准确触发工具from langchain_core.tools import tool tool def get_weather(city: str) - str: 查询指定城市当前天气状况。 Args: city: 城市名比如北京、上海 # 这里只是示例真实场景应调用天气服务 return f{city}今天晴气温22摄氏度体感舒适。可以多定义几个工具比如查询时间、查交通路线让模型有选择空间。工具越多越能感受到LangGraph的“工具编排”价值。你只需要把工具放进一个列表里统一交给框架不用写一堆switch-case。3.2 add_node、ToolNode与条件边把“决策循环”画出来先创建模型节点。为了让模型知道“自己可以调用哪些工具”需要绑定工具列表tools [get_weather] llm_with_tools llm.bind_tools(tools) def call_model(state: AgentState): model_response llm_with_tools.invoke(state[messages]) return {messages: [model_response]}bind_tools之后模型返回的对象会携带tool_calls数组。有了这个数组我们才能判断Agent当前处于“需要调用工具”还是“直接回答”状态。接着建图。LangGraph里有个预置节点ToolNode它专门负责执行工具from langgraph.prebuilt import ToolNode tool_node ToolNode(tools) def should_continue(state: AgentState): last_message state[messages][-1] # 如果模型要求调用工具则进入工具节点 if getattr(last_message, tool_calls, []): return continue # 否则结束把最终答案返回给用户 return end builder StateGraph(AgentState) builder.add_node(agent, call_model) builder.add_node(tools, tool_node) builder.add_edge(START, agent) # 条件边agent节点之后通过should_continue的结果决定去哪个节点 builder.add_conditional_edges( agent, should_continue, { continue: tools, end: END, } ) # 工具执行完强制回到agent节点让模型继续判断 builder.add_edge(tools, agent) graph builder.compile()这套图的运转逻辑你需要反复体会从START出发先进agent节点agent节点调用模型模型要么生成最终回答要么生成一个tool_calls条件边变成“路由开关”如果有tool_calls就去tools节点执行工具如果没有直接到ENDtools节点执行完工具后会有一条回边重新指向agent节点于是模型看到工具返回内容再次决定下一步如此循环直到模型认为可以结束时停止。这解决了文章开头说的“循环逻辑没法优雅表达”问题。你不需要自己在代码里写whileLangGraph通过“条件边 回边”天然表达循环。3.3 完整调用观察Agent如何自主走完循环写完后我们直接用一段输入来验证user_input 北京天气怎么样 result graph.invoke({ messages: [(human, user_input)] }) for message in result[messages]: message.pretty_print()你可能会看到类似这样的执行轨迹第一轮Human消息agent节点输出的AIMessage带有一个tool_calls内容是调用get_weather参数{city: 北京}工具节点输出的ToolMessage内容是“北京今天晴气温22摄氏度...”agent节点再次输出的AIMessage不带tool_calls直接给出最终回答综合天气信息回复用户。这就是一次完整的“模型决策 → 工具执行 → 模型总结”循环。整个过程你可能没有写任何一步“手动调用工具”全部由LangGraph帮你托底。通过约二十行图定义省略了手工解析JSON、函数分发、结果包装这些重复劳动。如果想看中间执行详情可以给invoke传一个配置参数限制最多走多少步graph.invoke( {messages: [(human, 北京天气怎么样)]}, config{recursion_limit: 10} )当图里出现死循环或连续多次工具调用时这个参数能帮你快速截断不至于让程序挂死。我建议新手调试阶段一定加上。3.4 加入Checkpointer多轮对话与状态持久化目前上面这个图虽然能完成单轮工具调用但多轮对话还缺一块如果用户第二轮问“那上海呢”模型应该结合上一轮说的“北京”理解新问题。没有记忆机制模型每次都是“失忆”状态。LangGraph提供的checkpointer正好解决这个问题。LangGraph里有一个内存型检查点适合开发和单进程测试from langgraph.checkpoint.memory import MemorySaver memory MemorySaver() graph_with_memory builder.compile(checkpointermemory) config {configurable: {thread_id: 用户A的会话ID}} # 第一轮 result1 graph_with_memory.invoke( {messages: [(human, 北京天气怎么样)]}, configconfig, ) # 第二轮 result2 graph_with_memory.invoke( {messages: [(human, 那上海呢)]}, configconfig, )关键在于每次调用传入同样的thread_id框架会从检查点加载该会话的历史状态所以第二轮的状态里已经包含了“北京天气怎么样的所有对话记录”。不同用户使用不同的thread_id状态彼此隔离不会串数据。MemorySaver适合本地跑但一旦服务重启数据就没了。生产环境建议换成数据库后端比如SqliteSaver或基于PostgreSQL的保存器。换检查点后端不需要改图的定义只需要在compile时传入不同的checkpointer对象。这一点是LangGraph设计里很讨喜的地方图结构和状态存储解耦迁移成本极低。4. 新手最容易踩的坑问题排查与实践心得4.1 常见运行错误与原因速查下面这些错误基本是新手在LangGraph里会反复遇到的。我按自己的踩坑顺序排个序号。现象常见原因解决办法Node not foundadd_node拼错名字或者条件边里的节点名没有先注册所有节点名必须和add_node严格一致建议定义一个常量字符串ValueError: An edge must connect to a node把add_edge或条件边指向了不存在的节点比如拼成end而不是END检查终点是否用了END常量条件边的值要和字典键一一对应跑完只输出最后一句历史全丢状态类里的messages没有用Annotated[list, add_messages]节点返回值覆盖了旧消息检查状态定义是否带有正确的reducer模型返回tool_calls但工具没执行工具的name和bind_tools里绑定的工具列表不一致确认ToolNode(tools)的列表和llm.bind_tools(tools)用的是同一个列表无限循环停不下来条件边判断逻辑有问题比如tool_calls为空却返回continue打印最后一条消息的tool_calls检查getattr和布尔判断结果多轮对话会串号所有用户共用一个thread_id每次会话都生成独立thread_id可以在请求进来时用UUID生成另一个容易忽略的问题是条件边字典的配置。add_conditional_edges的第二个参数是一个返回字符串的函数第三个参数是一个字典把这个函数可能返回的值映射到节点名。万一你的函数返回了一个字典里没有的字符串LangGraph会直接报错。你可以在函数末尾打印返回值帮助快速定位。4.2 调试技巧如何看清每一步到底发生了什么LangGraph最难调试的地方在于“图是隐式的状态是局部的”。新手最常见的反应是“我也不知道它卡在哪一步了”。我的三板斧是第一板斧每个节点函数里的state打印关键信息。比如在call_model里打print(进入agent节点消息数量:, len(state[messages]))第二板斧利用get_state读取某次会话的最新状态。带上config调用graph.get_state(config)能看到当前的消息列表、当前节点等信息。第三板斧把recursion_limit调小再试。如果设置成3推一步就报“达到限制”你就能快速知道循环次数点在哪里。还有一个经验之谈不要一开始就把工具复杂度拉满。我遇到过不少新手朋友图里塞了十来个工具结果模型频繁调错工具他们以为是LangGraph的锅。其实最优先要验证的是“单一工具是否走通”。先解决一个工具再逐步加第二个、第三个。这样出问题时你至少能判断是“工具定义”的问题还是“模型识别工具”的问题而不是把所有代码混在一起瞎猜。4.3 模型上下文塞爆怎么办消息修剪的入门建议多轮对话跑久了state[messages]会越来越长。到达模型上下文长度上限后调用会直接报错或者模型开始“忘记”开场指令。处理这个问题最直接的方法是消息修剪。LangGraph官方推荐使用trim_messages结合LastValue状态来限制保留的消息数。一个简单做法在状态里额外维护一个summary字段当消息超长时用摘要替换部分旧消息。入门时不用做得很复杂先用最基础的截断from langchain_core.messages import RemoveMessage def trim_messages(state: AgentState): if len(state[messages]) 10: return {} # 只保留最后6条消息 delete_count len(state[messages]) - 6 return { messages: [RemoveMessage(message.id) for message in state[messages][:delete_count]] }把这个节点放在所有节点之前或之后即可但注意必须在调用模型前完成。修剪逻辑不能过度删减否则多轮对话上下文会断掉。我一般会保留最近两三轮的用户输入和模型输出再配合一个summary字段做压缩两套策略共同作用。4.4 并发、异步和新手心态最后几条实战心得在实际服务多个用户时我建议把thread_id设计成“用户身份 会话编号”的组合。这样同一个用户可以开多个独立会话不同会话之间互不干扰。如果使用了MemorySaver它在单进程内可用但并发量高时会成为瓶颈部署时最好切换到真正的数据库后端。异步也是常被忽略的话题。如果你的工具函数里包含网络请求比如调用另一个API建议用httpx.AsyncClient或先普通的同步实现跑通再考虑异步改造。因为LangGraph库里已经有对应的异步节点封装但理解异步执行顺序对新手有一定门槛。先把同步跑对再去追异步性能才合理。最后分享一个心态上的建议学LangGraph不要一上来就照着长篇项目模板抄代码。官方文档和第三方示例都很好但如果你不能亲手写一遍“两个节点的图”和“带条件边的图”后面理解复杂概念时会特别吃力。我自己是从一个只调用天气工具的二十行示例起步的跑通之后再往里加检查器、加消息修剪、加并发逻辑每一步都有明确的反馈。这个循序渐进的过程比看一百篇“高级Agent架构解析”都管用。如果你在实践过程中发现某个步骤和我写的略有出入大概率是版本差异。LangGraph更新挺频繁我建议打印langgraph.__version__对照官方文档的版本说明做微调。代码报错时优先看当前用的API是否被标记为Deprecated再排查你的逻辑。用框架就是这样工具更新快是常态真正值钱的是你掌握的“图式思维”把复杂的Agent拆成节点、边和状态让每一步都可控、可测、可恢复。有了这个思维换什么AI底层模型你都能快速搭出想要的应用。