公司动态

LangGraph工具调用实战:从模型意图到真实动作的完整链路

📅 2026/9/2 7:03:08
LangGraph工具调用实战:从模型意图到真实动作的完整链路
大模型本身不具备调用外部系统的能力。它能记住上下文能生成通顺文字但一旦需要获取实时数据、操作数据库、调用第三方服务它的能力边界就出现了。LangGraph 工具调用Tool Calling正是为了解决这个缺口让语言模型在对话过程中产出结构化的“调用意图”再由外部代码负责具体执行。这个机制是智能体Agent开发的基石也是 AI 编程中“模型选动作、代码做动作”这种协作模式的核心实现方式。这篇文章以 LangGraph 为核心框架围绕工具调用的完整链路展开先说明模型、工具、状态图三者如何配合再给出一个可运行的查询天气示例接着解释 bind_tools、ToolNode、条件边等关键设计最后整理常见报错和落地建议。如果你已经了解 LangChain 的基本用法但还没有把 Agent 真正跑通这篇文章可以帮你把工具调用这一环打通。它也可以作为高校“AI 编程与智能体开发”课程中 LangGraph 部分的学习笔记重点内容放在可复现代码和排错方法上。1. 工具调用是智能体从“会聊天”到“能办事”的关键一步1.1 先理解模型输出与真实动作之间的断层LLM 可以做翻译、总结、代码生成但真实系统里往往需要“动作”查库存、创建订单、发消息。模型不能直接执行这些动作但它可以输出一个明确的调用请求。这个请求就是 Tool Call。例如用户说“帮我查一下厦门今天的天气”模型可能不会直接给出天气预报而是输出一个对get_weather工具的调用参数是city厦门。真正的天气数据由外部函数执行后返回模型再根据返回内容组织最终回答。这里需要区分两个概念函数调用和工具调用。在普通编程里函数调用是代码主动调函数在智能体开发里工具调用通常指模型生成结构化调用参数代码负责执行。模型没有“主动执行”能力但被训练成在合适的场景输出tool_calls结构。LangGraph 的作用是把这种结构转换成一次真实计算再把计算结果送回模型上下文。1.2 LangGraph 的核心思路把工具调用当成图节点之间的消息流动LangGraph 本身并不关心工具的内部实现它关心的是执行顺序和数据状态。一个包含工具调用的 Agent可以简化成两个节点模型节点和工具节点。模型节点接收历史消息后生成响应如果响应里包含tool_calls条件路由就会进入工具节点工具节点按调用参数执行真实工具并把结果包装成ToolMessage写回消息列表随后又回到模型节点让模型读取工具结果并生成最终回答。整个过程是一个循环直到模型不再产生新的工具调用。这种设计的价值在于每一次工具调用都体现在状态中任何一轮流程都可以回放。对调试和审计非常有利。相比直接在一个函数内部等待模型返回 JSON 再执行LangGraph 把状态显式化每一步是“谁调用谁、传了什么参数、返回了什么结果”都一清二楚。1.3 LangGraph 与 LangChain 传统 Agent 的差异LangChain 早期用AgentExecutor管理 ReAct 循环Agent 内部隐藏了较多执行细节。LangGraph 则把流程显式建模成图开发者能自己控制节点、边和共享状态。对于工具调用来说LangGraph 的明显价值是可观测中间每一步都是图中的一类消息。可扩展可以插入 human-in-the-loop、检查点、并行节点。可控制可以用条件边自定义如何决定继续调用工具还是结束。维度传统 LangChain AgentLangGraph Agent流程建模封装在 Executor 里显式图结构工具调用按预置循环处理由条件边和 ToolNode 控制状态管理消息列表内部化显式 State 在节点间传递调试需要看 Agent 日志可查看每一步图状态扩展中间拦截较难可插入中断、持久化、子图在简单场景下使用AgentExecutor没有太大问题但做复杂流程时LangGraph 的控制力更合适。建议把工具调用理解成“图上的一次节点执行”而不是“模型自动触发函数”这正是 LangGraph 与其他 Agent 框架在使用体验上最大的不同。2. 环境准备版本确认、依赖安装和项目结构2.1 先确认 Python、LangChain 和 LangGraph 的版本工具调用 API 变化比较快。LangChain/LangGraph 的包结构在 2024 年到 2025 年做过多次调整。为了避免看文档时对不上建议先确认当前环境中已安装的版本。python --version pip show langgraph langchain-core langchain-openai如果没有安装可以先创建虚拟环境然后安装python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate pip install --upgrade langgraph langchain-openai python-dotenvlangchain-openai提供ChatOpenAI模型接口langgraph提供StateGraph、ToolNode等核心类python-dotenv用于读取.env文件中的 API Key。实际项目中如果使用其他模型还要安装对应的包例如使用 Anthropic 就安装langchain-anthropic。各依赖包的用途可以先用表格记住包名作用langgraph状态图、节点、边、ToolNodelangchain-coreMessage、tool 装饰器、接口定义langchain-openaiOpenAI 兼容模型接入python-dotenv读取 .env 环境变量2.2 模型接口选择能用 OpenAI也能用兼容接口不同大模型对工具调用支持的格式不同。OpenAI 的 gpt-4o 系列支持 function calling很多开源或国产模型也提供兼容接口。在 LangGraph 中只要模型类支持bind_tools并且返回的标准消息中包含tool_calls就可以接入同一个图结构。如果你没有 OpenAI 的 Key也可以使用兼容接口。比如配置base_url指向支持 OpenAI API 规范的服务from langchain_openai import ChatOpenAI llm ChatOpenAI( modelqwen-plus, api_keyyour-api-key, base_urlhttps://your-endpoint.example.com/v1, temperature0, )这里要提醒base_url、模型名和工具调用支持度以实际服务为准。生产环境不要硬编码 Token应通过环境变量或密钥管理服务注入。工具调用的稳定性非常依赖模型自身能力同一个图结构切换模型后需要重新测试工具描述是否被正确理解。2.3 最小项目目录agent_tool_demo/ ├── .env ├── requirements.txt ├── agent.py └── tools.pyrequirements.txt放依赖。tools.py放业务工具。agent.py放模型绑定、图构建和运行入口。.env放OPENAI_API_KEY等敏感配置注意加入.gitignore。这只是最小结构。如果项目变大可以把图构建、工具注册、配置读取拆成独立模块方便测试和复用。3. 从零实现一个可运行的 LangGraph 工具调用示例3.1 定义业务工具先定义两个简单工具查询天气和查询当前时间。tool装饰器来自langchain_core.tools它会把函数签名自动转成模型可见的 JSON Schema。from datetime import datetime from langchain_core.tools import tool tool def get_weather(city: str) - str: 查询指定城市的天气信息。 Args: city: 城市名称例如“厦门”。 Returns: 天气描述字符串。 weather_map { 厦门: 厦门今天多云26℃~32℃东南风 3 级。, 上海: 上海今天小雨24℃~29℃东北风 4 级。, 北京: 北京今天晴天18℃~31℃西北风 2 级。, } return weather_map.get(city, f暂时没有 {city} 的天气数据。) tool def get_current_time() - str: 获取服务器当前时间返回格式化字符串。 return datetime.now().strftime(%Y-%m-%d %H:%M:%S)关键点tool装饰器会把函数名作为工具名Docstring 作为工具描述类型注解作为参数 Schema。想让模型准确调用描述里要说明参数含义、示例值、返回内容。不要只写“查询天气”尽量写成“查询指定城市当天的天气情况输入城市中文名返回天气和温度”。模型对描述的理解直接决定了它会不会调用这个工具。3.2 绑定工具到模型需要先实例化模型然后使用bind_tools(tools)把工具列表传给模型。之所以用bind而不是在 prompt 中手动拼 JSON是因为 OpenAI 等模型原生支持tool_calls输出结构识别准确率更高from langchain_openai import ChatOpenAI tools [get_weather, get_current_time] llm ChatOpenAI(modelgpt-4o-mini, temperature0) model_with_tools llm.bind_tools(tools)调用模型时如果用户问题需要工具模型返回的AIMessage.tool_calls会包含调用请求如果不需要tool_calls为空列表。绑定发生在模型实例化之后、放入图之前这样 agent 节点每次执行时都会用已经绑定工具列表的模型生成响应。3.3 构建状态图和 ToolNode工具调用会写回消息列表所以 State 里的 messages 要使用add_messagesreducer保证新消息追加而不是覆盖from typing import Annotated, TypedDict from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from langgraph.prebuilt import ToolNode, tools_condition class State(TypedDict): messages: Annotated[list, add_messages] def agent_node(state: State): result model_with_tools.invoke(state[messages]) return {messages: [result]}然后创建图graph_builder StateGraph(State) graph_builder.add_node(agent, agent_node) tool_node ToolNode(toolstools) graph_builder.add_node(tools, tool_node) graph_builder.add_edge(START, agent) graph_builder.add_conditional_edges( agent, tools_condition, { tools: tools, END: END, } ) graph_builder.add_edge(tools, agent) graph graph_builder.compile()说明tools_condition是预置路由函数会检查最后一条 AI 消息是否包含tool_calls。如果有返回tools没有则返回END。ToolNode接收工具列表自动从消息中解析调用并执行。add_edge(tools, agent)表示工具执行完后把ToolMessage返回给模型让模型继续处理。最终如果模型不再调用工具流程在 agent 节点结束。这个流程已经是一个最小但完整的 ReAct 循环。模型可以连续调用多个工具工具结果会依次写回消息列表。3.4 完整运行脚本把上面的内容合并成一个脚本加上主函数def main(): user_input input(请输入问题例如“厦门今天天气怎么样”\n ) result graph.invoke({messages: [(user, user_input)]}) for msg in result[messages]: if msg.type in (ai, tool): print(f[{msg.type}] {msg}) if __name__ __main__: main()运行方式export OPENAI_API_KEYsk-... python agent.py示例输入厦门今天天气怎么样预期输出不同模型可能有差异[ai] content tool_calls[{name: get_weather, args: {city: 厦门}, ...}] [tool] 厦门今天多云26℃~32℃东南风 3 级。 [ai] 厦门今天多云26℃到32℃左右东南风 3 级。这一步说明模型没有自己编造天气而是先请求工具工具返回真实数据后再回复。4. 深入理解 bind_tools、ToolNode 与条件边的配合4.1 bind_tools 的常用参数与影响以 OpenAI 风格模型为例bind_tools可以传入多个参数参数含义常见值注意事项tools工具列表[get_weather, get_current_time]每个工具必须有清晰的 name 和 descriptiontool_choice控制是否强制调用某个工具auto、none、工具名强制工具名会降低灵活性慎用parallel_tool_calls是否允许多个工具并行调用True / FalseTrue 时模型可能一次调用多个工具strict是否启用严格 schema部分模型False / True启用后参数必须严格匹配但依赖模型支持temperature不是bind_tools的参数而是ChatOpenAI的初始化参数。工具调用场景建议设为 0 或较低值减少输出随机性。tool_choice设为具体工具名时表示模型只能调用指定工具适合流程固定的场景不适合开放问答。parallel_tool_calls在多工具场景很有用但如果工具之间存在依赖比如第二个工具需要第一个工具的结果生成参数就要关闭并行否则会在一次请求里拿到多个参数不完整的调用。4.2 为什么用 reducer 管理消息在线程编程中多个节点可能共享同一个 State。add_messages是一个 reducer它会按消息 ID 合并列表把新消息追加进去。在 Agent 场景里这一设计保证了工具执行结果不会覆盖模型历史。如果你漏掉add_messages多次节点更新会互相覆盖最常见现象是模型节点返回后工具节点传入时状态里只剩最后一条消息导致后续模型看不到工具结果甚至陷入死循环。调试时可以先打印result[messages]检查是否包含HumanMessage、AIMessage、ToolMessage三类完整的消息链。4.3 自定义条件路由tools_condition适合最简单的判断但真实项目里往往需要额外条件。例如当模型调用了某类“只读工具”时可以先去工具节点当调用“写操作”时需要先经过人工确认节点。这时可以自己写一个路由器def route_after_agent(state: State): last_message state[messages][-1] if not last_message.tool_calls: return END if last_message.tool_calls[0][name] create_order: return human_confirm return tools然后graph_builder.add_conditional_edges( agent, route_after_agent, { tools: tools, human_confirm: human_confirm, END: END, } )这里展示的是组合能力工具调用并不是只能进入ToolNode它可以经过人工审批、权限检查、日志记录等多个节点。条件路由函数接收当前 State返回下一个节点的标识LangGraph 会按下标的映射继续执行。4.4 ToolNode 内部做了什么ToolNode做的事情可以理解为读取最近一条AIMessage的tool_calls。对每个调用找到同名工具。用args调用工具函数。把返回值包装成ToolMessage并写入工具名、调用 ID。返回{messages: [tool_message]}列表状态图继续流转。自己实现 ToolNode 时要注意工具异常不能直接导致整个图崩溃应把异常捕获后转成错误消息返回给模型。例如tool def safe_http_request(url: str) - str: 发起 HTTP GET 请求返回状态码和摘要。 try: import requests resp requests.get(url, timeout5) return fHTTP {resp.status_code}, 内容长度 {len(resp.text)} except Exception as e: return f请求失败{e}在工具函数内部捕获异常比试图在 ToolNode 外层拦截更稳。因为工具返回的是字符串模型可以直接理解失败原因并调整参数重试。5. 运行验证、调试与日志观察5.1 从返回值确认工具调用链路完整运行脚本后检查result[messages]。正常链路应该包含HumanMessage用户输入。AIMessage模型第一次响应内容可能为空但包含tool_calls。ToolMessage工具执行结果每条ToolMessage都有一个与AIMessage匹配的tool_call_id。AIMessage模型读取工具结果后的最终回答。如果缺少ToolMessage说明没有走到工具节点。如果最终AIMessage后面还有tool_calls说明流程可能没有结束需要检查条件边和recursion_limit。5.2 用 graph.stream 观察每一步执行graph.invoke只返回最终结果调试时更适合用graph.stream它会逐步输出每个节点的执行结果for step in graph.stream( {messages: [(user, 厦门今天天气怎么样)]}, config{recursion_limit: 10}, ): print(step)输出会展示哪一步是 agent 节点、哪一步是 tools 节点。例如{agent: {messages: [AIMessage(content, tool_calls[...])]}} {tools: {messages: [ToolMessage(content厦门今天多云..., ...)]}} {agent: {messages: [AIMessage(content厦门今天天气...)]}}这样可以直观看到模型确实先发起了工具调用然后在工具结果返回后生成了最终回答。生产环境可以把这些 step 记录到日志系统方便事后回放。5.3 模型差异不同模型工具调用格式不统一不同模型对工具描述的字段要求不同。OpenAI 系列支持 name、description、parameters部分开源模型可能要求严格 JSON 或缺少结构化输出。遇到“模型不调用工具”时先确认模型原生支持 tool calling再看是否需要在 prompt 中补充“你可以使用工具”。某些情况下模型支持工具调用但因为工具描述不清它选择用已有知识直接回答。例如用户问“今天天气”如果工具描述里没有提到“今天、天气”模型可能不会触发。建议在系统消息中写明system_prompt 你是智能助手。当需要实时信息时请使用相应工具。工具结果返回后请基于工具结果回答。 messages [(system, system_prompt), (user, user_input)]然后把 messages 作为初始状态传入graph.invoke。要注意不是所有模型都需要系统提示才会调用工具但加上之后能明显提高触发率。6. 常见问题排查工具调用的典型故障与处理思路问题现象常见原因检查方式处理建议模型始终不调用工具工具描述不清晰、模型不支持、未绑定工具检查bind_tools是否传入打印 AIMessage.tool_calls优化工具描述降低 temperature确认工具列表工具参数乱编参数名与函数签名不一致、缺少类型注解打印生成的 tool_calls 参数和工具 schema 对比使用显式类型在描述中给出示例值调用后死循环工具结果没形成 ToolMessage或路由始终进入 tools打印每一步 state查看 recursion_limit使用add_messages配置tools_condition设置recursion_limit多个工具同时调用时结果混乱模型并行返回多个 tool_calls工具之间有依赖打印 ToolMessage 的 tool_call_id设置parallel_tool_callsFalse在工具内做依赖校验工具执行异常导致图崩溃工具函数抛出未捕获异常查看完整异常堆栈在工具内 try/except返回可读错误消息工具调用后答非所问模型没有读取 ToolMessage 就直接结束检查最终 AIMessage 是否基于工具结果在模型输入中保留完整 messages 链不要丢弃工具消息6.1 推荐排查顺序按以下顺序排查能更快缩小问题范围输入是否正确用户问题里是否包含工具函数的触发条件。依赖版本langgraph 和 langchain-core 版本是否匹配是否安装正确。bind_tools是否把工具列表交给模型。模型输出打印response.tool_calls确认模型是否产生调用。路由条件边是否能正确判断tool_calls。状态更新messages 是否按链路追加。完整报错看堆栈开头而不是只读最后一行。6.2 日志与审计如果在生产环境接入了 LangSmith 或自定义日志可以记录每个tool_call的 id、name、args、result形成审计链路。至少要在工具节点前后各打一条日志def agent_node(state: State): result model_with_tools.invoke(state[messages]) if result.tool_calls: for call in result.tool_calls: print(f[tool_call] name{call[name]} args{call[args]}) return {messages: [result]}打印工具名称和参数能快速发现模型是否在乱传参数。注意不要把敏感信息打印到日志例如查询条件中包含手机号时要做脱敏。7. 工具调用的工程化最佳实践7.1 工具设计要像写 API 一样严谨工具函数暴露给模型本质上是模型可见的 API。名称建议用动词开头例如get_weather、send_email、create_order。描述建议包含这个工具什么时候用。参数含义和格式。返回值含义。不适合用这个工具的场景。参数尽量用基本类型string、number、boolean。复杂对象会增加模型生成错误参数的概率。可选参数要标注默认值不要把必填项描述成可选。工具返回内容不要太长模型需要把它继续放进上下文返回一万字会让后续生成变慢且费用变高。7.2 权限、安全与敏感信息工具返回值会被模型读取再生成文本返回给用户。如果工具返回了敏感信息模型可能把它直接输出。因此工具层要做字段级脱敏不要把内部 ID、密钥、手机号完整返回给模型。权限校验在工具内部再次校验用户身份不要假设只有模型能调用工具。操作类工具要谨慎写操作、删除操作尽量走人工确认或二次校验。超时和限流外部 API 调用要有超时时间避免图节点长时间挂起。tool def query_user_order(order_id: str) - str: 查询订单摘要供客服使用。敏感字段已在返回前脱敏。 order db.get_order(order_id) if order is None: return 未找到订单。 return f订单 {order_id} 状态是 {order.status}金额已脱敏。这个例子里工具不会返回用户真实手机号、支付账户等敏感字段只给模型生成回答所需的摘要信息。7.3 成本与延迟控制工具调用会消耗更多 token模型输出tool_calls工具结果又作为新消息继续进入上下文。如果工具返回过长例如大段数据库日志会推高成本并拖慢后续回答。建议对工具返回值做长度限制def safe_result(result: str, max_len: int 500): if len(result) max_len: return result return result[:max_len] ...(已截断)同时使用recursion_limit限制最大循环次数并在图编译后用测试用例反复验证。常见配置可以是 10 或 20超过限制说明流程可能异常结束需要告警。7.4 下一步扩展方向跑通工具调用后可以继续扩展以下方向人类确认节点用interrupt暂停图交由人工批准后再继续。持久化使用 checkpointer 保存 Agent 会话状态支持断点续跑。子图把复杂流程拆成多个图在工具节点中调用子图。多 Agent不同 Agent 各管一组工具通过消息协调。流式输出用graph.stream或stream_mode把工具调用过程实时返回前端。这些方向都建立在“工具调用是图上一个节点”这个基础上。先把这个最小链路跑通再逐步扩展会比一上来就搭建复杂多 Agent 系统更稳。工具调用这一层跑通后续无论做 RAG 查询、业务系统操作还是多智能体协作都有了一个可观测、可扩展的地基。实际项目里最容易出问题的不是代码语法而是工具描述写得不清楚、状态更新被覆盖、异常没有回传给模型。只要这三条处理好LangGraph 工具调用就能稳定地承担智能体的真实执行任务。建议从今天的最小示例开始先把“模型产生调用、工具执行、结果回填、模型再回答”这条链路亲手跑一遍再逐步加入权限、人工审批和持久化。