在讨论 Tool Calling 之前,需要先明确一个容易被忽略的前提:LLM 本质上仍然是一个基于上下文预测下一个 token 的概率模型。它并不知道什么是文件系统,也不会真正执行代码;所谓“调用工具”,并不是模型突然获得了执行函数的能力,而是它能够根据上下文生成一段符合协议约束的结构化输出。
换句话说,Tool Calling 的核心不是“模型会执行函数”,而是“模型会生成一种应用程序能够识别和执行的控制语言”。真正读取文件、查询数据库、调用接口或者执行命令的,始终是模型外部的应用程序。
下文的接口示例主要以 OpenAI API 为准,因此会使用 tools、tool_calls、finish_reason="tool_calls"、role="tool"、tool_call_id 等字段名。其他模型服务商也可能支持类似的工具调用能力,但请求字段、响应结构、参数序列化方式和消息角色命名不一定完全相同。阅读这些示例时,可以先把重点放在“模型生成结构化调用请求,应用程序执行工具并回传结果”这个通用流程上;具体接入某个厂商时,再以该厂商的官方接口文档为准。
为什么 LLM 天生不能调用工具?
大语言模型的基本工作方式是:给定一段上下文,预测下一个最可能出现的 token,然后不断重复这个过程,直到生成完整输出。它可以描述“应该读取 config.json”,也可以生成一段看起来像函数调用的文本,但如果没有外部系统接管,这段文本本身不会产生任何真实效果。
Tool Calling 正是为了解决这个断点而出现的工程机制。它把用户意图、模型判断和应用程序执行连接起来,形成一个可控的闭环。完整流程通常可以概括为四步:开发者先告诉模型有哪些工具可用;模型根据用户请求生成工具调用请求;应用程序解析请求并执行真实函数;最后再把执行结果送回模型,让模型基于结果继续推理或生成最终回答。
Tool Calling 的本质:Schema 约束
很多人第一次接触 Tool Calling 时,会把它理解成“LLM 学会了调用函数”。这个说法容易造成误解,因为模型并没有进入你的运行时环境,也不会直接触发某个函数栈。它真正学会的是:在特定上下文中,生成符合 Schema 约束的 token 序列。
例如,一个读取文件的工具调用可能长这样:
{
"name": "read_file",
"arguments": {
"path": "config.json"
}
}
对于应用程序来说,这是一条可以解析和执行的指令;但对于模型来说,它仍然只是一段符合格式要求的文本。JSON Mode、Structured Output 和 Tool Calling 都建立在这个基础之上,只是约束目标不同:JSON Mode 要求输出合法 JSON,Structured Output 要求输出符合指定 Schema 的 JSON,而 Tool Calling 则要求模型输出符合 Tool Schema 的调用结构。
从这个角度看,Tool Calling 可以理解为 Structured Output 的一种特殊形式。区别在于,Tool Calling 的输出不会直接展示给用户,而是由应用程序接管,并转化为真实的外部操作。
第一步:告诉模型有哪些工具
在 OpenAI API 中,每个工具都需要用 JSON Schema 描述。这个描述并不是函数实现,而是工具的接口说明,包括工具叫什么、什么时候应该使用它、需要哪些参数,以及每个参数是什么类型。
下面是一个最简单的工具定义:
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取指定文件的内容",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "文件路径"
}
},
"required": [
"path"
]
}
}
}
这里最重要的字段通常是 description 和 parameters。name 负责提供稳定的工具标识,description 负责告诉模型这个工具适合解决什么问题,而 parameters 则用 JSON Schema 约束参数结构。模型会把用户请求和工具描述放在同一个上下文里理解,因此工具描述写得是否准确,往往会直接影响工具选择的效果。
实际发送请求时,你只需要把工具定义放进 tools 字段,并不需要把真实函数实现交给模型:
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-5.5",
messages=[
{
"role": "user",
"content": "帮我读取 config.json 文件"
}
],
tools=[
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取指定文件的内容",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "文件路径"
}
},
"required": ["path"]
}
}
},
{
"type": "function",
"function": {
"name": "list_dir",
"description": "列出目录下的所有文件",
"parameters": {
"type": "object",
"properties": {
"dir": {
"type": "string",
"description": "目录路径"
}
},
"required": ["dir"]
}
}
}
]
)
这一点非常关键:模型需要知道工具名字、用途和参数结构,但它不需要、也不能直接访问你的函数实现。真实代码仍然留在应用程序一侧,由应用程序决定是否执行、如何执行,以及如何处理异常和权限问题。
模型是如何“知道”工具的?
LLM 只能处理 token,那么它为什么能理解 JSON Schema?答案并不神秘:Tool Definition 最终也会被编码进模型上下文。对模型来说,tools、messages、system prompt、JSON Schema 都会成为上下文中的 token 序列。
模型在训练阶段见过大量 JSON、XML、OpenAPI、TypeScript 类型、函数签名和结构化文档,因此它具备理解结构化描述的能力。当你把工具定义传给模型时,本质上是在告诉它:“接下来如果用户意图符合这些描述,你可以按照这些 Schema 生成一段工具调用协议。”
第二步:模型决定是否调用工具
当用户发送请求后,模型会先理解用户意图,再将这个意图与可用工具的描述进行语义匹配。比如用户说:
帮我读取 config.json 文件
模型可能会推理出:用户想读取文件;当前上下文里存在一个 read_file 工具;这个工具需要 path 参数;从用户请求中可以抽取出 path = "config.json";因此应该生成一次 read_file 的工具调用。
需要注意的是,Tool Calling 并不意味着“模型不会回答”。很多时候,即使模型知道一个大概答案,它也应该调用工具,因为工具能提供更实时、更确定、更可信的外部信息。天气查询需要实时数据,计算器可以提供确定性计算,数据库查询依赖外部状态,文件系统操作则需要真实的系统权限。Tool Usage 的本质不是模型“不会”,而是任务需要模型之外的能力。
当模型决定调用工具时,它不会继续生成最终自然语言回答,而是返回一个带有 tool_calls 字段的 assistant message:
{
"choices": [
{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\":\"config.json\"}"
}
}
]
}
}
]
}
这里有几个字段需要重点关注。finish_reason="tool_calls" 表示模型决定进入工具调用流程;tool_calls 是本轮请求中模型生成的工具调用列表;function.name 是要调用的工具名;function.arguments 则是参数。
一个容易踩坑的地方是:arguments 通常是 JSON 字符串,而不是已经解析好的对象。也就是说,应用程序需要显式解析它:
import json
args = json.loads(tool_call.function.arguments)
解析之后,应用程序才能根据 name 找到对应函数,并把参数传进去执行。
第三步:应用程序执行工具
这是 Tool Calling 中最容易被误解的一环:LLM 不会执行任何代码。它只生成了一段结构化调用协议,真正负责执行协议的是你的应用程序。
应用程序通常需要完成几件事:检测响应中是否存在 tool_calls,解析每个工具调用的参数,根据工具名找到对应的本地函数或远程服务,执行真实操作,拿到结果后再把结果包装成 tool 消息发回模型。
整个循环可以用下面的流程图表示:
这个循环继续扩展下去,就已经非常接近 Agent Loop。所谓 Agent,并不是某个完全不同的新模型能力,而是让模型在“生成工具调用、接收工具结果、继续推理”之间反复迭代。
第四步:把工具结果送回模型
假设第一次请求中,模型生成了一个 read_file 工具调用:
response1 = client.chat.completions.create(
model="gpt-5.5",
messages=[
{
"role": "user",
"content": "帮我读取 config.json"
},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": tool_call.id,
"type": "function",
"function": {
"name": "read_file",
"arguments": tool_call.function.arguments
}
}
]
},
],
tools=tools
)
应用程序取出工具调用,解析参数,并执行真实函数:
tool_call = response1.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
result = read_file(args["path"])
然后,应用程序需要把模型刚才发出的 tool call 以及工具执行结果一起放回对话历史,再发起第二次请求:
response2 = client.chat.completions.create(
model="gpt-5.5",
messages=[
{
"role": "user",
"content": "帮我读取 config.json"
},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": tool_call.id,
"type": "function",
"function": {
"name": "read_file",
"arguments": tool_call.function.arguments
}
}
]
},
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
}
],
tools=tools
)
这里的 role="tool" 用来明确告诉模型:这段内容不是用户新输入,也不是模型自己生成的回答,而是某个工具调用的执行结果。tool_call_id 则负责把结果和前面的工具调用关联起来。这个字段在一次响应包含多个工具调用时尤其重要,否则模型无法可靠区分哪个结果对应哪个调用。
第二轮请求中通常仍然要传入 tools,因为模型看到工具结果后,可能还需要继续调用别的工具。例如它可能先调用 list_dir() 查看目录,再调用 read_file() 读取文件,最后调用另一个工具做摘要或格式转换。
并行工具调用:一次生成多个工具调用
现代模型支持 Parallel Tool Calls,也就是一次响应中生成多个工具调用。例如:
{
"tool_calls": [
{
"function": {
"name": "list_dir"
}
},
{
"function": {
"name": "read_file"
}
}
]
}
这并不要求你的应用一定要并行执行它们。应用程序可以根据工具之间是否存在依赖关系,选择串行执行或并行执行,再把所有结果统一送回模型。如果多个工具调用之间互不依赖,并行执行可以降低延迟;如果后一个调用依赖前一个调用的结果,则应该串行处理,或者让模型在下一轮再生成新的工具调用。
工具选择:控制模型如何使用工具
除了提供工具列表,开发者还可以通过 tool_choice 控制模型是否允许调用工具,以及是否必须调用某个特定工具。
当你希望模型自行决定是否使用工具时,可以使用:
tool_choice="auto"
如果当前场景只允许模型直接回答,不允许使用工具,可以设置:
tool_choice="none"
如果你希望强制模型调用某个工具,可以指定具体工具:
tool_choice={
"type": "function",
"function": {
"name": "read_file"
}
}
这些控制选项在产品中非常实用。比如搜索场景可以默认允许模型自行决定是否查外部数据,而表单提交、权限敏感操作或某些确定性流程,则更适合由应用程序明确约束模型的工具使用方式。
Agent 的本质是什么?
很多人会把 Agent 理解成一种神秘的新能力,但从工程实现上看,Agent 通常可以拆解为四个部分:LLM、Tool Calling、Memory 和 Loop。
一个典型 Agent 会不断经历这样的循环:
理解当前任务
↓
决定是否调用工具
↓
接收工具结果
↓
基于新信息继续推理
↓
必要时继续调用工具
因此,Agent 并不是模型突然拥有了“行动能力”,而是应用程序把 Tool Calling 包装成了一个可持续运行的循环。模型负责在每一轮中判断下一步该做什么,应用程序负责执行真实动作、维护状态、处理错误,并决定循环何时停止。
MCP 与 Tool Calling 的关系
近来很多人讨论 MCP,也就是 Model Context Protocol。MCP 并不是另一种全新的 Tool Calling 能力,而更像是 Tool Calling 生态的标准化协议层。
在传统 Tool Calling 中,每个应用都需要自己定义工具、维护 Schema、实现执行逻辑,并决定如何暴露能力给模型。MCP 试图把这些环节标准化,例如工具发现、能力描述、调用协议和上下文传递。这样一来,不同应用和工具服务之间就更容易互相接入。
可以简单理解为:Tool Calling 是模型使用工具的机制,而 MCP 是围绕工具生态建立的一套标准协议。前者回答“模型如何表达要调用工具”,后者更关注“工具如何被发现、描述、连接和调用”。
常见问题
LLM 真正在执行代码吗?
不是。LLM 只是在生成一段符合协议格式的结构化文本,真正执行代码的是应用程序。这个边界非常重要,因为权限控制、参数校验、错误处理和审计日志都应该发生在应用程序侧。
为什么需要 tool_call_id?
因为一次模型回复可能包含多个 tool call。tool_call_id 用来把每个 Tool Result 和对应的 Tool Call 关联起来,避免模型把不同工具的返回结果混在一起。
为什么 arguments 是字符串?
OpenAI API 会将 arguments 序列化为 JSON string,因此应用程序需要手动执行 json.loads(arguments)。解析之后,仍然应该做运行时校验,因为 Schema 约束可以提高生成质量,但不能替代真实系统中的安全检查。
LLM 会不会乱调用工具?
会。模型可能调用不存在的工具,生成非法参数,传入错误类型,或者遗漏 required field。因此,Tool Schema 不等于运行时安全。应用程序仍然需要做参数校验、权限控制、错误处理、超时控制和沙箱隔离,尤其是在工具会访问文件系统、数据库、网络或生产环境资源时。
完整流程图
总结
Tool Calling 的职责边界可以概括为:
| 阶段 | 谁负责 | 做什么 |
|---|---|---|
| Tool Definition | 开发者 | 描述工具 Schema |
| Tool Selection | LLM | 选择是否使用工具 |
| Tool Invocation | LLM | 生成 tool_calls |
| Tool Execution | 应用程序 | 执行真实代码 |
| Tool Result | 应用程序 | 返回执行结果 |
| Final Response | LLM | 基于结果生成回答 |
最终结论是:Tool Calling 并不是 LLM 在调用函数,而是 LLM 在生成一种符合协议约束的控制语言。应用程序读取这段协议,执行真实世界中的操作,拿到结果后再反馈给模型。
所以,在 Tool Calling 架构里,LLM 负责“说出下一步要做什么”,应用程序负责“把这一步真正做出来”。理解这个边界,才能正确设计工具 Schema、运行时执行逻辑和安全控制。