A2A 协议 v1.0 完全指南:方法名、Agent Card、三种绑定与 FastAPI 实战

A2A 协议 v1.0 完全指南:方法名、Agent Card、三种绑定与 FastAPI 实战(附 pre-1.0 迁移清单)

摘要:A2A(Agent2Agent)在 2026 年 3 月发布 v1.0.0 后,协议层发生了大量 breaking change------tasks/send 改名、Agent Card 结构重写、Part 统一、状态枚举全大写。本文基于官方 v1.0 规范与迁移文档,系统梳理 A2A 的定位、时间线、核心变更,并给出一个可直接跑的 FastAPI 接入示例,最后附上 12 条 pre-1.0 迁移避坑清单。适合正在做多智能体协作、或准备把现有 Agent 暴露成标准服务的同学收藏。


目录

  • [一、A2A 到底是什么:先搞清楚分层](#一、A2A 到底是什么:先搞清楚分层)
  • [二、时间线:从 2025-04 发布到 2026-03 v1.0](#二、时间线:从 2025-04 发布到 2026-03 v1.0)
  • [三、v1.0 核心变更总览](#三、v1.0 核心变更总览)
  • [四、Agent Card:结构与发现路径](#四、Agent Card:结构与发现路径)
  • [五、方法名映射:pre-1.0 vs v1.0](#五、方法名映射:pre-1.0 vs v1.0)
  • [六、三种协议绑定:JSON-RPC / gRPC / HTTP+JSON](#六、三种协议绑定:JSON-RPC / gRPC / HTTP+JSON)
  • [七、FastAPI 实战:把 LangGraph Agent 包成 A2A Server](#七、FastAPI 实战:把 LangGraph Agent 包成 A2A Server)
  • [八、12 条 pre-1.0 迁移避坑清单](#八、12 条 pre-1.0 迁移避坑清单)
  • 九、FAQ
  • 十、总结

一、A2A 到底是什么:先搞清楚分层

A2A(Agent2Agent)是 Agent 与 Agent 之间的水平协作协议,由 Google 于 2025 年 4 月发起,后捐入 Linux Foundation,2026 年并入 AAIF(Agentic AI Foundation),与 MCP 同源治理。

2026 年的 Agent 协议栈基本是三层格局:

协议 方向 类比 解决什么问题
MCP Agent → Tool(垂直接入) 给 Agent 装"手" 工具、数据源、API 的统一接入
A2A Agent ↔ Agent(水平协作) 让 Agent 之间说"普通话" 跨进程、跨厂商的 Agent 互操作
PAP Personal Agent Protocol 个人身份/权限层 个人 Agent 的身份与授权

一句话记住官方在 v1.0 公告里重申的那句:"MCP inside agents, A2A between agents"。两者不冲突,是叠加关系。

很多项目里其实已经有"A2A 雏形"------比如主智能体按领域路由、delegate_task 派发任务、子智能体回收结果。这套机制如果绑定在同一进程、同一套代码里,就是"自研版 A2A";A2A 做的事,是把它标准化、跨进程化,让任何系统里的 Agent 都能用统一协议互相调用。

A2A 的核心价值:消灭 N² 点对点集成。N 个 Agent 两两对接要写 N² 套自定义接口;统一协议后,新增一个 Agent 只需实现 Agent Card + 标准端点。


二、时间线:从 2025-04 发布到 2026-03 v1.0

网上很多文章把这段历史压缩成一句话,导致"2025 年就捐给 AAIF"这类错误流传。准确时间线如下:

时间 事件
2025-04-09 Google 发布 A2A,50+ 初始伙伴
2025-05 Microsoft、SAP、Zoom 等加入
2025-06-23 在 Open Source Summit North America 上捐入 Linux Foundation
2025 年下半年 IBM 的 ACP(Agent Communication Protocol)在 LF AI & Data 伞下并入 A2A 生态
2025-12 AAIF(Agentic AI Foundation)成立,MCP、AGENTS.md、goose 首批入驻
2026-03-12 A2A v1.0.0 正式发布,首个稳定、生产就绪版本
2026-04-09 一周年:150+ 组织、22K stars、5 语言 SDK GA
2026-08 A2A 正式并入 AAIF,与 MCP 同源治理

所以正确表述是:2025-06 捐入 Linux Foundation,2026-08 并入 AAIF。这两个是不同事件,不要压缩。


三、v1.0 核心变更总览

v1.0 不是小版本迭代,协议层做了大量 breaking change。先看总表,后面逐项展开:

变更项 pre-1.0(v0.3.0) v1.0
方法名 tasks/send、message/send SendMessage、SendStreamingMessage、GetTask、ListTasks、CancelTask、SubscribeToTask
Agent Card 顶层 有 protocolVersion、url、preferredTransport 全部移除,合并为 supportedInterfaces
发现路径 /.well-known/agent.json /.well-known/agent-card.json
Task 状态 "completed"、"input-required" TASK_STATE_COMPLETED、TASK_STATE_INPUT_REQUIRED
Message role "user" / "agent" ROLE_USER / ROLE_AGENT
Part 判别 三种类型 + kind 字段 单一 Part,按 text/url/raw/data 字段存在判别
媒体类型字段 mimeType mediaType
流事件判别 kind: "status-update" + final 字段 {"statusUpdate": {...}} 包装对象,终态靠流关闭
ID 复合 ID(tasks/{id}) 简单 UUID
错误模型 RFC 9457 / 自定义 google.rpc.Status + ErrorInfo(domain: "a2a-protocol.org")
OAuth 支持 implicit / password 移除;新增 Device Code + PKCE
多租户 无 请求与 AgentInterface 均带 tenant 字段
REST 绑定前缀 /v1/... 去掉 /v1,如 POST /message:send

四、Agent Card:结构与发现路径

Agent Card 是 Agent 的"能力名片",客户端通过它发现服务、了解能力边界、选择协议绑定。

4.1 发现路径

标准路径是 /.well-known/agent-card.json (RFC 8615 well-known URI),Content-Type 建议 application/a2a+json。

注意:/.well-known/agent.json 是 pre-1.0 的旧路径,v1.0 客户端不会去拉它。

4.2 v1.0 结构示例

json 复制代码
{
  "name": "news-llm-agent",
  "description": "新闻舆情分析与智能问答智能体",
  "version": "1.0.0",
  "supportedInterfaces": [
    {
      "url": "http://127.0.0.1:8000/a2a",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    },
    {
      "url": "http://127.0.0.1:8000/rest",
      "protocolBinding": "HTTP+JSON",
      "protocolVersion": "1.0"
    }
  ],
  "capabilities": {
    "streaming": true,
    "pushNotifications": false,
    "extendedAgentCard": false
  },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    {
      "id": "news-analysis",
      "name": "新闻舆情分析",
      "description": "基于实时新闻库回答舆情问题、生成分析报告",
      "tags": ["news", "analysis", "risk"]
    }
  ],
  "signatures": []
}

关键点:

  • 顶层不再有 protocolVersion、url、preferredTransport、additionalInterfaces,全部收进 supportedInterfaces 数组;
  • 每个 interface 声明自己的 url、protocolBinding(JSONRPC / GRPC / HTTP+JSON)、protocolVersion;
  • signatures 是可选字段,用于签名 Agent Card(JWS + RFC 8785 规范化),解决"名片是否由声明的域签发",不认证每一次请求。

五、方法名映射:pre-1.0 vs v1.0

v1.0 把核心方法名统一成 PascalCase。如果你还在用 tasks/send,v1.0 客户端会直接返回 Method not found。

pre-1.0(v0.3.0) v1.0 备注
message/send SendMessage 明确 Task vs Message 返回语义
message/stream SendStreamingMessage 流事件去掉 kind / final 字段
tasks/get GetTask 仅返回调用者可见任务
tasks/list ListTasks v1.0 新增,游标分页(pageToken)
tasks/cancel CancelTask 明确可取消时机与状态迁移
tasks/resubscribe SubscribeToTask SSE 断线重连
agent/getAuthenticatedExtendedCard GetExtendedAgentCard 卡片扩展获取
push notification 四件套 Create/Get/List/DeleteTaskPushNotificationConfig 模型扁平化

版本协商:A2A-Version header

客户端请求应带 A2A-Version: 1.0。服务端支持时路由到 v1.0 handler;不支持则返回 VersionNotSupportedError。缺失或为 0.3 时,默认走 v0.3 兼容路径(如果你的服务同时支持两个版本)。


六、三种协议绑定:JSON-RPC / gRPC / HTTP+JSON

v1.0 正式定义了三种等价绑定,客户端可根据场景选择,服务端在 supportedInterfaces 中如实声明:

绑定 传输 序列化 适用场景
JSON-RPC 2.0 HTTP POST JSON 默认,兼容性最广,调试方便
gRPC HTTP/2 Protobuf 3 高吞吐、强类型、内部服务间调用
HTTP+JSON(REST) HTTP 动词 JSON RESTful 集成,v1.0 已去掉 /v1 前缀

REST 绑定的典型路径(v1.0):

  • POST /message:send
  • GET /tasks/{id}
  • POST /tasks/{id}:cancel
  • GET /tasks

建议 :至少实现 JSON-RPC + REST 两种。gRPC 按需补,但一旦在 Agent Card 里声明了 GRPC,就必须真的可连,否则客户端探测会失败。


七、FastAPI 实战:把 LangGraph Agent 包成 A2A Server

假设你有一个 FastAPI + LangGraph 的新闻舆情智能体(全局变量 graph),把它暴露成 A2A v1.0 Server 只需要三件事:Agent Card → JSON-RPC 端点 → 把 Task 映射到 graph.ainvoke。

以下为演示版,生产环境建议直接用官方 Python SDK(a2a-sdk),已覆盖三种绑定和 0.3→1.0 迁移逻辑。

7.1 服务端代码

python 复制代码
# a2a_server.py ------ A2A v1.0 兼容演示版
import asyncio
import uuid
from datetime import datetime

from fastapi import FastAPI, Header
from pydantic import BaseModel
from langchain_core.messages import HumanMessage

from app import graph, _msg_text, _strip_manual_cite_list  # 复用项目全局图

app = FastAPI()

AGENT_CARD = {
    "name": "news-llm-agent",
    "description": "新闻舆情分析与智能问答智能体",
    "version": "1.0.0",
    "supportedInterfaces": [
        {
            "url": "http://127.0.0.1:8000/a2a",
            "protocolBinding": "JSONRPC",
            "protocolVersion": "1.0",
        }
    ],
    "capabilities": {"streaming": True, "pushNotifications": False, "extendedAgentCard": False},
    "defaultInputModes": ["text/plain"],
    "defaultOutputModes": ["text/plain"],
    "skills": [
        {
            "id": "news-analysis",
            "name": "新闻舆情分析",
            "description": "基于实时新闻库回答舆情问题、生成分析报告",
            "tags": ["news", "analysis", "risk"],
        }
    ],
    "signatures": [],
}

# 生产级应落库,这里用内存字典演示
_active_tasks: dict = {}


class A2ARequest(BaseModel):
    jsonrpc: str
    id: str
    method: str
    params: dict


@app.get("/.well-known/agent-card.json")
async def agent_card():
    return AGENT_CARD


@app.post("/a2a")
async def a2a_endpoint(req: A2ARequest, a2a_version: str = Header("1.0")):
    handlers = {
        "SendMessage": _send_message,
        "GetTask": _get_task,
        "CancelTask": _cancel_task,
        "ListTasks": _list_tasks,
    }
    handler = handlers.get(req.method)
    if handler is None or a2a_version not in ("1.0",):
        return {
            "jsonrpc": "2.0",
            "id": req.id,
            "error": {"code": -32601, "message": "Method not found"},
        }
    return await handler(req.id, req.params)


async def _send_message(req_id: str, params: dict):
    task_id = params.get("id") or f"task-{uuid.uuid4().hex[:8]}"

    # v1.0:统一 Part,无 kind 字段;role 为 ROLE_USER
    parts = params.get("message", {}).get("parts", [])
    prompt = "".join(p.get("text", "") for p in parts if isinstance(p, dict))
    thread_id = f"a2a_{task_id}"

    async def _run():
        config = {"configurable": {"thread_id": thread_id, "web_enabled": True}}
        try:
            result = await graph.ainvoke(
                {"messages": [HumanMessage(content=prompt)]}, config=config
            )
            content = _strip_manual_cite_list(_msg_text(result["messages"][-1].content))
            _active_tasks[task_id] = {
                "status": {
                    "state": "TASK_STATE_COMPLETED",
                    "timestamp": datetime.utcnow().isoformat(timespec="milliseconds") + "Z",
                },
                "artifacts": [
                    {
                        "name": "answer",
                        "parts": [{"text": content, "mediaType": "text/plain"}],
                    }
                ],
            }
        except Exception as e:
            _active_tasks[task_id] = {
                "status": {"state": "TASK_STATE_FAILED"},
                "error": {"code": -32000, "message": str(e)},
            }

    _active_tasks[task_id] = {"status": {"state": "TASK_STATE_WORKING"}}
    asyncio.create_task(_run())

    return {
        "jsonrpc": "2.0",
        "id": req_id,
        "result": {
            "id": task_id,
            "status": {"state": "TASK_STATE_WORKING"},
            "metadata": {"thread_id": thread_id},
        },
    }


async def _get_task(req_id: str, params: dict):
    task_id = params.get("id", "")
    task = _active_tasks.get(task_id)
    if task is None:
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "error": {"code": -32602, "message": "Task not found"},
        }
    return {"jsonrpc": "2.0", "id": req_id, "result": {"id": task_id, **task}}


async def _cancel_task(req_id: str, params: dict):
    task_id = params.get("id", "")
    if task_id in _active_tasks:
        _active_tasks[task_id] = {"status": {"state": "TASK_STATE_CANCELED"}}
    return {"jsonrpc": "2.0", "id": req_id, "result": {"id": task_id}}


async def _list_tasks(req_id: str, params: dict):
    page_size = params.get("pageSize", 20)
    return {
        "jsonrpc": "2.0",
        "id": req_id,
        "result": {"tasks": list(_active_tasks.items())[:page_size]},
    }

7.2 客户端调用(curl)

bash 复制代码
curl -X POST http://127.0.0.1:8000/a2a \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "SendMessage",
    "params": {
      "id": "t1",
      "message": {
        "role": "ROLE_USER",
        "parts": [
          {"text": "今天期货相关的重大舆情有哪些?", "mediaType": "text/plain"}
        ]
      }
    }
  }'

7.3 客户端封装(Python)

python 复制代码
import requests

def a2a_send(agent_url: str, prompt: str, timeout: int = 60):
    body = {
        "jsonrpc": "2.0",
        "id": "req-001",
        "method": "SendMessage",
        "params": {
            "id": "task-001",
            "message": {
                "role": "ROLE_USER",
                "parts": [{"text": prompt, "mediaType": "text/plain"}],
            },
        },
    }
    r = requests.post(
        agent_url.rstrip("/") + "/a2a",
        json=body,
        headers={"A2A-Version": "1.0"},
        timeout=timeout,
    )
    r.raise_for_status()
    return r.json()["result"]

核心就一句话 :把 A2A Task 翻译成对 graph.ainvoke 的一次调用。你的 SSE 流式、子智能体追踪、引用清洗、记忆抽取都可以复用,A2A 只是换一个入口。


八、12 条 pre-1.0 迁移避坑清单

如果你手上有基于 v0.3.0 写的 A2A 适配器,按下面清单逐条改:

  1. 发现路径 :/.well-known/agent.json → /.well-known/agent-card.json
  2. 方法名 :tasks/send → SendMessage,tasks/get → GetTask,tasks/cancel → CancelTask,message/stream → SendStreamingMessage
  3. Agent Card 顶层字段 :删掉 protocolVersion、url、preferredTransport、additionalInterfaces,改到 supportedInterfaces 数组
  4. Task 状态 :"completed" → "TASK_STATE_COMPLETED",全大写 + 前缀
  5. Message role :"user" → "ROLE_USER","agent" → "ROLE_AGENT"
  6. Part 判别 :删掉 kind 字段,改用 text / url / raw / data 哪个字段存在来判别类型
  7. 媒体类型 :mimeType → mediaType
  8. 流事件 :kind: "status-update" → {"statusUpdate": {...}} 包装对象;终态靠流关闭,不再有 final 字段
  9. Task ID :复合 ID(tasks/{id})→ 简单 UUID
  10. 错误模型 :改用 google.rpc.Status + ErrorInfo,reason 用 UPPER_SNAKE_CASE,domain 固定 "a2a-protocol.org"
  11. OAuth :移除 implicit / password flow,新增 Device Code flow(RFC 8628)+ PKCE(pkceRequired)
  12. REST 绑定 :去掉 /v1 前缀,如 POST /v1/message:send → POST /message:send

另外两个容易漏的点:

  • 版本协商 :客户端要带 A2A-Version header,服务端要能识别并返回 VersionNotSupportedError;
  • 多租户 :v1.0 新增 tenant 字段,如果你的服务是多租户的,要在请求和 AgentInterface 里带上。

九、FAQ

Q1:A2A 和 MCP 到底什么关系?会不会重复?

不重复。MCP 是 Agent 接工具/数据的垂直接口,A2A 是 Agent 之间的水平协作协议。一个 Agent 完全可以"内部用 MCP 接工具,对外用 A2A 接其他 Agent",两层叠加,互不冲突。

Q2:必须实现三种绑定吗?

不必须。至少实现 JSON-RPC 2.0 就能跑通大多数场景,REST 绑定建议补上,gRPC 按需。关键是 Agent Card 里声明了什么就必须真的支持什么,否则客户端探测会失败。

Q3:签名 Agent Card 能让我的服务变安全吗?

不能单独靠它。签名 Agent Card 是 MAY(可选) 能力,解决的是"这张名片是否由声明的域签发",不认证每一次 A2A 请求。请求级鉴权(OAuth 2.0 / API Key)仍需单独设计。

Q4:v0.3 的客户端还能调用 v1.0 服务吗?

取决于服务端是否做双版本兼容。v1.0 定义了 A2A-Version header 做协商,服务端可以同时挂 v0.3 和 v1.0 的 handler。如果只实现 v1.0,v0.3 客户端会收到版本不支持错误。

Q5:官方 SDK 选哪个?

官方提供 Python / JS / Java / Go / .NET 五个 GA SDK。FastAPI 项目直接上 Python SDK(a2a-sdk),它已覆盖三种绑定和 0.3→1.0 迁移逻辑,比手写 JSON-RPC 路由省事得多。


十、总结

  • A2A 的定位:Agent 之间的"标准普通话",与 MCP 是叠加关系,不是替代关系。
  • v1.0 是分水岭:2026-03-12 发布,方法名 PascalCase 化、Agent Card 结构重写、Part 统一、状态枚举全大写、三种绑定正式定义。照着 pre-1.0 的教程写代码,大概率对不上。
  • 接入成本不高 :对 FastAPI + LangGraph 项目,加一个 Agent Card 路由 + 一个 JSON-RPC 端点,把 Task 映射到 graph.ainvoke,剩下的全是复用。
  • 迁移重点看清单:本文第八节的 12 条,基本覆盖了实际迁移中最容易踩的坑。

如果你正在做多智能体协作,或者准备把现有 Agent 暴露成标准服务,建议先把 Agent Card 和 SendMessage 跑通,再逐步补流式、ListTasks、鉴权和任务持久化。协议本身不复杂,复杂的是版本细节------这也是为什么 v1.0 的迁移文档值得逐条读一遍。


参考来源 :A2A 官方规范站 a2a-protocol.org(v1.0 规范与 what's-new 迁移文档)、a2aproject GitHub 发布公告、Linux Foundation / AAIF 相关公告。代码示例为演示用途,生产环境请使用官方 SDK 并补齐鉴权、持久化与错误处理。

相关推荐
譕痕1 小时前
Rabbit 实例
java·开发语言·java-rabbitmq
综合简报1 小时前
黑五海外发稿投给谁?三类海外媒体发稿渠道的分工
开发语言·php·媒体
大风起兮云飞扬丶1 小时前
Java——线程池
java·开发语言
vHelios2 小时前
Python FastAPI 422 Unprocessable Entity报错排查实录 error:Invalid \\escape
fastapi
我命由我123452 小时前
Python 开发 - Python 虚拟环境
运维·开发语言·后端·python·http·https·java-ee
c#上位机2 小时前
C#上位机项目实战——exe项目调试更改dll中源代码
开发语言·c#
basketball6162 小时前
C++ 中字符串与常见类型的转换总结
开发语言·c++
梦影_2 小时前
MySQL进阶教程(一)—— 储存过程与触发器(上)
开发语言·数据库·后端·sql·mysql·中间件
云和数据.ChenGuang2 小时前
LLM大模型对接本地Ollama部署模型
开发语言·人工智能·python·langchain