欢迎来到 Super AI Google Workshop!在这个实验里,你会亲手搭建一款叫 World Cup Mania(世界杯狂热) 的浏览器足球游戏。这个游戏最特别的地方是:场上的每一名球员,背后都是一个 AI 智能体(Agent)。当你在场边"喊话"(比如喊"全员进攻!"),你的喊声会经过一整条 AI 指挥链,最终改变每个球员在场上的跑位和行为。

别被"多智能体""A2A""MCP"这些词吓到。我们会用生活里的比喻把每一步讲清楚,你只要照着复制粘贴代码,就能一步步把整套系统跑起来。

实验封面

你会造出什么

这个实验分成两大部分:

你会学到的核心概念(先混个脸熟)

概念

一句话比喻

LlmAgent

一个"会思考的员工":给它一段职责说明(instruction),它就能按职责干活。

A2A(Agent-to-Agent)

AI 之间的"企业微信/Slack":让跑在不同服务器上的两个 Agent 能互相发消息、派活。

AgentTool

把一个 Agent"打包成一个工具",让另一个 Agent 可以像调用函数一样直接调用它。

MCP(Model Context Protocol)

AI 的"USB 接口/智能手表数据线":让 Agent 用统一标准去调用外部的工具服务(比如查体力、申请换人)。

你需要准备

本实验使用讲师提供的实验平台(Qwiklabs / Google Skills)分配的临时学员账号临时 GCP 项目,不会用到你自己的账号。

  1. 在实验页面左上角点击 Start Lab(开始实验)。计时器开始倒计时,倒计时代表云资源对你可用的时长。
  2. 左侧出现 Lab Setup(实验详情) 面板。这次除了 Project ID(项目 ID),你还会看到两个新东西:
    • Lab Region(实验区域):例如 us-central1,代表你的云资源所在的大区
    • Lab Zone(实验可用区):例如 us-central1-a,是大区里更细的具体机房
  1. 右键点击 Open Google Cloud console,选择 在无痕窗口中打开链接(Open Link in Incognito Window),用面板里的 Username / Password 登录。

我们全程在 Cloud Shell 里操作。Cloud Shell 是 Google Cloud 自带的一个网页版命令行终端——你不用在自己电脑上装任何东西,打开就能用,而且已经登录好了你的实验账号。

  1. 在 Google Cloud 控制台右上角,点击 Activate Cloud Shell(激活 Cloud Shell) 图标(一个 >_ 样子的按钮)。第一次打开会让你点 Continue(继续) 授权。
  2. Cloud Shell 打开后,先把工作目录切到主目录:
cloudshell workspace ~
  1. 从 GitHub 下载本实验的全部代码:
cd ~
git clone https://github.com/salomonerobert/agent-football.git
  1. 进入项目文件夹:
cd agent-football

在写代码之前,我们先给项目搭好"厨房"——一个干净的 Python 环境,再让它拿到调用 Google AI 的"钥匙"。

1. 创建并激活虚拟环境

虚拟环境(venv) 就像给这个项目单独准备的一套餐具,和你系统里其他 Python 项目互不干扰。

python3 -m venv venv
source venv/bin/activate

激活成功后,命令行开头会出现 (venv) 字样。

2. 安装 LAB01 依赖

pip install -r LAB01/requirements.txt

3. 准备 .env 配置文件

.env 文件用来存放项目的"环境变量"——这里我们要告诉代码:用 Vertex AI 来跑 Gemini,以及用哪个 GCP 项目

先从模板复制一份:

cp .env.example .env

然后用 Cloud Shell 自带的编辑器打开它:

cloudshell edit .env

把文件内容改成下面这样(YOUR_PROJECT_ID 会被右上角卡片自动替换成你的真实项目 ID):

GOOGLE_GENAI_USE_VERTEXAI=true
GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID

改完记得保存(Ctrl+S)

4. 登录并绑定项目、区域

下面这几条命令,让 Cloud Shell 拿到访问 Google AI 的权限,并把默认项目 / 区域 / 可用区设好:

gcloud auth login
gcloud auth application-default login
gcloud config set project YOUR_PROJECT_ID
gcloud config set compute/region YOUR_REGION
gcloud config set compute/zone YOUR_ZONE

5. 开启 Vertex AI 接口

gcloud services enable aiplatform.googleapis.com

这一步告诉你的项目:"我要用 Vertex AI 了,请打开这个开关。"

在动手写代码前,我们先鸟瞰一下整个系统长什么样。理解了这张"组织架构图",后面每一步你就知道自己在拼哪一块拼图了。

架构图 A:三台服务器的分工

整套系统跑在三个部分上,像一条流水线:

架构图 A:服务器拓扑

教练和队长跑在两个不同的服务器上,它们之间靠 A2A(AI 之间的"企业微信")通信;而队长指挥四位专家时,用的是 AgentTool(把专家当函数直接调)。

架构图 B:喊话指令的流动路径

当你喊出"全员进攻",消息是这样一层层往下传的:

架构图 B:喊话流转

教练把你的意图转交给队长,队长再把任务并行下发给四位球员专家,每位专家把自己的新状态写进一个 JSON 档案,游戏画面读取这些档案后,球员的跑位就变了。

架构图 C:自动换人是怎么发生的(选做部分)

在选做的高级环节里,球员还会"喊累":

架构图 C:换人循环

当某个球员体力低于阈值,球员智能体会判断"是不是该下场了",然后通过 MCP 调用一个后台工具服务,把换人请求写到磁盘,游戏读到后就实时换人。

一支球队,总得先有球员。LAB01 的目标,就是用 Gemini 的生图能力,批量生成一支风格统一的球队形象。

这里的关键难点是"风格一致":我们不是随便生成 11 张互不相干的图,而是要让同一支队的球员穿同款球衣、同种画风。做法很巧妙——我们和 Gemini 开一个连续的聊天会话(chat session),让它"记住"前面画过的风格,后面再画就会自动保持一致,就像同一个设计师连续给你出图。

接下来的 Task 1~3,我们都在编辑同一个文件:LAB01/app.py。请在 Cloud Shell 里打开它:

cloudshell edit LAB01/app.py

这段在干嘛: 要用 Gemini,先得创建一个"客户端(Client)"对象——你可以把它理解成拨通 Google AI 的电话。有了这个电话,后面才能给 Gemini 打电话下单。

app.py 里找到 # TODO: Task 1 这一行,把客户端初始化代码补上:

client = genai.Client()

这段在干嘛: 这是保证"全队画风统一"的核心。我们不是每画一张图就重新找 Gemini,而是开一个持续的聊天会话,让它像同一个设计师那样,记住整队的球衣颜色和画风。

# TODO: Task 2 处,把创建聊天会话的代码补上:

chat = client.aio.chats.create(model="publishers/google/models/gemini-3.1-flash-image")

这段在干嘛: 现在真正下单画图了。我们要生成两类图:外场球员(前锋/中场/后卫)和门将。因为它们在同一个聊天会话里先后生成,门将会自动和外场球员保持同款画风

任务 3a:生成外场球员精灵图

# TODO: Task 3a 处填入:

response = await chat.send_message(
    player_prompt,
    config=types.GenerateContentConfig(
        response_modalities=["IMAGE"],
        image_config=types.ImageConfig(aspect_ratio="16:9"),
    ),
)

任务 3b:生成风格一致的门将精灵图

# TODO: Task 3b 处填入(注意这次用的是 gk_prompt):

response = await chat.send_message(
    gk_prompt,
    config=types.GenerateContentConfig(
        response_modalities=["IMAGE"],
        image_config=types.ImageConfig(aspect_ratio="16:9"),
    ),
)

写完记得保存

代码写好了,现在跑起来看看效果!我们会启动一个网页版"球员入职门户"。

  1. 进入 LAB01 目录并启动服务:
cd LAB01
uvicorn app:app --host 127.0.0.1 --port 8002 --reload
  1. 服务启动后,在 Cloud Shell 里点击 Web Preview(网页预览) 图标 → 选择 Change port(更改端口) → 输入 8002 → 打开。或直接访问 http://127.0.0.1:8002
  2. 在打开的入职门户里:
    • 点击 Generate Avatars(生成头像),等待 Gemini 为蓝队和红队生成球员精灵图。
    • 生成完成后,你会看到两队的球员形象和门将形象。
    LAB01 入职门户:生成的球队资产
  3. 接着点击 Configure Player Profiles(配置球员档案),给每名球员调节速度、侵略性等属性滑块,配置好后点击 Save(保存)LAB01 配置球员档案
  1. 确认生成无误后,回到 Cloud Shell,按 Ctrl + C 停止这个服务。

小验证

你可以确认这几个产物已生成:

做完 LAB01,来检验一下理解(答案在每题下方):

Q1. 要开启一个能"记住上下文、保持画风一致"的图像生成会话,应该用哪个方法?

Q2. 为什么门将的画风能和外场球员保持一致?

Q3. 生成的球员图需要透明背景,靠的是什么技术?

球员招募好了,接下来是本实验的重头戏:LAB02——组建教练团队

我们会经历一个非常真实的"架构演进"过程:

  1. 先做一个"巨石版"教练(Task 1):一个 Agent 大包大揽,啥都自己干。简单,但不好扩展。
  2. 把队长独立出去(Task 2、3):让队长跑在自己的服务器上,教练通过 A2A 远程指挥它。这就像公司变大后,老板不再事必躬亲,而是设一个"队长"岗位分担。
  3. 给队长配四位位置专家(Task 4、5):后卫、中场、前锋、门将各有专精,队长用 AgentTool 并行调度他们。
  4. (选做)接入 MCP 工具(Task 6):让球员能查体力、自动申请换人。

这正是架构图 A / B 画的那套分层结构。走完这几步,你就亲手把它搭出来了。

这段在干嘛: 我们先用最朴素的方式实现:一个教练 Agent 独自处理所有喊话。它收到"全员进攻"后,自己直接喊出战术口号(TACTICAL SHOUTS)。这样你能先看到"一个 Agent 能干活"的最小闭环。

编辑教练文件:

cloudshell edit LAB02/football_agents/agent.py

# TODO Task 1 处,给教练写上"职责说明"(instruction)。这段说明就是教练的"岗位职责书"——告诉它:收到喊话时,直接给出战术口号并回应。

临时测试一下这个巨石教练

为了单独测试教练,我们先做几个准备:

  1. 安装 LAB02 依赖:
cd LAB02
pip install -r football_agents/requirements.txt
  1. 临时注释掉 football_agents/agent.py 里的第 64、65 行(这两行是后面才用到的队长相关代码,现在还没写,先临时去掉以便单测)。
  2. 启动 ADK 的开发界面:
bash run_lab02.sh
  1. 打开 http://127.0.0.1:8000,在左上角选择 football_agents,然后在对话框里输入:
everyone attack

看看教练是否直接回了战术口号。

  1. 测试完毕,按 Ctrl + C 停止,并把刚才注释掉的第 64、65 行恢复回来(后面 Task 3 要用)。

这段在干嘛: 巨石教练啥都自己干,难以扩展。现在我们把"队长"这个角色独立成一个单独的服务,让它跑在**自己的服务器(端口 8001)**上。这样教练和队长就能各司其职、分开部署——这正是 A2A 要解决的问题:让不同服务器上的 Agent 互相通信

这一步分三小步。

任务 2a:定义队长 Agent

编辑队长文件:

cloudshell edit LAB02/football_agents/captain.py

在对应位置定义队长 Agent:

captain_agent = LlmAgent(
    name="TeamCaptain",
    model=GeminiConstants.GEMINI_FLASH_LITE,
    description="...",
    instruction="""You are the team captain...""",
)

任务 2b:给队长写一个"对外服务器"

编辑队长服务器文件:

cloudshell edit LAB02/captain_server.py

在文件顶部补上这几行导入:

from google.adk.a2a.utils.agent_to_a2a import to_a2a
import uvicorn
from football_agents.captain import captain_agent

任务 2c:把队长"变成"一个 A2A 网络服务

captain_server.py 里继续补上:

HOST = os.environ.get("CAPTAIN_HOST", "localhost")
PORT = int(os.environ.get("CAPTAIN_PORT", "8001"))

app = to_a2a(captain_agent, host=HOST, port=PORT)

if __name__ == "__main__":
    uvicorn.run(app, host=HOST, port=PORT)

写完记得保存所有文件。

这段在干嘛: 队长现在是个独立服务了,教练要怎么找到它并派活?答案就是用 RemoteA2aAgent——它相当于教练手机里存的队长联系方式。教练不需要知道队长内部怎么干活,只要"拨号"给它就行。

回到教练文件:

cloudshell edit LAB02/football_agents/agent.py

任务 3a:登记队长的"联系方式"

# TODO Task 3a 处补上:

from google.adk.agents.remote_a2a_agent import RemoteA2aAgent, AGENT_CARD_WELL_KNOWN_PATH

CAPTAIN_A2A_URL = os.environ.get(
    "CAPTAIN_A2A_URL",
    f"http://localhost:8001{AGENT_CARD_WELL_KNOWN_PATH}",
)

team_captain_remote = RemoteA2aAgent(
    name="team_captain",
    description="...",
    agent_card=CAPTAIN_A2A_URL,
)

任务 3b:把队长设为教练的"下属"

# TODO Task 3b 处,定义教练 Agent,并把队长挂成它的子 Agent:

coach_agent = LlmAgent(
    name="ManagerAgent",
    model=GeminiConstants.GEMINI_FLASH_LITE,
    description="...",
    instruction="""...transfer to team_captain...""",
    tools=[backup_baseline_profiles, restore_baseline_profiles],
    sub_agents=[team_captain_remote],
)

保存所有文件。

这段在干嘛: 队长一个人也管不过来全队。我们给它配四位专精球员:后卫、中场、前锋、门将。每位专家只懂自己位置的战术,收到指令后更新自己的档案(比如变得更激进或更保守)。

这四个专家的代码结构几乎一模一样,只是位置不同。我们以后卫为例,其余三个照葫芦画瓢。

编辑后卫文件:

cloudshell edit LAB02/football_agents/specialist_agents/defender.py

# TODO Task 4a 处填入:

defender_agent = LlmAgent(
    name="DefenderSpecialist",
    model=GeminiConstants.GEMINI_FLASH_LITE,
    description="...",
    instruction="""You are a defender specialist...""",
    tools=[update_profile],
    output_key="defender_response",
)

然后用同样的方式,分别编辑并填好另外三个文件:

保存所有文件。

这段在干嘛: 现在把四位专家"交给"队长。这里用的不是 A2A(那是跨服务器用的),而是 AgentTool——因为专家和队长跑在同一个服务器里,队长可以把每位专家当成一个工具函数直接调用,而且能**同时(并行)**调用四个,效率最高。这对应架构图 A 里"③ 本地 AgentTool 调用"那几条箭头。

回到队长文件:

cloudshell edit LAB02/football_agents/captain.py

任务 5a:导入四位专家和 AgentTool

在文件顶部补上导入:

from google.adk.tools import AgentTool
from football_agents.specialist_agents.defender import defender_agent
from football_agents.specialist_agents.midfielder import midfielder_agent
from football_agents.specialist_agents.forward import forward_agent
from football_agents.specialist_agents.goalkeeper import goalkeeper_agent

任务 5b:把四位专家挂成队长的工具

修改队长 Agent 的定义,给它加上 tools,并在 instruction 里要求它输出一份 JSON 格式的"战术会议纪要(huddle)":

captain_agent = LlmAgent(
    name="TeamCaptain",
    model=GeminiConstants.GEMINI_FLASH_LITE,
    description="...",
    instruction="""...output a JSON huddle...""",
    tools=[
        AgentTool(defender_agent),
        AgentTool(midfielder_agent),
        AgentTool(forward_agent),
        AgentTool(goalkeeper_agent),
    ],
)

保存所有文件。

这段在干嘛: 到目前为止球员只会执行战术。这一步我们让球员能感知体力,并在太累时自动申请换人。实现方式是 MCP(Model Context Protocol)——一套让 Agent 调用外部工具服务的统一标准接口。你可以把 MCP 想成球员戴的智能手表:手表(MCP 服务)持续监测体力,数据通过标准接口回传给球员智能体,球员据此决定要不要下场。这对应架构图 C。

任务 6a:导入体力工具集

编辑后卫文件(其它专家同理):

cloudshell edit LAB02/football_agents/specialist_agents/defender.py

在导入区补上:

from .tools import make_condition_toolset, CONDITION_GUIDANCE

任务 6b:把体力工具加进专家的工具箱

修改专家的 tools,注意要用 * 把工具集展开:

tools=[update_profile, *make_condition_toolset()]

任务 6c:打开"真实 MCP 服务器"开关

编辑工具文件:

cloudshell edit LAB02/football_agents/specialist_agents/tools.py

把开关改为 True:

USE_REAL_MCP_SERVER = True

保存所有文件。

激动人心的时刻到了——把整套系统跑起来,亲自当一回教练!

  1. 确保你在 LAB02 目录并装好依赖:
cd LAB02
pip install -r football_agents/requirements.txt
  1. 一键启动整套服务(它会同时拉起 游戏前端 5173队长服务 8001教练服务 8000):
bash run_lab02.sh
  1. 通过 Cloud Shell 的 Web Preview(端口填 5173),或直接访问 http://localhost:5173 打开游戏。
  2. 点击 Kick Off!(开球),然后在喊话框里试着输入各种战术口号,例如:
everyone attack

观察场上球员的跑位随你的喊话而改变。恭喜——你正在指挥一支全 AI 驱动的足球队!

  1. 玩够了,回到 Cloud Shell 按 Ctrl + C 停止所有服务。

Q1. 要把一个普通 Agent 变成能被别的服务器远程调用的 A2A 服务,应该用哪个函数?

Q2. 队长要在同一进程里直接、并行调用四位专家,用的是什么?

Q3. Task 6 里球员智能体和体力/换人工具服务之间,靠什么协议通信?

你已经从零搭出了一支完全由 AI 驱动的足球队,并亲手实现了一套分层多智能体系统。回顾一下你掌握的硬核技能:

这套"编排层级 + A2A + 工具化 + MCP"的组合,正是当下构建生产级多智能体应用的主流范式。把它用到你自己的业务场景里——把"球员"换成你的业务模块,把"喊话"换成用户请求,一样成立。

延伸学习