1. 为什么单智能体越来越不够用:从 A2A 协议要解决的真实问题说起
如果你最近在折腾 AI 智能体,大概率会遇到一个尴尬的场面:你写了一个能查天气的 Agent,又写了一个能订机票的 Agent,还想让它们配合完成“帮我规划一趟东京五日游”,结果发现两个 Agent 之间根本没法对话。你要么把逻辑全塞进一个巨型 Prompt 里,要么写一堆 if-else 硬编码调用关系。Agent2Agent(简称 A2A)协议就是冲着这个痛点来的——它是一套让不同团队、不同框架、甚至不同组织开发的 AI 智能体能够互相通信、协商、协作的开放标准。
一句话概括:A2A 是智能体之间的“普通话”,底层用 JSON-RPC 2.0 over HTTP(S) 传输,通过 Agent Card 做能力发现,通过 Task 做状态化任务管理。它和 MCP 不是竞争关系——MCP 解决的是“模型怎么调用工具”,A2A 解决的是“智能体怎么找智能体、怎么把任务委托出去”。你可以把 MCP 理解成给机械师配的扳手和诊断仪,A2A 则是店长和机械师、机械师和零件供应商之间的对讲机。
这篇文章适合三类人:一是正在做多智能体编排、被点对点集成折磨的工程师;二是想快速搭一条可运行的 A2A 协作链路、不想啃完整规范的人;三是已经在用统一 API 通道调用多家模型、想把 Agent 协作也纳入同一套 Key 体系的人。我会用 TaoToken 的统一 API 通道作为模型调用底座,把 A2A 端点的配置、JSON-RPC 请求、连通性验证一步步跑通,你照着做就能得到一条能实际发消息、能拿到 Task 结果的协作链路。
需要先明确一个边界:A2A 本身是通信协议,它不负责模型推理。你的智能体背后用哪个模型、走哪条 API 通道,是另一层的事。把这两层分开,后面配置才不会乱。我试过把模型调用和 A2A 通信混在一起写,结果排障时完全分不清是协议层的问题还是模型层的问题,这个坑你可以提前避开。
2. TaoToken 统一 API 通道:给 A2A 智能体一个稳定的模型底座
在搭 A2A 链路之前,得先解决“智能体的大脑从哪来”。一个 A2A 服务端智能体收到任务后,通常要调用大模型做推理、规划、生成回复。如果你每个智能体都单独配一家模型的 Key,多智能体一多,Key 管理、额度、模型切换就会变成一团乱麻。TaoToken 在这里的角色是统一 API 通道:你用一套 Key、一个 Base URL,就能调用多家模型,A2A 里的每个智能体都能复用这套通道,不用各自维护供应商配置。
先把关键地址记清楚,后面配置会反复用到:
- 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API 基址:https://taotoken.net/api (这个不加 UTM,直接作为 Base URL 用)
- 模型对话体验:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=models
- Coding Plan(长期编码/Agent 场景):https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=codingplan
- 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=console
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=apikeys
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
- Claude Code 接入:https://taotoken.net/claudecode?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=claudecode
为什么 A2A 场景特别需要统一通道?因为 A2A 的典型用法是“客户端智能体把任务委托给远程智能体”,远程智能体可能跑在另一台机器、另一个容器里。如果每个远程智能体都要单独配 Key,你部署三个智能体就要管三套凭证。用 TaoToken 统一通道后,所有智能体的模型调用都指向同一个 Base URL,Key 也统一,部署时只需要注入一次环境变量。这对多智能体编排的运维成本是实打实的降低。
这里要强调一个概念区分,很多人第一次接触会混淆:TaoToken 是模型 API 通道,A2A 是智能体通信协议,两者是叠加关系。你的 A2A 服务端代码里,收到 JSON-RPC 请求后,内部再去调用 TaoToken 的模型接口生成回复。链路是“A2A 客户端 → A2A 服务端 → TaoToken API → 模型 → 返回”。把这条链路画清楚,后面排障时你就能快速定位问题出在哪一段。
关于 Key 的获取,直接去 API Keys 页面创建即可,创建后复制保存,后面配置里用TAOTOKEN_API_KEY这个环境变量承载。模型 ID 的选择上,A2A 服务端做任务规划建议用推理能力强的模型,做简单回复可以用轻量模型,具体可用模型列表在模型对话页面能看到。如果你打算长期跑 Agent 编排,Coding Plan 那条通道对高频调用更友好,可以按需了解。
3. 可复制的 A2A 端点配置:Agent Card、JSON-RPC 与 settings 片段
这一节是全文最核心的部分,我直接把可复制的配置给你。A2A 服务端要对外暴露两个东西:一个是 Agent Card(智能体名片),放在/.well-known/agent.json;另一个是 JSON-RPC 端点,通常挂在/a2a路径下。客户端先拉 Agent Card 发现能力,再往 JSON-RPC 端点发message/send请求。
先看 Agent Card 的完整 JSON,这是服务端要返回给发现请求的内容:
{ "name": "travel-planner-agent", "description": "负责旅行规划与任务拆分的 A2A 服务端智能体", "provider": "TaoToken Demo", "url": "https://your-agent-host/a2a", "version": "1.0.0", "capabilities": { "streaming": true, "pushNotifications": false }, "authentication": { "schemes": ["Bearer"] }, "defaultInputModes": ["text"], "defaultOutputModes": ["text"], "skills": [ { "id": "trip-planning", "name": "行程规划", "description": "根据目的地和天数生成行程草案", "inputModes": ["text"], "outputModes": ["text"] } ] }注意url字段必须和你的 JSON-RPC 端点真实地址一致,客户端会拿这个地址发请求。capabilities.streaming设为 true 表示支持 SSE 流式返回,如果你的实现暂时不支持,设成 false 避免客户端误判。
接下来是服务端的模型调用配置。A2A 服务端内部调 TaoToken,用环境变量承载凭证,配置文件可以写成这样(以常见的.env加代码读取为例):
# .env TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的Key TAOTOKEN_MODEL_ID=你的模型ID A2A_HOST=0.0.0.0 A2A_PORT=8080如果你用的是支持 settings 文件的框架(比如某些 Agent 运行时),可以写成 TOML:
[a2a] host = "0.0.0.0" port = 8080 agent_card_path = "/.well-known/agent.json" rpc_path = "/a2a" [model] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model_id = "你的模型ID"这里三件套必须齐全:Base URL 指向https://taotoken.net/api,Key 走环境变量注入,Model ID 明确写死或从环境读取。缺任何一个,服务端在收到任务后调模型就会失败。我见过有人只配了 Base URL 和 Key,忘了 Model ID,结果 A2A 请求能进来、Task 能创建,但一直卡在 working 状态,最后超时——这就是模型层没配全的典型表现。
然后是 JSON-RPC 请求示例。客户端向服务端发message/send,请求体结构如下:
{ "jsonrpc": "2.0", "id": "req-001", "method": "message/send", "params": { "message": { "role": "user", "messageId": "msg-001", "parts": [ { "kind": "text", "text": "帮我规划东京五日游,偏好文化类景点" } ] } } }服务端处理完后返回 Task 对象,包含id、status、artifacts等字段。如果任务需要多轮,状态会先到input-required,客户端补充消息后再继续。这个状态机是 A2A 区别于普通 REST 调用的关键,配置时要把状态流转逻辑写清楚。
4. 验证请求与成功结果:用 curl 跑通一次完整协作
配置写完,必须验证。我习惯先用 curl 打两个请求:先拉 Agent Card,再发一条 message/send。这样能把“发现”和“通信”两段分开验证,出问题好定位。
第一步,验证 Agent Card 可发现:
curl -s http://localhost:8080/.well-known/agent.json | python -m json.tool预期返回就是第 3 节那份 JSON,name、url、skills都在。如果返回 404,说明你的路由没挂对;如果返回 HTML,说明被前端路由拦截了,检查服务端路由优先级。
第二步,发一条 JSON-RPC 请求:
curl -s -X POST http://localhost:8080/a2a \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "jsonrpc": "2.0", "id": "req-001", "method": "message/send", "params": { "message": { "role": "user", "messageId": "msg-001", "parts": [{"kind": "text", "text": "帮我规划东京五日游"}] } } }' | python -m json.tool成功的话你会拿到类似这样的返回:
{ "jsonrpc": "2.0", "id": "req-001", "result": { "id": "task-abc123", "status": { "state": "completed" }, "artifacts": [ { "name": "trip-plan", "parts": [ { "kind": "text", "text": "第一天:浅草寺、上野公园……" } ] } ] } }看到state: completed和artifacts里有内容,说明整条链路通了:A2A 请求进来 → 服务端调 TaoToken → 模型返回 → 封装成 Artifact 返回。如果state是working一直不变,多半是模型调用卡住或超时;如果是failed,看返回里的错误信息,通常是模型层报错。
第三步,验证流式。如果你的服务端开了streaming,可以用 curl 带 SSE 头:
curl -N -X POST http://localhost:8080/a2a \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{"jsonrpc":"2.0","id":"req-002","method":"message/stream","params":{"message":{"role":"user","messageId":"msg-002","parts":[{"kind":"text","text":"继续补充第二天行程"}]}}}'你会看到分块返回的data:行,每块是一个增量 Part。流式验证通过,说明你的 A2A 服务端支持增量输出,前端体验会更好。
实测下来,最容易出问题的不是协议本身,而是模型层的连通性。建议在写 A2A 服务端之前,先用一段最小代码单独验证 TaoToken 通道能通:
import os, requests resp = requests.post( f"{os.environ['TAOTOKEN_BASE_URL']}/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ['TAOTOKEN_API_KEY']}"}, json={ "model": os.environ["TAOTOKEN_MODEL_ID"], "messages": [{"role": "user", "content": "ping"}] }, timeout=30 ) print(resp.status_code, resp.json()["choices"][0]["message"]["content"])这段先跑通,再去接 A2A,排障范围能缩小一半。
5. 本篇常见错误排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来,我把踩过的和读者反馈最多的几类整理出来,对照着查。
401 Unauthorized。这个最常见,出现在两个位置:一是 A2A 端点本身的鉴权,二是服务端调 TaoToken 时的鉴权。先确认你 curl 时带的Authorization头是否正确,再确认服务端环境变量TAOTOKEN_API_KEY有没有真正注入。很多人本地.env写了,但容器启动时没挂载,进程里读到的是空字符串,结果调模型就 401。排查方法:在服务端启动日志里打印 Key 的前 6 位(别打全),确认非空。
local proxy failed / connection refused。这个报错通常和网络层有关,不是 A2A 协议问题。检查你的TAOTOKEN_BASE_URL是不是写成了带路径的完整地址,正确值是https://taotoken.net/api,不要多加/v1或结尾斜杠。另外确认服务端所在环境能正常发起 HTTPS 出站请求,有些沙箱环境默认禁出站,需要放行。
reading 'choices' of undefined。这是典型的模型返回结构不符合预期。原因一般是:请求体里model字段为空或写错,或者返回的其实是错误对象而不是正常 completion。排查时先把原始resp.text打出来,别直接取choices。如果返回里是{"error": ...},那就是模型层报错,按错误信息处理;如果返回是空,检查请求是否真的发出去了。
OAuth / Bearer 混用导致鉴权失败。A2A 的 Agent Card 里authentication.schemes声明了用 Bearer,但有些实现会误配成 OAuth 流程,客户端拿不到 token。如果你只是内部协作,用 Bearer + 静态 Key 最简单,别一上来就上 OAuth。等链路跑通、需要多租户时再升级鉴权方案。
Task 一直 input-required 不推进。这不是报错,是状态机没写对。A2A 允许服务端在需要补充信息时把状态置为input-required,等客户端再发一条 message 续上。如果你的服务端逻辑里没有处理“续接同一 Task”的分支,就会一直卡住。检查你的 Task 存储是否按taskId关联了多轮消息。
Agent Card 拉到了但 url 字段指向错误。客户端会拿 Agent Card 里的url去发 JSON-RPC,如果这个字段写的是localhost而客户端在另一台机器,就会连接失败。部署时把url配成对外可达的地址,别图省事写死 localhost。
排障时记住一个原则:先分层,再定位。A2A 链路分四层——发现层(Agent Card)、协议层(JSON-RPC 格式)、模型层(TaoToken 调用)、网络层(出站连通性)。哪一层报错就查哪一层,别混着改。我见过有人一报错就同时改协议格式和模型配置,结果越改越乱。
6. 把 A2A 协作链路接进你的统一通道
走到这里,你已经有了 Agent Card、JSON-RPC 端点、模型调用配置和一套排障对照表。接下来最实际的动作,是把这条链路接到你日常用的统一通道上,让 A2A 智能体的模型调用和你的其他 AI 应用共用一套 Key。
具体怎么做:先去 API Keys 页面创建或复用你的 Key,把它注入到 A2A 服务端的环境变量里;Base URL 统一用https://taotoken.net/api;Model ID 按你的场景选,规划类任务选推理强的,回复类选轻量的。接入文档里有各语言的调用示例,照着改 Base URL 和 Key 就能迁移。如果你打算长期跑多智能体编排、调用频率高,Coding Plan 那条通道值得看一下,对 Agent 场景的额度更友好。
验证模型是否可用,可以直接在模型对话页面手动发一条消息,确认通道正常,再去跑 A2A 请求。这样能把“模型通道”和“A2A 协议”两段分开验证,出问题时定位更快。
最后给一个实用建议:多智能体协作最容易失控的地方不是协议,而是上下文传递。A2A 的 Task 支持多轮,但你要自己决定哪些上下文放进 message、哪些放进 Artifact。我的做法是——指令和追问放 message,结构化结果放 Artifact,长文档放 FilePart。这样客户端和服务端的职责清晰,后续加智能体也不用重构消息格式。链路跑通只是开始,把消息边界设计好,这套协作才能长期维护下去。