很多人第一次看到大模型调用天气查询、数据库搜索或计算器时,会产生一种错觉,模型好像突然获得了执行代码的能力。
其实不是。
模型只是在对话中生成了一条结构化的「调用请求」,真正执行函数、访问网络、读取数据库的人,仍然是你的应用程序。LangChain Tools 的价值,就是把普通 Python 函数包装成模型能够理解的工具契约,再把模型的调用请求接回运行时,形成一条可观察、可控制的执行链路。
这件事听起来简单,但生产系统中最常见的故障,恰恰发生在边界上。有人把工具调用请求当成工具执行结果,有人漏掉了 ToolMessage,有人让模型一次拿到几十个权限过大的工具,最后才发现 Agent 的行为无法解释。
本文从一个完整的消息循环开始,带你理解 Tool Calling 的数据结构、LangChain 的核心 API、多工具调用和 tool_choice,最后再讨论重试、权限、幂等和人工确认。示例可以独立运行,工具使用内存中的演示数据,不依赖真实天气或搜索服务。
本文解决什么,理解工具描述、tool_calls、ToolMessage 与模型循环之间的关系,并能写出一个有轮数上限的基础工具调用程序。
本文不展开什么,不展开 Agent 的状态图编排、LangSmith 的观测配置和具体业务系统的鉴权实现,这些内容需要结合自己的运行时设计。
读完能完成什么,读者可以注册两个演示工具,验证模型是否提出调用请求,执行工具并把结果送回模型,同时知道哪些地方必须由应用侧兜底。
除非段落明确写出「完整示例」,其余代码块都是解释某个 API 的局部片段,依赖条件会在片段前说明。
先回答一个关键问题,工具到底是什么
在 LangChain 中,工具是一个拥有明确输入和输出契约的可调用函数。它通常包含四类信息。
| 信息 | 作用 | 示例 |
|---|---|---|
| 名称 | 让模型在多个工具中选择目标 | get_weather |
| 描述 | 解释何时使用、能解决什么问题 | 查询指定城市的当前天气 |
| 参数模式 | 规定参数名、类型、是否必填 | city: str |
| 执行函数 | 在应用侧完成真实动作 | 查询缓存或调用 HTTP API |
普通函数只需要让 Python 调用者看懂。工具还要让模型看懂,所以描述和参数模式不是装饰品,而是模型决策的一部分。
工具也不是数据库、搜索引擎或支付系统本身。它只是一个受控入口。真正的外部副作用发生在函数体里,应该由应用代码负责鉴权、超时、审计和错误转换。
Tool Calling 的完整消息回路
一次典型的工具调用至少包含四个阶段。
- 用户发送自然语言问题。
- 模型返回
AIMessage,其中的tool_calls描述工具名称和参数。 - 应用根据
tool_calls找到对应函数并执行,生成ToolMessage。 - 应用把完整消息列表再次交给模型,模型根据工具结果生成最终回答。
可以把它画成一条消息序列。注意,图里的「执行工具」不是模型内部行为,而是你的 Python 进程或后端服务在做的事。
工具函数 聊天模型 应用运行时 用户 工具函数 聊天模型 应用运行时 用户 #mermaid-svg-9qdw81WDIWUunSam{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9qdw81WDIWUunSam .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9qdw81WDIWUunSam .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9qdw81WDIWUunSam .error-icon{fill:#552222;}#mermaid-svg-9qdw81WDIWUunSam .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9qdw81WDIWUunSam .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9qdw81WDIWUunSam .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9qdw81WDIWUunSam .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9qdw81WDIWUunSam .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9qdw81WDIWUunSam .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9qdw81WDIWUunSam .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9qdw81WDIWUunSam .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9qdw81WDIWUunSam .marker.cross{stroke:#333333;}#mermaid-svg-9qdw81WDIWUunSam svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9qdw81WDIWUunSam p{margin:0;}#mermaid-svg-9qdw81WDIWUunSam .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9qdw81WDIWUunSam text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9qdw81WDIWUunSam .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9qdw81WDIWUunSam .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9qdw81WDIWUunSam .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9qdw81WDIWUunSam #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9qdw81WDIWUunSam .sequenceNumber{fill:white;}#mermaid-svg-9qdw81WDIWUunSam #sequencenumber{fill:#333;}#mermaid-svg-9qdw81WDIWUunSam #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9qdw81WDIWUunSam .messageText{fill:#333;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9qdw81WDIWUunSam .labelText,#mermaid-svg-9qdw81WDIWUunSam .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .loopText,#mermaid-svg-9qdw81WDIWUunSam .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9qdw81WDIWUunSam .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9qdw81WDIWUunSam .noteText,#mermaid-svg-9qdw81WDIWUunSam .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-9qdw81WDIWUunSam .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9qdw81WDIWUunSam .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9qdw81WDIWUunSam .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9qdw81WDIWUunSam .actorPopupMenu{position:absolute;}#mermaid-svg-9qdw81WDIWUunSam .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9qdw81WDIWUunSam .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9qdw81WDIWUunSam .actor-man circle,#mermaid-svg-9qdw81WDIWUunSam line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-9qdw81WDIWUunSam :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发送问题 HumanMessage AIMessage(tool_calls) 按名称和参数调用函数 返回结果或错误 Human + AI(tool_calls) + ToolMessage 最终 AIMessage 展示自然语言答案
如果第三步没有发生,模型不会凭空知道工具结果。如果第四步没有发生,用户通常只能看到一条空内容的 AIMessage 或「我已经查询」之类的半成品。
版本、安装与环境变量
本文按 LangChain 1.x 的统一 API 编写。LangChain 仍在快速迭代,模型供应商对工具调用能力和参数格式的支持也不完全相同。发布或上线前,应将依赖固定在经过测试的版本,并重新阅读对应集成包的变更记录。
安装最小依赖。
shell
pip install -U "langchain>=1.0,<2" "langchain-openai>=1.0,<2" python-dotenv
在运行环境中配置密钥。示例使用 OpenAI 兼容模型,模型名可以通过环境变量替换。
dotenv
OPENAI_API_KEY=replace_with_your_key
MODEL_NAME=gpt-4o-mini
不要把密钥写进代码、工具参数、Trace metadata 或错误日志。若使用其他供应商,请安装对应的 LangChain 集成包,并按照官方文档修改模型初始化方式。
一个可以跑通的最小示例
下面的脚本实现了一个小型城市助手。它拥有天气查询和城市时区两个工具,工具返回的是固定演示数据,因此读者不需要先申请第三方天气 API。模型仍然负责判断是否调用工具、调用哪个工具和传递什么参数。
python
import os
from typing import Any
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, ToolMessage
from langchain.tools import tool
load_dotenv()
@tool
def get_weather(city: str) -> str:
"""查询演示天气数据。
Args:
city: 城市名称,例如北京、上海或杭州。
"""
weather = {
"北京": "晴,22 摄氏度,西北风 2 级",
"上海": "多云,25 摄氏度,东南风 3 级",
"杭州": "小雨,21 摄氏度,东风 2 级",
}
return weather.get(city, f"暂时没有{city}的演示天气数据")
@tool
def get_timezone(city: str) -> str:
"""查询城市所属时区。
Args:
city: 城市名称,例如北京、上海或纽约。
"""
zones = {
"北京": "Asia/Shanghai,UTC+8",
"上海": "Asia/Shanghai,UTC+8",
"杭州": "Asia/Shanghai,UTC+8",
"纽约": "America/New_York,通常为 UTC-5 或 UTC-4",
}
return zones.get(city, f"暂时没有{city}的演示时区数据")
def build_model():
"""按环境变量创建聊天模型,避免把模型实例藏在调用函数的局部作用域。"""
model_name = os.getenv("MODEL_NAME", "gpt-4o-mini")
return init_chat_model(f"openai:{model_name}", temperature=0)
def run_assistant(question: str) -> str:
"""运行一次受控的工具调用循环。
模型每轮最多提出一次或多次调用,应用执行后把 ToolMessage 放回消息列表。
max_rounds 防止异常模型或异常工具结果导致无限循环。
"""
model = build_model()
tools = [get_weather, get_timezone]
tool_map = {item.name: item for item in tools}
model_with_tools = model.bind_tools(tools)
messages: list[Any] = [HumanMessage(content=question)]
max_rounds = 4
for _ in range(max_rounds):
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
return str(response.content)
for call in response.tool_calls:
tool_name = call.get("name")
tool = tool_map.get(tool_name)
if tool is None:
# 未知工具不能静默忽略,否则模型会一直等待一个不存在的结果。
messages.append(
ToolMessage(
content=f"工具{tool_name}不存在,无法执行。",
tool_call_id=call["id"],
name=tool_name,
)
)
continue
try:
# 传入完整 tool call,LangChain 会保留 tool_call_id 并生成 ToolMessage。
tool_message = tool.invoke(call)
except Exception as exc:
# 工具异常应转换成模型可读的结果,同时由日志系统记录原始异常。
tool_message = ToolMessage(
content=f"工具执行失败,请基于失败信息重新判断。错误类型:{type(exc).__name__}",
tool_call_id=call["id"],
name=tool_name,
)
messages.append(tool_message)
return "工具调用轮数超过上限,已停止本次请求。"
if __name__ == "__main__":
print(run_assistant("请告诉我北京的天气和时区。"))
这段代码里有几个值得停下来看的细节。
@tool 把函数注册成 BaseTool 对象。函数名、docstring 和类型注解会参与生成工具 Schema。bind_tools 不会执行函数,它只会把工具描述发送给模型,并返回一个绑定了工具能力的可运行对象。
模型返回的 AIMessage 可能没有文本内容,但有 tool_calls。每一项通常至少包含 name、args 和 id。id 用于把后续的 ToolMessage 和原始请求对应起来,不能随意丢弃或重新生成。
tool.invoke(call) 才是应用侧执行函数的动作。函数返回值会被包装成 ToolMessage,然后和原始 AIMessage 一起再次传给模型。模型看到完整上下文后,才有机会把「天气数据」翻译成面向用户的回答。
直接调用工具和让模型选择工具
本节的代码是局部片段,依赖上方完整脚本已经定义的 get_weather、get_timezone 和 build_model。需要直接运行某个片段时,先执行完整脚本中的定义,再在片段开头写 model = build_model();模型实例不再依赖某个函数的局部变量。
工具对象仍然可以像普通函数一样直接调用。
python
print(get_weather.invoke({"city": "北京"}))
这种方式适合单元测试、后台定时任务或开发阶段验证函数本身。它不涉及模型,也没有自然语言理解成本。
开发中的主流方式是绑定工具。
python
model = build_model()
model_with_tools = model.bind_tools([get_weather, get_timezone])
response = model_with_tools.invoke("北京今天适合出门吗?")
print(response.tool_calls)
这里的返回值仍然是 AIMessage。是否真的调用了工具,要看 response.tool_calls。不要因为绑定了工具,就假设每次请求都会执行工具。
模型选择工具大致经过这样的判断。
#mermaid-svg-dmm4ziYEanloqiBD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dmm4ziYEanloqiBD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dmm4ziYEanloqiBD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dmm4ziYEanloqiBD .error-icon{fill:#552222;}#mermaid-svg-dmm4ziYEanloqiBD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dmm4ziYEanloqiBD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dmm4ziYEanloqiBD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dmm4ziYEanloqiBD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dmm4ziYEanloqiBD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dmm4ziYEanloqiBD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dmm4ziYEanloqiBD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dmm4ziYEanloqiBD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dmm4ziYEanloqiBD .marker.cross{stroke:#333333;}#mermaid-svg-dmm4ziYEanloqiBD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dmm4ziYEanloqiBD p{margin:0;}#mermaid-svg-dmm4ziYEanloqiBD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dmm4ziYEanloqiBD .cluster-label text{fill:#333;}#mermaid-svg-dmm4ziYEanloqiBD .cluster-label span{color:#333;}#mermaid-svg-dmm4ziYEanloqiBD .cluster-label span p{background-color:transparent;}#mermaid-svg-dmm4ziYEanloqiBD .label text,#mermaid-svg-dmm4ziYEanloqiBD span{fill:#333;color:#333;}#mermaid-svg-dmm4ziYEanloqiBD .node rect,#mermaid-svg-dmm4ziYEanloqiBD .node circle,#mermaid-svg-dmm4ziYEanloqiBD .node ellipse,#mermaid-svg-dmm4ziYEanloqiBD .node polygon,#mermaid-svg-dmm4ziYEanloqiBD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dmm4ziYEanloqiBD .rough-node .label text,#mermaid-svg-dmm4ziYEanloqiBD .node .label text,#mermaid-svg-dmm4ziYEanloqiBD .image-shape .label,#mermaid-svg-dmm4ziYEanloqiBD .icon-shape .label{text-anchor:middle;}#mermaid-svg-dmm4ziYEanloqiBD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dmm4ziYEanloqiBD .rough-node .label,#mermaid-svg-dmm4ziYEanloqiBD .node .label,#mermaid-svg-dmm4ziYEanloqiBD .image-shape .label,#mermaid-svg-dmm4ziYEanloqiBD .icon-shape .label{text-align:center;}#mermaid-svg-dmm4ziYEanloqiBD .node.clickable{cursor:pointer;}#mermaid-svg-dmm4ziYEanloqiBD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dmm4ziYEanloqiBD .arrowheadPath{fill:#333333;}#mermaid-svg-dmm4ziYEanloqiBD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dmm4ziYEanloqiBD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dmm4ziYEanloqiBD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dmm4ziYEanloqiBD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dmm4ziYEanloqiBD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dmm4ziYEanloqiBD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dmm4ziYEanloqiBD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dmm4ziYEanloqiBD .cluster text{fill:#333;}#mermaid-svg-dmm4ziYEanloqiBD .cluster span{color:#333;}#mermaid-svg-dmm4ziYEanloqiBD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dmm4ziYEanloqiBD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dmm4ziYEanloqiBD rect.text{fill:none;stroke-width:0;}#mermaid-svg-dmm4ziYEanloqiBD .icon-shape,#mermaid-svg-dmm4ziYEanloqiBD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dmm4ziYEanloqiBD .icon-shape p,#mermaid-svg-dmm4ziYEanloqiBD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dmm4ziYEanloqiBD .icon-shape .label rect,#mermaid-svg-dmm4ziYEanloqiBD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dmm4ziYEanloqiBD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dmm4ziYEanloqiBD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dmm4ziYEanloqiBD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
用户问题
模型读取消息与工具 Schema
是否需要外部信息或动作
直接生成 AIMessage 文本
选择工具与参数
返回 tool_calls
运行时校验并执行
ToolMessage 结果
模型生成最终回答
多工具调用不是多了一行代码
当问题同时涉及天气和时区时,模型可能在同一个 AIMessage 中返回多个调用。应用需要遍历 response.tool_calls,逐个执行,并按原顺序把结果加入消息列表。
如果工具之间互不依赖,可以在运行时并发执行来降低延迟,但并发会带来速率限制、结果排序、取消和异常聚合等问题。初学阶段先使用串行循环,确认消息协议正确后再引入异步并发。
下面是多工具循环的核心形态。
python
response = model_with_tools.invoke(messages)
messages.append(response)
for call in response.tool_calls:
selected = tool_map[call["name"]]
messages.append(selected.invoke(call))
final_response = model_with_tools.invoke(messages)
上面仍是循环的核心片段,messages 应包含 HumanMessage,tool_map 应由已注册工具按名称建立。不要把它当成脱离上下文的完整脚本;完整的未知工具、异常转换和轮数限制已经包含在本文前面的最小示例中。
一轮工具调用结束后,如果最终 AIMessage 仍然包含 tool_calls,就继续循环。生产代码必须设置轮数、时间或预算上限,不能把「直到模型满意」当成可靠的停止条件。
用 tool_choice 约束模型行为
bind_tools 支持 tool_choice,常见取值如下。
| 取值 | 行为 | 适用情况 |
|---|---|---|
auto |
模型自行决定是否调用工具 | 通用问答,默认选择 |
none |
禁止调用工具 | 只允许知识解释或离线模式 |
required |
必须调用至少一个工具 | 必须经过查询或分类的流程 |
| 指定工具 | 强制调用某一个工具 | 已由业务路由确定工具 |
不同模型供应商对 required、并行调用和指定工具的支持细节可能不同,应该以对应集成包文档为准。
python
model = build_model()
# 只允许模型回答,不允许访问天气工具
text_only_model = model.bind_tools(tools, tool_choice="none")
# 强制至少调用一个工具,适合必须先经过外部查询的流程
query_model = model.bind_tools(tools, tool_choice="required")
# 具体格式由供应商决定,常见形式是指定函数名称
weather_only_model = model.bind_tools(
tools,
tool_choice={"type": "function", "function": {"name": "get_weather"}},
)
强制工具并不等于工具结果一定可信。工具函数仍然可能超时、返回空数据或被错误参数触发。tool_choice 只解决「要不要提出调用请求」,不能替代权限和业务校验。
常见错误,按消息协议排查
只调用一次模型就返回给用户
症状是输出为空,或者回答「我会帮你查询」却没有具体结果。
原因是代码拿到 AIMessage(tool_calls) 后直接结束,没有执行工具,也没有把 ToolMessage 发回模型。修复方法是实现完整循环,并在没有 tool_calls 时才把文本交给用户。
手工拼接 ToolMessage 时丢失 call id
每个 ToolMessage 都应该使用对应调用的 tool_call_id。如果 id 错位,部分供应商会拒绝请求,或者模型无法把结果和工具请求配对。
docstring 和类型注解不完整
没有参数类型时,Schema 缺少有用的类型约束。使用 parse_docstring=True 却没有按支持的 Google 风格写 Args,可能在创建工具时直接抛出解析异常。工具描述应说明数据范围、单位、何时使用和失败时的返回语义。
把模型的工具调用当成安全授权
模型说要调用 delete_order,不代表用户已经授权删除订单。高风险工具必须在执行前再次检查当前用户、资源归属、审批状态和幂等键。模型输出只能是意图,不是权限令牌。
工具返回复杂对象导致上下文膨胀
工具返回值最终会进入下一次模型上下文。不要把数据库整表、完整 HTML 或原始堆栈直接返回。优先返回稳定、精简、可解释的字符串或小型 JSON,并在服务端保留详细日志。
生产环境的工具边界
工具系统真正难的地方不在装饰器,而在执行边界。建议把一次调用拆成四层。
#mermaid-svg-Itys6EavzOl5bZeS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Itys6EavzOl5bZeS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Itys6EavzOl5bZeS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Itys6EavzOl5bZeS .error-icon{fill:#552222;}#mermaid-svg-Itys6EavzOl5bZeS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Itys6EavzOl5bZeS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Itys6EavzOl5bZeS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Itys6EavzOl5bZeS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Itys6EavzOl5bZeS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Itys6EavzOl5bZeS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Itys6EavzOl5bZeS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Itys6EavzOl5bZeS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Itys6EavzOl5bZeS .marker.cross{stroke:#333333;}#mermaid-svg-Itys6EavzOl5bZeS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Itys6EavzOl5bZeS p{margin:0;}#mermaid-svg-Itys6EavzOl5bZeS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Itys6EavzOl5bZeS .cluster-label text{fill:#333;}#mermaid-svg-Itys6EavzOl5bZeS .cluster-label span{color:#333;}#mermaid-svg-Itys6EavzOl5bZeS .cluster-label span p{background-color:transparent;}#mermaid-svg-Itys6EavzOl5bZeS .label text,#mermaid-svg-Itys6EavzOl5bZeS span{fill:#333;color:#333;}#mermaid-svg-Itys6EavzOl5bZeS .node rect,#mermaid-svg-Itys6EavzOl5bZeS .node circle,#mermaid-svg-Itys6EavzOl5bZeS .node ellipse,#mermaid-svg-Itys6EavzOl5bZeS .node polygon,#mermaid-svg-Itys6EavzOl5bZeS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Itys6EavzOl5bZeS .rough-node .label text,#mermaid-svg-Itys6EavzOl5bZeS .node .label text,#mermaid-svg-Itys6EavzOl5bZeS .image-shape .label,#mermaid-svg-Itys6EavzOl5bZeS .icon-shape .label{text-anchor:middle;}#mermaid-svg-Itys6EavzOl5bZeS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Itys6EavzOl5bZeS .rough-node .label,#mermaid-svg-Itys6EavzOl5bZeS .node .label,#mermaid-svg-Itys6EavzOl5bZeS .image-shape .label,#mermaid-svg-Itys6EavzOl5bZeS .icon-shape .label{text-align:center;}#mermaid-svg-Itys6EavzOl5bZeS .node.clickable{cursor:pointer;}#mermaid-svg-Itys6EavzOl5bZeS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Itys6EavzOl5bZeS .arrowheadPath{fill:#333333;}#mermaid-svg-Itys6EavzOl5bZeS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Itys6EavzOl5bZeS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Itys6EavzOl5bZeS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Itys6EavzOl5bZeS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Itys6EavzOl5bZeS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Itys6EavzOl5bZeS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Itys6EavzOl5bZeS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Itys6EavzOl5bZeS .cluster text{fill:#333;}#mermaid-svg-Itys6EavzOl5bZeS .cluster span{color:#333;}#mermaid-svg-Itys6EavzOl5bZeS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Itys6EavzOl5bZeS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Itys6EavzOl5bZeS rect.text{fill:none;stroke-width:0;}#mermaid-svg-Itys6EavzOl5bZeS .icon-shape,#mermaid-svg-Itys6EavzOl5bZeS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Itys6EavzOl5bZeS .icon-shape p,#mermaid-svg-Itys6EavzOl5bZeS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Itys6EavzOl5bZeS .icon-shape .label rect,#mermaid-svg-Itys6EavzOl5bZeS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Itys6EavzOl5bZeS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Itys6EavzOl5bZeS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Itys6EavzOl5bZeS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 模型提出意图
参数 Schema 校验
权限与业务规则
执行外部副作用
审计、指标、结果裁剪
参数 Schema 负责类型和范围,例如城市不能为空、金额必须大于零。权限层负责当前用户是否能访问订单或发送邮件。执行层负责超时、重试、连接池和事务。审计层记录调用名、耗时、结果状态和追踪 ID,但不记录密钥和未经脱敏的敏感数据。
涉及付款、删除、发信、改库或发布内容的工具,最好采用「模型提出请求,应用展示摘要,用户确认,服务执行」的两阶段模式。对于会重试的网络操作,工具必须设计幂等键,否则一次超时重试可能变成两次扣款。
同步工具适合纯计算或短小的内存操作。网络、数据库、文件和队列通常是 IO 密集型,应提供异步实现,并设置连接和整体超时。重试要区分可重试错误和业务拒绝,不能对参数错误无限重试。
调试时应该记录什么
至少记录以下字段。
- 请求追踪 ID 和会话 ID
- 模型名称、工具列表版本和提示词版本
AIMessage.tool_calls的工具名与脱敏参数- 工具耗时、状态码、重试次数和结果摘要
- 最终回答与用户反馈
LangSmith 等追踪平台可以把模型调用、工具调用和最终回答串在同一条 Trace 中。追踪解决的是「发生了什么」,并不自动解决「是否允许发生」。权限、审计和脱敏仍由应用负责。
一份发布前检查清单
工具是否只有一个清晰职责,名称和描述是否能让模型区分相似工具。参数是否有类型、范围、单位和必填规则。未知工具、缺少参数、超时和第三方错误是否都有明确结果。是否限制了每轮工具数量、最大循环轮数和总耗时。高风险副作用是否需要人工确认,重试是否具备幂等性。日志和 Trace 是否经过脱敏,是否能根据调用 ID 复现一次失败。
这些问题都回答清楚后,Tools 才不只是一个能在演示中跑通的装饰器,而是一组可以被测试、审计和演进的应用接口。
官方文档与进一步阅读
- LangChain Tools 官方文档
- LangChain 模型与 bind_tools 说明
- BaseChatModel.bind_tools API 参考
- OpenAI Function Calling 指南
本文示例基于 LangChain 1.x 的 API 习惯。具体模型是否支持并行工具调用、tool_choice 的完整取值和参数格式,请在锁定依赖版本后以供应商及 LangChain 集成包的官方文档为准。