上手A2A实战(下):启动服务并与你的智能体"聊聊天"

329 阅读11分钟

上一篇文章中,我们从零开始搭建了一个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())

这个客户端脚本提供了以下功能:

  1. 获取Agent Card
  2. 发送基本文本消息
  3. 发送结构化JSON数据
  4. 使用流式处理发送任务并实时接收响应

基础交互:发送简单请求

让我们首先尝试最基本的交互方式——发送文本消息并获取响应。

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,这意味着智能体可以"记住"之前的交互。

尝试以下多轮对话:

  1. 第一轮:用英语问候王五 (智能体返回英文问候)
  2. 第二轮:现在用法语 (智能体应该理解你是要用法语问候同一个人)

要实现真正的上下文感知,你需要在智能体执行器中添加会话状态管理。例如,修改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智能体吧!


系列回顾

  1. 《告别AI孤岛!Google A2A协议为你揭秘智能体协作新纪元》
  2. 《解密A2A核心:智能体如何通过"名片"和"技能"实现对话?》
  3. 《A2A与MCP:Google如何打造智能体协作的"双引擎"?》
  4. 《上手A2A实战(上):从零搭建你的第一个A2A智能体》
  5. 《上手A2A实战(下):启动服务并与你的智能体"聊聊天"》