Skip to content
BaiRuic
Go back

LLM 是如何调用工具的

Updated:
目录

在讨论 Tool Calling 之前,需要先明确一个容易被忽略的前提:LLM 本质上仍然是一个基于上下文预测下一个 token 的概率模型。它并不知道什么是文件系统,也不会真正执行代码;所谓“调用工具”,并不是模型突然获得了执行函数的能力,而是它能够根据上下文生成一段符合协议约束的结构化输出。

换句话说,Tool Calling 的核心不是“模型会执行函数”,而是“模型会生成一种应用程序能够识别和执行的控制语言”。真正读取文件、查询数据库、调用接口或者执行命令的,始终是模型外部的应用程序。

下文的接口示例主要以 OpenAI API 为准,因此会使用 toolstool_callsfinish_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"
      ]
    }
  }
}

这里最重要的字段通常是 descriptionparametersname 负责提供稳定的工具标识,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 最终也会被编码进模型上下文。对模型来说,toolsmessages、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 SelectionLLM选择是否使用工具
Tool InvocationLLM生成 tool_calls
Tool Execution应用程序执行真实代码
Tool Result应用程序返回执行结果
Final ResponseLLM基于结果生成回答

最终结论是:Tool Calling 并不是 LLM 在调用函数,而是 LLM 在生成一种符合协议约束的控制语言。应用程序读取这段协议,执行真实世界中的操作,拿到结果后再反馈给模型。

所以,在 Tool Calling 架构里,LLM 负责“说出下一步要做什么”,应用程序负责“把这一步真正做出来”。理解这个边界,才能正确设计工具 Schema、运行时执行逻辑和安全控制。