ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LangGraph工具调用完整指南:从bind_tools到多工具循环

LangGraph工具调用完整指南:从bind_tools到多工具循环 LangGraph 的工具调用是学智能体开发时绕不开的一环。我在跑 LangGraph 教程时最大的感受是工具调用并不是“让模型在回答里写一段函数调用”那么简单而是要把工具的 schema 注册给模型再由 LangGraph 的图节点去执行工具最后把结果放回消息列表里继续推理。这个主题也是厦门大学林子雨老师在《AI 编程与智能体开发》课程中单独拿出来讲的第 9.5 节如果你正在从 LangChain 基础转向智能体开发这一节值得反复看。这篇文章会按实际落地顺序拆开讲先理解 LangGraph 为什么用图结构来组织工具调用再准备一个最小可运行环境然后定义工具、绑定模型、处理多工具和循环最后补上调试和工程化经验。看完之后你应该能自己搭一个“模型决定调用工具 - 工具执行 - 结果回传 - 模型继续回答”的完整智能体流程。1. 先理解工具调用在 LangGraph 里的位置1.1 工具调用和“让模型生成一段文字”不是一回事大模型本身只能输出文本工具调用则是让模型输出一个结构化的调用指令通常是工具名称和参数字典。LangChain 生态里常见的bind_tools就是把这些工具的描述和参数格式转换成模型能理解的 JSON Schema再塞给模型。很多人第一次写工具调用会想着“在提示词里告诉模型你可以调用某个工具”但这样通常不稳定。模型确实会在回答里写出一段像代码的东西可你的程序无法可靠地从这段文本里解析出工具名和参数。真正的工具调用要求模型返回的是结构化字段例如tool_calls列表里面包含name和args。代码才能根据这个结构去执行。所以在 LangGraph 里工具调用不是一个提示词技巧而是一个数据流问题模型输出结构化调用指令图节点负责执行执行结果再回到对话上下文中。1.2 LangGraph 的图结构让工具调用变得可控LangGraph 的核心概念是节点和边。一个智能体流程可以拆成“模型节点”、“工具节点”和“条件路由”。模型节点的作用是接收当前消息列表调用绑定了工具的模型得到一个 AI 回答。如果模型决定调用工具这个 AI 回答里会带着tool_calls。工具节点的作用是遍历tool_calls逐个执行工具并把返回结果封装成ToolMessage。条件路由的作用是判断下一步该去工具节点还是直接结束。这种设计的好处是流程可见、可控。你可以在任意节点之间加日志可以在工具节点里处理异常也可以随时加一个最大步数限制来避免无限循环。1.3 什么时候才值得引入工具节点如果你的场景只是纯文本问答不读取外部数据、不执行计算、不调用接口那不需要工具调用。但遇到下面这些情况工具节点就非常有必要查询数据库或业务系统。调用第三方 API比如天气、地图、订单查询。执行本地计算比如数学推导、文件处理。需要访问实时数据模型训练时并不知道这些信息。需要人工审批或外部系统确认。判断标准很简单任务里有没有“模型自身做不了必须靠代码完成”的环节。有就应该把对应能力封装成工具再接入 LangGraph 的工具调用流程。2. 搭建最小运行环境依赖、代码和第一条可跑通的链路2.1 安装依赖先确认 Python 版本和模型接口我建议先建一个干净的 Python 环境。安装命令大致如下pip install langgraph langchain-core langchain-openailanggraph负责图流程langchain-core提供工具定义和消息结构langchain-openai用于接入 OpenAI 兼容接口。如果你用的是本地模型或国内模型只要对方提供 OpenAI 兼容接口也可以继续用ChatOpenAI配置base_url来切换。这里有个容易忽略的点LangGraph 是一个持续更新的库接口细节可能变化。我写示例时不会依赖某个精确版本但你本地安装时最好先看官方文档确认当前版本的导入路径。Python 版本建议 3.9 以上实际要求以官方说明为准。2.2 一个最小的工具调用图结构下面这个例子包含两个工具一个获取当前城市一个查询天气。模型节点绑定这两个工具工具节点执行匹配到的调用最后再回到模型节点。from langchain_core.tools import tool from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, MessagesState, START, END from langgraph.prebuilt import ToolNode tool def get_city() - str: 获取当前用户所在城市。如果用户没有说明城市可以调用这个工具。 tool def get_weather(city: str, date: str) - str: 查询指定城市、指定日期的天气情况。 Args: city: 城市中文名例如“北京”。 date: 日期格式为 YYYY-MM-DD。 return 晴25℃ tools [get_city, get_weather] model ChatOpenAI(modelgpt-4o-mini, temperature0) def model_node(state: MessagesState): response model.bind_tools(tools).invoke(state[messages]) return {messages: [response]} def route_after_model(state): last_message state[messages][-1] if len(last_message.tool_calls) 0: return tools return __end__ builder StateGraph(MessagesState) builder.add_node(model, model_node) builder.add_node(tools, ToolNode(tools)) builder.add_edge(START, model) builder.add_conditional_edges( model, route_after_model, {tools: tools, __end__: END} ) builder.add_edge(tools, model) graph builder.compile()注意几个关键点tool装饰器会把 Python 函数的函数名、docstring、参数签名自动转换成工具 Schema。模型节点里使用bind_tools(tools)把工具注册给模型这一步不能省。路由函数检查最后一条 AI 消息里有没有tool_calls有就跳转工具节点没有就结束。ToolNode是 LangGraph 预置的工具执行节点它会自动处理tool_calls并生成ToolMessage。2.3 先跑单条消息验证链路是否完整第一次测试不要设计复杂场景直接用一句能触发工具调用的话result graph.invoke({messages: [{role: user, content: 北京今天天气怎么样}]}) for message in result[messages]: print(message.type) print(message.content) if getattr(message, tool_calls, None): print(message.tool_calls)如果链路正常你会在消息列表里看到用户消息。AI 消息内容可能是空字符串但tool_calls里有get_weather和参数。工具消息内容是工具返回的“晴25℃”。最后的 AI 消息根据工具结果组织成自然语言回答。如果第二步里没有tool_calls大概率是模型没有把工具调用当回事。先检查工具描述是否清楚再检查你的模型是否支持 Function Calling。有一部分本地模型对结构化调用支持一般输出不稳定并不是 LangGraph 本身的问题。3. 定义工具的几个细节函数名、描述、参数 Schema 和返回值3.1 工具描述写得好不好直接决定模型会不会调用很多人在定义工具时只写一句话比如“查询天气”。这当然能跑但效果不稳定。模型本质上是靠工具名和描述来理解“什么时候该用这个工具”描述越准确调用准确率越高。更好的写法是说明以下几点工具能做什么。在什么场景下使用。参数单位和格式。返回结果里包含什么信息。有哪些边界条件。一个示例tool def search_stock(symbol: str) - dict: 查询 A 股股票最近一个交易日的行情。 Args: symbol: 股票代码例如 600519。 return {symbol: symbol, price: 1700.0, change_percent: 0.5}我这里只写了一个示意返回实际生产环境会去请求行情接口。重点是symbol参数说明为“股票代码例如 600519”模型就不会把“贵州茅台”直接当成symbol传进去。3.2 参数 Schema 越规范模型越不容易传错tool装饰器会根据函数签名生成 JSON Schema。你写的参数名、类型注解、默认值和 docstring都会影响模型如何构造参数。几个常见建议参数类型要明确尽量用str、int、float、bool不要全写Any。可选参数要有默认值并在描述里说明何时不传。如果日期是固定格式直接在描述里写“格式 YYYY-MM-DD”。如果是枚举值可以在类型注解里用Literal限制范围。比如from typing import Literal tool def set_room_temperature(room: str, mode: Literal[cool, heat, fan], temperature: int) - str: 设置空调模式。 Args: room: 房间名称例如“客厅”。 mode: 空调模式只能传 cool、heat 或 fan。 temperature: 目标温度范围 16 到 30。 return f{room} 已设置为 {mode}目标温度 {temperature}℃这样模型在生成参数时mode就不容易编出“制冷”这种中文词。3.3 返回值必须能序列化否则状态管理会出问题LangGraph 会把节点返回结果更新到状态里。为了让消息列表可以正确传递和检查工具返回的内容最好是字符串、字典、列表这些 Python 原生可序列化类型。我见过不少人在工具里返回一个自定义类对象然后在模型节点里直接读取属性。这个在小规模测试里可能没问题但一旦用ToolNode或序列化状态时就会报错日志还不容易看懂。最稳妥的做法工具函数内部处理逻辑外部返回字符串或 JSON 可序列化数据。需要返回结构时可以用json.dumps转成字符串也可以直接返回字典。LangGraph 的状态和 LangChain 消息结构本身通常支持字典但为了减少排查成本字符串是最保守的选择。3.4bind_tools绑定的到底是什么bind_tools不是把 Python 函数直接传给模型而是把模型的请求参数里加上tools字段里面是工具的 JSON Schema。模型在生成回答时会根据这些 Schema 判断是否需要调用工具以及如何构造参数。如果你在调用模型时忘记bind_tools模型节点仍然会返回普通文本绝不会因为你的截图或描述里提到工具就自动产生tool_calls。所以排查“为什么模型没有调用工具”时先看有没有执行bind_tools。有些模型还可以通过tool_choice强制调用某个工具。但大多数场景下不要用强制选择让模型自己判断会更灵活。强制调用适合“用户输入必然需要某个工具”的任务比如语音输入后的固定查库操作。4. 多工具场景模型选工具、参数错误、工具异常和结果回传4.1 工具一多模型就会选错模型在多个工具之间做选择本质上是在做语义匹配。如果两个工具描述接近比如“获取天气”和“获取温度”模型就会困惑。优化方法有几种工具名带领域前缀例如weather_openweather和weather_caiyun。描述里明确各自适用的数据源和场景。减少功能重叠的工具能不拆就不拆。如果两个工具逻辑差异很大但名称容易混淆可以给一个更独特的名字。我在实际测试中见过模型在“查订单状态”和“查物流轨迹”之间来回摇摆。后来发现不是模型笨而是工具描述都写了“查询订单信息”没有区分“订单状态是商家处理进度物流轨迹是快递运输节点”。把描述改清楚后准确率立刻上来了。4.2 模型输出的参数和 Schema 不一致怎么办这个问题在本地模型或参数较小的模型上特别常见。表现是模型生成了tool_calls但args里的字段名、类型或格式和工具签名不匹配。排查顺序建议如下打印模型的原始输出确认tool_calls里到底是什么。检查工具参数名称是否和模型输出一致。检查参数类型是否匹配比如模型传了字符串123但工具需要整数123。检查模型本身对 Function Calling 的支持程度。如果模型经常生成错误参数最简单的办法是换一个对 Function Calling 支持更好的模型。另一个办法是在工具函数内部做参数转换和兜底比如tool def query_order(order_id: str) - str: try: numeric_id int(str(order_id).strip()) except ValueError: return 订单号格式不正确请提供数字订单号。 # 继续处理这种方式至少能避免工具直接抛异常。4.3 工具执行时报错不要中断整条链路工具节点里调用的可能是数据库、外部 API 或本地资源任何一个都可能失败。默认情况下ToolNode执行工具抛出异常会让整个图运行失败。更稳妥的做法是把错误信息返回给模型让模型决定怎么补救。示例tool def query_inventory(item_id: str) - str: 查询商品库存数量。 try: data external_inventory_api(item_id) return f商品 {item_id} 库存为 {data[stock]} except Exception as e: return f查询库存失败错误信息{str(e)}。请提示用户稍后重试。这样当外部接口短暂不可用时模型会收到一条“查询失败”的工具消息它可以再问用户一次或者换个参数重试。这比直接让整个智能体崩溃要自然得多。4.4 工具执行结果回传时tool_call_id不能丢LangGraph 预置的ToolNode会处理好ToolMessage的tool_call_id链到对应的那一次tool_calls。但如果你自己实现工具节点就要特别注意这个问题。模型需要知道“这条工具结果对应哪一次调用”。如果tool_call_id对不上模型可能无法正确理解结果甚至把好几条工具结果混在一起。所以自己写工具节点时至少要做两件事读取 AI 消息里的tool_calls。执行每个工具后用ToolMessage(contentresult, tool_call_idcall[id])回传。不要简单地把工具结果追加成普通字符串那样模型无法建立调用和结果之间的对应关系。5. 把工具调用放进智能体循环条件路由、终止条件和子图5.1 为什么必须有循环有些任务只需要调用一次工具就能回答。但更复杂的任务比如“先查用户所在城市再查天气”模型可能需要连续调用两个工具。第一次模型调用get_city得到城市名后再调用get_weather。这就要在图结构里有一条从工具节点回到模型节点的边。LangGraph 里就是add_edge(tools, model)这句话。每次工具结果回到模型节点模型会把新消息加入上下文继续判断还需要调用哪个工具或者直接输出最终回答。这条链路和 ReAct 模式的思路一致推理、行动、观察、再推理。5.2 条件路由的常见写法条件路由判断的是“当前这一步该往哪走”。最常用的判断依据是最后一条 AI 消息里有没有tool_callsdef route_after_model(state): last_message state[messages][-1] if len(last_message.tool_calls) 0: return tools return __end__这个函数返回节点名或端点名。有工具调用就去工具节点没有就结束。如果你希望流程一直循环到模型不再调用工具为止这个写法就够了。但要注意路由判断的是“最后一条消息”。如果你的状态里夹杂着工具消息、系统消息或其他内容一定要确认索引取的是 AI 节点刚生成的那条消息而不是旧消息。5.3 设置最大步数避免无限循环模型在工具调用上也可能“上头”。比如工具总是返回错误模型就不断换参数重试或者模型陷入了“查了又查”的循环。生产环境里必须限制最大轮数。一种做法是在状态里记录步骤数from typing import Annotated, TypedDict from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] steps: int def model_node(state: AgentState): response model.bind_tools(tools).invoke(state[messages]) return {messages: [response], steps: state[steps] 1} def route_after_model(state: AgentState): if state[steps] 5: return END last_message state[messages][-1] if len(last_message.tool_calls) 0: return tools return END这里的steps每经过一次模型节点就加一。到达上限后直接结束图运行。具体上限可以根据任务复杂度设置但一般不要超过 10 到 15 轮否则用户会等到失去耐心。5.4 用子图复用工具调用流程当多个智能体共享同一套工具调用逻辑时可以先把“模型节点 工具节点 条件路由”封装成一个子图再在主图里引用。子图的优点是隔离性更好也方便把一组工具作为一个能力模块挂到不同场景下。实际项目里一个常见的做法是有一个订单智能体、一个客服智能体但它们都复用“地图查询”和“天气查询”工具。这时候把工具调用子图抽出来比每个智能体里复制一遍节点定义要干净得多。子图虽然会带来一点状态传递的复杂度但只要明确父子图之间的状态是整体共享还是局部隔离问题就是可控的。6. 调试和优化从启动失败到并发限制的排查顺序6.1 先确认链路跑通再谈效果优化我一般会分三步调先跑一条不需要工具调用的消息比如“你好”确认图能正常结束。再跑一条需要单工具调用的消息确认tool_calls和ToolMessage正确。最后跑一条需要多工具调用的消息确认循环和条件路由正常。如果第一步就报错看一下环境变量、依赖版本和模型接口地址。如果前三步都正常但还是觉得工具调用结果不理想再去看工具描述和模型参数。打印图结构也是一个有效的检查方式print(graph.get_graph().print_ascii())这样可以直观看到节点和边是否按预期连接。如果工具节点没有回到模型节点流程就会少一环。6.2 常见报错和排查顺序下面这张表是我排查工具调用问题时最常用到的顺序现象常见原因优先排查点启动时报ModuleNotFoundError依赖没装全确认是否安装了langgraph、langchain-core、langchain-openai没有设置模型 API Key提示鉴权失败检查环境变量比如OPENAI_API_KEY模型没有生成tool_calls没有bind_tools或模型不支持先确认绑定逻辑再看模型能力工具参数格式不对模型输出和 Schema 不一致打印原始输出对照工具参数名和类型工具执行报错外部接口异常或参数不合法在工具内部加try/except先把错误转成文本无限循环缺少最大步数限制在状态里加steps路由里设置上限结果回传混乱tool_call_id对不上使用ToolNode或手动确保 id 一致不要一上来就怀疑 LangGraph 本身。大多数问题出在依赖版本、模型接口或工具参数上。6.3 并发调用和资源占用需要提前设计工具调用不只是单个请求的问题。如果你的智能体要服务多个用户工具节点可能同时被多次调用而每个工具背后可能还有外部 API 限流。这时先别急着并发拉满。正确的思路是确认单个工具请求的平均耗时和最大耗时。确认外部 API 的并发上限。在工具函数内部加超时控制比如timeout10。对受外部限流的工具加信号量或队列。LangGraph 支持异步执行可以用ainvoke或astream。但异步不等于无限并发。工具函数如果操作共享文件、数据库连接池还要特别注意资源释放。6.4 低配置机器怎么降低负担如果你用的是本地模型工具调用会占用额外的上下文长度因为每个工具 Schema 都会追加到模型请求里。工具越多请求越长显存和内存压力越大。低配置机器可以参考这几个做法减少同时绑定的工具数量按场景分组。使用更短的描述降低 token 占用。模型切到参数量更小的版本。先把高频工具绑定进去低频工具放到第二轮再绑定。用 mock 工具先验证流程再切换到真实 API。低配置能跑通不代表能支撑并发生产任务。学习阶段用默认配置没问题长期运行就要把日志、输出目录、任务队列和资源监控都补上。7. 从 Demo 到工程工具调用还要补这些能力7.1 把工具描述当成产品文案来写工具描述是模型理解工具的窗口。实际项目中模型多一次误调用用户体验就会差一截。所以工具描述要写清楚“能做什么、不能做什么、什么时候该用”。比如一个支付接口的工具描述如果只写“执行支付”模型可能会在用户还没明确确认时就触发支付。这时候描述里应该加上“只有用户明确表达支付意向后才调用调用前需要订单号和金额确认”。工具描述是产品规则的一部分不只是代码注释。7.2 工具入口要做参数校验和权限控制大模型生成的参数不一定安全。一个查询工具用户可能通过对话让模型传入恶意路径、超长字符串或越权参数。工具函数内部要做白名单校验、长度限制和权限检查。例如文件读取工具要确认路径在允许的目录范围内。数据库查询工具要限制结果行数。HTTP 请求工具要校验 URL 和请求头。敏感操作工具要有审批或二次确认机制。不是所有工具都能暴露给模型随意调用。工程化之后你需要在“模型调用工具”和“允许调用工具”之间加一层控制。7.3 超时、重试和幂等是逃不掉的外部接口可能超时网络可能抖动。工具调用必须设置超时时间并区分“临时错误”和“参数错误”。临时错误网络超时、接口 5xx可以重试。 参数错误模型传了非法参数重试大概率还会失败应该把错误返回给模型修正。写代码时可以用循环控制重试次数但每次重试之间加一点退避时间。涉及写操作的工具比如下单、发送消息、扣款如果可能被重复执行就要在业务侧保证幂等。模型一旦因为网络超时重试不能产生两笔订单。7.4 工具调用日志要能回答四个问题生产环境里排查问题最怕日志只记录了“工具调用失败”。一份完整的工具调用日志至少要能回答用户当时问了什么。模型选择了哪个工具。传了哪些参数。工具返回了什么耗时多久。如果这些信息都有大多数问题都能快速定位到是模型选错、参数错、工具代码错还是外部接口错。我自己的习惯是在工具函数外部统一记录日志而不是在每个工具里重复写。比如在自定义工具节点中遍历tool_calls时统一打印for call in tool_calls: logger.info(tool_name%s, args%s, call[name], call[args]) result execute_tool(call) logger.info(tool_result%s, result)这样比事后从模型输出里反推参数要省力得多。如果把这篇文章浓缩成一句话LangGraph 的工具调用核心不是“能不能调工具”而是“让模型在合适的时候、用正确的参数、调用合适的工具并把结果稳定地放回流程里”。我个人建议先把单条链路跑稳再加多工具、加循环、加并发。踩过几次坑之后你会发现大多数问题不是模型不够聪明而是工具描述、参数 Schema、错误处理和环境配置没有提前整理好。
返回列表