架构
我们将使用与 [旅行规划器]
相同的架构,并在此基础上扩展 A2A + MCP 协议。
下面的演示只是为了说明 A2A 协议在多个代理之间的通信,仅用于说明目的。
上面的架构使用了模块化的多代理 AI 系统,其中每个代理都是独立可部署的,并且通过谷歌的A2A(Agent-to-Agent)协议进行通信。
核心组件
- 用户界面层 —— 向前端服务器发送 HTTP 请求
- 代理层 —— 协调宿主代理、代理 1 和代理 2 之间的交互
- 协议层 —— 代理之间通过 A2A 协议进行通信
- 外部数据层 —— 使用 MCP 访问外部 API
代理角色:
- 行程规划代理 —— 作为中央协调者 —— 宿主代理,协调用户与专业代理之间的交互。
- 航班搜索代理 —— 一个专门负责根据用户输入获取航班选项的代理
- 酒店搜索代理 —— 一个专门负责根据用户偏好获取酒店住宿的代理
MCP 在本项目中的实现:
航班搜索 MCP 服务器
- 连接:航班搜索(代理 1)连接到 MCP 航班服务器
- 功能:连接到航班预订 API 和数据库
酒店搜索 MCP 服务器
- 连接:酒店搜索(代理 2)连接到 MCP 酒店服务器
- 功能:连接到酒店预订系统和聚合器
代理通信流程
以下是使用 Mermaid 语法绘制的流程图,描述了用户通过 Streamlit UI 提交旅行查询并生成完整行程的过程:
流程图说明:
- 用户通过 Streamlit UI 提交旅行查询:用户在前端界面输入旅行需求。
- 旅行规划器解析查询以提取关键信息:旅行规划器解析用户输入,提取出发地、目的地、日期等关键信息。
- 旅行规划器向航班搜索代理请求航班信息:旅行规划器将关键信息发送给航班搜索代理。
- 航班搜索代理通过调用 MCP 服务器返回可用航班:航班搜索代理查询航班信息并返回结果。
- 旅行规划器提取目的地详细信息:旅行规划器从航班信息中提取目的地相关细节。
- 旅行规划器向酒店搜索代理请求酒店信息:旅行规划器将目的地信息发送给酒店搜索代理。
- 酒店搜索代理返回住宿选项:酒店搜索代理查询酒店信息并返回结果。
- 旅行规划器将所有数据综合成一个完整的行程:旅行规划器整合航班和酒店信息,生成完整的行程。
这个流程图清晰地展示了从用户输入到生成完整行程的整个过程。
实现
让我们深入了解一下如何使用 ADK + MCP + Gemini AI 构建这个多代理系统,我们将把它分解为关键的实现步骤。
准备工作
- 安装 Python 3.11+
2. 获取谷歌 Gemini 生成式 AI 的 API 密钥
3. 获取有效的 SerpAPI 密钥
4. 获取有效的 OpenAI GPT 密钥
项目文件结构
├── common
│ ├── __init__.py
│ ├── client
│ │ ├── __init__.py
│ │ ├── card_resolver.py
│ │ └── client.py
│ ├── server
│ │ ├── __init__.py
│ │ ├── server.py
│ │ ├── task_manager.py
│ │ └── utils.py
│ ├── types.py
│ └── utils
│ ├── in_memory_cache.py
│ └── push_notification_auth.py
├── flight_search_app
│ ├── a2a_agent_card.json
│ ├── agent.py
│ ├── main.py
│ ├── static
│ │ └── .well-known
│ │ └── agent.json
│ └── streamlit_ui.py
├── hotel_search_app
│ ├── README.md
│ ├── a2a_agent_card.json
│ ├── langchain_agent.py
│ ├── langchain_server.py
│ ├── langchain_streamlit.py
│ ├── static
│ │ └── .well-known
│ │ └── agent.json
│ └── streamlit_ui.py
└── itinerary_planner
├── __init__.py
├── a2a
│ ├── __init__.py
│ └── a2a_client.py
├── a2a_agent_card.json
├── event_log.py
├── itinerary_agent.py
├── itinerary_server.py
├── run_all.py
├── static
│ └── .well-known
│ └── agent.json
└── streamlit_ui.py
第一步:设置虚拟环境
安装依赖项
# 设置虚拟环境
python -m venv .venv # 激活虚拟环境
source .venv/bin/activate# 安装依赖项
pip install fastapi uvicorn streamlit httpx python-dotenv pydantic
pip install google-generativeai google-adk langchain langchain-openai
第二步:安装 MCP 服务器包
mcp 酒店服务器 — https://pypi.org/project/mcp-hotel-search/
mcp 航班服务器 — https://pypi.org/project/mcp-flight-search/
# 安装 mcp 酒店搜索
pip install mcp-hotel-search# 安装 mcp 航班搜索
pip install mcp-flight-search
第三步:设置 Gemini、OpenAI、SerpAI 的环境变量
设置上面准备工作中提到的环境变量
GOOGLE_API_KEY=your_google_api_key
OPENAI_API_KEY=your_openai_api_key
SERP_API_KEY=your_serp_api_key
第四步:使用 ADK 设置航班搜索(代理)作为 MCP 客户端,使用 Gemini 2.0 Flash
使用https://github.com/google/A2A/tree/main/samples/python/common 中的可复用模块。
├── common/ # 共享 A2A 协议组件
│ ├── __init__.py
│ ├── client/ # 客户端实现
│ │ ├── __init__.py
│ │ └── client.py # 基础 A2A 客户端
│ ├── server/ # 服务器实现
│ │ ├── __init__.py
│ │ ├── server.py # A2A 服务器实现
│ │ └── task_manager.py # 任务管理工具
│ └── types.py # A2A 共享类型定义
├── flight_search_app/ # 航班搜索代理(代理 1)
│ ├── __init__.py
│ ├── a2a_agent_card.json # 代理能力声明
│ ├── agent.py # ADK 代理实现
│ ├── main.py # ADK 服务器入口点,使用 Gemini LLM
│ └── static/ # 静态文件
│ └── .well-known/ # 代理发现目录
│ └── agent.json # 标准化代理发现文件
4.1 使用 ADK 代理实现作为 MCP 客户端从 MCP 服务器获取工具
from google.adk.agents.llm_agent import LlmAgent
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters..
..
# 从 MCP 服务器获取工具
server_params = StdioServerParameters(
command="mcp-flight-search",
args=["--connection_type", "stdio"],
env={"SERP_API_KEY": serp_api_key},) tools, exit_stack = await MCPToolset.from_server(
connection_params=server_params)
..
..
4.2 使用通用 A2A 服务器组件和类型以及谷歌 ADK 运行器、会话和代理定义 ADK 服务器入口点
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.agents import Agent
from .agent import get_agent_async# 导入通用 A2A 服务器组件和类型
from common.server.server import A2AServer
from common.server.task_manager import InMemoryTaskManager
from common.types import (
AgentCard,
SendTaskRequest,
SendTaskResponse,
Task,
TaskStatus,
Message,
TextPart,
TaskState,
)# --- 自定义航班搜索任务管理器 ---
class FlightAgentTaskManager(InMemoryTaskManager):
"""特定于 ADK 航班搜索代理的任务管理器。"""
def __init__(self, agent: Agent, runner: Runner, session_service: InMemorySessionService):
super().__init__()
self.agent = agent
self.runner = runner
self.session_service = session_service
logger.info("FlightAgentTaskManager 初始化完成。")...
...
4.3 使用代理卡片创建 A2A 服务器实例
# --- 主执行块 ---
async def run_server():
"""初始化服务并启动航班搜索 A2A 服务器。"""
logger.info("开始航班搜索 A2A 服务器初始化...") session_service = None
exit_stack = None
try:
session_service = InMemorySessionService()
agent, exit_stack = await get_agent_async()
runner = Runner(
app_name='flight_search_a2a_app',
agent=agent,
session_service=session_service,
) # 创建特定的任务管理器
task_manager = FlightAgentTaskManager(
agent=agent,
runner=runner,
session_service=session_service
) # 定义代理卡片
port = int(os.getenv("PORT", "8000"))
host = os.getenv("HOST", "localhost")
listen_host = "0.0.0.0" agent_card = AgentCard(
name="Flight Search Agent (A2A)",
description="根据用户查询提供航班信息。",
url=f"http://{host}:{port}/",
version="1.0.0",
defaultInputModes=["text"],
defaultOutputModes=["text"],
capabilities={"streaming": False},
skills=[
{
"id": "search_flights",
"name": "Search Flights",
"description": "根据出发地、目的地和日期搜索航班。",
"tags": ["flights", "travel"],
"examples": ["Find flights from JFK to LAX tomorrow"]
}
]
) # 创建 A2AServer 实例
a2a_server = A2AServer(
agent_card=agent_card,
task_manager=task_manager,
host=listen_host,
port=port
) # 配置 Uvicorn
config = uvicorn.Config(
app=a2a_server.app, # 传递 A2AServer 的 Starlette 应用
host=listen_host,
port=port,
log_level="info"
)
server = uvicorn.Server(config)
...
...
4.4 让我们启动航班搜索应用
第五步:使用 LangChain 配置酒店搜索代理作为 MCP 客户端,并使用 OpenAI(GPT-4o)作为 LLM
├── hotel_search_app/ # 酒店搜索代理(代理 2)
│ ├── __init__.py
│ ├── a2a_agent_card.json # 代理能力声明
│ ├── langchain_agent.py # LangChain 代理实现
│ ├── langchain_server.py # 服务器入口点
│ └── static/ # 静态文件
│ └── .well-known/ # 代理发现目录
│ └── agent.json # 标准化代理发现文件
图片由作者提供
5.1. LangChain 代理 实现作为 MCP 客户端,使用 OpenAI LLM 作为语言模型
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_mcp_adapters.client import MultiServerMCPClient# MCP 客户端配置
MCP_CONFIG = {
"hotel_search": {
"command": "mcp-hotel-search",
"args": ["--connection_type", "stdio"],
"transport": "stdio",
"env": {"SERP_API_KEY": os.getenv("SERP_API_KEY")},
}
}class HotelSearchAgent:
"""使用 LangChain MCP 适配器的酒店搜索代理。""" def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o", temperature=0) def _create_prompt(self):
"""创建一个带有自定义系统消息的提示模板。"""
system_message = """你是一个有用的酒店搜索助手。
""" return ChatPromptTemplate.from_messages([
("system", system_message),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
..
..
async def process_query(self, query):
... # 为这个查询创建一个 MCP 客户端实例
async with MultiServerMCPClient(MCP_CONFIG) as client:
# 从这个客户端实例获取工具
tools = client.get_tools() # 创建一个提示
prompt = self._create_prompt() # 使用这些工具创建一个代理
agent = create_openai_functions_agent(
llm=self.llm,
tools=tools,
prompt=prompt
) # 使用这些工具创建一个执行器
executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
handle_parsing_errors=True,
)
5.2 使用通用 A2A 服务器组件和类型创建 A2AServer 实例
# 直接使用底层代理
from hotel_search_app.langchain_agent import get_agent, HotelSearchAgent # 导入通用 A2A 服务器组件和类型
from common.server.server import A2AServer
from common.server.task_manager import InMemoryTaskManager
from common.types import (
AgentCard,
SendTaskRequest,
SendTaskResponse,
Task,
TaskStatus,
Message,
TextPart,
TaskState
)
..
..class HotelAgentTaskManager(InMemoryTaskManager):
"""特定于酒店搜索代理的任务管理器。"""
def __init__(self, agent: HotelSearchAgent):
super().__init__()
self.agent = agent # HotelSearchAgent 实例
logger.info("HotelAgentTaskManager 初始化完成。") async def on_send_task(self, request: SendTaskRequest) -> SendTaskResponse:
"""通过调用代理的 process_query 处理 tasks/send 请求。"""
task_params = request.params
task_id = task_params.id
user_message_text = None logger.info(f"HotelAgentTaskManager 正在处理任务 {task_id}")# --- 主执行块 ---
async def run_server():
"""初始化服务并启动酒店搜索 A2A 服务器。"""
logger.info("开始酒店搜索 A2A 服务器初始化...") agent_instance: Optional[HotelSearchAgent] = None
try:
agent_instance = await get_agent()
if not agent_instance:
raise RuntimeError("初始化 HotelSearchAgent 失败") # 创建特定的任务管理器
task_manager = HotelAgentTaskManager(agent=agent_instance) # 定义代理卡片
port = int(os.getenv("PORT", "8003")) # 默认端口 8003
host = os.getenv("HOST", "localhost")
listen_host = "0.0.0.0" agent_card = AgentCard(
name="Hotel Search Agent (A2A)",
description="根据位置、入住/退房日期和客人数量提供酒店信息。",
url=f"http://{host}:{port}/",
version="1.0.0",
defaultInputModes=["text"],
defaultOutputModes=["text"],
capabilities={"streaming": False},
skills=[
{
"id": "search_hotels",
"name": "Search Hotels",
"description": "根据位置、入住/退房日期和客人数量搜索酒店。",
"tags": ["hotels", "travel", "accommodation"],
"examples": ["Find hotels in London from July 1st to July 5th for 2 adults"]
}
]
) # 创建 A2AServer 实例,不使用 endpoint 参数
a2a_server = A2AServer(
agent_card=agent_card,
task_manager=task_manager,
host=listen_host,
port=port
) config = uvicorn.Config(
app=a2a_server.app, # 传递 A2AServer 的 Starlette 应用
host=listen_host,
port=port,
log_level="info"
)
5.3 让我们启动酒店搜索应用(Langchain)作为入口点以调用 MCP 服务器
第六步:实现宿主代理作为代理之间的协调者,使用 A2A 协议
行程规划器是上述旅行规划的核心组件,使用 A2A 协议与航班和酒店服务进行通信。
├── itinerary_planner/ # 行程规划宿主代理(代理 3)
│ ├── __init__.py
│ ├── a2a/ # A2A 客户端实现
│ │ ├── __init__.py
│ │ └── a2a_client.py # 航班和酒店代理的客户端
│ ├── a2a_agent_card.json # 代理能力声明
│ ├── event_log.py # 事件日志工具
│ ├── itinerary_agent.py # 主规划器实现
│ ├── itinerary_server.py # FastAPI 服务器
│ ├── run_all.py # 运行所有组件的脚本
│ ├── static/ # 静态文件
│ │ └── .well-known/ # 代理发现目录
│ │ └── agent.json # 标准化代理发现文件
│ └── streamlit_ui.py # 主用户界面
6.1 使用航班和酒店 API URL 实现 A2A 协议
- 包含与服务通信的客户端代码
- 实现 Agent-to-Agent 协议
- 包含调用航班和酒店搜索服务的模块
# A2A 兼容代理 API 的根端点
FLIGHT_SEARCH_API_URL = os.getenv("FLIGHT_SEARCH_API_URL", "http://localhost:8000")
HOTEL_SEARCH_API_URL = os.getenv("HOTEL_SEARCH_API_URL", "http://localhost:8003")class A2AClientBase:
"""通过根端点与 A2A 兼容代理通信的基础客户端。""" async def send_a2a_task(self, user_message: str, task_id: Optional[str] = None, agent_type: str = "generic") -> Dict[str, Any]:
...
....
# 构造带有 A2A 方法和修正后的参数结构的 JSON-RPC 负载
payload = {
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {
"id": task_id,
"taskId": task_id,
"message": {
"role": "user",
"parts": [
{"type": "text", "text": user_message}
]
}
},
"id": task_id
}
6.2 行程规划代理卡片
JSON 元数据文件,描述代理的能力、端点、认证要求和技能。在 A2A 协议中用于服务发现。
{
"name": "Travel Itinerary Planner",
"displayName": "Travel Itinerary Planner",
"description": "一个协调航班和酒店信息以创建综合旅行行程的代理",
"version": "1.0.0",
"contact": "code.aicloudlab@gmail.com",
"endpointUrl": "http://localhost:8005",
"authentication": {
"type": "none"
},
"capabilities": ["streaming"],
"skills": [
{
"name": "createItinerary",
"description": "创建包含航班和住宿的综合旅行行程",
"inputs": [
{
"name": "origin",
"type": "string",
"description": "出发城市或机场代码"
},
{
"name": "destination",
"type": "string",
"description": "目的地城市或地区"
},
{
"name": "departureDate",
"type": "string",
"description": "出发日期,格式为 YYYY-MM-DD"
},
{
"name": "returnDate",
"type": "string",
"description": "返回日期,格式为 YYYY-MM-DD(可选)"
},
{
"name": "travelers",
"type": "integer",
"description": "旅行者人数"
},
{
"name": "preferences",
"type": "object",
"description": "其他偏好,如预算、酒店设施等"
}
],
"outputs": [
{
"name": "itinerary",
"type": "object",
"description": "包含航班、酒店和行程的完整旅行行程"
}
]
}
]
}
6.3 使用谷歌生成式 AI SDK 的行程代理
为了简化演示,这里使用了 GenAI SDK(也可以使用 ADK、CrewAI 或其他框架)
行程代理 是系统的中央宿主代理,它协调与航班和酒店搜索服务的通信,并使用语言模型解析自然语言请求。
import google.generativeai as genai # 直接使用 SDK
..
..
from itinerary_planner.a2a.a2a_client import FlightSearchClient, HotelSearchClient# 配置谷歌生成式 AI SDK
genai.configure(api_key=api_key)class ItineraryPlanner:
"""一个使用谷歌生成式 AI SDK 协调航班和酒店搜索代理以创建行程的规划器。""" def __init__(self):
"""初始化行程规划器。"""
logger.info("使用谷歌生成式 AI SDK 初始化行程规划器")
self.flight_client = FlightSearchClient()
self.hotel_client = HotelSearchClient() # 使用 SDK 创建 Gemini 模型实例
self.model = genai.GenerativeModel(
model_name="gemini-2.0-flash",
)
..
..
6.4 行程服务器 —— FastAPI 服务器,暴露行程规划器的端点,处理传入的 HTTP 请求并将请求路由到行程代理
from fastapi import FastAPI, HTTPException, Requestfrom itinerary_planner.itinerary_agent import ItineraryPlanner@app.post("/v1/tasks/send")
async def send_task(request: TaskRequest):
"""处理 A2A tasks/send 请求。"""
global planner if not planner:
raise HTTPException(status_code=503, detail="规划器未初始化") try:
task_id = request.taskId # 提取用户的消息
user_message = None
for part in request.message.get("parts", []):
if "text" in part:
user_message = part["text"]
break if not user_message:
raise HTTPException(status_code=400, detail="请求中未找到文本消息") # 根据查询生成行程
itinerary = await planner.create_itinerary(user_message) # 创建 A2A 响应
response = {
"task": {
"taskId": task_id,
"state": "completed",
"messages": [
{
"role": "user",
"parts": [{"text": user_message}]
},
{
"role": "agent",
"parts": [{"text": itinerary}]
}
],
"artifacts": []
}
} return response
6.5 Streamlit_ui —— 使用 Streamlit 构建的用户界面,为旅行规划提供表单,并以用户友好的格式显示结果
...
...
# API 端点
API_URL = "http://localhost:8005/v1/tasks/send"def generate_itinerary(query: str):
"""向行程规划器 API 发送查询。"""
try:
task_id = "task-" + datetime.now().strftime("%Y%m%d%H%M%S") payload = {
"taskId": task_id,
"message": {
"role": "user",
"parts": [
{
"text": query
}
]
}
} # 将用户查询和请求记录到事件日志
log_user_query(query)
log_itinerary_request(payload) response = requests.post(
API_URL,
json=payload,
headers={"Content-Type": "application/json"}
)
response.raise_for_status() result = response.json() # 提取代理的响应消息
agent_message = None
for message in result.get("task", {}).get("messages", []):
if message.get("role") == "agent":
for part in message.get("parts", []):
if "text" in part:
agent_message = part["text"]
break
if agent_message:
break
..
..
...
第七步:最终演示
在每个终端中,按照以下方式启动服务器代理,正如下面的演示所看到的那样:
# 启动航班搜索代理 - 1,端口 8000
python -m flight_search_app.main# 启动酒店搜索代理 - 2,端口 8003
python -m hotel_search_app.langchain_server# 启动行程宿主代理 - 端口 8005
python -m itinerary_planner.itinerary_server# 启动前端 UI - 端口 8501
streamlit run itinerary_planner/streamlit_ui.py
航班搜索日志,显示从宿主代理发起的任务 ID
酒店搜索日志,显示从宿主代理发起的任务
行程规划器 —— 宿主代理,显示所有请求/响应
代理事件日志
这个演示实现了 谷歌 A2A 协议 的核心原则,使代理能够以结构化、可互操作的方式进行通信。在上面的演示中实现的组件如下:
- 代理卡片 —— 所有代理都暴露了 .well-known/agent.json 文件以供发现。
- A2A 服务器 —— 每个代理都作为一个 A2A 服务器运行:flight_search_app、hotel_search_app 和 itinerary_planner
- A2A 客户端 —— 行程规划器包含了针对航班和酒店代理的专用 A2A 客户端。
- 任务管理 —— 每个请求/响应都被建模为一个 A2A 任务,状态包括已提交、正在处理和已完成。
- 消息结构 —— 使用标准的 JSON-RPC 格式,包含角色(用户/代理)和部分(主要是 TextPart)。
以下组件在我们的演示中尚未实现,但可以扩展为适用于企业级代理:
- 流式传输(SSE) —— A2A 支持服务器发送事件用于长时间运行的任务,但我们的演示使用的是简单的请求/响应,耗时不到 3-5 秒。
- 推送通知 —— 尚未使用 Webhook 更新机制。
- 复杂部分 —— 只使用了 TextPart。可以添加 DataPart、FilePart 等以支持更丰富的负载。
- 高级发现 —— 实现了基本的 .well-known/agent.json,但尚未实现高级认证、JWKS 或授权流程。
A2A 协议 —— 官方文档
总结
在本文中,我们探索了如何使用可复用的 A2A 组件、ADK、LangChain 和 MCP 构建一个功能齐全的多代理系统,用于旅行规划场景。通过结合这些开源工具和框架,我们的代理能够做到以下几点:
- 使用 A2A 动态发现并调用彼此
- 通过 MCP 以模型友好的方式连接到外部 API
- 利用现代框架,如 ADK 和 LangChain
- 以异步方式通信,具有清晰的任务生命周期和结构化结果
同样的原理可以扩展到更多领域,如零售、客户服务自动化、操作工作流以及 AI 辅助的企业工具。
感谢Arjun Prabhulal的分享https://medium.com/ai-cloud-lab/building-multi-agent-ai-app-with-googles-a2a-agent2agent-protocol-adk-and-mcp-a-deep-a94de2237200