A2A简述

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": "杭州今天天气怎么样" }
        ]
      }
    }
  }'

生产环境可以加一层网关或注册中心,让客户端只面对一个入口。

相关推荐
xiami_world1 小时前
Xmind、Miro、博思白板AI怎么选?思维导图/流程图自动生成
人工智能·ai·信息可视化·流程图·xmind
古少侠2 小时前
Mermaid图表和 LaTeX公式怎么无损转成 Word
ai
小新科研测评2 小时前
文献管理工具同步太麻烦?2026年4款多端同步工具实测,换设备不丢笔记
论文阅读·人工智能·笔记·ai·电脑
VIP_CQCRE2 小时前
ChatBox 接入 Ace Data Cloud 实战:配置 OpenAI 兼容模型,附 404/401 排错
ai·api·chatbox·ace data cloud
Summer-Bright2 小时前
深度 | 记忆的第二幕:当聊天记录变成生产状态
ai·agent记忆·记忆层
tigershang2 小时前
亲手拆开 Transformer:从手算入门到理论全景
神经网络·ai·矩阵·transformer
fengyangaiGEO3 小时前
东莞AI搜索优化公司参数分析
ai
每天一道题3 小时前
Agent 评测的关键,不是给它出难题,而是让它做选择
人工智能·ai
程序员无隅4 小时前
Matt Pocock 的 Agent 开发工作流:用 /implement-spec、/pr、/retro 完成实现、审查与复盘
ai