目录
[一、为什么需要 A2A 协议](#一、为什么需要 A2A 协议)
[1.1 现状的痛点](#1.1 现状的痛点)
[1.2 用一个故事理解](#1.2 用一个故事理解)
[1.3 A2A 的设计目标](#1.3 A2A 的设计目标)
[二、A2A 协议核心概念详解](#二、A2A 协议核心概念详解)
[2.1 核心概念一览表](#2.1 核心概念一览表)
[2.2 任务生命周期(Task State Machine)](#2.2 任务生命周期(Task State Machine))
[2.3 通信流程图](#2.3 通信流程图)
[3.1 整体架构](#3.1 整体架构)
[3.2 项目目录结构](#3.2 项目目录结构)
[3.3 技术栈](#3.3 技术栈)
[四、基础实战------模拟版多 Agent 协作](#四、基础实战——模拟版多 Agent 协作)
[4.1 环境准备与依赖安装](#4.1 环境准备与依赖安装)
[4.2 创建"天气专家"Agent](#4.2 创建"天气专家"Agent)
[4.3 创建"票务专家"Agent](#4.3 创建"票务专家"Agent)
[4.4 创建"总控Agent"------团队的大脑](#4.4 创建"总控Agent"——团队的大脑)
[4.5 运行与验证](#4.5 运行与验证)
[五、进阶实战------集成 LLM 实现真实业务](#五、进阶实战——集成 LLM 实现真实业务)
[5.1 整体预览](#5.1 整体预览)
[5.2 通用 LLM 配置](#5.2 通用 LLM 配置)
[5.3 天气专家进阶:Function Calling](#5.3 天气专家进阶:Function Calling)
[5.4 票务专家进阶:LangGraph 工作流](#5.4 票务专家进阶:LangGraph 工作流)
[5.5 总控Agent进阶:意图识别路由](#5.5 总控Agent进阶:意图识别路由)
[六、A2A 与 MCP 的关系辨析](#六、A2A 与 MCP 的关系辨析)
[7.1 核心收获](#7.1 核心收获)
[7.2 实际生产中的扩展方向](#7.2 实际生产中的扩展方向)
一、为什么需要 A2A 协议
1.1 现状的痛点
在当前的 AI Agent 生态中,每一个 Agent 都是"孤岛":
- 能力单一:一个 Agent 通常只擅长一个领域(查天气、写代码、搜索信息......),没有"全能Agent"。
- 接口各异:不同框架(LangChain、CrewAI、AutoGen、自研框架)构建的 Agent,通信方式千差万别,无法直接对话。
- 协作困难:当一个任务需要多个 Agent 协同完成时,开发者必须手动编写胶水代码,将它们"硬编码"在一起。
1.2 用一个故事理解
想象你是一位忙碌的旅行规划师(我们称之为"主控Agent")。你的客户(用户)对你说:
"帮我规划一次北京到上海的出差,顺便看看那边天气怎么样。"
如果你单打独斗,你需要:
- 1.打开浏览器查天气。
- 2.打开 12306 订火车票。
- 3.把两件事的结果拼在一起告诉客户。
这非常繁琐,而且你不是万能的。但如果你有一个团队------"天气专家"和"票务专家"各司其职,你只需要发一封标准化的工作邮件 给他们,他们干完活再把结果填在交接单上发回来。
A2A 协议就是这套"标准化的工作邮件模板"和"任务交接单"。
1.3 A2A 的设计目标
Google 提出 A2A 协议时,明确了以下设计原则:
| 设计原则 | 说明 |
|---|---|
| 标准化 | 定义统一的消息格式、任务生命周期和错误处理机制 |
| 互操作性 | 不同框架、不同语言构建的 Agent 可以无缝通信 |
| 去中心化 | 没有单一的中央调度器,Agent 之间直接对等通信 |
| 可发现性 | Agent 通过"名片"(AgentCard)自我暴露能力,其他 Agent 可以自动发现 |
| 异步优先 | 天然支持长时间运行的任务和人工介入场景 |
二、A2A 协议核心概念详解
在写代码之前,我们先把 A2A 协议中的核心概念彻底弄清楚。这些概念是后续所有代码的"地基"。
2.1 核心概念一览表
| A2A 术语 | 类比 | 说明 | 代码中的体现 |
|---|---|---|---|
| Agent | 一个专家/员工 | 具有特定能力的 AI 实体,能接收任务并返回结果 | A2AServer 的子类 |
| AgentCard | 专家的名片 | 描述 Agent 的名称、能力、地址等元信息,用于服务发现 | AgentCard 对象 |
| AgentSkill | 名片上的技能标签 | 描述 Agent 擅长的具体技能,包含名称、描述和示例 | AgentSkill 对象 |
| Task | 工作交接单 | A2A 通信的核心载体,包含消息、状态、结果等完整信息 | Task 对象 |
| Message | 交接单中的内容 | 用户或 Agent 发送的具体消息,包含角色和内容 | Message 对象 |
| Artifact | 工作成果物 | Agent 完成任务后输出的结果,附着在 Task 上 | task.artifacts |
| TaskStatus | 交接单的盖章状态 | 标记任务当前所处阶段:待处理、进行中、已完成、失败 | TaskStatus / TaskState |
| AgentNetwork | 专家通讯录 | 总控端维护的可调用 Agent 列表 | AgentNetwork 对象 |
2.2 任务生命周期(Task State Machine)
A2A 协议中的任务有明确的状态流转:
提交(SUBMITTED) → 进行中(WORKING) → 已完成(COMPLETED)
│ ↘ 失败(FAILED)
↘ 需要输入(INPUT_REQUIRED) → 进行中(WORKING) → ...
| 状态 | 含义 |
|---|---|
TaskState.SUBMITTED |
任务已提交,等待处理 |
TaskState.WORKING |
Agent 正在处理该任务 |
TaskState.INPUT_REQUIRED |
任务需要额外信息才能继续(如订票缺少出发城市) |
TaskState.COMPLETED |
任务已成功完成,结果在 artifacts 中 |
TaskState.FAILED |
任务处理失败 |
2.3 通信流程图
┌──────────────┐ A2A Protocol ┌──────────────┐
│ 用户 (User) │ │ WeatherAgent │
│ │ │ (Server) │
└──────┬───────┘ └──────▲───────┘
│ │
│ ① 发送请求 │ ④ 返回 Task+Artifact
▼ │
┌──────────────┐ ② 发送 Task (HTTP/JSON-RPC) ┌────┴─────────┐
│ Orchestrator │ ──────────────────────────────────▶│ │
│ (Client) │ │ 天气专家 │
│ │ ──────────────────────────────────▶│ 票务专家 │
│ │ ③ 并发发送 Task │ ...更多专家 │
└──────┬───────┘ └──────────────┘
│
│ ⑤ 汇总结果返回用户
▼
┌──────────────┐
│ 用户 (User) │
└──────────────┘
三、系统架构设计
3.1 整体架构
本实战项目采用星型拓扑结构,以"总控Agent"为中心,协调多个专家Agent:
┌─────────────────┐
│ 用户输入 │
└────────┬────────┘
│
┌────────▼────────┐
│ 总控Agent │
│ (Orchestrator) │
│ - 意图识别 │
│ - 任务分发 │
│ - 结果汇总 │
└───┬─────────┬───┘
│ │
┌───────────▼──┐ ┌──▼───────────┐
│ 天气专家Agent │ │ 票务专家Agent │
│ Port: 5008 │ │ Port: 5009 │
│ - Function │ │ - LangGraph │
│ Calling │ │ 工作流 │
└──────────────┘ └──────────────┘
3.2 项目目录结构
a2a_travel_system/
├── common/
│ └── llm.py # 通用 LLM 配置
├── __001__weather_agent/
│ ├── __001__parse_weather.py # 天气查询逻辑(LangChain Function Calling)
│ └── __002__weather_agent.py # 天气专家 A2A Server
├── __002__ticket_agent/
│ ├── __001__ticket_workflow.py # 订票逻辑(LangGraph 工作流)
│ └── __002__ticket_agent.py # 票务专家 A2A Server
├── __003__orchestrator/
│ ├── __001__weather_ticket_intent.py # 意图识别模块
│ └── __002__orchestrator.py # 总控 Agent
├── .env # 环境变量(API Key 等)
└── requirements.txt
3.3 技术栈
| 组件 | 技术选型 | 作用 |
|---|---|---|
| A2A 通信层 | python-a2a |
实现 Agent 间的标准化通信 |
| LLM 调用 | langchain-openai |
通过 OpenAI 兼容接口调用大模型 |
| 工具调用 | LangChain @tool + Function Calling |
天气专家的技能实现 |
| 工作流编排 | LangGraph | 票务专家的多步骤推理 |
| 意图识别 | LangChain + Pydantic Output Parser | 总控Agent的智能路由 |
四、基础实战------模拟版多 Agent 协作
我们先用最简单的方式跑通整个 A2A 流程,然后再逐步升级。
4.1 环境准备与依赖安装
python
# 创建虚拟环境(推荐)
python -m venv a2a_env
source a2a_env/bin/activate # macOS/Linux
# a2a_env\Scripts\activate # Windows
# 安装核心依赖
pip install python-a2a
# 进阶部分额外依赖
pip install langchain-openai langchain-core langgraph pydantic python-dotenv
注意 :
python-a2a是 Python 社区对 A2A 协议的实现库,它封装了 Server 启动、Client 通信、消息序列化等底层细节,让开发者可以聚焦于业务逻辑。
4.2 创建"天气专家"Agent
这个 Agent 专注于一件事:接收天气查询请求,返回天气信息。
代码文件:weather_agent.py
python
"""
天气专家 Agent - A2A Server
功能:接收天气查询请求,返回模拟天气数据。
"""
from python_a2a import (
A2AServer, run_server,
AgentCard, AgentSkill,
TaskStatus, TaskState
)
# ========== 1. 定义 Agent 名片 ==========
agent_card = AgentCard(
name="WeatherExpert",
description="负责查询天气的专家,能够根据城市名称返回天气信息。",
url="http://127.0.0.1:5008",
skills=[
AgentSkill(
name="get_weather",
description="根据城市查询当前天气状况,包括温度、天气类型等。",
examples=["北京天气怎么样?", "查一下上海天气", "广州今天下雨吗?"]
)
]
)
# ========== 2. 实现 Server 逻辑 ==========
class WeatherExpertServer(A2AServer):
"""
天气专家服务端。
继承 A2AServer,只需实现 handle_task 方法即可。
"""
def __init__(self):
super().__init__(agent_card=agent_card)
def weather_service(self, text: str) -> str:
"""天气业务逻辑(模拟版)"""
# 简单的关键词匹配(后续会用 LLM 替代)
weather_data = {
"北京": "北京今天晴空万里,温度 28°C,微风,适合出行!☀️",
"上海": "上海今天多云转晴,温度 25°C,空气质量良好。🌤️",
"广州": "广州今天有小雨,温度 22°C,出门记得带伞。🌧️",
"深圳": "深圳今天阵雨,温度 26°C,湿度较高。⛈️",
"成都": "成都今天阴天,温度 24°C,适合吃火锅。☁️",
"杭州": "杭州今天多云,温度 25°C,西湖值得一去。🌥️",
}
for city, weather in weather_data.items():
if city in text:
return weather
return "抱歉,暂不支持该城市的天气查询。请试试北京、上海、广州、深圳、成都、杭州。"
def handle_task(self, task):
"""
A2A 任务处理入口 ------ 这是 A2AServer 要求实现的核心方法。
流程:
1. 从 task.message 中提取用户文本
2. 调用业务逻辑获取结果
3. 将结果封装到 task.artifacts 中
4. 更新 task.status 为 COMPLETED
5. 返回 task
"""
print("📨 [天气专家] 收到任务")
# Step 1: 提取用户消息
text = (task.message or {}).get("content", {}).get("text", "")
print(f" 用户输入:{text}")
# Step 2: 调用业务逻辑
result = self.weather_service(text)
# Step 3: 封装结果到 artifacts(工作成果物)
task.artifacts = [{
"parts": [{"type": "text", "text": result}]
}]
# Step 4: 更新任务状态为已完成
task.status = TaskStatus(state=TaskState.COMPLETED)
print(f"✅ [天气专家] 任务完成,返回:{result}")
return task
# ========== 3. 启动服务 ==========
if __name__ == "__main__":
server = WeatherExpertServer()
print(f"✅ [天气专家] 启动成功,监听地址:{server.agent_card.url}")
run_server(server, host="127.0.0.1", port=5008, debug=True)
启动方式:
python weather_agent.py
启动成功后,你会看到类似日志:
python
✅ [天气专家] 启动成功,监听地址:http://127.0.0.1:5008
* Running on http://127.0.0.1:5008
单独测试天气专家:
python
"""测试天气专家 Agent(需先启动 weather_agent.py)"""
import asyncio
import uuid
from python_a2a import A2AClient, Message, MessageRole, Task, TextContent
WEATHER_AGENT_URL = "http://127.0.0.1:5008"
async def test_weather_agent(text: str = "北京天气怎么样?"):
# 构建消息
message = Message(
role=MessageRole.USER,
content=TextContent(text=text)
)
# 构建任务
task = Task(
id=f"test-{uuid.uuid4()}",
message=message.to_dict()
)
# 发送任务并等待结果
client = A2AClient(WEATHER_AGENT_URL)
result_task = await client.send_task_async(task)
# 提取结果
reply = result_task.artifacts[0]["parts"][0]["text"]
print(f"请求:{text}")
print(f"回复:{reply}")
if __name__ == "__main__":
asyncio.run(test_weather_agent())
预期输出:
python
请求:北京天气怎么样?
回复:北京今天晴空万里,温度 28°C,微风,适合出行!☀️
4.3 创建"票务专家"Agent
与天气专家同理,票务专家专注于火车票预订。
代码文件:ticket_agent.py
python
"""
票务专家 Agent - A2A Server
功能:接收订票请求,返回模拟的订票结果。
"""
from python_a2a import (
A2AServer, run_server,
AgentCard, AgentSkill,
TaskStatus, TaskState
)
# ========== 1. 定义 Agent 名片 ==========
agent_card = AgentCard(
name="TicketExpert",
description="负责预订火车票的专家,能够根据出发地和目的地预订车票。",
url="http://127.0.0.1:5009",
skills=[
AgentSkill(
name="book_train_ticket",
description="根据出发地和目的地预订火车票。",
examples=[
"帮我订北京到上海的火车票",
"买张去广州的票",
"订一张后天从深圳到成都的高铁"
]
)
]
)
# ========== 2. 实现 Server 逻辑 ==========
class TicketExpertServer(A2AServer):
def __init__(self):
super().__init__(agent_card=agent_card)
def ticket_service(self, text: str) -> str:
"""订票业务逻辑(模拟版)"""
# 预设线路
routes = {
("北京", "上海"): "G101次列车,北京南站 → 上海虹桥站,08:00发车,12:28到达",
("上海", "北京"): "G102次列车,上海虹桥站 → 北京南站,09:00发车,13:28到达",
("北京", "广州"): "G71次列车,北京西站 → 广州南站,07:00发车,14:38到达",
("广州", "深圳"): "C7001次列车,广州南站 → 深圳北站,08:00发车,08:35到达",
}
for (start, end), detail in routes.items():
if start in text and end in text:
return f"✅ 已为您预订{detail},座位号:10车05A,请准时乘车!"
return "❌ 抱歉,目前仅支持以下线路:北京⇄上海、北京→广州、广州→深圳。"
def handle_task(self, task):
print("📨 [票务专家] 收到任务")
text = (task.message or {}).get("content", {}).get("text", "")
print(f" 用户输入:{text}")
result = self.ticket_service(text)
task.artifacts = [{
"parts": [{"type": "text", "text": result}]
}]
task.status = TaskStatus(state=TaskState.COMPLETED)
print(f"✅ [票务专家] 任务完成,返回:{result}")
return task
# ========== 3. 启动服务 ==========
if __name__ == "__main__":
server = TicketExpertServer()
print(f"✅ [票务专家] 启动成功,监听地址:{server.agent_card.url}")
run_server(server, host="127.0.0.1", port=5009, debug=True)
4.4 创建"总控Agent"------团队的大脑
总控 Agent 是整个系统的指挥中心。它本身不生产任何业务能力,而是负责:
- 1.维护专家通讯录(AgentNetwork)
- 2.接收用户请求并分析意图
- 3.分发任务给对应的专家 Agent
- 4.汇总结果并回复用户
代码文件:orchestrator.py
python
"""
总控 Agent(Orchestrator)
角色:A2A Client,负责接收用户请求、分发任务、汇总结果。
"""
import asyncio
import uuid
from python_a2a import (
AgentNetwork, Task,
Message, MessageRole, TextContent
)
# ========== 1. 专家通讯录 ==========
network = AgentNetwork(name="TravelTeam")
network.add("WeatherExpert", "http://127.0.0.1:5008")
network.add("TicketExpert", "http://127.0.0.1:5009")
class Orchestrator:
"""
总控Agent - 旅行规划团队的大脑。
职责:
- 分析用户请求中的关键词,判断需要调用哪些专家
- 并发分发任务(使用 asyncio.gather)
- 汇总所有专家的结果,组装最终回复
"""
def __init__(self, network):
self.network = network
def make_task(self, text: str) -> Task:
"""构建 A2A 标准任务对象"""
message = Message(
role=MessageRole.USER,
content=TextContent(text=text)
)
return Task(
id=f"task-{uuid.uuid4()}",
message=message.to_dict()
)
async def call_agent(self, agent_name: str, text: str):
"""
调用单个专家 Agent。
Args:
agent_name: 专家名称(对应通讯录中的 key)
text: 用户原文
Returns:
(agent_name, result_text) 元组
"""
print(f"📤 [总控Agent] 正在向【{agent_name}】发送任务...")
client = self.network.get_agent(agent_name)
task = self.make_task(text)
result_task = await client.send_task_async(task)
result = result_task.artifacts[0]["parts"][0]["text"]
print(f"📥 [总控Agent] 收到【{agent_name}】的回复")
return agent_name, result
async def handle_user_request(self, text: str) -> str:
"""
处理用户请求的主流程。
策略(基础版用关键词匹配,进阶版用 LLM 意图识别):
- 包含"天气" → 调用天气专家
- 包含"订票"或"火车票" → 调用票务专家
- 两者可能同时出现 → 并发调用
"""
print("\n" + "=" * 60)
print(f"👋 [总控Agent] 收到用户请求:{text}")
# ---- 意图分析(基础版:关键词匹配) ----
jobs = []
if "天气" in text:
print("🔍 [总控Agent] 分析意图 → 需要查询天气")
jobs.append(self.call_agent("WeatherExpert", text))
if "订票" in text or "火车票" in text:
print("🔍 [总控Agent] 分析意图 → 需要预订火车票")
jobs.append(self.call_agent("TicketExpert", text))
if not jobs:
return "抱歉,我目前只会查天气和订火车票。请说"查天气"或"订票"试试。"
# ---- 并发执行所有任务 ----
results = await asyncio.gather(*jobs, return_exceptions=True)
# ---- 汇总结果 ----
final_parts = []
for item in results:
if isinstance(item, Exception):
final_parts.append(f"❌ 调用失败:{item}")
else:
agent_name, result = item
final_parts.append(f"【{agent_name}】{result}")
final_response = "\n\n".join(final_parts)
print("\n✅ [总控Agent] 任务汇总完成")
print("=" * 60)
return final_response
async def main():
# 打印通讯录
print("✅ [总控Agent] 专家通讯录已加载:")
for agent in network.list_agents():
print(f" 📋 {agent['name']}: {agent['description']}")
orchestrator = Orchestrator(network)
print("\n" + "=" * 60)
print("🚀 欢迎使用旅行规划总控Agent!")
print("💡 我可以帮您:查天气、订火车票")
print("📝 输入 'quit' 退出")
print("=" * 60)
while True:
text = input("\n🧑 您:").strip()
if not text:
continue
if text.lower() == "quit":
print("👋 再见!")
break
result = await orchestrator.handle_user_request(text)
print(f"\n🤖 助手:\n{result}")
if __name__ == "__main__":
asyncio.run(main())
4.5 运行与验证
启动顺序很重要------先启动两个专家,再启动总控:
python
# 终端 1:启动天气专家
python weather_agent.py
# 终端 2:启动票务专家
python ticket_agent.py
# 终端 3:启动总控 Agent
python orchestrator.py
场景一:查询天气
python
🧑 您:帮我查一下上海的天气
============================================================
👋 [总控Agent] 收到用户请求:帮我查一下上海的天气
🔍 [总控Agent] 分析意图 → 需要查询天气
📤 [总控Agent] 正在向【WeatherExpert】发送任务...
📨 [天气专家] 收到任务
用户输入:帮我查一下上海的天气
✅ [天气专家] 任务完成
📥 [总控Agent] 收到【WeatherExpert】的回复
✅ [总控Agent] 任务汇总完成
============================================================
🤖 助手:
【WeatherExpert】上海今天多云转晴,温度 25°C,空气质量良好。🌤️
场景二:复合任务(同时查天气 + 订票)
python
🧑 您:我要去上海出差,帮我订票并看看天气如何?
============================================================
👋 [总控Agent] 收到用户请求:我要去上海出差,帮我订票并看看天气如何?
🔍 [总控Agent] 分析意图 → 需要查询天气
🔍 [总控Agent] 分析意图 → 需要预订火车票
📤 [总控Agent] 正在向【WeatherExpert】发送任务...
📤 [总控Agent] 正在向【TicketExpert】发送任务...
📨 [天气专家] 收到任务
📨 [票务专家] 收到任务
📥 [总控Agent] 收到【WeatherExpert】的回复
📥 [总控Agent] 收到【TicketExpert】的回复
✅ [总控Agent] 任务汇总完成
============================================================
🤖 助手:
【WeatherExpert】上海今天多云转晴,温度 25°C,空气质量良好。🌤️
【TicketExpert】✅ 已为您预订G101次列车,北京南站 → 上海虹桥站,08:00发车,12:28到达,座位号:10车05A,请准时乘车!
关键观察 :两个任务是通过
asyncio.gather并发执行的,这意味着查天气和订票是同时进行的,总耗时取决于最慢的那个,而非两者之和。这正是 A2A 协议"异步优先"设计理念的体现。
五、进阶实战------集成 LLM 实现真实业务
基础版用关键词匹配来判断意图、返回结果,逻辑过于简单。在进阶版中,我们引入**大语言模型(LLM)**来实现:
- 天气专家:使用 LangChain Function Calling 自动调用天气工具
- 票务专家:使用 LangGraph 工作流实现信息抽取 + 订票的多步骤推理
- 总控 Agent:使用 LLM 意图识别替代关键词匹配
5.1 整体预览
进阶版的系统架构保持不变,但每个组件的"大脑"从硬编码逻辑升级为 LLM 驱动:
python
┌────────────────────────────────────────────────────┐
│ 总控 Agent │
│ ┌──────────────────────────────────┐ │
│ │ LLM 意图识别 │ │
│ │ "need_weather: true, │ │
│ │ need_ticket: true" │ │
│ └────────────┬─────────┬───────────┘ │
│ │ │ │
│ ┌──────▼──┐ ┌───▼───────┐ │
│ │天气专家 │ │票务专家 │ │
│ │ │ │ │ │
│ │LLM + │ │LLM + │ │
│ │Function │ │LangGraph │ │
│ │Calling │ │工作流 │ │
│ └─────────┘ └───────────┘ │
└────────────────────────────────────────────────────┘
5.2 通用 LLM 配置
文件:common/llm.py
python
"""
通用 LLM 配置模块
所有需要调用大模型的组件共用此配置,避免重复初始化。
支持 OpenAI 兼容接口(如 DeepSeek、通义千问、本地 Ollama 等)。
"""
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
# 加载 .env 文件(从项目根目录向上查找)
env_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
".env"
)
load_dotenv(env_path)
MODEL_API_KEY = os.getenv("MODEL_API_KEY")
MODEL_BASE_URL = os.getenv("MODEL_BASE_URL")
MODEL_NAME = os.getenv("MODEL_NAME")
my_llm = ChatOpenAI(
api_key=MODEL_API_KEY,
base_url=MODEL_BASE_URL,
model=MODEL_NAME,
temperature=0, # 设为 0 保证意图识别和信息抽取的稳定性
)
if __name__ == "__main__":
# 快速测试 LLM 连通性
print(f"模型:{MODEL_NAME}")
print(f"接口:{MODEL_BASE_URL}")
print("测试输出:", end="")
for chunk in my_llm.stream("你好,请用一句话介绍你自己。"):
print(chunk.content, flush=True, end="")
print()
.env 文件示例:
python
MODEL_API_KEY=sk-your-api-key-here
MODEL_BASE_URL=https://api.openai.com/v1
MODEL_NAME=gpt-4o-mini
5.3 天气专家进阶:Function Calling
使用 LangChain 的 @tool 装饰器定义天气查询工具,然后通过 Function Calling 让 LLM 自动决定何时调用以及如何解析用户输入。
文件:__001__weather_agent/__001__parse_weather.py
python
"""
天气查询逻辑 ------ 基于 LangChain Function Calling
LLM 会自动判断用户是否在问天气,并提取城市名称,调用对应工具。
"""
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from common.llm import my_llm
# ========== 1. 定义工具 ==========
@tool
def get_weather(city: str) -> str:
"""根据城市名称返回该城市的当前天气情况。支持全国主要城市。"""
# 模拟天气数据(实际项目中替换为真实 API,如和风天气、OpenWeatherMap)
weather_db = {
"北京": "晴,28°C,微风",
"上海": "多云,22°C,东南风3级",
"广州": "小雨,19°C,湿度85%",
"深圳": "阵雨,26°C,体感温度29°C",
"成都": "阴,24°C,空气质量良",
"杭州": "多云,25°C,适合出行",
"南京": "晴,27°C,紫外线较强",
"武汉": "雷阵雨,23°C,请注意安全",
"西安": "晴,30°C,高温预警",
"重庆": "雾,21°C,能见度较低",
"天津": "多云,26°C,海风较大",
"苏州": "小雨,20°C,出门带伞",
"长沙": "阴,22°C,湿度适中",
"郑州": "晴,29°C,干燥",
"青岛": "晴,24°C,海滨城市空气清新",
"厦门": "多云,27°C,适合旅游",
"昆明": "晴,23°C,四季如春",
"大连": "多云,18°C,海风凉爽",
"哈尔滨": "晴,15°C,早晚温差大",
"乌鲁木齐": "晴,26°C,干燥少雨",
}
return weather_db.get(city, f"未找到 {city} 的天气信息,目前支持全国主要省会及一线城市。")
# ========== 2. 组装 Agent(LLM + Tools) ==========
tools = [get_weather]
# 绑定工具到 LLM,让模型具备 Function Calling 能力
llm_with_tools = my_llm.bind_tools(tools)
def parse_weather_text(user_input: str) -> str:
"""
天气查询入口函数。
流程:
1. 将用户输入发送给绑定了工具的 LLM
2. LLM 自动判断是否需要调用 get_weather 工具
3. 如果需要,LLM 提取 city 参数并调用工具
4. 将工具返回结果再次交给 LLM,生成自然语言回复
"""
# 第一轮:LLM 分析是否需要调用工具
response = llm_with_tools.invoke([HumanMessage(content=user_input)])
# 如果 LLM 调用了工具
if response.tool_calls:
tool_results = []
for tool_call in response.tool_calls:
# 执行工具调用
result = get_weather.invoke(tool_call["args"])
tool_results.append(result)
# 第二轮:将工具结果交给 LLM 生成最终回复
from langchain_core.messages import ToolMessage
messages = [
HumanMessage(content=user_input),
response,
ToolMessage(content="\n".join(tool_results), tool_call_id=response.tool_calls[0]["id"])
]
final_response = llm_with_tools.invoke(messages)
return final_response.content
# 如果 LLM 认为不需要调用工具,直接返回回复
return response.content
if __name__ == "__main__":
# 测试用例
test_inputs = [
"北京的天气怎么样呢?",
"查一下上海今天天气如何",
"广州下雨吗?",
"你好,今天过得怎么样?", # 非天气问题
]
for inp in test_inputs:
print(f"\n输入:{inp}")
print(f"输出:{parse_weather_text(inp)}")
print("-" * 40)
天气 Agent Server 不需要大改,只需替换 weather_service 方法的实现:
文件:__001__weather_agent/__002__weather_agent.py
python
"""
天气专家 Agent(进阶版)
核心变化:weather_service 从硬编码逻辑替换为 LLM Function Calling。
"""
from python_a2a import (
A2AServer, run_server,
AgentCard, AgentSkill,
TaskStatus, TaskState
)
from __001__weather_agent.__001__parse_weather import parse_weather_text
agent_card = AgentCard(
name="WeatherExpert",
description="负责查询天气的专家,能够根据城市名称返回实时天气信息。",
url="http://127.0.0.1:5008",
skills=[
AgentSkill(
name="get_weather",
description="根据城市查询天气,支持全国主要城市。",
examples=["北京天气怎么样?", "查一下上海天气", "广州今天下雨吗?"]
)
]
)
class WeatherExpertServer(A2AServer):
def __init__(self):
super().__init__(agent_card=agent_card)
def weather_service(self, text: str) -> str:
"""调用 LLM Function Calling 处理天气查询"""
return parse_weather_text(text)
def handle_task(self, task):
print("📨 [天气专家] 收到任务")
text = (task.message or {}).get("content", {}).get("text", "")
print(f" 用户输入:{text}")
result = self.weather_service(text)
task.artifacts = [{
"parts": [{"type": "text", "text": result}]
}]
task.status = TaskStatus(state=TaskState.COMPLETED)
print(f"✅ [天气专家] 已返回:{result}")
return task
if __name__ == "__main__":
server = WeatherExpertServer()
print(f"✅ [天气专家] 启动成功:{server.agent_card.url}")
run_server(server, host="0.0.0.0", port=5008, debug=True)
5.4 票务专家进阶:LangGraph 工作流
票务专家的逻辑更复杂,需要:
- 1.信息抽取:从自然语言中提取出发城市、目的城市、出行时间
- 2.信息校验:检查是否有缺失字段,必要时提示用户补充
- 3.执行订票:组装订票结果
使用 LangGraph 构建一个两节点的线性工作流。
文件:__002__ticket_agent/__001__ticket_workflow.py
python
"""
订票工作流 ------ 基于 LangGraph 的两步推理链
工作流:
START → extract_city_node(信息抽取) → order_ticket_node(订票/补全提示) → END
"""
from typing import TypedDict
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langgraph.constants import END, START
from langgraph.graph import StateGraph
from pydantic import BaseModel, Field
from common.llm import my_llm
# ========== 1. 定义状态结构 ==========
class AgentState(TypedDict):
"""LangGraph 工作流的共享状态"""
input: str # 用户原始输入
start_city: str # 抽取出的出发城市
end_city: str # 抽取出的目的城市
departure_time: str # 抽取出的出行时间
output: str # 最终输出结果
# ========== 2. 定义信息抽取的 Pydantic Schema ==========
class TicketBookingExtract(BaseModel):
"""订票信息的结构化表示"""
start_city: str = Field(
default="",
description="出发城市,如北京、上海;用户未提及或无法确定时填空字符串",
)
end_city: str = Field(
default="",
description="目的城市;用户未提及或无法确定时填空字符串",
)
departure_time: str = Field(
default="",
description="预定出行时间,保留用户原意,如 2026-05-20、明天上午、下周一;未提及或无法确定时填空字符串",
)
# ========== 3. 构建信息抽取 Chain ==========
_extract_parser = PydanticOutputParser(pydantic_object=TicketBookingExtract)
_extract_prompt = ChatPromptTemplate.from_messages([
(
"system",
"你是火车票预订信息抽取助手。从用户自然语言中提取出发城市、目的城市和出行/预定时间。"
"城市使用标准中文地名(如'北京'而非'帝都'),不要编造用户未提及的信息。"
"任一字段抽不出来时输出空字符串 \"\",不要用'未知''无'等占位词。"
),
(
"user",
"请从以下内容抽取订票信息。\n"
"{format_instructions}\n\n"
"用户输入:{user_input}",
),
]).partial(format_instructions=_extract_parser.get_format_instructions())
_extract_chain = _extract_prompt | my_llm | _extract_parser
# 无效值集合,用于清洗 LLM 输出
_PLACEHOLDER_VALUES = frozenset(
{"未知", "无", "未提及", "null", "none", "n/a", "na", "不详", "待定"}
)
def _clean_field(value: str) -> str:
"""清洗 LLM 输出中的无效占位值"""
s = (value or "").strip()
if not s or s.lower() in _PLACEHOLDER_VALUES:
return ""
return s
# ========== 4. LangGraph 节点函数 ==========
def extract_city_node(state: AgentState) -> AgentState:
"""节点1:使用 LLM + Pydantic Parser 从用户输入中抽取订票信息"""
user_input = state.get("input", "")
print(f" [信息抽取] 正在解析用户输入:{user_input}")
extracted = _extract_chain.invoke({"user_input": user_input})
state["start_city"] = _clean_field(extracted.start_city)
state["end_city"] = _clean_field(extracted.end_city)
state["departure_time"] = _clean_field(extracted.departure_time)
print(f" [信息抽取] 结果 → 出发:{state['start_city'] or '未提取'},"
f"目的:{state['end_city'] or '未提取'},"
f"时间:{state['departure_time'] or '未提取'}")
return state
def order_ticket_node(state: AgentState) -> AgentState:
"""节点2:校验信息完整性,执行订票或提示补充"""
start_city = state.get("start_city", "")
end_city = state.get("end_city", "")
departure_time = state.get("departure_time", "")
# 检查缺失字段
missing = []
if not start_city:
missing.append("出发城市")
if not end_city:
missing.append("目的城市")
if not departure_time:
missing.append("出行时间")
if missing:
# 信息不完整,提示用户补充
state["output"] = (
f"订票信息不完整,还需要您补充以下信息:{'、'.join(missing)}。\n"
f"请提供完整信息后重新下单。"
)
else:
# 信息完整,执行订票(模拟)
state["output"] = (
f"✅ 已为您成功预订火车票!\n"
f" 🚄 线路:{start_city} → {end_city}\n"
f" 📅 时间:{departure_time}\n"
f" 💺 座位:08车 06F(靠窗)\n"
f" 💰 票价:二等座 ¥553.0"
)
print(f" [订票节点] 输出:{state['output'][:50]}...")
return state
# ========== 5. 构建 LangGraph ==========
def build_langgraph():
"""构建并编译 LangGraph 工作流"""
graph_builder = StateGraph(AgentState)
# 添加节点
graph_builder.add_node("extract_city_node", extract_city_node)
graph_builder.add_node("order_ticket_node", order_ticket_node)
# 添加边
graph_builder.add_edge(START, "extract_city_node")
graph_builder.add_edge("extract_city_node", "order_ticket_node")
graph_builder.add_edge("order_ticket_node", END)
return graph_builder.compile()
# 编译工作流(模块加载时执行一次)
graph = build_langgraph()
def parse_ticket_text(text: str) -> str:
"""订票入口函数"""
result = graph.invoke({"input": text})
return result.get("output", "处理出错。")
if __name__ == "__main__":
# 测试用例
test_cases = [
"帮我订一张后天从北京到上海的火车票",
"我想买张去广州的票", # 缺少出发城市
"从深圳到成都,下周一出发", # 完整信息
]
for case in test_cases:
print(f"\n输入:{case}")
print(f"输出:{parse_ticket_text(case)}")
print("-" * 50)
票务 Agent Server:
文件:__002__ticket_agent/__002__ticket_agent.py
python
"""
票务专家 Agent(进阶版)
核心变化:ticket_service 从硬编码替换为 LangGraph 工作流。
"""
from python_a2a import (
A2AServer, run_server,
AgentCard, AgentSkill,
TaskStatus, TaskState
)
from __002__ticket_agent.__001__ticket_workflow import parse_ticket_text
agent_card = AgentCard(
name="TicketExpert",
description="负责预订火车票的专家,能够理解自然语言并自动提取订票信息。",
url="http://127.0.0.1:5009",
skills=[
AgentSkill(
name="book_train_ticket",
description="根据出发地和目的地预订火车票,支持自然语言输入。",
examples=[
"帮我订北京到上海的火车票",
"买张去广州的票",
"订一张后天从深圳到成都的高铁"
]
)
]
)
class TicketExpertServer(A2AServer):
def __init__(self):
super().__init__(agent_card=agent_card)
def ticket_service(self, text: str) -> str:
"""调用 LangGraph 工作流处理订票请求"""
return parse_ticket_text(text)
def handle_task(self, task):
print("📨 [票务专家] 收到任务")
text = (task.message or {}).get("content", {}).get("text", "")
print(f" 用户输入:{text}")
result = self.ticket_service(text)
task.artifacts = [{
"parts": [{"type": "text", "text": result}]
}]
task.status = TaskStatus(state=TaskState.COMPLETED)
print(f"✅ [票务专家] 已返回:{result[:50]}...")
return task
if __name__ == "__main__":
server = TicketExpertServer()
print(f"✅ [票务专家] 启动成功:{server.agent_card.url}")
run_server(server, host="0.0.0.0", port=5009, debug=True)
5.5 总控Agent进阶:意图识别路由
进阶版的总控 Agent 不再使用关键词匹配,而是通过 LLM 进行结构化意图识别。
文件:__003__orchestrator/__001__weather_ticket_intent.py
python
"""
意图识别模块 ------ 使用 LLM + Pydantic 判断用户是否需要查天气或订票
"""
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field
from common.llm import my_llm
class WeatherTicketIntent(BaseModel):
"""意图识别的结构化输出"""
need_weather: bool = Field(
description="用户是否需要查询某地天气、气温、是否下雨等气象信息"
)
need_ticket: bool = Field(
description="用户是否需要预订或购买火车票、高铁票、机票等出行票务"
)
# 构建意图识别 Chain
_intent_parser = PydanticOutputParser(pydantic_object=WeatherTicketIntent)
_intent_prompt = ChatPromptTemplate.from_messages([
(
"system",
"你是旅行助手意图识别模块。根据用户的一句话,判断是否需要查天气、是否需要订票。\n"
"规则:\n"
"1. 只根据用户明确或合理隐含的需求判断,不要臆测\n"
"2. 与天气、票务无关的闲聊,两项均为 false\n"
"3. 同一句话里可能同时需要查天气和订票,此时对应字段均为 true\n"
"4. '出差''旅行'等隐含出行需求时,need_ticket 可为 true\n"
"5. 提到某个城市但没有明确出行意图时,不要设 need_ticket 为 true"
),
(
"user",
"请判断以下用户输入的意图。\n"
"{format_instructions}\n\n"
"用户输入:{user_input}",
),
]).partial(format_instructions=_intent_parser.get_format_instructions())
_intent_chain = _intent_prompt | my_llm | _intent_parser
def detect_weather_ticket_intent(user_input: str) -> WeatherTicketIntent:
"""用 LLM 判断用户输入是否包含查天气或订票需求。"""
text = (user_input or "").strip()
if not text:
return WeatherTicketIntent(need_weather=False, need_ticket=False)
return _intent_chain.invoke({"user_input": text})
if __name__ == "__main__":
# 测试意图识别
test_samples = [
"北京今天天气怎么样?", # weather=True, ticket=False
"帮我订一张后天从北京到上海的火车票", # weather=False, ticket=True
"查一下广州天气,再订明天去深圳的票", # weather=True, ticket=True
"我要去上海出差,帮我订票并看看天气如何", # weather=True, ticket=True
"你好,介绍一下你自己", # weather=False, ticket=False
"北京有什么好玩的?", # weather=False, ticket=False
]
for s in test_samples:
intent = detect_weather_ticket_intent(s)
print(f"输入:{s}")
print(f" → 查天气={intent.need_weather},订票={intent.need_ticket}\n")
总控 Agent 主文件(进阶版):
文件:__003__orchestrator/__002__orchestrator.py
python
"""
总控 Agent(进阶版)
核心变化:意图判断从关键词匹配升级为 LLM 结构化识别。
"""
import asyncio
import uuid
from python_a2a import (
AgentNetwork, Task,
Message, MessageRole, TextContent
)
from __003__orchestrator.__001__weather_ticket_intent import (
detect_weather_ticket_intent,
)
# ========== 专家通讯录 ==========
network = AgentNetwork(name="TravelTeam")
network.add("WeatherExpert", "http://127.0.0.1:5008")
network.add("TicketExpert", "http://127.0.0.1:5009")
class Orchestrator:
def __init__(self, network):
self.network = network
def make_task(self, text: str) -> Task:
message = Message(
role=MessageRole.USER,
content=TextContent(text=text)
)
return Task(
id=f"task-{uuid.uuid4()}",
message=message.to_dict()
)
async def call_agent(self, agent_name: str, text: str):
print(f"📤 [总控Agent] 正在调用【{agent_name}】")
client = self.network.get_agent(agent_name)
task = self.make_task(text)
result_task = await client.send_task_async(task)
result = result_task.artifacts[0]["parts"][0]["text"]
print(f"📥 [总控Agent] 收到【{agent_name}】回复")
return agent_name, result
async def handle_user_request(self, text: str) -> str:
print("\n" + "=" * 60)
print(f"👋 [总控Agent] 收到用户请求:{text}")
# ---- LLM 意图识别 ----
intent = detect_weather_ticket_intent(text)
print(f"🔍 [总控Agent] 意图识别结果:查天气={intent.need_weather},订票={intent.need_ticket}")
jobs = []
if intent.need_weather:
jobs.append(self.call_agent("WeatherExpert", text))
if intent.need_ticket:
jobs.append(self.call_agent("TicketExpert", text))
if not jobs:
return "抱歉,我目前只会查天气和订火车票。您可以这样说:\n• "北京天气怎么样?"\n• "帮我订一张去上海的火车票""
results = await asyncio.gather(*jobs, return_exceptions=True)
final_parts = []
for item in results:
if isinstance(item, Exception):
final_parts.append(f"❌ 调用失败:{item}")
else:
agent_name, result = item
final_parts.append(f"【{agent_name}】\n{result}")
final_response = "\n\n".join(final_parts)
print("\n✅ [总控Agent] 汇总完成")
print("=" * 60)
return final_response
async def main():
print("✅ [总控Agent] 通讯录已加载:")
for agent in network.list_agents():
print(f" 📋 {agent['name']}: {agent['description']}")
orchestrator = Orchestrator(network)
print("\n" + "=" * 60)
print("🚀 欢迎使用旅行规划总控Agent(LLM 进阶版)!")
print("💡 我可以帮您:查天气、订火车票")
print("📝 输入 'quit' 退出")
print("=" * 60)
while True:
text = input("\n🧑 您:").strip()
if not text:
continue
if text.lower() == "quit":
print("👋 再见!")
break
result = await orchestrator.handle_user_request(text)
print(f"\n🤖 助手:\n{result}")
if __name__ == "__main__":
asyncio.run(main())
进阶版运行效果:
python
🧑 您:我要去上海出差,帮我看看天气顺便订个票
============================================================
👋 [总控Agent] 收到用户请求:我要去上海出差,帮我看看天气顺便订个票
🔍 [总控Agent] 意图识别结果:查天气=True,订票=True
📤 [总控Agent] 正在调用【WeatherExpert】
📤 [总控Agent] 正在调用【TicketExpert】
📨 [天气专家] 收到任务
📨 [票务专家] 收到任务
[信息抽取] 正在解析用户输入:我要去上海出差...
[信息抽取] 结果 → 出发:(未提及),目的:上海,时间:(未提及)
📥 [总控Agent] 收到【WeatherExpert】回复
📥 [总控Agent] 收到【TicketExpert】回复
✅ [总控Agent] 汇总完成
============================================================
🤖 助手:
【WeatherExpert】
上海今天多云,温度22°C,东南风3级,适合出行。
【TicketExpert】
订票信息不完整,还需要您补充以下信息:出发城市、出行时间。
请提供完整信息后重新下单。
注意看:LLM 驱动的票务专家能够智能识别出用户只说了"去上海"但没说"从哪出发"和"什么时间",会主动提示补充信息,而不是盲目返回一个错误结果。这就是 LLM 相比硬编码的巨大优势。
六、A2A 与 MCP 的关系辨析
在学习 A2A 的过程中,很多人会将它与 Anthropic 的 MCP(Model Context Protocol) 混淆。这里做一个清晰的辨析:
| 维度 | MCP (Model Context Protocol) | A2A (Agent-to-Agent) |
|---|---|---|
| 提出者 | Anthropic (2024年11月) | Google (2025年4月) |
| 解决的问题 | Agent 如何调用工具和数据源 | Agent 之间如何互相通信和协作 |
| 通信对象 | Agent ↔ Tool(工具) | Agent ↔ Agent(智能体) |
| 类比 | 工人和工具箱之间的接口标准 | 工人与工人之间的协作流程 |
| 关注点 | 工具发现、数据注入、上下文管理 | 任务分发、状态流转、结果汇总 |
| 典型场景 | LLM 调用数据库查询、调用搜索引擎 | 总控Agent调度天气Agent和票务Agent |
两者是互补关系,而非竞争关系:
- 一个 Agent 可以通过 MCP 接入各种工具(数据库、API、文件系统)。
- 多个 Agent 之间可以通过 A2A 互相发现、互相委托任务。
- 在复杂系统中,MCP 负责"纵向"的工具调用,A2A 负责"横向"的智能体协作。
七、总结与展望
7.1 核心收获
通过本教程的实战,我们掌握了以下关键知识:
概念层面:
- A2A 是什么:一套让 AI Agent 之间标准化通信和协作的开放协议
- 核心组件:Agent(工作者)、AgentCard(名片)、AgentSkill(技能标签)、Task(工单)、Artifact(成果物)
- 通信流程:Client Agent 通过 AgentNetwork 发现 Server Agent → 发送 Task → 接收 Artifact → 汇总响应
工程层面:
- 使用
python-a2a快速搭建 A2A Server 和 Client - 使用 LangChain Function Calling 实现工具调用型 Agent
- 使用 LangGraph 构建多步骤推理型 Agent
- 使用 Pydantic + LLM 实现结构化意图识别
架构层面:
- 星型拓扑的编排模式(Orchestrator 模式)
asyncio.gather实现任务并发执行- 各 Agent 独立部署、独立扩展的微服务思想
7.2 实际生产中的扩展方向
| 方向 | 说明 |
|---|---|
| 接入真实 API | 天气对接和风天气/高德天气,票务对接 12306 或携程开放平台 |
| 增加更多专家 | 酒店预订 Agent、景点推荐 Agent、行程日历 Agent 等 |
| 错误重试机制 | 为 Task 增加超时控制和重试策略 |
| Agent 动态发现 | 通过服务注册中心(如 Consul)实现 Agent 的自动注册和发现 |
| 流式响应 | 利用 A2A 的 Streaming 能力,实时返回处理进度 |
| 多轮对话 | 利用 TaskState.INPUT_REQUIRED 实现任务中断-补充-继续的交互模式 |
| 安全认证 | 为 AgentCard 增加认证机制,防止未授权的 Agent 访问 |