ACP:Agent Client Protocol

11 分钟读完

P13
深入解析 · 协议与互操作

Agent Client Protocol 把“谁拥有文件系统”这件事反了过来——智能体要请编辑器代为读写文件,而正是这一处反转,让它成为一份协议,而不是一套插件 API。

Zed 的 ACP,正是 Claude Code、Codex CLI 与 Gemini CLI 能跑在同一个编辑器窗口里的原因。有意思的部分不是 JSON-RPC 的封装形式,而是:一个 ACP 智能体被禁止直接碰磁盘,它要调用 fs/read_text_file,请客户端去做。官方给出的理由是编辑器里尚未保存的缓冲区。而由此带来的后果是:权限、变更追踪与沙箱全部落到了客户端一侧,因而对每一个会说这门协议的智能体都是统一的。读懂能力握手与文件系统反转,ACP 的其余部分自然成立;把它读成“智能体版 LSP”,这两样你都会错过。

STEP 1

两个协议,一个缩写——而这一个才是活着的那个。

先说技术之外的一件事:叫 ACP 的协议前后有两个,而搜索结果至今仍把它们随意混作一谈。本文讲的这个是 Agent Client Protocol,由 Zed Industries 于 2025 年 8 月创建,用来把代码编辑器连接到编码智能体。另一个是 Agent Communication Protocol,一个以 REST 为原生的智能体间协议,2025 年 7 月被贡献给 Linux 基金会,随后并入 A2A;本站在 ACP:后来怎么了 里记述了它短暂的一生。如果你正在读的页面把 ACP 描述成 A2A 的替代品,它说的是已经死掉的那个;如果它把 ACP 描述成你的编辑器会说的东西,那说的才是这一个。

这种混淆不只是用词问题,因为两个协议压在不同的接缝上。Agent Communication Protocol 想标准化的是智能体到智能体的委派,而那道接缝如今归 A2A 所有。Agent Client Protocol 标准化的是客户端到智能体的控制:一个编辑器,多个可互换的智能体。这是两个正交的问题,一个系统完全可以同时说两种——对编辑器说 ACP,对同侪说 A2A,对工具说 MCP。把它们当成竞争关系,是这一领域二手报道里最常见的一个错误。

值得关心它的理由是采用度。继 Zed 之后 JetBrains 加入,两家在 2026 年 1 月共同推出了一个智能体注册表,截至 2026 年已列出 40 多个智能体。关键在于,大多数人真正在用的那三个 CLI 都可以接入:Gemini CLI 原生实现了 ACP,而 Claude Code 与 Codex CLI 是通过 Zed 构建的适配器连接的,并非第一方支持。这个“适配器”的区别会影响你的预期——适配器包裹的是一个围绕自家 harness 设计出来的 CLI,因此能力覆盖面取决于适配器选择映射了什么,而不是该智能体本身能做什么。

STEP 2

形态:stdio 上的 JSON-RPC 2.0,以及 LSP 类比在哪里失效。

ACP 是在客户端与它派生出的子进程智能体之间,通过 stdin 与 stdout 讲的 JSON-RPC 2.0。消息形态恰好只有两种:方法,即请求/响应对;以及通知,单向的,而且规范说得很明确——通知“永远不会收到响应(无论成功还是错误)”。正是这一划分,让一次提示轮次在外层边界上保持同步(你调用了 session/prompt,你就会拿到一个响应),同时又能不阻塞地流式推送数量不限的中间事件。

显而易见的类比是 Language Server Protocol,在一定范围内它也确实是个好类比:两者都把 N 个编辑器 × M 个实现的集成矩阵变成了 N + M,而这正是 互操作问题 一文在一般意义上给出的结构性论证。这个类比在经济学上成立,在语义上要丢掉。语言服务器回答的是关于某个缓冲区的问题,本质上每次请求都是无状态的;而一个 ACP 智能体维持的是一场长期存在、成本高昂、非确定性的对话,它会在轮次中途请求许可,按 token 花钱,还可能在一次编辑做到一半时被打断。LSP 里没有任何概念对应得上“服务器可能会在继续之前向编辑器请求授权”,而 ACP 的设计压力,大半就落在这个概念上。

子进程模型是一处刻意的约束,而不是实现上的顺手。因为是客户端派生了智能体,客户端本就掌握着进程边界——它可以设定工作目录、控制环境变量、杀掉进程,并且(原则上)为它加沙箱。一份走 HTTP 连到远端智能体的协议,这些都得自己重新发明一遍。ACP 则直接假定客户端是那个有特权的一方,并把其余每一个决定都建立在这个假定之上——接下来三步讲的其实都是这件事。

STEP 3

initialize:先协商版本,再做能力门禁。

每一次连接都以 initialize 开场,它一次做两件事:协商协议版本,交换能力。版本协商刻意做得很粗——protocolVersion 是“一个用于标识 MAJOR 协议版本的整数”,只在出现破坏性变更时递增。客户端发出自己支持的最新版本;若智能体支持它,智能体必须原样回同一个版本,否则必须回它自己支持的最新版本。若不匹配,客户端应当关闭连接并告知用户。这里没有次版本号,也没有“按版本开特性开关”这回事——所有更细的粒度都是能力。

能力在两个方向上各自声明,而其中的门禁规则才是要点。客户端声明 clientCapabilities:一个含 readTextFilewriteTextFile 布尔值的 fs 对象、一个覆盖全部 terminal/* 方法的 terminal 布尔值,以及一个表示它能否在交互式终端中复现该智能体调用的 auth.terminal 标志。智能体声明 agentCapabilities:表示 session/load 是否存在的 loadSession、一个含 image/audio/embeddedContext 布尔值、用于声明基础文本与资源链接之外内容类型的 promptCapabilities 对象,以及带 httpsse 传输标志的 mcpCapabilities

让这一切可以放心实现的那条规则是:“客户端与智能体必须initialize 请求中省略的一切能力视为不支持。”缺席是一个硬性的“否”,而不是“也许”——这意味着一个正确的智能体默认降级而不是去试探,而一个较旧的客户端在与较新的智能体对话时,压根看不到新增的那一片接口。这与 能力发现 一文中为 Agent Card 描述的纪律是同一种,只是施加在进程启动时而非网络之上。如果你只从 ACP 里正确实现一件事,就实现这一条:智能体调用了一个其能力未被声明的方法,那是缺陷,不是优雅降级。

STEP 4

一次提示轮次,以及其间流动的东西。

工作发生在会话里。session/new 创建一个会话,它锚定在一个主工作目录上,可选地带上额外的文件系统根,并携带自己那份隔离的上下文——对话历史、状态,以及它自己的 MCP 服务器连接。最后这处细节正是 ACP 与 MCP 组合的地方:客户端把 MCP 服务器配置交进会话,智能体连上那些服务器,两份协议各占一道接缝而互不重叠。另一半请见 MCP:宿主、客户端、服务器

于是一次轮次是这样的:客户端带着用户消息调用 session/prompt;智能体一边工作一边流式发出 session/update 通知;智能体可能中途打断自己去调用 session/request_permission 或某个文件系统方法;客户端可以用一条 session/cancel 通知中止;最后 session/prompt 这个请求带着一个停止原因返回。停止原因才是值得围绕它做设计的那部分——客户端正是靠它得知这一轮结束是因为智能体做完了、撞上了某个限额,还是被用户取消了,而这三种情况需要不同的界面。

session/update 这条流比一条 token 流要丰富得多。它承载按角色标注的消息分片——agent、user,以及 thought,推理正是靠它与输出分开呈现——外加工具调用及其后续更新、计划、可用命令集合的变化,以及模式变更。由此落下两处设计上的推论。其一,因为工具调用是以更新而非不透明文本的形式抵达的,客户端无需知道某个智能体的工具词汇表,就能为它渲染出真正的工具调用界面。其二,因为 thought 是一种独立的分片类型,客户端可以把展示与否当作一个显示决策——并且理应只当作显示,理由见 思维链忠实性

STEP 5

那处反转:文件系统归客户端所有,智能体必须开口请求。

下面这个设计决定,把 ACP 与一切“让智能体在终端里跑”的安排区分了开来。ACP 智能体不自己读写项目文件。它调用 fs/read_text_filefs/write_text_file,由客户端执行该操作。门禁很严:若 readTextFilewriteTextFile 为 false 或缺席,智能体“必须不得尝试调用对应的文件系统方法”。

官方给出的动机很朴素,也完全能说服人:编辑器里尚未保存的状态。开发者的缓冲区经常与磁盘上的内容不一致,而一个直接读文件的智能体,拿到的是人类正盯着看的那份东西的过期副本——然后它编辑它,并在写回时悄无声息地毁掉那些未保存的工作。经由客户端中转,意味着智能体看到的是活的缓冲区。单凭这一条就足以支撑这个设计。但官方给出的第二个理由,才是带来架构性后果的那个:它“让客户端得以追踪智能体执行期间所做的文件修改”。客户端不只是个代理,它是那本账。

顺着这条线走下去,回报很大。因为每一次改动都经过客户端,撤销/重做集成、变更预览、逐文件 diff 与选择性应用,全都可以在编辑器里实现一次,随后对每一个说 ACP 的智能体都同样奏效——包括那些你看不见内部、只能经由适配器触达的智能体。同一处反转也覆盖了执行:terminal/* 方法被单个 terminal 能力挡在门后,于是想跑命令的智能体是去请求,而不是自己派生进程。而 session/request_permission 把这个模式推广到了任意工具调用——智能体无法单方面触达客户端资源,它请求,由客户端裁决。

这里正是把“这买到了什么、没买到什么”讲清楚的地方。它给了你一个统一的、客户端一侧的策略面,这确实很有价值,也正是一个编辑器能够为形态各异的智能体提供一致审批体验的原因。它没有给你隔离:智能体仍然是你机器上的一个子进程,握有操作系统授予它的那份环境权限,而 ACP 里没有任何东西能阻止一个铁了心的、或已被攻陷的智能体用普通系统调用直接打开一个文件。ACP 的文件系统方法是一份协作契约,不是一个沙箱。要真正的隔离,你需要的是 沙箱与代码执行 里的那些机制;ACP 与它们组合得很好,但并不取代它们。

STEP 6

ACP 到哪里为止——以及它与之共处一栈的三道接缝。

ACP 有三个会话方法,而它们之间的差别很容易被过度解读。session/new 开一个全新的上下文。session/load 只在智能体声明了 loadSession 时可用,用于重连到一个已存储的会话——而“load”的含义是:智能体把整场对话以一串 session/update 通知重放给客户端,并且要等每一条条目都推送完毕,才回应最初那个请求。session/resume 则是不带这次重放地重连。

请仔细看这次重放的方向,因为这是关于 ACP 最常被误解的一件事。重放的流向是智能体 → 客户端,其目的是让编辑器把一条它并不存储的线程重新画出来。对话归产生它的那个智能体所有;会话 id 也是那个智能体铸造的。所以 ACP 并不能让你的编辑器把一个 Claude Code 会话交给 Codex——在同一个窗口里跑这两个,你得到的是两条线程里的两个智能体,而不是一个共享的会话。ACP 标准化的是接口,不是状态,这一论点在 每一种真正好用的交接,都把逐字记录扔了 中有充分展开。

于是剩下一幅干净的三接缝图景,而它值得被整体地握住。ACP 是客户端到智能体:一个编辑器驱动多个可互换的智能体,而客户端握着文件系统、终端与许可裁决。MCP 是智能体到工具:智能体触达各种能力与数据,由客户端按会话配置。A2A 是智能体到智能体:跨越组织边界的委派,其中对端刻意保持不透明,返回的是产物而非历史——见 A2A v1.0。它们是互补关系,不是替代关系,一个像样的智能体产品会把三种都说。判断某个问题属于哪一道接缝,有个很实用的检验:问一句“谁被允许看到这场对话”。如果答案是“负责渲染它的那个东西”,那是 ACP;如果是“智能体之外没有人”,那是 A2A;如果这个问题压根不涉及一场对话,那就是 MCP。