跳转到内容

ACP 协议与 JSON-RPC 声明式路由

ACP 协议与 JSON-RPC 声明式路由

学习目标

读完本章后,你将能够:

  • 理解 ACP(Agent Client Protocol)的设计动机和定位
  • 使用过程宏实现声明式 JSON-RPC 路由,消除样板代码
  • 设计双传输模式(HTTP + Stdio)的协议层
  • 区分 ACP 与 MCP 的适用场景

前置知识


核心概念

1. 什么是 ACP

ACP(Agent Client Protocol) 是一种将 AI Agent 嵌入外部应用的标准化协议。它允许桌面应用、IDE 插件、终端工具等客户端通过 JSON-RPC 与 Agent 通信,控制会话、处理工具确认、管理扩展。

ACP vs MCP 的定位差异:

维度MCPACP
目标暴露工具、资源、提示词暴露整个 Agent
角色Server 提供工具,Host 消费Agent 提供能力,Client 控制
场景一个 Agent 调用多个工具服务一个客户端控制一个 Agent
粒度工具级会话级

MCP 回答「这个工具能做什么」,ACP 回答「这个 Agent 能做什么」。一个 Agent 可以通过 MCP 调用外部工具,同时通过 ACP 向外部客户端暴露自身。

2. 传输层

ACP 支持两种传输模式:

传输启动方式适用场景
HTTP / WebSocketgoose serve(默认 127.0.0.1:3284)桌面应用、Web 前端
Stdiogoose 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> { ... }
}

宏生成两个方法:

  1. handle_custom_request(method, params) — 字符串匹配的路由分发器
  2. 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")只定义在请求结构体上一次,路由和处理函数自动对齐。添加新方法只需:

  1. 定义请求结构体 + #[request(method = "...")]
  2. 写处理函数 + #[custom_method(RequestType)]
  3. 不需要修改路由分发器

派发流程

标准 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)不太适合描述这种交互。


参考来源