在上一章中我们把 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。大模型在接收到这两条消息后,再做出下一轮的响应。我们可以把这个流程绘制成一张如下的时序图:
tool 消息追加进上下文;参数非法则直接回 error + hint,让模型据此重新决策。这两条特殊 message(assistant 的调用意图 + tool 的执行结果)一并读进上下文,才有了最终回复。小节一下:在 Agent 中工具的本质是一个函数,它有一个标准四件套:
| 字段 | 作用 |
|---|---|
| name | 全局唯一标识,给 LLM 在 tool_call 里引用 |
| description | 自然语言说明,告诉 LLM 这工具何时该用、何时不该用 |
| parameters | JSON Schema,告诉 LLM 参数怎么填、有什么约束 |
| execute() | 实际执行函数,接收 dict 参数返回字符串结果 |
同时,我也给出一些工具描述的协作守则作为参考:
- 以动词开头:Fetches、Calculates、Sends、Searches
- 写清何时不该用:Use only for public web. DO NOT use for internal docs.
- 参数要强 typing:用 enum、minimum/maximum 约束有限集合
- 每个参数都写 description:不要假设 LLM 看名字就懂
- 顶层参数尽量少:参数越多 LLM 越容易漏填
- 描述不要超过 1024 字符:太长稀释信号
这是引入 MCP 之前的版本——工具直接写在项目代码里。问它一个需要联网的问题,观察模型如何决定调用工具、把结果读回来再作答。此前端固定指向「支持Tool调用」提交,独立于本章其余演示的后端。
- 工具基类与注册——工具如何定义并登记到可调用清单
- web search 工具——一个具体工具的实现样例
- 工具的执行逻辑——模型发起调用后如何被执行、结果如何回流
3.2 MCP
我们的工具都写在自己项目的代码里,别人造的工具用不上。如果我想用别人写的工具,我们的工具编写规范可能不一样,如你用“name”,我用“tool_name”来表征工具名称,都合法,但不互通。同时,工具代码可能是个人或者公司的核心资产,其业务逻辑不能对外暴露。在这种情况我如果仍然期望将能力向别人进行暴露,但又要考虑到兼容性以及资产安全,我应当怎么办呢?这就是 MCP(Model Context Protocol) 要解决的问题。
你不会为每一台设备重造一个插孔——USB 定了一套统一接口,任何厂商的设备插上就能用,而没人需要知道设备内部的电路怎么走。MCP 对工具做的是同一件事。
于是兼容性与资产安全同时被照顾到了。
MCP 的核心仍然是工具,但它在此基础上构建了一套完整的协议,包含工具的定义、发现、调用、通信等等方面。事实上,现在 ClaudeCode 已经不支持通过自定义脚本或者插件的方式扩展其 Agent 工具集,你想要在 cc 中扩展一个新工具(例如获取食堂每日菜单),只能通过 MCP 的方式向 cc 接入新工具。
以下以 MCP 调用核心时序来解析其工作原理
MCP 在底层采用了一个 Server(Tool)-Client(Agent) 架构,维护了与工具执行层的通信连接,将工具调用指令从 Agent 端发送给 Tool 端,然后将 Tool 端执行的结果反馈给 Agent。
详解 MCP 协议三阶段
每次 client 与 server 建立连接后,会经过三个阶段:
- initialize
- Client 发 initialize 请求,含 protocolVersion、capabilities、clientInfo
- Server 回响应,声明自己支持的能力(tools / resources / prompts)
- list_tools(或 list_resources / list_prompts)
- Client 询问“你有哪些工具?”
- Server 回工具清单——每条含 name / description / inputSchema,与本地 Tool 完全同构
- call_tool
- 每次 LLM 想调远程工具时,Client 转发 call_tool 给 Server
- 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,区别如下:
| 方式 | stdio | Streamable HTTP |
|---|---|---|
| 启动方式 | Client fork 一个子进程作为 Server | Server 是独立长进程 |
| 通信 | 子进程的 stdin / stdout 互发 JSON-RPC | HTTP POST + 可选 SSE 流 |
| 适用场景 | 本机工具(CDP-mcp、filesystem-mcp) | 远程工具、跨主机协作、SaaS MCP |
| 安全 | 进程级隔离 | 网络级隔离 + 标准 OAuth 鉴权 |
| 缺点 | 进程开销、消息不能含换行 | 需要管理端口、TLS 与认证 |
对于 LLM 而言它是不感知 MCP 的存在的,MCP 在整个 Agent 体系中的意义更像是一个标准化的胶水层来串联 Agent 和 Tool,它把远程工具包装成本地工具放到 Agent 的上下文提示中。
除此以外,MCP 还有其它价值:
- 标准化——标准化能够极大促进生态能力发展。
- 解耦——Agent Runtime 与 Tool Excute 解耦,仅通过协议进行通信,系统不感知 Tool 实现逻辑。
- 安全——解耦带来的环境可隔离,Tool 挂掉或者环境破坏并不影响整体系统安全
启动 demo/serve.sh 后,在此让它调用工具,观察 tool_call 的双消息往返。
- 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 的热情开始冷却