这一篇 详解A2A(Agent2Agent)协议 就够你了解A2A:用TaoToken统一Key跑通JSON-RPC与SSE双通道
1. 从一次 Agent 联调失败说起A2A 协议到底解决什么问题如果你正在做多 Agent 协作大概率遇到过这种场景一个负责规划的 Agent 想把子任务交给另一个负责执行的 Agent结果两边一个是 Python 写的、一个是 Node 写的工具调用格式对不上流式输出接不住最后只能靠手写 HTTP 接口硬拼。A2AAgent2Agent协议就是冲着这个痛点来的——它由 Google Cloud 推出目标很明确让不同厂商、不同框架构建的 AI 代理之间能够标准化通信和协作。A2A 是什么一句话概括它是一套建立在 HTTP、SSE 和 JSON-RPC 之上的开放协议定义了 Agent 之间如何发现彼此能力、如何发起任务、如何流式接收结果。能做什么你可以让一个 Agent 通过标准 JSON-RPC 请求把任务发给远程 Agent远程 Agent 用 SSE 把中间状态和最终产物一段段推回来。适合谁适合正在做多 Agent 编排、想让自己的 Agent 接入更大生态、或者单纯想快速验证 Agent2Agent 通信链路的开发者。A2A 里有三个核心角色用户User、客户端Client、服务端Server。客户端代表用户向远程代理发起请求服务端就是那个“黑盒”远程代理。和 MCP 相比A2A 少了 Host 这个角色设计上更偏向开放实现而非规范一个固定机制。它的核心概念包括 AgentCard描述 Agent 能力的 JSON 文件官方建议托管在/.well-known/agent.json、Task有状态的任务实体、Artifact任务产出的不可变结果、Message承载思考、指令、错误等非工件内容以及 Part消息或工件中的内容片段支持文本、文件、数据三种类型。通信机制上A2A 支持标准请求/响应模式也支持通过 SSE 进行流式更新。客户端处理长时间运行的任务时可以轮询代理也可以通过 SSE 主动推送状态更新。更关键的是异步支持客户端可以注册一个 webhook异步获取长任务的最终结果这就是 PushNotification 相关实现。用过 LLM API 的人都知道输出慢、输出过程中干不了别的事是常态有了异步回调或重新订阅客户端开发就能更灵活用户体验也更好。错误处理方面A2A 沿用标准 JSON-RPC 错误码-32700 JSON 解析错误、-32600 无效请求、-32601 方法未找到、-32602 无效参数、-32603 内部错误以及 -32001 任务未找到、-32002 任务无法取消、-32003 不支持推送通知、-32004 不支持的操作、-32005 内容类型不兼容等实现特定错误码。这些错误码在你联调 JSON-RPC 握手时会频繁出现后面排障章节会逐个对照。理解了这些你就明白为什么需要一套统一的 Key 和 Base URL 来跑通链路——因为 A2A 客户端和服务端都要调用底层模型如果每个 Agent 都配一套不同的凭证联调成本会非常高。这也是我接下来要引入 TaoToken 统一 Key 的原因。2. 用 TaoToken 统一 Key 打通 A2A 的模型调用层A2A 协议本身只规定 Agent 之间怎么通信不规定 Agent 内部用哪个模型。但实际联调时你的 Coder Agent 需要调用 LLM 来生成代码这个调用如果直连各家厂商就会遇到 Key 分散、Base URL 不统一、模型 ID 写法各异的问题。我的做法是用 TaoToken 作为统一的模型接入层让 A2A 服务端和客户端都指向同一个 Base URL 和同一把 Key。TaoToken 在这里扮演的角色是“模型调用的统一入口”。你可以在官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 了解它的能力API 端点则是 https://taotoken.net/api注意这个地址不加 UTM 参数。它的价值在于你只需要维护一套凭证就能在 A2A 的各个 Agent 里调用不同模型联调时不用来回切换配置。具体怎么配A2A 的示例项目通常用环境变量读取模型配置。以我改造过的 a2a-agent-coder 为例它原本用 OpenRouter我把它改成兼容 OpenAI 形式的 TaoToken 接入。你需要在项目根目录创建一个.env文件内容如下OPENAI_API_KEY你的TaoTokenKey OPENAI_BASE_URLhttps://taotoken.net/api OPENAI_MODELclaude-3.5-haiku这三行就是 A2A 接入的“三件套”Base URL、Key、Model ID。Base URL 固定指向 TaoToken 的 API 端点Key 从控制台获取Model ID 按你实际要用的模型填写。注意OPENAI_BASE_URL后面不要带/v1之类的后缀具体以接入文档为准因为不同 SDK 对路径拼接的处理不一样带错了会出现 404。如果你用的是 Claude Code 这类工具做 A2A 服务端的代码润色或生成配置逻辑是一样的在 settings 里指定 Base URL 和 Key模型 ID 填你套餐里可用的。TaoToken 的 Coding Plan 适合长期编码和 Agent 场景如果你打算把 A2A 链路跑成常态化的开发流程可以关注一下 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 这个入口。这里有个容易踩的坑A2A 服务端和客户端可能跑在两个不同的进程里环境变量不会自动共享。我的做法是把.env放在项目根目录服务端启动前用export $(cat .env | xargs)让变量生效客户端如果也需要调模型同样处理。如果客户端只是发 JSON-RPC 请求、不直接调模型那它不需要模型 Key只需要知道服务端的 URL。还有一点A2A 的 AgentCard 里会声明capabilities.streaming是否为 true。如果你打算用 SSE 流式返回服务端必须支持流式而流式的前提是底层模型调用也支持流式。TaoToken 的 API 兼容 OpenAI 的流式接口所以只要你的 A2A 服务端代码正确处理了stream: true的响应SSE 通道就能跑通。配置阶段先把这三件套写对后面验证请求时就不会因为凭证问题卡住。3. 可复制的 A2A 服务端与客户端配置片段这一节给你可以直接复制粘贴的配置和代码片段。我以 a2a-agent-coder 这个 TypeScript 项目为蓝本它原本用 OpenRouter我改成了 TaoToken 接入。你不需要完全照搬重点是理解配置的结构然后套用到你自己的 A2A 实现里。首先是服务端的模型配置。在项目里找到读取环境变量的地方通常是src/agent.ts或类似的入口文件。确保它用的是 OpenAI 兼容的客户端初始化方式import OpenAI from openai; const client new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: process.env.OPENAI_BASE_URL, }); const model process.env.OPENAI_MODEL || claude-3.5-haiku;这段代码的关键是baseURL指向 TaoToken 的 API 端点apiKey从环境变量读取。如果你的 A2A 实现用的是其他语言比如 Python对应的初始化是from openai import OpenAI client OpenAI( api_keyos.environ[OPENAI_API_KEY], base_urlos.environ[OPENAI_BASE_URL], )接下来是 A2A 服务端的 AgentCard 配置。AgentCard 是一个 JSON 文件官方建议托管在/.well-known/agent.json。一个最小可用的 AgentCard 长这样{ name: Coder Agent, description: An agent that generates code based on natural language instructions and streams file outputs., url: http://localhost:41241, version: 0.0.1, capabilities: { streaming: true, pushNotifications: false }, defaultInputModes: [text], defaultOutputModes: [text, file], skills: [ { id: code-generation, name: Code Generation, description: Generate code from natural language instructions, tags: [coding, generation], examples: [implement binary search] } ] }注意capabilities.streaming设为 true这样客户端才知道可以用 SSE 订阅。url填你服务端实际监听的地址和端口。客户端的配置相对简单它主要需要知道服务端的 URL。在 a2a-agent-coder 的 CLI 里默认连的是http://localhost:41241。如果你要改找到src/cli.ts里的 Agent URL 配置项。客户端发起任务时核心是构造 JSON-RPC 请求{ jsonrpc: 2.0, id: a1a608b3-3015-4404-a83f-6ccc05083761, method: tasks/sendSubscribe, params: { id: task-001, message: { role: user, parts: [ { type: text, text: implement binary search } ] } } }这个请求通过 HTTP POST 发给服务端method是tasks/sendSubscribe表示要订阅任务流。服务端收到后会先返回一个 SSE 流然后陆续推送状态更新和工件。如果你用的是 Cline MCP 或类似的工具来管理 A2A 服务端进程配置里同样要写全三件套。以 Cline 的 MCP 配置为例在 settings 里添加{ mcpServers: { a2a-coder: { command: bun, args: [run, agents:coder], env: { OPENAI_API_KEY: 你的TaoTokenKey, OPENAI_BASE_URL: https://taotoken.net/api, OPENAI_MODEL: claude-3.5-haiku } } } }这样 Cline 启动 A2A 服务端时环境变量会自动注入。如果你用 Codex 的 auth.json 方式管理凭证逻辑类似把 Base URL 和 Key 写进对应的配置字段即可。配置写完后启动服务端export $(cat .env | xargs) bun run agents:coder看到服务端监听在 41241 端口就说明配置生效了。接下来打开另一个终端跑客户端验证整条链路。4. 用 curl 与日志验证 JSON-RPC 握手和 SSE 事件流配置写好了怎么确认 JSON-RPC 握手成功、SSE 事件流正常返回我习惯先用 curl 手动打一发看原始响应再跑客户端看完整流程。这样出问题时能快速定位是协议层还是应用层的问题。第一步验证 AgentCard 能否获取。A2A 规定 AgentCard 托管在/.well-known/agent.json用 curl 直接 GETcurl -s http://localhost:41241/.well-known/agent.json | jq .如果返回了完整的 JSON包含 name、description、capabilities、skills 等字段说明服务端的发现端点正常。如果返回 404检查你的服务端有没有正确注册这个路由如果返回 500看服务端日志里的报错。第二步手动发一个 JSON-RPC 请求验证握手。用 curl 发 POSTcurl -N -X POST http://localhost:41241 \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: test-001, method: tasks/sendSubscribe, params: { id: task-001, message: { role: user, parts: [{type: text, text: implement binary search}] } } }注意-N参数它关闭 curl 的缓冲让你能实时看到 SSE 流。正常返回应该是一系列data:开头的事件每个事件是一个 JSON 对象包含任务状态更新或工件内容。你会看到类似这样的输出data: {id:task-001,status:{state:working,message:{role:agent,parts:[{type:text,text:Generating code...}]}},final:false} data: {id:task-001,artifact:{name:src/algorithms/binary_search.py,parts:[{type:text,text:def binary_search(arr, target):\n ...}]},index:0} data: {id:task-001,status:{state:completed},final:true}如果看到state从working变成completed并且final为 true说明 SSE 事件流完整走通了。如果卡在working不动可能是模型调用超时或流式响应没接住如果直接返回错误对象看error.code对照 JSON-RPC 错误码表。第三步跑完整客户端看端到端流程。在另一个终端执行bun run a2a:cli客户端会先拉取 AgentCard打印出 Agent 名称和描述然后提示你输入消息。输入implement binary search后你会看到状态从 working 到 completed中间夹杂着生成的代码文件内容。我实测下来从发送到收到完整工件大约几秒钟取决于模型响应速度。日志方面服务端控制台会打印每个 JSON-RPC 请求的方法名和任务 ID客户端会打印 SSE 事件的接收情况。如果 SSE 流中断客户端通常会报SSE stream finished或连接重置。这时候检查服务端有没有正确设置Content-Type: text/event-stream以及有没有在final: true之后关闭连接。还有一个验证点任务状态历史。A2A 的 Task 支持history字段如果你在请求里带了historyLength服务端会返回最近的消息记录。用 curl 发一个带historyLength的请求看返回里有没有history数组能进一步确认任务状态管理是否正常。5. 联调常见报错排查401、local proxy failed、reading choices、OAuth联调 A2A 时报错基本集中在模型调用层和协议层。我把踩过的坑按报错信息整理出来你对照着排查。401 Unauthorized这是最常见的。原因通常是 Key 没生效或 Base URL 写错。先确认.env里的OPENAI_API_KEY是不是完整的 TaoToken Key有没有多余空格。然后确认OPENAI_BASE_URL是不是https://taotoken.net/api不要带/v1或结尾斜杠。如果你在 Cline MCP 或 Codex auth.json 里配置检查 JSON 字段名有没有写错比如把apiKey写成api_key。还有一个隐蔽原因环境变量没导出。服务端进程如果没读到.env就会用空 Key 去请求直接 401。用export $(cat .env | xargs)确保变量注入。local proxy failed这个报错通常出现在客户端尝试连接服务端时。检查服务端有没有真正启动端口是不是 41241防火墙有没有拦。如果你在容器里跑服务端客户端在宿主机localhost是不通的要把 AgentCard 里的url改成容器 IP 或映射后的地址。另外有些 A2A 实现会走本地代理转发如果代理配置和实际监听端口不一致也会报这个错。确认url字段和实际监听地址完全一致。reading choices 相关报错这个报错来自 OpenAI 兼容接口的响应解析。当你用流式请求时SDK 期望返回的 chunk 里有choices数组如果 TaoToken 返回的格式和 SDK 预期不一致就会在读取choices时抛错。排查方法先用非流式请求测一下把stream设为 false看能不能正常返回。如果非流式正常、流式报错检查你的 A2A 服务端代码有没有正确处理 SSE 的data:行有没有在 chunk 不完整时就尝试 JSON.parse。另一个原因是模型 ID 写错了TaoToken 返回了错误对象而不是正常的 choices 结构SDK 解析时就崩了。确认OPENAI_MODEL是你套餐里真实可用的模型 ID。OAuth 相关报错如果你在 A2A 的 AgentCard 里声明了authentication.schemes但客户端没带对应的凭证服务端会返回认证错误。A2A 的认证结构参考 OpenAPI支持 Basic、Bearer 等。如果你只是本地联调先把 AgentCard 里的authentication字段去掉或留空避免不必要的认证拦截。如果确实需要认证确保客户端在请求头里带了Authorization: Bearer token并且 token 和服务端配置的一致。JSON-RPC 错误码对照如果服务端返回-32601 Method not found说明你请求的method服务端没实现检查是不是拼成了tasks/send而不是tasks/sendSubscribe。返回-32602 Invalid params检查params里的id、message、parts结构是否符合 A2A 定义。返回-32001 Task not found说明你查询的任务 ID 不存在可能是任务已经过期或被清理。返回-32003 Push notifications not supported说明服务端没实现推送通知但你的请求里带了pushNotification配置去掉即可。SSE 流提前关闭客户端收到final: true之前连接就断了。检查服务端有没有在模型流式响应结束前就关闭了 HTTP 连接。正确做法是等模型返回[DONE]或流结束信号后再发送final: true的事件并关闭。另外某些反向代理会缓冲 SSE导致事件不是实时推送检查代理配置里有没有关闭缓冲。排查时养成看原始响应的习惯。curl 加-v看请求头和响应头确认Content-Type和状态码。服务端日志打印每个请求的 method 和 params客户端日志打印每个 SSE 事件的类型。两边日志一对问题基本就定位了。6. 跑通之后A2A 与 MCP 的边界以及下一步怎么走链路跑通后你可能会问A2A 和 MCP 到底什么关系我大概总结一下。A2A 主要用于代理间通信和协作核心结构是客户端-服务端代理-代理MCP 主要用于为模型提供工具和上下文、连接外部资源核心结构是客户端-主机-服务端应用-LLM-外部资源。接口标准上A2A 用 JSON 规范、代理卡、任务、消息、工件MCP 用 JSON-RPC 2.0、资源、工具、记忆、提示。通信协议上A2A 走 HTTP、JSON-RPC、SSEMCP 走 JSON-RPC 2.0 over stdio 或 HTTP with SSE。性能侧重点上A2A 强调异步通信、处理负载MCP 强调上下文管理、并行处理、缓存。从技术上看A2A 适应的场景更多某种程度上包含了 MCP 的场景。如果未来 Agent 很多、MCP server 也很多会构成一个什么样的网络前者更倾向于去中心化后者更倾向于中心化前者偏向分散自治后者是集中管理。这个边界目前还在演化没有绝对答案。回到实操。你现在已经能用 TaoToken 统一 Key 跑通 A2A 的 JSON-RPC 握手和 SSE 事件流了。下一步可以做的把 AgentCard 托管到真实域名让其他 Agent 能发现你的服务实现 PushNotification让长任务在完成后异步回调或者把多个 A2A Agent 串起来做一个多跳的任务编排。如果你想把这条链路用于日常编码TaoToken 的 Coding Plan 提供了适合长期 Agent 场景的额度方案入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。需要管理多把 Key 或查看用量去控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。想先验证模型对话效果可以用模型对话页 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。API Key 的创建和管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。最后分享一个实用技巧A2A 联调时把服务端的日志级别调到 debug打印每个 JSON-RPC 请求的完整 params 和每个 SSE 事件的原始内容。这样当客户端报reading choices或流中断时你能直接看到服务端到底发了什么比在客户端猜要快得多。另外AgentCard 的capabilities.streaming一定要和实际实现一致声明了 true 但服务端不支持流式客户端会一直等不到事件。