news 2026/10/15 2:14:48

LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解

为什么单独聊工具调用

很多人学 LangChain,第一个完整例子往往是 Agent:给几个 Tool,接上模型,一句agent.invoke()就出结果。跑通很快,但一旦工具报错、模型不调用工具、或者需要自己控制中间步骤,就会卡住。原因在于 Agent 把工具调用的完整链路包起来了,看不到里面发生了什么。

工具调用本身是一条清晰的链路:模型判断要不要调用工具、生成结构化参数、你的代码执行工具、把结果塞回对话、模型再生成最终回答。这条链路理解了,Agent 就只是外面包了一层循环。

我下面用一个具体场景把这条链路走一遍:用户问“北京现在多少度,换算成华氏度是多少”。这需要两个工具,一个是查天气,一个是温度换算。为什么用这个例子?因为它天然需要两次工具调用,而且第二次依赖第一次的结果,能顺带看清多轮工具调用的处理方式。

需要先说明的是,LangChain 更新比较快,包结构经历过拆分。本文代码基于langchain-core、langchain、langchain-openai这几个包在 2024 年下半年之后的常见结构,模型用的是 OpenAI 兼容接口。不同小版本之间 API 可能有细微差异,你按自己安装的版本对照即可,不确定的地方我会标出来。

工具调用的基本链路

先不写代码,把链路说清楚。

  1. 你把工具的定义(名字、描述、参数 schema)和用户消息一起发给模型。
  2. 模型返回的不是普通文本,而是一个或多个tool_calls,每个包含工具名和 JSON 参数。
  3. 你的代码根据工具名找到对应函数,用参数执行,拿到结果。
  4. 把结果作为一条ToolMessage追加到对话历史。
  5. 再次调用模型,模型基于工具结果生成自然语言回答。

关键点在于第 2 步和第 3 步之间:模型只负责“决定调什么、传什么参数”,真正执行的是你的代码。模型不会、也没能力执行任何函数。这一点决定了安全边界和错误处理都落在你这边。

定义 Tool

LangChain 里定义工具最直接的方式是@tool装饰器,加类型注解和 docstring。

# 依赖:langchain-corefromlangchain_core.toolsimporttool@tooldefget_weather(city:str)->str:"""查询指定城市的当前天气情况。当用户询问某地天气、温度时使用此工具。 Args: city: 城市名称,例如 "北京"、"上海"。 """# 真实项目里这里应调用天气 API,这里用固定数据演示fake_db={"北京":"晴,气温 25 摄氏度","上海":"多云,气温 28 摄氏度",}returnfake_db.get(city,f"未找到{city}的天气数据")@tooldefcelsius_to_fahrenheit(celsius:float)->float:"""把摄氏度转换为华氏度。当需要温度单位换算时使用此工具。 Args: celsius: 摄氏度数值。 """returncelsius*9/5+32

这里有几个细节值得说。

docstring 不是装饰,它会被转成工具描述发给模型,模型靠这段文字判断什么时候调用这个工具。写得含糊,模型就容易漏调或错调。我一般会写清楚“什么时候用”,而不只是“这是什么”。

参数名和类型注解决定了生成给模型的 JSON Schema。city: str会变成{"type": "string"}。如果参数是可选或复杂结构,用Optional、Literal、Pydantic 模型都可以,LangChain 会帮你转换,但复杂类型建议先跑一次看生成的 schema 对不对。

工具函数最好保持“纯函数”风格:输入明确,输出明确,不依赖外部可变状态。因为模型可能并行调用多个工具,也可能重试,有副作用的工具要特别小心。

绑定模型并拿到 tool_calls

定义好工具后,用bind_tools把工具挂到模型上。

# 依赖:langchain-openaifromlangchain_openaiimportChatOpenAI llm=ChatOpenAI(model="gpt-4o-mini",temperature=0)tools=[get_weather,celsius_to_fahrenheit]llm_with_tools=llm.bind_tools(tools)

bind_tools做的事,是把工具的 schema 转成模型 API 认识的tools参数格式,附在每次请求上。注意它是返回一个新的 runnable,不是原地修改llm。

接下来发一条消息,看模型返回什么。

fromlangchain_core.messagesimportHumanMessage messages=[HumanMessage(content="北京现在多少度?换算成华氏度是多少?")]ai_msg=llm_with_tools.invoke(messages)print(ai_msg.tool_calls)

如果模型决定调用工具,ai_msg.content通常是空的,ai_msg.tool_calls会是一个列表,结构大致是这样:

[{"name":"get_weather","args":{"city":"北京"},"id":"call_abc123","type":"tool_call",}]

id很重要。回填结果时,ToolMessage必须带上对应的tool_call_id,模型靠它把结果和请求对应起来。尤其在并行调用多个工具时,少了这个 id 或者 id 对不上,模型就分不清哪条结果属于哪个调用。

【踩坑提醒】.content为空不代表出错。工具调用场景下,模型把信息放在tool_calls字段里,content为空是正常的。反过来,如果tool_calls为空而content有内容,说明模型选择直接回答,这时候就不用执行工具了。

手动执行工具并回填结果

先看不借助 Agent 的手动链路,这样每一步都在自己手里。

fromlangchain_core.messagesimportToolMessage# 保存模型这轮的输出,里面带着 tool_callsmessages.append(ai_msg)# 建立工具名到工具对象的映射tool_map={t.name:tfortintools}fortool_callinai_msg.tool_calls:selected=tool_map[tool_call["name"]]# invoke 传入参数 dict,langchain 会按 schema 校验result=selected.invoke(tool_call["args"])messages.append(ToolMessage(content=str(result),tool_call_id=tool_call["id"]))# 带着工具结果再问一次模型final_msg=llm_with_tools.invoke(messages)print(final_msg.content)

这套流程跑通,你就理解了 Agent 内部在做什么。create_tool_calling_agent之类的高层封装,本质就是把这个 for 循环和再次调用包进一个循环里,直到模型不再请求工具、直接给出文本为止。

有个细节:selected.invoke(tool_call["args"])里,LangChain 会用工具的参数 schema 校验args。如果模型给错了参数名或类型,这一步可能抛异常,而不是静默返回错误内容。这其实是好事,能尽早暴露问题。但生产环境里我一般会用try/except包起来,把异常转成一条说明性的ToolMessage回给模型,让模型有机会自己修正参数重试,而不是直接让整个请求崩掉。

fortool_callinai_msg.tool_calls:selected=tool_map.get(tool_call["name"])ifselectedisNone:messages.append(ToolMessage(content=f"未找到名为{tool_call['name']}的工具",tool_call_id=tool_call["id"],))continuetry:result=selected.invoke(tool_call["args"])content=str(result)exceptExceptionase:content=f"工具执行失败:{e}"messages.append(ToolMessage(content=content,tool_call_id=tool_call["id"]))

把工具错误当成一条普通结果回给模型,是我比较推荐的做法。模型看到“参数错误”这类信息,往往能自己调整再调一次。

换成 Agent 写法

理解了手动链路,再看 Agent 封装会轻松很多。下面用的是create_tool_calling_agent配AgentExecutor的常见组合。

# 依赖:langchain(提供 agent 与 executor)fromlangchain.agentsimportAgentExecutor,create_tool_calling_agentfromlangchain_core.promptsimportChatPromptTemplate prompt=ChatPromptTemplate.from_messages([("system","你是一个助手,可以调用工具回答问题。"),("human","{input}"),("placeholder","{agent_scratchpad}"),])agent=create_tool_calling_agent(llm,tools,prompt)executor=AgentExecutor(agent=agent,tools=tools,verbose=True)result=executor.invoke({"input":"北京现在多少度?换算成华氏度是多少?"})print(result["output"])

agent_scratchpad这个占位符是关键,它会被替换成中间的工具调用和结果记录。手动链路里我们往messages里 append 的那些内容,在 Agent 里就是通过这个占位符注入的。

verbose=True在调试阶段很有用,它会打印出每一步模型决定调什么工具、参数是什么、工具返回什么。我第一次接一个工具时基本都开着它,确认模型确实按预期在调。

【注意】create_tool_calling_agent依赖模型本身支持工具调用(function calling)。不是所有模型都支持,比如一些本地小模型或者旧接口。如果模型不支持,这套写法会失败。这种情况要么换成支持工具调用的模型,要么用基于 ReAct 文本解析的老式 Agent,但那种方式靠解析模型输出的文本,稳定性差一些。

两种写法怎么选

方案优点缺点适用场景
手动链路每步可控,便于调试、审计、自定义重试代码多,需要自己写循环和终止条件需要精细控制、做可观测性、排查问题
create_tool_calling_agent + AgentExecutor代码少,开箱即用中间过程被封装,定制需要看源码快速验证、标准场景、原型

我的实际习惯是:原型阶段先用 Agent 快速验证模型和工具描述是否匹配;要上线或者要接监控,就改回手动链路,或者基于 Agent 的中间步骤回调做记录。两者不冲突,手动链路本身就是理解封装的工具。

几个容易出问题的地方

工具描述写不好,模型不调用。这是最常见的问题。如果模型该调工具却直接编了个答案,先检查 docstring 是否清楚说明了使用场景。描述里最好出现“当用户……时使用”这类触发条件。

参数类型对不上。模型生成的参数类型偶尔和 schema 不完全一致,比如该传数字传了字符串。Pydantic 校验能拦住大部分,但复杂嵌套结构建议单独测。这一点我没有对所有模型都验证过,具体表现跟模型有关。

并行调用的顺序。当模型一次返回多个tool_calls,它们是并行执行的,别假设有先后顺序。如果工具之间有依赖(比如本例里换算依赖查天气的结果),模型通常会分两轮调用,而不是一轮里同时调。如果发现模型硬要并行调有依赖的工具,多半是工具描述没把依赖关系说清楚。

消息历史要保留完整。手动链路里,ai_msg(带 tool_calls 的那条)必须 append 进 messages,否则回填ToolMessage时模型会报找不到对应调用。这个错误我见过几次,都是漏了 append。

写在最后

工具调用这条链路不复杂,核心就是“模型出参数、代码执行、结果回填、再问模型”四步。Agent 把它包起来是为了少写循环,但封装越厚,出问题时越难定位。我的建议是至少手动走通一遍,之后用不用 Agent 都心里有底。

下一步可以研究的方向是工具的错误恢复和重试策略,以及怎么在工具调用链路上加日志和耗时统计,这些在真实项目里比“能不能调通”更影响可用性。

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

Web经典高危漏洞深度防御实战白皮书:面向云原生时代的十重安全防线

在云原生微服务架构、容器编排与 Serverless 计算普及的今天,Web 攻防的战场已经发生了深刻的变化。过去单体系统时代的 Web 漏洞往往只影响单一主机的权限,而在由 Kubernetes、网关代理、分布式缓存与云元数据服务织就的庞大内网体系中,一个…

作者头像 李华
网站建设 2026/10/15 2:11:45

计算机基础408目录页

这里是计算机基础408的目录页,CSDN写的文章散落在一起,没有清晰的结构,查找起来比较麻烦,所以我建立了这个目录结构页面,它将包含其目录下所有文章的链接。 文章目录计算机基础408数据结构和算法计算机操作系统&#x…

作者头像 李华
网站建设 2026/10/15 2:09:31

数据管道健壮性与幂等第二周复盘:RocksDB 状态引擎、两阶段提交与高维向量混合去重

在百 TB 级大模型预训练数据清洗、多模态图文对质检以及高质量指令集微调构建的数据工程实践中,算法研究员与数据工程师面对的从来不是风平浪静的实验室环境。相反,这是一个充斥着节点突发 OOM 崩溃、物理网络闪断、磁盘 I/O 抖动、以及包含畸变非法字符…

作者头像 李华