在上一篇文章中,我们从零开始搭建了一个A2A兼容的智能体,完成了环境配置、Agent Card创建、技能定义和执行器实现等核心步骤。现在,我们的智能体已经运行起来了,但如何与它进行有效交互呢?本文将带你深入探索如何与A2A智能体进行通信,包括基本请求、流式处理、多轮对话等高级功能。
与A2A智能体通信的基础知识
在开始实际交互前,让我们先了解A2A协议中的几个核心通信概念:
1. JSON-RPC 2.0
A2A协议使用JSON-RPC 2.0作为通信格式。所有请求都需要遵循以下基本结构:
{
"jsonrpc": "2.0",
"method": "方法名",
"params": {
// 方法参数
},
"id": "请求ID"
}
2. 主要端点和方法
A2A服务器通常提供以下核心端点:
- Agent Card:
/.well-known/agent.json- 获取智能体的"名片" - RPC端点:
/- 用于发送所有JSON-RPC请求,包括:tasks/send: 发送同步任务请求tasks/sendSubscribe: 发送支持流式处理的任务请求tasks/get: 获取任务状态tasks/cancel: 取消正在执行的任务
3. 会话和任务
- 会话(Session): 代表一系列相关对话的上下文
- 任务(Task): 单次请求/响应交互,是A2A协议的基本工作单元
工具准备:创建A2A客户端
为了方便测试,让我们创建一个简单的Python客户端脚本,用于与我们的智能体服务器通信。
创建文件a2a_client.py:
import json
import requests
import uuid
import sseclient # 用于处理Server-Sent Events
import asyncio
import aiohttp
class A2AClient:
"""简单的A2A客户端"""
def __init__(self, base_url="http://localhost:8000"):
"""初始化客户端"""
self.base_url = base_url
self.session_id = f"session_{uuid.uuid4().hex[:8]}"
def get_agent_card(self):
"""获取智能体的Agent Card"""
response = requests.get(f"{self.base_url}/.well-known/agent.json")
response.raise_for_status()
return response.json()
def send_text_message(self, text, task_id=None):
"""发送文本消息,执行同步任务"""
if not task_id:
task_id = f"task_{uuid.uuid4().hex[:8]}"
payload = {
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {
"id": task_id,
"sessionId": self.session_id,
"message": {
"role": "user",
"parts": [{"type": "text", "text": text}]
}
},
"id": 1
}
response = requests.post(
self.base_url,
json=payload,
headers={"Content-Type": "application/json"}
)
response.raise_for_status()
return response.json()
def send_data_message(self, data, task_id=None):
"""发送结构化数据,执行同步任务"""
if not task_id:
task_id = f"task_{uuid.uuid4().hex[:8]}"
payload = {
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {
"id": task_id,
"sessionId": self.session_id,
"message": {
"role": "user",
"parts": [{"type": "data", "data": data}]
}
},
"id": 1
}
response = requests.post(
self.base_url,
json=payload,
headers={"Content-Type": "application/json"}
)
response.raise_for_status()
return response.json()
def get_task_status(self, task_id):
"""获取任务状态"""
payload = {
"jsonrpc": "2.0",
"method": "tasks/get",
"params": {
"id": task_id
},
"id": 1
}
response = requests.post(
self.base_url,
json=payload,
headers={"Content-Type": "application/json"}
)
response.raise_for_status()
return response.json()
async def stream_task(self, text):
"""使用流式处理发送任务并接收响应"""
task_id = f"task_{uuid.uuid4().hex[:8]}"
payload = {
"jsonrpc": "2.0",
"method": "tasks/sendSubscribe",
"params": {
"id": task_id,
"sessionId": self.session_id,
"message": {
"role": "user",
"parts": [{"type": "text", "text": text}]
}
},
"id": 1
}
async with aiohttp.ClientSession() as session:
async with session.post(
self.base_url,
json=payload,
headers={
"Content-Type": "application/json",
"Accept": "text/event-stream"
}
) as response:
if response.status != 200:
error_text = await response.text()
raise Exception(f"Error {response.status}: {error_text}")
# 处理SSE流
async for line in response.content:
line = line.decode('utf-8').strip()
if line.startswith('data: '):
event_data = line[6:] # 去掉'data: '前缀
if event_data == '[DONE]':
break
try:
event = json.loads(event_data)
yield event
except json.JSONDecodeError:
print(f"无法解析JSON: {event_data}")
# 使用示例
if __name__ == "__main__":
import sys
# 创建客户端
client = A2AClient()
# 获取并显示Agent Card
try:
card = client.get_agent_card()
print("=== Agent Card ===")
print(f"名称: {card['name']}")
print(f"描述: {card['description']}")
print(f"版本: {card['version']}")
print(f"技能: {len(card['skills'])}个")
for skill in card['skills']:
print(f" - {skill['name']}: {skill['description']}")
print("\n")
except Exception as e:
print(f"获取Agent Card失败: {e}")
sys.exit(1)
# 基本交互示例
while True:
command = input("请选择操作 (1=文本消息, 2=结构化消息, 3=流式处理, q=退出): ")
if command.lower() == 'q':
break
if command == '1':
# 发送文本消息
text = input("请输入消息: ")
print("发送中...")
try:
response = client.send_text_message(text)
result = response.get("result", {}).get("task", {})
print("\n=== 回复 ===")
# 处理响应消息
if "message" in result and "parts" in result["message"]:
for part in result["message"]["parts"]:
if part["type"] == "text":
print(part["text"])
elif part["type"] == "data":
print(json.dumps(part["data"], indent=2, ensure_ascii=False))
print("\n")
except Exception as e:
print(f"发送消息失败: {e}")
elif command == '2':
# 发送结构化消息
print("发送JSON格式的结构化消息:")
print("示例: {\"skill\": \"calculate\", \"params\": {\"operation\": \"add\", \"a\": 5, \"b\": 3}}")
data_str = input("请输入JSON数据: ")
try:
data = json.loads(data_str)
print("发送中...")
response = client.send_data_message(data)
result = response.get("result", {}).get("task", {})
print("\n=== 回复 ===")
# 处理响应消息
if "message" in result and "parts" in result["message"]:
for part in result["message"]["parts"]:
if part["type"] == "text":
print(part["text"])
elif part["type"] == "data":
print(json.dumps(part["data"], indent=2, ensure_ascii=False))
print("\n")
except json.JSONDecodeError:
print("JSON格式无效")
except Exception as e:
print(f"发送消息失败: {e}")
elif command == '3':
# 使用流式处理
text = input("请输入消息 (流式处理): ")
print("发送中...\n")
async def run_stream():
try:
print("=== 流式回复 ===")
complete_text = ""
async for event in client.stream_task(text):
# 处理不同类型的事件
if "method" in event:
if event["method"] == "tasks/artifact/update":
parts = event["params"]["artifacts"][0]["parts"]
for part in parts:
if part["type"] == "text":
chunk = part["text"]
print(chunk, end="", flush=True)
complete_text += chunk
elif event["method"] == "tasks/status/update":
status = event["params"]["status"]
if status in ["completed", "failed"]:
print(f"\n[任务状态: {status}]")
print("\n\n完整响应:", complete_text)
print("\n")
except Exception as e:
print(f"流式处理失败: {e}")
# 运行异步函数
asyncio.run(run_stream())
这个客户端脚本提供了以下功能:
- 获取Agent Card
- 发送基本文本消息
- 发送结构化JSON数据
- 使用流式处理发送任务并实时接收响应
基础交互:发送简单请求
让我们首先尝试最基本的交互方式——发送文本消息并获取响应。
1. 获取Agent Card
与A2A智能体交互的第一步是获取其Agent Card,了解它的能力和技能:
# 使用curl命令行工具
curl http://localhost:8000/.well-known/agent.json | jq
或者运行我们的客户端脚本:
# 启动客户端
python a2a_client.py
你将看到智能体的基本信息和支持的技能列表。
2. 发送文本消息
通过客户端,选择选项1,然后输入一条文本消息,例如:
问候张三
智能体应该会返回一个问候语:
Hello, 张三! Welcome to A2A!
或者你可以尝试:
用中文问候李四
智能体会返回中文问候:
你好,李四!欢迎使用A2A!
3. 发送结构化数据
选择选项2,然后输入JSON格式的结构化数据:
{"skill": "calculate", "params": {"operation": "add", "a": 5, "b": 3}}
智能体会执行加法运算并返回结果:
{
"operation": "add",
"a": 5,
"b": 3,
"result": 8,
"success": true
}
你还可以尝试其他运算:
{"skill": "calculate", "params": {"operation": "multiply", "a": 6, "b": 7}}
高级交互:流式处理
现代AI应用通常提供流式响应,以提升用户体验。A2A协议通过Server-Sent Events (SSE)支持流式处理。
选择客户端的选项3,然后输入一条消息:
计算 12345 乘以 6789
注意观察响应是如何逐步显示的,而不是等待整个计算完成才一次性返回。
在幕后,这是通过tasks/sendSubscribe方法实现的,它建立了一个持久连接,服务器可以通过这个连接持续发送事件。
多轮对话:维持会话上下文
A2A协议支持通过会话ID维持对话上下文。在我们的客户端中,所有请求都使用同一个会话ID,这意味着智能体可以"记住"之前的交互。
尝试以下多轮对话:
- 第一轮:
用英语问候王五(智能体返回英文问候) - 第二轮:
现在用法语(智能体应该理解你是要用法语问候同一个人)
要实现真正的上下文感知,你需要在智能体执行器中添加会话状态管理。例如,修改HelloWorldAgentExecutor类:
class HelloWorldAgentExecutor:
def __init__(self):
self.skills = {...}
self.sessions = {} # 存储会话状态
async def execute_task(self, task: Task) -> Task:
# 获取或创建会话状态
session_id = task.session_id
if session_id not in self.sessions:
self.sessions[session_id] = {
"history": [],
"context": {}
}
# 更新会话历史
if task.message:
self.sessions[session_id]["history"].append(task.message)
# 从历史和上下文中提取信息...
# 处理任务...
# 保存新的上下文信息
if result_message:
self.sessions[session_id]["history"].append(result_message)
# 清理过期会话
self._cleanup_old_sessions()
return task
错误处理与恢复
在实际应用中,错误处理是不可避免的。A2A协议提供了标准的错误响应格式:
{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "错误描述"
},
"id": 1
}
常见错误码包括:
-32700: 解析错误-32600: 无效请求-32601: 方法不存在-32602: 无效参数-32603: 内部错误
为了增强智能体的鲁棒性,我们应该修改执行器以优雅地处理错误:
async def execute_task(self, task: Task) -> Task:
try:
# 正常处理逻辑...
except ValueError as e:
task.status = TaskStatus.FAILED
task.message = Message(
role="agent",
parts=[TextPart(text=f"参数错误: {str(e)}")]
)
except Exception as e:
# 记录详细错误信息
logger.error(f"执行任务 {task.id} 时发生错误: {str(e)}", exc_info=True)
task.status = TaskStatus.FAILED
task.message = Message(
role="agent",
parts=[TextPart(text="处理请求时发生内部错误")]
)
return task
安全性考虑
在生产环境中部署A2A智能体时,安全性至关重要:
1. 添加认证
修改Agent Card以要求认证:
authentication = AgentAuthentication(
schemes=["Bearer"] # 使用Bearer令牌认证
)
并在服务器中实现认证检查:
async def authenticate(request):
"""验证请求认证"""
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(401, "未提供有效的认证令牌")
token = auth_header[7:] # 去掉"Bearer "前缀
# 验证令牌...
return True
2. 输入验证和清理
始终验证并清理用户输入,以防止注入攻击:
def validate_input(message):
"""验证并清理用户输入"""
if not message or not message.parts:
raise ValueError("消息不能为空")
for part in message.parts:
if part.type == "text":
# 限制文本长度
if len(part.text) > 10000:
raise ValueError("文本消息过长")
# 清理潜在的有害内容
part.text = clean_text(part.text)
elif part.type == "data":
# 验证数据结构
if "skill" in part.data and not isinstance(part.data["skill"], str):
raise ValueError("无效的技能名称")
return message
3. 限制请求频率
实现速率限制,防止滥用:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
import time
class RateLimitMiddleware(BaseHTTPMiddleware):
def __init__(self, app, max_requests=60, window_seconds=60):
super().__init__(app)
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = {} # IP -> [时间戳列表]
async def dispatch(self, request, call_next):
client_ip = request.client.host
now = time.time()
# 获取或创建该IP的请求历史
if client_ip not in self.requests:
self.requests[client_ip] = []
# 清理旧记录
self.requests[client_ip] = [t for t in self.requests[client_ip]
if now - t < self.window_seconds]
# 检查是否超过限制
if len(self.requests[client_ip]) >= self.max_requests:
return JSONResponse(
status_code=429,
content={"error": "请求过于频繁,请稍后再试"}
)
# 记录本次请求
self.requests[client_ip].append(now)
# 继续处理请求
return await call_next(request)
部署到生产环境
当你的A2A智能体准备好走向生产环境时,需要考虑以下几点:
1. 使用HTTPS
在生产环境中,始终使用HTTPS保护通信:
# 使用uvicorn启动带SSL的服务器
uvicorn.run(
app,
host="0.0.0.0",
port=443,
ssl_keyfile="key.pem",
ssl_certfile="cert.pem"
)
2. 使用环境变量管理配置
将配置分离到环境变量:
import os
from dotenv import load_dotenv
# 加载.env文件
load_dotenv()
# 配置参数
HOST = os.getenv("A2A_HOST", "0.0.0.0")
PORT = int(os.getenv("A2A_PORT", "8000"))
LOG_LEVEL = os.getenv("A2A_LOG_LEVEL", "info")
ENABLE_STREAMING = os.getenv("A2A_ENABLE_STREAMING", "true").lower() == "true"
3. 使用容器化部署
创建Dockerfile:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["python", "main.py"]
构建并运行容器:
docker build -t my-a2a-agent .
docker run -p 8000:8000 my-a2a-agent
4. 监控与日志
添加适当的日志记录:
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("app.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("a2a-agent")
# 在代码中使用
logger.info(f"启动服务器: {HOST}:{PORT}")
logger.error("发生错误", exc_info=True)
结语
恭喜!通过这两篇实战文章,你已经掌握了构建和使用A2A智能体的全套技能,从环境配置、组件开发到与智能体交互、安全部署。虽然我们的示例相对简单,但这些原则和模式可以扩展到更复杂的智能体系统中。
随着A2A协议的不断发展,我们可以期待更多智能体之间的协作场景。想象一下,你开发的智能体可以无缝地与其他开发者创建的专业智能体通信,共同完成复杂任务,这将为AI应用开辟一个全新的可能性空间。
是时候将你的创意付诸实践了,开始构建你专属的A2A智能体吧!
系列回顾:
- 《告别AI孤岛!Google A2A协议为你揭秘智能体协作新纪元》
- 《解密A2A核心:智能体如何通过"名片"和"技能"实现对话?》
- 《A2A与MCP:Google如何打造智能体协作的"双引擎"?》
- 《上手A2A实战(上):从零搭建你的第一个A2A智能体》
- 《上手A2A实战(下):启动服务并与你的智能体"聊聊天"》