Skip to content

以ACP实现Agent连接层

这篇文章记录的是我给 FylloCode 在 Agent Runtime 接入层上的一次核心技术选型:为什么没有选择逐个适配 CLI,而是把 ACP 放在 Agent 连接层,同时在内部消息层留出解耦和回退空间。

初识 ACP

在玩 OpenClaw 的时候,我想用它来驱动 Claude CodeCodex 完成一些 Coding 任务,搜索方案时在 GitHub 找到了 OpenClaw 的 acpx,进而发现了它只是一个 headless 的 ACP Client,底层使用 ACP 协议通过 JSON-RPC 2.0 与 Coding Agent 交互。

我做 FylloCode 的本意是让它驱动用户本地的 Coding Agent,常规来讲一般需要定义一个兼容层,然后逐个接入 CLI,并把每个 CLI 的个性化行为、参数都归一化到这个兼容层,由兼容层对外提供服务。如果这样来做的话,可以预想到的是 FylloCode 的大部分开发工作可能会落在这些 CLI 的兼容适配,但这并不应该是 FylloCode 的重心。

于是我又想到了 ACP,开始仔细研究它。

ACP 是由 Zed 最早推出,后面与 IntelliJ 一起合作推进,它确实做到了这种兼容层协议。基于 ACP 有两种身份,一种是 Agent,一种是 Client。

当前版本(v1)提供的基础功能包括:

Agent 能力

能力说明
imageprompt 中支持图像
audioprompt 中支持音频
embeddedContextprompt 中支持嵌入内容,比如文本块、文件
MCP支持 MCP 连接(stdio、http、sse)
additionalDirectories支持会话开始时附加目录,以扩展会话的有效工作目录
authentication在建立连接期间做身份验证,比如需要 login、或是配置 API-Key 等
session/new创建新会话
session/promptclient 发送用户内容
session/cancel主动取消会话
session/update会话过程中的数据更新,例如用户发送 prompt 后的流式数据、Agent 当前模式的变更、plan 列表、slash commands 等

会话设置

设置项说明
mode会话期间的模式,Agent 会返回支持的模式,比如 YOLOAccept EditsBypass Permissions
model会话期间的模型,Agent 会返回支持的模型列表

ACP 同时也支持了文件系统和终端,让 Agent 可以直接把文件读写、命令执行直接交给 Client 来做。

ACP 也提供了 registry,所有支持 ACP 的 Agent 都可以提交进来,可以理解为它是一份官方维护的 Agent 列表。

在 4 月份的时候 ACP registry 中已经有 20+ Agent,截止到今天,已经有了近 40 个 Agent,而且还有很多已经支持 ACP 但未提交到 registry 的 Agent 产品。

这对 FylloCode 很关键,因为我并不想让它变成一个 CLI 适配器集合。只要 Agent 愿意走 ACP,FylloCode 就可以把更多精力放在会话组织、任务流转和上下文沉淀上,而不是反复处理不同 CLI 的启动参数和输出格式。

集成 ACP

ACP 看起来是个标准协议,而且也在持续更新,在权衡之后,决定给 FylloCode 直接接入 ACP + registry,来避免 CLI 的逐个集成。

ACP 天然适合池化,每个 Agent 建立一个连接,一个连接可以维护多个 session,在用户与 Agent 对话时,我们只需要拿到 session/update 事件,然后推送到渲染层即可。

这件事在 FylloCode 里不是一个简单的 SDK 调用。ACP Agent 是长期存活的外部进程,需要有进程池管理启动、退出、重试和清理;session 是业务层可感知的会话资源,需要从 Agent 进程生命周期中拆出来;而流式事件又要能被前端稳定消费,所以中间必须有一层 runtime 来处理连接、session、事件映射和错误收敛。

但是我并没有选择直接推送 ACP 消息,目的是为了做一次解耦,为了防止未来 ACP 更新不活跃或者废弃时,还有后路可退,所以我选择了一个中间层 ai-sdk。选择 ai-sdk 的原因也很简单,因为它同时支持服务层 + 渲染层:

  • 服务层支持接入多个厂商的模型,这就意味着它自身的消息格式是中庸的,而且可扩展。未来也许会基于 ai-sdk 做一个 FylloCode 自己的 Agent;
  • 渲染层支持 ReactVue,而且前段时间 Vercel 收购了 Nuxt,Nuxt 直接放出了一个 Chat UI 的套件,功能丰富,扩展性强。

这里如果直接把 ACP 消息透传到前端,短期看会更省事,但前端就会被 ACP 的事件结构绑死。后面无论是 ACP schema 变化,还是我想接入非 ACP 的 Agent,都会变成一次比较大的迁移。

所以现在接入路径就比较清晰了:

这条线串下来,核心逻辑很简单,而且退一万步讲,如果真的 ACP 出现了问题,我也可以快速回退,以 ai-sdk 直接接入 LLM 的方式支持下去。

目前 FylloCode 已经围绕这条链路实现了 ACP process pool、session 管理、session/update 到内部事件的映射,以及多 Agent 的 tool_call 兼容处理。也就是说,ACP 在 FylloCode 里已经进入核心会话链路的基础设施。

强人所难的 ACP Adapter

用了 ACP 两个月,我有明显的感觉,这就是传统 IDE 厂商的自救。他们希望借助于 ACP,号称用户可以不离开 IDE 就可以用上多种 Agent。如果他们下场做自己的 Agent,将会迎来腹背受敌:一面是 IDE 厂商的竞争,一面是 Agent 厂商的打压。所以 ACP 不失为一个好的方向。

我最开始给 FylloCode 接入 ACP 的时候还比较早,之前大家用的 Agent 无外乎 Claude CodeCodexGemini CLIOpenCode 这几个,Gemini CLIOpenCode 是开源的,它们很快就跟进了 ACP,而且支持度很高。但是另外两个老大哥始终没有原生支持,所以 ACP 官方直接做了两个 Adapter。

Claude Agent 是基于 Claude Agent SDK 实现的,Codex ACP 是基于 Codex 的 module 实现的,这两种方式其实有些不稳,功能会比官方的 Agent 要滞后,而且 Claude 发布公告,订阅用户使用 Claude Agent SDK 是另外的额度……

Adapter 的问题在于,它很难真正代表原生体验。上游 CLI 或 SDK 的能力一变,Adapter 就要跟着追;而且很多细节不是“能不能跑”的问题,而是“能不能稳定地表达 Agent 当前在做什么”的问题。对 Client 来说,这些差异最后都会变成 UI 状态、权限请求、工具调用展示上的兼容成本。

但是好在,目前越来越多的 Agent 原生支持了 ACP,像 QodercliKimi Code 等,国产模型厂商还在持续发力,我们也不必被 Claude 一家锁死。

“开放”的 ACP

ACP 太过开放了,为了不给 Agent 产品增加难度,协议 schema 中很多事件,甚至很多字段都是可选的。最让我受不了的是 tool_call 事件,ACP 协议对 tool_call 字段的定义几乎全部是可选的,除 toolCallIdtitle 以外,status / kind / rawInput / rawOutput / content / locations 均为可选,而且协议没有规定各字段出现的时机。这意味着各 Agent 的差异行为都是协议合法的,想获得好的体验 Client 必须自己做兼容。

我测试了 5 款 Agent:Claude AgentCodex ACPGemini CLIOpenCodeQodercli,测试结果在这里。一句话,太乱了。迫不得已再给这些 tool_call 做一个兼容层。

这类问题单看协议字段可能只是小差异,但放到产品里就会很明显。比如同样是一次文件编辑,有的 Agent 会先给出工具标题,有的会先给出 diff,有的直到 completed 才补齐内容。如果 Client 不做额外整理,用户看到的就不是一个稳定的工具调用流程,而是一堆时序不一致的事件碎片。

最后我没有选择针对每个 Agent 写一串 if/else,而是把 tool_calltool_call_update 统一当成字段位置不稳定的事件处理。无论 rawInputcontentlocations 出现在 start 还是 update,都使用同一套提取逻辑;跨事件的补偿则交给下游 assembler 做 lazy-upsert,而不是让 mapper 自己维护状态。

这个边界对 Agent Runtime 很重要。mapper 只负责把单个 ACP 事件归一成内部事件,assembler 才负责把多个事件组装成用户能理解的消息。这样兼容逻辑不会堆成一个巨大的状态机,也方便后面继续接入新的 Agent。

ACP 的未来

近期 ACP 开始规划 v2 版本的 RFDs 了,一些问题也在逐渐被优化,而且近期的会议中提到未来的 ACP 将是 “Not only for Editors”,意味着这个协议将会转向更加开放。

所以我对 ACP 的态度其实很矛盾:它确实解决了 FylloCode 最不想做的那部分工作,让我不用一个个去适配 CLI;但它现在也把很多兼容成本留给了 Client。对我来说,ACP 仍然值得押注,只是不能天真地把它当成一个已经完全稳定的标准。

如果只把 ACP 当 API,很多问题最后都会推到 UI 层爆出来。FylloCode 的做法是把它当成 Agent Runtime 的接入层:进程要能被治理,session 要能被管理,事件要能被归一,协议风险也要能被隔离。只有这些底层东西稳住,上层的 Agent 工作流才有继续扩展的空间。

基于 MIT 发布