Agent开发 5.1 - 工具抽象与协议规范:Action Space、Schema 校验与 Anthropic MCP 协议

2026-10-27 | 96分钟 | yrobot | Agent开发, 工具抽象, Action Space, Schema校验, Zod, MCP协议, Anthropic MCP, JSON-RPC | 系列:Agent开发

在大模型具备了控制流编排、多智能体协同以及基于共享黑板的上下文状态治理能力之后,它依然只是一个局限在输入输出文本里的“纯计算模型”。要想让 Agent 真正解决真实世界的工程与业务问题,核心标志就是让它拥有调用外部环境接口的能力——通过工具(Tool)去读写数据库、请求三方 API、检索知识库与操作文件系统。

然而,在真实生产系统中,工具工程远不止“写几个 API 接口喂给大模型”那么简单。如果缺乏严密的架构设计,系统很快就会遭遇工具暴增导致的上下文膨胀、注意力稀释带来的决策失准、大模型输出参数畸变导致的下游崩溃,以及平台之间工具协议碎片化等一系列工程泥潭。

本文将从 3 个核心设计维度解构工具工程的技术谱系,剖析“工具全量堆叠”带来的生产灾难与“渐进式披露”的应对解法,构建基于 Zod 的运行时类型拦截防线,并深度剖析 Anthropic MCP(Model Context Protocol)协议的底层架构与通信机制。


工具工程的 3 个核心设计维度

在设计 Agent 的工具系统时,工程师通常需要在以下 3 个相互独立的核心维度上进行方案权衡与架构决策:

flowchart TD
    subgraph D1["维度一:行动空间暴露粒度 (Action Space)"]
        D1_1["自由代码执行 (Code-as-Action / REPL)\n直接给 Bash/Python 执行器,自由度极高但不可控"]
        D1_2["复合工具 (Monolithic Workflow Tools)\n将整套流程封为一个大函数,缺乏中间调整灵活性"]
        D1_3["原子化工具集 (Atomic Specialized Tools)\n单工具单一职责,便于复用与精准调试"]
        D1_4["渐进式暴露 (Progressive Disclosure / Skills 范式)\n极简元数据常驻,按需动态拉取 Schema 与操作说明"]
    end

    subgraph D2["维度二:接口契约与类型防御 (Contract & Validation)"]
        D2_1["弱约束自然语言描述 (Prompt-only Parsing)\n依赖文本正则匹配,字段漂移极易引发崩溃"]
        D2_2["模型端声明约束 (LLM Native JSON Schema)\n依赖模型遵循规范,但缺乏运行时物理拦截"]
        D2_3["运行时强类型拦截 (Runtime Schema Guard)\n本地确定性代码使用 Zod 校验,拦截脏数据并反馈自愈"]
    end

    subgraph D3["维度三:通信拓扑与协议标准 (Topology & Protocol)"]
        D3_1["进程内同构直接调用 (In-process Local Functions)\n单体代码无网络延迟,但无法跨语言、跨项目复用"]
        D3_2["专有 HTTP/RPC 网关 (Custom API Gateway)\n独立微服务,需维护大量定制客户端适配层"]
        D3_3["标准化开放协议 (Anthropic MCP)\n基于 JSON-RPC 2.0,统一 Host-Client-Server 三层接口规范"]
    end

1. 维度一:行动空间暴露粒度 —— 每一轮给模型开放多少选项?

行动空间指系统在当前推演微步(Step)中,提供给大模型所有可选工具动作的集合。

  • 自由代码执行(Code-as-Action):给模型一个代码执行环境(如沙箱中的 Bash 或 Python REPL),让模型现场编写脚本调用库函数。这种模式灵活性极高,但安全性风险极大,且模型容易在冷门语法或环境依赖上产生幻觉。
  • 复合工具:将复杂的端到端业务封装成单个大函数(例如 autoAnalyzeAndPurchaseStock())。模型决策路径极短,但缺乏细粒度的环境反馈,一旦中间步骤出现异常,模型无法动态纠偏。
  • 原子化工具集:遵循单一职责原则,每个工具只完成一项清晰的操作(如 queryAccount、createOrder、payOrder)。各工具职责正交不重叠,便于测试与复用。
  • 渐进式暴露(Agent Skills 范式):在系统提示词中仅常驻工具的轻量级元数据(名称与单句说明),当模型意图命中后,再按需动态加载完整的参数 Schema 与操作指南。

2. 维度二:接口契约与类型防御 —— 如何确保参数绝对合法?

大模型本质是基于概率生成的自然语言预测器,生成的 JSON 文本经常出现类型漂移、遗漏必填项或枚举越界。

  • 弱约束(纯 Prompt 约定):在提示词中用自然语言描述参数要求,依靠模型自觉和后置正则提取。这种方案在生产环境极其脆弱。
  • 模型端声明约束(原生 JSON Schema):在模型 API 调用层声明标准的 Function Calling 参数规范。模型会尽量对齐,但在代码层面若没有校验拦截,脏参数依然会击穿到下游服务。
  • 运行时强类型拦截(Runtime Schema Guard):在模型吐出参数后、派发给实际业务接口之前,本地确定性代码使用强类型工具库(如 TypeScript + Zod)执行校验。一旦失败,立即执行 Fail-Fast 拦截,并将错误信息转为观察结果(Observation)反馈给大模型重试。

3. 维度三:通信拓扑与协议标准 —— 宿主与工具如何连接与复用?

  • 进程内同构直接调用:工具就是宿主项目内的一个普通 TypeScript 函数。执行速度最快、零网络开销,但与宿主语言紧耦合,无法跨团队或跨平台共享。
  • 专有 HTTP/RPC 网关:工具部署为独立的后端微服务,宿主通过 HTTP 调用。每个工具都需要手写通信适配器与鉴权逻辑,维护成本随工具数量线性增长。
  • 标准化开放协议(Anthropic MCP):基于标准的 JSON-RPC 2.0 报文,解耦为 Host(宿主)、Client(客户端)与 Server(服务端)。工具被打包为独立的 MCP Server,任何支持 MCP 协议的客户端都可以自动发现并调用工具。

行动空间设计:为什么工具不是越多越好?

在早期 Agent 开发实践中,容易产生一个常见的工程误区:“既然 Agent 需要全能,就把后台现有的 40~50 个业务接口全封装成工具注册给模型。”

这种一股脑平铺所有工具的做法,在真实业务中会带来两个严重的工程瓶颈:

1. Token 线性膨胀与首字延迟黑洞

一个中等复杂度的生产工具,包含字段说明、嵌套对象、必填项约束与枚举说明,其 JSON Schema 通常占用 300 ~ 500 Token。 如果直接注册 40 个工具,每一轮系统提示词的工具描述开销就高达 12,000 ~ 20,000 Token。在一个包含 10 步推演的 ReAct 循环中,即便业务交互内容很短,光是每一步重复传输工具描述,就会白白消耗 15 万 ~ 20 万 Token,不仅推理账单成倍激增,首字生成延迟(TTFT)更会由毫秒级恶化为数秒。

2. 注意力稀释与工具选择漂移(Lost in the Tools)

大模型在海量选项中提取关联的能力是有限的。多项学术基准测试表明,当同时暴露的候选工具数量超过 15 ~ 20 个时,大模型匹配工具的准确率会出现陡峭下滑。特别是在存在相似语义工具时(例如 search_user_by_email 与 query_user_profile),模型极易陷入决策犹豫、死循环试探或误调工具。

flowchart LR
    subgraph S1["平铺暴露模式 (Naive Full Dump)"]
        T_All["40+ 个完整工具 Schema (15,000+ Token)\n每轮推演全量塞入上下文\n导致注意力分散与延迟飙升"]
    end

    subgraph S2["渐进式暴露模式 (Progressive Disclosure)"]
        L1["第 1 层:元数据常驻 (Metadata)\n仅加载工具名称与一句话描述 (~50 Token/个)"]
        L2["第 2 层:按需动态挂载 (On-demand Schema)\n意图命中时拉取完整 Schema 与使用指南"]
        L3["第 3 层:具体执行 (Execution)\n完成调用后释放或沉淀结果"]
        L1 -->|模型识别需要| L2 --> L3
    end

工业界的标准应对之道正是渐进式暴露(Progressive Disclosure)。平时常驻在系统上下文中的,仅有工具或技能的极简元数据(每个仅占几十 Token)。当大模型识别到用户意图涉及特定专业领域时,再通过元工具或特定分流机制,动态加载该领域专属工具的详细 Schema 与 SOP 操作指南。


接口契约与运行时类型防御(Runtime Schema Guard)

即便行动空间做了收敛,大模型输出的参数依然不能直接信任。

大模型生成 JSON 时最常见的四类故障:

  1. 语法碎裂:截断、漏反引号、结尾多打逗号导致 JSON.parse 抛错;
  2. 类型隐式漂移:数字 10 吐为字符串 "10",布尔值 true 吐为 "true";
  3. 枚举值幻觉:预定义状态为 ["ACTIVE", "PAUSED"],模型自由发挥吐出 "SUSPENDED";
  4. 脏字段入侵:凭空生成 Schema 中未定义的虚构参数。

如果将这些脏参数直接透传给后端系统或数据库,极易引发不可预料的运行时崩溃或数据污染。防御该问题的核心原则是:在工具调度层建立本地运行时类型拦截防线,实行 Fail-Fast 阻断,并将校验失败转化为结构化观察(Error Observation)反馈给模型自行修正。

以下为高信噪比的 TypeScript 强类型运行时校验守卫实现:

import { z } from "zod";

// 1. 定义工具业务参数契约 (Zod Schema)
export const TransferMoneySchema = z.object({
  recipient: z.string().min(2, "收款人姓名至少 2 个字符"),
  amount: z
    .number()
    .positive("转账金额必须大于 0")
    .max(10000, "单笔限额 10000"),
});

export type TransferMoneyInput = z.infer<typeof TransferMoneySchema>;

// 2. 运行时工具安全执行器
export async function executeToolSafely<T>(
  schema: z.ZodSchema<T>,
  rawArgsString: string,
  executor: (data: T) => Promise<string>
): Promise<{ isError: boolean; output: string }> {
  // 防线一:JSON 语法解析
  let rawJson: unknown;
  try {
    rawJson = JSON.parse(rawArgsString);
  } catch (err) {
    return {
      isError: true,
      output: `JSON 语法解析失败:${
        (err as Error).message
      },请输出合规 JSON 字符串。`,
    };
  }

  // 防线二:Zod 运行时强类型校验
  const validation = schema.safeParse(rawJson);
  if (!validation.success) {
    const errorDetails = validation.error.issues
      .map((issue) => `[${issue.path.join(".")}] ${issue.message}`)
      .join("; ");

    // 【关键】:本地拦截,不崩溃,将错误作为观察反馈给模型自愈
    return {
      isError: true,
      output: `参数契约校验失败:${errorDetails},请根据契约修正入参。`,
    };
  }

  // 防线三:派发真实业务执行
  try {
    const result = await executor(validation.data);
    return { isError: false, output: result };
  } catch (err) {
    return {
      isError: true,
      output: `工具内部执行失败:${(err as Error).message}`,
    };
  }
}

Anthropic MCP 协议深度解析

随着跨应用调用工具需求的增多,Anthropic 推出了开放的 Model Context Protocol(MCP),旨在解决不同模型、不同客户端与多样化外部服务之间的对接标准化问题。

1. 三层架构职责划分

MCP 将系统明确划分为三层角色:

  • Host(宿主环境):用户直接交互的运行环境,如 Claude Desktop、Cursor、VS Code 或自研的 Agent 微内核。Host 负责发起 LLM 推演,管理对话上下文。
  • Client(客户端):嵌入在 Host 内部的协议层实体。它负责与具体的 MCP Server 建立通信连接、管理生命周期、拉取可用能力清单并派发具体调用。
  • Server(服务端):独立运行的轻量级服务进程,专门封装特定外部系统(如 SQLite、GitHub、本地文件系统)的操作。
sequenceDiagram
    participant Host as Host (如 Agent 内核)
    participant Client as MCP Client
    participant Server as MCP Server (独立进程)

    Note over Host,Server: 1. 握手与能力协商
    Client->>Server: {"method": "initialize", "params": {...}}
    Server-->>Client: {"result": {"capabilities": {"tools": {...}}}}
    Client->>Server: {"method": "notifications/initialized"}

    Note over Host,Server: 2. 工具发现
    Client->>Server: {"method": "tools/list"}
    Server-->>Client: 返回所有工具的 Schema 清单

    Note over Host,Server: 3. 工具执行调用
    Host->>Client: 模型决定调用转账工具
    Client->>Server: {"method": "tools/call", "params": {"name": "transfer", "arguments": {...}}}
    Server-->>Client: {"result": {"content": [{"type": "text", "text": "转账成功"}]}}
    Client-->>Host: 工具调用结果作为 Observation 注入上下文

2. MCP 的三大核心原语

MCP 协议规范定义了三种维度的能力原语:

原语名称 语义类别 执行特征 典型应用场景
Tools (工具) 主动执行(带副作用) 由模型主动决策传参调用,返回计算或执行结果 执行数据库写入、发送邮件、调用外部 API
Resources (资源) 被动只读(上下文物料) 由客户端或模型通过标准 URI(如 file://、postgres://)读取,支持订阅变更 读取系统日志、加载表结构 Schema、加载代码文件
Prompts (提示模板) 引导指令(预设工作流) 由服务端导出的带参数提示词模板,供用户或模型展开特定任务 预设代码审查工作流、单元测试编写规范模板

3. 为什么本地 MCP 首选 stdio 传输通道?

MCP 支持通过 stdio(标准输入输出)和 HTTP with SSE 两种通道进行通信。在本地桌面或开发机环境下,官方推荐使用 stdio,其背后的工程决策依据包括:

  1. 生命周期自然绑定(杜绝僵尸进程):
    MCP Server 作为 Host 通过 child_process.spawn() 派生的子进程运行。一旦 Host 进程意外退出、崩溃或被强制杀死,子进程的 stdin 会立刻感知操作系统的 EOF/SIGPIPE 信号而随之退出,不会在后台残留任何占用内存的孤儿守护进程。
  2. 零端口冲突与高扩展性:
    若每个工具都启动一个本地 HTTP 端口(如 localhost:3000、8080),多工具并发时极易发生端口占用冲突。stdio 采用操作系统内核匿名管道,天然无端口概念。
  3. 本地沙箱与连接隔离:
    本地开放的 HTTP 端口可能被同机运行的其他流氓脚本跨站探测与请求。而 stdio 管道严格限定在父子进程上下文之间,具备天然的进程隔离安全性。

最小 MCP 协议与 Zod 校验调度实现

为了直观展现 MCP 协议底层基于 JSON-RPC 2.0 的报文交换机制,以下给出一个自包含的最小 MCP 交互与 Zod 参数防线的 TypeScript 原型:

import { z } from "zod";

// 1. 标准 JSON-RPC 2.0 报文协议类型定义
export interface JsonRpcRequest {
  jsonrpc: "2.0";
  id: string | number;
  method: string;
  params?: Record<string, unknown>;
}

export interface JsonRpcResponse {
  jsonrpc: "2.0";
  id: string | number;
  result?: unknown;
  error?: { code: number; message: string; data?: unknown };
}

// 2. 工具定义:绑定 Zod 校验规则与业务实现
const TransferInputSchema = z.object({
  recipient: z.string().min(2, "收款人姓名至少 2 个字符"),
  amount: z.number().positive("转账金额必须大于 0"),
});

export const transferMoneyTool = {
  name: "transfer_money",
  description: "执行资金转账操作",
  inputSchema: {
    type: "object",
    properties: {
      recipient: { type: "string", description: "收款人姓名" },
      amount: { type: "number", description: "转账金额" },
    },
    required: ["recipient", "amount"],
  },
  execute: async (rawArgs: unknown) => {
    // 本地 Zod 运行时强校验拦截
    const parsed = TransferInputSchema.safeParse(rawArgs);
    if (!parsed.success) {
      const issues = parsed.error.issues
        .map((i) => `${i.path.join(".")}: ${i.message}`)
        .join("; ");
      // 符合 MCP 规范:工具级业务校验错误返回 isError: true,绝不导致服务崩溃
      return {
        isError: true,
        content: [{ type: "text", text: `参数校验拦截:${issues}` }],
      };
    }

    const { recipient, amount } = parsed.data;
    return {
      isError: false,
      content: [{ type: "text", text: `成功向 ${recipient} 转账 ¥${amount}` }],
    };
  },
};

// 3. 极简 MCP Server 消息分发核心
export async function dispatchMcpMessage(
  request: JsonRpcRequest
): Promise<JsonRpcResponse> {
  // 处理工具发现能力:tools/list
  if (request.method === "tools/list") {
    return {
      jsonrpc: "2.0",
      id: request.id,
      result: {
        tools: [
          {
            name: transferMoneyTool.name,
            description: transferMoneyTool.description,
            inputSchema: transferMoneyTool.inputSchema,
          },
        ],
      },
    };
  }

  // 处理工具调用能力:tools/call
  if (request.method === "tools/call") {
    const { name, arguments: args } = (request.params || {}) as {
      name: string;
      arguments: unknown;
    };

    if (name === transferMoneyTool.name) {
      const executionResult = await transferMoneyTool.execute(args);
      return {
        jsonrpc: "2.0",
        id: request.id,
        result: executionResult,
      };
    }

    return {
      jsonrpc: "2.0",
      id: request.id,
      error: { code: -32601, message: `Tool not found: ${name}` },
    };
  }

  return {
    jsonrpc: "2.0",
    id: request.id,
    error: { code: -32600, message: "Invalid Request" },
  };
}

故障注入测试与自愈机制验证

通过模拟 Client 发出的两种调用场景,验证其防御与自愈特性:

// 场景一:模型输出畸变参数(金额传入负数)
const badRequest: JsonRpcRequest = {
  jsonrpc: "2.0",
  id: 1,
  method: "tools/call",
  params: {
    name: "transfer_money",
    arguments: { recipient: "张三", amount: -100 },
  },
};

const badResponse = await dispatchMcpMessage(badRequest);
// 输出结果:
// {
//   "jsonrpc": "2.0",
//   "id": 1,
//   "result": {
//     "isError": true,
//     "content": [{ "type": "text", "text": "参数校验拦截:amount: 转账金额必须大于 0" }]
//   }
// }

运行特征:进程未发生任何未捕获异常退出,错误信息清晰表达了违规字段与规则,模型在下一轮推演中看到此条 Observation 即可针对性自愈。

// 场景二:模型纠正参数重试
const goodRequest: JsonRpcRequest = {
  jsonrpc: "2.0",
  id: 2,
  method: "tools/call",
  params: {
    name: "transfer_money",
    arguments: { recipient: "张三", amount: 200 },
  },
};

const goodResponse = await dispatchMcpMessage(goodRequest);
// 输出结果:
// {
//   "jsonrpc": "2.0",
//   "id": 2,
//   "result": {
//     "isError": false,
//     "content": [{ "type": "text", "text": "成功向 张三 转账 ¥200" }]
//   }
// }

架构决策与选型指南

面对真实生产场景,工具工程的选型建议归纳如下:

业务场景 行动空间暴露模式 契约防御策略 通信协议选型
轻量对话 / 简单问答(工具数 < 5) 单体原子化平铺暴露 Zod 运行时基础校验 进程内单体直接调用(Zero Network Overhead)
中型业务系统(工具数 5~20) 原子化工具 + 领域简单分组 Zod 强校验 + 细粒度错误反馈自愈 进程内强类型 Server Actions 或单机 MCP
企业级复杂长程 Agent(工具数 > 20) 渐进式暴露(Skills 范式 + 动态元数据索引) 多层 Zod 拦截 + 业务审计门禁 独立部署 MCP Server 集群(stdio / SSE 混合)

工具工程绝不是简单编写几个三方 API 的 wrapper。只有在行动空间上做好分层收敛与渐进暴露、在接口调度上建立本地确定性的运行时类型防线,并在通信架构上遵循标准化的开放协议,才能构建出在长程复杂业务中高可靠、不崩溃、可自愈的工业级 Agent 系统。