Skip to content

通信协议

无法说同一种语言的代理不是团队。它们只是对着虚空大喊的陌生人。

类型: 构建 语言: 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:去中心化身份与信任

本课会深入到底层。你将阅读每个规范中的真实线协议格式,构建可运行的实现,并把这四种协议连接成一个统一系统。

核心概念

协议全景

可以把这四种协议看成不同层,每一层都在回答一个不同的问题:

mermaid
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 如何连接外部工具和数据源。它是一个客户端-服务器协议,代理(客户端)会发现并调用服务器暴露的工具。

mermaid
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 如何工作

mermaid
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 提供:

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 中最核心的工作单元。它们会在一组定义好的状态之间流转:

mermaid
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。下面是真实消息交换的样子:

客户端发送任务:

json
{
  "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
    }
  }
}

代理返回任务:

json
{
  "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 流式传输:

text
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(轨迹元数据):每个代理响应都可以携带一份详细日志,记录产出该响应的推理步骤与工具调用。

mermaid
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、reasoning

ACP 中的代理发现

ACP 定义了四种发现方式:

mermaid
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:#7c3aed

AgentManifest 比 A2A 的 Agent Card 更简单:

json
{
  "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} 轮询状态。
streamSSE 流。代理工作时事件会持续发出。
mermaid
stateDiagram-v2
    [*] --> 已创建
    已创建 --> 进行中
    进行中 --> 已完成: 成功
    进行中 --> 已失败: 错误
    进行中 --> 等待中: 需要输入
    等待中 --> 进行中: 客户端恢复
    进行中 --> 取消中: 取消请求
    取消中 --> 已取消

    已完成 --> [*]
    已失败 --> [*]
    已取消 --> [*]

TrajectoryMetadata(审计轨迹)

这是 ACP 最关键的差异化能力。每个消息片段都可以携带元数据,精确展示代理到底做了什么:

json
{
  "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(引用元数据) 来做来源归因:

json
{
  "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 有三层:

mermaid
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:#7c3aed

DID 文档(真实结构)

ANP 使用一种名为 did:wba(Web-Based Agent)的自定义 DID 方法。DID did:wba:example.com:user:alice 会解析到 https://example.com/user/alice/did.json

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。它的信任是双边的,并且在每次交互时单独验证:

mermaid
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/>+ 最小信任原则

信任来自三个来源:

  1. 域级 TLS:验证 DID 文档的宿主
  2. DID 密码学签名:验证代理身份
  3. 最小信任原则:只授予最小必要权限

这里没有基于 gossip 的信任传播,也没有 PageRank 式评分。你是通过它的 DID 直接验证每个代理。

元协议协商

这是 ANP 最有新意的特性。当两个来自不同生态的代理相遇时,它们不需要预先约定好数据格式。它们可以用自然语言协商:

json
{
  "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"
}
mermaid
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 轮),直到就某种格式达成一致,然后动态生成代码来处理它。状态值包括:negotiatingrejectedacceptedtimeout

这意味着,两个此前从未见过彼此的代理,也能在无人预先定义共享 schema 的情况下,自己找出通信方式。

对比(修正版)

MCPA2AACPANP
创建者AnthropicGoogle / Linux FoundationIBM / BeeAI社区
规范格式JSON-RPCJSON-RPC / REST / gRPCOpenAPI 3.1(REST)JSON-RPC
主要用途代理到工具代理到代理代理到代理代理到代理
发现方式工具列表/.well-known/agent-card.jsonGET /agents/.well-known/agent.yml/.well-known/agent-descriptions、DID service endpoints
身份隐式(本地)安全方案(OAuth、mTLS)服务器级带 E2EE 的 W3C DID(did:wba
审计轨迹N/A基础(任务历史)TrajectoryMetadata(工具调用、推理)未正式规定
状态机N/A9 种任务状态7 种 Run 状态N/A
流式传输N/ASSESSE与传输层无关
独特特性工具 schemaAgent Cards + SkillsTrajectory 审计轨迹元协议协商
最适用场景工具与数据动态协作受监管行业跨组织信任
状态稳定稳定(v1.0)正在并入 A2A活跃开发中

它们如何协同工作

这些协议并不互斥。一个现实的企业系统往往会同时使用多个:

mermaid
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:核心消息类型

每个多代理系统都从一种消息格式开始。我们定义一些类型,映射真实协议中使用的结构:

typescript
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 规范一致的代理发现机制:

typescript
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 任务生命周期

构建完整的任务状态机:

typescript
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 风格的审计轨迹

给通信过程包上一层轨迹追踪:

typescript
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 的身份与验证:

typescript
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:协议网关

把这四种协议接入同一个统一系统:

mermaid
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:#2563eb
typescript
class 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
    );
  }
}

这个网关在一次调用里做了四件事:

  1. ANP:通过 DID 签名验证调用方身份
  2. A2A:发现目标代理并检查能力
  3. ACP:用带轨迹的审计记录包装整个执行过程
  4. A2A:创建一个具备完整生命周期跟踪的任务

步骤 7:把一切接起来

typescript
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 就是标准。

选择合适的协议

mermaid
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

练习

  1. 多跳任务委派。 扩展 TaskManager,让代理处理器能够把子任务委派给其他代理。研究代理收到任务后,把“搜索”和“总结”两个子任务分别委派给两个专用代理,等待二者都完成,再把结果合并进自己的 artifacts。

  2. 流式审计轨迹。 修改 AuditableRunner,让它支持流式模式。不要等完整结果返回,而是在添加轨迹条目时实时产出 AuditEntry 更新。使用 async generator 来生成审计快照。

  3. DID 轮换。IdentityRegistry 增加密钥轮换。代理应该能够发布一个带更新密钥的新 DID 文档,同时保留一个 previousDid 引用。在宽限期内,验证方应同时接受当前密钥和上一把密钥的签名。

  4. 协议协商。 实现 ANP 的元协议概念。两个代理交换带候选格式的 protocolNegotiation 消息(例如“我会 JSON-RPC”对“我更喜欢 REST”)。最多经过 3 轮后,它们要么就某个格式达成一致,要么超时。协商结果将决定它们使用哪个 TaskManagerAuditableRunner

  5. 限流发现。 增加一个 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 中有状态的对象,用于从提交一路跟踪到完成。一旦进入终态就不可变。

延伸阅读