03 Tool Use

Tool Use

LLM 是如何长出手脚的。

本章聚焦  →  工具——模型伸向世界的手 · tool call 原理与标准协议 MCP

在上一章中我们把 LLM 的嘴给装好了,它能说话、能思考、能记得几轮的对话。但它现在只能停留在自己的训练数据里。你问他“今天 AI 圈发生了什么热点新闻”,它要么编造,要么坦白“我不知道”。所以单纯的 LLM 在其训练完成之后就与环境隔离开了。而 Agent 相较于 LLM 的最大区别,在于它能够与环境之间产生实时的动态交互,这种交互是通过工具调用(tool call)实现的。

类比 · 工具 = 手脚

训练完成之后,LLM 就与环境隔离开了——它知识渊博,却看不见此刻的网页,碰不到你的文件,也改不了任何东西。

能感知web_search 像一双眼睛,把训练语料之外的世界读进来
能改造write_file 像一只手,把它的决定落回到环境上

大脑(LLM)负责想,手脚(工具)负责够得着世界。tool call,就是从想到做的那一步。

将工具能力引入到 LLM 中的意义是跨时代的:

在 LLM 发展早期,学者通过 Prompt 注入的方式教导大模型产生调用工具的行为:他们在 system prompt 中写工具的调用方法和行为规范,例如使用 <tool_call>{...}</tool_call> 的方式发起工具调用请求,然后在服务端把大模型输出中的 toolcall 字段抽取出来并直接执行。这就是大模型调用工具的雏形。在初期的时候这种方式所面临的问题在于:

后来随着大模型的能力边界不断扩展,学者们在预训练阶段就直接向大模型注入工具调用的能力,并将这种能力进行了大规模拓展,例如调用海量工具、单词请求中调用多次工具、层级调用工具等等。

3.1 基本工具调用原理

工具执行的原理如下,首先工具的调用已经被基础组件进行了统一封装:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")

# 对话历史
messages = [
    {"role": "system", "content": "你是一个计算助手。你可以调用外部工具。"},
    {"role": "user", "content": "250的15%是多少?"}
]

# 工具定义 
tools = [
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "计算数学表达式",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string"}
                },
                "required": ["expression"]
            }
        }
    }
]

# Tokenizer 完成template转换
input = tokenizer.apply_chat_template(
    messages,
    tools=tools,
)

从 message 序列的角度来看工具执行的完整链路如下:

完整 message 链路 JSON(含 tools 定义)
{
  "model": "gpt-4o",
  "messages": [
    {
      "role": "system",
      "content": "你是一个计算助手。你可以调用外部工具。"
    },
    {
      "role": "user",
      "content": "250的15%是多少?"
    },
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "call_abc123xyz",
          "type": "function",
          "function": {
            "name": "calculator",
            "arguments": "{\"expression\": \"250 * 0.15\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_abc123xyz",
      "name": "calculator",
      "content": "37.5"
    },
    {
      "role": "assistant",
      "content": "250的15%是37.5。"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "calculator",
        "description": "计算数学表达式",
        "parameters": {
          "type": "object",
          "properties": {
            "expression": {
              "type": "string"
            }
          },
          "required": [
            "expression"
          ]
        }
      }
    }
  ]
}

从 token 序列的角度来看工具调用的过程会更加准确:

<|im_start|>system:你是一个计算助手。你可以调用外部工具。\n\n# Tools\nYou may call one or more functions to assist with the user query.\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>[{"type": "function","function": {"name": "calculator","description": "计算数学表达式","parameters": {"type": "object","properties": {"expression": {"type": "string"}},"required": ["expression"]}}}]</tools>\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags.<|im_end|><|im_start|>user:250的15%是多少?<|im_end|><|im_start|>assistant:<tool_call>{"name": "calculator", "arguments": {"expression": "250 * 0.15"}}</tool_call><|im_end|><|im_start|>tool<tool_response>{"name": "calculator", "content": "37.5"}</tool_response><|im_end|><|im_start|>assistant:250的15%是37.5。<|im_end|>

我们以可读性更高的形式展开一下:

可读性更高的 token 序列(展开)
<|im_start|>system
你是一个计算助手。你可以调用外部工具。

# Tools
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:
<tools>
[
  {
    "type": "function",
    "function": {
      "name": "calculator",
      "description": "计算数学表达式",
      "parameters": {
        "type": "object",
        "properties": {
          "expression": {"type": "string"}
        },
        "required": ["expression"]
      }
    }
  }
]
</tools>
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags.<|im_end|>
<|im_start|>user
250的15%是多少?<|im_end|>
<|im_start|>assistant
<tool_call>
{"name": "calculator", "arguments": {"expression": "250 * 0.15"}}
</tool_call><|im_end|>
<|im_start|>tool
<tool_response>
{"name": "calculator", "content": "37.5"}
</tool_response><|im_end|>
<|im_start|>assistant
250的15%是37.5。<|im_end|>

需要注意的是,一次 tool call 会产生两条 message:一条是 llm 生成的,role 为 assistant,这一条表明大模型决定在当前上下文的基础上它应该调用哪个工具,工具的参数是什么。系统在捕获到这个工具调用意图后,会按照同样的工具名以及参数来执行该工具,并将结果拼装成一条 tool 消息,role 为 tool。大模型在接收到这两条消息后,再做出下一轮的响应。我们可以把这个流程绘制成一张如下的时序图:

图 1 · 一次 tool call 的完整时序(含参数校验分支)
User LLM ToolRegistry Toolweb_search / calc / … ① 问题 + tools=[…] 定义 自回归解码 预测出 tool_call token ② tool_call(name, args) ③ validate_params (JSON Schema) alt [参数合法] ④ execute(**args) ⑤ result ⑥ tool_result 追加到 messages [参数非法] error + hint 模型据 hint 重新决策 继续推理,基于 tool_result ⑦ 最终回复
模型只“说”了两次(②的 tool_call 与 ⑦的最终回复),中间全由 ToolRegistry 代劳:先 ③ 用 JSON Schema 校验参数,再分两支——参数合法则 ④ 调 Tool 执行、⑤ 拿回 result、⑥ 以 tool 消息追加进上下文;参数非法则直接回 error + hint,让模型据此重新决策。这两条特殊 message(assistant 的调用意图 + tool 的执行结果)一并读进上下文,才有了最终回复。

小节一下:在 Agent 中工具的本质是一个函数,它有一个标准四件套:

字段作用
name全局唯一标识,给 LLM 在 tool_call 里引用
description自然语言说明,告诉 LLM 这工具何时该用、何时不该用
parametersJSON Schema,告诉 LLM 参数怎么填、有什么约束
execute()实际执行函数,接收 dict 参数返回字符串结果

同时,我也给出一些工具描述的协作守则作为参考:

现场演示 · 纯工具调用(提交「支持Tool调用」)

这是引入 MCP 之前的版本——工具直接写在项目代码里。问它一个需要联网的问题,观察模型如何决定调用工具、把结果读回来再作答。此前端固定指向「支持Tool调用」提交,独立于本章其余演示的后端。

对应 tool use 分支
  • 工具基类与注册——工具如何定义并登记到可调用清单
  • web search 工具——一个具体工具的实现样例
  • 工具的执行逻辑——模型发起调用后如何被执行、结果如何回流

3.2 MCP

我们的工具都写在自己项目的代码里,别人造的工具用不上。如果我想用别人写的工具,我们的工具编写规范可能不一样,如你用“name”,我用“tool_name”来表征工具名称,都合法,但不互通。同时,工具代码可能是个人或者公司的核心资产,其业务逻辑不能对外暴露。在这种情况我如果仍然期望将能力向别人进行暴露,但又要考虑到兼容性以及资产安全,我应当怎么办呢?这就是 MCP(Model Context Protocol) 要解决的问题。

类比 · MCP = USB 标准接口

你不会为每一台设备重造一个插孔——USB 定了一套统一接口,任何厂商的设备插上就能用,而没人需要知道设备内部的电路怎么走。MCP 对工具做的是同一件事。

统一接口任何厂商的工具都能对接,解决“各写各的、互不相通”
只暴露接口不暴露内部实现,护住工具方的核心资产

于是兼容性资产安全同时被照顾到了。

MCP 的核心仍然是工具,但它在此基础上构建了一套完整的协议,包含工具的定义、发现、调用、通信等等方面。事实上,现在 ClaudeCode 已经不支持通过自定义脚本或者插件的方式扩展其 Agent 工具集,你想要在 cc 中扩展一个新工具(例如获取食堂每日菜单),只能通过 MCP 的方式向 cc 接入新工具。

以下以 MCP 调用核心时序来解析其工作原理

MCP 在底层采用了一个 Server(Tool)-Client(Agent) 架构,维护了与工具执行层的通信连接,将工具调用指令从 Agent 端发送给 Tool 端,然后将 Tool 端执行的结果反馈给 Agent。

详解 MCP 协议三阶段

每次 client 与 server 建立连接后,会经过三个阶段:

图 2 · MCP 调用核心时序(两阶段 · 13 步)
阶段一 · 初始化与能力发现 阶段二 · 工具调用与执行 用户 (User) 大模型 (LLM) MCP Client宿主 Agent MCP Server外部技能服务 ① 发起连接并发送 initialize 请求 ② 返回 Server Capabilities(协议版本 / 能力类型) ③ 发送 notifications/initialized(握手完成) ④ 发送 tools/list 请求 ⑤ 返回可用 Tools 列表(含 JSON Schema 定义) Client 在内存中缓存这些 Schema, 等待后续按需组装给 LLM ⑥ 提问:《公平竞争审查条例》违约金上限? 拦截请求,将上一步获取的 Tools Schema 动态注入 System Prompt ⑦ 发送组装好的 Context(Prompt + Tools Schema) ⑧ 推理后返回 Tool Call 指令 (如 call: search_regulation) Client 解析出目标工具, 并将其路由到对应的 MCP Server ⑨ 发送 tools/call 请求(携带大模型生成的入参) 执行本地业务逻辑 (如查询 PostgreSQL / Vector DB) ⑩ 返回标准化的 Tool Result 数据 将外部执行结果组装为 role: "tool" 的消息体 ⑪ 将带有结果的 Context 再次发给 LLM ⑫ 结合外部知识,生成最终的自然语言回答 ⑬ 返回最终审查结论
阶段一(初始化与能力发现):Client 与 Server 通过 initialize 握手、tools/list 发现能力,并把 JSON Schema 缓存在内存里。阶段二(工具调用与执行):用户提问后,Client 把缓存的 Schema 动态注入 System Prompt 交给 LLM;LLM 返回 Tool Call,Client 路由到对应 MCP Server 执行本地业务逻辑,再把结果组装成 role: "tool" 消息回喂 LLM,最终生成自然语言回答返还用户。全程走 JSON-RPC 2.0。

整个会话走 JSON-RPC 2.0 协议(UTF-8 编码),不分传输方式。而从通信机制上来看,可以分为两种传输方式:stdio 与 Streamable HTTP,区别如下:

方式stdioStreamable HTTP
启动方式Client fork 一个子进程作为 ServerServer 是独立长进程
通信子进程的 stdin / stdout 互发 JSON-RPCHTTP POST + 可选 SSE 流
适用场景本机工具(CDP-mcp、filesystem-mcp)远程工具、跨主机协作、SaaS MCP
安全进程级隔离网络级隔离 + 标准 OAuth 鉴权
缺点进程开销、消息不能含换行需要管理端口、TLS 与认证
图 3 · 两种传输:stdio vs Streamable HTTP
stdio · 本机 同一台机器 · 进程级隔离 Client (Agent) 父进程 Server fork 出的子进程 fork stdin/ stdout Streamable HTTP · 跨主机 独立长进程 · 网络级隔离 + OAuth 网络 Client (Agent) 本地 Server 独立长进程 HTTP POST SSE 流(可选)
同一套 JSON-RPC,两种搬运方式:stdio 把 Server 当自己 fork 出来的子进程,靠 stdin/stdout 递消息(消息因此不能含换行);Streamable HTTP 让 Server 作独立长进程,靠 HTTP POST 发起、SSE 回流,可跨主机并接标准 OAuth 鉴权。

对于 LLM 而言它是不感知 MCP 的存在的,MCP 在整个 Agent 体系中的意义更像是一个标准化的胶水层来串联 Agent 和 Tool,它把远程工具包装成本地工具放到 Agent 的上下文提示中。

除此以外,MCP 还有其它价值:

现场演示 · mybot 前端

启动 demo/serve.sh 后,在此让它调用工具,观察 tool_call 的双消息往返。

MCP 演示看点
  • MCP 实现机制——对照本节时序图,看 initialize → list_tools → call_tool 的往返
  • 操作浏览器——接入本地 chrome MCP,让 Agent 直接驱动浏览器

3.3 小结与思考

本章小结
  • 工具 = 手脚——LLM 训练完就与环境隔离:工具让它能感知(web_search 是眼睛)、能改造(write_file 是手)。
  • 调用的本质——模型按约定格式“说”出要调哪个工具、传什么参数,由外部执行后把结果读回序列,再据此作答。
  • 好工具怎么写——动词开头、参数强 typing、写清边界与何时不该用、参数尽量少且每个都有 description。
  • MCP = USB 标准接口——统一协议让任何厂商的工具即插即用,只暴露接口、不暴露实现,兼顾兼容性与资产安全。
  • MCP 三步握手——initialize → list_tools → call_tool;其价值在标准化、解耦与环境隔离带来的安全。
思考题
  • tool_call tokens 模式崩坏会导致什么问题,应当如何处理
  • 了解一下 tool call 限制性解码,它的原理和作用分别是什么
  • MCP 机制的缺点是什么?为什么主流应用对于 MCP 的热情开始冷却
源码浏览器
选择左侧文件查看源码