Gemini CLI ACP 模式详解:通过 Agent Client Protocol 将 Agent 接入 IDE 的架构与实践
ACP(Agent Client Protocol,Agent Client Protocol)是 Gemini CLI 面向程序化集成的专用运行模式:它通过 stdio 上的 JSON-RPC 2.0 协议在 IDE 等客户端与 Gemini CLI Agent 之间建立双向通道,让编辑器可以以编程方式创建会话、发送提示词、切换审批模式并代理文件系统访问。读完本文,你将理解 gemini --acp 的完整工作原理——从进程启动到协议握手、MCP 双向集成、文件系统代理的安全边界,以及如何用调试日志与遥测排查协议问题,并能在自己的 IDE 插件或工具中实现一个 ACP 客户端来驱动 Gemini CLI。
什么是 ACP 模式,如何启动
ACP 模式是 Gemini CLI 的一种特殊运行模式,专为 IDE 和其他开发者工具的集成而设计。与交互式终端模式不同,ACP 模式下 Gemini CLI 不再渲染 TUI 界面,而是作为服务器常驻,等待客户端通过标准输入/输出发送 JSON-RPC 请求。
启动方式很简单:
gemini --acp
从源码看,--acp 是一个布尔型命令行选项,在 config.ts 中定义;此外还有一个已弃用的旧选项 --experimental-acp,其帮助文本明确写着 "Starts the agent in ACP mode (deprecated, use --acp instead)"。解析逻辑位于同一文件:
const isAcpMode = !!argv.acp || !!argv.experimentalAcp;
当 isAcpMode 为真时,配置对象会设置 acpMode: true,并在检测到 IDE 环境时把客户端名称标记为 acp-<ide.name>,否则标记为 acp——这个 clientName 会被写入遥测事件,用于区分 ACP 通道流量与普通终端流量。值得注意的是,config.test.ts 中还有专门的用例验证 --acp 模式会排除 ask_user 工具(因为 ACP 客户端有自己的提问通道,Agent 不应再使用终端式追问)。
为什么需要 ACP:解决客户端碎片化问题
ACP 是一个开放协议,标准化了 AI 编码 Agent 与代码编辑器/IDE 之间的通信方式。它解决的核心痛点是集成碎片化:在没有 ACP 之前,每个 Agent 想接入一个新的编辑器,都要为每个客户端写一套定制集成代码,维护成本随编辑器数量线性膨胀。
引入 ACP 之后,开发者只需实现一次 Agent 侧的协议,它就能兼容任何遵守 ACP 规范的编辑器——集成的复杂度从 M×N 降为 M+N。Gemini CLI 正是这样一个 ACP 兼容 Agent,可通过 ACP Agent Registry 在各类 IDE 中分发;关于如何在 JetBrains、Zed 等具体 IDE 中配置和使用,参见仓库中的 IDE 集成文档。
架构:stdio 上的 JSON-RPC 客户端-服务器模型
ACP 模式在客户端(你的 IDE/工具)与 Gemini CLI(Agent 服务器)之间建立客户端-服务器关系:
- 通信载体:全部通信走标准输入/输出(stdio),采用 JSON-RPC 2.0 协议;
- 客户端职责:发送请求(如提示词),处理来自 Gemini CLI 的响应与通知;
- Gemini CLI 职责:监听入站 JSON-RPC 请求,处理后返回响应,并流式推送中间更新。
传输层:ndjson 流与 AgentSideConnection
原始 I/O 由 acpStdioTransport.ts 承担。runAcpClient 函数把进程 stdin/stdout 包装为 Web Streams,并用行分隔 JSON(ndjson)构建 AgentSideConnection:
const stream = acp.ndJsonStream(stdout, stdin);
const connection = new acp.AgentSideConnection(
(connection) => new GeminiAgent(config, settings, argv, connection),
stream,
);
// SIGTERM/SIGINT handlers (in sdk.ts) don't fire when stdin closes.
// We must explicitly await the connection close to flush telemetry.
await connection.closed.finally(runExitCleanup);
两个细节值得注意:其一,协议消息以行分隔 JSON 而非裸 JSON 拼接传输,这保证了消息边界清晰、可增量解析;其二,源码注释指出 stdin 关闭时常规的信号处理不会触发,因此必须显式 await connection.closed 再执行清理,以确保证遥测数据(telemetry)在进程退出前刷盘。
协议分发层:GeminiAgent
入站 JSON-RPC 消息的主入口是 acpRpcDispatcher.ts 中的 GeminiAgent 类。它实现协议方法,并把会话相关工作委托给会话管理器与具体会话对象。文档中提到的"ACP 实现核心"在早期版本集中于单一文件,当前仓库已完成模块化重构——如 ACP 模块 README 所述,职责被拆分为:
| 模块 | 职责 |
|---|---|
| acpStdioTransport.ts | 原始 I/O,建立 ndjson 流与 AgentSideConnection |
| acpRpcDispatcher.ts | GeminiAgent,协议方法的 RPC 入口 |
| acpSessionManager.ts | 多会话状态管理(newSession / loadSession / 配置合并) |
| acpSession.ts | 单个会话:提示词执行、@path 文件解析、工具执行、流式更新 |
| acpUtils.ts | 共享辅助函数、类型映射与 Zod schema |
| acpErrors.ts | 集中式错误处理与 ACP 错误码映射 |
| acpCommandHandler.ts | 拦截并执行经 ACP 提示词发送的斜杠命令(如 /memory、/init) |
| acpFileSystemService.ts | 受工作区边界与权限约束的文件系统访问 |
初始化握手:能力协商与认证方法
initialize 是整个 ACP 会话的起点。查看 acpRpcDispatcher.ts 中的 initialize 实现,可以清楚看到响应中向客户端声明的内容:
- 协议版本:
protocolVersion: acp.PROTOCOL_VERSION(来自@agentclientprotocol/sdk); - 可用认证方法(
authMethods):login_with_google—— 使用 Google 账户登录;use_gemini—— 使用 Gemini Developer API 的 API Key;use_vertex_ai—— 使用 Vertex AI GenAI API;gateway—— 自定义 AI API Gateway(元数据中标注protocol: 'google')。
- Agent 信息:
agentInfo返回name: 'gemini-cli'、标题与版本号; - Agent 能力(
agentCapabilities):loadSession: true—— 支持恢复既有会话;promptCapabilities—— 提示词支持image、audio、embeddedContext三类内嵌内容;mcpCapabilities—— 支持http与sse两种 MCP 传输。
同时 initialize 会读取客户端上报的 clientCapabilities 并存入会话管理器,供后续判断客户端是否支持文件系统代理等特性。
authenticate 方法(acpRpcDispatcher.ts)则完成实际的凭据切换:用 Zod 的 z.nativeEnum(AuthType) 校验客户端传入的 methodId;若请求的 _meta 中携带 api-key 或 gateway(含 baseUrl 与自定义 headers)字段,会先做 schema 安全解析(解析失败抛出 -32602 的 Malformed gateway payload 错误),再调用 config.refreshAuth 刷新认证,并把所选认证类型写回用户级设置。切换认证方式时会先清除缓存的凭据文件,避免旧凭据残留。
核心方法一览
ACP 协议向客户端暴露的方法覆盖了会话全生命周期:
| 方法 | 说明 | 源码位置 |
|---|---|---|
initialize |
建立初始连接,协商协议版本/能力,客户端在此注册其 MCP server | acpRpcDispatcher.ts |
authenticate |
按客户端指定的方法完成用户认证 | acpRpcDispatcher.ts |
newSession |
启动新的聊天会话 | acpSessionManager.ts |
loadSession |
加载/恢复先前会话(依赖 loadSession 能力) |
acpSessionManager.ts |
prompt |
向 Agent 发送提示词 | acpRpcDispatcher.ts |
cancel |
取消正在进行的提示词 | acpRpcDispatcher.ts |
所有涉及会话的方法都会先通过 sessionManager.getSession(sessionId) 查找会话,找不到即抛出 -32602(Invalid params)语义的 Session not found: <sessionId> 错误,错误映射逻辑集中在 acpErrors.ts。prompt 执行进入 acpSession.ts 后,会话层负责提示词执行、@path 文件引用解析、工具执行,并把流式更新回推给客户端;斜杠命令(如 /init、/memory)则被 acpCommandHandler.ts 拦截并就地执行,不会发给模型。
会话控制
两个与常规请求不同的会话级控制方法:
setSessionMode:切换工具调用的审批级别,例如切到auto-approve模式后工具调用无需逐次确认,适合自动化场景;unstable_setSessionModel:更改当前会话使用的模型。方法名中的unstable前缀表明该接口属于协议中尚未稳定的扩展点,未来签名可能调整,集成时应对该能力做特性探测而非硬依赖。
两者同样通过 acpRpcDispatcher.ts 委托给对应会话对象的 setMode / setModel 实现。
与 MCP 的双向集成
ACP 可以与 MCP(Model Context Protocol)配合使用,形成一个双向集成回路:IDE 不仅接收 Agent 的输出,还能把自己的功能以"工具"形式暴露给 Gemini 模型调用。工作流程如下:
- 客户端实现一个 MCP server 并声明自己的工具;
- 在 ACP 的
newSession/loadSession请求中,客户端通过mcpServers字段提供 MCP server 的连接详情; - Gemini CLI 连接到该 MCP server,发现可用工具并注册给模型;
- 模型决定使用某个工具时,Gemini CLI 向该 MCP server 发出工具调用请求。
从源码看,合并逻辑在 acpSessionManager.ts:newSessionConfig 以本地设置中的 mcpServers 为基座,把客户端随会话请求传来的每个 server 逐个构造为 MCPServerConfig 后覆盖合并,再交给会话配置。结合 initialize 声明的 mcpCapabilities: { http: true, sse: true },可以确认当前支持的传输形态是 HTTP 与 SSE。MCP 客户端的底层逻辑位于 core 包的 mcp-client 实现(文档中路径为 packages/core/src/tools/mcp-client.ts)。这套机制意味着 Agent 可以借助 IDE 自身的能力(如"打开某文件"、"读取活动编辑器内容")来执行任务,而不局限于本地文件与 shell。
文件系统代理:以客户端为边界的访问控制
ACP 内置了一个代理式文件系统服务:当 Agent 需要读写文件时,请求经由 ACP 通道转发给客户端执行,而不是直接操作本机磁盘。这是一个安全特性——Agent 只能访问客户端(以及其背后的用户)明确授权的文件范围,例如 IDE 可以直接复用编辑器的缓冲区内容,实现"所见即所得"的读写一致性。
实现见 acpFileSystemService.ts。该类实现了 core 包的 FileSystemService 接口,核心路由逻辑在 shouldUseFallback:
private shouldUseFallback(filePath: string): boolean {
// 工作区之外的文件,以及 ~/.gemini 目录内的文件,始终走本地文件系统
return (
!isWithinRoot(filePath, this.root) ||
isWithinRoot(filePath, this.geminiDir)
);
}
两条规则值得注意:
- 能力探测:
readTextFile/writeTextFile前先检查客户端上报的capabilities.readTextFile/capabilities.writeTextFile,客户端不支持代理时直接回退本地实现(this.fallback); - 强制本地路径:工作区根(
root)之外的文件走本地文件系统;同时~/.gemini目录被显式排除在代理之外——注释解释了原因:全局 CLI 目录必须始终用原生文件系统访问,即使用户直接在家目录运行 CLI(此时 IDE 项目根会与全局目录重叠)。
此外,normalizeFileSystemError 会把客户端返回的各种"文件不存在"表述(Resource not found、ENOENT、does not exist 等)统一归一化为标准 ENOENT 错误,保证 Agent 侧错误处理的一致性。相关行为在 acpFileSystemService.test.ts 中有对应测试覆盖。
调试与遥测
调试日志
启用通用调试日志只需追加 --debug:
gemini --acp --debug
遥测文件输出
更细粒度的事件数据可通过以下环境变量捕获到本地文件:
GEMINI_TELEMETRY_ENABLED=true \
GEMINI_TELEMETRY_TARGET=local \
GEMINI_TELEMETRY_OUTFILE=/path/to/your/log.json \
gemini --acp
这会产生一个 JSON 日志文件,记录 Agent 内部发生的所有事件,包括 ACP 请求与响应的详细信息。仓库中 acp-telemetry.test.ts 集成测试提供了一个可直接参考的端到端示例,演示了如何设置上述环境变量并验证遥测输出;acp-env-auth.test.ts 则覆盖了 ACP 进程通过环境变量完成认证的场景。结合 acpStdioTransport.ts 中"必须显式等待连接关闭以刷出遥测"的清理逻辑,可以推断:排查"遥测文件末尾缺失事件"类问题时,应优先确认客户端是正常关闭了 stdin 而非强制杀进程。
小结
ACP 模式让 Gemini CLI 从一个终端交互工具变成了一个可被任意编辑器驱动的协议化 Agent 服务:传输层用 stdio + ndjson 保持零端口、零守护进程部署的简洁性;协议层通过 initialize 能力协商、会话级 prompt/cancel 控制、setSessionMode 审批切换构成完整的控制面;MCP 集成与文件系统代理则分别扩展了 Agent 的"手"与约束了它的"脚"。模块化的 packages/cli/src/acp 目录与并排的测试文件(acpRpcDispatcher.test.ts、acpSessionManager.test.ts、acpResume.test.ts 等)也为进一步阅读实现或为客户端编写集成提供了清晰的落点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00