ACP 协议详解——编辑器与编码 Agent 之间的开放标准
本文为「AI Agent 技术系列」第 9 篇。MCP(第 3 篇)解决“Agent 如何接入工具与数据”,A2A(第 8 篇)解决“独立 Agent 之间如何协作”,ACP 补上第三个方向:人每天使用的编辑器与编码 Agent 之间如何通信。三条通道互不替代,可以同时存在。
在编辑器里用 AI 编码助手已是日常,但“接进来”这件事并不便宜。每个编辑器要为每个想支持的 Agent 写定制集成;反过来,Agent 想触达用户,就得逐个实现编辑器的私有 API。落到使用者身上,就是你选定一个 Agent,往往等于接受了它所支持的界面 [1]。
Zed 团队在 2025 年发起的 Agent Client Protocol(ACP)正是冲着这个问题来的 [1][2]:像 LSP 统一语言服务器的接入那样,把 Agent 的接入也统一掉 [1]。本文以当前稳定的 v1 为基准,讲清它的架构、通信模型与一次会话的完整流程,再单列一节梳理 v2 的重构。除特别说明,方法名与字段以官方文档为准,JSON 示例中的 ID 仅为说明结构。
一、ACP 是什么:把 M×N 的集成矩阵变成 M+N
官方定义与本文版本
ACP 标准化代码编辑器(查看和编辑源码的交互程序)与编码 Agent(用生成式 AI 自主修改代码的程序)之间的通信,本地与远程场景都适用 [1][3]。本地 Agent 作为编辑器的子进程运行,走 stdio 上的 JSON-RPC;远程 Agent 可以托管在云端或独立基础设施上,走 HTTP 或 WebSocket,官方明确注明远程的完整支持仍在推进 [1]。
版本状态需要先说清楚:当前稳定的协议版本是 1;v2 已有完整的规范草案与迁移指南,但整个 v2 协议面仍标注为 draft [3][4]。所以本文以 v1 为主线,v2 的变化集中在第四节。
集成为什么痛
文档把问题归为三条 [1]:
| 痛点 | 表现 |
|---|---|
| 集成开销 | 每一个新的“编辑器 × Agent”组合都要定制开发 |
| 兼容性受限 | Agent 只能工作在一部分编辑器里 |
| 开发者锁定 | 选了 Agent,就得接受它支持的界面 |
好处是双向的:实现 ACP 的 Agent 可以用于任何兼容的编辑器,支持 ACP 的编辑器则获得整个 Agent 生态。解耦让两侧独立创新,M×N 的集成矩阵被压成 M+N [1]。
与 MCP、A2A 的位置关系
graph LR
U[🧑 用户] --> ED[🖥️ 编辑器 Client]
ED -->|ACP| AG[🤖 编码 Agent]
AG -->|MCP| TOOLS[🔧 工具与数据]
AG2[🤖 独立 Agent] <-->|A2A| AG
| 协议 | 连接的两侧 | 回答的问题 |
|---|---|---|
| MCP | Agent ↔ 工具与数据 | 一个 Agent 如何接入能力 |
| ACP | 编辑器 ↔ 编码 Agent | 人的工作台如何承载 Agent |
| A2A | 独立 Agent ↔ 独立 Agent | 跨系统的 Agent 如何协作 |
三者不是竞争关系。ACP 在 JSON 表示上尽可能复用 MCP 的类型,让集成方不必再造一套常见数据结构,同时为编码场景的交互体验定义了自己的类型,比如 diff 展示 [1][5]。一个编码 Agent 完全可以同时对用户讲 ACP、对工具讲 MCP、对其他 Agent 讲 A2A。
一个被刻意选择的信任模型
架构文档里有条原则值得单独拎出来:ACP 面向“你用编辑器对话一个你信任的模型”的场景。你仍然控制着 Agent 的工具调用,但编辑器会把本地文件和 MCP servers 的访问交给 Agent [5]。
这决定了 ACP 与 A2A 是两个不同的威胁模型:ACP 里 Agent 跑在你自己的机器上、改动你的代码,安全边界在于权限确认的交互设计;A2A 里对方是跨组织的外部系统,安全边界在于认证、授权与数据保护。给 Agent 选通道时,先分清自己站在哪个场景里。
二、架构与通信模型:双向 JSON-RPC 加流式通知
两个角色与连接方式
| 角色 | 是什么 | 职责 |
|---|---|---|
| Client | 编辑器、IDE,或其他给 Agent 提供 UI 的程序 | 管理环境、处理用户交互、控制资源访问 |
| Agent | 用生成式 AI 自主修改代码的程序 | 通常作为 Client 的子进程运行 [6] |
用户尝试连接某个 Agent 时,编辑器按需拉起子进程,之后的通信全部走 stdin/stdout。一条连接可以承载多个并发会话,几条互不干扰的“思路”可以同时进行 [5]。
三条设计原则
- MCP-friendly:协议建立在 JSON-RPC 之上,尽可能复用 MCP 类型,避免给集成方增加又一套数据表示 [5];
- UX-first:为“与 AI Agent 交互”的 UX 挑战而设计——足够灵活地呈现 Agent 的意图(diff、工具调用、计划),但不做多余的抽象 [5];
- Trusted:如上节所述,编辑器授予 Agent 本地文件与 MCP servers 的访问,用户保留对工具调用的控制 [5]。
通信原语:方法与通知
协议遵循 JSON-RPC 2.0,消息只有两类 [6]:
- 方法(Method):请求-响应成对出现,期待 result 或 error;
- 通知(Notification):单向消息,不期待任何响应。
ACP 大量使用通知,让 Agent 能把输出实时流式推给界面;同时利用 JSON-RPC 的双向性,Agent 也可以反向调用编辑器的方法,比如为一次工具调用请求用户授权 [5]。这是它与许多“单向网关 API”的本质区别:谁调用谁取决于消息的方向,而不是角色——Client 调 Agent 发提示词,Agent 也调 Client 要权限、要文件。
几条贯穿始终的约定:文件路径必须是绝对路径;行号从 1 开始;JSON 属性键用 camelCase,判别字段的取值用 snake_case;错误处理遵循 JSON-RPC 2.0 [6]。
一次会话的消息流
sequenceDiagram
participant U as 用户
participant C as 编辑器 Client
participant A as Agent 子进程
C->>A: initialize(协商版本与能力)
C->>A: authenticate(如需要)
C->>A: session/new(cwd 与 MCP 配置)
U->>C: 输入提示词
C->>A: session/prompt(内容块)
A-->>C: session/update 通知流(消息块 / 工具调用 / 计划)
A->>C: session/request_permission(反向请求授权)
C-->>A: 用户的选择
A-->>C: session/prompt 响应(stopReason)
与 MCP 的协作方式
编辑器通常持有用户配置的 MCP servers。向 Agent 转发提示词时,会把这些配置一并传过去,让 Agent 直连 MCP server [5]。
编辑器自己的工具想暴露给 Agent 时,也不必让 MCP 和 ACP 挤在同一个 socket 上:把自身包装成一个 MCP server 的配置交给 Agent 即可。考虑到部分 Agent 只支持 stdio 上的 MCP,编辑器可以提供一个小型 proxy,把请求隧道回自身 [5]。
三、v1 全貌:从 initialize 到 stopReason
典型消息流分三步
- 初始化:Client 调用
initialize建立连接;如果 Agent 要求,再调用authenticate认证; - 建立会话:
session/new创建新会话;或用session/load恢复已有会话(需要loadSession能力); - Prompt 回合:
session/prompt发送用户消息,Agent 用session/update通知流式回报进度,期间可能发生权限请求;必要时 Client 发session/cancel中断;回合以session/prompt的响应结束,携带 stopReason [6]。
初始化:版本与能力协商
initialize 是双方交换“我能做什么”的时刻(示例精简自官方迁移指南中的 v1 样例 [4]):
1 | { |
Agent 的响应会带回自己支持的协议版本、能力清单与认证方式:
1 | { |
这份清单决定了后续每个可选方法是否可用:协议把功能切成基线与可选两层,可选层一律要靠能力声明解锁。
Prompt 回合:流式输出、工具调用与反向请求
用户消息由内容块组成,共五种类型:text、image、audio、resource_link、resource [4]。Agent 的产出则通过 session/update 通知呈现,主要 variant 包括 [6]:
| variant | 内容 |
|---|---|
| user_message_chunk / agent_message_chunk / agent_thought_chunk | 用户消息、回复、思考过程的流式片段 |
| tool_call / tool_call_update | 工具调用及其状态更新 |
| plan | 任务计划 |
| available_commands_update | 可用的斜杠命令 |
| current_mode_update | 会话模式变化 |
工具调用的通知长这样:
1 | { |
需要用户授权时,Agent 反向调用 session/request_permission,给出 allow_once / allow_always / reject_once / reject_always 等选项,由编辑器呈现给用户 [6]。需要访问文件或终端时,Agent 调用 Client 的可选方法:fs/read_text_file、fs/write_text_file、terminal 系列(创建、输出、释放、等待退出、终止),以及 elicitation/create(向用户请求结构化输入)[6]。
注意这些方法都不是必选的:Client 可以一个都不实现,Agent 必须依据能力声明优雅降级。
回合结束时,session/prompt 的响应携带 stopReason(end_turn / max_tokens / max_turn_requests / refusal / cancelled)[4]。用户随时可以发 session/cancel 通知中断当前回合 [6]。
v1 方法总览
| 方法 | 调用方向 | 必选性 | 能力门槛 |
|---|---|---|---|
| initialize | Client → Agent | 基线 | — |
| authenticate | Client → Agent | 可选 | Agent 声明认证方式 |
| session/new | Client → Agent | 基线 | — |
| session/prompt | Client → Agent | 基线 | — |
| session/load | Client → Agent | 可选 | loadSession |
| session/set_mode | Client → Agent | 可选 | — |
| logout | Client → Agent | 可选 | auth.logout |
| session/cancel | Client → Agent 通知 | — | — |
| session/update | Agent → Client 通知 | — | — |
| session/request_permission | Agent → Client | 基线 | — |
| fs/read_text_file、fs/write_text_file | Agent → Client | 可选 | fs |
| terminal/create 等五个 | Agent → Client | 可选 | terminal |
| elicitation/create | Agent → Client | 可选 | 对应 elicitation 模式能力 |
这张表也解释了 ACP 的渐进式复杂度:实现一个最小可用 Client,只需要 request_permission 加通知处理;剩下的能力按需补齐。
四、v2:一次按生态现实做的重构
定位:合并式重构,v1/v2 并存
v2 是一次合并式重构(consolidation release):重新设计 prompt 生命周期、统一流式与非流式更新、让 schema 默认向前兼容,并移除生态已经实际放弃的部分 [4]。
两点前提必须强调:其一,v2 整体仍标注为 draft,文档要求用显式版本协商与 feature flag 控制接入,直到它稳定 [4];其二,迁移不等于放弃 v1——v1 的实现会在很长一段时间里继续存在,官方的推荐做法就是两侧并存,按连接协商协议版本,把 v2 放在开关后面逐步成熟 [4]。
如果只记五件事
官方迁移指南给了一个“只记五件事”的清单 [4],我按自己的理解展开:
1. session/prompt 的响应不再是回合结束。 响应只确认“已接受”(空对象),前台进度与完成全部改由 state_update 通知表达:running(工作中)、idle(空闲,携带 stopReason)、requires_action(等待用户操作,比如权限确认)。stopReason 从响应挪到了 idle 通知里 [4]。
2. 更新都是 upsert。 消息、工具调用、计划都按 ID 打补丁,语义统一:字段省略 = 不变,null = 清除,具体值 = 替换,chunk = 追加;messageId 变为必填,由 Agent 生成并作为消息身份的唯一来源 [4]。
3. Client 的文件系统与终端执行 API 被移除。 v1 的 fs/* 与 terminal/* 在少数 IDE 之外实现得参差不齐,Agent 无论如何都要自备文件与执行能力。v2 里客户端工具统一通过 MCP server 提供,和 Agent 用的其他工具站在同一起跑线上;Agent 侧的终端输出变成单独的 display-only 展示面,只播不控 [4]。
4. 能力结构重组。 双方统一为 capabilities 加必填的 info;会话域能力嵌套在 session 之下;支持标记从布尔值变成对象({} 表示支持,为将来扩展留位);宣告 session 能力即承诺实现 new、list、resume、close、prompt、cancel、update 七个基线方法,Client 不必再逐个探测 [4]。
5. 一切皆可扩展。 枚举与 tagged union 接受未知值:_ 前缀保留给实现私有扩展,不带下划线的未知值保留给未来的 ACP 版本;接收方应当保留这些值并安全降级,而不是解析失败 [4]。
Prompt 生命周期对比
这是 v2 最重要的语义变化,我单独列成表 [4]:
| 信号 | v1 | v2 |
|---|---|---|
| 提示词已被接受 | 隐含 | session/prompt 响应(空对象) |
| 前台工作运行中 | session/prompt 仍挂起 | state_update: running |
| 等待用户操作 | 隐含(权限请求挂起中) | state_update: requires_action |
| 前台工作结束 | session/prompt 响应带 stopReason | state_update: idle 携带 stopReason |
| 取消确认 | 响应 stopReason: cancelled | idle state_update 携带 cancelled |
为什么值得重构?v1 把“提示词已被接受”和“回合结束”纠缠在一个挂起请求里,导致历史回放、多客户端观察同一会话、后台工作、消息排队都不好表达。v2 把前台进度全部搬进通知之后,同一套消息流可以同时服务实时对话、resume 时的历史回放和多端观察 [4]。
方法映射精选
| v1 | v2 |
|---|---|
| authenticate / logout | auth/login / auth/logout(authMethods 非空则必须成对实现) |
| session/load + session/resume | 仅 session/resume,可选 replayFrom 控制是否回放历史 |
| — | session/list、session/close 转为必备 |
| session/set_mode + current_mode_update | 统一为 config options(mode / model / model_config / thought_level) |
| tool_call + tool_call_update | 仅保留 tool_call_update,首个通知即创建 |
| plan | plan_update(带 planId,支持多计划) |
| fs/* 与 terminal/*(Client 执行) | 移除;客户端工具经 MCP server 提供 |
| diff(oldText/newText) | 结构化 changes 加可选 git patch 文本 |
| clientCapabilities / agentCapabilities | 双向统一的 capabilities,对象化支持标记 |
以 diff 为例:v1 的 oldText/newText 无法区分“删除文件”与“清空文件”,表达不了重命名、复制和二进制变更,渲染 diff 还得客户端自己算。v2 改为结构化的 changes 列表(add / delete / modify / move / copy,附 fileType 与 mimeType),外加可选的 git patch 文本,客户端可以直接据此构建文件树和摘要 [4]。
权限请求也做了清理:v1 里 Agent 常把提示文案塞进 tool call 的 title,顺带污染了工具调用的展示状态。v2 把权限提示的 title(必填)与请求对象 subject(tool_call 或 command)分开,提示归提示,状态归状态 [4]。
还有几处变化,实现时都会碰到 [4][7]:MCP server 配置必须带 type 判别字段,删除了已废弃的 HTTP+SSE 传输;stdio 上明确遵循 JSON-RPC 2.0 batch 行为,但 initialize、auth/login、session/new、session/resume、session/prompt 这类会改变后续消息有效性的消息不要批处理。
五、生态现状:Agent、Client、Registry 与 SDK
Agent 侧:四十个左右的实现,适配器是常见的接入方式
官方 Agent 列表已有约四十项,知名的有 Gemini CLI、Codex CLI、Claude Agent、Cursor、GitHub Copilot(公开预览)、Qwen Code、OpenCode、Cline、Kimi CLI、Junie 等 [8]。
接入方式里有个模式很能说明问题:Claude Agent 经 Zed 的 SDK 适配器接入,Codex CLI 经官方的 codex-acp 适配器接入 [8]。两者都不是原生实现——为存量 Agent 写一层 ACP 适配,比重写执行循环划算得多,接入成本就是这样被压下来的。
Client 侧:从 IDE 到手机
Client 的名单更长:Zed(发起方)、JetBrains、Visual Studio Code(多个扩展)、Neovim(CodeCompanion、avante.nvim 等插件)、Emacs、Obsidian、Pulsar、Qt Creator、Sublime Text、Unity;再往外是桌面应用、移动 App,以及接到 Telegram、Discord、Slack、飞书、微信的消息桥 [9]。
ACP 的生态已经溢出了“编辑器”这个词:任何能给 Agent 提供 UI 和环境的地方,都可以长出一个 Client。这对 Agent 作者是个好消息——实现一次 ACP,可能同时出现在 IDE、笔记本应用、手机和聊天软件里。
Registry 与 SDK
Registry 是官方策展的 Agent 分发目录,目前收录支持认证的 Agent,任何讲 ACP 的 Client 都可以从这里发现和安装 [10]。
官方 SDK 覆盖 TypeScript、Rust、Python、Kotlin、Java 五种语言;协议本体以 JSON Schema 形式随 GitHub release 发布,其他语言可以据此生成 [3]。准备实现时,建议从 introduction 与 protocol overview 进入概念,用 JSON Schema 逐项核对字段,别凭记忆写接口。
结论
回到开头的问题:怎么终结 M×N 的集成矩阵。ACP 的答案是给“编辑器 ↔ Agent”定一份契约:Agent 实现一次,就能进入所有支持 ACP 的界面;界面实现一次,就能获得整个 Agent 生态。放到系列语境里:MCP 统一的是 Agent 的“手”,A2A 统一的是 Agent 之间的“对话”,而 ACP 补的是人与 Agent 之间的“工作台”。
我的落地建议是先选角色,再看版本:
- Agent 作者:如果程序只打算在某一个编辑器里运行,直接适配它的 API 也许更快;想进生态,就实现 ACP 或写一层适配器。从官方 SDK 的 examples 起步,先做 v1,v2 用 feature flag 跟进。
- Client 作者:一次接入换来整个生态,但要清楚标准化不覆盖的地方——权限 UX、diff 与终端的渲染恰恰是各家差异化的空间,也是安全边界所在。记住 ACP 的信任模型:Agent 跑在你机器上、动你的代码,request_permission 的呈现质量直接影响风险。
- 版本策略:按连接协商 protocolVersion,v1/v2 并行支持;v2 仍是 draft,不要让未灰度的用户直连 v2。如果只记 v2 的一条心法,就是“响应即确认,一切皆 upsert”。
最后是两点保留意见。ACP 目前的重心显然在“本地子进程”这个形态:远程 Agent 的完整支持仍在推进,远程传输(streamable HTTP 加 WebSocket)还停在独立的 RFD 阶段 [1][4],把它当现成的远程协议用为时尚早。更让我在意的是生态扩张带来的安全面——消息桥和移动端意味着本地文件与终端的授权可能从手机上发起,把本地资源交给 Agent 的每一道口子,都得在 Client 侧默认收紧。
参考资料
以下链接为英文官方资料;v2 相关内容目前标注为 draft,接入前请留意页面上的版本说明。
[2] Zed 博客:How the Community is Driving ACP Forward
[3] GitHub:agent-client-protocol
[4] Migrating from v1:v1 到 v2 迁移指南
[5] Architecture:架构
[6] Protocol v1 Overview:v1 协议总览
[7] Protocol v2 Overview:v2 协议总览
[8] Agents:Agent 列表