1. A2A 是什么
兼容不同厂商Agent的统一协议
A2A 是 Agent-to-Agent 协议,解决的是 Agent 之间怎么发现、委托、跟踪和交付任务。
表格
| 协议 | 方向 | 作用 |
|---|---|---|
| MCP | Agent ↔ 工具/数据 | 让 Agent 能调用工具、读数据 |
| A2A | Agent ↔ Agent | 让 Agent 之间能互相委托任务 |
| LangGraph / CrewAI | 单框架内编排 | 在一个进程里组织多个 Agent |
2. 核心概念
- Agent Card:Agent 的名片,包含名称、描述、URL、能力、skills、认证方式。
- Task :任务单元,有唯一 ID 和生命周期:
SUBMITTED → WORKING → COMPLETED / FAILED / CANCELED / REJECTED / INPUT_REQUIRED。 - Message :消息,包含
role和parts。 - Part :内容单元,支持
text、file、data。 - Artifact:任务产出物。
- Skill :Agent 对外暴露的能力,放在 Agent Card 的
skills数组里。
3. 服务端实现骨架
python
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.types import (
AgentCard,
AgentCapabilities,
AgentSkill,
TaskState,
TextPart,
)
from a2a.utils import completed_task, new_artifact
import uvicorn
class WeatherAgentExecutor(AgentExecutor):
async def execute(self, context: RequestContext, event_queue: EventQueue):
message = context.current_message
text = message.parts[0].text if message.parts else ""
result_text = f"杭州今天天气:晴,26°C(模拟结果,输入={text})"
task = completed_task(
context.task_id,
context.context_id,
[new_artifact(parts=[TextPart(text=result_text)])],
)
await event_queue.enqueue_event(task)
async def cancel(self, context: RequestContext, event_queue: EventQueue):
await event_queue.enqueue_event(
completed_task(
context.task_id,
context.context_id,
[],
state=TaskState.CANCELED,
)
)
def main():
weather_skill = AgentSkill(
id="query-weather",
name="查询天气",
description="根据城市查询当前天气",
tags=["weather"],
examples=["杭州今天天气怎么样"],
)
agent_card = AgentCard(
name="weather-agent",
description="天气查询 Agent",
url="http://localhost:8000",
version="1.0.0",
capabilities=AgentCapabilities(streaming=False),
skills=[weather_skill],
)
request_handler = DefaultRequestHandler(
agent_executor=WeatherAgentExecutor(),
task_store=InMemoryTaskStore(),
)
app = A2AStarletteApplication(
agent_card=agent_card,
http_handler=request_handler,
)
uvicorn.run(app.build(), host="0.0.0.0", port=8000)
if __name__ == "__main__":
main()
4. skillId 判断的服务端原理
这部分是刚才缺的重点。A2A 协议本身 没有强制规定服务端必须怎么路由 skill 。
skillId 是 Agent Card 里声明的能力标识,服务端收到请求后,需要自己决定走哪个 skill。
推荐的服务端路由顺序:
text
收到 tasks/send
↓
解析 message.parts
↓
如果带了 skillId → 直接路由
否则 → 从 data 部分尝试提取 skillId
仍没有 → 关键词 / 正则匹配
仍不确定 → 向量检索召回候选 skills
最后兜底 → 大模型从候选 skills 里选一个
↓
执行对应 skill
↓
把结果写入 artifacts / status
代码示例:
python
SKILLS = [
{
"id": "query-weather",
"name": "查询天气",
"description": "根据城市查询当前天气",
"tags": ["weather"],
"keywords": ["天气", "气温", "下雨", "晴", "温度"],
},
{
"id": "search-flights",
"name": "查询航班",
"description": "查询航班信息",
"tags": ["flight"],
"keywords": ["航班", "机票", "飞机"],
},
]
def extract_skill_id(parts: list[dict]) -> str | None:
for part in parts:
if part.get("type") == "data" and isinstance(part.get("data"), dict):
return part["data"].get("skillId")
return None
def route_skill(text: str, parts: list[dict]) -> dict:
# 1. 显式 skillId 优先
skill_id = extract_skill_id(parts)
if skill_id:
for s in SKILLS:
if s["id"] == skill_id:
return s
raise ValueError(f"unknown skill_id: {skill_id}")
# 2. 关键词粗匹配
scores = []
for s in SKILLS:
score = sum(1 for kw in s["keywords"] if kw in text)
scores.append((score, s))
scores.sort(key=lambda x: x[0], reverse=True)
best_score, best_skill = scores[0]
if best_score >= 2:
return best_skill
# 3. 不确定时再走大模型
return llm_choose_skill(text, SKILLS)
def llm_choose_skill(text: str, skills: list[dict]) -> dict:
prompt = (
"你是一个路由助手。根据用户文本,从下面技能列表中选择最合适的一个。\n\n"
+ "\n".join(
f"- {s['id']}: {s['name']},{s['description']}" for s in skills
)
+ f"\n\n用户文本:{text}\n只返回技能 id。"
)
chosen_id = call_llm(prompt).strip()
for s in skills:
if s["id"] == chosen_id:
return s
return skills[0]
接入 execute:
python
class WeatherAgentExecutor(AgentExecutor):
async def execute(self, context: RequestContext, event_queue: EventQueue):
message = context.current_message
parts = message.parts or []
text = parts[0].text if parts else ""
skill = route_skill(text, parts)
if skill["id"] == "query-weather":
result = run_weather_skill(text)
elif skill["id"] == "search-flights":
result = run_flight_skill(text)
else:
result = run_default_skill(text)
task = completed_task(
context.task_id,
context.context_id,
[new_artifact(parts=[TextPart(text=result)])],
)
await event_queue.enqueue_event(task)
如果 skill 很多,可以把每个 skill 的 name + description + examples 做成向量,用户 text 进来后先做向量召回,再让大模型从 Top-K 里选。这样大部分请求不用走大模型。
5. curl 调用流程
假设服务跑在:
bash
http://localhost:8000
发现 Agent Card
bash
curl -s http://localhost:8000/.well-known/agent-card.json | jq
如果 404,可以试:
bash
curl -s http://localhost:8000/.well-known/agent.json | jq
具体路径以 SDK 版本为准。
提交任务
bash
curl -X POST http://localhost:8000 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-001",
"method": "tasks/send",
"params": {
"id": "task-weather-001",
"message": {
"role": "user",
"parts": [
{
"type": "text",
"text": "杭州今天天气怎么样"
}
]
}
}
}' | jq
带 skillId 的请求
bash
curl -X POST http://localhost:8000 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-002",
"method": "tasks/send",
"params": {
"id": "task-weather-002",
"message": {
"role": "user",
"parts": [
{
"type": "data",
"data": {
"skillId": "query-weather",
"city": "杭州"
}
}
]
}
}
}' | jq
skillId 是可选字段,服务端不要强依赖它。
查询任务
bash
curl -X POST http://localhost:8000 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-003",
"method": "tasks/get",
"params": {
"id": "task-weather-001"
}
}' | jq
取消任务
bash
curl -X POST http://localhost:8000 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-004",
"method": "tasks/cancel",
"params": {
"id": "task-weather-001"
}
}' | jq
流式订阅
如果服务端支持流式,可以用类似:
bash
curl -N -X POST http://localhost:8000 \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "req-005",
"method": "tasks/sendSubscribe",
"params": {
"id": "task-weather-003",
"message": {
"role": "user",
"parts": [
{
"type": "text",
"text": "杭州今天天气怎么样"
}
]
}
}
}'
具体方法名以 SDK 版本为准,可能是 tasks/sendSubscribe、message/stream 或 message/sendSubscribe。
6. 客户端使用
python
import httpx
BASE_URL = "http://localhost:8000"
def get_agent_card():
resp = httpx.get(f"{BASE_URL}/.well-known/agent-card.json")
resp.raise_for_status()
return resp.json()
def send_task(task_id: str, text: str, skill_id: str | None = None):
parts = []
if skill_id:
parts.append({
"type": "data",
"data": {
"skillId": skill_id,
"query": text,
},
})
else:
parts.append({"type": "text", "text": text})
payload = {
"jsonrpc": "2.0",
"id": f"req-{task_id}",
"method": "tasks/send",
"params": {
"id": task_id,
"message": {
"role": "user",
"parts": parts,
},
},
}
resp = httpx.post(BASE_URL, json=payload)
resp.raise_for_status()
return resp.json()
def get_task(task_id: str):
payload = {
"jsonrpc": "2.0",
"id": f"req-get-{task_id}",
"method": "tasks/get",
"params": {"id": task_id},
}
resp = httpx.post(BASE_URL, json=payload)
resp.raise_for_status()
return resp.json()
7. LangChain 集成示例
把 LangChain Agent 包成 A2A Server
python
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""查询城市天气"""
return f"{city}今天晴,26°C"
llm = ChatOpenAI(model="gpt-4o-mini")
tools = [get_weather]
agent = create_react_agent(llm, tools)
agent_executor = AgentExecutor(agent=agent, tools=tools)
class LangChainWeatherExecutor(AgentExecutor):
async def execute(self, context: RequestContext, event_queue: EventQueue):
message = context.current_message
text = message.parts[0].text if message.parts else ""
result = agent_executor.invoke({"input": text})
output = result.get("output", "")
task = completed_task(
context.task_id,
context.context_id,
[new_artifact(parts=[TextPart(text=output)])],
)
await event_queue.enqueue_event(task)
让 LangChain 调用远程 A2A Agent
python
from langchain.tools import tool
import httpx
@tool
def call_weather_agent(query: str) -> str:
"""通过 A2A 调用天气 Agent"""
payload = {
"jsonrpc": "2.0",
"id": "req-1",
"method": "tasks/send",
"params": {
"id": "task-weather-1",
"message": {
"role": "user",
"parts": [{"type": "text", "text": query}],
},
},
}
resp = httpx.post("http://localhost:8000", json=payload)
resp.raise_for_status()
return resp.json()
LangChain v0.3 之后也开始提供 A2A 相关集成能力,但具体类名和用法要以你安装的版本为准。
8. 多 Agent 发布
更推荐这样理解:
- 一个 Agent 一张 Agent Card;
- 一张 Agent Card 里可以有多个 skills;
- 多个 Agent 就有多张 Agent Card;
- 客户端先拉 Agent Card,再根据 skills 决定调用谁。
例如:
text
http://localhost:8000/weather/.well-known/agent-card.json
http://localhost:8000/search/.well-known/agent-card.json
客户端调用:
bash
curl -X POST http://localhost:8000/weather \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-1",
"method": "tasks/send",
"params": {
"id": "task-weather-001",
"message": {
"role": "user",
"parts": [
{ "type": "text", "text": "杭州今天天气怎么样" }
]
}
}
}'
生产环境可以加一层网关或注册中心,让客户端只面对一个入口。