Agent 工程 · 第 5 章|工具设计与 MCP 协议:描述工程、协议详解、实现

第 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 要处理:

  1. 连接管理:为每个 server 维护一个 client 连接;启动时 discover 探测版本与能力;
  2. 工具聚合:把所有 server 的 tools/list 结果聚合进注册表(第 5.2 节的 registry 天然承接),按 ttlMs 缓存清单;
  3. 调用转发:模型的工具调用按名称路由到对应 server 的 tools/call,超时与错误要转换成第 5.1 节的"模型可读错误"格式;
  4. 隔离与信任分级:不同 server 是不同的信任域——第三方 server 的工具描述是不可信输入(description 可以被用来做注入,第 11 章展开),至少要在 UI 上区分内置工具与外部 server 工具,高危动作走审批。

实现作业

  1. 把第 2 章的三个笔记工具改造成 MCP server(上面给了骨架),并用任意 MCP 宿主(Claude Desktop / 自写 client)接入成功;
  2. 给你的最小 Agent 实现 MCPClient:聚合两个 server(自己的 notes + 一个第三方)的工具进 registry,模型无感地混合调用;
  3. 故意写一个返回 50k 文本的工具,观察上下文爆炸,然后按 5.1 纪律 3 修复它。

深入材料

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?