通信协议
无法说同一种语言的代理不是团队。它们只是对着虚空大喊的陌生人。
类型: 构建 语言: TypeScript 先修要求: 第 14 阶段(Agent Engineering),课程 16.01(Why Multi-Agent) 耗时: ~120 分钟
学习目标
- 实现 MCP 的工具发现与调用,让代理能够使用外部服务器暴露的工具
- 构建 A2A Agent Card 和任务端点,让一个代理能够通过 HTTP 把工作委派给另一个代理
- 对比 MCP(工具访问)、A2A(代理到代理)、ACP(企业审计)和 ANP(去中心化信任),并解释每种协议分别解决什么问题
- 在单一系统中把多种协议接起来:代理通过 MCP 发现工具,通过 A2A 委派任务
问题
你把系统拆成了多个代理。研究代理、编码代理、审查代理。它们各自都很擅长自己的工作。但现在你需要它们真正彼此交流。
你的第一次尝试很直接:传字符串。研究代理返回一大段文本,编码代理尽力去解析。起初能跑,直到编码代理误解了研究摘要,或者两个代理彼此等待导致死锁,或者你需要让不同团队构建的代理一起协作。此时,“直接传字符串”就突然崩了。
这就是通信协议 (communication protocol) 问题。没有一个共享契约来规定代理如何交换信息,多代理系统就会变得脆弱、不可审计,而且根本无法扩展到你亲手写的少数几个代理之外。
AI 生态对此给出了四种协议,每一种都解决这个问题的不同切面:
- MCP:工具访问
- A2A:代理到代理协作
- ACP:企业级可审计性
- ANP:去中心化身份与信任
本课会深入到底层。你将阅读每个规范中的真实线协议格式,构建可运行的实现,并把这四种协议连接成一个统一系统。
核心概念
协议全景
可以把这四种协议看成不同层,每一层都在回答一个不同的问题:
block-beta
columns 1
block:ANP["ANP — 代理如何信任陌生人?\n去中心化身份 (DID)、E2EE、元协议"]
end
block:A2A["A2A — 代理如何围绕目标协作?\nAgent Cards、任务生命周期、流式传输、协商"]
end
block:ACP["ACP — 代理如何在可审计系统中通信?\nRuns、轨迹元数据、会话连续性"]
end
block:MCP["MCP — 代理如何使用工具?\n工具发现、执行、上下文共享"]
end
style ANP fill:#f3e8ff,stroke:#7c3aed
style A2A fill:#dbeafe,stroke:#2563eb
style ACP fill:#fef3c7,stroke:#d97706
style MCP fill:#d1fae5,stroke:#059669它们不是竞争关系。它们是在不同层级解决不同问题。
MCP(回顾)
MCP 在第 13 阶段已经深入讲过。这里快速回顾一下:MCP 标准化了 LLM 如何连接外部工具和数据源。它是一个客户端-服务器协议,代理(客户端)会发现并调用服务器暴露的工具。
sequenceDiagram
participant Agent as 代理(客户端)
participant MCP1 as MCP 服务器<br/>(数据库、API、文件)
Agent->>MCP1: 列出工具
MCP1-->>Agent: 工具定义
Agent->>MCP1: 调用工具 X
MCP1-->>Agent: 结果MCP 是代理到工具通信。它并不能帮助代理彼此交谈。
A2A(Agent2Agent Protocol)
创建者: Google(现已归入 Linux Foundation,命名空间为 lf.a2a.v1) 规范版本: 1.0.0 问题: 自主代理如何彼此协作、协商并委派任务?
A2A 是用于点对点代理协作的协议。MCP 连接的是代理与工具,而 A2A 连接的是代理与其他代理。每个代理都会在一个众所周知的 URL 上发布一张 Agent Card,其他代理通过它来发现、协商并委派任务。
A2A 如何工作
sequenceDiagram
participant Client as 客户端代理
participant Remote as 远程代理
Client->>Remote: GET /.well-known/agent-card.json
Remote-->>Client: Agent Card(技能、模式、安全)
Client->>Remote: POST /message:send
Remote-->>Client: 任务(已提交 / 处理中)
alt 轮询
Client->>Remote: GET /tasks/{id}
Remote-->>Client: 任务状态 + 产物
else 流式
Client->>Remote: POST /message:stream
Remote-->>Client: SSE: statusUpdate
Remote-->>Client: SSE: artifactUpdate
Remote-->>Client: SSE: completed
end真实的 Agent Card
下面是现实世界里的 A2A Agent Card 实际长什么样。它通过 GET /.well-known/agent-card.json 提供:
{
"name": "Research Agent",
"description": "Searches documentation and summarizes findings",
"version": "1.0.0",
"supportedInterfaces": [
{
"url": "https://research-agent.example.com/a2a/v1",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
},
{
"url": "https://research-agent.example.com/a2a/rest",
"protocolBinding": "HTTP+JSON",
"protocolVersion": "1.0"
}
],
"provider": {
"organization": "Your Company",
"url": "https://example.com"
},
"capabilities": {
"streaming": true,
"pushNotifications": false
},
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["text/plain", "application/json"],
"skills": [
{
"id": "web-research",
"name": "Web Research",
"description": "Searches the web and synthesizes findings",
"tags": ["research", "search", "summarization"],
"examples": ["Research the latest changes in React 19"]
},
{
"id": "doc-analysis",
"name": "Documentation Analysis",
"description": "Reads and analyzes technical documentation",
"tags": ["docs", "analysis"],
"inputModes": ["text/plain", "application/pdf"],
"outputModes": ["application/json"]
}
],
"securitySchemes": {
"bearer": {
"httpAuthSecurityScheme": {
"scheme": "Bearer",
"bearerFormat": "JWT"
}
}
},
"security": [{ "bearer": [] }]
}需要注意的关键点:
- Skills 代表代理能做什么。每个技能都有 ID、标签,以及支持的输入/输出 MIME 类型。客户端代理正是据此判断这个远程代理能否处理自己的请求。
- supportedInterfaces 列出了多种协议绑定。单个代理可以同时支持 JSON-RPC、REST 和 gRPC。
- Security 直接内建在卡片中。客户端在发出任何请求之前,就知道自己需要什么认证。
任务生命周期
任务是 A2A 中最核心的工作单元。它们会在一组定义好的状态之间流转:
stateDiagram-v2
[*] --> 已提交
已提交 --> 处理中
处理中 --> 需要输入: 需要更多信息
需要输入 --> 处理中: 客户端发送数据
处理中 --> 已完成: 成功
处理中 --> 已失败: 错误
处理中 --> 已取消: 客户端取消
已提交 --> 已拒绝: 代理拒绝
已完成 --> [*]
已失败 --> [*]
已取消 --> [*]
已拒绝 --> [*]
note right of 已完成: 终态不可变。\n后续跟进会在同一 contextId\n中创建新任务。全部 8 种状态(规范里还定义了一个作为哨兵值的 UNSPECIFIED,这里省略):
| 状态 | 终态? | 含义 |
|---|---|---|
TASK_STATE_SUBMITTED | 否 | 已确认,但尚未开始处理 |
TASK_STATE_WORKING | 否 | 正在主动处理 |
TASK_STATE_INPUT_REQUIRED | 否 | 代理需要客户端提供更多信息 |
TASK_STATE_AUTH_REQUIRED | 否 | 需要认证 |
TASK_STATE_COMPLETED | 是 | 成功完成 |
TASK_STATE_FAILED | 是 | 处理出错后结束 |
TASK_STATE_CANCELED | 是 | 完成前被取消 |
TASK_STATE_REJECTED | 是 | 代理拒绝该任务 |
任务一旦进入终态,就不可变。不会再有后续消息。后续跟进会在同一 contextId 下创建一个新任务。
线协议格式
A2A 使用 JSON-RPC 2.0。下面是真实消息交换的样子:
客户端发送任务:
{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-001",
"role": "ROLE_USER",
"parts": [{ "text": "Research React 19 compiler features" }]
},
"configuration": {
"acceptedOutputModes": ["text/plain", "application/json"],
"historyLength": 10
}
}
}代理返回任务:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"task": {
"id": "task-abc-123",
"contextId": "ctx-xyz-789",
"status": {
"state": "TASK_STATE_COMPLETED",
"timestamp": "2026-03-27T10:30:00Z"
},
"artifacts": [
{
"artifactId": "art-001",
"name": "research-results",
"parts": [{
"data": {
"findings": [
"React 19 compiler auto-memoizes components",
"No more manual useMemo/useCallback needed",
"Compiler runs at build time, not runtime"
]
},
"mediaType": "application/json"
}]
}
]
}
}
}通过 SSE 流式传输:
POST /message:stream HTTP/1.1
Content-Type: application/json
A2A-Version: 1.0
data: {"task":{"id":"task-123","status":{"state":"TASK_STATE_WORKING"}}}
data: {"statusUpdate":{"taskId":"task-123","status":{"state":"TASK_STATE_WORKING","message":{"role":"ROLE_AGENT","parts":[{"text":"Searching documentation..."}]}}}}
data: {"artifactUpdate":{"taskId":"task-123","artifact":{"artifactId":"art-1","parts":[{"text":"partial findings..."}]},"append":true,"lastChunk":false}}
data: {"statusUpdate":{"taskId":"task-123","status":{"state":"TASK_STATE_COMPLETED"}}}ACP(Agent Communication Protocol)
创建者: IBM / BeeAI 规范版本: 0.2.0(OpenAPI 3.1.1) 状态: 正在并入 Linux Foundation 下的 A2A 问题: 代理如何在具备完整可审计性、会话连续性和轨迹跟踪的情况下通信?
ACP 是企业级协议。和许多总结里说的不一样,ACP 并不使用 JSON-LD。它本质上是一个通过 OpenAPI 定义的直接 REST/JSON API。它的特别之处在于 TrajectoryMetadata(轨迹元数据):每个代理响应都可以携带一份详细日志,记录产出该响应的推理步骤与工具调用。
sequenceDiagram
participant Client as 客户端
participant ACP as ACP 代理
participant Audit as 审计日志
Client->>ACP: POST /runs(mode: sync)
ACP->>ACP: 处理请求...
ACP->>Audit: 记录轨迹:<br/>推理 + 工具调用
ACP-->>Client: 响应 + TrajectoryMetadata
Note over Audit: 每一步都会被记录:<br/>tool_name、tool_input、<br/>tool_output、reasoningACP 中的代理发现
ACP 定义了四种发现方式:
graph LR
A[代理发现] --> B["运行时<br/>GET /agents"]
A --> C["开放<br/>.well-known/agent.yml"]
A --> D["注册表<br/>集中式目录"]
A --> E["嵌入式<br/>容器标签"]
style B fill:#dbeafe,stroke:#2563eb
style C fill:#d1fae5,stroke:#059669
style D fill:#fef3c7,stroke:#d97706
style E fill:#f3e8ff,stroke:#7c3aedAgentManifest 比 A2A 的 Agent Card 更简单:
{
"name": "summarizer",
"description": "Summarizes documents with source citations",
"input_content_types": ["text/plain", "application/pdf"],
"output_content_types": ["text/plain", "application/json"],
"metadata": {
"tags": ["summarization", "RAG"],
"framework": "BeeAI",
"capabilities": [
{
"name": "Document Summarization",
"description": "Condenses long documents into key points"
}
],
"recommended_models": ["llama3.3:70b-instruct-fp16"],
"license": "Apache-2.0",
"programming_language": "Python"
}
}Run 生命周期
ACP 使用 “Runs” 而不是 “Tasks”。一个 Run 就是一次代理执行,它有三种模式:
| 模式 | 行为 |
|---|---|
sync | 阻塞式。响应中直接包含完整结果。 |
async | 立即返回 202。通过 GET /runs/{id} 轮询状态。 |
stream | SSE 流。代理工作时事件会持续发出。 |
stateDiagram-v2
[*] --> 已创建
已创建 --> 进行中
进行中 --> 已完成: 成功
进行中 --> 已失败: 错误
进行中 --> 等待中: 需要输入
等待中 --> 进行中: 客户端恢复
进行中 --> 取消中: 取消请求
取消中 --> 已取消
已完成 --> [*]
已失败 --> [*]
已取消 --> [*]TrajectoryMetadata(审计轨迹)
这是 ACP 最关键的差异化能力。每个消息片段都可以携带元数据,精确展示代理到底做了什么:
{
"role": "agent/researcher",
"parts": [
{
"content_type": "text/plain",
"content": "The weather in San Francisco is 72F and sunny.",
"metadata": {
"kind": "trajectory",
"message": "I need to check the weather for this location",
"tool_name": "weather_api",
"tool_input": { "location": "San Francisco, CA" },
"tool_output": { "temperature": 72, "condition": "sunny" }
}
}
]
}对于受监管行业来说,这非常宝贵。每个答案都附带一条可证明的推理链:调用了哪些工具、用了哪些输入、得到了哪些输出。没有黑箱。
ACP 还支持 CitationMetadata(引用元数据) 来做来源归因:
{
"kind": "citation",
"start_index": 0,
"end_index": 47,
"url": "https://weather.gov/sf",
"title": "NWS San Francisco Forecast"
}ANP(Agent Network Protocol)
创建者: 开源社区(由 GaoWei Chang 发起) 仓库: github.com/agent-network-protocol/AgentNetworkProtocol问题: 来自不同组织的代理如何在没有中心化权威的情况下彼此信任?
ANP 是去中心化身份协议。它使用 W3C 去中心化标识符 (DID) 和端到端加密 (E2EE) 来建立信任。与 A2A 通过已知端点发现代理不同,ANP 允许代理通过密码学方式证明自己的身份。
ANP 有三层:
graph TB
subgraph Layer3["第 3 层:应用协议"]
AD[代理描述文档]
DISC[发现端点]
end
subgraph Layer2["第 2 层:元协议"]
NEG[AI 驱动的协议协商]
CODE[动态代码生成]
end
subgraph Layer1["第 1 层:身份与安全通信"]
DID["did:wba (W3C DID)"]
HPKE[HPKE E2EE - RFC 9180]
SIG[签名验证]
end
Layer3 --> Layer2
Layer2 --> Layer1
style Layer1 fill:#d1fae5,stroke:#059669
style Layer2 fill:#dbeafe,stroke:#2563eb
style Layer3 fill:#f3e8ff,stroke:#7c3aedDID 文档(真实结构)
ANP 使用一种名为 did:wba(Web-Based Agent)的自定义 DID 方法。DID did:wba:example.com:user:alice 会解析到 https://example.com/user/alice/did.json:
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1"
],
"id": "did:wba:example.com:user:alice",
"verificationMethod": [
{
"id": "did:wba:example.com:user:alice#key-1",
"type": "EcdsaSecp256k1VerificationKey2019",
"controller": "did:wba:example.com:user:alice",
"publicKeyJwk": {
"crv": "secp256k1",
"x": "NtngWpJUr-rlNNbs0u-Aa8e16OwSJu6UiFf0Rdo1oJ4",
"y": "qN1jKupJlFsPFc1UkWinqljv4YE0mq_Ickwnjgasvmo",
"kty": "EC"
}
},
{
"id": "did:wba:example.com:user:alice#key-x25519-1",
"type": "X25519KeyAgreementKey2019",
"controller": "did:wba:example.com:user:alice",
"publicKeyMultibase": "z9hFgmPVfmBZwRvFEyniQDBkz9LmV7gDEqytWyGZLmDXE"
}
],
"authentication": [
"did:wba:example.com:user:alice#key-1"
],
"keyAgreement": [
"did:wba:example.com:user:alice#key-x25519-1"
],
"humanAuthorization": [
"did:wba:example.com:user:alice#key-1"
],
"service": [
{
"id": "did:wba:example.com:user:alice#agent-description",
"type": "AgentDescription",
"serviceEndpoint": "https://example.com/agents/alice/ad.json"
}
]
}需要注意的关键点:
- Key separation 是强制的。签名密钥(secp256k1)与加密密钥(X25519)彼此分离。
humanAuthorization是 ANP 独有的。这些密钥在使用前必须得到明确的人类批准(生物识别、密码、HSM)。像资金转账这样的高风险操作会走这条路径。keyAgreement密钥用于 HPKE 端到端加密(RFC 9180)。- service 部分会链接到 Agent Description 文档。
ANP 中的信任如何工作
ANP 不会使用 web-of-trust 或 endorsement graph。它的信任是双边的,并且在每次交互时单独验证:
sequenceDiagram
participant A as 代理 A
participant Domain as 代理 A 的域名
participant B as 代理 B
A->>B: HTTP 请求 + DID + 签名
B->>Domain: 获取 DID 文档 (HTTPS)
Domain-->>B: DID 文档 + 公钥
B->>B: 用公钥验证签名
B-->>A: 签发访问令牌
A->>B: 后续请求使用令牌
Note over A,B: 信任 = TLS 域名验证<br/>+ DID 签名验证<br/>+ 最小信任原则信任来自三个来源:
- 域级 TLS:验证 DID 文档的宿主
- DID 密码学签名:验证代理身份
- 最小信任原则:只授予最小必要权限
这里没有基于 gossip 的信任传播,也没有 PageRank 式评分。你是通过它的 DID 直接验证每个代理。
元协议协商
这是 ANP 最有新意的特性。当两个来自不同生态的代理相遇时,它们不需要预先约定好数据格式。它们可以用自然语言协商:
{
"action": "protocolNegotiation",
"sequenceId": 0,
"candidateProtocols": "I can communicate using:\n1. JSON-RPC with hotel booking schema\n2. REST with OpenAPI 3.1 spec\n3. Natural language over HTTP",
"modificationSummary": "Initial proposal",
"status": "negotiating"
}sequenceDiagram
participant A as 代理 A
participant B as 代理 B
A->>B: protocolNegotiation (candidateProtocols)
B->>A: protocolNegotiation (counter-proposal)
A->>B: protocolNegotiation (accepted)
Note over A,B: 代理会动态生成代码<br/>来处理协商后的格式。<br/>最多 10 轮,之后超时。两个代理会来回协商(最多 10 轮),直到就某种格式达成一致,然后动态生成代码来处理它。状态值包括:negotiating、rejected、accepted、timeout。
这意味着,两个此前从未见过彼此的代理,也能在无人预先定义共享 schema 的情况下,自己找出通信方式。
对比(修正版)
| MCP | A2A | ACP | ANP | |
|---|---|---|---|---|
| 创建者 | Anthropic | Google / Linux Foundation | IBM / BeeAI | 社区 |
| 规范格式 | JSON-RPC | JSON-RPC / REST / gRPC | OpenAPI 3.1(REST) | JSON-RPC |
| 主要用途 | 代理到工具 | 代理到代理 | 代理到代理 | 代理到代理 |
| 发现方式 | 工具列表 | /.well-known/agent-card.json | GET /agents、/.well-known/agent.yml | /.well-known/agent-descriptions、DID service endpoints |
| 身份 | 隐式(本地) | 安全方案(OAuth、mTLS) | 服务器级 | 带 E2EE 的 W3C DID(did:wba) |
| 审计轨迹 | N/A | 基础(任务历史) | TrajectoryMetadata(工具调用、推理) | 未正式规定 |
| 状态机 | N/A | 9 种任务状态 | 7 种 Run 状态 | N/A |
| 流式传输 | N/A | SSE | SSE | 与传输层无关 |
| 独特特性 | 工具 schema | Agent Cards + Skills | Trajectory 审计轨迹 | 元协议协商 |
| 最适用场景 | 工具与数据 | 动态协作 | 受监管行业 | 跨组织信任 |
| 状态 | 稳定 | 稳定(v1.0) | 正在并入 A2A | 活跃开发中 |
它们如何协同工作
这些协议并不互斥。一个现实的企业系统往往会同时使用多个:
graph TB
subgraph org["你的组织"]
RA[研究代理] <-->|A2A| CA[编码代理]
RA -->|MCP| SS[搜索服务器]
CA -->|MCP| GS[GitHub 服务器]
AUDIT["所有代理响应都携带<br/>ACP TrajectoryMetadata"]
end
subgraph ext["外部(通过 ANP 验证 DID)"]
EA[外部代理]
PA[合作方代理]
end
RA <-->|ANP + A2A| EA
CA <-->|ANP + A2A| PA
style org fill:#f8fafc,stroke:#334155
style ext fill:#fef2f2,stroke:#991b1b
style AUDIT fill:#fef3c7,stroke:#d97706- MCP 把每个代理连接到它的工具
- A2A 处理代理之间的协作(内部和外部)
- ACP 用轨迹元数据包装响应,以获得可审计性
- ANP 为你无法控制的代理提供身份验证
动手构建
步骤 1:核心消息类型
每个多代理系统都从一种消息格式开始。我们定义一些类型,映射真实协议中使用的结构:
import crypto from "node:crypto";
type MessageRole = "user" | "agent";
type MessagePart =
| { kind: "text"; text: string }
| { kind: "data"; data: unknown; mediaType: string }
| { kind: "file"; name: string; url: string; mediaType: string };
type TrajectoryEntry = {
reasoning: string;
toolName?: string;
toolInput?: unknown;
toolOutput?: unknown;
timestamp: number;
};
type AgentMessage = {
id: string;
role: MessageRole;
parts: MessagePart[];
trajectory?: TrajectoryEntry[];
replyTo?: string;
timestamp: number;
};
function createMessage(
role: MessageRole,
parts: MessagePart[],
replyTo?: string
): AgentMessage {
return {
id: crypto.randomUUID(),
role,
parts,
replyTo,
timestamp: Date.now(),
};
}
function textMessage(role: MessageRole, text: string): AgentMessage {
return createMessage(role, [{ kind: "text", text }]);
}注意:MessagePart 是多模态的(文本、结构化数据、文件),这和真实的 A2A 与 ACP 规范一样。TrajectoryEntry 则捕获推理链,对应 ACP 里的 TrajectoryMetadata。
步骤 2:A2A Agent Card 与注册表
构建与真实 A2A 规范一致的代理发现机制:
type Skill = {
id: string;
name: string;
description: string;
tags: string[];
inputModes: string[];
outputModes: string[];
};
type AgentCard = {
name: string;
description: string;
version: string;
url: string;
capabilities: {
streaming: boolean;
pushNotifications: boolean;
};
defaultInputModes: string[];
defaultOutputModes: string[];
skills: Skill[];
};
class AgentRegistry {
private cards: Map<string, AgentCard> = new Map();
register(card: AgentCard) {
this.cards.set(card.name, card);
}
discoverBySkillTag(tag: string): AgentCard[] {
return [...this.cards.values()].filter((card) =>
card.skills.some((skill) => skill.tags.includes(tag))
);
}
discoverByInputMode(mimeType: string): AgentCard[] {
return [...this.cards.values()].filter(
(card) =>
card.defaultInputModes.includes(mimeType) ||
card.skills.some((skill) => skill.inputModes.includes(mimeType))
);
}
resolve(name: string): AgentCard | undefined {
return this.cards.get(name);
}
listAll(): AgentCard[] {
return [...this.cards.values()];
}
}这比一个简单的“名称到能力”的映射丰富得多。你可以像真实 A2A 规范那样,按技能标签、输入 MIME 类型或名称来发现代理。
步骤 3:A2A 任务生命周期
构建完整的任务状态机:
type TaskState =
| "submitted"
| "working"
| "input-required"
| "auth-required"
| "completed"
| "failed"
| "canceled"
| "rejected";
const TERMINAL_STATES: TaskState[] = [
"completed",
"failed",
"canceled",
"rejected",
];
type TaskStatus = {
state: TaskState;
message?: AgentMessage;
timestamp: number;
};
type Artifact = {
id: string;
name: string;
parts: MessagePart[];
};
type Task = {
id: string;
contextId: string;
status: TaskStatus;
artifacts: Artifact[];
history: AgentMessage[];
};
type TaskEvent =
| { kind: "statusUpdate"; taskId: string; status: TaskStatus }
| {
kind: "artifactUpdate";
taskId: string;
artifact: Artifact;
append: boolean;
lastChunk: boolean;
};
type TaskHandler = (
task: Task,
message: AgentMessage
) => AsyncGenerator<TaskEvent>;
class TaskManager {
private tasks: Map<string, Task> = new Map();
private handlers: Map<string, TaskHandler> = new Map();
private listeners: Map<string, ((event: TaskEvent) => void)[]> = new Map();
registerHandler(agentName: string, handler: TaskHandler) {
this.handlers.set(agentName, handler);
}
subscribe(taskId: string, listener: (event: TaskEvent) => void) {
const existing = this.listeners.get(taskId) ?? [];
existing.push(listener);
this.listeners.set(taskId, existing);
}
async sendMessage(
agentName: string,
message: AgentMessage,
contextId?: string
): Promise<Task> {
const handler = this.handlers.get(agentName);
if (!handler) {
const task = this.createTask(contextId);
task.status = {
state: "rejected",
timestamp: Date.now(),
message: textMessage("agent", `No handler for ${agentName}`),
};
return task;
}
const task = this.createTask(contextId);
task.history.push(message);
task.status = { state: "submitted", timestamp: Date.now() };
this.processTask(task, handler, message).catch((err) => {
task.status = {
state: "failed",
timestamp: Date.now(),
message: textMessage("agent", String(err)),
};
});
return task;
}
getTask(taskId: string): Task | undefined {
return this.tasks.get(taskId);
}
cancelTask(taskId: string): boolean {
const task = this.tasks.get(taskId);
if (!task || TERMINAL_STATES.includes(task.status.state)) return false;
task.status = { state: "canceled", timestamp: Date.now() };
this.emit(taskId, {
kind: "statusUpdate",
taskId,
status: task.status,
});
return true;
}
private createTask(contextId?: string): Task {
const task: Task = {
id: crypto.randomUUID(),
contextId: contextId ?? crypto.randomUUID(),
status: { state: "submitted", timestamp: Date.now() },
artifacts: [],
history: [],
};
this.tasks.set(task.id, task);
return task;
}
private async processTask(
task: Task,
handler: TaskHandler,
message: AgentMessage
) {
task.status = { state: "working", timestamp: Date.now() };
this.emit(task.id, {
kind: "statusUpdate",
taskId: task.id,
status: task.status,
});
try {
for await (const event of handler(task, message)) {
if (TERMINAL_STATES.includes(task.status.state)) break;
if (event.kind === "statusUpdate") {
task.status = event.status;
}
if (event.kind === "artifactUpdate") {
const existing = task.artifacts.find(
(a) => a.id === event.artifact.id
);
if (existing && event.append) {
existing.parts.push(...event.artifact.parts);
} else {
task.artifacts.push(event.artifact);
}
}
this.emit(task.id, event);
}
} catch (err) {
task.status = {
state: "failed",
timestamp: Date.now(),
message: textMessage("agent", String(err)),
};
this.emit(task.id, {
kind: "statusUpdate",
taskId: task.id,
status: task.status,
});
}
}
private emit(taskId: string, event: TaskEvent) {
for (const listener of this.listeners.get(taskId) ?? []) {
listener(event);
}
}
}这实现了真实的 A2A 任务生命周期:submitted、working、input-required,以及各种终态。处理器是 async generator,会不断产出事件(状态更新和产物分块),这正对应 SSE 流式模型。
步骤 4:ACP 风格的审计轨迹
给通信过程包上一层轨迹追踪:
type AuditEntry = {
runId: string;
agentName: string;
input: AgentMessage[];
output: AgentMessage[];
trajectory: TrajectoryEntry[];
status: "created" | "in-progress" | "completed" | "failed" | "awaiting";
startedAt: number;
completedAt?: number;
sessionId?: string;
};
class AuditableRunner {
private log: AuditEntry[] = [];
private handlers: Map<
string,
(input: AgentMessage[]) => Promise<{
output: AgentMessage[];
trajectory: TrajectoryEntry[];
}>
> = new Map();
registerAgent(
name: string,
handler: (input: AgentMessage[]) => Promise<{
output: AgentMessage[];
trajectory: TrajectoryEntry[];
}>
) {
this.handlers.set(name, handler);
}
async run(
agentName: string,
input: AgentMessage[],
sessionId?: string
): Promise<AuditEntry> {
const entry: AuditEntry = {
runId: crypto.randomUUID(),
agentName,
input: structuredClone(input),
output: [],
trajectory: [],
status: "created",
startedAt: Date.now(),
sessionId,
};
this.log.push(entry);
const handler = this.handlers.get(agentName);
if (!handler) {
entry.status = "failed";
return entry;
}
entry.status = "in-progress";
try {
const result = await handler(input);
entry.output = structuredClone(result.output);
entry.trajectory = structuredClone(result.trajectory);
entry.status = "completed";
entry.completedAt = Date.now();
} catch (err) {
entry.status = "failed";
entry.trajectory.push({
reasoning: `Error: ${String(err)}`,
timestamp: Date.now(),
});
entry.completedAt = Date.now();
}
return entry;
}
getFullAuditLog(): AuditEntry[] {
return structuredClone(this.log);
}
getAuditLogForAgent(agentName: string): AuditEntry[] {
return structuredClone(
this.log.filter((e) => e.agentName === agentName)
);
}
getAuditLogForSession(sessionId: string): AuditEntry[] {
return structuredClone(
this.log.filter((e) => e.sessionId === sessionId)
);
}
getTrajectoryForRun(runId: string): TrajectoryEntry[] {
const entry = this.log.find((e) => e.runId === runId);
return entry ? structuredClone(entry.trajectory) : [];
}
}每一次代理执行都会产生一条完整的审计记录:输入了什么,输出了什么,以及中间完整的工具调用与推理轨迹。你可以按代理查询、按会话查询,也可以按某一次运行单独查询。
步骤 5:ANP 风格的身份验证
构建基于 DID 的身份与验证:
type VerificationMethod = {
id: string;
type: string;
controller: string;
publicKeyDer: string;
};
type DIDDocument = {
id: string;
verificationMethod: VerificationMethod[];
authentication: string[];
keyAgreement: string[];
humanAuthorization: string[];
service: { id: string; type: string; serviceEndpoint: string }[];
};
type AgentIdentity = {
did: string;
document: DIDDocument;
privateKey: crypto.KeyObject;
publicKey: crypto.KeyObject;
};
class IdentityRegistry {
private documents: Map<string, DIDDocument> = new Map();
publish(doc: DIDDocument) {
this.documents.set(doc.id, doc);
}
resolve(did: string): DIDDocument | undefined {
return this.documents.get(did);
}
verify(did: string, signature: string, payload: string): boolean {
const doc = this.documents.get(did);
if (!doc) return false;
const authKeyIds = doc.authentication;
const authKeys = doc.verificationMethod.filter((vm) =>
authKeyIds.includes(vm.id)
);
for (const key of authKeys) {
const publicKey = crypto.createPublicKey({
key: Buffer.from(key.publicKeyDer, "base64"),
format: "der",
type: "spki",
});
const isValid = crypto.verify(
null,
Buffer.from(payload),
publicKey,
Buffer.from(signature, "hex")
);
if (isValid) return true;
}
return false;
}
requiresHumanAuth(did: string, operationKeyId: string): boolean {
const doc = this.documents.get(did);
if (!doc) return false;
return doc.humanAuthorization.includes(operationKeyId);
}
}
function createIdentity(domain: string, agentName: string): AgentIdentity {
const did = `did:wba:${domain}:agent:${agentName}`;
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyDer = publicKey
.export({ format: "der", type: "spki" })
.toString("base64");
const keyId = `${did}#key-1`;
const encKeyId = `${did}#key-x25519-1`;
const document: DIDDocument = {
id: did,
verificationMethod: [
{
id: keyId,
type: "Ed25519VerificationKey2020",
controller: did,
publicKeyDer,
},
{
id: encKeyId,
type: "X25519KeyAgreementKey2019",
controller: did,
publicKeyDer,
},
],
authentication: [keyId],
keyAgreement: [encKeyId],
humanAuthorization: [],
service: [
{
id: `${did}#agent-description`,
type: "AgentDescription",
serviceEndpoint: `https://${domain}/agents/${agentName}/ad.json`,
},
],
};
return { did, document, privateKey, publicKey };
}
function signPayload(identity: AgentIdentity, payload: string): string {
return crypto
.sign(null, Buffer.from(payload), identity.privateKey)
.toString("hex");
}这对应了真实的 ANP 身份模型:代理拥有 DID 文档,其中把认证、密钥协商与人工授权密钥彼此分开。IdentityRegistry 模拟了 DID 解析过程(生产环境里,这通常会变成对代理域名发起 HTTP 获取)。
步骤 6:协议网关
把这四种协议接入同一个统一系统:
graph LR
REQ[传入请求] --> ANP_V{ANP: 验证 DID}
ANP_V -->|有效| A2A_D{A2A: 发现代理}
ANP_V -->|无效| REJECT[拒绝]
A2A_D -->|已找到| ACP_A[ACP: 审计 Run]
A2A_D -->|未找到| REJECT
ACP_A --> A2A_T[A2A: 创建任务]
A2A_T --> RESULT[任务 + 审计记录]
style ANP_V fill:#d1fae5,stroke:#059669
style A2A_D fill:#dbeafe,stroke:#2563eb
style ACP_A fill:#fef3c7,stroke:#d97706
style A2A_T fill:#dbeafe,stroke:#2563ebclass ProtocolGateway {
private registry: AgentRegistry;
private taskManager: TaskManager;
private auditRunner: AuditableRunner;
private identityRegistry: IdentityRegistry;
constructor(
registry: AgentRegistry,
taskManager: TaskManager,
auditRunner: AuditableRunner,
identityRegistry: IdentityRegistry
) {
this.registry = registry;
this.taskManager = taskManager;
this.auditRunner = auditRunner;
this.identityRegistry = identityRegistry;
}
async delegateTask(
fromDid: string,
signature: string,
targetAgent: string,
message: AgentMessage,
sessionId?: string
): Promise<{ task: Task; audit: AuditEntry } | { error: string }> {
if (!this.identityRegistry.verify(fromDid, signature, message.id)) {
return { error: "Identity verification failed" };
}
const card = this.registry.resolve(targetAgent);
if (!card) {
return { error: `Agent ${targetAgent} not found in registry` };
}
const audit = await this.auditRunner.run(
targetAgent,
[message],
sessionId
);
const task = await this.taskManager.sendMessage(targetAgent, message);
return { task, audit };
}
discoverAndDelegate(
fromDid: string,
signature: string,
skillTag: string,
message: AgentMessage
): Promise<{ task: Task; audit: AuditEntry } | { error: string }> {
const candidates = this.registry.discoverBySkillTag(skillTag);
if (candidates.length === 0) {
return Promise.resolve({
error: `No agents found with skill tag: ${skillTag}`,
});
}
return this.delegateTask(
fromDid,
signature,
candidates[0].name,
message
);
}
}这个网关在一次调用里做了四件事:
- ANP:通过 DID 签名验证调用方身份
- A2A:发现目标代理并检查能力
- ACP:用带轨迹的审计记录包装整个执行过程
- A2A:创建一个具备完整生命周期跟踪的任务
步骤 7:把一切接起来
async function protocolDemo() {
const registry = new AgentRegistry();
registry.register({
name: "researcher",
description: "Searches and summarizes findings",
version: "1.0.0",
url: "https://researcher.local/a2a/v1",
capabilities: { streaming: true, pushNotifications: false },
defaultInputModes: ["text/plain"],
defaultOutputModes: ["text/plain", "application/json"],
skills: [
{
id: "web-research",
name: "Web Research",
description: "Searches the web",
tags: ["research", "search", "summarization"],
inputModes: ["text/plain"],
outputModes: ["application/json"],
},
],
});
registry.register({
name: "coder",
description: "Writes code from specs",
version: "1.0.0",
url: "https://coder.local/a2a/v1",
capabilities: { streaming: false, pushNotifications: false },
defaultInputModes: ["text/plain", "application/json"],
defaultOutputModes: ["text/plain"],
skills: [
{
id: "code-gen",
name: "Code Generation",
description: "Generates code",
tags: ["coding", "generation"],
inputModes: ["text/plain", "application/json"],
outputModes: ["text/plain"],
},
],
});
const taskManager = new TaskManager();
const auditRunner = new AuditableRunner();
const researchTrajectory: TrajectoryEntry[] = [];
taskManager.registerHandler(
"researcher",
async function* (task, message) {
yield {
kind: "statusUpdate" as const,
taskId: task.id,
status: { state: "working" as const, timestamp: Date.now() },
};
researchTrajectory.push({
reasoning: "Searching for React 19 documentation",
toolName: "web_search",
toolInput: { query: "React 19 compiler features" },
toolOutput: {
results: ["react.dev/blog/react-19", "github.com/react/react"],
},
timestamp: Date.now(),
});
researchTrajectory.push({
reasoning: "Extracting key findings from search results",
toolName: "doc_analysis",
toolInput: { url: "react.dev/blog/react-19" },
toolOutput: {
summary:
"React 19 compiler auto-memoizes, no manual useMemo needed",
},
timestamp: Date.now(),
});
yield {
kind: "artifactUpdate" as const,
taskId: task.id,
artifact: {
id: crypto.randomUUID(),
name: "research-results",
parts: [
{
kind: "data" as const,
data: {
findings: [
"React 19 compiler auto-memoizes components",
"No more manual useMemo/useCallback needed",
"Compiler runs at build time, not runtime",
],
sources: ["react.dev/blog/react-19"],
},
mediaType: "application/json",
},
],
},
append: false,
lastChunk: true,
};
yield {
kind: "statusUpdate" as const,
taskId: task.id,
status: { state: "completed" as const, timestamp: Date.now() },
};
}
);
auditRunner.registerAgent("researcher", async () => ({
output: [
textMessage("agent", "React 19 compiler auto-memoizes components"),
],
trajectory: researchTrajectory,
}));
const identityRegistry = new IdentityRegistry();
const coderIdentity = createIdentity("coder.local", "coder");
const researcherIdentity = createIdentity("researcher.local", "researcher");
identityRegistry.publish(coderIdentity.document);
identityRegistry.publish(researcherIdentity.document);
const gateway = new ProtocolGateway(
registry,
taskManager,
auditRunner,
identityRegistry
);
console.log("=== Protocol Demo ===\n");
console.log("1. Agent Discovery (A2A)");
const researchAgents = registry.discoverBySkillTag("research");
console.log(
` Found ${researchAgents.length} agent(s):`,
researchAgents.map((a) => a.name)
);
console.log("\n2. Identity Verification (ANP)");
const message = textMessage("user", "Research React 19 compiler features");
const signature = signPayload(coderIdentity, message.id);
const verified = identityRegistry.verify(
coderIdentity.did,
signature,
message.id
);
console.log(` Coder DID: ${coderIdentity.did}`);
console.log(` Signature verified: ${verified}`);
console.log("\n3. Task Delegation (A2A + ACP + ANP)");
const result = await gateway.delegateTask(
coderIdentity.did,
signature,
"researcher",
message,
"session-001"
);
if ("error" in result) {
console.log(` Error: ${result.error}`);
return;
}
console.log(` Task ID: ${result.task.id}`);
console.log(` Task state: ${result.task.status.state}`);
console.log(` Artifacts: ${result.task.artifacts.length}`);
console.log("\n4. Audit Trail (ACP)");
console.log(` Run ID: ${result.audit.runId}`);
console.log(` Status: ${result.audit.status}`);
console.log(` Trajectory steps: ${result.audit.trajectory.length}`);
for (const step of result.audit.trajectory) {
console.log(` - ${step.reasoning}`);
if (step.toolName) {
console.log(` Tool: ${step.toolName}`);
}
}
console.log("\n5. Full Audit Log");
const fullLog = auditRunner.getFullAuditLog();
console.log(` Total runs: ${fullLog.length}`);
for (const entry of fullLog) {
const duration = entry.completedAt
? `${entry.completedAt - entry.startedAt}ms`
: "in-progress";
console.log(` ${entry.agentName}: ${entry.status} (${duration})`);
}
}
protocolDemo().catch((err) => {
console.error("Protocol demo failed:", err);
process.exitCode = 1;
});会出什么问题
协议解决的是理想路径。下面这些问题会在生产环境里出错:
Schema 漂移。 Agent A 发布的 Agent Card 声称自己输出 application/json。但 JSON schema 在版本之间变了。Agent B 仍按旧格式解析,结果得到一堆垃圾。修复方法:为技能和输出 schema 做版本控制。A2A 规范支持在 Agent Card 上加 version,就是为了解决这个问题。
状态机违规。 某个代理处理器先产出了一个 completed 事件,然后还想继续产出更多 artifacts。任务此时已经不可变。你的代码要么静默丢弃这些更新,要么直接抛错。修复方法:在 yield 前先检查是否已进入终态。上面的 TaskManager 就通过终态后的 break 强制执行了这一点。
信任解析失败。 Agent A 试图验证 Agent B 的 DID,但 Agent B 的域名挂了,导致 DID 文档拉不下来。你是要失败开放(接受未验证代理),还是失败关闭(全部拒绝)?ANP 推荐采用失败关闭,并遵循最小信任原则。
轨迹膨胀。 ACP 的轨迹日志能力非常强,但代价也很高。一个复杂代理如果每次运行调用 200 次工具,就会生成极其庞大的审计记录。修复方法:让轨迹日志支持可配置的详细级别。为了合规,记录工具名和输入输出;对于不受监管的工作负载,可以跳过推理步骤。
发现风暴。 50 个代理在启动时同时查询 GET /agents。修复方法:给 Agent Card 加带 TTL 的缓存、错峰发现间隔,或者改用基于推送的注册,而不是轮询。
使用它
真实实现
A2A 是最成熟的。Google 的官方规范已在 Linux Foundation 下开源,提供 Python 和 TypeScript SDK。如果你的代理需要动态发现与协作,就从这里开始。
ACP 正在并入 A2A。IBM 的 BeeAI 项目 把 ACP 作为一个 REST-first 的替代方案提出来,但其中“轨迹元数据”的概念正在被 A2A 生态吸收。即便你把 A2A 作为传输层,也可以采用 ACP 的模式(轨迹日志、Run 生命周期)。
ANP 是最实验性的。社区仓库提供了 Python SDK(AgentConnect)。元协议协商这个概念确实很新,值得为跨组织代理部署持续关注。
MCP 在第 13 阶段已经讲过。如果你想让代理使用工具,MCP 就是标准。
选择合适的协议
graph TD
START{代理是否需要<br/>使用工具?}
START -->|是| MCP_R[使用 MCP]
START -->|否| TALK{代理是否需要<br/>彼此通信?}
TALK -->|否| NONE[你不需要<br/>协议]
TALK -->|是| AUDIT{是否需要为合规提供<br/>审计轨迹?}
AUDIT -->|是| ACP_R[A2A + ACP<br/>轨迹模式]
AUDIT -->|否| ORG{所有代理都在<br/>你的组织内?}
ORG -->|是| A2A_R[A2A<br/>Agent Cards + Tasks]
ORG -->|否| INFRA{是否有共享<br/>基础设施?}
INFRA -->|是| BROKER[A2A + 消息代理]
INFRA -->|否| ANP_R[ANP + A2A<br/>DID 验证]
style MCP_R fill:#d1fae5,stroke:#059669
style A2A_R fill:#dbeafe,stroke:#2563eb
style ACP_R fill:#fef3c7,stroke:#d97706
style ANP_R fill:#f3e8ff,stroke:#7c3aed
style BROKER fill:#e0e7ff,stroke:#4338ca交付成果
本课将产出:
code/main.ts—— 四种协议模式的完整实现outputs/prompt-protocol-selector.md—— 一个帮助你为系统选择协议的 prompt
练习
多跳任务委派。 扩展
TaskManager,让代理处理器能够把子任务委派给其他代理。研究代理收到任务后,把“搜索”和“总结”两个子任务分别委派给两个专用代理,等待二者都完成,再把结果合并进自己的 artifacts。流式审计轨迹。 修改
AuditableRunner,让它支持流式模式。不要等完整结果返回,而是在添加轨迹条目时实时产出AuditEntry更新。使用 async generator 来生成审计快照。DID 轮换。 给
IdentityRegistry增加密钥轮换。代理应该能够发布一个带更新密钥的新 DID 文档,同时保留一个previousDid引用。在宽限期内,验证方应同时接受当前密钥和上一把密钥的签名。协议协商。 实现 ANP 的元协议概念。两个代理交换带候选格式的
protocolNegotiation消息(例如“我会 JSON-RPC”对“我更喜欢 REST”)。最多经过 3 轮后,它们要么就某个格式达成一致,要么超时。协商结果将决定它们使用哪个TaskManager或AuditableRunner。限流发现。 增加一个
RateLimitedRegistry包装器,对 Agent Card 查询做带可配置 TTL 的缓存,并限制每个代理每秒的发现查询次数。模拟 100 个代理在启动时彼此发现的风暴,并测量差异。
关键术语
| 术语 | 人们常说什么 | 它实际意味着什么 |
|---|---|---|
| MCP | “AI 工具的协议” | 一种客户端-服务器协议,让代理发现并使用工具。它是代理到工具,不是代理到代理。 |
| A2A | “Google 的代理协议” | Linux Foundation 旗下、用于代理协作的点对点协议。通过 Agent Cards 做发现,拥有 9 状态任务生命周期,并通过 SSE 实现流式传输。支持 JSON-RPC、REST 和 gRPC 绑定。 |
| ACP | “企业代理消息传递” | IBM/BeeAI 的 REST API,用于带 TrajectoryMetadata 的代理运行:每个响应都携带完整的推理链与工具调用。正在并入 A2A。 |
| ANP | “去中心化代理身份” | 社区协议,使用 did:wba(DID)提供密码学身份、使用 HPKE 提供 E2EE,并通过 AI 驱动的元协议协商让彼此从未见过的代理也能通信。 |
| Agent Card | “代理的名片” | 位于 /.well-known/agent-card.json 的 JSON 文档,用来描述技能、支持的 MIME 类型、安全方案和协议绑定。 |
| DID | “去中心化 ID” | 一种 W3C 标准,用于由代理自身域名托管、可被密码学验证的身份。ANP 使用 did:wba 方法。 |
| TrajectoryMetadata | “审计回执” | ACP 的机制,用于把推理步骤、工具调用以及它们的输入/输出附加到每一个代理响应上。 |
| Meta-protocol | “代理协商如何交流” | ANP 的做法:代理先用自然语言动态商定数据格式,再生成处理该格式的代码。 |
| Task | “一个工作单元” | A2A 中有状态的对象,用于从提交一路跟踪到完成。一旦进入终态就不可变。 |
延伸阅读
- Google A2A specification —— 官方规范与 SDK(v1.0.0,Linux Foundation)
- IBM/BeeAI ACP specification —— 用于代理运行和轨迹元数据的 OpenAPI 3.1 规范
- Agent Network Protocol —— 基于 DID 的身份、E2EE、元协议协商
- Model Context Protocol docs —— Anthropic 的 MCP 规范文档(第 13 阶段已覆盖)
- W3C Decentralized Identifiers —— ANP 所依托的身份标准
- RFC 9180 (HPKE) —— ANP 用于 E2EE 的加密方案
- FIPA Agent Communication Language —— 现代代理协议的学术前身