这是一个在每一个发布 Agent 的团队中都会重复发生的迁移故事。你基于某个供应商的 SDK 构建了你的 Agent。工具定义以该供应商预期的 JSON Schema 形式存在。工具结果被格式化为该供应商的消息结构。接着,某些原因迫使你做出改变 —— 竞争对手发布了更好的模型,采购部门为了冗余要求引入第二个供应商,或者一个 MCP 服务器取代了手写的集成 —— 然后你发现迁移的真正工作量:它不仅仅是一个 API 客户端。它是代码库中的每一个工具定义、每一个结果格式化器、每一个重试处理器以及每一个测试固件。
失败的原因并不是你选错了供应商。而是你让别人的序列化格式变成了你的内部架构。针对这个问题,早在二十年前就有了一个答案 —— Alistair Cockburn 的六边形架构,更广为人知的名字是“端口与适配器” —— 而 Agent 系统是它多年来最引人注目的新用例。
工具层才是锁定的真正所在
当工程师担心 LLM 的供应商锁定(Vendor Lock-in)时,他们通常想到的是 Prompt。Prompt 确实会偏向某个模型的惯用表达,但它们只是字符串 —— 重新调整虽然烦人,但隔离成本很低。工具层则本质上不同。一个生产环境的 Agent 可能会暴露几十个工具,而每一个工具至少在四个地方触及供应商特定的格式:
- Schema 声明 —— 工具的名称、描述和参数如何被序列化到请求中。
- 调用解析 —— 模型决定调用工具时,如何返回(一个专门的消息角色、一个内容块、或者一个流式增量)。
- 结果格式化 —— 你在下一轮对话中如何将工具的输出交还给模型。
- 错误与重试语义 —— 当模型生成了格式错误的参数或工具执行失败时会发生什么。
这些格式确实存在差异,而且不仅仅是表面上的。OpenAI 通过专门的 tool 角色返回工具结果;Anthropic 将它们作为结构化内容块映射到用户消息中。Anthropic 要求系统 Prompt 作为顶级字段,而 OpenAI 则在消息数组中接受它们。Google 的 API 会拒绝带有未类型化数组项的 JSON Schema,而这些在其他地方完全有效,并且没有主流供应商接受顶级的 $ref。
这些怪癖比序列化更深入。如果你在启用扩展思考(Extended Thinking)时强制使用工具,Anthropic 会报错。而流式处理(Streaming)比这些都要糟糕:一个供应商发出结构化的多阶段事件,另一个发出生成片段(Completion Chunks),在两者之间转换需要在流的过程中维护状态。
这些差异如果只处理一次并不难。问题在于团队在哪里处理它们。当供应商的格式就是你编写工具的格式时,每一个怪癖都要在每一个工具定义处处理,你在迁移过程中必须修改的地方的数量随工具数量而扩展,而不是随供应商数量而扩展。
端口:你拥有的 Schema
六边形架构的核心举措是将依赖箭头指向内部。应用核心定义了端口(Ports) —— 用领域自身的词汇表达的稳定接口 —— 而混乱的外部世界通过实现这些端口的**适配器(Adapters)**进行连接。核心从不导入适配器;而是适配器导入核心。
应用到 Agent 上,端口就是你的工具契约:一个名称、一个人类和模型均可读的描述、一个参数 Schema、一个结果类型和错误语义 —— 编写一次,采用你版本化并拥有的中立表示。既不是 “OpenAI 函数调用格式”,也不是 “MCP 服务器发布的格式”,即使你的中立格式碰巧看起来与其中之一相似。区别不在于语法,而在于所有权。当你拥有契约时,供应商改变其序列化方式只是一个适配器 Bug。当供应商拥有契约时,同样的改变就是一个波及整个代码库的事件。
基于这个单一契约,薄薄的适配器在两个方向上进行机械转换:
- 供应商适配器 (Provider Adapter) 将你的工具契约序列化为每个厂商的请求格式,并从每个厂商的响应格式中解析出工具调用决策 —— 包括流式变体。
- 后端适配器 (Backend Adapter) 将契约的执行侧连接到实际执行工作的任何地方:一个本地函数、一个内部 REST 服务、一个 MCP 服务器、或者一个队列。
这与 LLM 网关(Gateway)的形式相同,网关是购买现成的供应商适配器部分的好方法。但网关只规范了面向模型的一侧。后端一侧 —— “search_orders” 今天是一个 Postgres 查询,下个季度是一个由另一个团队拥有的 MCP 服务器 —— 这是你的领域,没有任何供应商产品会为你定义这些契约。
真正重要的边界不是 Agent 与工具之间
大多数 Agent 框架图只画了一个边界:一侧是 Agent,另一侧是工具。那个边界是真实的,但它不是最让人痛苦的。昂贵的边界存在于你的语义与其他人的序列化之间 —— 并且它横穿了图表的两侧。
在模型侧,你的语义是“Agent 可以根据客户和日期范围搜索订单”。序列化则是该意图是以带有嵌套 function 对象的 tools 数组形式传输,还是作为 input_schema 块传输。在执行侧,你的语义是“搜索订单最多返回 50 条带有人类可读状态字段的结果”。序列化则是它通过 HTTP 运行,还是通过 MCP 的 tools/call 往返,或者是进程内函数运行。
这种重构解释了从业者不断重新发现的一个模式:研究文献将现状称为“协议碎片化” —— OpenAPI、MCP、原生函数调用和框架特定格式共存,没有统一标准。一项对统一工具集成层的研究衡量出,仅仅通过在工具定义和消费它们的协议之间放置一个单一注册表,就能减少 60–80% 的代码。代码减少是真实的,但更深层的胜利不在于行数变少 —— 而在于剩下的行各司其职,只因唯一的原因而改变。当你的领域改变时,工具契约会改变。当供应商改变时,适配器会改变。两者互不干扰。
这也澄清了 MCP 是什么以及不是什么。MCP 标准化了 Agent 宿主与工具服务器之间的连线 —— 它是一个适配器层协议,而且是一个优秀的协议。但采用 MCP 并不免除你拥有契约的责任。一个 MCP 服务器发布的 Schema 仍然是他人的序列化;如果你将 Agent 的行为直接与其绑定,该服务器的下一个破坏性变更将像供应商 SDK 变更一样传播。端口位于 MCP 之上,而不是其内部。
Mock 适配器是每天都能感受到的回报
迁移保险是让这种模式获得批准的理由,而日常的回报则是测试。
Agent 系统因难以测试而臭名昭著,因为两类非确定性来源在此叠加:模型的选择和工具的实时后端。你无法完全消除前者 —— 那正是产品本身。但团队通常会无缘无故地接受后者,在实时数据库和第三方 API 上运行 Agent 评估(evals),然后纳闷为什么同样的评估在周二的分数会不一样。
只要有了端口(ports),Mock 适配器几乎是免费的:它实现与生产环境相同的工具契约,并提供受控且确定性的数据。仅此一个组件就能解锁三种不同的测试模式:
- 确定性的 Agent 评估。 对 Mock 工具运行真实模型。现在,每一次分数变化都可以归因于提示词(prompt)、模型或工具描述 —— 而不是预发布数据库所处的任何状态。
- 故障注入。 根据需要让 Mock 返回超时、空结果、格式错误的负载和权限错误。Agent 如何从工具故障中恢复是你应该评估的行为,而这在实时后端上几乎不可能可靠地进行演练。
- 适配器的契约测试。 对 Mock 和每个真实的后端适配器运行同一套测试。差异意味着适配器违反了契约 —— 这种问题能在 CI 中被捕捉到,而不是三周后在 Agent 的对话记录中被发现。
对工具质量本身还有一个更微妙的好处。Anthropic 关于为 Agent 编写工具的指南强调,描述性的措辞会显著影响任务完成率,而改进工具是一个评估驱动的循环。只有当评估成本低且可复现时,这个循环才是紧密的。拥有 Mock 适配器的团队像迭代代码一样迭代工具描述;没有它们的团队则靠感觉(vibes)迭代。
“薄”意味着什么,以及在哪里停止
任何抽象层最经典的失效模式就是它不再“薄”了。两条规则可以保持这种模式的纯粹。
适配器只负责翻译,不负责决策。 适配器将一种序列化映射到另一种。一旦适配器包含业务逻辑 —— 过滤结果、选择对业务领域至关重要的默认值、决定每个工具的重试策略 —— 逻辑就脱离了核心,并针对每个供应商重复存在。应该将其上推到契约层,在那里只编写一次。
标准化交集,有意识地暴露差异。 如果不牺牲每个供应商的价值(如:深度思考、显式的提示词缓存控制、特定于供应商的结构化输出模式),你就无法将它们扁平化为纯粹的可互换性。假装可以做到这一点会产生一个“最小公分母”层,团队会绕过它,这就是抽象消亡的方式。可行的立场是:端口涵盖了每个供应商必须支持的语义,而特定于供应商的能力则作为显式的、命名的扩展(extensions)暴露出来 —— 它们在代码审查中可见,在下次迁移时可被搜索到,绝不会在暗处承担关键负载。
还要知道什么时候根本不需要构建这个。一个只有三个工具和一个供应商的原型不需要“六边形架构”;它需要的是上线。只有当你拥有达到两位数的工具、多个消费者,或者有任何引入第二个供应商的可能性时,这种模式的开销才物有所值 —— 考虑到模型排行榜更迭之快,大多数生产系统在一年内都会遇到这种情况。
掌控契约,租用其他一切
对过去两年的战略解读是,Agent 周围的一切都在剧烈动荡:模型排名每季度翻转一次,协议仍在竞争,框架在崛起又被抛弃。在这种环境下,杠杆率最高的问题是代码库中的哪些资产是持久的。提示词会针对每个模型重新调优。适配器会针对每个供应商重新编写。编排(Orchestration)会随着框架流行周期的更替而被替换。
工具契约才是持久的资产。“按客户和日期范围搜索订单,最多返回 50 条人类可读的结果”在函数调用(function calling)出现之前就是成立的,在当前的协议战争结束后依然成立。在你拥有的格式中编写一次契约,并为其标注版本号。让别人的序列化方式保持原样 —— 那是别人的问题,在一个可以随时删除的适配器文件中处理即可。
会员专享
余下内容仅对会员开放。
会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
- —完整文章,包含未公开存档的部分
- —可落地的工作框架,附带权衡与决策依据
- —新文章抢先看,先于公开发布
随时取消 · 一次订阅,畅读全部