16|双剑合璧:MCP和A2A联合构建智能Agents系统

你好,我是黄佳。

到目前为止,我们已经演练了很多 MCP 和 A2A 的实战示例。在这节课中,我将带你通过实际操作,使用开源的 python-a2a 库构建一个具备 A2A 通信与 MCP 工具调用能力的智能 Agent 系统。我们会从零开始,创建工具服务、实现代理通信,并集成大模型,使Agent具备真实的感知与行动能力。

这节课将构建一个完整的 A2A + MCP 智能 Agent 系统

3 分钟复习一下 A2A 与 MCP 协议

在未来的多智能体系统中,A2A 和 MCP 将成为两个关键基础协议。

A2A 是 Google 于 2025 年 4 月发布的开放协议,专为AI代理之间的协作与互操作设计,特别适用于大规模、多智能体系统。其目标是建立统一通信格式,使不同 AI 代理能够互相发现、理解能力,并实现任务协作、状态共享。

A2A的关键能力包括:

  • 安全协作:企业级认证和权限控制。

  • 状态管理:支持任务跟踪、实时通知。

  • 能力发现与协商:自动对接功能,提升交互质量。

  • 多模态通信:支持文本、音频、视频等信息交换。

MCP 由 Anthropic 于 2024 年 11 月推出,致力于标准化 AI 模型对外部工具与数据的调用,推动从“会聊天”向“能执行”进化。目标是统一函数调用接口,连接 API、数据库、文件系统等上下文资源,降低 AI 模型与外部系统的集成成本。

A2A 和 MCP 聚焦点不同,却高度互补:

  • A2A 关注代理间如何沟通、协作、分工,定义“谁和谁说话,为了什么目标”。

  • MCP 关注代理如何调用外部工具,定义“有什么工具、怎么用、返回什么结果”

二者结合,就构成了智能体系统的通信中枢 + 行动引擎。

以旅行规划为例,当用户向“旅行总代理”提出请求(A2A 发起),总代理将任务拆分为子任务并通过 A2A 分配给多个代理:

  • 机票代理 → 通过 MCP 调用航班比价工具

  • 酒店代理 → 通过 MCP 查询酒店数据库

  • 景点代理 → 调用天气工具和用户偏好服务

子代理完成任务后通过 A2A 汇总结果,总代理整合后返回给用户。

第一部分:构建 MCP 工具服务

我们的第一个任务是创建一个 MCP 服务,它将提供一些基础的工具供后续的 A2A 代理调用。

首先,进入代码示例库的A2A-MCP目录,并在你的终端中运行 uv pip install. 来自动安装环境,也可以通过requirements.txt来手动创建环境。此处的关键包是python-a2a 库。这是一个开源的A2A和MCP协议的社区实现,适合于协议的学习。

此外,在部署MCP工具时涉及第三方API Key的创建,需要在这个链接中申请天气API。

接下来我们创建一个简化版的工具脚本,创建 MCP 工具代码,命名为 mcp_server.py。

# mcp_server.py
from   python_a2a.mcp import FastMCP, text_response, create_fastapi_app
import   uvicorn
from   datetime import datetime
import   time # 用于 get_current_time
import   requests

# 1. 创建 FastMCP 服务实例
# FastMCP   是一个轻量级的 MCP 服务器实现
utility_mcp   = FastMCP(
   name="My Utility Tools",
   description="一些常用的实用MCP工具集合",
   version="1.0.0"
)

# 2. 定义第一个工具:计算器
@utility_mcp.tool(
   name="calculator", # 工具的唯一名称
   description="执行一个简单的数学表达式字符串,例如   '5 * 3 + 2'" 
   # 工具的描述,LLM   可以理解这个描述来决定何时使用它
)
def   calculate(expression: str): # 类型提示很重要,MCP 会据此生成工具的 schema
   """
   安全地评估一个数学表达式字符串。
   Args:
       expression: 要评估的数学表达式,例如   "10 + 5*2"
   Returns:
       包含计算结果的文本响应。
   """
   try:
       result = eval(expression,   {"__builtins__": {}}, {"abs": abs, "max": max,   "min": min, "pow": pow, "round": round,   "sum": sum})
       return text_response(f"计算结果: {expression} = {result}")
   except Exception as e:
       return text_response(f"计算错误 '{expression}': {str(e)}")

# 3. 定义第二个工具:获取当前时间
@utility_mcp.tool(
   name="get_current_time",
   description="获取当前本地的日期和时间信息"
)
def   get_current_time_tool(): # 注意:工具函数名可以和工具名不同
   """
   获取当前的日期和时间。
   Returns:
       包含当前日期和时间的文本响应。
   """
   now = datetime.now()
     response = (
         f"当前日期:   {now.strftime('%Y-%m-%d')}\\n"
         f"当前时间:   {now.strftime('%H:%M:%S')}\\n"
         f"星期几:   {now.strftime('%A')}"
     )
     return text_response(response)
 
 OPENWEATHER_API_KEY="ee0c0d496e95d9237069b703fb4b8fa4"
 
 @utility_mcp.tool(
     name="get_current_weather",
     description="获取当前的天气信息,需要提供城市名称"
 )
 def   get_current_weather_tool(city: str):
     try:
         # 请求 OpenWeatherMap   API
         url =   f"http://api.openweathermap.org/data/2.5/weather"
         params = {
             "q": city,
             "appid":   OPENWEATHER_API_KEY,
             "units":   "metric",
             "lang":   "zh_cn"
         }
         response = requests.get(url,   params=params)
         data = response.json()
 
         if response.status_code != 200:
             return text_response(f"获取{city}天气失败:{data.get('message', '未知错误')}")
 
         weather_desc =   data['weather'][0]['description']
         temp = data['main']['temp']
         feels_like =   data['main']['feels_like']
         return text_response(f"{city}当前天气是 {weather_desc},温度为 {temp}°C,体感温度为 {feels_like}°C")
     
     except Exception as e:
         return text_response(f"获取{city}天气时出错:{str(e)}")
 
 # 运行 MCP 服务
 if   __name__ == "__main__":
     port = 7001 # 指定服务端口
     print(f"🚀 My Utility MCP 服务即将启动于 http://localhost:{port}")
     
     # create_fastapi_app 会将   FastMCP 实例转换为一个 FastAPI 应用
     app = create_fastapi_app(utility_mcp)
     
     # 使用 uvicorn 运行 FastAPI 应用
     # 这部分代码会阻塞,直到服务停止   (例如按 Ctrl+C)
     uvicorn.run(app,   host="0.0.0.0", port=port)

这里我们通过 FastMCP(…) 初始化了一个 MCP 服务,name、description、version 是服务的元数据。@utility_mcp.tool(…) 是定义 MCP 工具的关键装饰器。其中,name是工具的唯一标识符,代理将通过这个名称调用工具;description是对工具功能的自然语言描述。这个描述非常重要,因为 AI 代理(尤其是 LLM 驱动的代理)会根据这个描述来理解工具的用途,并决定在何时调用它。

测试MCP工具服务

通过 uv run mcp_server.py 或者 python mcp_server.py 启动刚刚创建的 MCP 工具服务,能看到类似后面的输出:

🚀 自定义   MCP 服务即将启动于 http://localhost:7001
INFO:     Started server process   [10040]
INFO:     Waiting for application   startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:7001   

这意味着我们的 MCP 工具服务已经在 http://localhost:7001 上成功部署。

你可以打开浏览器访问 http://localhost:7001/tools 来查看已注册工具的列表(JSON格式),或者访问 http://localhost:7001/openapi.json (或 /docs /redoc) 查看 OpenAPI 文档。

请保持这个终端窗口的运行,因为我们的 A2A 代理稍后需要连接到这个服务。

第二部分:构建简单的 A2A 代理

现在我们有了一个正在运行的 MCP 工具服务,接下来我们将创建一个 A2A 代理,让它能够使用这些定义的工具。(参考文件 a2a_agent_simple.py)

 # a2a_agent_simple.py
 from python_a2a import A2AServer, run_server, TaskStatus, TaskState,   AgentCard, AgentSkill
 import requests # 用于直接调用 MCP 工具
 import re
 
 # A2A 代理的配置
 AGENT_PORT = 7000 # A2A 代理监听的端口
 MCP_SERVER_URL = "http://localhost:7001" # 我们之前启动的   MCP 工具服务的地址
 
 class MyToolAgent(A2AServer):
     def __init__(self, agent_card,   mcp_url):
           super().__init__(agent_card=agent_card)
         self.mcp_url = mcp_url
         print(f"🛠️   MyToolAgent 初始化完成,将连接到 MCP 服务:   {self.mcp_url}")
 
     def _call_mcp_tool(self, tool_name,   params):
         """一个辅助方法,用于调用 MCP 工具"""
         if not self.mcp_url:
             return "错误:MCP 服务地址未配置。"
         
         tool_endpoint =   f"{self.mcp_url}/tools/{tool_name}"
         try:
             print(f"📞 正在调用 MCP 工具: {tool_endpoint},参数: {params}")
             response =   requests.post(tool_endpoint, json=params, timeout=10)
             response.raise_for_status()   # 如果 HTTP 状态码是 4xx 或 5xx,则抛出异常
             
             tool_response_json =   response.json()
             print(f"工具响应JSON: {tool_response_json}")
 
             # 从   MCP 响应中提取文本内容
             # MCP 响应通常在   content -> parts -> text
             if tool_response_json.get("content"):
                 parts =   tool_response_json["content"]
                 if isinstance(parts,   list) and len(parts) > 0 and "text" in parts[0]:
                     return   parts[0]["text"]
             return "工具成功执行,但未找到标准文本输出。"
 
         except   requests.exceptions.RequestException as e:
             error_msg = f"调用 MCP 工具 {tool_name} 失败: {e}"
             print(f"❌   {error_msg}")
             return error_msg
         except Exception as e_json: #   requests.post 成功,但响应不是期望的json或json结构不对
             error_msg = f"解析 MCP 工具 {tool_name} 响应失败: {e_json}"
             print(f"❌   {error_msg}")
             return error_msg
 
     def handle_task(self, task):
         """处理接收到的任务 (A2A 消息)"""
         message_data = task.message or   {}
         content =   message_data.get("content", {})
         text =   content.get("text", "").lower() # 用户输入的文本,转为小写方便匹配
         
         print(f"📨 收到任务: {text}")
         
         response_text = "抱歉,我不太明白您的意思。您可以尝试问我:\\n" \
                         "- '计算 123 + 456'\\n" \
                         "- '现在几点了?'"
 
         # 1. 尝试匹配计算任务
         expression = None
         # 模式1: 表达式在 "等于多少" 前面, e.g., "5+3等于多少"
         match_equals =   re.search(r"(.+?)\s*等于多少", text)
         if match_equals:
             expression =   match_equals.group(1).strip()
         else:
             # 模式2:   表达式在 "计算" 或 "算一下" 后面,   e.g., "计算 5+3"
             # 使用非捕获组   (?:...) 来匹配关键字,并捕获后面的表达式。
             # re.search 会在字符串中寻找匹配,所以 "帮我计算 5+3" 也能正常工作。
             match_action =   re.search(r"(?:算一下|计算)\s+(.+)",   text)
             if match_action:
                 expression =   match_action.group(1).strip()
 
         if expression:
             # 进一步清理表达式,以处理   "计算 5+3 等于多少" 这样的混合情况
             expression =   expression.split("等于多少")[0].strip()
             if expression: # 确保清理后还有表达式
                 print(f"识别到计算任务,表达式: '{expression}'")
                 response_text =   self._call_mcp_tool("calculator", {"expression":   expression})
             else:
                 response_text = "请输入要计算的数学表达式,例如:'计算 8*(9+4)'"
         
         # 2. 尝试匹配时间查询任务
         # 使用更广泛的关键字匹配时间/日期查询
         time_keywords = ["几点", "几点了", "时间", "日期"]
         if any(keyword in text for   keyword in time_keywords):
             print("识别到时间查询任务")
             response_text =   self._call_mcp_tool("get_current_time", {})
 
         # 3. 匹配天气查询
         weather_keywords = ["天气", "气温"]
         # 正则匹配识别城市,例如   "Shenzhen天气" 中的   "Shenzhen"
         city_match =   re.search(r"([\u4e00-\u9fa5a-zA-Z]+)的?天气", text)
         city = city_match.group(1) if   city_match else None
         if any(keyword in text for   keyword in weather_keywords):
             print("识别到天气查询任务")
             response_text =   self._call_mcp_tool("get_current_weather", {"city":   city})
 
         # 构建   A2A 响应
         task.artifacts =   [{"parts": [{"type": "text", "text":   str(response_text)}]}] # 确保是字符串
         task.status =   TaskStatus(state=TaskState.COMPLETED)
         print(f"📤 回复任务: {response_text}")
         return task
 
 if __name__ == "__main__":
     # 定义代理的   "名片"
     agent_card = AgentCard(
         name="My Tool-Powered   Agent",
         description="一个可以使用计算器和时间查询工具的   A2A 代理",
           url=f"http://localhost:{AGENT_PORT}", # 代理自己的访问地址
         version="1.0.0",
         skills=[ # 列出代理具备的技能
             AgentSkill(
                 name="Calculator   Skill",
                 description="执行数学计算",
                 examples=["计算 20 * 4 + 5", " 13 * (5 + 2) 等于多少?"]
             ),
             AgentSkill(
                 name="Time   Skill",
                 description="查询当前时间和日期",
                 examples=["现在几点了?", "今天是什么日期?"]
             ),
             AgentSkill(
                 name="Weather   Skill",
                 description="查询当前天气",
                 examples=["深圳天气如何?", "北京气温多少?"]
             )
         ]
     )
 
     my_agent = MyToolAgent(agent_card,   MCP_SERVER_URL)
     
     print(f"🚀 My A2A Agent   即将启动于 http://localhost:{AGENT_PORT}")
     print(f"🔗 它将连接到 MCP 服务于   {MCP_SERVER_URL}")
     
     # run_server 会启动一个   Flask (默认) 或 FastAPI 服务器来托管 A2A 代理
     # 这部分代码也会阻塞
     run_server(my_agent,   host="0.0.0.0", port=AGENT_PORT)

此处 MyToolAgent(A2AServer) 这个代理类继承自 python_a2a.A2AServer。构造函数init()接收一个 agent_card(代理的元数据)和 mcp_url (MCP 服务的地址)。

_call_mcp_tool(…) 是一个自定义的辅助方法,封装了通过 HTTP POST 请求调用 MCP 工具的逻辑。它构造了工具的完整 URL (例如 http://localhost:7001/tools/calculator),使用 requests.post() 发送 JSON 数据,解析 MCP 服务返回的 JSON 响应,并提取其中的文本内容。

handle_task(self, task) 是 A2A 代理的核心方法,每当代理收到一个任务(通常是一个用户消息),这个方法就会被调用,通过task.message.content.text 获取用户发送的文本消息,然后判断用户的意图。在a2a_agent_simple.py这个例子中,我们通过简单的关键字匹配和正则表达式来判断用户的意图。

  • 如果检测到用户意图是计算,就调用 MCP 技能库中的 calculator 工具。

  • 如果检测到用户意图是查询时间,就调用 MCP 技能库的 get_current_time 工具。

在handle_task方法的最后,通过task.artifacts = […] 设置任务的产出物,这里是将工具的响应或默认回复包装成 A2A 协议要求的格式。task.status = TaskStatus(state=TaskState.COMPLETED) 则将任务状态标记为已完成。

在主程序中,AgentCard 和 AgentSkill用于定义代理的元数据,如名称、描述、它提供的技能等。这对于代理发现和服务描述很重要。

测试A2A Agent(Simple)

下面我们来运行并测试这个简单A2A代理。

首先确保 mcp_server.py (MCP 工具服务) 仍在7001端口运行,然后打开一个新的终端窗口,导航到 a2a_agent_simple.py 文件所在的目录,然后运行uv run a2a_agent_simple.py 或python a2a_agent_simple.py。

🛠️ MyToolAgent 初始化完成,将连接到 MCP 服务:   http://localhost:7001
🚀 My A2A Agent 即将启动于 http://localhost:7000
🔗 它将连接到 MCP 服务于   http://localhost:7001
Starting A2A server on http://0.0.0.0:7000/a2a
Google A2A compatibility: Enabled
* Serving Flask app   'python_a2a.server.http'
* Debug mode: off
INFO:werkzeug:WARNING: This is a development server. Do not use it   in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:7000
* Running on http://【本机在局域网的ip地址】:7000

输出显示,我们的 A2A 代理已经在 http://localhost:7000 上成功运行了。我们打开 http://localhost:7000,可以看到A2A给我们提供了一个dashboard,Agent Information 是我们定义的Agent Card,Available Skills 这里是可用工具。

下面创建一个jupyter notebook,使用 python_a2a 库提供的 A2AClient 功能来测试A2A Agent。

首先确保我们的虚拟环境支持Jupyter Notebook运行。

在 mcp+a2a 目录下,先激活 .venv:

source .venv/bin/activate

在 .venv 环境下,安装 ipykernel 到当前虚拟环境:

install ipykernel

注册当前虚拟环境为 Jupyter 内核。

python -m ipykernel install --user --name a2a-venv --display-name "Python 3 (a2a-mcp)"

其中:

  • –name 是内核的唯一标识(随便取,建议用项目名相关)。

  • –display-name 是你在 Jupyter Notebook 里看到的名字。

选择当前虚拟环境来运行notebook,在新建的notebook中定义测试端口:

 from python_a2a.client import   A2AClient
 
 agent_url = "http://localhost:7000"
 # 创建客户端,指向你运行的代理服务地址
 client = A2AClient(agent_url)

并测试计算器功能:

 response = client.ask("3*100+20等于多少")
 
 print(response)

输出如下:

此时可以在部署 MCP 工具的终端看到调用了 calculator 工具的提示。

INFO:     127.0.0.1:XXXX - "POST   /tools/calculator HTTP/1.1" 200 OK

而在部署 A2A 服务的终端,可以看到中间的过程提示。

📨 收到任务:   从1加到100等于多少
   识别到计算任务,表达式: '从1加到100'
   📞 正在调用 MCP 工具:   http://localhost:7001/tools/calculator,参数:   {'expression': '从1加到100'}     
   工具响应JSON: {'content': [{'type': 'text', 'text':   "计算错误 '从1加到100': name '从1加到100'   is not defined"}], 'isError': False}
   📤 回复任务: 计算错误 '从1加到100': name '从1加到100' is not defined
   INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 14:37:10] "POST /tasks/send   HTTP/1.1" 200 -
   📨 收到任务: 3*100+20等于多少
   识别到计算任务,表达式: '3*100+20'
   📞 正在调用 MCP 工具:   http://localhost:7001/tools/calculator,参数:   {'expression': '3*100+20'}       
   工具响应JSON: {'content': [{'type': 'text', 'text': '计算结果: 3*100+20 = 320'}], 'isError': False}
   📤 回复任务: 计算结果:   3*100+20 = 320
   INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 14:37:16] "POST /tasks/send   HTTP/1.1" 200 -

我们继续测试查询时间的功能。

 response = client.ask("现在是什么时间?")
 print(response)

输出如下。

如果一切顺利的话,我们就能在部署 MCP 工具的终端看到调用了定义的 get_current_time 工具并返回当前系统时间。

INFO:     127.0.0.1:XXXX - "POST   /tools/get_current_time HTTP/1.1" 200 OK

而在部署 A2A 服务的终端,可以看到中间的过程提示。

   📨 收到任务:   现在是什么时间?
   识别到时间查询任务
   📞 正在调用 MCP 工具:   http://localhost:7001/tools/get_current_time,参数: {}
   工具响应JSON: {'content': [{'type': 'text', 'text': '当前日期: 2025-06-28\\n当前时间:   15:44:36\\n星期几: Saturday'}], 'isError': False}
   📤 回复任务: 当前日期:   2025-06-28\n当前时间: 15:44:36\n星期几:   Saturday
   INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 15:44:36] "POST /tasks/send   HTTP/1.1" 200 -

继续测试查询天气的功能。这里需要传入城市英文名称。

 response =   client.ask("Shenzhen天气")
 print(response)

输出如下。

在部署 MCP 工具的终端,我们也会看到调用了定义的get_current_weather工具并返回要查询的城市的天气。

INFO:     127.0.0.1:XXXXX - "POST   /tools/get_current_weather HTTP/1.1" 200 
OK

而在部署 A2A 服务的终端,可以看到中间的过程提示。

📨 收到任务:   深圳天气
   识别到天气查询任务
   📞 正在调用 MCP 工具:   http://localhost:7001/tools/get_current_weather,参数:   {'city': '深圳'}          
   工具响应JSON: {'content': [{'type': 'text', 'text': '获取深圳天气失败:city not found'}], 'isError': False}
   📤 回复任务: 获取深圳天气失败:city   not found
   INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 17:35:26] "POST /tasks/send   HTTP/1.1" 200 -
   📨 收到任务: shenzhen天气
   识别到天气查询任务
   📞 正在调用 MCP 工具:   http://localhost:7001/tools/get_current_weather,参数:   {'city': 'shenzhen'}    
   工具响应JSON: {'content': [{'type': 'text', 'text':   'shenzhen当前天气是 阴,多云,温度为 28.63°C,体 感温度为   32.98°C'}], 'isError': False}
   📤 回复任务: shenzhen当前天气是 阴,多云,温度为   28.63°C,体感温度为 32.98°C
   INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 17:35:32] "POST /tasks/send   HTTP/1.1" 200 -

至此,通过A2A 与 MCP 的协同工作机制,建成了可调用外部 MCP 工具的 A2A 代理。

第三部分:让 A2A 代理更加智能

尽管上述的简单A2A代理已经实现了A2A+MCP架构的雏形,但是它依赖于简单的关键字匹配来决定调用哪个工具,并且直接返回工具的结果。但很多情况下,这个程度的智能并不理想,例如:

  • 用户倾向于使用自然语言表达需求,仅依赖关键词匹配可能无法准确理解其意图(例如计算“从1加到100”)。

  • 当前的参数匹配基于正则表达式,但实际应用中需要更精确的参数提取方法(例如无法提取中文城市名直接用作获取天气的参数)。

  • 用户的需求可能涉及多个工具协同工作,或要求对返回结果进行额外处理和语义解释。

这时,就轮到大语言模型出场了!你可以将 LLM集成到我们的 A2A 代理中,让它充当“翻译官”,来理解用户意图、选择合适的工具(如果需要)、调用工具、传入合适参数(如有),并以更自然、更智能的方式回复用户。—— 此时,LLM就相当于是一个A2A架构中的“调度Agent”。

现在,我们来创建一个新的程序 a2a_agent_advanced.py,它将结合LLM和我们之前创建的 MCP 工具。

 # a2a_agent_advanced.py
 from   python_a2a import A2AServer, run_server, TaskStatus, TaskState, AgentCard,   AgentSkill
 import   requests
 import re
 import os
 import   json
 import time
 from   openai import OpenAI
 from   dotenv import load_dotenv
 
 # 加载 .env 文件中的环境变量
 load_dotenv()
 
 # --- 配置 ---
 AGENT_PORT   = 7002
 MCP_SERVER_URL   = "http://localhost:7001" # 我们的 MCP 工具服务
 OPENAI_API_KEY   = os.getenv("OPENAI_API_KEY")
 OPENAI_BASE_URL   = "https://api.openai.com/v1"
 OPENAI_MODEL   = "gpt-4o" # 或者其他模型
 
 # 初始化 OpenAI 客户端
 openai_client   = OpenAI(
     api_key=OPENAI_API_KEY,
     base_url=OPENAI_BASE_URL,
 )
 
 class   OpenAIEnhancedAgent(A2AServer):
     def __init__(self, agent_card, mcp_url):
         super().__init__(agent_card=agent_card)
         self.mcp_url = mcp_url
         print(f"🤖   OpenAIEnhancedAgent 初始化,MCP 服务:   {self.mcp_url}")
 
     def _call_mcp_tool(self, tool_name,   params):
         """一个辅助方法,用于调用   MCP 工具"""
         if not self.mcp_url:
             return "错误:MCP   服务地址未配置。"
         
         tool_endpoint =   f"{self.mcp_url}/tools/{tool_name}"
         try:
             print(f"📞 正在调用 MCP 工具: {tool_endpoint},参数: {params}")
             response =   requests.post(tool_endpoint, json=params, timeout=10)
             response.raise_for_status() # 如果 HTTP 状态码是 4xx 或   5xx,则抛出异常
             
             tool_response_json =   response.json()
             print(f"工具响应JSON:   {tool_response_json}")
 
             # 从 MCP 响应中提取文本内容
             # MCP 响应通常在   content -> parts -> text
             if   tool_response_json.get("content"):
                 parts =   tool_response_json["content"]
                 if isinstance(parts, list)   and len(parts) > 0 and "text" in parts[0]:
                     return   parts[0]["text"]
             return "工具成功执行,但未找到标准文本输出。"
 
         except   requests.exceptions.RequestException as e:
             error_msg = f"调用 MCP 工具 {tool_name} 失败: {e}"
             print(f"❌ {error_msg}")
             return error_msg
         except Exception as e_json: #   requests.post 成功,但响应不是期望的json或json结构不对
             error_msg = f"解析 MCP 工具 {tool_name} 响应失败: {e_json}"
             print(f"❌ {error_msg}")
             return error_msg
 
     def _get_openai_response(self,   text_prompt, tools=None, max_iterations=5):
         """调用   OpenAI API 获取回复"""
         messages = [{
             "role":   "user",
             "content": text_prompt
         }]
 
         for i in range(max_iterations):
             try:
                 response =   openai_client.chat.completions.create(
                     model=OPENAI_MODEL,
                     messages=messages,
                     max_tokens=1500,
                     tools=tools if tools else   [],
                     tool_choice='auto' if   tools else None
                 )
                 
                 tool_calls =   response.choices[0].message.tool_calls
                 return {
                     "message":   response.choices[0].message.content,
                     "tool_calls":   tool_calls,
                     "usage":   response.usage
                 }
             except Exception as e:
                 print(f"❌ OpenAI API调用失败: {e}")
                 time.sleep(0.1)
                 if i == max_iterations - 1:
                     raise   Exception("OpenAI API调用多次失败")
 
     def handle_task(self, task):
         message_data = task.message or {}
         content =   message_data.get("content", {})
         user_text =   content.get("text", "") # 用户原始输入
         
         print(f"📨 (OpenAI Agent)   收到任务: '{user_text}'")
         
         # 定义可用的工具
         tools = [
             {
                 "type":   "function",
                 "function": {
                     "name":   "calculator",
                     "description":   "执行数学计算",
                     "parameters": {
                         "type":   "object",
                           "properties": {
                               "expression": {
                                   "type": "string",
                                   "description": "要计算的数学表达式"
                             }
                         },
                         "required":   ["expression"]
                     }
                 }
             },
             {
                 "type":   "function",
                 "function": {
                     "name":   "get_current_time",
                     "description":   "获取的当前本地的时间",
                     "parameters": {
                         "type":   "object",
                           "properties": {}
                     }
                 }
             },
             {
                 "type":   "function",
                 "function": {
                     "name":   "get_current_weather",
                     "description":   "获取指定城市的天气信息(请使用英文城市名)",
                     "parameters": {
                         "type":   "object",
                           "properties": {
                             "city":   {
                                   "type": "string",
                                   "description": "要查询天气的城市名称(必须使用英文,如:Beijing,   Tokyo, New York)"
                             }
                         },
                         "required":   ["city"]
                     }
                 }
             }
         ]
 
         # 让OpenAI选择工具和补全参数
         try:
             response = self._get_openai_response(
                 text_prompt=user_text,
                 tools=tools
             )
 
             tool_calls =   response.get("tool_calls")
             final_response = ""
             tool_result_for_openai =   ""
 
             if tool_calls:
                 # 执行工具调用
                 for tool_call in tool_calls:
                     function_name =   tool_call.function.name
                     function_args =   json.loads(tool_call.function.arguments)    # 使用json.loads替代eval
                     
                     tool_result =   self._call_mcp_tool(function_name, function_args)
                     tool_result_for_openai +=   f"使用{function_name}工具,参数是{function_args},结果是:'{tool_result}'。\n"
 
                 # 让OpenAI基于工具结果生成最终回复
                 prompt_for_openai = f"用户问:'{user_text}'。\n我已经调用了工具,结果如下:\n{tool_result_for_openai}\n请基于这些信息,以友好和清晰的方式回答用户。"
                 final_response =   self._get_openai_response(prompt_for_openai,   tools=None).get("message")
             else:
                 # 如果OpenAI认为不需要使用工具,直接生成回复
                 final_response =   response.get("message")
 
         except Exception as e:
             print(f"❌ OpenAI API调用失败: {e}")
             # 如果OpenAI调用失败,退回到直接对话模式
             final_response =   self._get_openai_response(user_text, tools=None).get("message")
 
         task.artifacts = [{"parts":   [{"type": "text", "text": final_response}]}]
         task.status =   TaskStatus(state=TaskState.COMPLETED)
         print(f"📤 (OpenAI Agent)   回复任务: '{final_response}'")
         return task
 
 if   __name__ == "__main__":
     agent_card = AgentCard(
         name="LLM Enhanced   Assistant",
         description="一个由   LLM 驱动,并能使用外部工具的智能助手",
           url=f"http://localhost:{AGENT_PORT}",
         version="1.1.0",
         skills=[
               AgentSkill(name="Conversational AI", description="通过 OpenAI 大模型进行自然语言对话"),
               AgentSkill(name="Calculator", description="执行数学计算"),
             AgentSkill(name="Time   Service", description="查询当前时间和日期"),
             AgentSkill(name="Weather   Service", description="查询指定城市的天气")
         ]
     )
 
     openai_agent =   OpenAIEnhancedAgent(agent_card, MCP_SERVER_URL)
     
     print(f"🚀 OpenAI Enhanced   A2A Agent 即将启动于 http://localhost:{AGENT_PORT}")
     print(f"🔗 它将连接到   MCP 服务于 {MCP_SERVER_URL}")
     print(f"🧠 它将使用   OpenAI 模型: {OPENAI_MODEL}")
     
     # 启动服务,这会阻塞当前终端
     # 建议在实际部署时,MCP 服务和 A2A Agent 服务分别在不同的进程或服务器上运行
     run_server(openai_agent,   host="0.0.0.0", port=AGENT_PORT)

代码稍微长了一点,但其实很好理解。核心的改进位于handle_task(…) 中,通过意图识别与工具触发,我们不再依赖固定关键字匹配,而是借助 gpt-4o 的函数调用能力,根据用户输入自动判断是否需要调用工具。gpt-4o 会解析输入内容,自主选择合适的工具(如计算器、时间查询、天气服务)并提取参数。

若触发工具调用,系统会将用户原始问题与工具返回结果拼接为增强提示(prompt_for_openai),再发送给 gpt-4o 生成最终回答。gpt-4o 会结合上下文与数据,生成自然、连贯、具备语义理解的响应(如:“北京今天多云,气温 25°C,建议带伞。”)。

测试A2A Agent(Advanced)

下面运行 A2A 代理(advanced),并进行测试。

首先确保 mcp_server.py(MCP 工具服务)仍在运行(监听在 http://localhost:7001)。

打开一个新的终端,导航到 a2a_agent_advanced.py 文件所在的目录,然后运行python a2a_agent_advanced.py。

代理启动,并监听 http://localhost:7002 端口。

 🤖 OpenAIEnhancedAgent 初始化,MCP 服务: http://localhost:7001
 🚀 OpenAI Enhanced A2A Agent 即将启动于   http://localhost:7002
 🔗 它将连接到 MCP 服务于 http://localhost:7001
 🧠 它将使用 OpenAI 模型:   gpt-4o
 Starting A2A server on http://0.0.0.0:7002/a2a
 Google A2A compatibility: Enabled
  * Serving Flask app   'python_a2a.server.http'
  * Debug mode: off
 INFO:werkzeug:WARNING: This is a development server. Do not use it in a   production deployment. Use a production WSGI server instead.
  * Running on all addresses (0.0.0.0)
  * Running on http://127.0.0.1:7002
  * Running on http://192.168.1.1:7002
 INFO:werkzeug:Press CTRL+C to quit

保持MCP端口监听。我们继续通过notebook,使用 A2AClient测试,现在我们连接到7002端口。

 from python_a2a.client import   A2AClient
 
 agent_url = "http://localhost:7002"
 # 创建客户端,指向你运行的代理服务地址
 client = A2AClient(agent_url)

输出如下:

 INFO:werkzeug:127.0.0.1 - -   [28/Jun/2025 18:09:46] "GET /.well-known/agent.json HTTP/1.1" 200 -

测试计算器功能:

response =   client.ask("3*100+20等于多少")
print(response)

在部署 MCP 工具的终端可以看到调用了 calculator 工具的提示。

 INFO:     127.0.0.1:XXXX - "POST   /tools/calculator HTTP/1.1" 200 OK
 INFO:     127.0.0.1:YYYY - "POST /tools/calculator   HTTP/1.1" 200 OK

在部署 A2A 服务的终端看到中间的过程提示如下。

 📨 (OpenAI Agent) 收到任务: '从1加到200等于多少'
 INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"     
 📞 正在调用 MCP 工具:   http://localhost:7001/tools/calculator,参数:   {'expression': '200*(200+1)/2'}
 工具响应JSON: {'content': [{'type': 'text', 'text': '计算结果: 200*(200+1)/2 = 20100.0'}], 'isError': False}
 INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"     
 📤 (OpenAI Agent) 回复任务: '从1加到200的总和等于20100。'
 INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 18:23:30] "POST /tasks/send   HTTP/1.1" 200 -
 📨 (OpenAI Agent) 收到任务: '3*100+20等于多少'
 INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
 📞 正在调用 MCP 工具:   http://localhost:7001/tools/calculator,参数:   {'expression': '3*100+20'}
 工具响应JSON: {'content': [{'type': 'text', 'text': '计算结果: 3*100+20 = 320'}], 'isError': False}
 INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
 📤 (OpenAI Agent) 回复任务: '答案是320。您的计算3*100+20等于320。'
 INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 18:28:56] "POST /tasks/send   HTTP/1.1" 200 -

这里GPT大模型理解了用户意图,并且调用MCP工具,传入等差数列的计算公式来计算1到200的加和。在输出时的回复也不同于之前简单测试A2A时模板化的定义。

继续测试查询时间的功能:

response = client.ask("现在是什么时间?")
print(response)

终端调用了定义的 get_current_time 工具并返回当前系统时间。

Bash
   📨 (OpenAI Agent) 收到任务: '现在几点了'
   INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
   📞 正在调用 MCP 工具:   http://localhost:7001/tools/get_current_time,参数: {}
   工具响应JSON: {'content': [{'type': 'text', 'text': '当前日期: 2025-06-28\\n当前时间:   18:41:50\\n星期几: Saturday'}], 'isError': False}
   INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
   📤 (OpenAI Agent) 回复任务: '现在是2025年6月28日星期六,时间为晚上18点41分。'
   INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 18:41:52] "POST /tasks/send   HTTP/1.1" 200 -
   📨 (OpenAI Agent) 收到任务: '今天是几号?昨天什么日期?明天什么日期?'
   INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
   📞 正在调用 MCP 工具:   http://localhost:7001/tools/get_current_time,参数: {}
   工具响应JSON: {'content': [{'type': 'text', 'text': '当前日期: 2025-06-28\\n当前时间:   18:42:00\\n星期几: Saturday'}], 'isError': False}
   INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
   📤 (OpenAI Agent) 回复任务: '今天的日期是2025年6月28日,是星期六。昨天是2025年6月27日,而明天将会是2025年6月29日。'
   INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 18:42:02] "POST /tasks/send   HTTP/1.1" 200 -

继续测试查询天气的功能。这里GPT正确地从用户输入中,识别到了需要作为参数传入的城市名称。

 # Test Case 1
 response = client.ask("帮我查东京的温度")
 print(response)
 
 # Test Case 2
 response = client.ask("深圳现在多少度?")
 print(response)

Agent调用了定义的 get_current_weather 工具,传入了正确形式的参数,并返回要查询的城市的天气。最后,它还额外给出了适当的出行建议。

📨 (OpenAI Agent) 收到任务: '帮我查东京的温度'
   INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
   📞 正在调用 MCP 工具:   http://localhost:7001/tools/get_current_weather,参数:   {'city': 'Tokyo'}        
   工具响应JSON: {'content': [{'type': 'text', 'text':   'Tokyo当前天气是 阴,多云,温度为 28.69°C,体感温 度为   29.73°C'}], 'isError': False}
   INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
   📤 (OpenAI Agent) 回复任务: '东京当前的天气情况是阴天并伴有多云,温度在28.69°C左右,感觉起来大概在29.73°C。请根据实际情况适当添加衣物,保持舒适哦。'
   INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 18:44:49] "POST /tasks/send   HTTP/1.1" 200 -
   📨 (OpenAI Agent) 收到任务: '深圳现在多少度?'
   INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
   📞 正在调用 MCP 工具:   http://localhost:7001/tools/get_current_weather,参数:   {'city': 'Shenzhen'}
   工具响应JSON: {'content': [{'type': 'text', 'text':   'Shenzhen当前天气是 阴,多云,温度为 31.64°C,体感温度为   38.64°C'}], 'isError': False}
   INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions   "HTTP/1.1 200 OK"
   📤 (OpenAI Agent) 回复任务: '深圳现在的天气状况是阴天,多云,当前温度为31.64°C,但体感温度达到了38.64°C,请注意防晒和补水,保持舒适哦。'
   INFO:werkzeug:127.0.0.1 - - [28/Jun/2025 18:46:23] "POST /tasks/send   HTTP/1.1" 200 -

总结一下

今天又是一节实操性极强的动手实践课,我们学习并实践了:

  • A2A 与 MCP 的核心概念及协作方式:A2A 负责多智能体协作通信,MCP 提供工具调用能力,二者结合实现感知与行动闭环。

  • 使用 python-a2a 开源包构建了 MCP 工具服务(如计算与时间查询)和可调用这些工具的 A2A 代理。

  • 集成大语言模型,使代理具备理解意图、智能调用工具、自然生成回复的能力。

其中,A2A + MCP + LLM的组合,是构建具备执行力与协作能力智能体系统的关键。

思考题

  1. 把OpenAI模型改成DeepSeek等国内模型,并添加其它的实用工具。

  2. 添加多轮对话的上下文管理和控制机制,实现支持长对话与状态管理、流式响应与错误容错等机制。

  3. 这个示例中我们使用的是开源社区的python-a2a包,你能否用 A2A 和 MCP 官方包来实现类似的应用程序框架。

期待你在留言区分享你的思考或者疑问,如果这节课对你有启发,别忘了分享给身边更多朋友!

精选留言

  • Geek_159499

    2025-07-23 10:09:28

    这篇文章主要说了Agent如何实现,Agent与Agent之间如何注册和发现呢,如何调用呢?
  • Geek_098006

    2025-07-21 22:58:34

    老师,请问,LangGraph里调用了MCPtools,然后再使用A2A协议实现多agent系统这样的架构是否合理
    作者回复

    我觉得不仅合理,而且恰恰是一种典型的AI大模型设计模式,是一种非常清晰、可维护、可扩展的混合架构,适合大型、复杂的 AI Agent 系统。

    2025-07-22 10:54:30