LLM工具调用深度解析——从Function Calling到MCP
本文是「AI 知识体系深度解析」系列的第三篇。LLM 的工具调用能力是 Agent 系统的地基:没有它,模型只是一个会聊天的文字盒子。从 2023 年 OpenAI 推出 Function Calling,到 2024 年 Anthropic 发布 MCP 协议,再到 2025 年 Skill 与 A2A 的出现,“让模型真正能做事” 这件事经历了一整条技术栈的演进。本文将这条栈从底到顶完整拆开——从模型如何学会输出 JSON、到工具如何标准化接入、再到多个 Agent 如何跨进程协作,一次性建立完整的知识地图。
核心问题列表
阅读本文,你将能够回答以下关键问题:
- Function Calling 到底是谁在执行工具? 模型只负责"决策",代码负责"执行",这个分工为何是整个机制的基石?
- 大模型是天生就会调工具的吗? SFT 和 RLHF 各自解决了什么问题?为什么缺一不可?
- MCP 和 Function Calling 是竞争关系吗? 它们到底处于不同的抽象层次还是同一层面?
- 推理模型为什么曾经不支持工具调用? 生成范式的冲突到底是什么?后来又是怎么解决的?
- Skill 和 MCP 有什么区别? “操作手册"和"工具箱"是怎么配合工作的?
- A2A 和 MCP 有什么区别? 为什么复杂 Agent 系统里两者都要用?
- SSE、WebSocket、WebRTC、stdio 各自适合什么场景? 通信协议该怎么选?
- LLM 网关和普通 API 网关有什么本质区别? 语义缓存为什么是 LLM 特有的能力?
引言:从一个"错觉"说起
很多人第一次接触 LLM 工具调用时,会有一个直觉性的误解:“模型自己去访问网络、查数据库,然后把结果返回给用户。”
这句话里藏着一个根本性错误——模型有直接执行代码的能力吗?它能联网吗?答案是不能。大语言模型在预训练阶段学的是"给定前面的文字,预测下一个 token”,整个训练过程完全在文本空间里进行,模型从未见过"工具调用"这件事。哪怕你在 prompt 里写"你可以调用天气 API",没经过专门训练的模型也只会生成一段自然语言描述——“我需要调用天气 API 来回答你”——而不是输出一段可以被程序解析和执行的 JSON 调用请求。
描述一个意图和输出可执行的结构化指令,这两件事之间有本质的差距。
正是为了跨越这道鸿沟,整个 LLM 工具调用的技术栈被一层层搭建起来:
┌─────────────────────────────────────────────────────┐
│ A2A (Agent 间协作) │ ← 横向:多Agent通信
├─────────────────────────────────────────────────────┤
│ Skill (任务流程知识化) │ ← 第3层:操作手册
├─────────────────────────────────────────────────────┤
│ MCP (工具标准化接入协议) │ ← 第2层:工具箱
├─────────────────────────────────────────────────────┤
│ Function Calling (模型调用语言) │ ← 第1层:调用协议
├─────────────────────────────────────────────────────┤
│ LLM (大语言模型本体) │ ← 地基
└─────────────────────────────────────────────────────┘
↑ 底层 通信协议层(横切) 顶层 ↑
(SSE/WebSocket/WebRTC/stdio) (LLM Gateway 横切中间件)
本文将按"从底到顶"的顺序,把这张图里的每一层拆开讲透。
第一章:Function Calling 原理
1.1 Function Calling 解决了什么问题
在 Function Calling 出现之前,想让模型帮你调工具,完全靠解析自然语言。模型输出"我需要查一下北京的天气",你再写 if/else 判断它"说"的是要查天气,然后手动去调 API。这个做法极其脆弱——模型换个说法,你的 if/else 就失配了,也根本没办法标准化对接。
Function Calling 的核心改进是:模型不再"说"要调工具,而是直接输出一段结构化的 JSON,开发者按格式解析就行,准确率大幅提升,也有了统一标准可以对接。这套机制由 OpenAI 在 2023 年推出,现在 Claude、Gemini、Qwen 等主流模型都支持。
1.2 三个角色:把 Function Calling 理解成一场任务委托
理解 Function Calling 的关键是搞清楚"谁做什么"。可以把这套流程理解成一场"任务委托":
| 角色 | 比喻 | 职责 |
|---|---|---|
| 开发者 | HR | 给每个工具写"职位说明书"(JSON schema),告诉模型有哪些工具、能做什么、需要什么参数 |
| 模型 | 经理 | 读完说明书后决定"调哪个工具、参数填什么",把指令下达出来 |
| 宿主代码 | 员工 | 真正去跑函数、访问网络、查数据库,把结果汇报回来 |
关键点:模型全程只是在"下指令",它不亲自执行任何代码,也没有直接访问网络的权限。执行的事一律由宿主程序代码完成。这个"模型决策、代码执行"的分工是整个机制的核心设计——LLM 擅长理解意图和推理,但不应该有直接操作系统资源的权限;宿主程序负责执行,可以做权限控制、参数校验、执行沙箱等安全措施。
1.3 工具定义:schema 的每个字段都有含义
工具 schema 就是一份结构化的"工具说明书",用 JSON 格式写:
tools = [
{
"type": "function",
"function": {
"name": "get_weather", # 工具的唯一标识
"description": "查询指定城市的实时天气,包含气温、天气状况、风向风速,仅支持中国大陆城市",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如「北京」「上海」,不要带省份前缀"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认用摄氏度"
}
},
"required": ["city"]
}
}
}
]
其中最关键的字段是 description。如果 description 写得含糊(比如只写"获取天气"),模型会"瞎猜"——拿到一个带英文名的城市也照样调,拿到"这周天气如何"这种时间跨度不对的问题也硬往里塞。模型在决定"要不要调这个工具、参数怎么填"的时候,能依赖的唯一依据就是这段描述。写得越清晰,模型的选择越准确。
1.4 完整调用流程:两轮对话加中间执行
Function Calling 的运行时本质上是"两轮对话 + 中间执行"的闭环:
用户 模型 宿主代码
│ │ │
│──── "北京天气?" ────→│ │
│ (携带 tools schema) │ │
│ │ │
│ [判断:需要调工具] │
│←─ finish_reason: ────│ │
│ "tool_calls" │ │
│ {name, arguments} │ │
│ │ │
│ │──── 执行 get_weather ─→│
│ │ ("city":"北京") │
│ │←──── "晴,15°C" ───────│
│ │ │
│ │ [拿到结果,生成答案] │
│←── "北京今天晴朗..." ─│ │
第一轮,你把工具列表和用户的问题一起传给模型。模型如果判断需要调工具,就不直接输出最终答案,而是输出一个 finish_reason 为 "tool_calls" 的响应,里面包含要调用的工具名和参数——这是个明确信号,告诉你"我需要工具帮助,还没准备好给答案"。
中间环节交给你的代码:解析 tool_calls,找到对应的函数执行,拿到结果。
第二轮,把工具执行结果以 role: "tool" 的消息塞回对话历史,再次调用模型。这次模型有了工具结果,有了充分信息,才给出最终的自然语言答案。
import openai, json
client = openai.OpenAI()
messages = [{"role": "user", "content": "北京今天天气怎么样?"}]
# 第一轮:把工具定义和问题一起传给模型
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto" # auto 让模型自己判断,也可设 required 强制调
)
msg = response.choices[0].message
if msg.finish_reason == "tool_calls": # 模型要调工具
tool_call = msg.tool_calls[0]
func_args = json.loads(tool_call.function.arguments) # {"city": "北京"}
# 中间执行:你的代码真正去跑函数
result = f"{func_args['city']}今天晴,15°C,东北风 3 级"
# 第二轮:把工具结果塞回对话,再问一次模型
messages.append(msg)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id, # 和 tool_calls 里的 id 对应
"content": result
})
final = client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools)
print(final.choices[0].message.content)
# 输出:北京今天天气晴朗,气温 15°C,东北风 3 级,适合外出。
1.5 并行工具调用
如果用户一口气问了好几件事——“帮我查北京、上海、广州三个城市的天气”——模型可以在一次响应里同时输出多个 tool_calls(它是一个列表)。你的代码可以同时执行这些工具(比如用 asyncio.gather),拿到所有结果后一次性塞回对话,再调一次模型拿到综合答案。
整个过程从"两轮对话 × N 个工具串行"压缩成了"一轮对话 + 并行执行",总耗时大幅降低。但前提是这几个工具之间没有依赖关系:查北京和上海天气互不影响,可以并行;但"先查用户订单号,再用订单号查物流",第二个调用依赖第一个的结果,只能串行,模型也会正确地分两轮输出。
import asyncio
async def execute_tool_call(tool_call):
"""并行执行单个工具调用"""
func_args = json.loads(tool_call.function.arguments)
# 这里 dispatch 到真实函数
return await dispatch_function(tool_call.function.name, func_args)
# 并行执行模型一次返回的多个 tool_calls
results = await asyncio.gather(*[execute_tool_call(tc) for tc in msg.tool_calls])
# 把所有结果塞回对话
for tc, result in zip(msg.tool_calls, results):
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
本章实践要点
- description 是灵魂:工具描述写得越清晰,模型选择越准确。务必写明适用范围、参数格式、限制条件。
- 模型只决策不执行:永远不要让模型直接操作系统资源,执行逻辑放在宿主代码里,便于权限控制和沙箱隔离。
- 并行调用要判断依赖:无依赖的工具用并行执行降低延迟,有依赖的必须串行。
- tool_call_id 要对应:第二轮把结果塞回去时,
tool_call_id必须和tool_calls里的 id 一一对应。
第二章:Function Calling 训练
2.1 原始 LLM 为什么不会调工具
想象一个人从出生到成年,只生活在文字的世界里,读过几乎所有的书,却从没接触过任何工具——没用过锤子、没开过车、也没见过 API。你突然跟他说"去帮我查一下天气 API",他最多只会用语言描述"我需要查天气 API 来获取数据……",绝对不会真的去操作工具。
大语言模型在预训练阶段经历的就是这样一个过程。预训练的目标是"预测下一个 token",整个训练完全在文本空间里进行,模型从未见过"工具调用"这件事。预训练语料里有代码、有文档、有对话,但几乎没有"给定一组工具 schema,该在什么场景输出什么 JSON"这样的成对样本。
所以工具调用能力不是天生的,是后天"教"出来的。怎么教?靠两个阶段:SFT 教会怎么调,RLHF 教会什么时候调。
2.2 第一阶段:SFT,让模型"见过"工具调用
SFT(Supervised Fine-Tuning,监督微调)的核心思路非常直接:给模型看大量正确的示例,让它学会模仿。就像培养一名新员工,前期让他看几百份填好的工单,他自然就学会了"遇到这类问题该怎么写工单、该走哪个流程"。
一条完整的训练样本包含所有角色的消息:
System: [工具定义 schema] ← 模型从这里"认识"工具
User: "北京今天天气怎么样?" ← 用户提问
Assistant: {"tool_calls": [ ← 关键!结构化JSON,不是自然语言
{"name":"get_weather",
"arguments":{"city":"北京"}}]}
Tool: "晴,15°C,东北风3级" ← 模拟工具返回
Assistant: "北京今天天气晴朗..." ← 最终自然语言回答
模型在几十万甚至上百万条这样的样本上反复训练,通过反向传播(backpropagation)学习:当模型输出的内容偏离正确 JSON 时,损失函数产生惩罚信号,梯度往回传,调整参数权重,让下次输出更接近正确格式。
2.3 训练数据需要覆盖的五类场景
训练数据的多样性直接决定了 Function Call 能力的上限,不能只有"正常调一个工具"这一种情况。好的训练数据至少要覆盖五类场景:
| 场景 | 说明 | 缺失的后果 |
|---|---|---|
| 单工具调用 | 最基础的入门场景 | 模型连基础调用都不会 |
| 多工具并行调用 | 一次输出多个 tool_calls | 模型不知道可以并行,傻乎乎串行 |
| 工具调用失败后重试 | API 超时、参数错误等情况 | 模型遇到错误直接崩掉或傻傻重复 |
| 不需要工具直接回答 | “1+1等于几"这类问题 | 模型形成"遇到问题就调工具"的惯性 |
| 多轮对话中的工具调用 | 引用历史上下文中的工具结果 | 模型无视历史重新调用 |
2.4 SFT 的短板:会了,但不知道"该不该调”
SFT 让模型学会了"调工具"这个动作,但它不知道什么时候该调、什么时候不该调。因为 SFT 的训练样本里"该调的场景"占了绝大多数(毕竟我们就是要教它调工具),模型在模仿的过程中会过拟合这种"积极调用"的倾向,没看过足够多的"不该调"反例。
“SFT 之后的模型也有类似的毛病:可能对简单问题也尝试调工具,或者遇到工具调用失败时不知道该怎么处理,行为边界感很弱。”
2.5 第二阶段:RLHF,用反馈建立边界感
RLHF(Reinforcement Learning from Human Feedback,人类反馈强化学习)的流程分四步:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1.生成多样回答 │───→│ 2.人类打分排序 │───→│ 3.训练奖励模型│───→│ 4.强化学习优化│
│ (覆盖各种情况) │ │ (哪种回答更好) │ │ (学会打分的RM)│ │ (PPO调主模型) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
- 生成多样回答:对同一个问题,让模型生成几种不同的处理方式——有的调了工具、有的直接回答、有的参数填错了,故意覆盖各种情况。
- 人类打分:标注员评判哪种回答更合理。“1+1 等于几"直接回答最好,“北京天气怎么样"调工具才对。这批打分数据记录了人类的判断偏好。
- 训练奖励模型(RM):用这批打分数据单独训练一个小模型,专门负责打分。它不回答问题,只判断"这个回答人类会喜欢吗”——相当于一个"会打分的裁判”。人类的判断被"蒸馏"进了这个裁判。
- 强化学习优化主模型:拿奖励模型的打分,通过 PPO 等算法持续调整主模型参数,让它越来越倾向于产出"高分回答"。
2.6 为什么偏偏是 PPO
强化学习算法那么多,选 PPO 有两个务实的理由:
- 相对稳定:训练过程不容易崩。传统策略梯度算法很容易因为单步更新太大把模型直接调废。
- 内置 KL 散度约束:强迫新模型和旧模型的输出分布不要差得太远,避免"为了讨好奖励模型,模型把自己训成只会重复几句套话的怪胎"这种退化情况。
RLHF 本质上要让模型在"追求高奖励"和"保持语言能力"之间走钢丝,PPO 在这个平衡上是目前公认好用的工具。
RLAIF(Reinforcement Learning from AI Feedback)是 RLHF 的变体,用更强的 AI 模型(比如 GPT-4)代替人类标注员打分,成本能低 10-100 倍。但代价是"AI 的偏见会传递"——如果打分的 AI 本身对某些场景判断有偏差,这些偏差也会被学进去。现在业界很多模型训练都在混用 RLHF 和 RLAIF,关键数据用人工保质量,量大的地方用 AI 提效率。
2.7 两个阶段各司其职
SFT RLHF
"会不会调" "该不该调"
(教会格式) (建立边界感)
│ │
└──────────┬─────────────────┘
↓
训练好的 Function Call 能力
"知道怎么调,也知道什么时候该调"
只有 SFT 而没有 RLHF 的模型,可能遇到什么问题都冲动地调工具;反过来,只有 RLHF 而没有 SFT,模型连工具调用的格式都输不出来,奖励信号根本没地方发力。两个阶段配合起来,才能训练出完整的工具使用能力。
本章实践要点
- 预训练学不会工具调用:不要指望参数量够大就自然涌现,必须做专项微调。
- 训练数据要覆盖五类场景:缺哪个场景就会在哪个场景翻车,特别是"不需要工具直接回答"这一类经常被忽略。
- 数据来源注意幻觉传递:用强模型自动生成训练数据时(Self-Instruct / Distillation),必须人工抽查质量,否则等于在教模型学错误答案。
- RLHF 的奖励模型质量是天花板:人类标注员标准不一致,奖励模型就会学到歪的打分标准。
第三章:MCP 协议
3.1 N×M 问题:没有 MCP 之前,接工具有多麻烦
想象你要给 Claude 接入 GitHub 工具。你得手写 GitHub API 的调用代码、处理认证(OAuth token 怎么传)、处理各种返回格式、把 API 响应转成模型能理解的格式……好不容易接好了。结果过了两个月 Claude 升了个版,接口有变化,对接代码得改。更麻烦的是,你同时接了十个工具,每个工具都有自己的一套对接代码。现在产品方说这套工具也要给 Cursor 用——不好意思,重写一遍,因为 Cursor 和 Claude Desktop 的接入方式完全不同。
这就是 MCP 出现之前 AI 工具生态的真实状态:碎片化、难复用、强绑定。
把这个问题抽象出来就是经典的 N×M 问题:N 个 AI 应用要对接 M 个工具,需要维护 N×M 份对接代码。
没有MCP: N×M 份对接代码 有MCP: N+M 份
App1 ──→ ToolA (对接代码) App1 ──┐
App1 ──→ ToolB (对接代码) App2 ──┤ ┌── MCP Server A
App1 ──→ ToolC (对接代码) App3 ──┤ ┌──→(ToolA)
──────┤ │
App2 ──→ ToolA (重写一份) MCP ├─┤
App2 ──→ ToolB (重写一份) Client├─┤ ┌── MCP Server B
App2 ──→ ToolC (重写一份) 模块 │ └──→(ToolB)
(N个) │
App3 ──→ ToolA (再重写) ├─┐
App3 ──→ ToolB (再重写) │ └── MCP Server C
App3 ──→ ToolC (再重写) (ToolC)
= N×M 份代码, 改一个工具改 N 处 = N+M 份代码, 各自独立维护
MCP(Model Context Protocol,模型上下文协议)的思路是用 USB 接口来类比:在 USB 标准出现之前,鼠标用一个接口、键盘用另一个、打印机又是另一个,换台电脑就愁接口不兼容。USB 出现之后,所有外设统一接口,设备厂商只需要做一次适配,全球所有 USB 电脑都能用。MCP 做的是同一件事:为"AI 接工具"这件事定一套统一的协议标准。
3.2 Client-Server 架构与三个角色
MCP 采用标准的 Client-Server 架构,但定义了三个角色,不只是两个:
| 角色 | 职责 | 比喻 |
|---|---|---|
| Host | AI 应用本身(如 Claude Desktop),启动和管理所有 Client,控制连哪些 Server | 公司 |
| Client | Host 内部的连接模块,一个 Client 对应一个 Server 连接。负责初始化连接、能力发现、转发调用请求 | 驻场联络员 |
| Server | 工具提供方实现的独立进程,对外暴露 Tools/Resources/Prompts | 外部供应商 |
关键设计:一个 Host 可以同时连多个 Server。你把文件系统 Server + GitHub Server + PostgreSQL Server 都接上,模型就同时拥有了操作本地文件、读写代码仓库、查询数据库这三套工具能力,而你不需要写任何对接代码,只需要在配置文件里加几行 JSON。
Server 完全不关心上面是哪个 Host 在用它,只需要按 MCP 协议响应 Client 的请求就行。这也是 MCP 的核心价值:Server 写一次,任何支持 MCP 的 Host 都能直接用。
3.3 三类核心能力:Tools / Resources / Prompts
MCP Server 可以向 Client 暴露三类能力,各有各的定位:
| 能力 | 本质 | 副作用 | 授权要求 |
|---|---|---|---|
| Tools(工具) | 有副作用的操作,执行后改变外部状态 | 有(创建文件、提交代码、发消息、调API) | 通常需要用户授权确认 |
| Resources(资源) | 只读数据,不改变任何东西 | 无(读取日志、查数据库记录、获取文档) | 可更宽松暴露 |
| Prompts(提示模板) | 预定义的提示词模板,带参数占位符 | 无 | 可共享复用 |
三者的本质区别可以这样记:Tools 改变世界,Resources 观察世界,Prompts 结构化表达。
Resources 和 Tools 最本质的区别是一个字:“只读”。你可以把 Resources 理解成"工具的资料室",模型可以进去查资料,但不能修改里面的东西。正因为只读、无副作用,Resources 可以更宽松地暴露给模型,不需要像 Tools 那样谨慎授权。
3.4 JSON-RPC 2.0:底层消息格式
Client 和 Server 之间的消息格式统一用 JSON-RPC 2.0。每条消息是一个 JSON 对象,格式固定:
// Client 向 Server 查询工具列表
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
// Server 返回工具列表
{"jsonrpc": "2.0", "id": 1, "result": {"tools": [{"name": "read_file", ...}]}}
// Client 请求调用某个工具
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "read_file", "arguments": {"path": "/tmp/log.txt"}}}
用 JSON 而不是二进制格式,好处是易读、易调试、语言无关,不管 Server 是 Python 写的还是 TypeScript 写的,消息格式是一样的。MCP 用 JSON-RPC 2.0(相比 1.0 加了批量请求、通知消息等功能)。
3.5 两种传输方式
消息格式定了,怎么传呢?MCP 支持两种传输方式:
┌─────────────────────┐ ┌─────────────────────────┐
│ stdio (本地) │ │ Streamable HTTP (远程) │
├─────────────────────┤ ├─────────────────────────┤
│ Server 作为子进程 │ │ Server 作为 HTTP 服务 │
│ 通过 OS 管道通信 │ │ Client 通过网络连接 │
│ │ │ │
│ ✓ 延迟极低 │ │ ✓ 多Client共享一个Server │
│ ✓ 不开端口,无安全问题 │ │ ✓ 支持跨机器访问 │
│ ✓ 生命周期自动管理 │ │ ✗ 有网络开销 │
│ ✗ 仅限本地 │ │ ✗ 需处理认证/重连 │
└─────────────────────┘ └─────────────────────────┘
stdio(标准输入输出):Server 作为本地子进程运行,Host 通过操作系统的管道和它通信,Server 从 stdin 读请求、把结果写到 stdout。整个过程不经过网卡、不经过 TCP/IP 协议栈,数据在 RAM 里走了一趟就到了,延迟天然比网络请求低得多。Claude Desktop 接本地 MCP Server 走的就是这种方式。
Streamable HTTP:Server 作为 HTTP 服务独立部署,Client 通过网络连接访问。用单个 HTTP 端点(通常是 /mcp)同时处理请求和响应:Client 用 POST 发请求,Server 根据情况灵活返回——短请求直接回普通 JSON,长请求则把 HTTP 响应升级为 SSE 流持续推送中间结果。
这里有一个很重要的设计点:消息格式(JSON-RPC 2.0)和传输方式(stdio / Streamable HTTP)是解耦的。同一套 JSON-RPC 消息可以跑在任意传输层上,切换传输方式不影响上层的工具调用逻辑。这让 MCP Server 既可以轻量地作为本地进程运行,也可以作为正式的微服务部署。
3.6 SSE 双端点到 Streamable HTTP 的演进
MCP 早期版本(2024-11-05 规范)的远程传输方案叫"HTTP + SSE",是双端点结构:一个 GET 端点开 SSE 长连接接收 Server 推送,一个 POST 端点用来发请求。这套方案在 2025 年 3 月的规范更新里被改成了单端点的 Streamable HTTP(老的 HTTP+SSE 被标记为 deprecated,但保留向后兼容)。
为什么要替换?因为双端点有一个小尴尬:同一个对话被拆成了两条通道。Client POST 了一条消息之后网络突然断了,那条消息到底被处理了没、SSE 流会不会推回结果,Client 没有一个简单的办法判断,出问题时排查链路很长。
Streamable HTTP 把两条通道合并成一个端点,架构更简洁,对负载均衡器和 serverless 环境都更友好。注意:Streamable HTTP 并没有抛弃 SSE,流式推送的部分底层还是 SSE(Content-Type: text/event-stream),只是把端点从两个合成一个。
3.7 生态发展快的原因
MCP 是 Anthropic 在 2024 年底发布的,发布后发展速度很快,主要有两个原因:
- 极低的实现门槛:Anthropic 开源了协议规范和多语言 SDK(Python、TypeScript 都有),写一个最简单的 MCP Server 不到 30 行代码。
- 头部工具第一时间跟进:GitHub、Slack、PostgreSQL、Puppeteer、Google Maps 等高频工具都有了官方或社区维护的 MCP Server,接一个新工具就是配置文件里加几行 JSON,零代码。
本章实践要点
- MCP 是协议不是框架:它解决的是"工具怎么标准化接入",不是"模型怎么输出调用请求"。
- Host ≠ Client:Host 是宿主应用本身,Client 是 Host 内部负责和 Server 通信的模块,一个 Host 可以连多个 Server。
- 三类能力职责分明:Tools 有副作用需授权,Resources 只读无副作用,Prompts 是可复用模板。不要把只读数据和有副作用的操作混为一谈。
- 消息格式和传输方式解耦:JSON-RPC 2.0 定义消息长什么样,stdio/Streamable HTTP 定义消息怎么传,两者互不耦合。
- 新项目用 Streamable HTTP:HTTP+SSE 双端点方案已 deprecated,仍向后兼容但不推荐新项目使用。
第四章:MCP vs Function Calling
4.1 语言层 vs 工具箱层
很多人第一次看到 MCP 会有一个直觉困惑:Function Calling 不是已经能调工具了吗,为什么还要再搞一个协议?这个困惑的根源,是把"能调工具"和"管好工具"混在一起了。
有一个很好的类比:HTTP 协议出来之后,我们已经能在网络上传数据了,为什么还需要 REST API 规范?因为 HTTP 解决的是"怎么传"(一次请求长什么样、用什么方法、怎么编码),REST 解决的是"怎么组织和管理"(资源怎么命名、端点怎么设计、状态怎么表达、多个服务之间怎么复用同一套约定)。
Function Calling 和 MCP 也是同样的关系:
| 维度 | Function Calling | MCP |
|---|---|---|
| 解决什么 | 模型怎么输出调用请求 | 工具怎么标准化接入 |
| 层次 | 调用语言(格式) | 工具生态(规范+管理) |
| 类比 | HTTP 请求格式 | REST API 规范 + 服务注册发现 |
| 工具管理 | 无,每次手动写 schema | 有,自动发现、注册 |
| 跨项目复用 | 无,换个项目重写 | 有,一次实现到处复用 |
| 跨模型兼容 | 无,各家格式不同 | 有,协议统一 |
4.2 Function Calling 的痛点:每次都是一次性的
“每次手动"到底有多痛?假设你团队里有 5 个应用,每个应用要接 8 个工具,也就是 40 份工具对接代码在维护。某天 GitHub API 的某个字段变了,你要在 5 个地方同步改,只要其中一个忘了,那个应用在凌晨报警。再假设你要从 Claude 迁到 GPT-4,这 40 份代码里的 Function Calling 格式全要重新适配一遍。
同一个工具,换个项目就要重新对接一遍,每次都是一次性的手工活。这就是 Function Calling 解决不了的核心问题:工具的管理、复用和跨平台兼容。
4.3 最关键的联系:MCP 底层依然靠 Function Calling 驱动
这是很多人没想清楚的一点:MCP 不是 Function Calling 的替代品,而是建立在 Function Calling 之上的。
当 MCP Client 连上一个 Server 之后,会自动向 Server 拉取所有工具的定义(调用 list_tools 接口),然后把这些定义转换成模型原生的 Function Calling 格式传给模型。模型依然通过输出 tool_calls 来表达"我要调哪个工具”,MCP Client 再把这个请求路由到对应的 Server 去执行,拿到结果后以 tool 消息的形式喂回对话。
模型视角 MCP Client (宿主程序层) MCP Server
│ │ │
│ ← tools schema ─────── │ (list_tools, 转成FC格式) ←─── │
│ │ │
│ ── tool_calls JSON ──→ │ (路由到对应Server) ─────────→│ (执行)
│ │ │
│ ← tool result ──────── │ (tool消息喂回) ←──────────── │ (返回)
│ │ │
│ [模型完全感知不到MCP] │ [所有"魔法"都在这一层] │
从模型的视角来看,它完全感知不到 MCP 的存在,它以为自己只是在做普通的 Function Calling,根本不知道背后有一套 Server 在运行。MCP 的所有"魔法"都发生在宿主程序层:工具的自动发现、schema 的格式转换、调用请求的路由、执行结果的返回,全都在这一层默默完成。
这也意味着:如果模型本身不支持 Function Calling,MCP 就完全没办法用,因为这个"翻译层"失效了。
4.4 自己写一个 MCP Server 有多简单
以 Python SDK 为例,核心就三步:用 @app.list_tools() 装饰器声明工具、用 @app.call_tool() 装饰器实现执行逻辑、用 stdio 方式运行。
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
app = Server("calculator")
# 1. 定义工具列表
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="add_numbers",
description="计算两个数字的和",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "第一个数字"},
"b": {"type": "number", "description": "第二个数字"}
},
"required": ["a", "b"]
}
)
]
# 2. 实现工具执行逻辑
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "add_numbers":
result = arguments.get("a", 0) + arguments.get("b", 0)
return [TextContent(type="text", text=f"计算结果: {result}")]
return [TextContent(type="text", text=f"未知工具: {name}")]
# 3. 启动 Server (stdio模式)
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(read_stream, write_stream,
app.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
然后在 claude_desktop_config.json 里加几行配置:
{
"mcpServers": {
"calculator": {
"command": "python",
"args": ["/path/to/your/calculator_server.py"]
}
}
}
配好之后重启 Claude Desktop,直接输入"帮我算一下 25 加 17 等于多少",Claude 就会自动调用你写的 add_numbers 工具。整个过程你只写了工具逻辑本身,所有的通信、发现、调用路由都由 MCP 框架搞定了。
4.5 选型判断框架
碰到"用 Function Calling 还是 MCP"这个选择题,按几个问题依次过一遍:
开始
│
┌───────────▼───────────┐
│ 社区有现成MCP Server? │
└───┬───────────────┬───┘
是│ 否│
↓ │
直接用MCP │
┌────▼───────────┐
│ 工具需要跨项目 │
│ /跨团队复用? │
└──┬──────────┬──┘
是│ 否│
↓ │
用MCP │
┌────▼────────┐
│ 工具规模起来了? │ (数量+复杂度+团队规模+变更频率)
└──┬───────┬──┘
是│ 否│
↓ │
用MCP │
┌───▼──────┐
│ 做正式 │
│ Agent系统?│
└─┬──────┬─┘
是│ │
↓ │
用MCP │
┌──▼──────┐
│受限环境? │ (Serverless不能起子进程)
└──┬──────┘
是│
↓
用Function Calling
总结成一句话:“只用自己、只用一次、不需要复用"才适合 Function Calling,其他情况优先考虑 MCP。两者不是竞争关系,MCP 底层本来就是靠 Function Calling 驱动的,选哪个取决于你的工程需求。
本章实践要点
- 不是替代关系:MCP 底层靠 Function Calling 驱动,模型感知不到 MCP 的存在。
- 选型看多维度:社区现成 Server、复用需求、工具规模、Agent 系统、部署环境,不能只看一个指标。
- 内嵌 vs 独立:Function Calling 的工具"内嵌"在应用代码里,MCP 的工具"独立"为标准进程。
- 受限环境退回 FC:某些 Serverless 平台不允许启动子进程,stdio 模式的 MCP Server 没法用。
第五章:Skill——任务流程知识化
5.1 从"重复贴 prompt"的痛点说起
你一定遇到过这种情况:每次让 AI 帮你做代码审查,你都要贴一大段指令——“检查这几类问题、用这种格式输出、重点关注安全漏洞”。第一次贴还好,第十次你就开始烦了,每次新对话都要从头贴一遍,漏掉某个细节就会导致输出质量不稳定。
这还只是一个人的情况。如果是团队协作呢?十个人做代码审查,每个人贴的 prompt 都不一样,有人关注安全,有人关注性能,审查标准完全没法统一。你可能想到把 prompt 写到共享文档里让大家复制,但这本质上还是靠人工维护和执行,版本一多就容易乱。
Skill 要解决的就是这个问题:把那些你反复在用的指令、流程、模板,打包成一个标准化的模块,Agent 自己知道什么时候该用它、怎么用它,不再依赖你手动复制粘贴。
5.2 Skill 的结构
一个 Skill 说白了就是一个文件夹,里面最核心的是一份 SKILL.md 文件:
code-review/ # Skill 文件夹,名字就是标识
├── SKILL.md # 核心指令文件(必须有)
├── scripts/ # 可选:可执行的脚本
│ └── check_security.py # 比如一个安全检查脚本
├── references/ # 可选:参考文档
│ └── review_standards.md # 比如团队的审查标准文档
└── assets/ # 可选:模板、资源文件
└── report_template.md # 比如审查报告的输出模板
SKILL.md 的内容分两部分:顶部是 YAML 格式的元数据(frontmatter),声明名字和一句话描述;下面是 Markdown 正文,写具体的指令和步骤:
---
name: code-review
description: "对代码进行全面审查,检查 bug、安全漏洞和性能问题,输出结构化审查报告"
---
# 代码审查 Skill
## 指令
### 第一步:理解代码上下文
阅读提交的代码,理解它的功能和所属模块,确认修改范围。
### 第二步:逐项检查
按以下维度逐一检查:
1. 功能正确性:逻辑是否有 bug,边界条件是否处理了
2. 安全性:是否有注入、XSS、权限绕过等漏洞
3. 性能:是否有 N+1 查询、不必要的循环、内存泄漏风险
### 第三步:输出报告
使用 assets/report_template.md 的模板格式,输出结构化的审查报告。
5.3 渐进式加载:Skill 最聪明的设计
Skill 最让人眼前一亮的设计不是"能打包”,而是它的加载方式——渐进式加载(Progressive Disclosure)。
假设你有 20 个 Skill,每个平均 2000 token,全部加载就是 4 万 token 打底,吃掉了 20 万上下文窗口的五分之一,而大部分在当前任务里根本用不上。Skill 用三层机制解决这个问题:
┌──────────────────────────────────────────────────────┐
│ 第1层:只看简历 │
│ 启动时只加载 name + description │
│ 每个 Skill ~30-50 token │
│ "我手上有哪些能力可以用" │
├──────────────────────────────────────────────────────┤
│ 第2层:翻开详细资料 (按需) │
│ Agent判断某Skill与当前任务相关时 │
│ 才加载 SKILL.md 正文 │
│ 不相关的Skill始终不加载 │
├──────────────────────────────────────────────────────┤
│ 第3层:需要时再取 (用到才取) │
│ 执行中指令提到"使用assets/xxx模板"时 │
│ 才读取那个模板文件 │
│ 参考文档、脚本同理 │
└──────────────────────────────────────────────────────┘
这个设计用一个类比就很好理解:Skill 就像公司给新员工准备的入职手册。你入职第一天不会把整本手册从头到尾看完,而是先扫一眼目录,知道里面有"报销流程"“请假制度"“代码规范"这些章节就行了。等你真的要报销了,再翻开"报销流程"那一章仔细看。
为什么这个设计这么重要? 因为 context window 是 Agent 最宝贵的资源。如果把所有 Skill 的全部内容一股脑塞进去,真正有用的用户任务信息反而会被淹没,Agent 的注意力被分散,输出质量反而下降。
5.4 Skill 与 MCP 的关系:操作手册层
Skill 和 MCP 处于完全不同的层次,用一个类比就能讲清楚:
| 概念 | 比喻 | 作用 |
|---|---|---|
| MCP / Tool | 公司给员工配的电脑、软件和数据库权限 | 提供能力(能做事) |
| Skill | 操作手册和 SOP 流程 | 提供知识和流程(知道怎么做) |
| Prompt | 口头跟员工说的一句话指令 | 一次性、临时 |
MCP 给 Agent 提供的是工具和数据的访问能力,Skill 教 Agent 拿到这些工具之后该怎么用。一个是"能力”,一个是"知识和流程”。
那 Slash Command 呢?它也是把指令保存下来复用,但必须由你手动触发(输入 /code-review)。而 Skill 可以被 Agent 自动发现和调用——Agent 看到你的任务后,自己判断"这个任务需要用 code-review Skill",然后主动去加载和执行。
5.5 Skill 和 MCP 怎么配合工作
用一个代码审查场景走一遍:
用户:"帮我审查这次提交的代码"
│
▼
┌─────────────────┐
│ Skill层起作用 │ Agent扫描Skill列表
│ │ 发现code-review匹配
│ 加载SKILL.md: │ 读取执行流程:
│ 第1步:读代码 │
│ 第2步:安全检查 │
│ 第3步:输出报告 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ MCP层起作用 │ 执行第1步"读代码"
│ │ 调用文件系统MCP Server:
│ code = mcp_ │ read_file("src/auth.py")
│ client.call_ │
│ tool("read_ │
│ file", {...}) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Skill自带脚本 │ 执行第2步"安全检查"
│ │ 加载scripts/check_security.py
│ 运行脚本 │ (Skill不只有文字,还能带脚本)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Skill自带模板 │ 执行第3步"输出报告"
│ │ 加载assets/report_template.md
│ 按模板格式输出 │ 整理成结构化报告
└─────────────────┘
分工非常清晰:Skill 扮演"编排者",定义了做什么、按什么顺序做、用什么标准做;MCP 扮演"执行者",提供了每一步需要调用的具体工具。缺了 Skill,Agent 拿着一堆工具不知道该什么时候用;缺了 MCP,Skill 再详细的流程也只是纸上谈兵。
本章实践要点
- Skill 不是 prompt:它是一个包含指令、脚本、模板的可复用能力模块,Agent 可以自动发现和按需加载。
- 渐进式加载是核心设计:三层加载机制(只读元数据 → 按需加载指令 → 用到时才取资源)体现的是"context 工程"思维。
- Skill 和 MCP 互补:MCP 提供工具和数据访问,Skill 提供用这些工具完成任务的知识和流程。
- Skill 粒度比 MCP 粗得多:MCP 粒度是单个函数调用,Skill 粒度是完整工作流程,内部可能涉及好几个步骤、调用好几个工具。
第六章:FC / MCP / Skill 三层架构全景
6.1 为什么会有三个概念
这三个概念放在一起确实容易让人迷惑,但它们各自解决的是完全不同层次的问题,是三层架构,不是三个竞争方案。
从时间线来看更清楚:
- Function Calling(2023):最先出现,核心问题是"模型只会生成文本,怎么让它触发外部调用"。
- MCP(2024 底):Function Calling 普及后,痛点变成"每个应用都要自己写代码对接各种工具,重复劳动太多"。MCP 把工具接入标准化。
- Skill(2025 年 10 月):工具有了、接入也标准化了,又冒出新问题"Agent 有了一堆工具,但不知道该按什么流程用"。Skill 解决知识和流程复用。
三者的出现背景不同,自然解决的是不同层次的东西。
6.2 三层架构全景图
╔══════════════════════════════════════════════════════════╗
║ 用户任务输入 ║
║ "帮我分析最近三个月的销售数据" ║
╠══════════════════════════════════════════════════════════╣
║ ║
║ ┌─ Skill 层 (操作手册) ─────────────────────────────┐ ║
║ │ Agent扫描Skill列表 → 匹配"数据分析报告"Skill │ ║
║ │ 加载SKILL.md: │ ║
║ │ 第1步: 从数据库取数据 │ ║
║ │ 第2步: 用Python做趋势分析 │ ║
║ │ 第3步: 按模板写报告 │ ║
║ │ [定义流程: 做什么、什么顺序、什么标准] │ ║
║ └───────────────────────┬──────────────────────────┘ ║
║ │ 每步需要调工具时 ║
║ ┌───────────────────────▼──────────────────────────┐ ║
║ │ MCP 层 (工具箱) │ ║
║ │ MCP Client自动发现工具: │ ║
║ │ • query_database (数据库Server) │ ║
║ │ • run_python (Python执行器Server) │ ║
║ │ [提供工具: 一次实现,到处复用,自动发现] │ ║
║ └───────────────────────┬──────────────────────────┘ ║
║ │ 模型触发调用时 ║
║ ┌───────────────────────▼──────────────────────────┐ ║
║ │ Function Calling 层 (调用语言) │ ║
║ │ 模型输出: │ ║
║ │ tool_calls: query_database(sql="SELECT...") │ ║
║ │ 模型输出: │ ║
║ │ tool_calls: run_python(code="df.groupby...") │ ║
║ │ [模型决策: 调哪个函数、参数是什么] │ ║
║ └───────────────────────────────────────────────────┘ ║
║ ║
╠══════════════════════════════════════════════════════════╣
║ 最终结构化报告输出 ║
╚══════════════════════════════════════════════════════════╝
6.3 用做菜来类比
用做菜来类比就很好理解三层的关系:
| 层 | 类比 | 说明 |
|---|---|---|
| Function Calling | 你的"手" | 能拿刀、能点火、能翻锅,最基础的操作能力 |
| MCP | 你的"厨房" | 里面有各种厨具和食材,刀在抽屉里、调料在架子上,走进去就知道有什么可用 |
| Skill | “菜谱” | 告诉你先热锅凉油、再放姜蒜爆香、然后下主料翻炒、最后调味出锅 |
跳过其中一层会怎样?
- 没有手(FC):你站在厨房里、拿着菜谱,什么都做不了,连刀都拿不起来。
- 没有厨房(MCP):就算你有手、有菜谱,家里什么厨具都没有,只能空手对着菜谱发呆。
- 没有菜谱(Skill):你有手、也有满厨房的厨具,但面对一桌食材不知道先切什么、后炒什么,只能瞎折腾。
三者缺一不可,各管各的层次。完整的链条是:Skill(定义流程)→ MCP(提供工具)→ Function Calling(模型触发调用),从上到下三层,每一层都建立在下一层的基础之上。
6.4 三层各自"谁和谁通信"
| 机制 | 发生在 | 主语 | 粒度 |
|---|---|---|---|
| Function Calling | 模型 ↔ 函数 | 模型说"我要调这个函数" | 单次调用 |
| MCP | AI客户端 ↔ 工具服务 | 工具服务说"我能提供这些函数" | 原子操作 |
| Skill | Agent ↔ 知识模块 | 操作手册说"用这些工具按这个流程做" | 工作流程 |
三句话的主语不一样,说话对象不一样,粒度也不一样,这就是三者的本质差异。
本章实践要点
- 不是三个竞争方案:它们是三层架构,从底到顶各司其职。
- 层级依赖明确:Skill 依赖 MCP 提供工具,MCP 依赖 Function Calling 触发调用。
- 用完整场景串联:理解三者的最好方式是把它们放在同一个任务里走一遍——Skill 编排流程,MCP 提供工具,Function Calling 做模型和工具的通信。
第七章:推理模型与工具调用冲突
7.1 范式冲突的本质
普通模型的工作方式很直接:问题进来,答案出去。推理模型(Reasoning Model)不一样,它在给出最终答案之前,会先生成一大段"内部思考"(thinking tokens),在思考里自言自语地推演、验证、反驳,甚至推翻自己前面的结论重来。代表模型有 OpenAI o1/o3 系列、DeepSeek-R1、Claude 的扩展思考(Extended Thinking)模式。
工具调用的本质是"中途暂停":模型生成调用请求 → 停下来等宿主程序执行 → 拿到结果 → 继续生成。而推理模型的思考链是一次性连续生成的,不能中途打断。
这两种生成范式是根本冲突的。
普通模型 + 工具调用 (没问题):
────生成────[暂停]──执行──[恢复]────生成────
可以随时截断再重新启动
推理模型 + 工具调用 (冲突!):
──思考──思考──思考──[???暂停???]──思考──思考──
思考链是连续整体, 中途打断=推理上下文全断
7.2 直觉类比:写推理过程中途被打断
想象你正在心无旁骛地写一篇复杂的推理论文,脑子里已经建立了一整套逻辑框架,各个论点之间的关系都串起来了,正处于思维最活跃的时刻。突然电话来了,你放下笔去接了 20 分钟电话,再回来坐下——很多细节想不起来了,之前好不容易建立的推理脉络断了,只能重新梳理。
推理模型的思考链就是这种东西。它是一个依赖完整上下文的连续生成过程,每一步推理都建立在前面所有推理内容的基础上。中途强行打断,之前建立的推理状态会断掉,模型没办法从中间接着想,只能整个重来。
7.3 “那保存状态再恢复不就行了?”
有人可能会问:暂停时把状态保存起来,工具执行完再恢复,不就解决了?
听起来可行,但实际代价很大。模型推理时的中间状态可以想象成一本"思考草稿本"——技术上叫 KV Cache,是模型缓存注意力计算结果的结构,体积非常庞大。一旦暂停,这本草稿本就得原封不动占着一块 GPU 显存,工具执行要几秒、几十秒,这块显存就一直被占着不能给别的请求用。工具执行完再接着想,显存占用翻倍、吞吐量直接腰斩,整体延迟也大幅上升。对一个需要同时服务成千上万请求的在线推理系统来说,这个代价完全承受不了。
更难解决的是一致性问题:思考过程中途接入工具结果,等于在模型"想到一半"时改变了输入。模型之前的思路是基于"我还不知道工具结果"建立的,突然工具结果来了,模型需要重新校准,之前的推理链和新来的工具结果可能是矛盾的。
7.4 训练目标上的冲突
除了生成范式,训练层面也有根本性冲突。推理模型的训练核心是:用强化学习大量奖励"思考链完整且结论正确"的输出,模型越来越倾向于"一直想、想到底"。而 Function Calling 的训练恰好相反,需要模型学会在合适时机打断自己,从推理状态切换出来输出结构化 JSON。
如果强行把两种训练数据混在一起喂,常见的失败模式有三种:
- 模型在思考到一半突然跳出来输出一段奇怪的 JSON,格式还错了,两边都没干好。
- 模型彻底偏向一边——要么思考链缩水变浅(变成普通模型),要么干脆不会输出工具调用。
- 融合处的推理断裂,工具结果回来之后模型的后续推理和之前的思考链对不上,答非所问。
所以早期推理模型宁可先放弃工具调用,也要先把推理能力做扎实。
7.5 折中方案:interleaved thinking
后续版本找到的工程解法是:让工具调用发生在思考阶段结束之后。
折中方案:
──思考思考思考思考──[思考结束]──[调工具]──[拿结果]──生成答案──
思考过程仍然一次性完整生成, 不被打断
工具调用发生在"答案生成阶段"
具体做法是:模型先把整个推理链完整跑完,进入"输出最终答案"阶段时,才触发 Function Calling 流程。这样思考过程仍然是一次性完整生成的,推理质量得以保住。代价是:思考阶段完全感知不到工具结果,模型只能基于自己的已有知识来推理。
o3 和 Claude Extended Thinking 走的都是这条路。Claude 还进一步推出了 interleaved thinking(交错思考) 模式,允许模型在多次工具调用之间穿插思考,而不是只能在所有思考结束后才调用工具,在一定程度上缓解了"思考阶段感知不到工具结果"的局限。
但内在限制的本质仍然存在,各家的解法都是在"保证推理质量"和"支持工具调用"之间做权衡。
7.6 为什么不支持 FC 就不支持 MCP
这个传导关系很直接:MCP 底层完全依赖 Function Calling。如果推理模型不支持 Function Calling,MCP 的"翻译层"(工具定义 → Function Calling 格式转换)就完全失效了,工具信息没办法让模型理解,调用请求也无法被模型生成,整条链路从中间直接断掉。
“推理模型不支持 Function Calling → 不支持 MCP"是一个很自然的传导关系,不是 MCP 本身有什么问题,是底层能力缺失导致的连锁反应。
本章实践要点
- 冲突的本质是生成范式:推理模型的思考链是连续整体,工具调用需要中途暂停,两者天然不兼容。
- KV Cache 是暂停的代价:保存推理状态要占大量 GPU 显存,在线系统承受不起。
- 折中方案的局限:让工具调用发生在思考结束后,保证了推理质量但牺牲了"带着工具结果深度推理"的能力。
- interleaved thinking 是缓解不是根治:允许在多次工具调用间穿插思考,但根本权衡仍在。
第八章:A2A 协议
8.1 单个 Agent 的天花板
要理解 A2A 是干什么的,得先把"单 Agent 的天花板"搞清楚。一个 Agent 的本质是:一个 LLM + 一组工具 + 一段上下文窗口,这三个维度都有自己的天花板:
- 工具数量限制:你不可能给一个 Agent 装 100 个工具,模型处理起来效率极低,容易混乱。
- 上下文窗口限制:128K tokens 听起来很多,但复杂任务积累的中间产物(搜索结果、草稿、反思记录)会很快把窗口塞满。
- 专业能力限制:同一个 Agent 既做代码审查又做市场分析,不如专门配置或微调的 Agent 效果好。
解决方案很自然:把任务拆开,交给不同的专业 Agent 并行处理,最后汇总。但多 Agent 系统有一个绕不开的基础问题——Agent 之间怎么互相认识?
8.2 Agent Card:能力声明与自动发现
最笨的方案是写死配置:Agent A 的代码里硬编码"B 可以做竞品分析”。这样太脆了,B 的能力一变,A 的代码就得改。
更好的方案是让 B 主动"发名片",声明自己能做什么——这就是 A2A 里 Agent Card 的设计思路。每个 A2A Agent 都在一个约定位置发布一张 JSON 格式的名片(推荐路径 /.well-known/agent-card.json),里面写清楚自己叫什么、能做哪类任务(Skill 列表)、支不支持流式返回、支不支持异步回调。
任何想和它协作的 Agent,先去拿这张名片,再决定要不要把任务委托给它。这套机制让整个多 Agent 系统变得可插拔:新加一个 Agent,发布它的 Agent Card,调度 Agent 就能自动发现和利用它,完全不需要改调度 Agent 的代码。
8.3 Task 状态机:异步长任务协作
A2A 里任务协作的基本单位是 Task,有完整的生命周期状态管理:
创建 开始执行
┌──────────→ submitted ──────────→ working
│ │
│ ┌─────┴─────┐
│ │ │
│ ▼ ▼
│ completed failed
│ (成功) (失败)
│
└─ 调度Agent可以轮询状态
或通过push notification等待回调
为什么需要这么完整的状态机?因为 A2A 专门为长时间任务设计。一个"竞品分析"任务可能要跑几分钟——先搜索、再整理、再写报告,不可能让调度 Agent 同步等着。调度 Agent 提交任务后可以去处理其他事情,通过轮询状态或者 push notification(任务完成时接收方主动回调通知)来得知任务完成了。
上下文隔离的核心收益:调度 Agent 把"做行业趋势分析"委托给市场 Agent,市场 Agent 自己去搜几十个网页、写草稿、反复迭代,这些中间过程都在它自己的上下文里。任务做完,它只把最终结论(一份几百字的摘要)通过 A2A 返回给调度 Agent。调度 Agent 的上下文里只多了一份摘要,而不是几十个网页的原文——调研过程的上下文压力被隔离在了市场 Agent 内部。
8.4 Agent 的微服务化
如果你有后端开发经验,A2A 其实不陌生:它就是 Agent 世界里的微服务架构。
| 微服务概念 | A2A 对应 |
|---|---|
| 服务独立部署 | Agent 独立部署为 HTTP 服务 |
| API 文档 | Agent Card |
| 异步消息队列 | Task 状态机 |
| 服务注册中心 | .well-known/agent-card.json |
| HTTP 互相调用 | Agent 间 A2A 通信 |
每个 A2A Agent 对外就是一个 HTTP 服务,任何支持 A2A 的系统都可以发现它、向它发任务、接收结果,不绑定特定的 AI 框架,也不依赖特定的编程语言。这个设计理念和 MCP 是一脉相承的:MCP 让工具成为独立标准化服务,A2A 让 Agent 成为独立标准化服务。
8.5 A2A 与 MCP:一纵一横,各管一层
理清两者关系最简单的方式是看方向:
┌─────────────────────────────────────┐
│ 调度 Agent (Agent A) │
│ │
│ ┌──A2A──→ 市场分析Agent (B) │ ← 横向:A2A
│ ├──A2A──→ 技术研究Agent (C) │ (Agent间协作)
│ ├──A2A──→ 报告撰写Agent (D) │
│ │ │
│ │ ┌──MCP──→ 数据库工具 │ ← 纵向:MCP
│ │ ├──MCP──→ 文件系统工具 │ (Agent连工具)
│ │ └──MCP──→ 代码执行器 │
└──┴───────────────────────────────────┘
- MCP 是 Agent 向下连工具(纵向):数据库、浏览器、代码执行器。
- A2A 是 Agent 向外连其他 Agent(横向):任务委派、结果接收。
打个比方,MCP 就像公司里每个员工的"工具箱",决定了这个人能用什么工具干活。A2A 就像公司里的"协作流程",决定了不同岗位的人怎么分工、怎么交接任务。工具箱和协作流程是两回事,缺了哪个都不行。在复杂的多 Agent 系统里,这两者通常同时在用。
本章实践要点
- A2A 不是 MCP 的竞品:MCP 向下连工具,A2A 向外连 Agent,方向完全不同。
- Agent Card 是自动发现的基础:新 Agent 发布名片即可被发现,调度 Agent 无需改代码。
- Task 状态机专为长任务设计:支持异步、轮询、push notification,调度 Agent 保持轻量。
- 本质是微服务架构:Agent 对外就是 HTTP 服务,可独立部署、跨框架、跨语言。
第九章:通信协议对比
9.1 从 HTTP 的本质说起
要理解各种通信协议的区别,得先回到一个根本问题:它们到底在解决什么问题?答案是普通 HTTP 做不到的事情。
标准 HTTP 是"一问一答"模型:客户端发请求,服务端返回响应,连接关闭。服务端在任何时候都不能主动"推"数据给客户端。但 AI 对话场景不行——模型生成一个完整回答需要几秒甚至十几秒,如果等全部生成完再一次性返回,用户只能干瞪着空白屏幕等待。我们需要的效果是:模型生成一个词就推一个词,用户实时看到文字逐渐出现。
9.2 SSE:用普通 HTTP 撑开一条单向水管
SSE(Server-Sent Events)本质上是对 HTTP 的一种"巧用",不是新协议,而是 HTTP/1.1 里本来就有的特性。客户端发一个普通 GET 请求,但在请求头声明 Accept: text/event-stream,服务端收到后不关闭连接,保持持续打开,不停往里写数据。这条连接在技术上仍然是一个 HTTP 响应,只不过响应体是"无限长"的。
可以把它理解为"一根从服务端流向客户端的单向水管",水只能从服务端流向客户端。
为什么 SSE 成为 LLM 流式输出的行业标准? 有一个关键但容易被忽视的原因:文字传输天然适合 TCP 的可靠有序传输。模型输出是连续文本,如果中间某个 token 丢了,整段话的意思可能完全变了。所以你希望每个 token 都准确到达、不乱序——这恰恰是 TCP 的强项,你愿意等网络重传,因为等来的是正确的内容。这和语音场景完全相反,那里 TCP 的重传会造成不可接受的延迟。
9.3 WebSocket:从 HTTP 升级成全双工信道
WebSocket 是一个独立协议,建立在 TCP 之上,但不是 HTTP 的特性。建立过程有一个特殊的"握手仪式":客户端先发一个看起来像普通 HTTP 请求的东西,但请求头带了"我想升级成 WebSocket"。服务端如果同意,回一个 101 Switching Protocols 响应,从这一刻起这条 TCP 连接就"变性"了,变成双方都可以随时说话的全双工信道。
和 SSE 最本质的区别是通信方向:SSE 只有服务端能主动推,客户端想发消息必须另起一个 HTTP 请求;WebSocket 是真正的双向,客户端和服务端都可以随时主动发消息。
9.4 WebRTC:为实时语音选择 UDP
语音场景和文字场景对网络的要求完全相反。人类大脑对语音时序极其敏感,超过 200ms 延迟就会明显感觉到"卡顿"。丢掉一个 20ms 的音频片段不是大事,人耳感知不到一小段静音;但为了等这 20ms 的片段重传,把后续所有音频都堵住,延迟积累到几百毫秒,体验就彻底崩了。语音容忍丢包,绝不容忍延迟,TCP 的设计哲学正好和这个需求相反。
WebRTC 把底层从 TCP 换成 UDP,丢包了不等重传,直接用"丢包隐藏(Packet Loss Concealment)“技术自动填补——用前后帧插值生成一段听起来合理的音频来替代,整体播放不中断,只是极短暂的音质轻微下降。延迟能控制在 50-150 毫秒。
WebRTC 还内置了回声消除(AEC)、噪声抑制(NS)、自动增益控制(AGC)、自适应码率(ABR)这些语音处理能力,这些用 WebSocket 全得自己造轮子。
9.5 四种传输方式对比
| 维度 | SSE | WebSocket | WebRTC | stdio |
|---|---|---|---|---|
| 底层协议 | HTTP/1.1(特性) | TCP(独立协议) | UDP(协议族) | OS 管道 |
| 通信方向 | 服务端单向推 | 全双工 | 全双工(P2P) | 双向(管道) |
| 延迟 | 低(TCP) | 50-500ms(受TCP重传影响) | 50-150ms(UDP不重传) | 极低(内存) |
| 连接数限制 | HTTP/1.1同域6条 | 无 | 无 | 无 |
| 二进制支持 | 否(需Base64膨胀33%) | 是 | 是(原生音视频) | 是 |
| 横向扩展 | 简单(无状态HTTP) | 麻烦(有状态需Redis) | 复杂(需信令服务器) | 不适用 |
| 代理穿透 | 好(普通HTTP) | 差(Upgrade易被拦) | 差(需ICE/STUN/TURN) | 不适用 |
| 适合场景 | LLM文字流式输出 | 双向实时交互 | 实时音视频 | MCP本地工具 |
9.6 选型决策
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| LLM 流式文字输出(ChatGPT 风格) | SSE | 单向推送够用,轻量,HTTP 原生支持 |
| 多轮对话(用户发消息 + 模型回复) | SSE + POST | 用户发消息走 POST,模型回复走 SSE |
| 需要用户中途打断模型输出 | WebSocket | 需要客户端在流式输出中途主动发消息 |
| 多人协同(实时同步) | WebSocket | 频繁双向消息 |
| 实时语音对话 | WebRTC | 音频流需要 UDP + 低延迟 |
| MCP 本地 Server | stdio | 不走网络、延迟极低、生命周期自动管理 |
| MCP 远程 Server | Streamable HTTP | 多 Client 共享、跨机器访问 |
大原则是:单向推用 SSE,真正需要双向才上 WebSocket,实时语音必须 WebRTC,本地进程间用 stdio。绝大多数 LLM 文字对话产品用 SSE 就够了,这也是 OpenAI、Anthropic 的 API 都选 SSE 而不是 WebSocket的原因。
本章实践要点
- SSE 不是 WebSocket 的子集:SSE 是 HTTP 原生特性,WebSocket 是独立协议,底层机制完全不同。
- SSE 的三个坑:HTTP/1.1 同域连接数上限(6条)、只支持文本(二进制需 Base64 膨胀 33%)、单向性导致双通道架构。
- WebSocket 的三个坑:有状态导致横向扩展麻烦、容易被企业代理/防火墙拦截 Upgrade 握手、没有内置请求-响应配对机制。
- WebRTC 建连仍需 WebSocket:WebSocket 负责信令交换(SDP),真正的音频流走 UDP,两者是配合不是替代。
- stdio 是 MCP 本地首选:不经过网卡、不需要端口、生命周期自动管理。
第十章:LLM 网关
10.1 网关是什么,放在哪
没有网关时,你的应用直接对接各个模型 API:应用 → OpenAI API、应用 → Anthropic API、应用 → 其他 API。有了网关之后,调用链变成:应用 → 网关 → OpenAI/Anthropic/其他 API。
网关就是一个"中间人”,坐在你的应用和各个模型 API 之间。你的应用只认识网关,不需要直接对接多个 API。这个"中间人"的定位是理解网关所有功能的基础——它集中拦截和处理了所有出入流量,所以能在这个位置统一做很多事情。
10.2 没有网关时的痛点
一个稍微大一点的 AI 产品,同时用多个模型是常态:主流程用 GPT-4o,成本敏感的任务用 GPT-4o-mini,代码任务用 Claude Sonnet,向量化用 text-embedding-3-small。每家厂商的 SDK 不一样、鉴权方式不一样、参数格式也有细微差异。如果不做网关,这些差异会渗透到每个业务服务里,带来一连串麻烦:
- 安全问题:API Key 散落在各个服务的配置文件里,任何一处泄露都是安全事故。
- 重复劳动:每个服务都要自己写重试和限流逻辑,版本还不一样。
- 成本黑箱:各服务各记各的,月底无法统计哪个业务线花了多少钱。
- 灵活性差:想换模型、想限制调用量都得改代码。
这些问题都源自同一个根本原因:没有一个集中的地方来统一管理这些"横切关注点"。
10.3 六大核心功能
功能一:多模型统一接口
大多数 LLM 网关对外暴露一个 OpenAI 兼容的接口。业务代码只需要改两个地方:把 base_url 指向网关地址,把 api_key 换成网关分配的虚拟 Key,其他代码一行不动。换模型只改网关路由配置,“换模型"这件事对业务层彻底隐形。
功能二:API Key 集中管理
API Key 只存在网关这一个地方,业务服务拿到的是网关分配的虚拟 Key,根本接触不到真实密钥。降低泄漏风险。
功能三:负载均衡和故障转移
给同一个"模型名"配置多条路由规则,主路由指向 OpenAI,备用路由指向 Azure OpenAI 或 Anthropic。主路由连续失败达到阈值时,网关自动切换到备用路由,业务层完全无感知。
功能四:限流和配额
给每个团队的虚拟 Key 设置独立的 token 日预算。一旦超出配额,该 Key 的请求直接返回 429,其他团队不受影响。
功能五:成本追踪和可观测性
网关集中记录每次调用的 token 用量、响应时间、错误率,可以回答"哪个接口最烧钱"“各团队用了多少"这类问题。通常可以直接对接 Langfuse、Prometheus 等监控系统。
功能六:语义缓存(LLM 网关的亮点)
这是 LLM 网关区别于普通 API 网关的关键能力。普通 HTTP 缓存是精确匹配,请求内容一字不差才能命中。但 LLM 的问题往往有大量语义相近的变体:“北京今天热吗"“北京现在天气怎样"“今天北京气温多少”——三个问题本质上是同一个需求,精确匹配全部 miss。
语义缓存的核心原理是:把用户问题转换成一个向量(embedding),然后在向量数据库里做相似度搜索,如果找到一个指纹很接近的历史问题(相似度超过设定阈值),就直接返回那次的答案,完全跳过 LLM 调用。
# 语义缓存的简化流程
def semantic_cache_lookup(user_question):
# 1. 把问题向量化
question_vector = embed(user_question)
# 2. 在向量数据库里做相似度搜索
similar = vector_db.search(
question_vector,
top_k=1,
threshold=0.90 # 相似度阈值
)
if similar and similar[0].score >= 0.90:
# 3. 命中缓存,直接返回历史答案
return similar[0].answer # 跳过 LLM 调用!
else:
# 4. 未命中,调 LLM 并缓存
answer = llm_call(user_question)
vector_db.insert(question_vector, answer)
return answer
语义缓存要用好,有两个工程细节需要注意:
- 相似度阈值怎么设:太高(0.99)只有一模一样的问题才命中,缓存形同虚设;太低(0.7)可能出现"北京今天热吗"命中了"上海今天热吗"的答案。通常在 0.85-0.95 之间调试。
- 缓存有效期:天气、股价这类实时信息缓存几分钟就够;产品 FAQ、技术文档这类稳定知识缓存几天甚至更长。
10.4 Prompt 安全过滤
在网关层可以统一做输入输出的安全校验,包括检测 prompt 注入攻击、过滤个人隐私信息(身份证号、手机号不应该被发送到外部 API)、内容安全审核。集中在网关做的好处是不需要每个业务服务各自实现,安全策略统一更新,任何一个接入点都自动获得保护。
10.5 常见网关框架对比
| 框架 | 类型 | 特点 |
|---|---|---|
| LiteLLM | 开源,Python | 支持 100+ 模型,OpenAI 兼容接口,社区最活跃(注意关注安全公告) |
| Bifrost | 开源,Rust | 高性能低延迟,2026 年新兴,适合对性能要求高的场景 |
| PortKey | 商业+开源 | 功能完整,有托管版,适合不想自运维的团队 |
| Kong AI Gateway | 商业(Kong 扩展) | 基于成熟的 Kong 网关扩展 AI 能力,适合已有 Kong 的团队 |
| One API | 开源,Go | 国内社区活跃,支持国产模型,部署简单 |
| Nginx/Envoy 自研 | 自研 | 灵活但工作量大,适合有特殊需求的大厂 |
本章实践要点
- 不是普通 API 网关:LLM 网关除了统一接口和负载均衡,更核心的价值是 token 配额、语义缓存、prompt 安全这些 LLM 特有能力。
- 语义缓存阈值要调:0.85-0.95 是常见范围,具体看场景和用户提问多样性。
- 虚拟 Key 隔离风险:业务服务接触不到真实密钥,降低泄漏风险。
- 故障转移保障可用性:多路由配置 + 自动切换,业务层无感知。
- 安全事件要关注:LiteLLM 在 2026 年 3 月发生过供应链安全事件,生产环境建议版本锁定和校验。
核心知识点回顾
关键认知
-
模型只决策不执行——这是整个工具调用体系的基石。模型输出结构化 JSON,代码负责执行。LLM 擅长理解意图和推理,但不应该有直接操作系统资源的权限。
-
SFT 教会怎么调,RLHF 教会什么时候调——两个阶段缺一不可。只有 SFT 模型会过激调用,只有 RLHF 模型连格式都输不出来。
-
MCP 底层靠 Function Calling 驱动——模型感知不到 MCP 的存在。MCP 是工具箱层,FC 是调用语言层,不是竞争关系。
-
三层架构缺一不可——Function Calling 是"手”(调用语言),MCP 是"厨房”(工具箱),Skill 是"菜谱”(操作手册)。Skill 定义流程 → MCP 提供工具 → Function Calling 触发调用。
-
推理模型与工具调用的冲突是范式性的——思考链是连续整体,工具调用需要中途暂停。折中方案让工具调用发生在思考结束后,interleaved thinking 部分缓解了局限。
-
A2A 和 MCP 一纵一横——MCP 向下连工具(纵向),A2A 向外连 Agent(横向)。复杂多 Agent 系统两者都要用。
-
通信协议选型看场景——文字用 SSE,双向实时用 WebSocket,语音用 WebRTC,本地进程间用 stdio。
-
LLM 网关的核心差异化是语义缓存——这是普通 API 网关做不到的,用向量相似度匹配语义相近问题,跳过 LLM 调用。
技术栈全景速查
╔══════════════════════════════════════════════════════════════╗
║ 用户 / 业务应用 ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ ┌─ A2A ──────────────────────────────────────────────────┐ ║
║ │ Agent间协作: Agent Card + Task状态机 + 微服务化 │ ║
║ └────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌─ Skill ────────────────────────────────────────────────┐ ║
║ │ 任务流程知识化: SKILL.md + scripts + 渐进式加载 │ ║
║ └────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌─ MCP ──────────────────────────────────────────────────┐ ║
║ │ 工具标准化: Host/Client/Server + Tools/Resources/Prompts│ ║
║ │ 传输: stdio(本地) / Streamable HTTP(远程) │ ║
║ │ 消息: JSON-RPC 2.0 │ ║
║ └────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌─ Function Calling ─────────────────────────────────────┐ ║
║ │ 调用语言: schema定义 + tool_calls JSON + 两轮对话 │ ║
║ │ 训练: SFT(怎么调) + RLHF/PPO(该不该调) │ ║
║ └────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌─ LLM 网关 (横切中间件) ────────────────────────────────┐ ║
║ │ 统一接口 + Key管理 + 负载均衡 + 配额 + 成本追踪 │ ║
║ │ + 语义缓存 + Prompt安全 │ ║
║ └────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌─ 通信协议 (横切基础设施) ───────────────────────────────┐ ║
║ │ SSE(文字流) / WebSocket(双向) / WebRTC(语音) / stdio(本地)│ ║
║ └────────────────────────────────────────────────────────┘ ║
║ ║
╠══════════════════════════════════════════════════════════════╣
║ 各家模型 API (OpenAI/Anthropic/...) ║
╚══════════════════════════════════════════════════════════════╝
主题关联
本文涉及的各主题之间有着紧密的逻辑递进关系:
- Function Calling 是整条技术栈的地基,没有它上层一切都无从谈起。
- 训练(SFT+RLHF) 解释了 Function Calling 能力从何而来,理解了训练才能理解为什么推理模型会冲突。
- MCP 在 Function Calling 之上解决工具管理问题,但本质还是靠 FC 驱动。
- Skill 在 MCP 之上解决流程知识问题,三层共同构成完整的 Agent 能力栈。
- 推理模型冲突 是 Function Calling 的一个"边界案例”,揭示了工具调用并非万能,有范式上的约束。
- A2A 把视角从单 Agent 扩展到多 Agent,和 MCP 形成"纵向+横向"的互补。
- 通信协议 是支撑以上所有机制的底层基础设施,选对协议直接影响系统性能。
- LLM 网关 是横切所有上层应用的中间件,解决工程化和治理问题。
进一步阅读
- Function Calling 官方文档:OpenAI Function Calling Guide、Anthropic Tool Use 文档
- MCP 规范:Anthropic Model Context Protocol 官方规范(2025-03-26 版本)
- Agent Skills:Anthropic Agent Skills 开放标准规范(2025 年 12 月发布)
- A2A 协议:Google Agent-to-Agent Protocol 规范
- 通信协议深入:MDN SSE/WebSocket 文档、WebRTC 官方文档
- LLM 网关:LiteLLM 文档、GPTCache(语义缓存实现)文档
系列导航:本文是「AI 知识体系深度解析」系列第三篇。前序篇章覆盖 LLM 基础架构与训练原理,后续篇章将深入 Agent 系统设计与多模态应用,敬请关注。