news 2026/11/3 22:51:20

LangGraph零基础实战:手把手构建可调用工具的AI智能体

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LangGraph零基础实战:手把手构建可调用工具的AI智能体

刚接触LangGraph那会儿,我正在做一个需要“自己决定调用哪个工具”的AI智能体。做之前我觉得不复杂:模型不行就多写几个if else,工具多了加个函数字典。等真上手才发现,一旦进入“多轮对话 + 工具调用 + 状态记忆”的组合,代码会迅速失控:流程边界模糊,历史消息越攒越多,想回退某一步都无从下手。换到LangGraph之后,我才理清这类问题的根源——AI智能体开发本质上不是写一个个孤立的函数,而是在编排一张“有状态、能循环决策、可打断可恢复”的执行图。这篇文章想把LangGraph的零基础落地方案完整拆解一遍,从环境准备到可调工具的Agent实现,再到常见坑的排查,尽量做到你看完能直接抄作业。

1. Agent开发复杂在哪:为什么需要LangGraph

1.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_key=os.getenv("MODEL_API_KEY"), model=os.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节点执行工具;如果没有,直接到END;
  • tools节点执行完工具后,会有一条回边重新指向agent节点,于是模型看到工具返回内容,再次决定下一步;
  • 如此循环,直到模型认为可以结束时停止。

这解决了文章开头说的“循环逻辑没法优雅表达”问题。你不需要自己在代码里写while,LangGraph通过“条件边 + 回边”天然表达循环。

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(checkpointer=memory) config = {"configurable": {"thread_id": "用户A的会话ID"}} # 第一轮 result1 = graph_with_memory.invoke( {"messages": [("human", "北京天气怎么样?")]}, config=config, ) # 第二轮 result2 = graph_with_memory.invoke( {"messages": [("human", "那上海呢?")]}, config=config, )

关键在于:每次调用传入同样的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底层模型你都能快速搭出想要的应用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 12:01:29

基于Springboot + vue实现的校园资料分享平台

&#x1f942;(❁◡❁)您的点赞&#x1f44d;➕评论&#x1f4dd;➕收藏⭐是作者创作的最大动力&#x1f91e;&#x1f496;&#x1f4d5;&#x1f389;&#x1f525; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;欢迎留言讨论我们常常在当下感到时间慢&…

作者头像 李华
网站建设 2026/10/30 5:59:40

安徽背单词小程序怎么挑:从孩子动力不足切入的4个激励设计观察

很多安徽家长在选背单词小程序时&#xff0c;第一反应是看词库大不大、发音准不准。但真正把孩子的英语成绩拖住的&#xff0c;往往不是资源不够&#xff0c;而是打开率撑不过两周。合肥一位初二家长跟我聊过&#xff1a;孩子前三天每天主动打卡&#xff0c;第四天开始要催&…

作者头像 李华
网站建设 2026/10/29 20:56:33

存量监控系统AI改造:零硬件替换与全兼容方案落地指南

存量监控系统改造这件事&#xff0c;我前前后后经手过七八个项目&#xff0c;从最早的纯模拟矩阵到后来的NVR联网&#xff0c;再到这两年密集出现的"老系统加AI"需求。说实话&#xff0c;绝大多数甲方一开始问的都是同一句话&#xff1a;"能不能不换摄像头&…

作者头像 李华
网站建设 2026/10/21 15:57:53

阿里云服务器省钱攻略:配置选型与优惠避坑指南

做云服务器选型这事&#xff0c;我前后折腾了五年多&#xff0c;踩过的坑比很多人见过的主机都多。去年重新梳理了自己在阿里云里面的几台服务器时&#xff0c;发现一个扎心的事实&#xff1a;同样的业务体量&#xff0c;有的人一年省出一台新机器&#xff0c;有的人却把预算翻…

作者头像 李华
网站建设 2026/10/22 13:40:39

NLP论文每日汇总实战:半自动工作流与信息提炼方法

1. 从一篇论文汇总说起&#xff1a;这个项目到底在解决什么问题做自然语言处理这行的朋友大概都有过类似的体验&#xff1a;每天醒来&#xff0c;各种预印本平台上新挂出来的论文少则几十篇、多则上百篇&#xff0c;光是扫标题就要花掉大半个小时&#xff0c;更别提判断哪篇值得…

作者头像 李华