ACP 协议与 JSON-RPC 声明式路由
ACP 协议与 JSON-RPC 声明式路由
学习目标
读完本章后,你将能够:
- 理解 ACP(Agent Client Protocol)的设计动机和定位
- 使用过程宏实现声明式 JSON-RPC 路由,消除样板代码
- 设计双传输模式(HTTP + Stdio)的协议层
- 区分 ACP 与 MCP 的适用场景
前置知识
- MCP 协议与生态集成
- JSON-RPC 2.0 基础
核心概念
1. 什么是 ACP
ACP(Agent Client Protocol) 是一种将 AI Agent 嵌入外部应用的标准化协议。它允许桌面应用、IDE 插件、终端工具等客户端通过 JSON-RPC 与 Agent 通信,控制会话、处理工具确认、管理扩展。
ACP vs MCP 的定位差异:
| 维度 | MCP | ACP |
|---|---|---|
| 目标 | 暴露工具、资源、提示词 | 暴露整个 Agent |
| 角色 | Server 提供工具,Host 消费 | Agent 提供能力,Client 控制 |
| 场景 | 一个 Agent 调用多个工具服务 | 一个客户端控制一个 Agent |
| 粒度 | 工具级 | 会话级 |
MCP 回答「这个工具能做什么」,ACP 回答「这个 Agent 能做什么」。一个 Agent 可以通过 MCP 调用外部工具,同时通过 ACP 向外部客户端暴露自身。
2. 传输层
ACP 支持两种传输模式:
| 传输 | 启动方式 | 适用场景 |
|---|---|---|
| HTTP / WebSocket | goose serve(默认 127.0.0.1:3284) | 桌面应用、Web 前端 |
| Stdio | goose acp | 进程间通信、CLI 管道 |
两种传输共享同一套路由逻辑,只是底层 I/O 不同。
3. ACP 暴露的核心能力
设计模式详解
声明式路由宏:#[custom_methods]
ACP 包含 40+ 个自定义方法(addExtension、getTools、reply、…),如果手动编写路由分发器:
// 手动路由的样板代码(每增加一个方法都要改)match method { "addExtension" => { let req: AddExtensionRequest = serde_json::from_value(params)?; let resp = self.handle_add_extension(req).await?; Ok(serde_json::to_value(resp)?) } "getTools" => { let req: GetToolsRequest = serde_json::from_value(params)?; ... } // 40+ 个重复的 arm}#[custom_methods] 过程宏消除了这种样板代码:
#[custom_methods]impl GooseAcpAgent { // 第一个方法:路由入口(无 #[custom_method] 标注) pub async fn dispatch_custom_request( &self, method: &str, params: serde_json::Value, ) -> Result<serde_json::Value, Error> { self.handle_custom_request(method, params).await }
// 每个带 #[custom_method] 的方法自动注册为路由目标 #[custom_method(AddExtensionRequest)] async fn dispatch_add_extension( &self, req: AddExtensionRequest, ) -> Result<EmptyResponse, Error> { ... }
#[custom_method(GetToolsRequest)] async fn dispatch_get_tools( &self, req: GetToolsRequest, ) -> Result<ToolsResponse, Error> { ... }}宏生成两个方法:
handle_custom_request(method, params)— 字符串匹配的路由分发器custom_method_schemas()— 为所有方法生成 JSON Schema(用于 OpenAPI 文档)
路由匹配机制
路由不使用硬编码的字符串,而是通过 <RequestType>::matches_method(method) 匹配:
// 请求类型定义(在 agent-client-protocol-schema crate 中)#[derive(JsonRpcRequest)]#[request(method = "addExtension")]struct AddExtensionRequest { ... }
// 匹配时:if <AddExtensionRequest as JsonRpcMessage>::matches_method(method) { let req = serde_json::from_value(params)?; // 自动反序列化 let result = self.dispatch_add_extension(req).await?; return serde_json::to_value(&result)?; // 自动序列化}关键优势:方法名("addExtension")只定义在请求结构体上一次,路由和处理函数自动对齐。添加新方法只需:
- 定义请求结构体 +
#[request(method = "...")] - 写处理函数 +
#[custom_method(RequestType)] - 不需要修改路由分发器
派发流程
标准 ACP 方法(PromptRequest、NewSessionRequest 等)由 MatchDispatchFrom 匹配。未匹配的自定义方法通过 .otherwise() 路由到 dispatch_custom_request(),由宏生成的 handle_custom_request() 分发。
问题与规避
方法名冲突
问题:两个请求结构体定义了相同的 method 字符串。
对策:JsonRpcRequest 派生宏在编译时检查方法名唯一性。重复的方法名会导致编译错误。
类型不匹配
问题:处理函数的参数类型与请求结构体不一致。
对策:#[custom_method(RequestType)] 在编译时验证:处理函数的第一个参数类型必须与 RequestType 一致。不一致时编译失败。
未知方法处理
问题:客户端发送了未注册的方法名。
对策:handle_custom_request() 遍历所有注册的方法后,如果都没有匹配,返回 Error::method_not_found()。这是 JSON-RPC 2.0 的标准错误码。
Schema 生成性能
问题:每次调用 custom_method_schemas() 都要为 40+ 个方法生成 JSON Schema。
对策:
- 跳过
serde_json::Value类型的参数(不生成 Schema) - 使用
schemars::SchemaGenerator缓存已生成的 Schema - 仅在首次调用时生成,结果可缓存
设计取舍
过程宏路由 vs 手动匹配
| 维度 | 过程宏路由 | 手动匹配 |
|---|---|---|
| 新增方法成本 | 写一个函数 + 一个标注 | 写函数 + 修改路由 arm |
| 编译时检查 | 类型自动验证 | 容易遗漏 |
| 调试体验 | 宏展开后代码晦涩 | 直接可读 |
| 灵活性 | 受限于宏的约束 | 完全自由 |
当方法数量超过 10 个时,过程宏路由的可维护性优势显著。但调试宏生成的代码需要 cargo expand 工具。
ACP vs 直接 HTTP API
| 维度 | ACP (JSON-RPC) | REST API |
|---|---|---|
| 协议 | JSON-RPC 2.0 标准 | 自定义 RESTful |
| 双向通信 | 通过 WebSocket | 通过 SSE(单向) |
| 方法发现 | JSON Schema 自动生成 | 需要 OpenAPI/Swagger |
| 错误处理 | 标准 JSON-RPC error | 自定义 HTTP 状态码 |
| 生态兼容 | 兼容所有 JSON-RPC 客户端 | 需要自定义客户端 |
ACP 选择 JSON-RPC 而非 REST,因为 Agent 的交互模式更适合 RPC:客户端发起操作(prompt、confirm、configure),Agent 返回结果。REST 的资源模型(CRUD)不太适合描述这种交互。