第 9 章

基于协议的可扩展性

第 9 章:通过协议实现可扩展性

你发布了你的智能体(Agent)。它能读取文件、编写代码、运行测试。用户很满意——大约持续了一个星期。

接着,各种需求接踵而至。

你可以添加 Jira 支持,接入内部文档 API,甚至对接部署流水线。但总会有下一个工具、下一个 API,以及另一个由于公司性质而存在的闭源系统。你不可能跑赢这种增长速度,没人能做到。

这就是可扩展性瓶颈。如果你不解决它,你的智能体要么会因为集成过多而变得臃肿,要么会因为功能缺失而显得乏力。试图支持一切会导致代码失控;而不去支持则无法满足实际业务需求。

核心方案不是增加人力,而是停止预测用户的每一个需求,转而让他们自带能力

为什么硬编码集成死路一条

硬编码模式有四个结构性缺陷:

需求膨胀 (Scope Creep):每个组织都有自己的工具链和内部系统。如果你试图支持其中哪怕一小部分,你的智能体也会充斥着 99% 用户从未用过的插件。最终,你的代码库将充斥着大量无人维护的过时连接器。

权限隐患 (Permission Risks):添加 Jira 集成意味着要在所有用户的客户端中分发处理 Jira 凭据的代码——即便他们从未使用过 Jira。用户理所当然地会质疑:为什么要信任一个集成了大量冗余服务接口的智能体?

迭代脱节 (Velocity Mismatch):外部服务在不断变化。Slack 更新了 API,GitHub 推出了新功能。如果所有能力都硬编码在你的库中,你将永远疲于应对那些你无法控制的变更。你的产品路线图会沦为对他司发布计划的应激反应。

信任壁垒 (Trust Barrier):如果某位天才工程师为公司内部工具编写了完美的集成,他该如何分享?在硬编码模式下,他必须向你提交代码,经过漫长的审核、测试和发布周期。这种以月为单位的流程,最终会让优秀的集成方案胎死腹中。

协议:解决之道

答案早于 AI 出现。这就是让 VS Code 能够拥有数万扩展、让浏览器运行任何网站、让 LSP 改变代码编辑器的成功模式。

你不再去手动实现每一个工具,而是定义一个协议——一份规定工具如何自声明、如何被调用以及如何返回结果的契约。智能体不需要了解 Jira,它只需要知道如何通过协议与外界通信。

┌─────────────┐                        ┌─────────────────┐
│             │          协议          │                 │
│    智能体    │ ←────────────────────→ │    工具服务器    │
│   (Agent)   │                        │  (Tool Server)  │
└─────────────┘                        └─────────────────┘

协议定义了:
  • 工具如何声明其能力
  • 智能体如何调用工具
  • 结果如何返回
  • 错误如何呈现
  • 身份验证如何工作

语言服务器协议 (LSP) 已经证明了这种模式在大规模环境下的有效性。在 LSP 出现之前,每个编辑器都要从零开始实现语言支持。想在 Vim 中使用 TypeScript?得有人为 Vim 编写 TypeScript 分析。想在 Emacs 中使用?再写一遍。Sublime Text?还是得重写。现在,只需要一个 TypeScript 语言服务器,任何遵循 LSP 协议的编辑器都能获得 TypeScript 支持,无需重新实现分析逻辑。

模型上下文协议 (Model Context Protocol, MCP) 将这一模式应用到了 AI 智能体上。一个 MCP 服务器可以公开工具 (Tools)、资源 (Resources) 或提示词 (Prompts)。一个遵循 MCP 协议的智能体可以使用任何服务器,而不管它是谁编写的。生态系统会产生复利效应:每一个新工具都会让每一个兼容的智能体受益。

传输层:消息如何传递

协议定义了交换什么。传输层定义了如何交换。不同的场景需要不同的传输方式。

stdio:本地主力

对于在同一台机器上运行的工具,标准输入/输出 (stdio) 是难以超越的选择。智能体将工具作为一个子进程启动,并通过 stdin/stdout 进行通信——这与连接 Shell 命令的管道相同。

┌─────────────┐          启动          ┌─────────────────┐
│             │ ──────────────────────→│                 │
│    智能体    │         stdin         │    本地工具      │
│     进程    │ ──────────────────────→│     服务器       │
│             │         stdout         │   (子进程)       │
│             │ ←──────────────────────│                 │
└─────────────┘                        └─────────────────┘

stdio 的优雅之处在于其简单性:无需网络配置、无需端口、无需防火墙规则。工具以用户的权限运行,自然地访问本地文件和凭据。当智能体退出时,子进程也会随之退出——清理工作是自动完成的。

折中之处在于局限性。stdio 无法触及云服务或共享基础设施。为此,你需要网络传输。

HTTP:无状态且可扩展

对于远程工具,HTTP 是自然的选择。每次工具调用都是一个请求,每个结果都是一个响应。这是 Web 开发人员熟悉的领域。

┌─────────────┐      HTTP POST /call     ┌─────────────────┐
│             │ ────────────────────────→│                 │
│    智能体    │                          │    远程工具     │
│             │       JSON 响应          │     服务器       │
│             │ ←────────────────────────│                 │
└─────────────┘                          └─────────────────┘

HTTP 的无状态性是一个特性:负载均衡器可以自动分发请求,你可以将工具服务器部署在任何地方——云函数、容器、边缘网络。标准基础设施可以处理 TLS、速率限制和日志记录。

局限性在于请求-响应模型。每次调用都要支付连接开销,而且流式传输部分结果需要额外的处理。

SSE:单向流式传输

服务器发送事件 (Server-Sent Events, SSE) 扩展了 HTTP 以支持流式传输。客户端打开连接,服务器随时间推移推送事件。

┌─────────────┐     GET (EventStream)    ┌─────────────────┐
│             │ ────────────────────────→│                 │
│    智能体    │     event: data          │    工具服务器    │
│             │ ←────────────────────────│                 │
│             │     event: data          │                 │
│             │ ←────────────────────────│                 │
│             │     event: done          │                 │
│             │ ←────────────────────────│                 │
└─────────────┘                          └─────────────────┘

SSE 适用于产生增量结果的工具。数据库查询可以在行数据到达时进行流式传输。搜索工具可以逐步返回匹配项。智能体可以在后续结果仍在传输时处理早期结果。

约束是:SSE 是单向的。服务器流向客户端,但客户端无法在中途发送数据。对于真正的双向通信,你需要 WebSocket。

WebSocket:全双工

WebSocket 维持一个持久的、双向的通道。任何一方都可以在任何时间发送消息。

┌─────────────┐      WebSocket 升级       ┌─────────────────┐
│             │ ────────────────────────────→│                 │
│    智能体    │                              │    工具服务器    │
│             │ ←──────── 消息交互 ────────→   │                 │
│             │                              │                 │
└─────────────┘         (持久连接)            └─────────────────┘

WebSocket 适用于需要来回交互的工具——如调试会话、协作编辑、实时通知。持久连接消除了单次调用的延迟。

成本是复杂性。WebSocket 连接是有状态的,需要服务器跟踪连接状态。负载均衡需要会话保持(Sticky Sessions)。掉线后需要重连逻辑。

传输层无关性

无论采用哪种传输方式,协议本身保持不变。无论通过 stdio 还是 HTTP 访问,工具描述其能力的方式都是相同的。智能体的工具调用逻辑不会因消息传递方式的不同而改变。

在实现协议支持时,请将传输层设计为可插拔的抽象。核心协议处理(解析、调度、错误处理)应当与传输层无关。传输适配器负责处理具体的交付机制。

发现机制:了解可用项

对于内置工具,你在编译时就知道其能力。可扩展性要求在运行时进行发现 (Discovery)——智能体在连接时了解可用项。

当智能体连接到工具服务器时,服务器会宣告它提供的功能:

智能体连接到服务器
         │
         ▼
┌─────────────────────────────────────────────────────────┐
│ 服务器能力响应 (Capabilities Response)                    │
├─────────────────────────────────────────────────────────┤
│  tools: [                                               │
│    {                                                    │
│      name: "query_database",                            │
│      description: "对数据库执行 SQL 查询",                 │
│      inputSchema: { ... JSON Schema ... }               │
│    },                                                   │
│    {                                                    │
│      name: "list_tables",                               │
│      description: "列出数据库中所有的表",                   │
│      inputSchema: { ... }                               │
│    }                                                    │
│  ],                                                     │
│  resources: [                                           │
│    { uri: "db://schema", description: "数据库模式" }       │
│  ],                                                     │
│  prompts: [                                             │
│    { name: "sql_expert", description: "SQL 专家指南" }    │
│  ]                                                      │
└─────────────────────────────────────────────────────────┘

智能体将这些能力呈现给语言模型,由模型决定何时使用它们。

发现过程并非一次性的。当能力发生变化(添加了新工具或弃用了旧工具)时,服务器可以通知智能体。智能体无需重启即可更新。工具甚至可以是临时性的:仅在工作时间内可用,或仅在后端服务健康时可用。

除了工具,服务器还会公开另外两种能力类型:

资源 (Resources) 是增强上下文的静态数据。文档服务器可以公开 Markdown 文件;数据库服务器可以公开 Schema 信息;项目管理工具可以公开任务列表。智能体可以将这些内容直接拉取到上下文窗口中,作为背景知识供模型参考,而无需执行专门的操作。

提示词 (Prompts) 是引导智能体行为的预设模板。SQL 服务器可能会提供一个 sql_expert 提示词,其中封装了复杂的查询最佳实践。用户通过调用这些提示词,可以让智能体瞬间切换到特定领域的专家模式。

命名与身份识别

对于单个智能体和少数几个工具,命名是微不足道的。但可扩展性引入了冲突风险:两个服务器可能都提供“搜索”工具。该调用哪一个?

解决方案是命名空间 (Namespacing)。一种常见的模式是使用带有双下划线的层级名称:

{提供商}__{服务器}__{工具}

示例:
  github-mcp-server__github__search_code
  database__postgres__query
  company__internal-api__get_users

这种约定提供了唯一性(数百个服务器之间不会冲突)、可解析性(智能体可以提取服务器归属)以及权限粒度(允许一个服务器的工具而限制另一个)。

别名 (Aliasing) 提供了便利。用户可以配置快捷方式:在他们的环境中将 search 映射到 github-mcp-server__github__search_code。规范的命名空间名称仍然是权威的;别名只是为了节省输入。

当工具演进时,弃用路径 (Deprecation paths) 至关重要。服务器可以在过渡期间同时宣告新旧名称,并将旧名称标记为已弃用。智能体在保持向后兼容性的同时向用户发出警告。

身份验证模式

工具通常代表用户访问受保护的资源。插件身份验证面临独特的挑战:智能体不应持有永久凭据、不同的工具需要不同的权限,且长时间运行的会话会遇到令牌过期。

API Key

最简单的模式。用户提供密钥,服务器在请求中使用它。

server: database
api_key: ${DB_API_KEY}   # 来自环境变量

API Key 适用于个人工具和内部服务。它们易于理解和实现。缺点是:密钥通常是长效的(安全风险),且通常授予完全访问权限(缺乏粒度)。

OAuth2

对于第三方服务,OAuth2 是标准。用户显式授予工具访问特定资源的权限。

┌─────────────┐      ┌─────────────────┐      ┌─────────────────┐
│     用户     │      │    工具服务器    │      │  OAuth 提供商    │
└──────┬──────┘      └────────┬────────┘      └────────┬────────┘
       │                      │                        │
       │  "连接 GitHub"       │                        │
       │─────────────────────→│                        │
       │                      │                        │
       │      重定向至授权页面                        │
       │←─────────────────────────────────────────────│
       │                      │                        │
       │    用户审核范围并授权                        │
       │──────────────────────────────────────────────→│
       │                      │                        │
       │                      │       授权码 (Code)    │
       │                      │←───────────────────────│
       │                      │                        │
       │                      │        换取令牌        │
       │                      │───────────────────────→│
       │                      │                        │
       │                      │    访问 + 刷新令牌      │
       │                      │←───────────────────────│
       │                      │                        │
       │  "连接成功!"         │                        │
       │←─────────────────────│                        │

范围限制 (Scoped access) 对安全至关重要。连接 GitHub 时,用户可以看到工具请求的具体权限——可能是对公共仓库的读取权限,而不是对所有内容的写入权限。这是知情同意。

令牌刷新 (Token Refresh)

OAuth 访问令牌通常在一小时内过期。长时间运行的智能体需要处理刷新逻辑。

┌─────────────────┐                    ┌─────────────────┐
│    工具服务器    │                    │  OAuth 提供商    │
└────────┬────────┘                    └────────┬────────┘
         │                                      │
         │  使用 access_token 发起调用           │
         │─────────────────────────────────────→│
         │                                      │
         │  401 Unauthorized (已过期)           │
         │←─────────────────────────────────────│
         │                                      │
         │  请求刷新令牌                         │
         │─────────────────────────────────────→│
         │                                      │
         │  新的访问令牌                         │
         │←─────────────────────────────────────│
         │                                      │
         │  重试原始请求                         │
         │─────────────────────────────────────→│
         │                                      │
         │  成功                                │
         │←─────────────────────────────────────│

优秀的工具服务器会透明地处理刷新。智能体不知道令牌已过期——服务器负责刷新并重试。只有当刷新失败(用户撤销了权限)时,错误才会传达到智能体。

插件架构

原始协议支持实现了可扩展性,但用户需要更高层的抽象:插件 (Plugins)。插件将工具服务器与安装、配置和生命周期管理的元数据捆绑在一起。

插件清单 (Plugin Manifest)

清单声明了运行工具服务器所需的一切:

name: github-integration
version: 2.1.0
author: octocat
description: GitHub 集成,支持代码搜索、PR 管理等

server:
  transport: stdio
  command: npx
  args: [-y, "@github/mcp-server"]
  
config:
  - name: GITHUB_TOKEN
    description: 个人访问令牌
    required: true
    secret: true
  - name: DEFAULT_ORG
    description: 默认查询组织
    required: false

permissions:
  - network: api.github.com
  - read: ~/.gitconfig

capabilities:
  tools: true
  resources: true
  prompts: false

清单使得自动化插件管理成为可能:

操作发生什么
安装 (Install)下载包、验证签名、存储配置
配置 (Configure)呈现 UI 供用户选择选项
启动 (Start)使用正确的命令和参数启动服务器
更新 (Update)检查版本、处理迁移
移除 (Remove)干净地关闭、删除配置、清除缓存

沙箱机制 (Sandboxing)

插件运行的是非你编写的代码。出于安全考虑,必须限制它们的行为。

网络沙箱限制插件可以联系的主机。GitHub 插件需要 api.github.com,但不应将数据泄露到任意服务器。清单声明了所需的主机,运行时强制执行这一边界。

文件系统沙箱限制文件访问。插件可能需要某些配置文件,但不应读取 SSH 密钥或浏览器 Cookie。声明权限,运行时强制执行。

进程沙箱将插件彼此隔离,同时也与智能体隔离。一个崩溃的插件不会导致智能体挂掉。容器技术、seccomp 过滤器或平台沙箱提供了这种隔离。

这种权衡反映了安全性与功能的传统博弈。限制过严会导致插件失效,权限过宽则会埋下安全隐患。通过引入这种基于清单的体系,我们让这种权衡变得透明:用户能够直观地看到每个插件申请了哪些权限,并据此决定是否授予信任。

扩展中的错误处理

扩展引入了内置工具中不存在的失败模式。健壮的错误处理至关重要。

服务器故障

工具服务器可能无法启动、在会话中途崩溃或变得无响应。智能体应当:

  1. 快速检测:不要无线等待已经死掉的服务器。
  2. 优雅降级:从可用能力中移除该服务器的工具。
  3. 通知模型:让模型知道某个能力不可用,从而调整策略。
  4. 尝试恢复:使用指数退避算法进行重试。
智能体尝试 github__search_code
         │
         ▼
服务器无响应
         │
         ▼
返回给语言模型:
  “工具 github__search_code 暂时不可用。
   GitHub 服务器无响应。
   请考虑替代方案或稍后重试。”
         │
         ▼
模型调整策略 (使用不同工具、询问用户、改变方法)

透明度至关重要。模型需要知道某个能力宕机了,以便做出相应的规划。静默失败或误导性的错误会导致行为混乱。

身份验证失败

在长会话中,令牌过期很常见。良好的处理方式能将身份验证问题与其他错误区分开来:

工具调用返回 401 Unauthorized
         │
         ▼
令牌可刷新?
    │           │
    是          否
    │           │
    ▼           ▼
 刷新并重试    呈现鉴权错误:
              “需要重新身份验证”

对于可刷新的令牌,更新是无感的。对于不可刷新的凭据,错误应当带着清晰的指引呈现:“您的 GitHub 令牌已过期,请重新连接您的账户。”

工具自身错误

有时工具成功运行但返回了错误:查询失败、文件不存在、API 返回异常响应。这些错误应当返回给智能体,而不是导致服务器崩溃。

{
  "error": {
    "code": "QUERY_FAILED",
    "message": "表 'users' 不存在",
    "details": { ... }
  }
}

语言模型处理工具错误的能力往往超出预期。它们可能会意识到自己使用了错误的表名并重试、向用户寻求澄清,或者尝试不同的方法。你的职责是如实报告错误,而不是越俎代庖去尝试恢复。

版本不匹配

协议在演进。服务器可能使用了智能体不支持的特性。连接时的版本协商可以防止莫名其妙的失败:

智能体: { "protocolVersion": "2024.1" }
服务器: { "protocolVersion": "2024.1", "capabilities": [...] }

当版本不兼容时,清晰的提示很有帮助:“此工具需要 2024.2 版本的协议,但您的智能体仅支持 2024.1。请更新您的智能体或使用该工具的旧版本。”

平台思维

可扩展性将你的智能体从一套固定的能力集转型为一个平台。用户可以根据自己独特的需求进行适配,而无需等待你去实现每一个可能的集成。

这种基于协议的架构——涵盖了定义契约的协议、消息传输层、宣告能力的发现机制、保护资源的身份验证、打包能力的清单以及确保鲁棒性的错误处理——共同构成了平台进化的基石。

如果你正在构建 AI 智能体,请务必及早实现协议支持。在项目后期才考虑扩展性往往代价巨大。建议先从适用于本地工具的 stdio 接口入手(实现和调试最简单),当有远程调用需求时,再逐步引入 HTTP 传输层。

如果你正在为智能体构建工具,请遵循已建立的协议。你的工具将立即能被任何兼容的智能体使用——触及到那些你通过特定智能体集成永远无法触及的用户。

生态系统效应会产生叠加。每一个新的工具服务器都会让每一个兼容的智能体受益。每一个新的智能体都能从每一个现有的工具中获益。这就是 VS Code 积累数千个扩展、浏览器支持数十亿个网站的方式,也是智能体系统将如何扩展到超出任何单一团队所能构建的规模的原因。