Agent Loop 中的工具调用与 MCP 协议详解
目录
1. MCP 协议概述
MCP(Model Context Protocol)是一个基于 JSON-RPC 2.0 的标准化协议,定义了 LLM 应用(Host/Client)与外部工具/数据源(Server)之间的通信方式。
1.1 核心角色
| 角色 | 职责 | 举例 |
|---|---|---|
| Host (Client) | 建立连接、路由工具调用、管理生命周期 | Claude Code、Cursor 等 |
| MCP Server | 提供工具定义、执行实际逻辑 | 文件系统、数据库、自定义 API |
| LLM 模型 | 基于工具定义决策是否调用、调用哪个 | Claude、GPT 等 |
1.2 架构概览
flowchart LR
Host["LLM Host\n(管理者)"] <-- "JSON-RPC 2.0\nstdio / HTTP+SSE" --> MCP["MCP Server\n(执行者)"]
Host -->|"LLM API 调用"| LLM["LLM 模型\n(推理引擎)"]
关键认知:LLM 模型本身不知道 MCP Server 的存在。Host 才是真正的”连接管理者”,LLM 只负责决策。
2. 传输层
MCP 支持两种传输方式:
2.1 stdio(本地场景,最常用)
Host 将 MCP Server 作为子进程启动,通过 stdin/stdout 传输 JSON-RPC 消息:
- 每条 JSON-RPC 消息占一行,
\n分隔 - stderr 保留给日志输出
- 适合本地开发、CLI 工具
2.2 HTTP + SSE(远程场景)
- Server 作为 HTTP 服务启动,暴露 SSE 端点
- Client 通过 HTTP POST 发送请求
- 通过 SSE 接收 Server 推送的响应
- 适合跨网络、跨进程的远程场景
3. 交互全流程
3.1 建立连接与初始化
连接建立后,Host 和 Server 进行能力交换:
Client 发送 initialize 请求
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": { "roots": { "listChanged": true } },
"clientInfo": { "name": "claude-code", "version": "1.0.0" }
}
}
Server 返回 initialize 响应
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true },
"prompts": { "listChanged": true }
},
"serverInfo": { "name": "filesystem", "version": "1.0.0" }
}
}
随后 Client 发送 notifications/initialized 通知,握手完成。
3.2 能力发现
Host 查询 Server 提供了哪些能力:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }
Server 响应:
{
"jsonrpc": "2.0", "id": 2,
"result": {
"tools": [
{
"name": "read_file",
"description": "读取文件内容",
"inputSchema": {
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "文件绝对路径" }
},
"required": ["file_path"]
}
}
]
}
}
4. 工具定义如何进入 LLM 上下文
这是理解 Agent Loop 的关键环节。
4.1 格式转换
MCP Server 返回的工具定义(inputSchema)本身就是 JSON Schema,与主流 LLM API 的 tool format 高度兼容,Host 只需做简单字段映射:
| MCP 格式 | Anthropic API 格式 | OpenAI API 格式 |
|---|---|---|
inputSchema | input_schema | function.parameters |
name | name | function.name |
description | description | function.description |
4.2 请求体结构
工具定义作为 顶层字段 传入,与 messages 平级:
{
"model": "claude-sonnet-4-6",
"max_tokens": 4096,
"tools": [
{
"name": "read_file",
"description": "读取文件内容",
"input_schema": { ... }
}
],
"messages": [
{ "role": "user", "content": "帮我读取 /tmp/test.txt" }
]
}
4.3 内部序列化
LLM 服务端收到请求后,在内部将 tools 序列化为系统级提示词的一部分,拼入上下文窗口:
flowchart LR
A["tools tokens\n工具定义"] --> D["统一 token 流"]
B["system prompt tokens\n角色设定"] --> D
C["message tokens\n用户消息和历史"] --> D
D --> E["Transformer 模型推理"]
最终整个 token 序列输入 Transformer 模型。
4.4 tools 与 messages 的关系
flowchart LR
subgraph Storage["客户端本地存储"]
T[("tools\n工具配置")]
M[("messages\n对话历史")]
end
subgraph Request["API 请求体"]
Req["tools + messages"]
end
subgraph LLM["LLM 上下文"]
Tokens["统一 token 流"]
end
T --> Req
M --> Req
Req --> Tokens
关键区别:
| 维度 | tools | messages |
|---|---|---|
| 持久化在对话历史里? | 否 | 是 |
| 每轮 API 调用都传? | 是(重新拼接) | 是(完整历史) |
| 能被动态修改? | 是(随时增减工具) | 否(已发送的消息不能改) |
| 上下文压缩时被截断? | 否(受保护) | 是(中间消息可能被压缩) |
tools 是”能力配置”,messages 是”对话记录”,两者在客户端分开管理,每次请求时临时组装。对话历史中永远只存消息本身,不包含工具 schema。
5. Agent Loop 完整流程
5.1 流程图
flowchart TD
A["用户输入"] --> B["Host 追加到 messages\n拼接 tools 与 messages"]
B --> C["POST /v1/messages\n发给 LLM API"]
C --> D["LLM 内部序列化\ntools 转 system tokens\nmessages 转 message tokens"]
D --> E["Transformer 推理"]
E --> F{"需要工具调用?"}
F -->|"否"| G["返回文本给用户"]
F -->|"是"| H["Host 解析 tool_use\n路由到对应 MCP Server"]
H --> I["JSON-RPC tools/call\nMCP Server 执行实际逻辑"]
I --> J["工具执行结果\ntext 或 image 或 resource"]
J --> K["Host 追加 tool_result\n到 messages"]
K --> L["下一轮迭代\n重新发送 tools 与 messages"]
L --> C
G --> UserEnd((用户))
classDef user fill:#e1f5fe,stroke:#01579b
classDef host fill:#fff3e0,stroke:#e65100
classDef llm fill:#fce4ec,stroke:#880e4f
classDef mcp fill:#e8f5e9,stroke:#1b5e20
class A,UserEnd user
class B,C,I,K,L host
class D,E,F,G llm
class H,J mcp
5.2 调用时序
sequenceDiagram
autonumber
participant U as 用户
participant H as Host (Client)
participant Msg as messages[]
participant T as tools[]
participant L as LLM API
participant R as MCP Router
participant S1 as MCP Server A
Note over H,L: 第 N 轮对话
U->>H: 用户输入
H->>Msg: 追加 role user 到 messages
H->>H: 组装请求体 tools 与 messages
Note over H,L: tools 是独立字段,不在 messages 中
H->>L: POST /v1/messages 携带 tools 与 messages
Note over L: 内部序列化 system tokens + tools tokens + messages tokens
L-->>H: 响应 文本 或 tool_use
alt 直接回复 无需工具
H->>Msg: 追加 role assistant 文本
H-->>U: 返回回答
else 需要调用工具
H->>Msg: 追加 role assistant tool_use
Note over H,R: Host 解析 tool_use 路由到对应 MCP Server
H->>R: JSON-RPC tools/call 携带 name 与 arguments
R->>S1: stdio 或 SSE 转发
S1-->>R: 执行结果 content text
R-->>H: JSON-RPC result
H->>Msg: 追加 role user tool_result
Note over H,L: 进入下一轮迭代
H->>L: POST /v1/messages 含 tool_result
L-->>H: 最终回答 或 继续 tool_use
H->>Msg: 追加 assistant 回复
H-->>U: 返回回答
end
5.3 多 Server 场景
一个 Host 可以同时连接多个 MCP Server:
flowchart TB
Host["Host Client\ntools 包含 read_file write_file query_db create_ticket"]
S1["MCP Server A\n文件系统\nread_file write_file"]
S2["MCP Server B\n数据库\nquery_db"]
S3["MCP Server C\n工单系统\ncreate_ticket"]
Host -->|"read_file write_file"| S1
Host -->|"query_db"| S2
Host -->|"create_ticket"| S3
当 LLM 返回 tool_use: read_file 时,Host 根据工具名称路由到对应的 MCP Server。
6. 关键约束与限制
| 约束 | 说明 |
|---|---|
| 工具数量上限 | Anthropic 限制最多 50 个工具,OpenAI 通常限制 128 个 |
| 消耗上下文窗口 | 工具定义会消耗 token 额度,工具越多、schema 越复杂,占用越多 |
| 每轮必传 | 多轮对话中,tools 字段必须在每次 API 调用时都带上 |
| 影响延迟 | 工具定义越多,输入 token 越多,首 token 延迟越高 |
7. 总结
MCP 本质上是 LLM 与外部世界之间的”USB 接口”:
- Host 从 MCP Server 获取工具 schema,转换为 LLM API 格式
- 工具定义作为顶层字段随每次 API 请求传入,不在对话历史中
- LLM 服务端将工具定义序列化为系统级提示词,拼入上下文窗口
- LLM 基于工具定义决策是否调用、调用哪个
- Host 通过 JSON-RPC 将调用请求路由到对应 MCP Server 执行
- 执行结果追加到对话历史,触发下一轮迭代
整个循环中,工具定义与对话历史是分开管理、临时组装的。这保证了工具列表不会因为对话变长而被压缩或截断。