第 5 章 · 工具设计与 MCP 协议
工具是 Agent 的手脚。这一章讲两件事:怎么设计工具(决定模型用得好不好),以及 MCP 协议(决定工具怎么跨进程、跨厂商连接)。
5.1 工具设计原则
第 2 章的最小 Agent 已经展示了工具的三件套(名称/描述/JSON Schema)。生产级工具设计有五条纪律:
1. 描述即接口文档。模型只看描述做决策。好的描述包含:做什么、什么时候用、什么时候不用、返回什么。
差:description: "搜索"
好:description: "在网络中搜索公开信息。当需要最新事件、外部事实、
或知识截止日期之后的信息时使用。不适合查询内部系统数据(用 query_db)。"
2. 错误是给模型看的,不是给用户看的。工具抛异常等于把异常信息丢给模型猜。正确的失败设计:
def read_file(path: str) -> str:
if not os.path.exists(path):
return json.dumps({
"error": "file_not_found",
"message": f"路径不存在:{path}",
"hint": "先用 list_files 查看目录结构确认文件名",
}, ensure_ascii=False)
结构化错误 + 可执行的下一步提示(hint),让模型能够自救。裸 traceback 或直接 raise 是新手最常见的失败设计——模型要么放弃要么胡猜。
3. 返回值尊重上下文预算(呼应 3.1)。工具结果直接进上下文,单次返回数万 token 是事故。纪律:列表类返回分页(返回前 20 条 + "共 N 条,用 offset 翻页");大文件先给摘要/行号目录;二进制内容返回路径而不是内容。
4. 幂等与副作用分级。读操作天然幂等可并行;写操作要考虑重复调用(模型重试是常态)——write_note 整体覆盖是幂等的,"追加一行"不是。有副作用的工具要在描述里声明副作用,供权限层(第 11 章)分级管控。
5. 粗粒度优于细粒度,但别做成万能工具。create_ticket(title, desc, priority) 比 http_request(method, url, body) 好(后者把 API 细节全部推给模型,错一个字段就是失败);但 50 个单字段的 CRUD 工具也会淹没模型——按"任务动词"设计(创建/查询/更新/派单),不按 REST 路由设计。
5.2 工具注册与发现
Agent 运行时需要一个工具注册表(registry)作为唯一事实源:
class ToolRegistry:
def __init__(self):
self._tools: dict[str, Tool] = {}
def register(self, impl: callable, schema: dict, *, danger: bool = False):
name = schema["function"]["name"]
self._tools[name] = Tool(impl=impl, schema=schema, danger=danger)
def schemas_for(self, agent: "AgentContext") -> list[dict]:
"""按上下文过滤:白名单、禁用状态、角色权限,决定模型能看见哪些工具"""
return [t.schema for t in self._tools.values()
if t.name not in agent.disabled_tools
and (agent.allow_dangerous or not t.danger)]
def execute(self, name: str, args: dict, ctx: "AgentContext") -> str:
tool = self._tools[name] # 不存在 → 明确报错
self._audit(ctx, name, args) # 审计先行
return tool.impl(**args)
可见性与执行能力是两件事:模型"看不见"的工具就不存在(不占 schema token,也不会被幻觉调用),而执行层仍有独立的门禁与审计——防止 prompt injection 通过间接渠道调用不可见工具(第 11 章)。
5.3 MCP 协议
MCP(Model Context Protocol)解决的问题是:工具生态的 M×N 问题。没有标准时,M 个 Agent 应用要适配 N 个工具源 = M×N 份胶水代码;有了 MCP,工具源只需实现一次协议,所有 MCP 客户端即插即用。2025 年 11 月开源、2026 年成为事实标准(Anthropic/OpenAI/Google/MS 均支持,服务器数过万)。
架构与消息
MCP 是 JSON-RPC 2.0 over 本地传输(stdio)或远程传输(Streamable HTTP)。角色:
MCP Host(Agent 应用)
└── MCP Client(Host 内每个 server 一个连接)
└── MCP Server(工具提供方,可以是进程/容器/远程服务)
└── 本地文件、数据库、SaaS API……
三类能力(primitives):
- tools:模型可调用的动作(与 function calling 同构,server 声明 schema,宿主转发调用);
- resources:可读取的数据(文件、配置),URI 寻址,
resources/read读取; - prompts:预置的提示词模板(服务端提供的"玩法")。
2026-07-28 版本的关键变化
这是 MCP 至今最大的一次修订,每个使用方都会受影响:
| 变化 | 内容 | 工程影响 |
|---|---|---|
| 无状态化 | 移除 initialize 握手与 Mcp-Session-Id;协议版本与客户端能力随每次请求的 _meta 携带 |
客户端不再维护连接会话;HTTP 层可任意负载均衡与重试;服务器可以水平扩展 |
server/discover |
服务器必须实现的能力通告 RPC | 启动时探测版本与能力,取代旧握手 |
| MRTR 多轮往返 | 服务器执行中途需要补充输入时返回 resultType: "input_required",客户端补参重发原请求 |
协议级的 Human-in-the-Loop 标准形态 |
| tasks 扩展 | 长任务移出核心协议,成为扩展(tasks/get 轮询) |
异步工具调用有标准模式 |
| Roots/Sampling/Logging 弃用 | 12 个月弃用窗口,官方给出迁移建议 | 依赖这些特性的集成需要排期替换 |
| 授权加固 | RFC 9207 iss 校验、凭据按 issuer 绑定、动态客户端注册弃用 | 企业接入的合规重点 |
| 清单缓存 | tools/list 等结果必须带 ttlMs/cacheScope |
客户端缓存工具清单,减少轮询 |
理解无状态化的动机:旧协议里会话状态挂在 HTTP 连接上,Agent 平台的多副本部署、重试、负载均衡都要做"粘性会话";无状态之后,每个请求自包含,基础设施回到标准的无状态 HTTP 世界。
实现一个 MCP Server
用官方 Python SDK 写一个最小但完整的 server(stdio 传输):
"""server.py — 一个笔记管理 MCP server"""
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("notes-server")
@mcp.tool()
def list_notes(dir_path: str = ".") -> str:
"""列出目录下的所有笔记文件名。"""
import os
return "\n".join(sorted(os.listdir(dir_path)))
@mcp.tool()
def read_note(path: str) -> str:
"""读取一个笔记文件的完整内容。路径必须是已存在的文件。"""
with open(path, encoding="utf-8") as f:
return f.read()
@mcp.tool()
def write_note(path: str, content: str) -> str:
"""把内容整体覆盖写入笔记文件。返回确认信息。"""
with open(path, "w", encoding="utf-8") as f:
f.write(content)
return f"已写入 {path}({len(content)} 字符)"
if __name__ == "__main__":
mcp.run(transport="stdio")
宿主侧的配置就是声明"如何拉起这个 server":
{"mcpServers": {
"notes": {"command": "python", "args": ["server.py"]}
}}
注意这份代码与第 2 章最小 Agent 里的工具一字不差——这就是 MCP 的价值:同一份工具实现,既能在你的 Agent 里进程内调用,也能作为标准 server 被任何 MCP 宿主使用。
Client 侧的职责
一个生产级 MCP client 要处理:
- 连接管理:为每个 server 维护一个 client 连接;启动时
discover探测版本与能力; - 工具聚合:把所有 server 的
tools/list结果聚合进注册表(第 5.2 节的 registry 天然承接),按ttlMs缓存清单; - 调用转发:模型的工具调用按名称路由到对应 server 的
tools/call,超时与错误要转换成第 5.1 节的"模型可读错误"格式; - 隔离与信任分级:不同 server 是不同的信任域——第三方 server 的工具描述是不可信输入(description 可以被用来做注入,第 11 章展开),至少要在 UI 上区分内置工具与外部 server 工具,高危动作走审批。
实现作业
- 把第 2 章的三个笔记工具改造成 MCP server(上面给了骨架),并用任意 MCP 宿主(Claude Desktop / 自写 client)接入成功;
- 给你的最小 Agent 实现
MCPClient:聚合两个 server(自己的 notes + 一个第三方)的工具进 registry,模型无感地混合调用; - 故意写一个返回 50k 文本的工具,观察上下文爆炸,然后按 5.1 纪律 3 修复它。
深入材料
- MCP 规范与 2026-07-28 changelog:modelcontextprotocol.io(本章节内容的原始依据)
- MCP 官方 Python/TypeScript SDK(github.com/modelcontextprotocol)
- OpenAI function calling 与 Anthropic tool use 文档(协议差异对照)
读者留言
COMMENTS 暂无还没有留言,来说第一句?