ADK Python 实战:将远程 A2A 代理作为根代理(A2A Root Sample 全解析)
2026/9/13 13:23:05 网站建设 项目流程

ADK Python 实战:将远程 A2A 代理作为根代理(A2A Root Sample 全解析)

【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python

本文以 Agent Development Kit(ADK,Python)官方示例a2a_root为核心,完整讲解如何**把一个远程 Agent-to-Agent(A2A)服务作为主代理(root agent)**来驱动整个会话:远端用 uvicorn 直接启动一个基于to_a2a()转换的 A2A 服务,本地用RemoteA2aAgent以 Agent Card 方式接入。读完本文,你将掌握「标准 ADK 代理 → A2A 服务 → 远程根代理」这条分布式部署链路的完整落地方法,并能基于仓库源码理解其底层机制。

示例定位与整体架构

a2a_root示例位于 contributing/samples/a2a/a2a_root,其核心思路是:主代理本身并不包含业务逻辑,而是一个远程 A2A 代理的代理(proxy)。整个示例由两个进程组成:

角色文件职责
根代理(Root Agent)contributing/samples/a2a/a2a_root/agent.py一个RemoteA2aAgent,通过 A2A 协议连接远端服务,充当本地入口
远程 Hello World 代理contributing/samples/a2a/a2a_root/remote_a2a/hello_world/agent.py真正执行掷骰子、素数检查等逻辑的代理,运行在独立服务器上

原文档给出的架构示意如下:

┌─────────────────┐ ┌────────────────────┐ │ Root Agent │───▶│ Remote Hello │ │ (RemoteA2aAgent)│ │ World Agent │ │ (localhost:8000)│ │ (localhost:8001) │ └─────────────────┘ └────────────────────┘
  • 本地根代理由adk web服务承载,监听localhost:8000
  • 远程 A2A 代理由 uvicorn 承载,监听localhost:8001
  • 两者之间通过 A2A 协议通信,根代理先读取远端的 Agent Card(机器可读的代理能力描述),再把用户任务以 JSON-RPC 形式投递过去。

这种「远程代理即主代理」的模式展示了 A2A 架构在分布式代理部署上的灵活性:你可以把一个团队开发的代理部署为独立 Web 服务,然后在任意位置以极小配置量复用它。

四大关键特性

原文档将本示例的价值提炼为四点,逐一展开:

1. 远程 A2A 作为根代理

根代理是RemoteA2aAgent,它不运行本地 LLM 推理,而是把请求转发给远程 A2A 服务。这演示了如何用远程代理替代本地代理作为主代理,是 A2A 分布式部署能力最直观的体现。关于RemoteA2aAgent的客户端机制,详见后文「RemoteA2aAgent 客户端机制」一节。

2. Uvicorn 服务器部署

远程代理使用 uvicorn(轻量级 ASGI 服务器)启动,而不是 ADK CLI。这给出了一种不依赖 ADK 命令行、把 A2A 代理暴露为独立 Web 服务的简洁部署路径:

uvicorn contributing.samples.a2a.a2a_root.remote_a2a.hello_world.agent:a2a_app \ --host localhost --port 8001

3. 代理业务能力

远程代理具备四类能力,覆盖了函数工具、异步工具、状态管理与并行调用的典型用法:

  • 掷骰子(Dice Rolling):可配置骰子面数的roll_die工具;
  • 素数检查(Prime Number Checking):批量判断数字是否为素数的check_prime异步工具;
  • 状态管理(State Management):在工具上下文中维护滚动历史记录(tool_context.state['rolls']);
  • 并行工具执行(Parallel Tool Execution):代理被指令引导为「一次请求、同一轮内」并行调用多个工具。

4. 极简部署模式

远程侧使用to_a2a()工具函数,把一个标准 ADK 代理转换为 A2A 服务,无需额外配置即可完成远程代理的部署。to_a2a的完整参数与底层原理见后文专节。

环境准备与启动步骤

前置条件

按原文档,需要依次启动两个服务:

第一步:启动远程 A2A 代理服务器(终端一):

# Start the remote agent using uvicorn uvicorn contributing.samples.a2a.a2a_root.remote_a2a.hello_world.agent:a2a_app --host localhost --port 8001

其中a2a_app是模块内通过to_a2a(root_agent, port=8001)创建的 Starlette 应用(详见源码 remote_a2a/hello_world/agent.py)。注意:此处模块路径必须以仓库根目录为基准且大小写精确,--port 8001必须与根代理 Agent Card 中记录的端口一致。

第二步:运行主代理(另开一个终端):

# In a separate terminal, run the adk web server adk web contributing/samples/a2a

adk web会以 Web UI 方式启动本地根代理,指定目录为contributing/samples/a2a(即包含所有 A2A 示例的父目录),根代理读取 agent.py 中定义的RemoteA2aAgent

交互示例

两个服务就绪后,即可与根代理对话(原文档示例完整保留):

简单掷骰子:

User: Roll a 6-sided die Bot: I rolled a 4 for you.

素数检查:

User: Is 7 a prime number? Bot: Yes, 7 is a prime number.

组合操作(掷骰子 + 素数判断):

User: Roll a 10-sided die and check if it's prime Bot: I rolled an 8 for you. Bot: 8 is not a prime number.

多次掷骰并筛选素数:

User: Roll a die 3 times and check which results are prime Bot: I rolled a 3 for you. Bot: I rolled a 7 for you. Bot: I rolled a 4 for you. Bot: 3, 7 are prime numbers.

最后一次交互展示了状态管理的能力:roll_die把每次结果追加到工具上下文状态中,check_prime可以基于历史滚动结果批量判断。

代码结构深度解析

根代理(agent.py):RemoteA2aAgent 的极简用法

contributing/samples/a2a/a2a_root/agent.py 全文件只有一次构造调用:

from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH from google.adk.agents.remote_a2a_agent import RemoteA2aAgent root_agent = RemoteA2aAgent( name="hello_world_agent", description=( "Helpful assistant that can roll dice and check if numbers are prime." ), agent_card=f"http://localhost:8001/{AGENT_CARD_WELL_KNOWN_PATH}", )

关键点:

  • agent_card参数指向远端 well-known 端点AGENT_CARD_WELL_KNOWN_PATH由 src/google/adk/agents/remote_a2a_agent.py 导出,优先从a2a.utils.constants导入,旧版 a2a-sdk 兜底为"/.well-known/agent.json"。根代理正是通过这个 URL 拉取远端代理的「名片」。
  • description会被父代理用于构建转移指令:从源码看,若传入的 Agent Card 本身带描述而description为空,构造器会从卡片自动填充描述(见RemoteA2aAgent.__init__AgentCard类型的处理);而通过网络拉取的卡片描述会被截断到 1024 字符并作为「不可信引用文本」处理(_adopted_card_description),防止远端注入恶意指令。

远程 Hello World 代理(remote_a2a/hello_world/agent.py)

contributing/samples/a2a/a2a_root/remote_a2a/hello_world/agent.py 是业务逻辑所在,包含四个核心构件:

roll_die(sides: int, tool_context: ToolContext) -> int:有状态函数工具。除返回random.randint(1, sides)外,还把每次结果追加到tool_context.state['rolls']列表,为后续「基于历史掷骰结果判断素数」提供状态支撑:

def roll_die(sides: int, tool_context: ToolContext) -> int: result = random.randint(1, sides) if not 'rolls' in tool_context.state: tool_context.state['rolls'] = [] tool_context.state['rolls'] = tool_context.state['rolls'] + [result] return result

check_prime(nums: list[int]) -> str:异步函数工具。采用试除法(遍历 2 到sqrt(n))判断批量数字是否为素数,返回人类可读的汇总字符串;无素数时返回'No prime numbers found.'

root_agent:带详尽指令的主代理。指令文本是关键工程细节——它明确要求:掷骰子必须调用roll_die工具并传整数(禁止传字符串、禁止自行掷骰);必须先等roll_die返回再调用check_prime;回答时必须包含roll_die的结果;检查素数时禁止依赖历史记录中的旧结果。这种「指令即协议」的做法是让 LLM 稳定走完「掷骰 → 判断 → 汇报」多步工具链的保障。

此外该代理还做了两处实用配置:

  • generate_content_config关闭了HARM_CATEGORY_DANGEROUS_CONTENT安全阈值(源码注释说明是为了避免掷骰子被误判为危险内容);
  • 被注释掉的planner=BuiltInPlanner(...)展示了可选升级路径:若需思考过程可见,可启用内置规划器。

a2a_app:通过to_a2a(root_agent, port=8001)创建的 Starlette 应用,供 uvicorn 加载。

to_a2a:把 ADK 代理变成 A2A 服务

to_a2a定义于 src/google/adk/a2a/utils/agent_to_a2a.py,官方指南见 docs/guides/a2a/utils/agent_to_a2a/index.md。它接受一个BaseAgentWorkflow,返回一个会说 A2A 协议(JSON-RPC over HTTP)的 Starlette 应用,启动后暴露两条路由:一条是接收任务的 JSON-RPC 端点,另一条是发布代理能力描述的 well-known Agent Card 端点(如http://localhost:8001/.well-known/agent-card.json)。

官方指南给出的最小示例与本示例的远程代理写法一致:

from google.adk import Agent from google.adk.a2a.utils.agent_to_a2a import to_a2a root_agent = Agent(name="hello_world_agent", tools=[roll_die], ...) a2a_app = to_a2a(root_agent, port=8001) # 然后: uvicorn my_module:a2a_app --host localhost --port 8001

参数速查表(来自官方指南)

选项类型默认值说明
agentBaseAgent \| Workflow必填要服务的单元;位置参数,其余均为关键字参数
hoststr"localhost"写入 Agent Card 广告 RPC URL 的主机名,不执行绑定
portint8000写入 Agent Card 广告 RPC URL 的端口,不执行绑定
protocolstr"http"写入广告 URL 的协议
rpc_pathstr""两条路由的挂载路径前缀,首尾斜杠会被去除
agent_cardAgentCard \| str \| NoneNone预构建的卡片,或卡片 JSON 文件路径
push_config_storePushNotificationConfigStore \| NoneNone推送通知配置存储
task_storeTaskStore \| NoneNoneA2A 任务状态存储
runnerRunner \| NoneNone预构建 Runner,替代内存版默认实现
lifespanCallable[[Starlette], AbstractAsyncContextManager[None]] \| NoneNone自定义启动/关闭逻辑
agent_executor_factoryCallable[[Runner], A2aAgentExecutor] \| NoneNone给定 Runner 构建执行器

两个必须理解的底层行为

  1. port要写两遍,且这不是笔误to_a2a本身从不打开 socket——它只是把protocol://host:port拼进 Agent Card;真正监听端口的是 uvicorn。因此示例中to_a2a(..., port=8001)uvicorn ... --port 8001必须一致,否则服务器正常启动、但所有读取卡片的客户端都会连错地方。源码中rpc_url = f"{protocol}://{host}:{port}{prefix}/"仅用于构建卡片与AgentCardBuilder,印证了这一设计。
  2. 调用to_a2a时几乎不做实事。真正的装配发生在 ASGI 服务器启动(Starlette lifespan)阶段:解析Runner(默认由InMemorySessionServiceInMemoryArtifactServiceInMemoryMemoryServiceInMemoryCredentialService四个内存服务构建)、构建A2aAgentExecutor、解析TaskStorePushNotificationConfigStore、调用AgentCardBuilder.build()从代理派生卡片(每个进程只构建一次,启动后运行期改动的工具列表不会反映到卡片上),最后挂载路由。因此卡片构建失败会表现为 uvicorn 的启动错误,而非 import 时的异常。

部署进阶与限制(官方指南要点)

  • 持久化:默认全部使用内存实现,进程退出即丢失一切。可通过runner=传入带DatabaseSessionService的 Runner、task_store=DatabaseTaskStore(engine=...)持久化,并自行用lifespan释放引擎资源。
  • 自定义卡片:自动构建的卡片不携带 provider、安全方案,版本固定为0.0.1;需要用AgentCardBuilder自行构建后以agent_card=传入。
  • 实验性警告to_a2a标注了@a2a_experimental,每次调用都会发出UserWarning,可用环境变量ADK_SUPPRESS_A2A_EXPERIMENTAL_FEATURE_WARNINGS=1关闭。A2A 协议本身并非实验性,实验的是 ADK 对该协议的实现。
  • 流式输出默认关闭:自动构建的卡片capabilities为空(streaming: false),message/stream请求会被拒绝;如需流式,必须自行构建AgentCapabilities(streaming=True)的卡片传入。
  • 一个应用一个代理:每次调用to_a2a都返回全新 Starlette 应用,同一进程服务多个代理应挂载多个应用,而非对同一应用重复调用。

RemoteA2aAgent 客户端机制

RemoteA2aAgent位于 src/google/adk/agents/remote_a2a_agent.py,是to_a2a的客户端另一半。它支持三种方式指定远端代理

  1. 直接传入AgentCard对象;
  2. 传入 Agent Card JSON 的 URL(本示例采用此方式,指向 well-known 端点);
  3. 传入本地 Agent Card JSON 文件路径。

其主要职责包括:

  • Agent Card 解析与校验:URL 方式通过A2ACardResolver拉取卡片;文件方式从 JSON 解析。解析失败统一抛出AgentCardResolutionError。值得注意的安全设计是_validate_card_rpc_targets:凡是从网络拉取的卡片,其提供的所有 RPC 端点必须为 https 且与卡片来源同源,仅 loopback 主机(如 localhost)允许明文 http——这正是本示例能在本地用http://localhost:8001跑通的原因。
  • 消息双向转换:通过GenAIPartToA2APartConverter/A2APartToGenAIPartConverter在 ADK 事件(Event)与 A2A 消息/任务之间互转,支持任务状态流式更新与产物事件。
  • 会话状态管理:跨请求维护会话;对无状态远端代理可通过full_history_when_stateless=True让每次请求携带全部历史事件(task 模式下默认开启)。
  • 可配置项timeout(默认 600 秒)、httpx_client/a2a_client_factory(HTTP 与 A2A 客户端定制)、auth_scheme/auth_credential/credential_key(向远端代理附加鉴权头,凭据按调用缓存)、mode="task"(作为父LlmAgent的任务子代理运行,要求远端通过finish_task工具显式宣告完成)。

此外,RemoteA2aAgent会对转发内容做「消毒」:剥离携带凭据的 part(_without_credential_parts)、将本地request_input/request_confirmation等人机交互函数调用扁平化为纯文本再转发,避免把人类输入答案或 OAuth 凭据泄漏给远端。

故障排查

原文档给出的排查要点如下:

连接问题(Connection Issues):

  • 确认 uvicorn 服务器在 8001 端口运行;
  • 检查是否有防火墙拦截 localhost 连接;
  • 核对根代理配置中的 Agent Card URL;
  • 查看 uvicorn 日志中是否有启动错误。

代理无响应(Agent Not Responding):

  • 检查 uvicorn 服务器日志中的报错;
  • 确保代理指令清晰无歧义(本示例中多步工具链高度依赖指令约束,指令含糊会导致工具调用顺序错乱);
  • 确认 A2A 应用配置的端口正确。

Uvicorn 问题(Uvicorn Issues):

  • 确保模块路径拼写正确:contributing.samples.a2a.a2a_root.remote_a2a.hello_world.agent:a2a_app
  • 确认所有依赖均已安装(包括 ADK、a2a-sdk、uvicorn、starlette)。

结合前文源码分析还可以补充两条:若根代理报AgentCardResolutionError,多半是 8001 端口未启动或卡片 URL 拼错;若出现「服务器正常但客户端连不上」,优先核对to_a2a(port=...)uvicorn --port ...两处端口是否一致。

延伸阅读

  • to_a2a官方指南:docs/guides/a2a/utils/agent_to_a2a/index.md
  • Agent Card 构建:docs/guides/a2a/utils/agent_card_builder/index.md
  • A2A 执行器与事件转换:docs/guides/a2a/executor/a2a_agent_executor/index.md
  • 对比示例:a2a_basic 展示了另一条部署路径——通过adk api_server --a2a配合手写agent.json卡片提供服务,其中不涉及to_a2a调用;
  • 仓库内其他 A2A 示例(如 a2a_human_in_loop、a2a_auth、a2a_state_forwarding)可继续探索 A2A 的人机协同、鉴权与状态转发能力。

【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询