一个 IDE 终端小人和一个 Agent 小人通过一根双向管子相连,消息泡泡在管子里来回飘

写在前面

这一篇讲的是 ACP(Agent Client Protocol)——Zed 团队在 2025 年推出的一个开放协议。它干的事很简单:定义 IDE / Agent 网关跟 AI Agent 之间怎么说话。

但在讲它之前,先把协议生态里容易混淆的几个名字分清楚:

  • MCP(Model Context Protocol,Anthropic):Agent 连工具(agent-to-tool)
  • ACP(Agent Client Protocol,Zed):Client 连 Agent(client-to-agent)
  • A2A(Agent-to-Agent,Google + Linux Foundation):Agent 连 Agent

这三个协议各管一段、互相正交,不是同一个东西的不同名字。

业界有时候会把"ACP"这个名字跟 IBM 捐给 Linux Foundation 的那套"Agent Communication Protocol"搞混——那个是 agent-to-agent 的,后来并进了 Google A2A。本文的 ACP 指 Zed 那个 client-to-agent 协议。

Codex Harness 的 app-server、Cursor 的 ACP 实现、Zed 自己的 ACP 实现——都属于这一套。


一、为什么需要 ACP

把 Agent 嵌入一个宿主环境(IDE、Agent 网关、Web 客户端),需要解决几个问题:

问题一:Agent 跟宿主怎么对话?

最早的做法是 HTTP 请求-响应:宿主 POST 一个 prompt 给 Agent,Agent 返回结果。但这套方案有几个硬伤:

  • 长任务不好处理(LLM 生成动辄几十秒,HTTP 长轮询不优雅)
  • 流式输出别扭(要么 SSE、要么 WebSocket,但协议栈变复杂)
  • 最关键的:Agent 想反向调宿主的工具,做不到

问题二:Agent 怎么反向调宿主工具?

Agent 在宿主里跑,经常需要:

  • 读宿主的文件(fs/read_text_file)
  • 写宿主的文件(fs/write_text_file)
  • 开宿主的终端执行命令(terminal/create、terminal/output)
  • 让宿主帮它做鉴权(拿 OAuth token、查用户身份)
  • 让宿主帮它查配额、计量计费

这些工具装在宿主身上,不是装在 Agent 身上。Agent 必须能反向"召唤"宿主来干活。

问题三:宿主怎么管控 Agent?

宿主不能让 Agent 想干嘛就干嘛:

  • 危险工具需要宿主审批
  • 文件写入需要宿主授权工作目录
  • 工具调用需要宿主计量计费
  • 长任务需要宿主能取消

这些都是宿主对 Agent 的管控能力,必须在协议层就支持。

ACP 就是为了解决这三个问题设计的。


二、ACP 在协议生态里的位置

先把 ACP 放进协议生态大图里看:

三层协议各管一段:ACP 管 Client-Agent、MCP 管 Agent-Tool、A2A 管 Agent-Agent

三种协议的边界:

协议谁连谁谁提出的典型场景
ACPClient ↔ AgentZedIDE 调 Codex CLI;Agent 网关调后端 Agent
MCPAgent ↔ ToolAnthropicAgent 调数据库、调搜索、调日历
A2AAgent ↔ AgentGoogle + Linux Foundation一个 Agent 调用另一个外部 Agent

它们的共同点:都用 JSON-RPC 2.0 作为消息信封。 这不是巧合——JSON-RPC 已经成为 Agent 生态事实上的标准消息格式。

一个完整的多 Agent 系统长这样:

[IDE / Agent 网关]
       │ ACP(双向)
       ▼
   [Agent A]
       │ MCP
       ▼
   [Tool Server 1] [Tool Server 2] ...

同时 Agent A 可能通过 A2A 调用 [外部 Agent B]

Codex Harness 的 app-server 就是 ACP 协议的典型实现:IDE 通过 ACP 调 app-server,app-server 再通过 MCP 调外部工具,app-server 也能反向调 IDE 让它读文件、开终端。


三、ACP 协议本身:一根双向管子

ACP 的核心思想可以用一句话概括:JSON-RPC 2.0 + 长连接 + 双向。

3.1 传输:stdio + newline-delimited JSON

ACP 默认用 stdio 作为传输层:

  • 宿主把 Agent 进程 spawn 出来当子进程
  • 宿主跟 Agent 通过 stdin/stdout 通信
  • 每条 JSON-RPC 消息占一行(newline-delimited)

这跟 LSP(Language Server Protocol)的设计一样——ACP 本质上是 LSP 在 Agent 领域的翻版。

为什么不直接用 HTTP?

  • 简单:不用管 HTTP 服务、端口冲突、SSL 证书
  • 安全:stdio 是进程间通道,外部不可达,天然沙箱
  • 持久:子进程跟着宿主一起启停,不用管连接超时
  • 双向:stdin/stdout 天然双向,没有"服务端/客户端"的不对称

3.2 消息信封:JSON-RPC 2.0

三种消息类型:

Request(请求):有 id、有 method、有 params,期望对端回复。

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "method": "session/prompt",
  "params": {"session_id": "abc", "prompt": "帮我修一下这个 bug"}
}

Response(响应):带对应的 id,要么 result 要么 error。

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": {"status": "ok"}
}

Notification(通知):没有 id,单向推送,不期望回复。

{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {"kind": "agent_thought_chunk", "content": {"thought": "Analyzing..."}}
}

3.3 生命周期:五步走

一次典型的 ACP 会话长这样:

ACP 生命周期五步:initialize → authenticate → session/new → session/prompt → session/cancel

① Initialize(握手)

Client 和 Agent 交换支持的协议版本和能力:

// Client → Agent
{"method": "initialize", "params": {"protocolVersion": 1, "clientInfo": {"name": "Zed", "version": "0.165"}}}

// Agent → Client
{"result": {"protocolVersion": 1, "capabilities": {"fs": true, "terminal": true}}}

② Authenticate(鉴权,可选)

Client 给 Agent 提供凭证(API Key、OAuth Token 等)。

③ Session 管理

  • session/new:开一个新会话
  • session/load:加载历史会话
  • session/close:结束会话

④ Prompt & Streaming

Client 发 prompt,Agent 流式回:

  • Client:session/prompt 请求
  • Agent:session/update 通知流式推送思考过程、工具调用、最终答案

⑤ 取消

Client 随时可以 session/cancel 中断当前任务。

3.4 双向:Agent 反过来调 Client

这是 ACP 最精妙的一点——Agent 不只是被动响应,它也能反向调 Client 的方法。

Client 暴露的方法(Agent 可调):

  • fs/read_text_file:读宿主上的文件
  • fs/write_text_file:写宿主上的文件
  • terminal/create:在宿主上开一个终端
  • terminal/output / terminal/kill:操作终端
  • 鉴权、配额、计费相关方法(看宿主实现)

Agent 想读文件时,直接反向发一个 JSON-RPC Request 给 Client:

// Agent → Client
{"id": "req-agent-001", "method": "fs/read_text_file", "params": {"path": "/project/main.py"}}

// Client → Agent
{"id": "req-agent-001", "result": {"content": "def main():\n    pass\n"}}

这一来一回,Agent 就把宿主变成了它的"工具集"。

3.5 工具生命周期:四事件标准化

Agent 调用宿主工具的全过程,被标准化成四个事件:

tool_use_start → tool_input_delta → tool_use_stop → tool_result
  • tool_use_start:Agent 宣布"我要用 X 工具"
  • tool_input_delta:流式传工具参数
  • tool_use_stop:Agent 宣布"参数传完了"
  • tool_result:宿主返回工具结果

宿主可以在 tool_use_start 和 tool_use_stop 之间插入审批流程(比如弹对话框让用户确认)。

3.6 协议分层:应用层 JSON-RPC,底层传输可换

ACP 跟 HTTP 一样是分层协议。把协议栈画清楚:

ACP 协议栈四层:应用层(ACP 方法)、消息层(JSON-RPC 2.0)、序列化、传输层(stdio/WebSocket/TCP)

┌─────────────────────────────────────────┐
│  应用层:ACP 方法                        │
│  initialize / session/new / prompt / ... │
├─────────────────────────────────────────┤
│  消息层:JSON-RPC 2.0                    │
│  Request(id+method+params)               │
│  Response(id+result/error)               │
│  Notification(method+params)             │
├─────────────────────────────────────────┤
│  序列化:newline-delimited JSON          │
│  或 WebSocket text frame                 │
├─────────────────────────────────────────┤
│  传输层:stdio / WebSocket / TCP / ...   │
└─────────────────────────────────────────┘

应用层是固定的——所有 ACP 实现都得支持 initialize / session/new / session/prompt 这些方法。

传输层是可换的——只要满足"双向字节流"这个接口,跑 stdio 还是 WebSocket 还是 TCP 都行。dsh 的 ACP 就是这么设计的:默认 stdio,但 Stream 接口一抽象,底下换啥都行:

// 来自 @agentclientprotocol/sdk 的 Stream 接口
type Stream = {
  writable: WritableStream<AnyMessage>;
  readable: ReadableStream<AnyMessage>;
}

dsh 源码里那行:

const stream: Stream = config.stream ?? ndJsonStream(process.stdout, process.stdin)

config.stream 不为空就用自定义传输,为空就 fallback 到 stdio。这是典型的策略模式——应用层不关心底下用的是啥。

各种传输的使用场景:

传输场景优劣
stdioIDE spawn Agent 子进程(dsh、Codex、Cursor 默认)简单、安全、持久;但只能同机
WebSocket跨机器、跨数据中心、浏览器接 Agent跨网、双向;但有握手开销
TCPAgent 网关、低延迟长连接轻量、快;但没有 WS 的协议特性
Unix Socket同机高性能 IPC比 TCP 快;但只能同机
进程间管道测试、嵌入场景简单可控

四、Agent 网关:ACP 比较有发挥潜力的工程场景

ACP 当前最成熟的场景就是 IDE ↔ Agent(Zed、Cursor、JetBrains 都在用)。但协议本身支持反向调用,这意味着如果未来要做’网关 ↔ 沙箱 Agent’这种分布式部署,ACP 在协议层是天然支持的

4.1 什么是 Agent 网关

一个 Agent 网关是一个独立部署的服务,作为 Client 跟 Agent 之间的中间层。它既代理会话、又提供工具能力、还做鉴权计费。

Agent 网关架构:网关独立部署,双向连接 Agent,向上对接 IDE/Client,向下管工具、鉴权、计费

4.2 网关干什么

一个典型的 Agent 网关负责这几件事:

① 会话代理

Client(IDE、Web 客户端)不直接连 Agent,连网关:

[IDE] ──ACP── [网关] ──ACP── [Agent A]
                        ──ACP── [Agent B]

网关根据请求路由到不同的 Agent,屏蔽后端 Agent 的异构性。

② 工具调用

网关自己挂了一堆工具,Agent 反向调网关来用这些工具:

  • 数据库查询
  • 内部 API 调用
  • 第三方 SaaS 集成

Agent 不需要知道这些工具的鉴权细节,网关替它搞定。

③ 鉴权

Agent 想调某个工具时:

  • Agent 反向问网关:“我能调 X 工具吗?”
  • 网关检查 Agent 的身份、权限、配额
  • 批准了才放行

这层鉴权是Agent 无法绕过的——因为工具根本不在 Agent 进程里,它只能反向求网关。

④ 计量计费

网关记录每一次工具调用:

  • 谁调的(Agent 身份)
  • 什么时候调的
  • 调了多少次
  • 消耗了多少 token / API 调用次数

这些数据可以用来计费、限流、审计。

⑤ 审批流

危险工具调用要过审批。审批逻辑在网关里:

  • 读文件 → 直接放行
  • 写文件 → 让用户确认
  • 删除文件 → 必须二次确认

网关是审批策略的统一定义点,Agent 自己改不了。

4.3 为什么网关要用 ACP

为什么网关不用 HTTP,非要用 ACP 这种长连接?

因为 ACP 的双向性是关键。

HTTP 是"客户端发起、服务端响应"的不对称模型。Agent 想反向调网关的工具,HTTP 做不到(除非再加一套反向通道,复杂)。

ACP 是天然双向的:同一根管子,Client 可以推消息给 Agent,Agent 也可以推消息给 Client。这让 Agent 能很自然地"召唤"网关来干活。

还有流式的好处:Agent 的思考过程、工具调用过程都通过 Notification 流式推给网关,网关再流式推给最终用户。HTTP 做流式要么 SSE 要么 WebSocket,多一层协议栈。


五、ACP 跟 Codex Harness 的关系

聊到这里,你应该能看出来:Codex Harness 的 app-server 就是一个 ACP 协议的实现(或者说 ACP 类协议的实现,因为 Codex 有自己的方法命名)。

Codex app-server 的消息方法(摘自 codex-rs/app-server/README.md):

  • initialize / initialized:握手
  • thread/start、thread/resume:会话管理(ACP 里叫 session)
  • turn/start:发 prompt(ACP 里叫 session/prompt)
  • item/started、item/completed:流式推送(ACP 里叫 session/update)
  • fs/readFile、fs/writeFile:文件操作(ACP 里叫 fs/read_text_file)
  • command/exec:执行命令(ACP 里叫 terminal/create)

方法名不一样,但模式完全一样:JSON-RPC + 双向 + 长连接 + 流式。

Zed 推 ACP 这件事的价值,是把 Codex 这种"私有 JSON-RPC 协议"抽象成一套开放标准。以后不管你用 Codex、Claude Code、还是别的 CLI Agent,IDE 接入都用同一套协议。


六、实际跑一遍 ACP

概念讲完了,看一段真实代码。这里演示 ACP 的分层实现——应用层(JSON-RPC)和传输层(stdio / WebSocket / TCP)各写一份,跑同一段 demo。

完整可运行的 Node 项目见文末链接,下面只抽最关键的部分。

6.1 应用层:一个 Transport 接口搞定

先写应用层——这是跟传输无关的部分:

// jsonrpc.ts —— 应用层,对所有传输一视同仁

type Transport = {
  send(msg: JsonRpcMessage): void
  onMessage(cb: (msg: JsonRpcMessage) => void): void
  close(): void
}

class RpcEndpoint {
  constructor(private transport: Transport) {
    transport.onMessage((msg) => this.onIncoming(msg))
  }

  // 注册本端方法(可被对端调用)
  registerMethod(method: string, handler: (params: any) => Promise<any>) { ... }

  // 主动调对端方法(双向的关键!)
  async request(method: string, params?: any): Promise<any> { ... }

  // 主动发通知(不期望回复)
  notify(method: string, params?: any): void { ... }
}

关键设计:同一个 RpcEndpoint 既能"接收"也能"发起"调用。 Client 和 Agent 各有一个 RpcEndpoint,双向对称。

6.2 三种传输:实现 Transport 接口

// ── stdio:默认方式 ──
function stdioTransport(): Transport {
  const rl = createInterface({ input: process.stdin })
  const cbs: ((m: JsonRpcMessage) => void)[] = []
  rl.on('line', (line) => cbs.forEach(cb => cb(JSON.parse(line))))
  return {
    send(msg) { process.stdout.write(JSON.stringify(msg) + '\n') },
    onMessage(cb) { cbs.push(cb) },
    close() { rl.close(); process.exit(0) },
  }
}

// ── WebSocket:跨机器用 ──
function wsTransport(ws: WebSocket): Transport {
  const cbs: ((m: JsonRpcMessage) => void)[] = []
  ws.on('message', (data) => cbs.forEach(cb => cb(JSON.parse(data.toString()))))
  return {
    send(msg) { ws.send(JSON.stringify(msg)) },
    onMessage(cb) { cbs.push(cb) },
    close() { ws.close() },
  }
}

// ── TCP:低延迟长连接 ──
function tcpTransport(socket: net.Socket): Transport {
  const rl = createInterface({ input: socket })
  const cbs: ((m: JsonRpcMessage) => void)[] = []
  rl.on('line', (line) => cbs.forEach(cb => cb(JSON.parse(line))))
  return {
    send(msg) { socket.write(JSON.stringify(msg) + '\n') },
    onMessage(cb) { cbs.push(cb) },
    close() { rl.close(); socket.destroy() },
  }
}

三种实现都长得很像——因为它们都在做同一件事:把"双向字节流"包装成"JSON-RPC 消息流"。应用层代码一行都不用改。

6.3 Agent 端:一个 Echo Agent

function makeEchoAgent(transport: Transport): RpcEndpoint {
  const endpoint = new RpcEndpoint(transport)

  endpoint.registerMethod('initialize', () => ({
    protocolVersion: 1,
    agentInfo: { name: 'echo-agent', version: '1.0.0' },
    capabilities: {},
  }))

  endpoint.registerMethod('session/new', (params) => ({
    sessionId: `sess-${Math.random().toString(36).slice(2)}`,
  }))

  endpoint.registerMethod('session/prompt', async (params) => {
    // ★ 反向调 Client 的工具 ★ —— 这就是双向的精髓
    const file = await endpoint.request('fs/read_text_file', {
      path: `${params.cwd}/demo.txt`,
    })

    // 流式推一条通知
    endpoint.notify('session/update', {
      sessionId: params.sessionId,
      update: { sessionUpdate: 'agent_message_chunk', content: file.content },
    })

    return { stopReason: 'end_turn' }
  })

  return endpoint
}

6.4 Client 端:挂上工具、接收反向调用

const client = new RpcEndpoint(transport)

// 注册 Client 侧工具:Agent 可以反向调 fs/read_text_file
client.registerMethod('fs/read_text_file', (params) => ({
  content: readFileSync(params.path, 'utf-8'),
}))

// 监听 Agent 流式推送的 session/update
client.onNotification('session/update', (params) => {
  process.stdout.write(`🤖 ${params.update.content}\n`)
})

// 走一遍生命周期
await client.request('initialize', { protocolVersion: 1 })
const { sessionId } = await client.request('session/new', { cwd: process.cwd() })
await client.request('session/prompt', { sessionId, prompt: 'Hi!' })

6.5 三种传输跑同一个 demo

下面是同一个 demo 在三种传输上跑出来的真实日志(摘录):

stdio 模式:

═══════════════════════════════════════════════════════════
  ACP Demo · 传输层:stdio(子进程 spawn)
═══════════════════════════════════════════════════════════
▶️  Client: initialize
  ✅ Agent 回复: {"protocolVersion":1,"agentInfo":{"name":"echo-agent"...}}
▶️  Client: session/new
  ✅ 会话创建: sessionId=sess-tss0ww
▶️  Client: session/prompt("Hello from ACP client!")
  📂 [Client 侧工具被调] fs/read_text_file(path=.../demo.txt)
  🤖 [Agent] 附:我从 Client 上读到了文件 ...
🎉 Demo 完成

WebSocket 模式:

═══════════════════════════════════════════════════════════
  ACP Demo · 传输层:WebSocket
═══════════════════════════════════════════════════════════
🟢 Agent Server listening on ws://localhost:8765
🔵 Client: 已连上 Agent Server
...(后续输出完全一致)

TCP 模式:

═══════════════════════════════════════════════════════════
  ACP Demo · 传输层:TCP(裸 socket,newline-delimited JSON)
═══════════════════════════════════════════════════════════
🟢 Agent Server listening on tcp://localhost:8766
🔵 Client: 已连上 Agent Server
...(后续输出完全一致)

三份日志除了开头那行传输类型,后面一模一样——这就是分层协议的价值:应用层代码只写一次,传输层随便换。

完整可运行的 Node 项目(含 tsconfig、package.json、完整源码)见:

👉 acp-node-demo/ 目录(跟本文同仓库)

跑法:

cd acp-node-demo
pnpm install
pnpm run:stdio   # stdio 模式(dsh/Codex 默认)
pnpm run:ws      # WebSocket 模式
pnpm run:tcp     # TCP 模式

七、ACP 为什么重要

理由一:让 Agent 可以"寄宿"在宿主里

过去 Agent 是个独立进程,跑自己的代码、用自己的工具。ACP 之后,Agent 可以像寄生虫一样寄宿在 IDE、网关、Web 客户端里,借用宿主的一切能力(文件系统、终端、鉴权、计费)。

这让"Agent 嵌入产品"变得非常轻量——产品只需要实现 ACP 客户端,就能接入任何 ACP 兼容的 Agent。

理由二:标准化 IDE-Agent 交互

Codex 的 app-server、Claude Code、Cursor 的 Agent 集成……这些以前都是各家自己写一套 JSON-RPC 方言。ACP 把这些方言统一了。

以后 Zed、Cursor、VS Code 这些 IDE 只要支持 ACP,就能接入任何 ACP 兼容的 Agent——跟当年 LSP 让所有编辑器都能接入所有语言服务器是一个逻辑。

理由三:Agent 网关成为可能

有了 ACP,Agent 网关这种架构才顺理成章:

  • 网关独立部署,对接多个 Agent
  • 网关提供工具、鉴权、计费
  • Agent 反向调网关来用工具
  • 网关代理会话、审批危险操作

这种架构非常适合企业级部署——Agent 是核心智能,网关是基础设施。两者解耦,各自独立演进。

理由四:跟 MCP/A2A 形成完整的协议栈

  • MCP 解决"Agent 怎么接工具"
  • ACP 解决"Agent 怎么被宿主接入"
  • A2A 解决"Agent 怎么互相调用"

三个协议正交互补,拼起来才是完整的 Agent 互操作性方案。Codex Harness、dsh、Claude Code 这些 Agent Harness 要真正成为生态级的基础设施,三个协议都得支持。


写在最后

如果这一篇只记住一件事,我希望是这句:

ACP 让 Agent 能寄宿在宿主里——用一根双向管子把 Agent 跟 Client 连起来,让 Agent 既能被宿主调用、也能反过来调宿主的能力。

这跟 MCP(Agent 调工具)和 A2A(Agent 调 Agent)是正交的。三个协议拼起来,才是 Agent 生态的完整协议栈。

Codex Harness 的 app-server 已经是 ACP 类协议的实现了(方法名不同但模式一致)。Zed 推 ACP 标准化,就是想让所有 IDE 跟所有 Agent 之间,不用每家都重写一套集成。

Agent 网关是 ACP 最有想象力的工程场景——独立部署、双向连接、代理会话、代理鉴权计费。这才是企业级 Agent 部署该有的样子。


相关参考资料

ACP 官方

  • ACP 协议官网:https://zed.dev/acp
  • ACP 协议 GitHub:https://github.com/agentclientprotocol/agent-client-protocol
  • ACP 参考实现(Elixir):https://hexdocs.pm/acpex/protocol_overview.html
  • Pydantic AI 的 ACP harness:https://pydantic.dev/docs/ai/harness/acp/

dsh 的 ACP 实现(TypeScript,本文参考的源码)

  • dsh 的 ACP 桥接:packages/acp/acp/src/index.ts in https://github.com/deepseek-ai/deepseek-harness
  • dsh 用的官方 SDK:@agentclientprotocol/sdk(npm)

Codex app-server(ACP 类协议实现)

  • Codex app-server 文档:https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md

协议生态对照

  • MCP 官方:https://modelcontextprotocol.io/
  • A2A 官方:https://github.com/a2aproject/a2a
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐