跳转到内容

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 方法(PromptRequestNewSessionRequest 等)由 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)不太适合描述这种交互。


参考来源