第4章:工具调用——Agent 怎么「做事」
本章解决的核心问题:LLM 只能生成文字,它怎么能「调 API」「搜网页」「写文件」?Function Calling 到底是什么?MCP 又是什么?Agent 乱调工具怎么办?
读完这一章,你会理解 Agent 工具调用的每一个技术细节——从 JSON schema 到执行沙箱,从 MCP 协议到安全策略。
4.1 一个核心认知:LLM 从来不是「执行者」

🎨 漫画图解:〈服务员不亲自炒菜〉
先把一个最容易被误解的点说清楚:
LLM 不会执行任何东西。它只会生成文字。 当你看到 Agent 「搜索了网页」「发了邮件」「写了个文件」——真正执行这些操作的不是 LLM,而是 LLM 之外的宿主程序。
LLM 的角色是决策者:它告诉你「我想调用搜索工具,关键词是『新能源汽车销量 2025』」。然后宿主程序收到这个指令,真的去调了搜索 API,把结果拿回来,告诉 LLM「你搜到了这些内容」。然后 LLM 再决定下一步。
sequenceDiagram
participant User as 👤 用户
participant Host as 🖥️ 宿主程序
participant LLM as 🧠 LLM
participant Tool as 🔧 外部工具
User->>Host: 「帮我查天气」
Host->>LLM: Prompt: 用户要查天气,你可以用 get_weather 工具
LLM->>Host: 我想调用 get_weather(city="北京")
(这是一个 JSON 格式的指令)
Host->>Tool: 真的去调天气 API
Tool-->>Host: 返回:北京 晴 26°C
Host->>LLM: 工具返回结果:北京 晴 26°C
请继续回答用户
LLM->>Host: 「北京今天晴天,气温 26°C。」
Host->>User: 「北京今天晴天,气温 26°C。」LLM 说的不是「我来查天气」,而是「请帮我查天气」。 前者是执行,后者是决策。这个区别至关重要。
4.2 Function Calling 的完整流程

🎨 漫画图解:〈一张工具订单的旅行〉
Function Calling(函数调用)是目前最主流的 Agent 工具调用方式。OpenAI 在 2023 年 6 月首次引入,现在几乎所有大模型都支持。
第一步:定义工具
开发者需要提前告诉 LLM:「你可以用哪些工具,每个工具叫什么、干什么、需要什么参数。」
一个工具的 JSON Schema 定义长这样:
{
"name": "search_web",
"description": "在互联网上搜索信息。当你需要获取实时信息或验证事实时使用。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词"
},
"num_results": {
"type": "integer",
"description": "返回结果数量,默认 5,最大 10",
"default": 5
}
},
"required": ["query"]
}
}
这个 JSON 告诉 LLM 三件事:
- 何时用:需要实时信息或验证事实时。
- 怎么用:函数叫
search_web,参数是query(必填)和num_results(选填)。 - 参数格式:
query是字符串,num_results是整数。
第二步:LLM 决定调用
当用户的请求需要工具时,LLM 不会直接回答——它会输出一个工具调用指令:
{
"tool_calls": [
{
"function": {
"name": "search_web",
"arguments": "{\"query\": \"2025年新能源汽车销量排行榜\"}"
}
}
]
}
注意:LLM 的输出依然是文本——只是格式是结构化的 JSON。宿主程序解析这个 JSON,提取函数名和参数。
第三步:宿主程序执行
宿主程序收到 search_web 的调用请求后,实际去执行搜索结果——调 Google API、Bing API 或 Tavily。
第四步:结果返回给 LLM
宿主程序把搜索结果格式化,作为一条新的消息追加到对话中:
工具 search_web 返回结果:
1. 2025年1-6月新能源汽车销量:比亚迪160万辆、特斯拉...
2. 中汽协:2025上半年新能源渗透率达45%...
3. ...
第五步:LLM 基于结果继续
LLM 看到工具返回的结果,基于这些信息生成最终回答——也可能决定「信息不够,再搜一次」或「换个工具」。
flowchart TB
A[👤 用户输入] --> B{🧠 LLM 判断}
B -->|「我能直接回答」| C[💬 直接输出文本]
B -->|「我需要用工具」| D[📤 输出工具调用 JSON]
D --> E[🖥️ 宿主程序执行工具]
E --> F[📥 工具返回结果]
F --> B
C --> G[✅ 最终回答]4.3 工具描述怎么写:一个好的 Tool Definition

🎨 漫画图解:〈工具说明书决定成败〉
工具描述的质量直接影响 Agent 的表现。一个写得很烂的工具定义,LLM 要么不会用,要么用错。
烂的工具定义
{
"name": "do_stuff",
"description": "做事情",
"parameters": {
"properties": {
"x": {"type": "string"}
}
}
}
LLM 根本不知道这个工具是干什么的、什么时候该用、参数 x 应该填什么。
好的工具定义
{
"name": "create_obsidian_note",
"description": "在 Obsidian vault 中创建一篇新的 Markdown 笔记。使用场景:用户要求「记下来」「创建笔记」「新建一篇关于XX的文档」。注意:如果目标路径已存在同名文件,需要询问用户是否覆盖。",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "笔记文件的相对路径,例如 'notes/会议记录/2025-01-15 周会.md'。必须以 .md 结尾。"
},
"content": {
"type": "string",
"description": "笔记的完整 Markdown 内容。建议包含 YAML frontmatter。"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "可选的标签列表,会写入 frontmatter 的 tags 字段。例如 ['meeting', 'weekly']"
}
},
"required": ["file_path", "content"]
}
}
一个好工具定义的三个要素:
- 清晰的使用场景:告诉 LLM「什么时候该用这个工具,什么时候不该用」。
- 精确的参数说明:每个参数的意义、格式、约束、示例。
- 边界条件提示:工具有什么限制?什么情况下会失败?
4.4 MCP:Agent 世界的 USB-C

🎨 漫画图解:〈MCP 万能工具集市〉
2024 年 11 月,Anthropic 开源了 MCP(Model Context Protocol)——一个 Agent 与外部工具和数据源通信的开放标准。
为什么需要 MCP?
在 MCP 出现之前,Agent 工具调用的生态是一团乱麻:
flowchart TB
subgraph 没有 MCP 的时代
Agent1[Claude] -->|专属适配| Gmail1[Gmail 插件]
Agent1 -->|专属适配| GitHub1[GitHub 插件]
Agent1 -.->|不支持| DB1[数据库插件]
Agent2[ChatGPT] -->|专属适配| Gmail2[Gmail 插件]
Agent2 -.->|不支持| GitHub2[GitHub 插件]
Agent2 -->|专属适配| DB2[数据库插件]
end每个工具需要为每个 Agent 单独开发适配器。N 个 Agent × M 个工具 = N × M 个适配器。碎片化到令人绝望。
MCP 的方案
MCP 定义了一个标准协议——Agent 只需要实现 MCP 客户端,工具只需要实现 MCP 服务端。一个工具开发一次,任何支持 MCP 的 Agent 都能用。
flowchart TB
subgraph 有 MCP 的时代
Agent1[Claude] --> MCP[MCP 协议]
Agent2[ChatGPT] --> MCP
Agent3[开源 Agent] --> MCP
MCP --> Tool1[Gmail]
MCP --> Tool2[GitHub]
MCP --> Tool3[数据库]
MCP --> Tool4[文件系统]
MCP --> Tool5[任意工具...]
endMCP 的角色就是 Agent 世界的 USB-C——一个统一的接口标准。任何实现了 MCP 的工具(无论是 Google Drive、PostgreSQL 还是你的 Obsidian vault),都能被任何支持 MCP 的 Agent 调用。
MCP 的架构
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ MCP 客户端 │ ◄─────► │ MCP 协议 │ ◄─────► │ MCP 服务端 │
│ (Agent 侧) │ JSON-RPC │ (传输层) │ JSON-RPC │ (工具/数据) │
└─────────────┘ └─────────────┘ └─────────────┘
- MCP 客户端:嵌入在 Agent 中,负责发现、连接和调用 MCP 服务端。
- MCP 服务端:包装了具体的工具或数据源,暴露标准化的接口。
- 通信协议:基于 JSON-RPC 2.0,可以走标准输入输出(stdio)或 HTTP。
为什么你应该关心 MCP
因为在第7章你做 Obsidian Agent 时,你可能想让 Agent 不只搜索笔记,还能读邮件、查日历、更新数据库——用了 MCP,这些能力就变成「USB 式」的了。一个工具写好一次,未来所有 Agent 项目都能复用。
4.5 工具调用的安全与权限

🎨 漫画图解:〈工具城的五道安全门〉
Agent 有了「动手能力」,也意味着它有了「搞破坏」的能力。一个被恶意利用的 Agent——或者一个判断失误的 Agent——可能:
- 删掉重要的文件
- 向外部 API 发送敏感数据
- 陷入无限循环烧掉几千美元的 API 费用
- 执行危险的系统命令
安全策略
| 策略 | 做法 | 效果 |
|---|---|---|
| 白名单 | 只允许调用预定义的几类工具 | 最安全,但灵活性受限 |
| 人工确认 | 在执行高危操作前弹窗让用户确认 | 安全 + 灵活,但打断体验 |
| 沙箱 | 在隔离环境中执行工具,限制文件系统和网络访问 | Docker 容器级别的安全 |
| 速率限制 | 限制每轮对话的工具调用次数 | 防止无限循环烧钱 |
| 审计日志 | 记录每一次工具调用及其结果 | 出问题时能追溯 |
Claude Code 的安全设计是一个好参考:它在执行写操作之前会请求用户确认,每轮对话限制工具调用频率,所有操作都有日志。在第7章做 Obsidian Agent 时,我们也会实现基本的安全策略。
4.6 工具调用的常见翻车
翻车1:参数幻觉
LLM 有时会「编」一个参数。比如工具要求 file_path 必须是 .md 结尾,LLM 可能输出 file_path: "my_note.txt"——格式不对。
解法:宿主程序侧做参数校验,格式不对的直接拒绝并告诉 LLM 正确格式。
翻车2:工具选择错误
用户说「帮我查一下最近有什么 AI 新闻」,LLM 可能去调用「读取本地文件」工具而不是「搜索网页」工具。
解法:工具描述写清楚使用场景。在 Prompt 中加入工具选择的指引。
翻车3:死循环
LLM 反复调用同一个工具但得不到期望的结果,陷入「调工具 → 结果不对 → 再调一次 → 还是不对」的循环。
解法:设置最大调用轮次(比如 20 轮),超过后强制终止并让 LLM 总结当前结果。这是 Claude Code 的默认策略。
本章小结
| 要点 | 一句话 |
|---|---|
| LLM 不执行 | LLM 只会「说它想调什么」,真正执行的是宿主程序 |
| Function Calling | LLM 输出一个 JSON,宿主程序解析并执行 |
| 工具定义 | 好描述 = 使用场景 + 参数说明 + 边界条件 |
| MCP | Agent 世界的 USB-C——统一工具调用标准 |
| 安全三件套 | 白名单 + 人工确认 + 调用次数限制 |
| 翻车预防 | 参数校验、工具选择指引、最大轮次限制 |
下一章预告
前三章我们搞懂了 Agent 的架构、RAG 和工具调用,但还有一个根本问题悬而未决:Agent 的行动能力到底是怎么学来的? 它不是「提示词工程」吗?还是 Agent 经过了特殊的训练?
下一章是整个系列技术含量最高的一章:Agent 的训练三阶段、强化学习怎么让 Agent 从失败中学、为什么 Agent 训练比 LLM 难一个数量级。
| ← 上一章 | 回到目录 | 下一章 → |
|---|---|---|
| 第3章:RAG | 第5章:Agent 是怎么被训练出来的 |