LangChain Tool Calling实战:从手动解析到LangGraph Agent落地

发布时间:2026/10/19 17:14:07
LangChain Tool Calling实战:从手动解析到LangGraph Agent落地 第一次在LangChain里跑通工具调用Tool Calling时我的第一反应是这不就是让模型学会填一张调用单吗等真正把它接到业务里我才意识到Tool Calling的本质是把模型说了什么和系统做了什么之间的模糊地带彻底变成了一道结构化协议。在它出现之前想让大模型查天气、查库存、算运费典型做法是让模型在回答里夹带暗号比如请查询城市上海然后开发者在外面用正则或字符串匹配去猜。模型稍微换个说法你的解析就跪了。更麻烦的是模型根本不知道你有哪些工具可用全凭它在训练数据里蒙。Tool Calling改变的是这个底层的交互方式模型不再靠猜而是在系统明确给出的工具清单里输出一个格式固定、参数清晰的调用意向然后由你自己的代码真正去执行。这篇文章不打算推荐什么终极Agent框架而是要把我自己的完整路径拆开——从手动解析JSON、绑定工具、读取调用意图到用LangGraph里的ToolNode跑起一个真正能落地的Agent包括那些文档里不会写、线上最容易翻车的坑。适合正在做Agent、想给模型挂工具的开发者也适合刚看完LangChain入门、想知道Tool Calling到底怎么落地的人。1. Tool Calling的本质模型只负责填调用单执行全靠你的代码1.1 没有Tool Calling时我们是怎么被逼疯的先回忆一下没有Tool Calling的日子。你想让模型帮你查某个城市的天气常规操作是把它当作一个文本生成任务系统提示词里写如果用户问天气请输出WEATHER:城市名然后你在外面用正则去抓。模型心情好的时候输出得很标准心情不好的时候会来一句请稍等我正在查询上海天气你的正则直接落空。再往后有人开始让模型输出JSON可模型经常把JSON塞进Markdown代码块或者把布尔值true写成True解析时又是一地鸡毛。这种做法的本质问题是模型输出的是给人看的话而你想让系统执行的是动作。这两者之间没有稳定的协议。你越是想靠提示词约束模型就越容易在边界情况里放飞。工具调用Tool Calling这个机制就是来补上这个协议空白的——它让模型不再直接输出自然语言去描述我想干什么而是输出一个结构化的调用意图例如调用哪个函数、传什么参数格式由模型API本身保证和文本内容分开传递。1.2 工具调用的协议长什么样不管底层实现怎么包装Tool Calling的核心数据结构万变不离其宗就是下面这种东西{ name: get_weather, arguments: {\city\: \上海\, \date\: \2025-06-01\} }注意arguments通常是一个字符串里面嵌套JSON需要二次解析。有些框架已经把这一步封装好了但理解这一点很重要因为当你手写日志、排查参数抽取错误时经常会看到原始API层返回的是这种嵌套字符串。这个协议的意思是模型通过分析用户请求认为此时应该调用一个名为get_weather的工具并传入参数。模型并不执行这个函数它只是把调用单填好交给你。真正去查天气、查库存、请求数据库的是你自己的代码。等工具执行完你再把执行结果作为一条消息返回给模型模型看到结果后继续生成下一轮回答或再次发起调用。1.3 模型如何知道有哪些工具可用模型能填出正确的调用单前提是它知道有哪些工具存在、每个工具是干什么的、参数长什么样。这在实现上就是把工具的元信息转换成一个JSON Schema列表随请求一起发给模型。LangChain的做法是你用装饰器或StructuredTool定义好工具再通过bind_tools挂到模型上。模型侧收到的是类似这样的信息{ type: function, function: { name: get_weather, description: 查询指定城市指定日期的天气情况, parameters: { type: object, properties: { city: {type: string, description: 城市名}, date: {type: string, description: 日期格式YYYY-MM-DD} }, required: [city] } } }也就是说绑工具这件事本质上是在给模型写一份工具菜单菜单写得越清楚模型点菜点得越准。后面聊到描述优化时我会专门讲这个菜单怎么设计才不容易翻车。2. 工具定义从普通函数到懂规矩的工具对象2.1 tool装饰器一行代码把函数变成工具最简单的方式是直接用装饰器。你不需要去继承某个基类也不需要手动拼JSON SchemaLangChain会根据函数的签名和docstring自动生成元信息from langchain_core.tools import tool tool def get_weather(city: str, date: str None) - str: 查询指定城市在指定日期的天气情况。 参数说明 - city: 城市中文名如上海、杭州 - date: 可选日期格式YYYY-MM-DD不传则返回当天与未来3天 return f模拟天气数据{city}{date or 今天}多云 25°C这里有三个细节值得注意。第一函数的参数名要尽量直观因为参数名会直接出现在给模型的Schema里city比c好一百倍。第二docstring一定要写清楚参数含义和边界情况在工具调用里docstring比普通函数里的注释重要得多因为模型是用它来理解什么时候该用这个工具的。第三返回类型建议统一用str工具执行结果是要塞进消息上下文给模型读的返回Python对象字符化之后往往又长又乱不如在工具内部就整理成一行可读的文本。2.2 用args_schema管住参数别让模型乱传参依赖函数签名没问题但函数签名能表达的约束有限。比如你想限制date必须是合法的日期格式或者city必须是枚举值之一函数签名做不到Pydantic模型可以。from typing import Optional from langchain_core.tools import StructuredTool from pydantic import BaseModel, Field class WeatherParams(BaseModel): city: str Field( description城市中文名, examples[北京, 上海, 广州] ) date: Optional[str] Field( defaultNone, description日期格式YYYY-MM-DD不传返回当天及未来3天 ) def _get_weather(city: str, date: Optional[str] None) - str: ... get_weather StructuredTool.from_function( func_get_weather, nameget_weather, description查询指定城市指定日期的天气情况, args_schemaWeatherParams, )用StructuredTool.from_function的好处是可以精细控制工具名和描述而且Pydantic字段里的description、examples会一并转成JSON Schema传给模型。实测下来examples的作用非常大模型在犹豫参数格式时会优先模仿examples里的写法。换句话说你给一个上海的例子模型就不太可能把城市名传给date字段。2.3 工具描述才是真正的提示词很多初学者把重心放在函数实现上却忽略了description。实际上在工具调用里description就是给模型看的提示词它决定了模型在什么场景下会选中这个工具。对比一下两种写法写法实际效果天气工具模型只知道有个天气工具不知道参数格式不知道返回值容易和其他工具混淆查询指定城市指定日期的天气情况。入参city为城市中文名date为可选参数格式YYYY-MM-DD不传date时返回今天及未来3天预报。适合用户询问天气、温度、降水、风力时使用。模型清楚知道触发条件、参数格式、返回范围第二个描述明显更可决策。模型每次调用工具前都会做一次匹配描述越精确误报率越低。注意不要在这里写太多花哨的话工具描述会占用上下文最长控制在200字左右把触发场景、参数约束、边界条件写清楚就够了。2.4 工具不是越多越好合并与拆分策略一个常见的误区是工具越多Agent能力越强。实践下来恰恰相反工具数量超过8个以后模型的选择准确率会明显下降尤其当存在多个描述相似的工具时。我的建议是遵循两条原则。第一同域操作合并成一个工具。比如查天气、查空气质量、查日出日落完全可以合成一个weather_tool内部用一个operation参数区分。模型只需要在调用单里填operationweather/air/sun执行分支由你代码去处理。这能显著降低模型在多个近似工具间的犹豫。第二工具语义边界要互相排他。如果两个工具都覆盖查询能力请在其中一方的描述里明确写本工具不适用于xxx场景请使用另一个工具。这听起来有点笨但模型确实会读这种排他说明而且很吃这一套。3. bind_tools与模型返回解析从人类读到机器读3.1 bind_tools到底做了什么理解了工具定义接下来把工具挂到模型上。在LangChain里这一步通常长这样llm_with_tools llm.bind_tools([get_weather])每次看到这种封装得很干净的API我都会习惯性问一句它在背后做了什么答案是把工具列表转换成了模型API能识别的tools参数然后在一次请求中把用户消息和工具Schema一起发给模型。它不改变模型的推理方式也不包含执行工具的逻辑只是建立了一个模型能看到哪些工具的契约。bind_tools返回的是一个新的、持有工具清单的模型对象原模型仍然是干净的这在高并发场景里很重要你不必反复重建模型而是可以预先绑定一份工具列表复用。3.2 读取模型的调用意图AIMessage.tool_calls模型响应在LangChain里被封装成AIMessage其中tool_calls属性就是模型填好的调用单resp llm_with_tools.invoke(上海明天天气怎么样) if resp.tool_calls: print(resp.tool_calls[0][name]) # get_weather print(resp.tool_calls[0][args]) # {city: 上海, date: 2025-06-02} print(resp.tool_calls[0][id]) # call_xxx平台生成的唯一ID每个tool_call里都有三个核心字段工具名、参数、ID。其中ID极其关键。执行完工具之后你要把执行结果包装成ToolMessage并把tool_call_id设置成这个ID模型才能把执行结果和之前发起的调用对应起来。如果ID对不上或者丢失轻则报错重则模型会认为结果不是它想要的又发起一次重复调用。3.3 完整的手动工具执行循环把上面的逻辑串起来一个最朴素但完整的工具调用循环是这样的import json from langchain_core.messages import ToolMessage # 工具注册表 tools_by_name {tool.name: tool for tool in [get_weather]} # 第一轮模型决定调用什么 resp llm_with_tools.invoke(上海明天天气怎么样) if resp.tool_calls: tool_messages [] for call in resp.tool_calls: tool tools_by_name.get(call[name]) if not tool: content f工具 {call[name]} 不存在 else: try: content str(tool.invoke(call[args])) except Exception as e: content f工具执行失败: {e} tool_messages.append(ToolMessage( contentcontent, tool_call_idcall[id], )) # 第二轮把工具结果回填给模型 final_resp llm_with_tools.invoke([resp, *tool_messages]) print(final_resp.content) else: print(resp.content)这个循环看起来简单但它包含了所有工具调用必须遵守的协议先取调用单再执行再回填。你后续无论用多复杂的框架底层都是这个循环。执行工具时注意要捕获异常并把错误信息返回给模型不要因为工具抛异常就中断整个流程——有时候模型正是看了错误信息才能纠正参数继续往下走。3.4 强制模型调用某个工具tool_choice的玩法默认情况下模型根据自己的判断决定调用哪个工具还是直接回答对应参数tool_choiceauto。但有些场景你需要强制模型先调用某个工具比如做数据接口路由时第一步必须调用router_tool判断请求去哪个下游系统。在LangChain里可以这样绑定llm_with_forced_tool llm.bind_tools( [router_tool, get_weather], tool_choicerouter_tool )强制调用在流程编排里特别好用先让模型做一个明确的分类调用拿到结果后再决定是否调用其他工具。注意不同模型API对tool_choice的支持程度不一样有的支持required表示必须调用任意工具有的支持any表示只能调用工具不能直接回答对接前先确认底层模型支持哪种语义别绑定完才发现不生效。4. 从手动循环到LangGraph工具调用在Agent流程里站稳脚跟4.1 为什么手动循环不够用上面的手动循环能跑通最简单的单轮工具调用但实际业务中的Agent往往是多步的先确认城市名再查天气然后根据天气推荐穿搭中间还可能调用另一个工具查限行。如果你全用手动循环写每增加一步你都得自己维护消息列表、判断要不要继续调用工具、控制最大轮数代码很快会膨胀成一坨条件分支。这个时候就需要一个能显式管理状态的编排框架我选择的是LangGraph它把调用工具和判断下一步拆成图里的节点和边逻辑一眼就能看清。4.2 ToolNode把遍历工具执行回填ToolMessage打包LangGraph的prebuilt里有个非常实用的组件叫ToolNode。它做了我在3.3节手写的那件事并且处理得更完整from langgraph.prebuilt import ToolNode tool_node ToolNode([get_weather])ToolNode的输入是一个消息列表它会自动扫描列表中所有AIMessage的tool_calls逐个执行对应工具再把结果封装成ToolMessage返回。你不用再手动拿call[id]去配对它全包了。如果你的工具是异步的ToolNode也支持异步执行不过要注意线程安全问题后面我会专门提。4.3 条件边让Agent自己决定继续还是回答光有ToolNode还不够Agent需要决定什么时候调用工具、什么时候结束。在LangGraph里这个决策靠条件边实现。核心逻辑非常简单看当前消息里是否还有tool_calls有就继续走工具节点没有就说明模型可以直接回答走结束节点。def should_continue(state): last_message state[messages][-1] return tools if last_message.tool_calls else end这个机制的最大好处是无限环节点有了明确的退出条件只要模型不再发起新的工具调用流程就自然收敛到最终答案。你还可以给Agent加上recursion_limit之类的上限防止极端情况下的死循环。4.4 更省事的做法create_react_agent如果不想自己拼状态图LangGraph的prebuilt直接提供了一个开箱即用的ReAct Agentfrom langgraph.prebuilt import create_react_agent agent create_react_agent( modelllm_with_tools, tools[get_weather], prompt你是智能助手可以使用工具解决问题。 ) result agent.invoke({messages: [(user, 上海明天天气怎么样)]})create_react_agent内部就是上面说的模型节点ToolNode条件边的组合。它会自动维护消息列表工具执行结果回填给模型后模型会继续推理直到完成回答。对大多数常规场景直接用它就足够了当你需要更细粒度控制时再回到手写状态图。4.5 并行工具调用与流式输出的注意事项现代的大模型API普遍支持一次返回多个tool_calls也就是模型同时在调用单上填了多行。LangChain对并行调用的支持很自然ToolNode会依次执行每个调用并把多个ToolMessage合并返回。但这里有几个坑。一个是并行执行工具时如果工具内部共享了全局状态比如同一个临时文件、同一个数据库连接容易出竞争问题建议工具内部做成无状态或者自带连接管理。另一个是流式输出场景模型API在流式返回时tool_calls的分段数据可能被拆成多个chunk先后到达你需要把chunk按index和id拼起来等所有partial_tool_calls都收齐后再去执行工具。如果用LangGraph的astream注意在tools节点阶段不要过早终结事件流否则会丢结果。5. 工具调用事故高发区我踩过的坑与排查思路5.1 报错一tool_call_id不匹配这个是最常见的低级错误但排起来很恶心。典型报错是Tool call not found或直接API层返回400。原因通常有三种回填ToolMessage时忘了传tool_call_id执行工具时擅自生成新的ID而不是原样使用模型返回的ID以及在手动拼接消息时不小心丢掉了原始AIMessage。排查方法很朴素把进入工具执行前和回填后的消息列表完整打印出来人工核对ID是否一致。我建议在开发阶段给每个工具调用环节打一条结构日志把工具名、调用的ID、参数、返回结果前200字全部记录下来。这看着笨但能省掉大量线上排查时间。5.2 工具抛错后Agent原地打转另一个高频问题是工具执行抛了异常Agent会反复调用同一个工具在同一个错误上不断打转直到撞上最大迭代次数。根因往往是错误信息没有正确回传给模型或者回传的信息太模糊模型看不懂该怎么纠正。我的做法是在工具内部捕获所有异常并把它转换成一句对模型友好的错误说明比如except Exception as e: return f参数校验失败date应为YYYY-MM-DD格式当前收到值{call[args].get(date)}。请修正后重试。这样模型看到的是哪里错了、你该改什么而不是一串看不懂的堆栈。同时我会在Agent上设置不高于10的迭代上限并让超限后的消息明确告诉用户连续失败建议更换问题描述。5.3 描述冲突导致模型选错工具假设你同时挂了get_weather和get_climate两个工具描述都写查询天气模型大概率会选错。这不是模型笨而是你给的信息本身没有区分度。解决思路有三个合并同类工具一个工具用operation区分模式在描述里明确排他边界比如写只查短期天气中长期气候预测请用另一个工具给参数加examples让模型从参数猜测更省力。5.4 参数类型和枚举值对不上模型生成的参数有时会突破你的预期把int传成字符串、把枚举传成不在列表里的值、把必选参数传成None。Pydantic的args_schema能解决一部分强转问题但真正稳妥的做法是工具内部做二次校验非法参数直接返回纠正提示而不是抛出异常让流程中断。5.5 工具返回内容过大撑爆上下文工具返回一整张报表或几万条JSON模型读不完上下文先爆了。解决思路是摘要化引用化在工具内部就对结果做裁剪只返回前20条或聚合统计同时把完整数据写入缓存返回一个引用ID。比如共查询到283条记录已返回前20条完整数据集ID为cache://xxx。模型需要更多数据时它会去调用get_cache工具继续取。这既能控制token又不会丢失信息。5.6 工具命名的隐藏规则给工具起名不是纯看心情很多模型API对工具名有约束一般只允许字母、数字、下划线和短横线不能有空格、中文或特殊符号。建议一律用小写字母加下划线比如get_weather_detail不要用getWeatherDetail或获取天气这样的命名一个是不兼容另一个是模型对驼峰分词的识别不如小写下划线稳定。6. 进阶组合结构化输出、强制路由与多轮兜底6.1 with_structured_output与Tool Calling的分工LangChain里还有一个容易和Tool Calling混淆的功能叫with_structured_output它让模型输出一个严格符合Pydantic结构的对象。两者的区别在于structured output要的是结果长成什么样工具调用要的是执行什么动作。实际项目里它们经常搭配使用比如先用structured output抽取出用户查询中的城市和日期再拿抽取结果去构造工具调用参数。这种先结构化抽取、再精准调用的组合模式比直接把原始用户问题丢给Agent要稳定得多尤其是面对不规范的输入时。6.2 串行链、分支路由与并行扇出工具调用不只是一次调用它可以是链式的。串行链最简单工具A的结果作为输入去找工具B比如先查城市坐标再用坐标查天气。分支路由通常配合3.4节的tool_choice强制手段第一步调用router_tool决定走哪个分支避免后续工具选择漂移。并行扇出则适合需要多源信息综合的场景比如同时查天气、查路况、查限行三个工具并行执行后汇总结果LangGraph对工具节点的一组调用会自然并行处理注意给工具加上合理的超时时间避免单个工具拖垮整个流程。6.3 异常自修复让Agent把报错变成下一步决策我强烈建议你在Agent里设计一层自修复机制。当工具返回错误信息后模型可以把错误当作决策依据修正参数后重新发起调用。这个机制不需要额外代码只要你在工具内部把错误描述写得足够可操作。实测下来这个自修复对模型是有效的绝大多数的参数错误它都能自己改对通常一轮到两轮内就会成功。如果连续两轮还是同一个错不要硬撑捕获异常走兜底逻辑。兜底可以是切换一个更强的模型重新跑一轮也可以是直接返回人工处理入口。原则是错误要让模型看得懂兜底要让流程停得住。6.4 落地的几点个人建议最后说几个实际项目的经验结论。工具数量控制在8个以内单个工具描述控制在200字左右工具返回内容尽量精简成10行以内的文本超长内容用摘要加引用替代。每次模型返回的tool_calls都要落日志包括工具名、参数、执行耗时和结果摘要这样线上出问题十分钟内就能定位到是哪一步调错了。给关键工具加上幂等键防止网络重试时产生重复的系统改动。改动工具描述后一定要做一轮回归测试用固定的问题集跑一遍看工具选择率和最终答案是否符合预期。我个人习惯是把工具清单当成一份会持续维护的接口文档来对待。每新增一个工具我都会先用几个固定问题测一轮把模型历史返回的tool_calls和工具执行结果原样存下来。这个习惯看着笨但排查问题时特别管用——它能让你快速看到模型在什么输入下选了哪个工具、为什么选错。工具调用这件事说穿了就是一层把模型意图翻译成系统动作的中间层中间层越透明你的Agent就越可控。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询