☰
A2A协议全解析:AI Agent互操作标准、MCP关系与从0到1实践
2026/9/26 8:49:32 网站建设 项目流程

在 Agent 开发圈子里混久了,你会发现一个很魔幻的现实:单个 Agent 的能力越来越强,但把两个不同团队做的 Agent 放在一起,它们根本没法好好说话。这不光是接口格式不统一的问题,连“你帮我干件事”“干完了,结果放这了”这种最基本的对话方式都没有一个通用标准。A2A 协议(Agent-to-Agent)就是为了填这个坑而出现的——它给 AI Agent 之间定义了一套互相发现、协商、派活、交活的公共语言。这篇文章我不打算照着官方文档念,而是从“为什么需要它”“它到底设计了什么”“我怎么从 0 到 1 搭一个能跑的 A2A 协作流程”这几个角度,把 A2A 掰开揉碎讲清楚,顺便把实际踩过的坑也一并交代了。

这套内容适合正在做 Agent 开发、想搞懂 A2A 和 MCP 到底什么关系、以及准备 Agent 方向技术面试的人。看的时候建议你手边备一个终端,后面第三部分的代码不是给你读的,是给你跑的。

1. 为什么 AI Agent 之间需要协作:A2A 协议的诞生背景

1.1 AI Agent 的尴尬现状:智能孤岛

先说个我自己的观察。去年我在做一个偏内部效率的 Agent 项目,核心功能是让一个“助理 Agent”帮忙查库存、写邮件、协调会议。一开始还挺顺利,但随着需求变多,我发现自己陷入了一个奇怪的局面:每个功能模块都长成了独立的 Agent,但因为没有一个共同的通信协议,模块之间只能靠我自己写一堆“胶水接口”去转接。

这种“胶水接口”有多痛苦?两个 Agent 之间的调用,你要处理 API 路径、鉴权方式、消息格式、错误重试……关键问题是这些都是项目私有的,换个 Agent 全得重写。这就像每家酒店都用自己的插座标准,你出门必须带一堆转接头。

A2A 要解决的就是这个问题:让不同厂商、不同技术栈、不同部署环境的 AI Agent 之间,能通过一套统一协议完成发现、通信和协作,而不是靠项目里写死的私有接口。它由 Google 在 2025 年 4 月联合多家厂商发起,目标非常直白——把 Agent 之间的互操作做成像浏览器访问网页那样自然。

1.2 A2A 与 MCP 到底什么关系

这是网上被问爆的问题。很多人一看到“Agent 通信标准”就以为是 MCP 的升级版,其实完全不是。MCP(Model Context Protocol)解决的是 Agent 与工具、数据源之间的连接,你可以把它理解成“Agent 的手和眼睛”——让模型去调用函数、读数据库、操作外部系统。而 A2A 解决的是 Agent 与 Agent 之间的协作,它是“Agent 的嘴和耳朵”。

用一个组合场景来说明:假设你有一个“行程规划 Agent”,它需要查询航班信息。最简单的做法是给它接一个 MCP 航班查询工具,自己直接去查。但更符合真实业务的做法是:你手上已经有一个由另一个团队维护的“航班查询 Agent”,它封装了所有航司接口和退改签逻辑,那么这个行程规划 Agent 就可以通过 A2A 直接调用那个航班查询 Agent——让它去查,查完把结果交回来。

所以 MCP 和 A2A 不是竞争关系,而是分工协作:MCP 负责让 Agent“用工具”,A2A 负责让 Agent“找伙伴”。在实际工程里,两者经常组合出现——一个 Agent 通过 MCP 接入内部系统,再通过 A2A 暴露自己的能力给其他 Agent 调用。

1.3 Agent、LLM 与 AI 模型:先把概念摆正

聊 A2A 之前,有一个基础概念必须理清,因为网上经常混着用。LLM(大语言模型)是“大脑”,负责理解语言、推理、生成文本,比如 DeepSeek、GPT、Claude 都属于这类。AI 模型是个更大的范畴,除了语言模型,还包括视觉模型、语音模型、多模态模型等。

而 Agent 是“完整的人”而不是“大脑”。一个 Agent 通常包含 LLM 作为推理内核,但还额外具备记忆、工具调用、任务规划、环境交互能力。也就是说:DeepSeek 是一个 LLM,它可以被用来“驱动”一个 Agent,但 DeepSeek 本身不是 Agent。

这个区别在 A2A 语境下特别重要,因为 A2A 设计的基本假设是:每个 Agent 内部用的是什么模型不重要,甚至可以不包含 LLM——比如一个负责固定计算的 Agent 完全可以由普通代码实现。A2A 只关心 Agent 对外暴露的行为契约:你能干什么、怎么调用你、怎么把结果给我。这也是它能够跨厂商协作的根本原因。

2. A2A 协议的核心设计:Agent Card、任务与消息

2.1 Agent Card:智能体的“名片”

A2A 协议里最基础的一个概念叫 Agent Card,它是一个 JSON 文件,存放于每个 Agent 的/.well-known/agent-card.json路径下。这个路径是有讲究的,它遵循 RFC 8615 的 Well-Known URI 规范,也就是说,任何人只要知道你的域名,就能通过固定路径拿到你的“能力说明书”。

Agent Card 里包含这些核心字段:name和description用来告诉别人你是谁;url是 A2A 入口地址;skills数组用来声明你具备哪些技能,每项技能包括技能 ID、名称、描述和可选参数结构;capabilities用来声明你支持流式输出还是推送通知;authentication字段声明调用你需要什么鉴权方式。

这里我想强调skills设计得好不好,直接影响 Agent 被“发现”和“匹配”的精准度。我见过一些团队把 skills 描述写得特别含糊,比如“能帮忙处理日常事务”,这等于没写。真正可用的写法应该是:“根据出发日期和目的地查询航班价格,支持国内主要航司”。

注意:Agent Card 不是只在注册中心里存一份,而是应该由每个 Agent 自己对外提供。这样你的 Agent 被谁发现,取决于谁能访问到这个 JSON 文件,而不是依赖某个中心化的目录服务。

2.2 任务生命周期:从创建到完成

A2A 协议把一次协作抽象成一个“任务”(Task),而不是简单的“请请求响应”。这个设计非常关键,因为真实场景里 Agent 干活常常不是瞬间完成的——它可能要去调接口、要等人确认、要后台跑几分钟分析,甚至中途还要反过来问你问题。

Task 有自己明确的状态流转。创建之后进入submitted,随后变成working,如果执行中需要更多输入,会进入input-required状态,此时客户端 Agent 需要补充消息再继续;正常结束是completed,失败是failed,被调用方主动终止则是canceled。整个流程是异步的,客户端可以轮询状态,也可以通过订阅 Webhook 等待状态变更通知。

我举个例子帮你建立直觉:你让同事帮你写一份报告,他不可能秒回,他可能写着写着发现资料不够,转头问你“去年的数据你有吗”。你补给他之后他继续写,最后把报告给你。任务状态机就是在这个自然协作过程的形式化表达。理解了这一点,你就明白 A2A 为什么不是简单发一个 HTTP 请求拿一个响应就完事——因为复杂任务天然是长时运行、多轮交互的。

2.3 消息与 Artifact:协作的“聊天记录”

任务执行过程中,参与双方需要交换信息。A2A 用Message对象来承载这些信息,每个消息有角色和内容。角色只有两种:user和agent,分别代表发起方和接收方。消息内容由Part构成,Part可以是文本、文件内容、或者一个结构化数据块。

除了对话消息,A2A 还定义了Artifact这个概念,它表示 Agent 执行任务后产生的正式产物。举个例子:一个“写代码 Agent”在任务过程中可能会给你发消息说“我开始写了”,最后完成的代码文件就是一个 artifact。之所以要把 artifact 单独拿出来,是因为它和普通对话消息有本质区别——它是任务的正式交付物,需要被引用、被版本化管理、被后续流程消费。

我在实际使用中的体会是:这个设计非常贴近真实团队协作。Message 是过程沟通,Artifact 是最终交付物,两者分开管理,你才能做到“多轮沟通不影响交付物追踪”。

2.4 能力发现与协商:A2A 为什么能“自动对接”

传统系统对接是 A 说了算:调用方写死 B 的地址、字段、鉴权方式,一旦 B 变了,A 必须跟着改。A2A 的做法完全不同,它把“对接”变成了“发现”加“协商”。

调用过程大致是这样的:客户端获取目标 Agent 的 Agent Card,解析里面的skills数组,发现自己需要的技能;然后读取capabilities,看看对方支不支持流式输出,支不支持推送通知;最后按照authentication要求做鉴权,发出一条携带具体任务的 JSON-RPC 请求。如果任务需要额外输入,服务方会通过input-required状态主动索要。

最关键的是,这个发现和协商的过程不是一次性的,而是每次调用都可以发生。也就是说,A2A 在架构层面让“对接”变成了一种运行时行为,而不是编译期/部署期的静态约定。这跟现在互联网领域的微服务注册发现很相似——Agent 世界也需要这样一个标准,不然每个 Agent 都得手动背诵对方的“电话号码”。

3. 从 0 到 1 搭建一个 A2A 工作流

3.1 环境准备:语言与依赖

纸上谈兵聊完了,进入实操环节。我用 Python 搭建一套最小可用的 A2A 工作流,用 FastAPI 做 HTTP 服务,用官方提供的a2a-sdk加速开发。之所以选 Python,是因为 Agent 生态里 Python 的资料最多,而且 FastAPI 的异步能力很适合处理 A2A 这种多轮任务场景;生产环境你完全可以用 Java 或 Go,协议本身没有语言绑定。

安装依赖只需要两行命令:

pip install fastapi uvicorn a2a-sdk

我建议你在一个干净的虚拟环境里操作,避免把系统 Python 环境弄乱。这一步做完,我们开始写第一个 A2A Server。

3.2 实现一个基础 A2A Server

先写 Agent Card。在项目根目录建一个static/agent-card.json文件,内容是最小可用的“翻译技能”描述:

{ "name": "translation-agent", "description": "一个提供中英互译能力的 Agent", "url": "http://localhost:8000/", "protocolVersion": "0.2.0", "skills": [ { "id": "translate", "name": "文本翻译", "description": "根据用户提供的文本执行中英互译", "inputModes": ["text/plain"], "outputModes": ["text/plain"] } ], "capabilities": { "streaming": false, "pushNotifications": false }, "authentication": { "schemes": [], "credentials": "none" } }

接着创建入口文件server.py,注册 A2A 路由:

from fastapi import FastAPI from a2a_sdk import create_a2a_server, AgentCard, Skill, NoAuth app = FastAPI() def translate_text(text: str, target_lang: str) -> str: # 这里替换成真实的翻译模型调用,比如某个 LLM 接口 if target_lang == "zh": return f"[中文翻译] {text}" return f"[English Translation] {text}" def handle_skill(skill_id: str, params: dict) -> str: if skill_id == "translate": text = params.get("text", "") target = params.get("target_lang", "zh") return translate_text(text, target) raise ValueError(f"未知技能: {skill_id}") agent_card = AgentCard( name="translation-agent", description="一个提供中英互译能力的 Agent", url="http://localhost:8000/", protocol_version="0.2.0", skills=[ Skill( id="translate", name="文本翻译", description="根据用户提供的文本执行中英互译", ) ], authentication=NoAuth(), ) a2a_server = create_a2a_server( agent_card=agent_card, skill_handler=handle_skill, ) app.mount("/", a2a_server) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)

这段代码实现了两件事:一是通过/根路径直接返回 Agent Card,二是把/rpc路径挂载成 JSON-RPC 入口,负责处理任务创建、状态查询、消息发送等标准请求。skill_handler是真正的业务逻辑所在,A2A 协议会把对方传来的任务参数解析后路由到这里。

3.3 实现客户端 Agent 并验证协作

服务端就绪后,我们写一个客户端 Agent,去“发现”翻译 Agent,并给它派发翻译任务。这里我用a2a-sdk里的客户端工具类来简化调用:

import asyncio from a2a_sdk import A2AClient, TaskInput, Message, TextPart async def main(): client = A2AClient("http://localhost:8000/") # 1. 获取 Agent Card card = await client.get_agent_card() print("发现 Agent:", card.name) print("支持技能:", [s.id for s in card.skills]) # 2. 创建翻译任务 input_message = Message( role="user", parts=[TextPart(text="Hello, world")] ) task_input = TaskInput( skill_id="translate", parameters={"text": "Hello, world", "target_lang": "zh"}, message=input_message ) task = await client.create_task(task_input) print("任务状态:", task.status) # 3. 轮询直到完成 while task.status not in ["completed", "failed", "canceled"]: await asyncio.sleep(1) task = await client.get_task(task.id) print("任务结果:", task.artifacts[0].text) asyncio.run(main())

跑起来之后,你的客户端 Agent 会经历完整的“发现-建任-轮询-取结果”链路。这一步验证通过,说明你已经具备了最基本的 A2A 互操作能力。在真实项目中,客户端 Agent 内部通常也是由一个 LLM 驱动,它通过读 Agent Card 来决定要不要调用你,而不是像示例里这样写死技能 ID——但对初学者来说,先跑通固定链路比追求动态决策重要得多。

3.4 多 Agent 场景:接力与协作

单点通了之后,我们来设计一个稍微复杂一点的协作:一个“翻译 Agent”加一个“摘要 Agent”,再由一个“编排 Agent”把它们串起来。场景是:用户输入一段英文长文,编排 Agent 先把英文翻成中文,再把中文做摘要。

这个场景里,编排 Agent 不需要知道翻译和摘要的具体实现,它只需要知道两个 Agent 的地址和 skills。它的工作流程是:先调用翻译 Agent 拿到中文文本,再把中文文本作为参数调用摘要 Agent,最后把摘要结果返回给用户。

这种“编排模式”是 A2A 最常见的用法。每个 Agent 保持单一职责,复杂度全部集中在编排层。好处是显而易见的:任何一个 Agent 的内部实现变了,只要它的 Agent Card 不变,整个链路就无需改动;如果你想替换翻译服务,只需要换一个同样声明translate技能的 Agent 地址。

实操心得:多 Agent 场景下,务必注意消息大小和超时设置。A2A 协议对单个 Artifact 的大小没有硬性限制,但实际部署时一定要在网关层设置合理的体积上限和超时时间,否则一个 Agent 传大文件,会把整条链路拖死。

4. 常见问题与排查技巧实录

4.1 Agent Card 404 或无法发现

这个问题的现象是:客户端访问/.well-known/agent-card.json返回 404,或者拿到的是 HTML 错误页。排查顺序我建议如下。

第一步检查路径大小写,well-known全是小写,agent-card.json也是小写;第二步检查静态文件挂载配置,FastAPI 里要用app.mount("/.well-known", StaticFiles(directory="static"))这类方式显式暴露;第三步检查是否有反向代理拦截了这个路径。很多网关默认会屏蔽以点开头的路径或者.well-known下的请求,这个坑我踩过不止一次。

另外提一个很容易被忽略的点:Agent Card 的 Content-Type 必须是application/json,如果你用静态文件服务器托管,记得确认 MIME 类型配置正确,否则客户端解析会失败。这类“看起来是 404,其实是配置不对”的问题,最有效的排查方法是先用curl -i看响应头。

4.2 任务一直卡在 working 状态

任务状态长时间停留在working不前进,是 A2A 调试里最高频的问题。原因通常有三类。

第一类是最常见的:服务端的返回结构不合法。A2A 规定任务结束必须返回completed状态并附带 artifacts,或者返回failed状态及错误信息,如果你在业务处理里抛了异常但没有被 SDK 转换成合法的任务终止状态,客户端就会一直轮询。第二类是网络问题,服务端实际已经处理完,但回调通知没有送达,客户端只能干等。第三类是死锁,服务端在处理任务时又发起了对客户端的反向调用,两边互相等待。

排查的时候,我会先看服务端日志里任务处理是否结束,再看客户端轮询请求是否正确携带了任务 ID,最后用curl手动调一下服务端的tasks/get接口确认状态。如果服务端已经completed但客户端还在轮询旧状态,绝大多数是回调或缓存的问题。

4.3 消息格式与版本兼容

A2A 目前还在快速演进中,protocolVersion 字段一定要从 Agent Card 里读清楚。生产环境里我见过不少案例:两个 Agent 用的是同一套协议,但版本号不一致,导致对 artifact 结构和状态枚举的理解不同——比如早起版本用completed-artifact,新版改成了artifact数组结构。

解决这个问题没有银弹,最可靠的做法是两边都使用官方 SDK,并且把 SDK 版本锁在一个已知兼容的组合上。如果你在做一个生产级的 A2A Server,强烈建议再加一层 schema 校验,对请求体做严格校验后再进入业务逻辑。毕竟 A2A 的调用方可能是外部团队,你无法假设它会发什么格式过来。

4.4 安全与鉴权注意事项

A2A 协议默认没有内置鉴权,authentication字段只是声明了“需要什么鉴权方式”,具体怎么校验完全由服务端自己实现。所以不要在公网裸奔暴露 A2A 端点,这是我在任何场合都要强调的第一条安全底线。

实操层面有几个建议。第一,正常的生产环境必须上 TLS,保证消息在传输过程中不被篡改;第二,用 OAuth 2.0 Bearer Token 或 API Key 做调用方身份识别,并确保 Agent Card 里声明的鉴权方式和实际校验逻辑一致;第三,实现最小权限原则——并不是所有调用方都能触发所有技能,有些敏感技能需要额外授权;第四,对任务输入和产物做内容过滤,防止恶意 Agent 通过任务参数注入异常指令。

重要:任何 Agent 都不应该盲目信任另一个 Agent 的输出。A2A 消息本质上是数据,你需要像处理外部 API 响应一样对它做校验和清理,尤其在 Agent 输出会被直接作为代码执行或系统命令的情况下。

最后再说两句

A2A 协议从发布到现在,已经有不少团队从“观望”转向“落地”了。在我个人的实践里,它最大的价值不是让你少写几行 JSON 解析代码,而是让 Agent 系统的边界变得清晰:各团队独立开发部署自己的 Agent,通过 A2A 暴露标准接口,上层编排者可以像拼乐高一样组合它们。这种松耦合架构,才是大规模 Agent 协作真正需要的东西。

如果你刚开始接触 A2A,别急着上复杂编排和动态协商,先把第三部分的示例代码跑通,把“一个 Agent 调另一个 Agent 拿结果”的链路走完整。然后再去思考 Agent Card 怎么写更清晰、任务状态怎么管理更健壮、安全边界怎么划分更合理。等你把这些基础打牢,再回头设计多 Agent 协同,就会有一种“原来如此”的通透感。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询