跳到主要内容《Agent Runtime 工程化》第六章 MCP 与 Provider 抽象 | 极客日志编程语言Agent Runtime工程化MCP陈堂会
《Agent Runtime 工程化》第六章 MCP 与 Provider 抽象
Agent 迟早要接外部工具,也迟早要面对多个模型供应商。本章讨论怎么接,以及怎么避免 runtime 被某个协议或某个 SDK 绑死。 前面几章里,工具都在本地注册,模型调用也被简化成一个 ModelProvider。这对入门很好,但真实系统不会停在这里。企业内部有文档库、浏览器、数据库、工单系统、设计工具、代码托管平台;模型侧也会同时接 OpenAI、A
站点编辑3 浏览

书名:《Agent Runtime 工程化:从工具调用循环到可恢复执行系统》
作者:陈堂会
平台:极客日志(https://zeeklog.com)
X:@Megick_com
联系邮箱:[email protected]
章节:第六章 MCP 与 Provider 抽象
第六章 MCP 与 Provider 抽象
Agent 迟早要接外部工具,也迟早要面对多个模型供应商。本章讨论怎么接,以及怎么避免 runtime 被某个协议或某个 SDK 绑死。
前面几章里,工具都在本地注册,模型调用也被简化成一个 ModelProvider。这对入门很好,但真实系统不会停在这里。企业内部有文档库、浏览器、数据库、工单系统、设计工具、代码托管平台;模型侧也会同时接 OpenAI、Anthropic、本地模型、云厂商模型和 mock model。Runtime 如果没有抽象层,很快就会变成一堆分支判断。
MCP 和 Provider Adapter 解决的是两个不同方向的问题。MCP 让外部工具和资源更容易被发现、描述和调用;Provider Adapter 让模型调用进入统一接口。二者都不是 Agent Runtime 的替代品。权限、上下文预算、trace、checkpoint、eval,仍然是 runtime 的责任。

图 6-1:Agent Runtime 位于模型 Provider 与 MCP 外部工具之间。Provider Adapter 统一模型调用;MCP Client 连接外部 Server;Tool Registry、权限、Trace 和 Checkpoint 仍由 runtime 控制。
6.1 MCP 在 runtime 里的位置
截至 2026 年 8 月 12 日,MCP 规范把通信角色分为 Host、Client、Server。Host 是发起连接的 LLM 应用;Client 是 Host 内部与某个 Server 通信的连接器;Server 提供上下文和能力。协议使用 JSON-RPC 2.0。Server 可以提供 Resources、Prompts、Tools,Client 侧还可以提供 Elicitation。
这几个名词容易让人误会。MCP Server 不是 Agent,MCP Client 也不是 runtime。Server 暴露能力,Client 负责通信,Host 里的 Agent Runtime 决定这些能力什么时候进入模型上下文、什么时候被调用、调用前是否需要审批、调用结果怎样写回 trace。
可以这样理解:
User
↓
Agent Runtime
├─ Context Builder
├─ Tool Registry
├─ Permission Gate
├─ Trace / Checkpoint
├─ Model Provider Adapter
└─ MCP Client Pool
├─ MCP Server: filesystem tools
├─ MCP Server: issue tracker resources
└─ MCP Server: design tool prompts/tools
MCP 解决'外部能力如何标准化连接'。Runtime 解决'这些能力如何被安全、可控、可恢复地使用'。这条边界要先想清楚。
6.2 tools/list 与 tools/call 的工程含义
MCP Tools 规范里,Client 通过 tools/list 发现工具,通过 tools/call 调用工具。工具通常包含名称、描述和输入 schema。工具列表可以为空,也可以随时间变化;当工具列表变化时,Server 可以通知 Client。
对 runtime 来说,这意味着工具注册从'启动时写死'变成'运行时发现'。随之而来的问题比接通协议本身更重要:
| 问题 | 风险 | Runtime 应对 |
|---|
| 工具列表变化 | 模型上下文里暴露了过期工具 | 加版本和刷新机制 |
| schema 变化 | 模型按旧参数调用新工具 | 校验失败转 observation |
| 名称冲突 | 两个 Server 都叫 search | 本地命名空间隔离 |
| 描述不可信 | 工具自称 safe 但可能写入 | 本地风险分类覆盖远端描述 |
| 结果过大 | 工具输出挤爆上下文 | 输出策略和 artifact 引用 |
| Server 授权变化 | 调用时才发现无权限 | 连接状态和失败分类进入 trace |
MCP 规范也强调工具安全:工具可能代表任意代码执行能力,Host 应让用户能理解和授权工具调用。写 runtime 时,不能把这句话当成文档里的礼貌提醒。只要一个 MCP Server 能读写本地文件或访问企业系统,它就进入了真实安全边界。
6.3 把 MCP 工具接入 Tool Registry
一个稳妥做法是:不要让模型直接面对'裸 MCP 工具'。Runtime 应把 MCP 工具包装成本地统一的 ToolDefinition,再交给前面第四章的工具治理管线。
type McpServerTrust = "trusted_local" | "team_audited" | "third_party" | "unknown";
function fromMcpTool(server: McpServerInfo, tool: McpTool): ToolDefinition {
return {
name: `mcp.${server.id}.${tool.name}`,
title: tool.title ?? tool.name,
description: tool.description ?? "",
version: `mcp:${server.version ?? "dynamic"}`,
inputSchema: tool.inputSchema,
riskLevel: classifyMcpRisk(server, tool),
sideEffects: inferMcpSideEffects(server, tool),
resources: [{ kind: "mcp", scope: server.id }],
timeoutMs: server.defaultTimeoutMs ?? 30_000,
outputPolicy: {
maxCharsForModel: 12_000,
summarizeWhenLarge: true,
redactSecrets: true,
},
async execute(input, ctx) {
return ctx.mcp.callTool(server.id, tool.name, input);
},
};
}
命名空间非常重要。不要让两个 Server 的 search 工具互相覆盖。建议把本地名称写成 mcp.<serverId>.<toolName>,给模型展示时可以用更友好的 title,但 trace 和 policy 中保留完整名字。
也不要直接相信工具描述里的'read only''safe'。风险等级应由本地策略决定,至少参考五类信号:
- Server 信任等级;
- 工具名称和描述;
- 输入 schema 中是否出现 path、command、url、query、mutation 等字段;
- Server 连接方式是本地还是远程;
- 用户或组织策略给这个 Server 的授权范围。
MCP 的好处是工具发现标准化了,不是安全自动解决了。工具越容易接入,runtime 越要有统一治理。
6.4 工具列表刷新与上下文暴露
动态工具发现带来一个新问题:什么时候把哪些工具暴露给模型?
如果把所有 MCP 工具都塞进模型上下文,token 会浪费,误调用概率也会上升。一个企业环境里可能有几十个 MCP Server、几百个工具。模型不需要每一步都知道所有能力。
type ToolExposure = {
allDiscovered: ToolDefinition[];
enabledForRun: ToolDefinition[];
exposedThisStep: ToolDefinition[];
};
allDiscovered 是当前连接发现的全部工具。enabledForRun 是当前用户、workspace 和 policy 允许使用的工具。exposedThisStep 是本轮模型真正看到的工具,应该由当前任务意图、工具相关性和上下文预算决定。
| 事件 | 行为 |
|---|
| Server 连接建立 | 调用 tools/list,记录工具版本摘要 |
| Server 工具列表变化 | 刷新 registry,写入 trace event |
调用遇到 UNKNOWN_TOOL | 刷新一次,再决定是否反馈模型 |
| schema hash 改变 | 清理旧工具缓存,eval/replay 标记不可直接复现 |
| Server 断开 | 禁用相关工具,并给模型 observation |
工具列表本身也要进入 trace。否则任务失败时,你不知道模型当时到底看见了哪些 MCP 工具。
6.5 Provider Adapter 统一模型调用
模型供应商的工具调用接口并不完全一致。不同 provider 对 message、tool schema、stream event、usage、finish reason、并行 tool calls、strict schema、response continuation 的支持都有差异。Runtime 应有自己的 provider adapter。
interface ModelProvider {
name: string;
capabilities: ModelCapabilities;
generate(input: ModelRequest): AsyncIterable<ModelEvent>;
}
type ModelRequest = {
messages: RuntimeMessage[];
tools: RuntimeToolSchema[];
toolChoice?: "auto" | "none" | { name: string };
maxOutputTokens?: number;
signal: AbortSignal;
};
type ModelEvent =
| { type: "text_delta"; text: string }
| { type: "tool_call_delta"; callId: string; name?: string; inputDelta?: string }
| { type: "tool_call_done"; call: ToolCall }
| { type: "usage"; usage: TokenUsage }
| { type: "done"; finishReason: string };
这个接口的目标不是假装所有模型一样。相反,它要把差异显式化:
type ModelCapabilities = {
toolCalling: boolean;
strictToolSchema: boolean;
parallelToolCalls: boolean;
streaming: boolean;
responseContinuation: boolean;
maxContextTokens: number;
};
Provider Adapter 把 provider 原生事件转成 runtime 事件,同时把 provider 特性保留在 metadata 中。这样做有两个好处:主循环不用关心每个 SDK 的字段名;eval 和 trace 又能看到影响行为的 provider 差异。
6.6 不要用降级绕过安全
模型调用失败时,runtime 可以重试或降级;但要分清失败类型。
| 类型 | 处理 |
|---|
RATE_LIMIT | 指数退避、排队或换同策略模型 |
TRANSIENT_NETWORK | 短重试,保留相同请求语义 |
CONTEXT_TOO_LARGE | 调用 Context Builder 降级 |
UNSUPPORTED_TOOL_SCHEMA | schema 简化或禁用 strict |
INVALID_TOOL_CALL | observation 反馈模型 |
POLICY_BLOCKED | 停止或请求用户改授权 |
有一条底线:安全策略拒绝,不能通过换 provider 绕过去。比如某个模型因为 policy 不允许执行网络工具,runtime 不应自动换一个没有这个限制的模型继续执行。同样,用户拒绝了 MCP 工具调用,恢复执行时也不能因为 provider 重试而重新发起同一个高风险调用。
6.7 限流、重试与幂等
Provider 调用和 MCP 工具调用都可能失败,但重试策略不同。
模型请求通常是无外部副作用的,可以在网络错误或 5xx 时短重试。MCP 工具调用不一定幂等。一个 read_project_file 可以重试;一个 create_issue、send_message、charge_customer 不能自动重试。
type RetryContext = {
operation: "model_generate" | "mcp_tool_call" | "local_tool_call";
idempotency: "idempotent" | "conditional" | "non_idempotent";
attempt: number;
maxAttempts: number;
lastErrorCode?: string;
};
function shouldRetry(ctx: RetryContext) {
if (ctx.idempotency === "non_idempotent") return false;
if (ctx.attempt >= ctx.maxAttempts) return false;
return ["RATE_LIMIT", "TRANSIENT_NETWORK", "SERVER_ERROR"].includes(ctx.lastErrorCode ?? "");
}
这段逻辑会在第八章继续展开。现在先记住:MCP 接入后,工具是否可重放必须成为工具定义和 trace 的一部分。
6.8 Mock Provider 是 eval 的地基
不要等接好真实模型后才测试 Agent Loop。Provider Adapter 的价值之一,是让你可以用 mock provider 做确定性测试。
一个 mock provider 可以按脚本返回 tool call:
class ScriptedProvider implements ModelProvider {
name = "scripted";
capabilities = {
toolCalling: true,
strictToolSchema: true,
parallelToolCalls: false,
streaming: false,
responseContinuation: false,
maxContextTokens: 64_000,
};
constructor(private script: ModelResponse[]) {}
async *generate(): AsyncIterable<ModelEvent> {
const next = this.script.shift();
if (!next) throw new Error("Script exhausted");
if (next.kind === "tool_calls") {
for (const call of next.calls) {
yield { type: "tool_call_done", call };
}
} else {
yield { type: "text_delta", text: next.text };
}
yield { type: "done", finishReason: "stop" };
}
}
用 mock provider 可以稳定覆盖:非法工具名、非法参数、重复错误、并行 tool calls、无工具 final answer、上下文超限后的重建。等这些路径稳定了,再接真实模型。
6.9 本章的实现路径
第一步,把本地 ModelProvider 接口稳定下来,支持流式事件、usage、finish reason、capabilities 和 AbortSignal。
第二步,实现一个真实 provider adapter,再实现一个 scripted mock provider。所有 runtime 单元测试优先跑 mock provider。
第三步,用 MCP TypeScript SDK 写一个本地 MCP Server,提供 read_project_file 和 search_project_text 两个只读工具。
第四步,把 MCP 工具包装进本地 ToolDefinition,加命名空间、风险分类、输出策略和超时。
第五步,加入工具列表刷新机制。每次发现、变更、断开都写入 trace。
第六步,准备限流、上下文超限、schema 不支持、MCP Server 断开四类失败用例,验证 runtime 能降级或停止。
6.10 验收标准
完成本章后,你的 Agent 应能动态接入和移除 MCP 工具;MCP 工具仍然经过本地 Tool Registry 和 Permission Gate;模型供应商替换不影响主循环;schema 改变不会把 runtime 弄崩;mock provider 能复现核心 tool call 路径。
| 用例 | 期望结果 |
|---|
| MCP Server 提供两个只读工具 | runtime 发现工具并加 mcp.<server>.<tool> 命名空间 |
| MCP 工具 schema 改变 | registry 刷新,旧调用校验失败并转 observation |
| 第三方 MCP 工具请求写入 | 默认需要确认或拒绝 |
| Provider 不支持 strict schema | adapter 降级 schema,并在 trace 标记 |
| mock provider 返回未知工具 | runtime 返回 UNKNOWN_TOOL,不崩溃 |
| 模型请求上下文超限 | Context Builder 降级后重试一次 |
到这里,Agent Runtime 已经能接入生态,但控制权仍在自己手里。这一点很重要。协议和供应商负责提供能力,runtime 负责把能力变成可治理的工程系统。
系列文章目录
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online