A2A 协议深度解析(下):流式通信、安全认证与多轮对话任务

2 阅读12分钟

上篇讲完了 A2A 的基本概念:Agent Card、Task 生命周期、最简单的请求-响应实现。这篇深入进阶内容:当任务需要实时反馈时怎么办?当 Agent 之间需要多轮对话时怎么处理?生产环境的安全认证怎么做?


从"同步等待"到"流式更新"

上篇的实现有一个明显的问题:/tasks/send 是同步接口,发出任务后需要等待 Agent 处理完毕才返回结果。

但现实中很多任务耗时较长:

  • 数据分析 Agent 处理一个百万行 CSV 可能要 30 秒
  • 代码生成 Agent 逐行生成代码,希望边生成边展示
  • 搜索 Agent 查询多个数据源,希望每查到一条就先返回

这就需要流式通信(Streaming)

A2A 的流式接口:SSE

A2A 协议采用 Server-Sent Events(SSE) 实现流式推送。SSE 是一种单向推送协议,服务端可以随时向客户端发送消息,非常适合"任务进度推送"这个场景。

流式任务的接口是 POST /tasks/sendSubscribe,返回的不是 JSON,而是一个持续的事件流:

POST /tasks/sendSubscribe
Content-Type: application/json
Accept: text/event-stream

{
  "id": "task-002",
  "message": {
    "role": "user",
    "parts": [{"text": "分析 sales_q1.csv 并生成报告"}]
  }
}

--- 服务端响应 ---
Content-Type: text/event-stream

data: {"id":"task-002","status":{"state":"working"},"message":{"role":"agent","parts":[{"text":"正在读取文件,共 1,024 行..."}]}}

data: {"id":"task-002","status":{"state":"working"},"message":{"role":"agent","parts":[{"text":"文件读取完成,开始分析..."}]}}

data: {"id":"task-002","status":{"state":"working"},"message":{"role":"agent","parts":[{"text":"发现销售额最高的 5 个产品,正在生成图表..."}]}}

data: {"id":"task-002","status":{"state":"completed"},"artifacts":[{"name":"report","parts":[{"text":"# Q1 销售报告\n\n...完整报告内容..."}]}]}

每条 data: 行都是一个完整的 JSON 对象,状态字段 status.state 会从 working 变成 completedfailed

实现流式 Agent

# streaming_agent.py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import asyncio
import json

app = FastAPI()

AGENT_CARD = {
    "name": "DataAnalystAgent",
    "description": "数据分析 Agent,支持 CSV/JSON 数据分析",
    "url": "http://localhost:8002",
    "version": "1.0.0",
    "capabilities": {
        "streaming": True  # 声明支持流式
    },
    "skills": [
        {
            "id": "analyze-data",
            "name": "数据分析",
            "description": "分析结构化数据并生成报告",
            "inputModes": ["text", "file"],
            "outputModes": ["text"]
        }
    ]
}

class TaskRequest(BaseModel):
    id: str
    message: dict

@app.get("/.well-known/agent.json")
def get_agent_card():
    return AGENT_CARD

@app.post("/tasks/send")
async def handle_task_sync(request: TaskRequest):
    """同步接口:等待处理完毕后返回"""
    result_text = await analyze_data(request.message)
    return {
        "id": request.id,
        "status": {"state": "completed"},
        "artifacts": [{"name": "result", "parts": [{"text": result_text}]}]
    }

@app.post("/tasks/sendSubscribe")
async def handle_task_stream(request: TaskRequest):
    """流式接口:边处理边推送进度"""
    
    async def event_generator():
        task_id = request.id
        
        # 发送进度更新的辅助函数
        def make_event(state: str, text: str, final=False, artifacts=None) -> str:
            payload = {
                "id": task_id,
                "status": {"state": state},
                "message": {"role": "agent", "parts": [{"text": text}]},
                "final": final
            }
            if artifacts:
                payload["artifacts"] = artifacts
            return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
        
        # Step 1: 接收任务
        yield make_event("working", "已接收任务,开始处理...")
        await asyncio.sleep(0.5)
        
        # Step 2: 解析数据
        yield make_event("working", "正在解析输入数据...")
        await asyncio.sleep(1.0)
        
        # Step 3: 执行分析
        yield make_event("working", "正在执行数据分析,发现 1,024 条记录...")
        await asyncio.sleep(1.5)
        
        # Step 4: 生成报告
        yield make_event("working", "分析完成,正在生成报告...")
        await asyncio.sleep(0.5)
        
        # Step 5: 返回最终结果
        report = await analyze_data(request.message)
        yield make_event(
            "completed",
            "任务完成",
            final=True,
            artifacts=[{"name": "report", "parts": [{"text": report}]}]
        )
    
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no"  # 禁用 Nginx 缓冲
        }
    )

async def analyze_data(message: dict) -> str:
    """实际的数据分析逻辑(示例)"""
    user_text = ""
    for part in message.get("parts", []):
        if "text" in part:
            user_text += part["text"]
    return f"分析完成。数据集包含 1,024 条记录,Q1 总销售额 ¥2,847,560,TOP 产品:A001(¥320,000)、B023(¥287,500)、C114(¥251,800)"

消费流式响应的编排器

# orchestrator_streaming.py
import httpx
import asyncio
import json

async def stream_task(agent_url: str, task_id: str, message: str):
    """向流式 Agent 发送任务,实时打印进度"""
    
    async with httpx.AsyncClient(timeout=60.0) as client:
        async with client.stream(
            "POST",
            f"{agent_url}/tasks/sendSubscribe",
            json={
                "id": task_id,
                "message": {
                    "role": "user",
                    "parts": [{"text": message}]
                }
            },
            headers={"Accept": "text/event-stream"}
        ) as response:
            
            final_result = None
            
            async for line in response.aiter_lines():
                if not line.startswith("data: "):
                    continue
                
                data = json.loads(line[6:])  # 去掉 "data: " 前缀
                state = data.get("status", {}).get("state")
                
                # 打印进度
                for part in data.get("message", {}).get("parts", []):
                    if "text" in part:
                        print(f"[{state}] {part['text']}")
                
                # 收集最终结果
                if data.get("final") or state == "completed":
                    for artifact in data.get("artifacts", []):
                        for part in artifact.get("parts", []):
                            if "text" in part:
                                final_result = part["text"]
                    break
    
    return final_result

Push Notification:主动推送任务结果

SSE 需要客户端保持长连接。但有些场景下客户端无法维持持久连接——比如一个移动端 App 发出请求后切入后台,或者任务需要等几个小时才能完成。

这时需要 Push Notification:任务完成后,Agent 主动回调通知客户端。

发送时附带回调地址

// 发送任务时,附带 pushNotification 配置
{
  "id": "task-long-003",
  "message": {
    "role": "user",
    "parts": [{"text": "对过去一年的销售数据做完整的趋势分析"}]
  },
  "pushNotification": {
    "url": "https://my-app.example.com/webhooks/a2a-callback",
    "token": "eyJhbGc..."  // 用于验证推送来源
  }
}

Agent 端发送回调

# 在 Agent 完成任务后,主动推送结果
async def notify_completion(callback_url: str, token: str, task_id: str, result: str):
    """任务完成后推送通知"""
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    
    payload = {
        "id": task_id,
        "status": {"state": "completed"},
        "artifacts": [
            {"name": "result", "parts": [{"text": result}]}
        ]
    }
    
    async with httpx.AsyncClient() as client:
        try:
            resp = await client.post(callback_url, json=payload, headers=headers)
            if resp.status_code == 200:
                print(f"[回调成功] 任务 {task_id} 结果已推送")
            else:
                print(f"[回调失败] HTTP {resp.status_code}")
        except Exception as e:
            print(f"[回调异常] {e}")
            # 实际生产中应有重试机制

回调接收端

# webhook_receiver.py(你的应用端)
from fastapi import FastAPI, Request, HTTPException
import hmac

app = FastAPI()
WEBHOOK_SECRET = "your-webhook-secret"

@app.post("/webhooks/a2a-callback")
async def receive_task_result(request: Request):
    """接收 Agent 的任务完成回调"""
    
    # 验证 Token(防止伪造回调)
    auth = request.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing token")
    
    token = auth[7:]
    # 验证 token 的合法性(实际应用中从数据库查询对应任务的预设 token)
    if not is_valid_token(token):
        raise HTTPException(status_code=403, detail="Invalid token")
    
    data = await request.json()
    task_id = data["id"]
    state = data["status"]["state"]
    
    if state == "completed":
        # 提取并处理结果
        for artifact in data.get("artifacts", []):
            for part in artifact.get("parts", []):
                if "text" in part:
                    print(f"任务 {task_id} 完成:{part['text'][:100]}...")
                    # 更新数据库、通知用户等后续操作
    
    return {"status": "received"}

def is_valid_token(token: str) -> bool:
    # 实际实现:从数据库查询 token 对应的任务,验证有效性
    return len(token) > 10  # 示例:简单验证

多轮对话任务:Input Required

并非所有任务都能一次完成。当 Agent 在处理中遇到需要用户补充信息的情况,A2A 定义了 input-required 状态:

submitted → working → input-required → working → completed

典型场景

用户 → Agent:"帮我预订明天上午的会议室"
Agent → 用户:"input-required:请问几点钟?需要几人的会议室?"
用户 → Agent:"任务更新(task-004):10:00,6 人"
Agent → 用户:"completed:已预订 601 会议室,10:00-11:00"

实现 input-required 状态

# interactive_agent.py
@app.post("/tasks/send")
async def handle_interactive_task(request: TaskRequest):
    """支持多轮对话的任务处理"""
    
    task_id = request.id
    user_text = extract_text(request.message)
    
    # 检查是否是对话的后续轮次(有 historyLength 字段)
    history_length = request.dict().get("historyLength", 0)
    
    if history_length == 0:
        # 第一轮:分析任务,判断是否需要更多信息
        if "预订" in user_text and ("几点" not in user_text and "时间" not in user_text):
            # 信息不完整,请求补充
            return {
                "id": task_id,
                "status": {"state": "input-required"},
                "message": {
                    "role": "agent",
                    "parts": [{"text": "请提供会议时间(几点开始,持续多久)和参会人数,以便我为您预订合适的会议室。"}]
                }
            }
    
    # 信息完整,执行预订
    result = await book_meeting_room(user_text)
    
    return {
        "id": task_id,
        "status": {"state": "completed"},
        "artifacts": [{"name": "booking", "parts": [{"text": result}]}]
    }

编排器处理多轮对话

# orchestrator 中处理 input-required
async def execute_task_with_followup(agent_url: str, initial_message: str):
    """处理可能需要多轮交互的任务"""
    
    task_id = str(uuid.uuid4())
    current_message = initial_message
    turn = 0
    
    while True:
        turn += 1
        print(f"\n[第 {turn} 轮]")
        
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{agent_url}/tasks/send",
                json={
                    "id": task_id,
                    "message": {
                        "role": "user",
                        "parts": [{"text": current_message}]
                    },
                    "historyLength": turn - 1  # 告知 Agent 这是第几轮
                }
            )
            result = response.json()
        
        state = result["status"]["state"]
        
        if state == "completed":
            # 任务完成,提取结果
            for artifact in result.get("artifacts", []):
                for part in artifact.get("parts", []):
                    if "text" in part:
                        print(f"[完成] {part['text']}")
                        return part["text"]
        
        elif state == "input-required":
            # Agent 需要更多信息
            agent_question = ""
            for part in result.get("message", {}).get("parts", []):
                if "text" in part:
                    agent_question = part["text"]
            
            print(f"[Agent 问] {agent_question}")
            
            # 在真实系统中,这里应该等待用户输入
            # 在自动化场景中,可以由另一个 Agent 回答
            current_message = input("你的回答: ")
        
        elif state == "failed":
            print(f"[失败] 任务执行失败")
            return None
        
        if turn > 10:  # 防止无限循环
            break
    
    return None

安全认证:生产环境的 A2A

上篇的示例代码没有任何认证——任何人都能调用你的 Agent。生产环境绝对不能这样。

A2A 协议的认证方案写在 Agent Card 的 authentication 字段中:

{
  "name": "SecureDataAgent",
  "url": "https://agent.example.com",
  "version": "1.0.0",
  "capabilities": {"streaming": true},
  "authentication": {
    "schemes": ["Bearer"],
    "credentials": "https://auth.example.com/agent-credentials"
  },
  "skills": [...]
}

方案一:Bearer Token(最常用)

# secure_agent.py
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt

app = FastAPI()
security = HTTPBearer()

SECRET_KEY = "your-secret-key"
VALID_AGENT_IDS = {"orchestrator-001", "orchestrator-002"}  # 授权的调用方

def verify_agent_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    """验证调用方的 JWT Token"""
    token = credentials.credentials
    
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        agent_id = payload.get("agent_id")
        
        if agent_id not in VALID_AGENT_IDS:
            raise HTTPException(status_code=403, detail=f"Agent {agent_id} 无访问权限")
        
        return payload
    
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token 已过期")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Token 无效")

@app.get("/.well-known/agent.json")
def get_agent_card():
    """Agent Card 无需认证(公开发现)"""
    return AGENT_CARD

@app.post("/tasks/send")
async def handle_task(
    request: TaskRequest,
    agent_info: dict = Depends(verify_agent_token)  # 注入认证验证
):
    """有认证保护的任务处理接口"""
    caller_id = agent_info["agent_id"]
    print(f"[认证通过] 调用方:{caller_id}")
    
    # ... 正常处理任务

生成调用方 Token

# token_generator.py(编排器一侧)
import jwt
import time

def generate_agent_token(agent_id: str, secret_key: str, expires_in: int = 3600) -> str:
    """生成 Agent 身份 Token"""
    payload = {
        "agent_id": agent_id,
        "iat": int(time.time()),
        "exp": int(time.time()) + expires_in
    }
    return jwt.encode(payload, secret_key, algorithm="HS256")

# 编排器调用前先生成 Token
token = generate_agent_token("orchestrator-001", SECRET_KEY)

async with httpx.AsyncClient() as client:
    response = await client.post(
        f"{agent_url}/tasks/send",
        json=task_payload,
        headers={"Authorization": f"Bearer {token}"}
    )

方案二:mTLS(双向 TLS,适合内网高安全场景)

# 编排器使用客户端证书
async with httpx.AsyncClient(
    cert=("orchestrator.crt", "orchestrator.key"),
    verify="ca.crt"  # 服务端 CA 证书
) as client:
    response = await client.post(f"{agent_url}/tasks/send", json=task_payload)

完整的生产级 A2A Agent 模板

把上面的所有内容整合起来,一个可以直接用于生产的 A2A Agent 长这样:

# production_agent.py
from fastapi import FastAPI, Depends, HTTPException
from fastapi.responses import StreamingResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from typing import Optional, List
import asyncio
import json
import jwt
import time
import uuid

app = FastAPI(title="ProductionAgent", version="1.0.0")
security = HTTPBearer()

# ─── 配置 ───────────────────────────────────────────────
SECRET_KEY = "change-me-in-production"
ALLOWED_CALLERS = {"main-orchestrator", "supervisor-agent"}

AGENT_CARD = {
    "name": "ProductionAgent",
    "description": "生产级 A2A Agent 示例",
    "url": "https://your-domain.com",
    "version": "1.0.0",
    "capabilities": {
        "streaming": True,
        "pushNotifications": True,
        "stateTransitionHistory": True
    },
    "authentication": {
        "schemes": ["Bearer"]
    },
    "skills": [
        {
            "id": "process-request",
            "name": "请求处理",
            "description": "处理各类业务请求",
            "inputModes": ["text"],
            "outputModes": ["text"]
        }
    ]
}

# ─── 数据模型 ─────────────────────────────────────────────
class MessagePart(BaseModel):
    text: Optional[str] = None
    file: Optional[dict] = None

class Message(BaseModel):
    role: str
    parts: List[MessagePart]

class PushNotification(BaseModel):
    url: str
    token: str

class TaskRequest(BaseModel):
    id: str
    message: Message
    pushNotification: Optional[PushNotification] = None
    historyLength: Optional[int] = 0

# ─── 认证 ─────────────────────────────────────────────────
def get_caller(credentials: HTTPAuthorizationCredentials = Depends(security)):
    try:
        payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=["HS256"])
        agent_id = payload.get("agent_id", "")
        if agent_id not in ALLOWED_CALLERS:
            raise HTTPException(403, f"Agent '{agent_id}' not authorized")
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(401, "Invalid token")

# ─── 路由 ─────────────────────────────────────────────────
@app.get("/.well-known/agent.json")
def agent_card():
    return AGENT_CARD

@app.post("/tasks/send")
async def task_sync(req: TaskRequest, caller=Depends(get_caller)):
    result = await process_business_logic(req.message)
    return {
        "id": req.id,
        "status": {"state": "completed"},
        "artifacts": [{"name": "result", "parts": [{"text": result}]}]
    }

@app.post("/tasks/sendSubscribe")
async def task_stream(req: TaskRequest, caller=Depends(get_caller)):
    async def generate():
        steps = ["接收任务...", "分析请求...", "执行处理...", "生成结果..."]
        for step in steps:
            event = {
                "id": req.id,
                "status": {"state": "working"},
                "message": {"role": "agent", "parts": [{"text": step}]}
            }
            yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
            await asyncio.sleep(0.3)
        
        result = await process_business_logic(req.message)
        final = {
            "id": req.id,
            "status": {"state": "completed"},
            "final": True,
            "artifacts": [{"name": "result", "parts": [{"text": result}]}]
        }
        yield f"data: {json.dumps(final, ensure_ascii=False)}\n\n"
    
    return StreamingResponse(generate(), media_type="text/event-stream")

async def process_business_logic(message: Message) -> str:
    """实际业务逻辑(替换成你的实现)"""
    user_input = " ".join(p.text for p in message.parts if p.text)
    await asyncio.sleep(0.1)  # 模拟处理耗时
    return f"已处理:{user_input}"

A2A 与 MCP 的协同工作

学完 A2A 之后,回头看 MCP,两者分工非常清晰:

用户请求
   │
   ▼
主 Agent(Orchestrator)
   │ ← 通过 MCP 调用本地工具(文件系统、数据库、代码执行)
   │
   ├─── A2A ──→ 数据分析 Agent(内部调用 MCP 的 Python 执行工具)
   ├─── A2A ──→ 搜索 Agent(内部调用 MCP 的 Web 搜索工具)
   └─── A2A ──→ 写作 Agent(内部调用 MCP 的文档工具)

一句话总结

  • MCP:解决"Agent ↔ 工具"的问题,让 Agent 能用工具
  • A2A:解决"Agent ↔ Agent"的问题,让 Agent 能组团

两者合在一起,才是"AI 团队协作"的完整技术栈。


最后:A2A 生态现状(2025 年)

A2A 协议由 Google 于 2025 年 4 月开源,目前已有多家厂商跟进支持:

厂商支持情况
Google原生支持,Vertex AI Agent Builder 已集成
LangChainlangchain-a2a 包,支持 LangGraph Agent 暴露为 A2A 接口
CrewAI实验性支持,Crew 可作为 A2A Agent 对外暴露
AutoGenMicrosoft 正在推进兼容性
OpenAI尚未官方支持,社区有第三方实现

协议本身仍在快速迭代,核心概念(Agent Card、Task 生命周期、SSE 流式)已趋于稳定,外围特性(认证、多模态 Artifact、任务中断恢复)还在完善中。

现在入场正是好时机:概念不复杂、开源实现已有、生态正在成型。


总结

上下两篇,我们完整走过了 A2A 协议从入门到实战的全部内容:

内容关键点
Agent Card/.well-known/agent.json,对外声明能力
Task 生命周期submitted → working → completed / failed / input-required
同步请求POST /tasks/send,适合快速任务
流式响应POST /tasks/sendSubscribe + SSE,适合长任务
Push Notification任务完成后主动回调,适合异步场景
多轮对话input-required 状态 + historyLength 字段
安全认证Bearer JWT 或 mTLS
与 MCP 协同MCP 管工具,A2A 管 Agent 协作

代码都是可以直接运行的,建议把上下两篇的示例跑通,亲手感受一下 Agent 之间互相"打招呼、派任务、收结果"的过程。