Day93

Agent 互联:A2A、MCP 与 Agent 间协作的协议之争

你的研究员如果是别的团队、别的公司做的 Agent,怎么发现它、调用它、信任它?今天读 A2A 1.0 规范:Agent Card、任务状态机、三种传输绑定、版本协商和鉴权;理清它和 MCP、ACP 的分工;再手写一个最小的 A2A 服务端,把 SupportPilot 包成一个能被别的 Agent 调用的 Agent,并聊聊哪些会成为标准。

读完约 29 分钟

进阶篇 · 第 33 天,共 93 天

今天要解决的问题

到目前为止,你写的多 Agent 系统里,每个 Agent 都是你自己的代码,跑在同一个进程或者同一个集群里,互相调用就是函数调用。现实中的情况会越来越多地变成这样:财务团队有一个对账 Agent,法务团队有一个合同审查 Agent,供应商提供了一个物流查询 Agent。你的 SupportPilot 要处理一个「发票金额不对、要求按合同赔偿」的工单,得请这几个 Agent 帮忙。它们用不同框架写的,部署在不同地方,你看不到它们的提示词和工具。

这时候问题就从「怎么编排」变成了「怎么互联」:怎么知道有哪些 Agent、各自能干什么,怎么给它派一个可能要跑十分钟的任务,它中途需要补充信息时怎么问回来,凭什么相信它,它返回的东西能不能信。A2A(Agent2Agent)协议就是为回答这些问题设计的。

学完今天你应该能:

  1. 说清 MCP、A2A、ACP 三个协议分别解决哪一层的问题,什么时候该用哪个。
  2. 读懂 A2A 1.0 的核心对象:Agent Card、Task、Message、Artifact、Part,以及任务的状态机。
  3. 知道 A2A 的三种传输绑定、版本协商和鉴权方式,能读规范、能发现规范里的小矛盾。
  4. 手写一个最小的 A2A 服务端和客户端,跑通多轮交互、终态拒绝、版本和鉴权检查。
  5. 对「哪些协议会成为标准」形成自己的判断,并能在技术选型里讲清楚。

预计用时 4 小时。前置:Day 22、23(MCP)、Day 26(多 Agent)、Day 84(MCP 网关与鉴权)。

三层协议,各管一段

先把几个经常被混在一起的协议放在一张图里:

你的 Agent SupportPilot 编辑器 / 控制台 Zed、Agent Canvas ACP 工具和数据 账单库、知识库、工单系统 MCP 财务对账 Agent 别的团队 物流查询 Agent 别的公司 A2A A2A
MCP 连工具和数据,A2A 连别的 Agent,ACP 连人用的编辑器或控制台。三者不是竞争关系,一个 Agent 可以同时说这三种「语言」。

MCP 管的是 Agent 和工具、数据之间的连接。工具是透明的:你知道它的参数、能预期它的行为,调用一次通常很快就返回。Day 22、23 已经讲透了。MCP 规范在持续更新,写这篇时官网上最新的版本是 2026-07-28。

A2A 管的是 Agent 和 Agent 之间。关键区别是对方是不透明的(opaque):你看不到它内部用什么模型、什么工具、什么提示词,只知道它声明的能力。任务可能要跑很久,中途可能需要你补充信息,产出可能是文件、结构化数据。规范在「1.2 指导原则」里专门列了一条「Opaque Execution」:Agent 之间基于声明的能力和交换的信息协作,不需要共享内部的思考、计划和工具实现。

ACP(Agent Client Protocol)是另一回事,管的是编辑器或控制台这类「人用的客户端」和 Coding Agent 之间的连接。Day 89 提过,OpenHands 的 Agent Canvas 就是通过 ACP 去跑 Claude Code、Codex 这些第三方 Agent 的。注意别和 IBM 早年的 Agent Communication Protocol 混淆,那个也简称 ACP,已经在 2025 年 8 月并入了 A2A。

一个简单的判断方法:对方是「能力确定的函数」,用 MCP;对方是「有自己判断力、可能要和你来回商量的同事」,用 A2A。

A2A 走到了哪一步

时间线大致是:Google 在 2025 年 4 月发布 A2A,6 月捐给 Linux 基金会,7 月发了 0.3 版。2026 年 3 月 12 日发布 1.0,这是第一个稳定版,而且相对 0.3 有不兼容的改动;5 月 28 日发了 1.0.1。2026 年 8 月,A2A 加入 Linux 基金会旗下的 Agentic AI Foundation(AAIF)。AAIF 的公告里把自己的项目按层次列了一遍:AGENTS.md 管给 Agent 的说明和约定,goose 是 Agent 运行时,MCP 管 Agent 到工具,agentgateway 管流量和策略,A2A 管 Agent 到 Agent。这张分层图和上面那张图的意思是一样的。

Python 这边,官方 SDK 是 PyPI 上的 a2a-sdk,写这篇时版本是 1.2.2(2026-10-19 又发了 1.2.3),基于 protobuf 定义生成类型。今天的练习故意不用 SDK、手写一个最小实现,目的是看清楚线上到底传的是什么;生产项目建议用官方 SDK,省得自己处理所有边界情况。

读规范:五个核心对象

规范在 a2a-protocol.org,仓库里的 docs/specification.md 是同一份内容,specification/a2a.proto 是权威的数据定义。一开始读不用全读,抓住这五个对象:

Agent Card:Agent 的名片,一个 JSON 文档,默认放在 Agent 所在域名下的 /.well-known/agent-card.json。里面有名字、描述、版本、提供方,supportedInterfaces(在哪个 URL、用哪种传输绑定、哪个协议版本可以访问它),capabilities(支不支持流式、推送通知),securitySchemes(怎么鉴权),以及最重要的 skills(能干什么,每个技能带描述和示例)。另外还有需要鉴权才能拿到的「扩展名片」,可以给已认证的客户端看更多信息;1.0 还加了名片签名,用来证明这张名片确实是这个提供方发的:签名格式是 JWS,签之前先按 RFC 8785(JSON Canonicalization Scheme)把 JSON 规范化,否则字段顺序、空格不同就会验签失败。

Task:一次协作的单位,有 id、contextId(把相关的多个任务归到同一个会话里)、状态、产出和历史。

Message:一来一回的一条消息,role 是 ROLE_USER 或 ROLE_AGENT,内容是若干个 Part。

Artifact:任务的产出物,比如一份报告、一个 JSON 结果。和 Message 的区别是,Message 是沟通过程,Artifact 是交付结果。

Part:内容的最小单位,可以是文本、结构化数据、文件(内联或 URL)。

任务的状态机是理解 A2A 的关键:

SUBMITTED WORKING INPUT_REQUIRED AUTH_REQUIRED COMPLETED FAILED CANCELED REJECTED 右侧四个是终态 虚线:用户补充信息后 回到 WORKING
状态名在 1.0 里都带 TASK_STATE_ 前缀。终态的任务不再接受新消息,往终态任务发消息会得到 UnsupportedOperationError。

INPUT_REQUIRED 就是「我需要你补充信息」,客户端带着同一个 taskId 再发一条消息,任务继续。AUTH_REQUIRED 是任务进行中发现需要额外授权(比如要代表用户访问他的邮箱),规范第 7.6 节专门讲了这种任务内授权。右边四个是终态:完成、失败、取消、被拒绝。

三种传输绑定和版本协商

同一套语义,A2A 1.0 定义了三种传输绑定:JSON-RPC、gRPC、HTTP+JSON(REST 风格)。规范第 5.3 节有一张方法对照表,比如「发消息」在 JSON-RPC 里叫 SendMessage,在 REST 里是 POST /message:send;「查任务」是 GetTask 和 GET /tasks/{id}。Agent 在名片的 supportedInterfaces 里声明自己支持哪些,客户端挑一个自己也支持的。

版本协商靠一个请求头:客户端每次请求都带 A2A-Version: 1.0(只写主次版本号,不写补丁号)。服务端不支持这个版本就返回 VersionNotSupportedError。有个细节很容易忽略:没带这个头,服务端要按 0.3 处理。这是为了兼容老客户端,但也意味着你的新客户端忘了带头,就会莫名其妙地被当成老版本。

从 0.3 迁到 1.0,最容易出错的是这几处不兼容改动(对照官方仓库的 What’s new in v1.0 整理):枚举值全部改成大写下划线加前缀,"working" 变成 "TASK_STATE_WORKING","user" 变成 "ROLE_USER";Part 和流式事件里用来区分类型的 kind 字段删掉了,改成看对象里有哪个字段(有 text 就是文本,有 data 就是结构化数据);名片上的 preferredTransport 和 additionalInterfaces 合并成 supportedInterfaces 数组,protocolVersion 从名片顶层挪到每个接口里;HTTP+JSON 绑定的 URL 去掉了 /v1 前缀;TaskStatusUpdateEvent 里的 final 字段删了,流什么时候结束改由传输层自己表示。另外 a2a.proto 从「gRPC 专用文件」升级成三种绑定共同的权威定义。老的 0.3 客户端和 1.0 服务端之间,如果没有 A2A-Version 头,服务端会按 0.3 回,所以过渡期两边能凑合着通;一旦你的客户端带上 A2A-Version: 1.0,上面这些字段就得全部按新格式来。

错误怎么表示,每种绑定各有规定。HTTP+JSON 绑定用 google.rpc.Status 的 JSON 格式,details 里必须放一个 ErrorInfo,reason 字段写错误类型(比如 TASK_NOT_FOUND),domain 写 a2a-protocol.org。因为好几种错误都映射到 HTTP 400,只看状态码分不清,必须看 reason。

读规范时我发现了一个小矛盾:第 6.4 节「版本协商错误」的示例响应用的是 application/problem+json 格式,而第 11.6 节规定 HTTP 错误用 google.rpc.Status 格式。遇到这种情况,以规定性的章节(11.6 节)为准,示例只是示例。把这类发现提给规范仓库,也是 Day 109 讲的「开源影响力」的一种很好的起点。

身份、授权和信任

跨团队、跨公司调用 Agent,难的不是协议,是信任。

鉴权:Agent Card 的 securitySchemes 支持 API Key、HTTP(Bearer 等)、OAuth 2.0、OpenID Connect、双向 TLS。凭证不在 A2A 消息里传,而是走 HTTP 层,和普通 API 一样。生产环境里,公司内部用 OAuth 2.0 的 client credentials 或者服务网格的 mTLS,跨公司用 OAuth 2.0 加签名名片。

代表谁在调用:这是最容易出事的地方。SupportPilot 代表某个客户去调用财务 Agent 查账,财务 Agent 看到的身份是「SupportPilot 服务」还是「这个客户」?如果是前者,财务 Agent 就有可能把别的客户的数据返回给 SupportPilot,这就是经典的「困惑的代理人」(confused deputy)问题。设计时要把最终用户的身份和权限范围一起传下去,由被调用方按用户维度做授权,而不是信任调用方说什么就做什么。Day 84 讲 MCP 网关鉴权时是同一个问题。

对方的输出能不能信:另一个 Agent 返回的文本和 Artifact,对你来说就是不可信的外部内容,和网页一样可能带注入。不要把它直接拼进你的系统提示词,不要让它触发你这边的写操作。结构化的 data part 比自由文本更安全,能用就用,并且在你这边做校验。

商务和责任:跨公司调用还牵涉 SLA、计费、数据处理协议、出错时谁负责。这些不在协议里,但会决定一个跨公司的 Agent 协作能不能真的上线。

技术蛋老师 · Function Calling、MCP和A2A的核心原理与区别 · 22 分钟,动画讲三者的关系,适合快速建立整体图景;注意视频录于 1.0 发布之前,具体字段以规范为准。

哪些会成为标准

这部分是我的判断,不是事实,你可以不同意,但面试时要能讲出自己的判断和依据。

MCP 在工具层已经赢了。 主流模型厂商、IDE、框架都支持,企业内部的工具接入基本默认用它。剩下的问题是治理(远程 MCP 的鉴权、工具注册、审计),这是 Day 84 的内容。

A2A 有了标准的形态,但用法还在早期。 有中立治理、有 1.0 稳定版、有主要云厂商和企业软件厂商参与,这些都是成为标准的必要条件。但真实的用法里,大量「多 Agent」需求在同一个团队、同一个进程里就能解决,用子 Agent 或者工作流比跨网络调用简单得多。我看到的 A2A 落地,主要在两种场景:大企业里跨部门的 Agent 平台(各团队把 Agent 注册到同一个目录里),以及 SaaS 厂商把自己的 Agent 能力开放给客户的 Agent 调用。

我的建议:公司内部同一个团队的 Agent 之间,用函数调用、子 Agent 或者内部 API,别为了用协议而用协议;跨团队、需要长时间任务和多轮交互、调用方和被调用方独立演进时,用 A2A;把你的 Agent 开放给外部时,同时提供 MCP(把它当工具用)和 A2A(把它当 Agent 用)两种接口,让调用方选。另外留意 Agent 目录和注册中心的发展,发现机制目前还是协议里最不成熟的部分。

Google Cloud Tech · Introduction to Agent2Agent (A2A) Protocol · 8 分钟,官方介绍 Agent Card、任务和多轮交互的基本概念。

动手练习

目标:手写一个最小的 A2A 1.0 服务端(HTTP+JSON 绑定),把 SupportPilot 包成可被调用的 Agent;写一个客户端,跑通发现、多轮交互、终态拒绝、版本协商和鉴权。

第 1 步:装依赖。

mkdir -p agentops/a2a && cd agentops/a2a
uv init --python 3.12
uv add fastapi uvicorn httpx && uv add --dev pytest

第 2 步:写服务端。 handle() 是 SupportPilot 的替身:没给发票号就进入 INPUT_REQUIRED 要信息,给了就完成并返回一个结构化的 Artifact。

# agentops/a2a/a2a_server.py
"""把 SupportPilot 包成一个 A2A 1.0 Agent(HTTP+JSON 绑定的最小子集)。
实现了:Agent Card 发现、SendMessage、GetTask、多轮(INPUT_REQUIRED)、版本协商、Bearer 鉴权、标准错误格式。
没实现:流式、推送通知、取消、Agent Card 签名。生产里建议直接用官方 SDK(a2a-sdk),这里手写是为了看清协议。"""
from __future__ import annotations

import re
import uuid

from fastapi import FastAPI, Header, Request
from fastapi.responses import JSONResponse

BASE = "/a2a/v1"
SUPPORTED = {"1.0"}
TOKENS = {"demo-token": "partner-billing-agent"}          # 生产里换成 OAuth 2.0 / OIDC
TERMINAL = {"TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_REJECTED"}
CT = "application/a2a+json"

app = FastAPI()
TASKS: dict[str, dict] = {}

CARD = {
    "name": "CloudLedger SupportPilot",
    "description": "CloudLedger 的客服工单 Agent:查询账单、解释退款政策、为重复扣款发起退款申请(需人工审批)。",
    "supportedInterfaces": [
        {"url": f"http://localhost:8000{BASE}", "protocolBinding": "HTTP+JSON", "protocolVersion": "1.0"}
    ],
    "provider": {"organization": "CloudLedger", "url": "https://cloudledger.example"},
    "version": "0.3.0",
    "capabilities": {"streaming": False, "pushNotifications": False, "extendedAgentCard": False},
    "securitySchemes": {"bearer": {"httpAuthSecurityScheme": {"scheme": "Bearer"}}},
    "securityRequirements": [{"schemes": {"bearer": {"list": []}}}],
    "defaultInputModes": ["text/plain"],
    "defaultOutputModes": ["text/plain", "application/json"],
    "skills": [
        {
            "id": "refund-request",
            "name": "退款申请",
            "description": "根据订单号核实是否重复扣款,符合政策的提交退款申请,返回申请编号。",
            "tags": ["billing", "refund"],
            "examples": ["订单 INV-2291 被扣了两次,帮我退一笔"],
        }
    ],
}


def error(http: int, status: str, reason: str, message: str, **meta) -> JSONResponse:
    """HTTP+JSON 绑定的错误格式:google.rpc.Status,details 里放 ErrorInfo。"""
    body = {"error": {"code": http, "status": status, "message": message, "details": [{
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": reason, "domain": "a2a-protocol.org", "metadata": meta,
    }]}}
    return JSONResponse(body, status_code=http, media_type=CT)


def check(version: str | None, auth: str | None) -> JSONResponse | None:
    if (version or "0.3") not in SUPPORTED:      # 规范:没带版本头按 0.3 处理
        return error(400, "FAILED_PRECONDITION", "VERSION_NOT_SUPPORTED",
                     f"A2A version {version or '(empty, treated as 0.3)'} is not supported", supported="1.0")
    token = (auth or "").removeprefix("Bearer ").strip()
    if token not in TOKENS:
        return JSONResponse({"error": {"code": 401, "status": "UNAUTHENTICATED", "message": "missing or invalid token"}},
                            status_code=401, media_type=CT)
    return None


def text_of(message: dict) -> str:
    return " ".join(p.get("text", "") for p in message.get("parts", []))


def agent_msg(text: str, task: dict) -> dict:
    return {"role": "ROLE_AGENT", "messageId": str(uuid.uuid4()), "taskId": task["id"],
            "contextId": task["contextId"], "parts": [{"text": text}]}


def handle(task: dict, user_text: str) -> None:
    """SupportPilot 的替身:真实系统里这里调用 Day 47 的 LangGraph 工作流。"""
    order = re.search(r"INV-\d+", user_text)
    if not order:
        task["status"] = {"state": "TASK_STATE_INPUT_REQUIRED",
                          "message": agent_msg("请提供被重复扣款的发票号(形如 INV-1234)。", task)}
        return
    task["status"] = {"state": "TASK_STATE_COMPLETED"}
    task["artifacts"] = [{
        "artifactId": str(uuid.uuid4()), "name": "退款申请回执",
        "parts": [{"data": {"invoice": order.group(0), "refundRequestId": "RF-" + uuid.uuid4().hex[:8],
                            "state": "pending_supervisor_approval"}}],
    }]


@app.get("/.well-known/agent-card.json")
def agent_card():
    return JSONResponse(CARD, media_type=CT)


@app.post(BASE + "/message:send")
async def send_message(request: Request, a2a_version: str | None = Header(None),
                       authorization: str | None = Header(None)):
    if (bad := check(a2a_version, authorization)) is not None:
        return bad
    msg = (await request.json()).get("message", {})
    if not msg.get("parts"):
        return error(400, "INVALID_ARGUMENT", "INVALID_ARGUMENT", "message.parts is required")
    task_id = msg.get("taskId")
    if task_id:                                          # 多轮:继续已有任务
        task = TASKS.get(task_id)
        if task is None:
            return error(404, "NOT_FOUND", "TASK_NOT_FOUND", "task not found", taskId=task_id)
        if task["status"]["state"] in TERMINAL:
            return error(400, "FAILED_PRECONDITION", "UNSUPPORTED_OPERATION",
                         "task is in a terminal state and cannot accept messages", taskId=task_id)
    else:
        task = {"id": str(uuid.uuid4()), "contextId": msg.get("contextId") or str(uuid.uuid4()),
                "status": {"state": "TASK_STATE_SUBMITTED"}, "history": []}
        TASKS[task["id"]] = task
    task["history"].append(msg)
    handle(task, text_of(msg))
    return JSONResponse({"task": task}, media_type=CT)


@app.get(BASE + "/tasks/{task_id}")
def get_task(task_id: str, a2a_version: str | None = Header(None), authorization: str | None = Header(None)):
    if (bad := check(a2a_version, authorization)) is not None:
        return bad
    task = TASKS.get(task_id)
    if task is None:
        return error(404, "NOT_FOUND", "TASK_NOT_FOUND", "task not found", taskId=task_id)
    return JSONResponse(task, media_type=CT)

第 3 步:写客户端。 客户端只认 Agent Card 的地址,其余信息都从名片里读。

# agentops/a2a/a2a_client.py
"""一个最小的 A2A 客户端:先读 Agent Card,挑一个自己支持的接口,再发消息;遇到 INPUT_REQUIRED 就补充信息继续。"""
from __future__ import annotations

import uuid

import httpx


class A2AClient:
    def __init__(self, http: httpx.Client, card_url: str, token: str):
        self.http = http
        card = http.get(card_url).json()
        iface = next(i for i in card["supportedInterfaces"]
                     if i["protocolBinding"] == "HTTP+JSON" and i["protocolVersion"] == "1.0")
        self.base = iface["url"]
        self.card = card
        self.headers = {"A2A-Version": "1.0", "Authorization": f"Bearer {token}",
                        "Content-Type": "application/a2a+json"}

    def send(self, text: str, task_id: str | None = None) -> dict:
        msg = {"role": "ROLE_USER", "messageId": str(uuid.uuid4()), "parts": [{"text": text}]}
        if task_id:
            msg["taskId"] = task_id
        r = self.http.post(f"{self.base}/message:send", json={"message": msg}, headers=self.headers)
        r.raise_for_status()
        return r.json()["task"]

第 4 步:写测试。 用 FastAPI 的 TestClient,不用真起服务。

# agentops/a2a/test_a2a.py
from fastapi.testclient import TestClient
from a2a_server import app, BASE
from a2a_client import A2AClient

H = {"A2A-Version": "1.0", "Authorization": "Bearer demo-token"}


def client():
    c = TestClient(app, base_url="http://localhost:8000")
    return c, A2AClient(c, "http://localhost:8000/.well-known/agent-card.json", "demo-token")


def test_card_discovery():
    _, a = client()
    assert a.base == "http://localhost:8000/a2a/v1"
    assert a.card["skills"][0]["id"] == "refund-request"


def test_multi_turn_input_required():
    _, a = client()
    t = a.send("我上个月被扣了两次钱")
    assert t["status"]["state"] == "TASK_STATE_INPUT_REQUIRED"
    t2 = a.send("发票号是 INV-2291", task_id=t["id"])
    assert t2["id"] == t["id"] and t2["status"]["state"] == "TASK_STATE_COMPLETED"
    assert t2["artifacts"][0]["parts"][0]["data"]["invoice"] == "INV-2291"


def test_terminal_task_rejects_message():
    c, a = client()
    t = a.send("INV-1 重复扣款")
    r = c.post(f"{BASE}/message:send", headers=H,
               json={"message": {"role": "ROLE_USER", "messageId": "m", "taskId": t["id"], "parts": [{"text": "再来"}]}})
    assert r.status_code == 400
    assert r.json()["error"]["details"][0]["reason"] == "UNSUPPORTED_OPERATION"


def test_version_and_auth():
    c, _ = client()
    body = {"message": {"role": "ROLE_USER", "messageId": "m", "parts": [{"text": "hi"}]}}
    r = c.post(f"{BASE}/message:send", json=body, headers={"Authorization": "Bearer demo-token"})
    assert r.json()["error"]["details"][0]["reason"] == "VERSION_NOT_SUPPORTED"
    r = c.post(f"{BASE}/message:send", json=body, headers={"A2A-Version": "1.0"})
    assert r.status_code == 401


def test_get_task_not_found():
    c, _ = client()
    r = c.get(f"{BASE}/tasks/nope", headers=H)
    assert r.status_code == 404 and r.json()["error"]["details"][0]["reason"] == "TASK_NOT_FOUND"
uv run pytest -q

输出大致长这样:5 passed。

第 5 步:用 curl 看线上到底传什么。 起服务:uv run uvicorn a2a_server:app --port 8000,然后:

curl -s localhost:8000/.well-known/agent-card.json | python3 -m json.tool | head -20
curl -s -X POST localhost:8000/a2a/v1/message:send \
  -H 'A2A-Version: 1.0' -H 'Authorization: Bearer demo-token' -H 'Content-Type: application/a2a+json' \
  -d '{"message":{"role":"ROLE_USER","messageId":"m1","parts":[{"text":"我被扣了两次钱"}]}}'

第二个请求返回的任务状态应该是 TASK_STATE_INPUT_REQUIRED,状态里带着一条问发票号的 Agent 消息。把返回的 task id 填进下一条请求的 taskId,补上发票号,看任务变成 COMPLETED。

第 6 步:接上真 Agent。 把 handle() 换成调用 Day 47 的 LangGraph 工作流:工作流里遇到「需要用户补充信息」的节点时,把任务置为 INPUT_REQUIRED 并把问题放进状态消息;退款需要主管审批时,任务保持 WORKING 并在 Artifact 里返回申请编号。思考一个问题:主管审批要等几小时,客户端应该一直轮询 GetTask,还是用推送通知?把你的选择和理由写下来。

第 7 步(选做):换成官方 SDK。 用 a2a-sdk 重写服务端,对比手写版本少了多少代码、多了哪些你没处理的边界情况。

验收标准:5 个测试通过;第 5 步的 curl 能完整走一遍多轮交互;第 6 步写出轮询和推送的取舍理由;写一段话说明你的服务端如何处理「代表哪个最终用户」的问题(目前的实现没有处理,你要设计怎么补)。

名词解释

名词意思
A2A(Agent2Agent)Agent 之间发现、通信、委派任务的开放协议,2026 年 3 月发布 1.0
MCPAgent 连接工具和数据源的协议
ACP(Agent Client Protocol)编辑器、控制台和 Coding Agent 之间的协议
AAIFAgentic AI Foundation,Linux 基金会旗下托管 MCP、A2A、AGENTS.md 等项目的组织
Agent CardAgent 的名片,声明能力、接口、鉴权方式,默认在 /.well-known/agent-card.json
不透明 Agent(opaque agent)调用方看不到内部实现,只通过声明的能力和协议交互的 Agent
Task / Message / Artifact / PartA2A 的核心对象:任务、沟通消息、交付产出、内容单元
INPUT_REQUIRED / AUTH_REQUIRED任务需要补充信息 / 需要额外授权时的状态
传输绑定(protocol binding)同一套语义在 JSON-RPC、gRPC、HTTP+JSON 上的具体表示
A2A-Version客户端声明协议版本的请求头,不带时按 0.3 处理
困惑的代理人(confused deputy)有权限的服务被调用方利用,替调用方做了它本不该做的事

常见误区

把 A2A 当成「另一种 MCP」。 MCP 连的是行为确定的工具,A2A 连的是有自主判断、可能长时间运行、需要多轮交互的 Agent。拿 A2A 去调一个查询接口是过度设计。

同一个团队内部也上 A2A。 进程内的子 Agent、工作流就能解决的事,引入网络协议只会增加延迟和故障点。协议的价值在跨边界。

客户端忘了带 A2A-Version。 服务端会按 0.3 处理,出现各种看不懂的字段错误。

只用调用方身份做授权。 下游不知道最终用户是谁,就无法按用户维度控制数据访问,容易出现越权。

信任对方 Agent 的返回内容。 它就是外部输入,可能有注入、可能有错误,要校验,不能直接触发写操作。

学习资料

文档与文章

B 站视频

  • 技术蛋老师《Function Calling、MCP和A2A的核心原理与区别》:已嵌入正文。
  • react__《Google Agent2Agent(A2A)vs MCP AI多智能体通信协议与代码实践》:10 P 共 81 分钟,有代码实践,看第 1 到 4 P 的协议讲解;录于 1.0 之前,字段名和状态名以 1.0 规范为准。
react__ · Google Agent2Agent(A2A)vs MCP AI多智能体通信协议与代码实践 · 10 P,先看前 4 P。

其他经典视频

  • Google Cloud Tech《Introduction to Agent2Agent (A2A) Protocol》:已嵌入正文。
  • IBM Technology《A2A Protocol (Agent2Agent) Explained: How AI Agents Collaborate》:9 分钟,用白板讲 A2A 和 MCP 怎么配合。
IBM Technology · A2A Protocol (Agent2Agent) Explained · 9 分钟,白板讲解,适合对照今天第一张图。

书籍

  • Martin Kleppmann《Designing Data-Intensive Applications》(O’Reilly, 2017;中文版《数据密集型应用系统设计》,中国电力出版社,2018):第 4 章「Encoding and Evolution」,讲数据格式和接口的向前向后兼容、版本演进,A2A 从 0.3 到 1.0 的不兼容升级、版本头的设计,背后都是这一章的问题,1.5 小时。

开源项目与代码

  • a2aproject/A2A:规范仓库,specification/a2a.proto 是权威定义,docs/specification.md 是正文。
  • a2aproject/a2a-python:官方 Python SDK 源码,看 a2a/client/card_resolver.py 怎么发现名片。
  • a2aproject/a2a-samples:各种框架实现 A2A 的示例,挑一个你熟悉的框架看。

自测

1. 什么情况下用 MCP,什么情况下用 A2A?各举一个 SupportPilot 里的例子。

答案

对方是行为确定、调用后很快返回的工具或数据源,用 MCP,比如 SupportPilot 查账单库、查知识库。对方是有自主判断、可能长时间运行、需要多轮交互、内部实现你看不到的 Agent,用 A2A,比如请财务团队的对账 Agent 核实一笔有争议的扣款,它可能需要几分钟、可能会反过来问你要合同编号。

2. 客户端没带 A2A-Version 头,服务端会怎么处理?为什么这样设计?

答案

按 0.3 版本处理。这是为了兼容 1.0 之前的老客户端,它们不知道要带这个头。副作用是新客户端如果忘了带,会被当成老版本,出现字段不兼容的错误,所以客户端 SDK 应该默认带上版本头。

3. 任务处于 TASK_STATE_COMPLETED,客户端又往这个任务发了一条消息,会发生什么?如果业务上确实要追问,应该怎么做?

答案

服务端返回 UnsupportedOperationError,终态任务不接受新消息。要追问的话,开一个新任务,用同一个 contextId 把它和之前的任务关联到同一个会话里,必要时在消息里引用前一个任务的 id。

4. 什么是 confused deputy 问题?在 A2A 场景里怎么防?

答案

有权限的服务被调用方利用,替调用方做了它本不该做的事。比如 SupportPilot 用自己的服务身份调用财务 Agent,财务 Agent 只认服务身份,就可能把其他客户的数据返回给它。防法是把最终用户的身份和授权范围随请求一起传递(比如用 OAuth 的代表用户授权或令牌交换),被调用方按最终用户维度做授权和审计,而不是只看调用方是谁。

5. 资深面试题:公司有 20 个团队各自做了 Agent,CTO 希望它们能互相调用。你会怎么设计这套互联方案?要不要上 A2A?

答案

先分类:团队内部的 Agent 协作用子 Agent 和工作流,不走协议;跨团队调用才需要统一方案。跨团队部分我会上 A2A,原因是各团队技术栈不同、需要独立演进、有长任务和多轮交互,A2A 的不透明 Agent 模型和任务状态机正好匹配,而且有中立治理和官方 SDK,不用自造协议。

设计上要有几块:一是 Agent 目录,各团队注册 Agent Card,支持按技能搜索,目录本身做访问控制;二是统一鉴权,内部用 OAuth 2.0 client credentials 加代表用户的令牌交换,确保下游能按最终用户授权;三是网关,统一做限流、审计、计费归因和可观测(OTel 贯穿调用链);四是规范,规定 Agent Card 必须写清技能和示例、错误码、SLA,返回内容视为不可信、写操作必须在被调用方做确认;五是版本治理,协议版本和各 Agent 自身版本都要有升级和废弃流程。落地顺序上先挑两三个真实有跨团队需求的场景试点,验证收益后再推广,而不是一开始就要求 20 个团队都改造。

明天预告

Day 94 讲 Agent 的产品与交互:人在回路里的体验设计。协议解决了 Agent 和 Agent 怎么说话,明天回到 Agent 和人:自主程度分几级、什么时候该让 Agent 自己做、什么时候必须停下来问人;怎么展示计划和步骤让用户敢信任它;出错时怎么让用户能中断和撤销。我们会用一篇关于自主程度分级的论文做框架,给 SupportPilot 的每个动作定一个自主级别,并实现一个带撤销窗口的动作策略引擎。

评论

  1. 加载中…

只保存昵称和内容。会显示你的大致地区(国家 / 省),不保存 IP 地址。