前置知识: AI Agent

通信协议

26 minIntermediate

不能说同一种语言的 Agent 不是团队。它们是向虚空呐喊的陌生人。

通信协议

不能说同一种语言的 Agent 不是团队。它们是向虚空呐喊的陌生人。

类型: 构建 语言: TypeScript 前置条件: Phase 14 (Agent 工程), Lesson 16.01 (为何多Agent) 时间: ~120 分钟

学习目标

  • 实现 MCP 工具发现和调用,使 Agent 能使用外部服务器暴露的工具
  • 构建 A2A Agent Card 和任务端点,允许一个 Agent 通过 HTTP 将工作委派给另一个 Agent
  • 比较 MCP (工具访问)、A2A (Agent 对 Agent)、ACP (企业审计) 和 ANP (去中心化信任),解释哪个协议解决哪个问题
  • 在单个系统中连接多个协议,Agent 通过 MCP 发现工具、通过 A2A 委派任务

问题

你将系统拆分为多个 Agent。一个研究员、一个编码者、一个审查员。它们各自的工作做得很好。但现在你需要它们真正地互相交流。

你的第一次尝试很显然:传递字符串。研究员返回一大段文本,编码者尽其所能解析。这能工作,直到编码者误解了研究摘要,或者两个 Agent 互相等待而死锁,或者你需要不同团队构建的 Agent 协作。突然间”只传字符串”就崩溃了。

这就是通信协议问题。没有 Agent 交换信息的共享契约,多 Agent 系统是脆弱的、不可审计的、无法扩展到少数几个你亲自编写的 Agent 之外的。

AI 生态系统用四个协议做出了回应,每个解决不同层面的问题:

  • MCP 用于工具访问
  • A2A 用于 Agent 间协作
  • ACP 用于企业可审计性
  • ANP 用于去中心化身份和信任

本课程深入探讨。你将阅读每个规范的真实线路格式,构建可工作的实现,并将所有四个连接成一个统一系统。

概念

协议全景

把这四个协议想象成层,每层解决不同的问题:

  • ANP — Agent 如何信任陌生人?去中心化身份 (DID)、E2EE、元协议
  • A2A — Agent 如何在目标上协作?Agent Card、任务生命周期、流式、协商
  • ACP — Agent 如何在可审计系统中通信?运行、轨迹元数据、会话连续性
  • MCP — Agent 如何使用工具?工具发现、执行、上下文共享

它们不是竞争者。它们在不同层面解决不同问题。

MCP (回顾)

MCP 在 Phase 13 中有深入讲解。快速回顾:MCP 标准化了 LLM 如何连接到外部工具和数据源。它是一个客户端-服务器协议,Agent (客户端) 发现并调用服务器暴露的工具。

MCP 是 Agent 到工具 的通信。它不帮助 Agent 之间互相交流。

A2A (Agent2Agent 协议)

创建者: Google (现属于 Linux 基金会,lf.a2a.v1) 规范版本: 1.0.0 问题: 自主 Agent 如何协作、协商和委派任务?

A2A 是对等 Agent 协作的协议。MCP 连接 Agent 到工具,A2A 连接 Agent 到其他 Agent。每个 Agent 在知名 URL 发布 Agent Card,其他 Agent 发现、协商和委派任务给它。

A2A 如何工作

  1. 客户端 Agent 获取远程 Agent 的 Agent Card
  2. 客户端发送消息创建任务
  3. 客户端通过轮询或 SSE 流式接收状态更新和制品

真实的 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 是 Agent 能做的事。每个有 ID、标签和支持的输入/输出 MIME 型。这是客户端 Agent 决定远程 Agent 是否能处理其请求的方式。
  • supportedInterfaces 列出多个协议绑定。单个 Agent 可以同时支持 JSON-RPC、REST 和 gRPC。
  • Security 内置在 Card 中。客户端在发出任何请求之前就知道需要什么认证。

任务生命周期

任务是 A2A 中的核心工作单元。它们在定义的状态间移动:

状态终态?含义
TASK_STATE_SUBMITTED已确认,尚未处理
TASK_STATE_WORKING正在处理
TASK_STATE_INPUT_REQUIREDAgent 需要更多信息
TASK_STATE_AUTH_REQUIRED需要认证
TASK_STATE_COMPLETED成功完成
TASK_STATE_FAILED出错完成
TASK_STATE_CANCELED完成前取消
TASK_STATE_REJECTEDAgent 拒绝了任务

一旦任务到达终态,它就是不可变的。不再有后续消息。后续操作在同一 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
    }
  }
}

Agent 响应任务:

{
  "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) 状态: 正在合并到 A2A (Linux 基金会) 问题: Agent 如何在完全可审计、会话连续和轨迹跟踪的情况下通信?

ACP 是企业协议。与许多摘要声称的不同,ACP 使用 JSON-LD。它是一个通过 OpenAPI 定义的简单 REST/JSON API。它的特殊之处在于 TrajectoryMetadata:每个 Agent 响应可以携带产生它的推理步骤和工具调用的详细日志。

ACP 中的 Agent 发现

ACP 定义了四种发现方法:

  • 运行时GET /agents
  • 开放.well-known/agent.yml
  • 注册表 — 集中目录
  • 嵌入式 — 容器标签

AgentManifest 比 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"
  }
}

运行生命周期

ACP 使用”运行”而不是”任务”。运行是具有三种模式的 Agent 执行:

模式行为
sync阻塞。响应包含完整结果。
async立即返回 202。轮询 GET /runs/{id} 获取状态。
streamSSE 流。Agent 工作时触发事件

TrajectoryMetadata (审计追踪)

这是 ACP 的关键差异化特性。每个消息部分可以包含显示 Agent 确切做了什么的元数据:

{
  "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 问题: 来自不同组织的 Agent 如何在没有中央权威的情况下互相信任?

ANP 是去中心化身份协议。它使用 W3C 去中心化标识符 (DID) 和端到端加密建立信任。与 A2A 通过已知端点发现 Agent 不同,ANP 让 Agent 密码学地证明其身份。

ANP 有三层:

第 3 层:应用协议 — Agent 描述文档、发现端点 第 2 层:元协议 — AI 驱动的协议协商、动态代码生成 第 1 层:身份与安全通信did:wba (W3C DID)、HPKE E2EE (RFC 9180)、签名验证

DID 文档 (真实结构)

ANP 使用自定义 DID 方法 did:wba (基于 Web 的 Agent)。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"
    }
  ]
}

关键要点:

  • 密钥分离 是强制性的。签名密钥 (secp256k1) 与加密密钥 (X25519) 分开。
  • humanAuthorization 是 ANP 独有的。这些密钥在使用前需要明确的人工批准(生物识别、密码、HSM)。资金转账等高风险操作通过此路径。
  • keyAgreement 密钥用于 HPKE 端到端加密 (RFC 9180)。
  • service 部分链接到 Agent 描述文档。

ANP 中的信任如何工作

ANP 使用信任网或背书。信任是双边的,每次交互验证:

  1. Agent A 向 Agent B 发送 HTTP 请求 + DID + 签名
  2. Agent B 从 Agent A 的域获取 DID 文档 (HTTPS)
  3. Agent B 用公钥验证签名
  4. Agent B 颁发访问令牌
  5. Agent A 后续请求使用令牌

信任来自三个来源:

  1. 域级 TLS 验证 DID 文档主机
  2. DID 密码学签名 验证 Agent 身份
  3. 最小信任原则 仅授予最低权限

没有基于流言的信任传播或 PageRank 评分。你通过 DID 直接验证每个 Agent。

元协议协商

这是 ANP 最新颖的功能。当来自不同生态系统的两个 Agent 相遇时,它们不需要预先约定的数据格式。它们用自然语言协商:

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

Agent 来回协商(最多 10 轮),直到就格式达成一致,然后动态生成代码来处理它。状态值:negotiatingrejectedacceptedtimeout

这意味着两个从未见过彼此的 Agent 可以弄清楚如何通信,而不需要任何人预定义共享 schema。

比较 (修正版)

MCPA2AACPANP
创建者AnthropicGoogle / Linux 基金会IBM / BeeAI社区
规范格式JSON-RPCJSON-RPC / REST / gRPCOpenAPI 3.1 (REST)JSON-RPC
主要用途Agent 到工具Agent 到 AgentAgent 到 AgentAgent 到 Agent
发现工具列表/.well-known/agent-card.jsonGET /agents, /.well-known/agent.yml/.well-known/agent-descriptions, DID 服务端点
身份隐式 (本地)安全方案 (OAuth, mTLS)服务器级W3C DID (did:wba) + E2EE
审计追踪N/A基本 (任务历史)TrajectoryMetadata (工具调用、推理)未正式指定
状态机N/A9 个任务状态7 个运行状态N/A
流式N/ASSESSE传输无关
独特功能工具 SchemaAgent Card + Skills轨迹审计追踪元协议协商
最适合工具和数据动态协作受监管行业跨组织信任
状态稳定稳定 (v1.0)合并到 A2A活跃开发

它们如何协同工作

这些协议不是互斥的。一个现实的企业系统使用多个:

  • MCP 连接每个 Agent 到其工具
  • A2A 处理 Agent 之间的协作(内部和外部)
  • ACP 用轨迹元数据包装响应以实现可审计性
  • ANP 为你不可控的 Agent 提供身份验证

构建它

步骤 1:核心消息

每个多 Agent 系统都从消息格式开始。我们定义映射到真实协议使用的型:

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 规范的 Agent 发现:

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()];
  }
}

这比简单的名称-能力映射丰富得多。你可以按技能标签、输入 MIME 型或名称发现 Agent,就像真实 A2A 规范支持的那样。

步骤 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、终态。处理器是异步生成器,产生事件(状态更新和制品块),匹配 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) : [];
  }
}

每个 Agent 执行产生一个完整的审计条目:什么输入、什么输出,以及中间工具调用和推理步骤的完整轨迹。你可以按 Agent、按会话或按单个运行查询。

步骤 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 身份模型:Agent 拥有带有独立认证、密钥协议和人工授权密钥的 DID 文档。IdentityRegistry 模拟 DID 解析(在生产中这将是向 Agent 域的 HTTP 获取)。

步骤 6:协议网关

将所有四个协议连接到一个统一系统:

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:发现目标 Agent 并检查能力
  3. ACP:用轨迹审计追踪包装执行
  4. 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 正是因为这个原因。

状态机违规。 Agent 处理器产生 completed 事件,然后试产生更多制品。任务是不可变的。你的代码静默丢弃更新或抛出异常。修复:在产生之前检查终态。上面的 TaskManager 通过终态后的 break 强制执行这一点。

信任解析失败。 Agent A 尝试验证 Agent B 的 DID,但 Agent B 的域宕机了。DID 文档无法获取。你是失败开放(接受未验证的 Agent)还是失败关闭(拒绝一切)?ANP 推荐使用最小信任原则失败关闭。

轨迹膨胀。 ACP 轨迹日志功能强大但昂贵。一个每次运行进行 200 次工具调用的复杂 Agent 会产生巨大的审计条目。修复:以可配置的详细程度级别记录轨迹。合规记录工具名称和 IO,非监管工作负载跳过推理步骤。

发现惊群。 50 个 Agent 在启动时同时查询 GET /agents。修复:用 TTL 缓存 Agent Card,错开发现间隔,或使用基于推送的注册替代轮询。

使用它

真实实现

A2A 最成熟。Google 的官方规范在 Linux 基金会下开源。Python 和 TypeScript SDK。如果你的 Agent 需要动态发现和协作,从这里开始。

ACP 正在合并到 A2A。IBM 的 BeeAI 项目创建了 ACP 作为 REST 优先的替代,但轨迹元数据概念正在被吸收到 A2A 生态中。即使你使用 A2A 作为传输,也可以使用 ACP 模式(轨迹日志、运行生命周期)。

ANP 最实验性。社区仓库有 Python SDK (AgentConnect)。元协议协商概念真正新颖。值得关注跨组织 Agent 部署。

MCP 已在 Phase 13 中涵盖。如果你想让 Agent 使用工具,MCP 是标准。

选择正确的协议

  • Agent 需要使用工具吗?→ 使用 MCP
  • Agent 需要互相交流吗?→ 继续
  • 需要合规审计追踪吗?→ A2A + ACP 轨迹模式
  • 所有 Agent 都在你的组织内吗?→ A2A (Agent Card + Tasks)
  • 有共享基础设施吗?→ A2A + 消息代理
  • 跨组织无共享基础设施?→ ANP + A2A (DID 验证)

发布它

本课程产生:

  • code/main.ts — 所有四种协议模式的完整实现
  • outputs/prompt-protocol-selector.md — 帮助你为系统选择协议的提示

练习

  1. 多跳任务委派。 扩展 TaskManager,使 Agent 处理器可以将子任务委派给其他 Agent。研究员接收任务,将”搜索”和”摘要”子任务委派给两个专家 Agent,等待两者完成,然后将结果合并到自己的制品中。

  2. 流式审计追踪。 修改 AuditableRunner 以支持流式模式。不等待完整结果,而是在添加轨迹条目时实时产生 AuditEntry 更新。使用产生审计快照的异步生成器

  3. DID 轮换。IdentityRegistry 添加密钥轮换。Agent 应该能够发布带有更新密钥的新 DID 文档,同时维护 previousDid 引用。验证者应在宽限期内接受当前和先前密钥的签名。

  4. 协议协商。 实现 ANP 的元协议概念。两个 Agent 交换带有候选格式的 protocolNegotiation 消息(例如,“我可以说 JSON-RPC” vs “我偏好 REST”)。最多 3 轮后,它们就格式达成一致或超时。约定的格式决定它们使用哪个 TaskManagerAuditableRunner

  5. 限速发现。 添加 RateLimitedRegistry 包装器,用可配置的 TTL 缓存 Agent Card 查找,并限制每个 Agent 每秒的发现查询。模拟 100 个 Agent 在启动时互相发现的惊群,并测量差异。

关键术语

术语人们怎么说实际含义
MCP”AI 工具的协议”Agent 发现和使用工具的客户端-服务器协议。Agent 到工具,不是 Agent 到 Agent。
A2A”Google 的 Agent 协议”Linux 基金会下的对等 Agent 协作协议。通过 Agent Card 发现、9 状态任务生命周期、SSE 流式。支持 JSON-RPC、REST 和 gRPC 绑定。
ACP”企业 Agent 消息传递”IBM/BeeAI 的 Agent 运行 REST API,带 TrajectoryMetadata:每个响应携带完整的推理链和工具调用。正在合并到 A2A。
ANP”去中心化 Agent 身份”社区协议,使用 did:wba (DID) 进行密码学身份验证,HPKE 用于 E2EE,AI 驱动的元协议协商用于从未见过彼此的 Agent。
Agent Card”Agent 的名片”位于 /.well-known/agent-card.json 的 JSON 文档,描述技能、支持的 MIME 型、安全方案和协议绑定。
DID”去中心化 ID”W3C 标准,用于在 Agent 自己的域上托管的密码学可验证身份。ANP 使用 did:wba 方法。
TrajectoryMetadata”审计收据”ACP 的机制,将推理步骤、工具调用及其输入/输出附加到每个 Agent 响应。
元协议”Agent 协商如何交流”ANP 的方法,Agent 使用自然语言动态同意数据格式,然后生成代码来处理它们。
Task”工作单元”A2A 的有状态对象,跟踪从提交到完成的工作。终态后不可变。

延伸阅读