Google 的代理堆栈在行动中:Google Cloud 上的 ADK、A2A、MCP

Google 的 Agent 堆栈在行动:Google Cloud 上的 ADK、A2A、MCP

关于此 Codelab

subject上次更新时间:6月 11, 2025
account_circleChristina Lin 编写

1. 学习内容

欢迎!我们今天即将开启一段非常酷的旅程。我们先来了解一下热门社交活动平台 InstaVibe。虽然该功能很实用,但我们知道,对于某些用户来说,实际规划群体活动可能是一种苦差事。想象一下,您要想办法了解所有好友的兴趣爱好,然后从无数的活动或场地选项中进行过滤,最后协调好所有事宜。这可真是了不起!这正是我们可以引入 AI(更具体地说是智能代理)来真正发挥作用的地方。

我们的想法是构建一个系统,让这些代理可以处理繁重的工作,例如巧妙地“倾听”用户和好友的偏好,然后主动推荐量身定制的绝佳活动。我们的目标是让 InstaVibe 上的社交活动规划变得顺畅又愉快。如需开始构建这些智能助理,我们需要使用合适的工具打下坚实的基础。

您将看到以下概念:

书名页

Google ADK 基础知识:掌握使用 Google 智能体开发套件 (ADK) 构建首个智能体的相关基础知识。了解基本组件、代理生命周期以及如何有效利用框架的内置工具。

使用 Model Context Protocol (MCP) 扩展智能体的功能:了解如何为智能体提供自定义工具和上下文,使其能够执行专门的任务并访问特定信息。介绍 Model Context Protocol (MCP) 概念。您将学习如何设置 MCP 服务器以提供此上下文。

设计智能体互动和编排:从单个智能体扩展到了解智能体编排。设计交互模式,从简单的顺序工作流到涉及循环、条件逻辑和并行处理的复杂场景。介绍了 ADK 框架中子代理的概念,以便管理模块化任务。

构建协作型多智能体系统:了解如何构建系统,让多个智能体协作实现复杂目标。学习和实现代理到代理 (A2A) 通信协议,为分布式智能体(可能在不同的机器或服务上运行)建立一种标准化的方式来可靠地进行交互。

在 Google Cloud 上将代理发布到正式版:将代理应用从开发环境迁移到云端。了解在 Google Cloud Platform (GCP) 上构建和部署可伸缩且强大的多代理系统的最佳实践。深入了解如何利用 Cloud Run 等 GCP 服务,并探索最新版 Google Agent Engine 用于托管和管理代理的功能。

2. 架构

利用 InstaVibe 实现 AI 赋能的社交媒体内容规划

什么是社交媒体聆听?

社交媒体聆听是指监控社交媒体、论坛和新闻网站等平台上的数字对话,以了解人们对某个主题、品牌或行业的看法。它可以提供有关公众情绪、趋势和用户需求的宝贵数据洞见。在本研讨会中,我们将在基于代理的系统中利用此概念。

您是 InstaVibe 团队的一员

假设您在“InstaVibe”工作,这是一家成功的初创公司,拥有一个面向年轻人的热门社交活动平台。一切进展顺利,但与许多科技公司一样,您的团队面临着来自投资者的压力,需要利用 AI 进行创新。在内部,您还注意到,有一部分用户的互动度不如其他用户 - 他们可能不太愿意发起小组活动,或者觉得规划过程很有挑战性。对于贵公司而言,这意味着这类重要用户群体的平台黏度较低。

您的团队的研究表明,AI 赋能的辅助功能可以显著改善这类用户的体验。该想法旨在根据用户及其好友的兴趣,主动推荐相关活动,从而简化社交活动的规划流程。您和您的同事面临的问题是:AI 智能体如何自动执行通常耗时较长的兴趣发现、活动研究和可能的初始协调任务?

基于代理的解决方案(原型概念)

您提议开发一个由多代理系统提供支持的原型功能。下面是概念细分:

用例

  • 社交画像代理:此代理会运用社交媒体监听技术来分析用户关联、互动情况,以及与用户偏好相关的可能更广泛的公共趋势。其目的是找出共同的兴趣和合适的活动特征(例如,偏好安静的聚会、特定的爱好)。
  • 活动策划代理:此代理会利用社交媒体画像代理提供的数据洞见,在线上资源中搜索符合指定条件(例如地理位置、兴趣)的特定活动、场地或创意。
  • 平台互动代理(使用 MCP):此代理会从活动规划代理获取最终方案。其主要功能是利用预定义的 MCP(模型上下文协议)工具直接与 InstaVibe 平台进行交互。此工具可让客服人员起草活动建议并创建一条概述计划的帖子。
  • 编排器代理:此代理充当中央协调者。它会接收来自 InstaVibe 平台的初始用户请求,了解总体目标(例如,“为我和我的朋友安排一次夜宵”),然后以逻辑顺序将具体任务委托给相应的专职客服人员。它会管理代理之间的信息流,并确保将最终结果传回给用户。

关键架构元素和技术

架构

Google Cloud Platform (GCP):

  • Vertex AI
    • Gemini 模型:提供对 Google 最先进的大语言模型 (LLM)(例如 Gemini)的访问权限,这些模型为我们的客服人员提供推理和决策能力。
    • Vertex AI Agent Engine:这项托管式服务用于部署、托管和扩缩我们的编排器代理,可简化生产环境部署流程并抽象出基础架构的复杂性。
  • Cloud Run:用于部署容器化应用的无服务器平台。我们会将其用于:
    • 托管主要的 InstaVibe Web 应用。
    • 将支持 A2A 的各个代理(Planner、Social Profiling、Platform Interaction)部署为独立的微服务。
    • 运行 MCP 工具服务器,以便代理可以使用 InstaVibe 的内部 API。
  • Spanner:一种全代管式、全球分布式且具有强一致性的关系型数据库。在本研讨会中,我们将利用其 GRAPH DDL 和查询功能,充分发挥其作为图数据库的优势,以便:
    • 对复杂的社交关系(用户、友谊、活动出席情况、帖子)进行建模和存储。
    • 让社交画像代理能够高效地查询这些关系。
  • Artifact Registry:一项全托管式服务,用于存储、管理和保护容器映像。
  • Cloud Build:可在 Google Cloud 上执行构建的服务。我们使用它从代理和应用源代码自动构建 Docker 容器映像。
  • Cloud Storage:Cloud Build 等服务用于存储构建工件,Agent Engine 用于满足其运维需求。
  • 核心代理框架和协议
    • Google 的 Agent Development Kit (ADK):以下各项的主要框架:
      • 为各个智能代理定义核心逻辑、行为和指令集。
      • 管理智能体的生命周期、状态和记忆(短期会话状态和可能的长期知识)。
      • 集成代理可以用来与世界互动的工具(例如 Google 搜索或自定义工具)。
      • 编排多智能体工作流,包括子智能体的顺序、循环和并行执行。
    • 代理到代理 (A2A) 通信协议:这是一种开放标准,可实现以下功能:
      • 不同 AI 代理之间的直接、标准化通信和协作,即使它们是作为单独的服务或在不同的机器上运行也是如此。
      • 代理可以发现彼此的能力(通过代理卡片),并委派任务。这对于我们的 Orchestrator 代理与专用 Planner、Social 和 Platform 代理进行交互至关重要。
    • Model Context Protocol (MCP):这是一种开放标准,可让智能体:
      • 以标准化的方式连接和使用外部工具、数据源和系统。
      • 我们的平台互动代理使用 MCP 客户端与 MCP 服务器通信,后者会提供用于与 InstaVibe 平台的现有 API 交互的工具。
  • 语言模型 (LLM):系统的“大脑”:
    • Google 的 Gemini 模型:具体而言,我们使用 gemini-2.0-flash 等版本。我们选择这些模型是因为:
      • 高级推理和指令遵循:它们能够理解复杂的提示、遵循详细的指令并推理任务,因此非常适合为客服人员决策提供支持。
      • 工具使用(函数调用):Gemini 模型擅长确定何时以及如何使用通过 ADK 提供的工具,从而让智能体能够收集信息或执行操作。
      • 效率(Flash 模型):“Flash”变体在性能和性价比方面取得了良好的平衡,适用于许多需要快速响应的互动式客服代理任务。

3. 准备工作

👉点击 Google Cloud 控制台顶部的激活 Cloud Shell(即 Cloud Shell 窗格顶部的终端形状图标),Cloud Shell

👉点击“打开编辑器”按钮(看起来像一个带有铅笔的打开文件夹)。此操作会在窗口中打开 Cloud Shell 代码编辑器。您会在左侧看到一个文件资源管理器。Cloud Shell

👉点击底部状态栏中的 Cloud Code 登录按钮,如图所示。按照说明对插件进行授权。如果您在状态栏中看到 Cloud Code - no project,请选择该选项,然后在下拉菜单中选择“Select a Google Cloud Project”(选择 Google Cloud 项目),接着从您创建的项目列表中选择特定的 Google Cloud 项目。Cloud Shell

👉在云端 IDE 中打开终端,Cloud Shell

👉在终端中,使用以下命令验证您是否已通过身份验证,以及项目是否已设置为您的项目 ID:

gcloud auth list

👉 从 GitHub 克隆 instavibe-bootstrap 项目:

git clone https://github.com/weimeilin79/instavibe-bootstrap.git
chmod +x ~/instavibe-bootstrap/init.sh
chmod +x ~/instavibe-bootstrap/set_env.sh

👉 查找您的 Google Cloud 项目 ID

  • 打开 Google Cloud 控制台:https://console.cloud.google.com
  • 从页面顶部的项目下拉菜单中选择要用于本研讨会的项目。
  • 您的项目 ID 会显示在信息中心的“项目信息”卡片中

Cloud Shell

👉 运行初始化脚本:

此脚本会提示您输入 Google Cloud 项目 ID

init.sh 脚本提示时,输入您在上一步中找到的 Google Cloud 项目 ID

cd ~/instavibe-bootstrap
./init.sh

👉设置所需的项目 ID:

gcloud config set project $(cat ~/project_id.txt) --quiet

👉运行以下命令以启用必要的 Google Cloud API:

gcloud services enable  run.googleapis.com \
                       
cloudfunctions.googleapis.com \
                       
cloudbuild.googleapis.com \
                       
artifactregistry.googleapis.com \
                       
spanner.googleapis.com \
                       
apikeys.googleapis.com \
                       
iam.googleapis.com \
                       
compute.googleapis.com \
                       
aiplatform.googleapis.com \
                       
cloudresourcemanager.googleapis.com \
                       
maps-backend.googleapis.com

👉设置所需的所有环境变量:

export PROJECT_ID=$(gcloud config get project)
export PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
export SPANNER_INSTANCE_ID="instavibe-graph-instance"
export SPANNER_DATABASE_ID="graphdb"
export GOOGLE_CLOUD_PROJECT=$(gcloud config get project)
export GOOGLE_GENAI_USE_VERTEXAI=TRUE
export GOOGLE_CLOUD_LOCATION="us-central1"

设置权限

👉 授予权限。在终端中,运行以下命令:

gcloud projects add-iam-policy-binding $PROJECT_ID \
 
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
 
--role="roles/spanner.admin"

# Spanner Database User
gcloud projects add-iam-policy-binding $PROJECT_ID \
 
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
 
--role="roles/spanner.databaseUser"

# Artifact Registry Admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
 
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
 
--role="roles/artifactregistry.admin"

# Cloud Build Editor
gcloud projects add-iam-policy-binding $PROJECT_ID \
 
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
 
--role="roles/cloudbuild.builds.editor"

# Cloud Run Admin
gcloud projects add-iam-policy-binding $PROJECT_ID \
 
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
 
--role="roles/run.admin"

# IAM Service Account User
gcloud projects add-iam-policy-binding $PROJECT_ID \
 
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
 
--role="roles/iam.serviceAccountUser"

# Vertex AI User
gcloud projects add-iam-policy-binding $PROJECT_ID \
 
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
 
--role="roles/aiplatform.user"

# Logging Writer (to allow writing logs)
gcloud projects add-iam-policy-binding $PROJECT_ID \
 
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
 
--role="roles/logging.logWriter"


gcloud projects add-iam-policy-binding $PROJECT_ID \
 
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
 
--role="roles/logging.viewer"


👉在 IAM 控制台中验证结果Cloud Shell

👉在终端中运行以下命令以创建 Artifact Registry 仓库。代理、MCP 服务器和 InstaVibe 应用的所有 Docker 映像都会先存储在此处,然后再部署到 Cloud Run 或 Agent Engine。

export REPO_NAME="introveally-repo"
gcloud artifacts repositories create $REPO_NAME \
 
--repository-format=docker \
 
--location=us-central1 \
 
--description="Docker repository for InstaVibe workshop"

为 API 密钥设置地图平台

如需在 InstaVibe 应用中使用 Google 地图服务,您需要创建一个 API 密钥并对其进行适当限制。

👉 在新标签页中,依次前往 API 和服务 > 凭据。在“凭据”页面上,点击顶部的“+ 创建凭据”按钮。从下拉菜单中选择“API 密钥”。替代文本

👉 系统随即会显示一个对话框,其中会显示您新创建的 API 密钥。您稍后需要将其用于应用配置。

👉 点击“已创建的 API 密钥”对话框中的“关闭”。

👉 您会看到新 API 密钥的列表(例如“API 密钥 1”)。点击右侧的汉堡形图标,然后选择修改 API 密钥以打开“限制和重命名 API 密钥”页面。替代文本

👉 在顶部的“名称”字段中,将默认名称更改为:Google Maps Platform API 密钥(🚨🚨重要提示🚨🚨:请使用此名称!)

👉 在“应用限制”部分下,确保选择

👉 在“API 限制”部分下,选择“限制密钥”单选按钮。

👉 点击“选择 API”下拉菜单。在随即显示的搜索框中,输入 Maps JavaScript API 并从列表中选择它。替代文本

👉 点击“确定”。

👉 点击页面底部的“保存”按钮。

关键结果

您现在已成功创建名为“Maps Platform API Key”的 API 密钥,并将其限制为仅允许使用“Maps JavaScript API”,同时确保为您的项目启用了该 API。

4. 设置图数据库

在构建智能代理之前,我们需要找到一种方法来存储和理解 InstaVibe 社交网络中的丰富关联。这正是图数据库的用武之地。与将数据存储在行和列表格中的传统关系型数据库不同,图数据库专门用于根据节点(例如人、活动或帖子)以及将这些节点连接起来的关系(例如友谊、活动出席情况或提及)来表示和查询数据。这种结构对于社交媒体应用非常强大,因为它反映了现实社交网络的结构方式,让您可以直观地探索不同实体之间的相互关联方式。

我们将使用 Google Cloud Spanner 实现此图形数据库。虽然 Spanner 主要被称为一种具备强一致性的全球分布式关系型数据库,但我们还可以直接在关系型表上定义和查询图结构。

这样,我们就能同时获得 Spanner 的可伸缩性、事务一致性和熟悉的 SQL 界面,以及图表查询的表达能力,从而分析对依托 AI 技术的功能至关重要的复杂社交动态。

👉 在 Cloud Shell IDE 终端中。在 Google Cloud 上预配必要的基础架构。首先,我们将创建一个 Spanner 实例,该实例将用作数据库的专用容器。实例准备就绪后,我们将在其中创建实际的 Spanner 数据库,该数据库将存储 InstaVibe 的所有表和图表数据:

. ~/instavibe-bootstrap/set_env.sh

gcloud spanner instances create $SPANNER_INSTANCE_ID \
 
--config=regional-us-central1 \
 
--description="GraphDB Instance InstaVibe" \
 
--processing-units=100 \
 
--edition=ENTERPRISE

gcloud spanner databases create $SPANNER_DATABASE_ID \
 
--instance=$SPANNER_INSTANCE_ID \
 
--database-dialect=GOOGLE_STANDARD_SQL

👉 向 Spanner 授予对默认服务账号的读写权限

echo "Granting Spanner read/write access to ${SERVICE_ACCOUNT_NAME} for database ${SPANNER_DATABASE_ID}..."

gcloud spanner databases add-iam-policy-binding ${SPANNER_DATABASE_ID} \
 
--instance=${SPANNER_INSTANCE_ID} \
 
--member="serviceAccount:${SERVICE_ACCOUNT_NAME}" \
 
--role="roles/spanner.databaseUser" \
 
--project=${PROJECT_ID}

👉 现在。我们将设置 Python 虚拟环境、安装所需的 Python 软件包,然后在 Spanner 中设置 Graph 数据库架构,并向其加载初始数据,然后运行 setup.py 脚本。

. ~/instavibe-bootstrap/set_env.sh
cd ~/instavibe-bootstrap
python -m venv env
source env/bin/activate
pip install -r requirements.txt
cd instavibe
python setup.py

👉 在新的浏览器标签页中,打开 Google Cloud 控制台,然后前往 Spanner,您应该会看到 Spanner 实例列表。点击 instavibe-graph-instanceSpanner 实例 👉 在实例概览页面上,您会看到该实例中的所有数据库。点击 graphdbspanner 数据库

👉 在数据库的左侧导航窗格中,点击 Spanner Studio Spanner Studio

👉 在查询编辑器(“无标题查询”标签页)中,粘贴以下 Graph SQL 查询。此查询将查找所有“Person”节点及其与其他“Person”节点之间的直接“Friendship”关系。然后点击运行以查看结果。

Graph SocialGraph
MATCH result_paths = ((p:Person)-[f:Friendship]-(friend:Person))
RETURN SAFE_TO_JSON(result_paths) AS result_paths

Spanner Graph

👉 在同一查询编辑器中,替换之前的 DDL,以查找参加过同一活动的用户,这意味着他们之间存在通过共同活动建立的间接关联。

Graph SocialGraph
MATCH result_paths =  (p1:Person)-[:Attended]->(e:Event)<-[:Attended]-(p2:Person)
WHERE p1.person_id < p2.person_id
RETURN SAFE_TO_JSON(result_paths) AS result_paths

Spanner Graph

👉 此查询用于探索另一种类型的关联,即特定用户好友撰写的帖子中提及的用户。在查询编辑器中,运行以下查询。

Graph SocialGraph
MATCH result_paths =  (user:Person {name: "Alice"})-[:Friendship]-(friend:Person)-[:Wrote]->(post:Post)-[:Mentioned]->(mentioned_person:Person)
WHERE user <> mentioned_person AND friend <> mentioned_person -- Avoid self-mentions or friend mentioning themselves in their own post if not intended
RETURN SAFE_TO_JSON(result_paths) AS result_paths

Spanner Graph

这些查询仅仅展示了将 Spanner 用作 InstaVibe 应用的图数据库的强大功能。通过将社交数据建模为相互关联的图表,我们能够对关系和活动进行精细分析,这对 AI 助理理解用户情境、发现兴趣并最终提供智能社交规划协助至关重要。

现在,我们已经完成了基础数据结构的构建和测试,接下来我们来关注一下客服人员将与之互动的现有 InstaVibe 应用。

5. InstaVibe 的当前状态

为了了解 AI 客服人员将在哪些方面发挥作用,我们首先需要部署并运行现有的 InstaVibe Web 应用。此应用提供了与我们已设置的 Spanner 图数据库连接的界面和基本功能。

首页

InstaVibe 应用使用 Google 地图在活动详情页面上直观地显示活动地点。如需启用此功能,应用需要我们之前创建的 API 密钥。以下脚本将使用我们分配的显示名称(“Google Maps Platform API 密钥”)检索实际密钥字符串。

活动页面

👉 返回 Cloud Shell IDE。运行以下脚本。之后,仔细检查输出,确保显示的 GOOGLE_MAPS_API_KEY 与您之前创建并从 Google Cloud 控制台中复制的密钥一致。

. ~/instavibe-bootstrap/set_env.sh
export KEY_DISPLAY_NAME="Maps Platform API Key"

GOOGLE_MAPS_KEY_ID=$(gcloud services api-keys list \
 
--project="${PROJECT_ID}" \
 
--filter="displayName='${KEY_DISPLAY_NAME}'" \
 
--format="value(uid)" \
 
--limit=1)

GOOGLE_MAPS_API_KEY=$(gcloud services api-keys get-key-string "${GOOGLE_MAPS_KEY_ID}" \
   
--project="${PROJECT_ID}" \
   
--format="value(keyString)")

echo "${GOOGLE_MAPS_API_KEY}" > ~/mapkey.txt

echo "Retrieved GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY}"

关键结果

👉 现在,我们来为 InstaVibe Web 应用构建容器映像,并将其推送到 Artifact Registry 仓库。

. ~/instavibe-bootstrap/set_env.sh

cd ~/instavibe-bootstrap/instavibe/
export IMAGE_TAG="latest"
export APP_FOLDER_NAME="instavibe"
export IMAGE_NAME="instavibe-webapp"
export IMAGE_PATH="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
export SERVICE_NAME="instavibe"

gcloud builds submit . \
 
--tag=${IMAGE_PATH} \
 
--project=${PROJECT_ID}

👉 将新构建的 InstaVibe Web 应用映像部署到 Cloud Run

. ~/instavibe-bootstrap/set_env.sh

cd ~/instavibe-bootstrap/instavibe/
export IMAGE_TAG="latest"
export APP_FOLDER_NAME="instavibe"
export IMAGE_NAME="instavibe-webapp"
export IMAGE_PATH="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
export SERVICE_NAME="instavibe"

gcloud run deploy ${SERVICE_NAME} \
 
--image=${IMAGE_PATH} \
 
--platform=managed \
 
--region=${REGION} \
 
--allow-unauthenticated \
 
--set-env-vars="SPANNER_INSTANCE_ID=${SPANNER_INSTANCE_ID}" \
 
--set-env-vars="SPANNER_DATABASE_ID=${SPANNER_DATABASE_ID}" \
 
--set-env-vars="APP_HOST=0.0.0.0" \
 
--set-env-vars="APP_PORT=8080" \
 
--set-env-vars="GOOGLE_CLOUD_LOCATION=${REGION}" \
 
--set-env-vars="GOOGLE_CLOUD_PROJECT=${PROJECT_ID}" \
 
--set-env-vars="GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY}" \
 
--project=${PROJECT_ID} \
 
--min-instances=1

部署成功完成后,Cloud Run 日志应显示正在运行的 InstaVibe 应用的公开网址。

网址

您还可以前往 Google Cloud 控制台中的 Cloud Run 部分,然后选择 instavibe 服务,以找到此网址。列表网址

现在,请在网络浏览器中打开该网址,探索基本 InstaVibe 平台。查看由我们设置的图数据库提供支持的帖子、活动和用户关联。

现在,我们的目标应用已经在运行,接下来,我们开始构建第一个智能代理来增强其功能。

6. 基本客服人员、使用 ADK 的活动策划者

ADK 框架

Google ADK 框架简介。现在,我们已经完成了基础设置(InstaVibe 应用和数据库),接下来可以开始使用 Google 的 Agent Development Kit (ADK) 构建我们的第一个智能客服。

智能体开发套件 (ADK) 是一个灵活的模块化框架,专为开发和部署 AI 智能体而设计。其设计原则是让智能体开发更像传统软件开发,旨在让开发者能够更轻松地创建、部署和编排智能体架构,从简单的单一用途任务到复杂的多智能体工作流,都能轻松处理。

ADK 的核心是 Agent 概念,它封装了指令、配置(例如所选的语言模型,例如 Gemini),以及一组可用于执行操作或收集信息的 Tools

06-agent.png

我们的初始客服人员将是“活动策划者”。其核心目的是接收用户的社交活动请求(指定地点、日期和兴趣),并生成量身定制的创意建议。为确保建议相关且基于最新信息(例如周末举办的特定活动),我们将利用 ADK 的一款内置工具:Google 搜索。这样,客服人员就可以根据实时网络搜索结果做出回复,提取与用户条件匹配的场馆、活动和活动的最新详细信息。

👉 返回 Cloud Shell IDE,在 ~/instavibe-bootstrap/agents/planner/agent.py 中添加以下提示和说明以创建代理

from google.adk.agents import Agent
from google.adk.tools import google_search

root_agent = Agent(
   
name="location_search_agent",
   
model="gemini-2.0-flash",
   
description="Agent tasked with generating creative and fun event plan suggestions",
   
instruction="""

        You are a specialized AI assistant tasked with generating creative and fun plan suggestions.

        **Request:**
        For the upcoming weekend, specifically from **[START_DATE_YYYY-MM-DD]** to **[END_DATE_YYYY-MM-DD]**, in the location specified as **[TARGET_LOCATION_NAME_OR_CITY_STATE]** (if latitude/longitude are provided, use these: Lat: **[TARGET_LATITUDE]**, Lon: **[TARGET_LONGITUDE]**), please generate **[NUMBER_OF_PLANS_TO_GENERATE, e.g., 3]** distinct planning suggestions.

        **Constraints and Guidelines for Suggestions:**
        1.  **Creativity & Fun:** Plans should be engaging, memorable, and offer a good experience for a date.
        2.  **Budget:** All generated plans should aim for a moderate budget (conceptually "$$"), meaning they should be affordable yet offer good value, without being overly cheap or extravagant. This budget level should be *reflected in the choice of activities and venues*, but **do not** explicitly state "Budget: $$" in the `plan_description`.
        3.  **Interest Alignment:**
            *   Consider the following user interests: **[COMMA_SEPARATED_LIST_OF_INTERESTS, e.g., outdoors, arts & culture, foodie, nightlife, unique local events, live music, active/sports]**. Tailor suggestions specifically to these where possible. The plan should *embody* these interests.
            *   **Fallback:** If specific events or venues perfectly matching all listed user interests cannot be found for the specified weekend, you should create a creative and fun generic dating plan that is still appealing, suitable for the location, and adheres to the moderate budget. This plan should still sound exciting and fun, even if it's more general.
        4.  **Current & Specific:** Prioritize finding specific, current events, festivals, pop-ups, or unique local venues operating or happening during the specified weekend dates. If exact current events cannot be found, suggest appealing evergreen options or implement the fallback generic plan.
        5.  **Location Details:** For each place or event mentioned within a plan, you MUST provide its name, precise latitude, precise longitude, and a brief, helpful description.

        **Output Format:**
        RETURN PLAN

    """,
   
tools=[google_search]
)

这就是我们定义的第一个代理!ADK 的一大优势在于其直观性和提供的实用工具。其中特别实用的是 ADK 开发者界面,您可以通过该界面以互动方式测试聊天机器人,并实时查看其回答。

👉 我们开始吧。以下命令将启动 ADK DEV 界面:

. ~/instavibe-bootstrap/set_env.sh
source ~/instavibe-bootstrap/env/bin/activate
cd  ~/instavibe-bootstrap/agents
sed -i "s|^\(O\?GOOGLE_CLOUD_PROJECT\)=.*|GOOGLE_CLOUD_PROJECT=${PROJECT_ID}|" ~/instavibe-bootstrap/agents/planner/.env
adk web

运行这些命令后,您应该会在终端中看到输出,表明 ADK Web 服务器已启动,如下所示:

+-----------------------------------------------------------------------------+
| ADK Web Server started                                                      |
|                                                                             |
| For local testing, access at http://localhost:8000.                         |
+-----------------------------------------------------------------------------+

INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

👉 接下来,如需通过浏览器访问 ADK 开发者界面,请执行以下操作:

在 Cloud Shell 工具栏(通常位于右上角)中的网页预览图标(通常看起来像眼睛或带箭头的方形)中,选择更改端口。在弹出式窗口中,将端口设置为 8000,然后点击“更改并预览”。然后,Cloud Shell 会打开一个新的浏览器标签页或窗口,其中会显示 ADK 开发界面。

网页预览

在浏览器中打开 ADK 开发者界面后,在界面右上角的下拉菜单中,选择 planner 作为您要与之互动的代理。现在,在右侧的聊天对话框中,尝试向客服人员分配任务。例如,与客服人员对话:

Search and plan something in Seattle for me this weekend
This weekend and I enjoy food and anime

建议日期(您的偏好)

May 12 2026

您应该会看到客服人员处理您的请求,并根据 Google 搜索结果提供方案。

adk 开发者界面

现在,与客服人员互动是一回事,但我们如何知道它是否始终按预期运行,尤其是在我们进行更改时?

由于 AI 代理具有生成性和非确定性特性,传统的软件测试方法通常不适用于它们。为了将酷炫的演示版转变为可靠的生产代理,制定完善的评估策略至关重要。与仅检查生成式模型的最终输出不同,评估代理通常涉及评估其决策过程,以及在各种场景中正确使用工具或遵循说明的能力。ADK 提供了一些有助于实现此目的的功能。

评估

👉 在 ADK 开发界面中,点击左侧导航栏中的“评估”标签页。您应该会看到一个名为 plan_eval 的预加载测试文件。此文件包含用于测试我们的规划者代理的预定义输入和条件。

👉 选择列出的任一城市(例如“Boston”),然后点击运行评估按钮。这将使用测试输入执行代理,并检查其输出是否符合定义的预期。这样,您就可以系统地测试客服人员的表现。

adk 开发者界面评估

浏览界面和评估完成后,返回 Cloud Shell Editor 终端,然后按 Ctrl+C 停止 ADK 开发者界面。

虽然自由形式的文本输出是一个不错的起点,但对于 InstaVibe 等应用来说,结构化数据(例如 JSON)更实用,因为这样可以轻松使用客服人员的建议。我们来修改代理,以便以一致的 JSON 格式返回其方案。

👉 在 ~/instavibe-bootstrap/agents/planner/agent.py 中,找到代理指令字符串中当前显示 RETURN PLAN 的行。将该行替换为以下详细 JSON 结构:

Return your response *exclusively* as a single JSON object. This object should contain a top-level key, "fun_plans", which holds a plan objects. Each plan object in the list must strictly adhere to the following structure:

       
--json--
       
{
         
"plan_description": "A summary of the overall plan, consisting of **exactly three sentences**. Craft these sentences in a friendly, enthusiastic, and conversational tone, as if you're suggesting this awesome idea to a close friend. Make it sound exciting and personal, highlighting the positive aspects and appeal of the plan without explicitly mentioning budget or listing interest categories.",
         
"locations_and_activities": [
             
{
             
"name": "Name of the specific place or event",
             
"latitude": 0.000000,  // Replace with actual latitude
             
"longitude": 0.000000, // Replace with actual longitude
             
"description": "A brief description of this place/event, why it's suitable for the date, and any specific details for the weekend (e.g., opening hours, event time)."
             
}
             
// Add more location/activity objects here if the plan involves multiple stops/parts
         
]
       
}

现在,您已更新代理的说明,以明确请求 JSON 输出,接下来我们来验证更改。

👉 使用与之前相同的命令重新启动 ADK 开发者界面:

. ~/instavibe-bootstrap/set_env.sh
source ~/instavibe-bootstrap/env/bin/activate
cd  ~/instavibe-bootstrap/agents
adk web

如果您已打开该标签页,请刷新该标签页。或者,按照之前的步骤在浏览器中打开 ADK 开发者界面(通过 Cloud Shell 的 Web 预览在端口 8000 上)。界面加载后,请确保选择了规划程序代理。

👉 这次,我们来给它发出其他请求。在聊天对话框中,输入以下内容:

Plan an event Boston this weekend with art and coffee

仔细检查客服人员的回复。现在,您应该会看到采用严格 JSON 对象格式的回答,而不是纯粹对话式的文本回复,并且该回答与我们在说明中定义的结构(包含 fun_plans、plan_description、locations_and_activities 等)相匹配。这确认了该代理现在可以生成适合 InstaVibe 应用以程序化方式使用的结构化输出。

adk dev ui json

确认 JSON 输出后,返回 Cloud Shell 终端,然后按 Ctrl+C 停止 ADK 开发者界面。

ADK 组件

虽然 ADK 开发者界面非常适合进行交互式测试,但我们通常需要以编程方式运行代理,可能作为更大的应用或后端服务的一部分。为了了解其运作方式,我们来看看与运行时和上下文管理相关的一些核心 ADK 概念。

为了进行有意义的多轮对话,客服人员需要了解上下文,回想之前说过和做过的事情,以保持连续性。ADK 提供了通过会话状态内存来管理此上下文的有序方式:

  • 会话:当用户开始与客服人员互动时,系统会创建一个会话。您可以将其视为单个特定聊天会话的容器。它包含唯一 ID、互动历史记录(事件)、当前工作数据(状态)以及上次更新时间等元数据。
  • 状态:这是代理在单个会话中的短期工作内存。它是一个可变字典,代理可以在此字典中存储完成当前任务所需的临时信息(例如,到目前为止收集的用户偏好设置、工具调用的中间结果)。
  • 记忆:表示客服人员在不同会话中长期回想或访问外部知识库的潜力。虽然会话和状态用于处理即时对话,但借助 Memory(通常由 MemoryService 管理),智能体可以从过往互动或结构化数据源检索信息,从而获得更广泛的知识背景。(注意:为简单起见,我们的简单客户端使用内存服务,这意味着内存/状态仅在脚本运行时保留。)
  • 事件:会话中的每项互动(用户消息、客服人员回复、工具使用请求、工具结果、状态更改、错误)都会记录为不可变事件。这会创建一个按时间顺序的日志,实质上是对话的转写内容和操作历史记录。

那么,在代理运行时,这些数据如何管理?这是运行程序的工作。

  • Runner:Runner 是 ADK 提供的核心执行引擎。您定义代理及其使用的工具,Runner 则负责协调执行用户请求的过程。它负责管理会话、处理事件流、更新状态、调用底层语言模型、协调工具调用,并可能与 MemoryService 交互。可以将其视为指挥家,确保所有不同部分协同正常工作。

我们可以使用 Runner 将代理作为独立的 Python 应用运行,完全独立于开发者界面。

我们来创建一个简单的客户端脚本,以便以编程方式调用我们的规划者代理。

👉 在 ~/instavibe-bootstrap/agents/planner/planner_client.py 文件中,在现有导入代码下方添加以下 Python 代码。在 planner_client.py 中,在导入内容下,添加以下代码:

async def async_main():
 
session_service = InMemorySessionService()
 
# Artifact service might not be needed for this example
 
artifacts_service = InMemoryArtifactService()

 
session = session_service.create_session(
     
state={}, app_name='planner_app', user_id='user_dc'
 
)

 
query = "Plan Something for me in San Francisco this weekend on wine and fashion "
 
print(f"User Query: '{query}'")
 
content = types.Content(role='user', parts=[types.Part(text=query)])

 
root_agent = agent.root_agent
 
runner = Runner(
       
app_name='planner_app',
       
agent=root_agent,
       
artifact_service=artifacts_service, # Optional
       
session_service=session_service,
 
)
 
print("Running agent...")
 
events_async = runner.run_async(
   
session_id=session.id, user_id=session.user_id, new_message=content
 
)

 
async for event in events_async:
   
print(f"Event received: {event}")


if __name__ == '__main__':
 
try:
   
asyncio.run(async_main())
 
except Exception as e:
   
print(f"An error occurred: {e}")

此代码会设置内存中的会话和工件管理服务(为本例保持简单),创建会话、定义用户查询、使用代理配置 Runner,然后异步运行代理,并输出执行期间生成的每个事件。

👉 现在,从终端执行以下客户端脚本:

. ~/instavibe-bootstrap/set_env.sh
source ~/instavibe-bootstrap/env/bin/activate
cd  ~/instavibe-bootstrap/agents
python -m planner.planner_client

观察输出。您将看到代理执行流程中生成的每个 Event 对象的详细结构,而不仅仅是最终的 JSON 方案。这包括初始用户消息事件、与工具调用(例如 Google 搜索)相关的潜在事件,以及包含 JSON 方案的模型响应事件。此详细事件流对于调试和了解 ADK 运行时内发生的分步处理非常有用。

Running agent...
Event received: content=Content(parts=[Part(video_metadata=None, thought=None, code_execution_result=None, executable_code=None, file_data=None, function_call=None, function_response=None, inline_data=None, text='```json\n{\n "fun_plans": [\n  {\n   "plan_description": "Embark on a stylish adventure through Hayes Valley,
...(turncated)
, offering a variety of fashion styles to browse and enjoy."\n    }\n   ]\n  }\n ]\n}\n```')], role='model') grounding_metadata=GroundingMetadata(grounding_chunks=[GroundingChunk(retrieved_context=None, web=GroundingChunkWeb(domain='islands.com', title='islands.com', uri='http
...(turncated)
QyTpPV7jS6wUt-Ix7GuP2mC9J4eY_8Km6Vv44liF9cb2VSs='))], grounding_supports=[GroundingSupport(confide
...(turncated)
>\n', sdk_blob=None), web_search_queries=['..e']) partial=None turn_complete=None error_code=None error_message=None interrupted=None custom_metadata=None invocation_id='e-04d97b8b-9021-47a5-ab41-17b5cbb4bf03' author='location_search_agent' actions=EventActions(skip_summarization=None, state_delta={}, artifact_delta={}, transfer_to_agent=None, escalate=None, requested_auth_configs={}) long_running_tool_ids=None branch=None id='CInHdkKw' timestamp=1746978846.232674

如果脚本连续运行或挂起,您可能需要按 Ctrl+C 手动停止它。

7. 平台互动代理 - 与 MCP 服务器交互

虽然 ADK 有助于构建我们的代理,但它们通常需要与外部系统或 API 交互才能执行实际操作。

模型上下文协议 (MCP)

模型上下文协议 (MCP) 是一种开放标准,旨在规范智能体等 AI 应用与外部数据源、工具和系统的连接方式。它旨在通过提供通用接口,解决需要为每种 AI 应用和数据源组合进行自定义集成的问题。MCP 采用客户端-服务器架构,其中位于 AI 应用(主机)中的 MCP 客户端管理与 MCP 服务器的连接。这些服务器是外部程序,可提供特定功能,例如访问本地数据、通过 API 与远程服务交互或提供预定义的提示,让 AI 模型能够访问当前信息并执行初始训练之外的任务。这种结构使 AI 模型能够以标准化的方式发现和与外部功能互动,从而简化集成并提高可扩展性。

构建和部署 InstaVibe MCP 服务器

07-mcp-server.png

我们的客服人员最终需要与 InstaVibe 平台本身进行互动,具体而言,需要使用该平台现有的 API 创建帖子和注册事件。InstaVibe 应用已通过标准 HTTP 端点公开以下功能:

Enpoint

网址

HTTP 方法

说明

创建帖子

api/posts

POST

用于添加新帖子的 API 端点。预期 JSON 正文:
{"author_name": "...", "text": "...", "sentiment": "..." (optional)}

创建活动

api/events

POST

用于添加新活动及其参加者的 API 端点(简化架构)。
预期 JSON 正文:{ "event_name": "...", "description": "...", "event_date": "YYYY-MM-DDTHH:MM:SSZ", "locations": [ {"name": "...", "description": "...", "latitude": 0.0, "longitude": 0.0, "address": "..."} ], "attendee_names": ["...", "..."] }

为了通过 MCP 向我们的客服人员提供这些功能,我们首先需要创建简单的 Python 函数,以便作为这些 API 调用的封装容器。这些函数将处理 HTTP 请求逻辑。

👉 首先,我们来实现用于创建帖子的封装容器函数。打开文件 ~/instavibe-bootstrap/tools/instavibe/instavibe.py,然后将 #REPLACE ME CREATE POST 注释替换为以下 Python 代码:

def create_post(author_name: str, text: str, sentiment: str, base_url: str = BASE_URL):
    """
    Sends a POST request to the /posts endpoint to create a new post.

    Args:
        author_name (str): The name of the post's author.
        text (str): The content of the post.
        sentiment (str): The sentiment associated with the post (e.g., 'positive', 'negative', 'neutral').
        base_url (str, optional): The base URL of the API. Defaults to BASE_URL.

    Returns:
        dict: The JSON response from the API if the request is successful.
              Returns None if an error occurs.

    Raises:
        requests.exceptions.RequestException: If there's an issue with the network request (e.g., connection error, timeout).
    """
   
url = f"{base_url}/posts"
   
headers = {"Content-Type": "application/json"}
   
payload = {
       
"author_name": author_name,
       
"text": text,
       
"sentiment": sentiment
   
}

   
try:
       
response = requests.post(url, headers=headers, json=payload)
       
response.raise_for_status()  # Raise an exception for bad status codes (4xx or 5xx)
       
print(f"Successfully created post. Status Code: {response.status_code}")
       
return response.json()
   
except requests.exceptions.RequestException as e:
       
print(f"Error creating post: {e}")
       
# Optionally re-raise the exception if the caller needs to handle it
       
# raise e
       
return None
   
except json.JSONDecodeError:
       
print(f"Error decoding JSON response from {url}. Response text: {response.text}")
       
return None

👉 接下来,我们将为事件创建 API 创建封装容器函数。在同一 ~/instavibe-bootstrap/tools/instavibe/instavibe.py 文件中,将 #REPLACE ME CREATE EVENTS 注释替换为以下代码:

def create_event(event_name: str, description: str, event_date: str, locations: list, attendee_names: list[str], base_url: str = BASE_URL):
    """
    Sends a POST request to the /events endpoint to create a new event registration.

    Args:
        event_name (str): The name of the event.
        description (str): The detailed description of the event.
        event_date (str): The date and time of the event (ISO 8601 format recommended, e.g., "2025-06-10T09:00:00Z").
        locations (list): A list of location dictionaries. Each dictionary should contain:
                          'name' (str), 'description' (str, optional),
                          'latitude' (float), 'longitude' (float),
                          'address' (str, optional).
        attendee_names (list[str]): A list of names of the people attending the event.
        base_url (str, optional): The base URL of the API. Defaults to BASE_URL.

    Returns:
        dict: The JSON response from the API if the request is successful.
              Returns None if an error occurs.

    Raises:
        requests.exceptions.RequestException: If there's an issue with the network request (e.g., connection error, timeout).
    """
   
url = f"{base_url}/events"
   
headers = {"Content-Type": "application/json"}
   
payload = {
       
"event_name": event_name,
       
"description": description,
       
"event_date": event_date,
       
"locations": locations,
       
"attendee_names": attendee_names,
   
}

   
try:
       
response = requests.post(url, headers=headers, json=payload)
       
response.raise_for_status()  # Raise an exception for bad status codes (4xx or 5xx)
       
print(f"Successfully created event registration. Status Code: {response.status_code}")
       
return response.json()
   
except requests.exceptions.RequestException as e:
       
print(f"Error creating event registration: {e}")
       
# Optionally re-raise the exception if the caller needs to handle it
       
# raise e
       
return None
   
except json.JSONDecodeError:
       
print(f"Error decoding JSON response from {url}. Response text: {response.text}")
       
return None

如您所见,这些函数是现有 InstaVibe API 的简单封装容器。如果您已经为服务创建了 API,那么通过创建此类封装容器,您可以轻松地将其功能作为工具公开给代理。

MCP 服务器实现

现在,我们已经有了执行操作(调用 InstaVibe API)的 Python 函数,接下来需要构建 MCP 服务器组件。该服务器将根据 MCP 标准将这些函数公开为“工具”,以便 MCP 客户端(例如我们的代理)发现和调用它们。

MCP 服务器通常实现两项关键功能:

  • list_tools:负责允许客户端发现服务器上的可用工具,提供名称、说明和必需参数等元数据,这些元数据通常使用 JSON 架构进行定义
  • call_tool:处理客户端请求的特定工具的执行,接收工具的名称和参数,并执行相应的操作,例如在本例中与 API 交互

MCP 服务器用于向 AI 模型提供对真实数据和操作的访问权限,以便执行发送电子邮件、在项目管理系统中创建任务、搜索数据库或与各种软件和 Web 服务互动等任务。虽然初始实现通常侧重于本地服务器(为了简单起见,尤其是在开发或“工作室”环境中),通过标准输入/输出 (stdio) 进行通信,但对于更广泛的采用和企业用例,改为使用 HTTP 和服务器发送的事件 (SSE) 等协议的远程服务器更为合理。

尽管增加了网络通信层,但远程架构具有显著优势:它允许多个 AI 客户端共享对单个服务器的访问权限,集中管理和更新工具,通过将敏感数据和 API 密钥保留在服务器端(而不是分布在可能的许多客户端机器上)来增强安全性,并将 AI 模型与外部系统集成的具体细节解耦,从而使整个生态系统更具可伸缩性、安全性和可维护性,而无需每个 AI 实例都管理自己的直接集成。

07-mcp-server.png

我们将使用 HTTP 和服务器发送的事件 (SSE) 实现 MCP 服务器,这非常适合可能长时间运行的工具执行和企业场景。

👉 首先,我们来实现 list_tools 端点。打开文件 ~/instavibe-bootstrap/tools/instavibe/mcp_server.py,并将 #REPLACE ME - LIST TOOLS 注释替换为以下代码。:

@app.list_tools()
async def list_tools() -> list[mcp_types.Tool]:
  """MCP handler to list available tools."""
 
# Convert the ADK tool's definition to MCP format
 
mcp_tool_schema_event = adk_to_mcp_tool_type(event_tool)
 
mcp_tool_schema_post = adk_to_mcp_tool_type(post_tool)
 
print(f"MCP Server: Received list_tools request. \n MCP Server: Advertising tool: {mcp_tool_schema_event.name} and {mcp_tool_schema_post}")
 
return [mcp_tool_schema_event,mcp_tool_schema_post]

此函数定义了工具(create_event、create_post),并告知连接的客户端这些工具。

👉 接下来,实现 call_tool 端点,该端点用于处理来自客户端的实际执行请求。在同一 ~/instavibe-bootstrap/tools/instavibe/mcp_server.py 文件中,将 #REPLACE ME - CALL TOOLS 注释替换为以下代码。

@app.call_tool()
async def call_tool(
   
name: str, arguments: dict
) -> list[mcp_types.TextContent | mcp_types.ImageContent | mcp_types.EmbeddedResource]:
  """MCP handler to execute a tool call."""
 
print(f"MCP Server: Received call_tool request for '{name}' with args: {arguments}")

 
# Look up the tool by name in our dictionary
 
tool_to_call = available_tools.get(name)
 
if tool_to_call:
   
try:
     
adk_response = await tool_to_call.run_async(
         
args=arguments,
         
tool_context=None,
     
)
     
print(f"MCP Server: ADK tool '{name}' executed successfully.")

     
response_text = json.dumps(adk_response, indent=2)
     
return [mcp_types.TextContent(type="text", text=response_text)]

   
except Exception as e:
     
print(f"MCP Server: Error executing ADK tool '{name}': {e}")
     
# Creating a proper MCP error response might be more robust
     
error_text = json.dumps({"error": f"Failed to execute tool '{name}': {str(e)}"})
     
return [mcp_types.TextContent(type="text", text=error_text)]
 
else:
     
# Handle calls to unknown tools
     
print(f"MCP Server: Tool '{name}' not found.")
     
error_text = json.dumps({"error": f"Tool '{name}' not implemented."})
     
return [mcp_types.TextContent(type="text", text=error_text)]

此函数会接收工具名称和参数,找到我们之前定义的相应 Python 封装容器函数,执行该函数,并返回结果

👉 定义 MCP 服务器逻辑后,我们现在需要将其打包为容器,在终端中运行以下脚本,以使用 Cloud Build 构建 Docker 映像:

. ~/instavibe-bootstrap/set_env.sh

cd ~/instavibe-bootstrap/tools/instavibe

export IMAGE_TAG="latest"
export MCP_IMAGE_NAME="mcp-tool-server"
export IMAGE_PATH="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${MCP_IMAGE_NAME}:${IMAGE_TAG}"
export SERVICE_NAME="mcp-tool-server"
export INSTAVIBE_BASE_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep instavibe)/api

gcloud builds submit . \
 
--tag=${IMAGE_PATH} \
 
--project=${PROJECT_ID}

👉 并将映像作为服务部署到 Google Cloud Run 上。

. ~/instavibe-bootstrap/set_env.sh

cd ~/instavibe-bootstrap/tools/instavibe

export IMAGE_TAG="latest"
export MCP_IMAGE_NAME="mcp-tool-server"
export IMAGE_PATH="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${MCP_IMAGE_NAME}:${IMAGE_TAG}"
export SERVICE_NAME="mcp-tool-server"
export INSTAVIBE_BASE_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep instavibe)/api

gcloud run deploy ${SERVICE_NAME} \
 
--image=${IMAGE_PATH} \
 
--platform=managed \
 
--region=${REGION} \
 
--allow-unauthenticated \
 
--set-env-vars="INSTAVIBE_BASE_URL=${INSTAVIBE_BASE_URL}" \
 
--set-env-vars="APP_HOST=0.0.0.0" \
 
--set-env-vars="APP_PORT=8080" \
 
--set-env-vars="GOOGLE_GENAI_USE_VERTEXAI=TRUE" \
 
--set-env-vars="GOOGLE_CLOUD_LOCATION=${REGION}" \
 
--set-env-vars="GOOGLE_CLOUD_PROJECT=${PROJECT_ID}" \
 
--project=${PROJECT_ID} \
 
--min-instances=1

👉 部署成功完成后,MCP 服务器将会运行,并且可通过公开网址访问。我们需要捕获此网址,以便我们的代理(充当 MCP 客户端)知道连接到何处。

export MCP_SERVER_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep mcp-tool-server)/sse

现在,您应该还可以在 Google Cloud 控制台的 Cloud Run 部分看到状态为“正在运行”的 mcp-tool-server 服务。

Cloud Run

部署 MCP 服务器并捕获其网址后,我们现在可以实现将充当 MCP 客户端的代理,并利用此服务器公开的工具。

8. 平台互动代理(使用 MCP)

MCP 客户端:MCP 客户端是位于 AI 应用或代理中的组件,充当 AI 模型与一个或多个 MCP 服务器之间的接口;在我们的实现中,此客户端将直接集成到我们的代理中。此客户端的主要功能是与 MCP 服务器通信,以便通过 list_tools 函数发现可用工具,然后使用 call_tool 函数请求执行特定工具,并传递 AI 模型或调度调用的代理提供的必要参数。

MCP 客户端

现在,我们将构建充当 MCP 客户端的代理。此代理在 ADK 框架中运行,将负责与我们刚刚部署的 mcp-tool-server 进行通信。

👉 首先,我们需要修改代理定义,以便从正在运行的 MCP 服务器动态提取工具。在 agents/platform_mcp_client/agent.py 中,将 #REPLACE ME - FETCH TOOLS 替换为以下代码:

"""Gets tools from the File System MCP Server."""
 
print("Attempting to connect to MCP Filesystem server...")
 
tools, exit_stack = await MCPToolset.from_server(
     
connection_params=SseServerParams(url=MCP_SERVER_URL, headers={})
 
)
 
log.info("MCP Toolset created successfully.")

此代码使用 MCPToolset.from_server 方法连接到 MCP_SERVER_网址(我们之前将其设置为环境变量),并检索可用工具的列表。

接下来,我们需要指示 ADK 代理定义实际使用这些动态提取的工具。👉 在 agents/platform_mcp_client/agent.py 中,将 #REPLACE ME - SET TOOLs 替换为以下内容:

  tools=tools,

👉 现在,我们将使用 ADK 开发者界面在本地测试此代理,看看它能否正确连接到 MCP 服务器,以及能否使用工具与正在运行的 InstaVibe 应用进行交互。

. ~/instavibe-bootstrap/set_env.sh
source ~/instavibe-bootstrap/env/bin/activate
export MCP_SERVER_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep mcp-tool-server)/sse

cd  ~/instavibe-bootstrap/agents
sed -i "s|^\(O\?GOOGLE_CLOUD_PROJECT\)=.*|GOOGLE_CLOUD_PROJECT=${PROJECT_ID}|" ~/instavibe-bootstrap/agents/platform_mcp_client/.env
sed -i "s|^\(O\?MCP_SERVER_URL\)=.*|MCP_SERVER_URL=${MCP_SERVER_URL}|" ~/instavibe-bootstrap/agents/platform_mcp_client/.env
adk web

再次在浏览器中打开 ADK 开发者界面(使用 Cloud Shell 的 Web 预览,端口为 8000)。这次,在右上角的下拉菜单中,选择 platform_mcp_client 代理。

我们来测试一下 create_post 工具。在聊天对话框中,输入以下请求:

Create a post saying "Y'all I just got the cutest lil void baby 😭✨ Naming him Abyss bc he's deep, mysterious, and lowkey chaotic 🔥🖤 #VoidCat #NewRoomie" I'm Julia

ADK 开发者界面帖子

代理应处理此请求,确定是否需要使用 create_post 工具,并与 MCP 服务器通信,后者会调用 InstaVibe API。

👉 验证步骤:客服人员确认操作后,打开运行 InstaVibe 应用的标签页(或刷新该标签页)。您应该会在主动态中看到“Julia”发布的新帖子!

InstaVibe 帖子

👉 如有需要,请运行以下脚本以获取 Instavibe 链接:

gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep instavibe

👉 现在,我们来测试 create_event 工具。在聊天对话框中输入以下多行请求:

Hey, can you set up an event for Hannah and George and me, and I'm Julia? Let's call it 'Mexico City Culinary & Art Day'.
here are more info
 
{"event_name": "Mexico City Culinary & Art Day",
 
"description": "A vibrant day in Mexico City for Hannah and George, starting with lunch at one of the city's best taco spots in the hip Condesa neighborhood, followed by an inspiring afternoon exploring the Museo Soumaya's stunning art collection.",
 
"event_date": "2025-05-17T12:00:00-06:00",
 
"locations": [
   
{
     
"name": "El Tizoncito",
     
"description": "Considered one of the original creators of tacos al pastor, El Tizoncito offers a legendary taco experience in the heart of Condesa. Their flavorful meats, house salsas, and casual vibe make it a must-visit for foodies.",
     
"latitude": 19.412179,
     
"longitude": -99.171308,
     
"address": "Av. Tamaulipas 122, Hipódromo, Cuauhtémoc, 06100 Ciudad de México, CDMX, Mexico"
   
},
   
{
     
"name": "Museo Soumaya",
     
"description": "An architectural icon in Mexico City, Museo Soumaya houses over 66,000 works of art, including pieces by Rodin, Dalí, and Rivera. The striking silver structure is a cultural landmark and a visual feast inside and out.",
     
"latitude": 19.440056,
     
"longitude": -99.204281,
     
"address": "Plaza Carso, Blvd. Miguel de Cervantes Saavedra 303, Granada, Miguel Hidalgo, 11529 Ciudad de México, CDMX, Mexico"
   
}
 
],
 
"attendee_names": ["Hannah", "George", Julia],
}

同样,代理应通过 MCP 服务器使用适当的工具。在“事件”标签页中,您可以随意点击各个事件,系统会显示执行过程的详细分步轨迹。

ADK 开发者界面事件

👉 验证步骤:返回正在运行的 InstaVibe 应用,然后前往“活动”部分(或等效部分)。现在,您应该会看到新创建的“Mexico City Culinary & Art Day”(墨西哥城美食与艺术日)活动。

InstaVibe 活动

这成功展示了 MCP 如何让代理能够以标准化的方式利用外部工具(在本例中为 InstaVibe 的 API)。

验证完这两个操作后,返回 Cloud Shell 终端,然后按 Ctrl+C 停止 ADK 开发者界面。

9. ADK 中的工作流智能体和多智能体

目前,我们的客服人员可以规划外出活动并与该平台互动。不过,要想真正实现个性化规划,就需要了解用户的社交圈。对于可能不会密切关注好友活动的繁忙用户,手动收集此情境信息非常困难。为了解决此问题,我们将构建一个社交画像代理,该代理利用 Spanner 图数据库分析好友活动和兴趣,从而提供更贴合用户需求的建议。

社交媒体性能分析代理

首先,我们需要为此代理提供访问图表数据的工具。

👉 将以下 Python 函数添加到 ~/instavibe-bootstrap/agents/social/instavibe.py 文件的末尾:

def get_person_attended_events(person_id: str)-> list[dict]:
    """
    Fetches events attended by a specific person using Graph Query.
    Args:
       person_id (str): The ID of the person whose posts to fetch.
    Returns: list[dict] or None.
    """
   
if not db_instance: return None

   
graph_sql = """
        Graph SocialGraph
        MATCH (p:Person)-[att:Attended]->(e:Event)
        WHERE p.person_id = @person_id
        RETURN e.event_id, e.name, e.event_date, att.attendance_time
        ORDER BY e.event_date DESC
    """
   
params = {"person_id": person_id}
   
param_types_map = {"person_id": param_types.STRING}
   
fields = ["event_id", "name", "event_date", "attendance_time"]

   
results = run_graph_query( graph_sql, params=params, param_types=param_types_map, expected_fields=fields)

   
if results is None: return None

   
for event in results:
       
if isinstance(event.get('event_date'), datetime):
           
event['event_date'] = event['event_date'].isoformat()
       
if isinstance(event.get('attendance_time'), datetime):
           
event['attendance_time'] = event['attendance_time'].isoformat()
   
return results

def get_person_id_by_name( name: str) -> str:
    """
    Fetches the person_id for a given name using SQL.

    Args:
       name (str): The name of the person to search for.

    Returns:
        str or None: The person_id if found, otherwise None.
                     Returns the ID of the *first* match if names are duplicated.
    """
   
if not db_instance: return None

   
sql = """
        SELECT person_id
        FROM Person
        WHERE name = @name
        LIMIT 1 -- Return only the first match in case of duplicate names
    """
   
params = {"name": name}
   
param_types_map = {"name": param_types.STRING}
   
fields = ["person_id"]

   
# Use the standard SQL query helper
   
results = run_sql_query( sql, params=params, param_types=param_types_map, expected_fields=fields)

   
if results: # Check if the list is not empty
       
return results[0].get('person_id') # Return the ID from the first dictionary
   
else:
       
return None # Name not found


def get_person_posts( person_id: str)-> list[dict]:
    """
    Fetches posts written by a specific person using Graph Query.

    Args:
        person_id (str): The ID of the person whose posts to fetch.


    Returns:
        list[dict] or None: List of post dictionaries with ISO date strings,
                           or None if an error occurs.
    """
   
if not db_instance: return None

   
# Graph Query: Find the specific Person node, follow 'Wrote' edge to Post nodes
   
graph_sql = """
        Graph SocialGraph
        MATCH (author:Person)-[w:Wrote]->(post:Post)
        WHERE author.person_id = @person_id
        RETURN post.post_id, post.author_id, post.text, post.sentiment, post.post_timestamp, author.name AS author_name
        ORDER BY post.post_timestamp DESC
    """
   
# Parameters now include person_id and limit
   
params = {
       
"person_id": person_id
   
}
   
param_types_map = {
       
"person_id": param_types.STRING
   
}
   
# Fields returned remain the same
   
fields = ["post_id", "author_id", "text", "sentiment", "post_timestamp", "author_name"]

   
results = run_graph_query(graph_sql, params=params, param_types=param_types_map, expected_fields=fields)

   
if results is None:
       
return None

   
# Convert datetime objects to ISO format strings
   
for post in results:
       
if isinstance(post.get('post_timestamp'), datetime):
           
post['post_timestamp'] = post['post_timestamp'].isoformat()

   
return results


def get_person_friends( person_id: str)-> list[dict]:
    """
    Fetches friends for a specific person using Graph Query.
    Args:
        person_id (str): The ID of the person whose posts to fetch.
    Returns: list[dict] or None.
    """
   
if not db_instance: return None

   
graph_sql = """
        Graph SocialGraph
        MATCH (p:Person {person_id: @person_id})-[f:Friendship]-(friend:Person)
        RETURN DISTINCT friend.person_id, friend.name
        ORDER BY friend.name
    """
   
params = {"person_id": person_id}
   
param_types_map = {"person_id": param_types.STRING}
   
fields = ["person_id", "name"]

   
results = run_graph_query( graph_sql, params=params, param_types=param_types_map, expected_fields=fields)

   
return results

现在,我们来讨论一下如何构建代理。分析多个好友的个人资料,然后总结调查结果,需要完成几个步骤。这是使用 ADK 的多智能体功能(尤其是工作流代理)的绝佳场景。

在 Google 的 ADK 中,Workflow 代理不会自行执行任务,而是会编排其他代理(称为子代理)。这样可以实现模块化设计,将复杂问题分解为专用组件。ADK 提供内置的工作流类型,例如

  • 顺序(分步)
  • 并行(并发执行)
  • 和循环(重复执行)

社交媒体性能分析代理

对于我们的社交画像任务(分析“Mike 和 Bob”等可能包含多个人的信息,然后进行总结),Loop Agent 是理想之选。我们可以使用 profile_agent 迭代分析配置文件的流程,并使用 summary_agent 更新摘要,直到处理完所有请求的配置文件(check_agent 和 CheckCondition)。

我们来定义此工作流所需的子代理。

👉 在 ~/instavibe-bootstrap/agents/social/agent.py 中,将 #REPLACE FOR profile_agent 替换为以下内容:

profile_agent = LlmAgent(
   
name="profile_agent",
   
model="gemini-2.0-flash",
   
description=(
       
"Agent to answer questions about the this person's social profile. User will ask person's profile using their name, make sure to fetch the id before getting other data."
   
),
   
instruction=(
       
"You are a helpful agent who can answer user questions about this person's social profile."
   
),
   
tools=[get_person_posts,get_person_friends,get_person_id_by_name,get_person_attended_events],
)

接下来,代理会获取收集的个人资料信息(在多次迭代中累积)并生成最终摘要,如果分析了多名用户,则会找出共同点。

👉 在同一 ~/instavibe-bootstrap/agents/social/agent.py 中,将 #REPLACE FOR summary_agent 替换为以下内容:

summary_agent = LlmAgent(
   
name="summary_agent",
   
model="gemini-2.0-flash",
   
description=(
       
"Generate a comprehensive social summary as a single, cohesive paragraph. This summary should cover the activities, posts, friend networks, and event participation of one or more individuals. If multiple profiles are analyzed, the paragraph must also identify and integrate any common ground found between them."
   
),
   
instruction=(
        """
        Your primary task is to synthesize social profile information into a single, comprehensive paragraph.

            **Input Scope & Default Behavior:**
            *   If specific individuals are named by the user, focus your analysis on them.
            *   **If no individuals are specified, or if the request is general, assume the user wants an analysis of *all relevant profiles available in the current dataset/context*.**

            **For each profile (whether specified or determined by default), you must analyze:**

            1.  **Post Analysis:**
                *   Systematically review their posts (e.g., content, topics, frequency, engagement).
                *   Identify recurring themes, primary interests, and expressed sentiments.

            2.  **Friendship Relationship Analysis:**
                *   Examine their connections/friends list.
                *   Identify key relationships, mutual friends (especially if comparing multiple profiles), and the general structure of their social network.

            3.  **Event Participation Analysis:**
                *   Investigate their past (and if available, upcoming) event participation.
                *   Note the types of events, frequency of attendance, and any notable roles (e.g., organizer, speaker).

            **Output Generation (Single Paragraph):**

            *   **Your entire output must be a single, cohesive summary paragraph.**
                *   **If analyzing a single profile:** This paragraph will detail their activities, interests, and social connections based on the post, friend, and event analysis.
                *   **If analyzing multiple profiles:** This paragraph will synthesize the key findings regarding posts, friends, and events for each individual. Crucially, it must then seamlessly integrate or conclude with an identification and description of the common ground found between them (e.g., shared interests from posts, overlapping event attendance, mutual friends). The aim is a unified narrative within this single paragraph.

            **Key Considerations:**
            *   Base your summary strictly on the available data.
            *   If data for a specific category (posts, friends, events) is missing or sparse for a profile, you may briefly acknowledge this within the narrative if relevant.
                """
       
),
   
output_key="summary"
)

我们需要一种方法来确定循环何时应停止(即所有请求的配置文件何时已汇总)

👉 在同一 ~/instavibe-bootstrap/agents/social/agent.py 中,将 #REPLACE FOR check_agent 替换为以下内容:

check_agent = LlmAgent(
   
name="check_agent",
   
model="gemini-2.0-flash",
   
description=(
       
"Check if everyone's social profile are summarized and has been generated. Output 'completed' or 'pending'."
   
),
   
output_key="summary_status"
)

我们添加了一个简单的程序化检查 (CheckCondition),用于明确查看 check_agent 返回的 State 中存储的 summary_status,并告知 Loop Agent 是继续 (escalate=False) 还是停止 (escalate=True)。

👉 在同一 ~/instavibe-bootstrap/agents/social/agent.py 中,将文件顶部的 #REPLACE FOR CheckCondition 替换为以下代码:

class CheckCondition(BaseAgent):
   
async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]:
       
#log.info(f"Checking status: {ctx.session.state.get("summary_status", "fail")}")
       
log.info(f"Summary: {ctx.session.state.get("summary")}")

       
status = ctx.session.state.get("summary_status", "fail").strip()
       
is_done = (status == "completed")

       
yield Event(author=self.name, actions=EventActions(escalate=is_done))

循环结果的状态和回调

在 Google 的 ADK 中,状态是一个关键概念,表示代理在执行期间的内存或工作数据。它本质上是一种持久性上下文,用于存储代理在不同步骤、工具调用或互动中需要维护的信息。此状态可以存储中间结果、用户信息、后续操作的参数,或代理在执行任务过程中需要记住的任何其他数据。

在我们的场景中,随着 Loop 代理迭代,summary_agentcheck_agent 会将其输出(summary 和 summary_status)存储在代理的状态中。这样,信息便可在迭代中保留。不过,Loop Agent 本身不会在完成时自动从状态返回最终摘要。

社交媒体性能分析代理

借助 ADK 中的回调,我们可以注入要在代理生命周期的特定时间点或响应特定事件(例如工具调用完成或代理完成执行之前)执行的自定义逻辑。它们提供了一种自定义代理行为和动态处理结果的方法。

我们将使用一个在循环结束时运行的 after_agent_callback(因为 CheckCondition 已上报)。此回调 modify_output_after_agent 会从状态中检索最终摘要,并将其格式化为客服人员的最终输出消息。

回拨

👉 在同一 ~/instavibe-bootstrap/agents/social/agent.py 中,将 #REPLACE FOR modify_output_after_agent 替换为以下内容:

def modify_output_after_agent(callback_context: CallbackContext) -> Optional[types.Content]:

   
agent_name = callback_context.agent_name
   
invocation_id = callback_context.invocation_id
   
current_state = callback_context.state.to_dict()
   
current_user_content = callback_context.user_content
   
print(f"[Callback] Exiting agent: {agent_name} (Inv: {invocation_id})")
   
print(f"[Callback] Current summary_status: {current_state.get("summary_status")}")
   
print(f"[Callback] Current Content: {current_user_content}")

   
status = current_state.get("summary_status").strip()
   
is_done = (status == "completed")
   
# Retrieve the final summary from the state

   
final_summary = current_state.get("summary")
   
print(f"[Callback] final_summary: {final_summary}")
   
if final_summary and is_done and isinstance(final_summary, str):
       
log.info(f"[Callback] Found final summary, constructing output Content.")
       
# Construct the final output Content object to be sent back
       
return types.Content(role="model", parts=[types.Part(text=final_summary.strip())])
   
else:
       
log.warning("[Callback] No final summary found in state or it's not a string.")
       
# Optionally return a default message or None if no summary was generated
       
return None

定义根循环代理

最后,我们定义主 LoopAgent。它会在每次循环迭代中按顺序协调子代理(profile_agent -> summary_agent -> check_agent -> CheckCondition)。它最多会重复此序列 max_iterations 次,或者直到 CheckCondition 发出完成信号。after_agent_callback 可确保返回最终摘要。

👉 在同一 ~/instavibe-bootstrap/agents/social/agent.py 中,将 #REPLACE FOR root_agent 替换为以下内容:

root_agent = LoopAgent(
   
name="InteractivePipeline",
   
sub_agents=[
       
profile_agent,
       
summary_agent,
       
check_agent,
       
CheckCondition(name="Checker")
   
],
   
description="Find everyone's social profile on events, post and friends",
   
max_iterations=10,
   
after_agent_callback=modify_output_after_agent
)

我们来使用 ADK 开发界面测试此多智能体工作流。

👉 启动 ADK Web 服务器:

. ~/instavibe-bootstrap/set_env.sh
source ~/instavibe-bootstrap/env/bin/activate
cd  ~/instavibe-bootstrap/agents
sed -i "s|^\(O\?GOOGLE_CLOUD_PROJECT\)=.*|GOOGLE_CLOUD_PROJECT=${PROJECT_ID}|" ~/instavibe-bootstrap/agents/social/.env
adk web

打开 ADK 开发者界面(通过网页预览访问端口 8000)。在客服人员下拉菜单(右上角)中,选择社交客服人员。

👉 现在,为其分配创建多位用户画像的任务。在聊天对话框中,输入以下内容:

Tell me about Mike and Bob

在客服人员回复后(由于循环和多次 LLM 调用,可能需要稍长时间),请不要只看最终的聊天输出。前往 ADK 开发者界面左侧窗格中的“Events”(事件)标签页。

👉 验证步骤:在“事件”标签页中,您会看到执行过程的详细分步轨迹。09-01-adk-dev-ui.png

观察代理如何在每次迭代中按顺序调用每个子代理(profile_agent、summary_agent、check_agent、Checker)。您可以查看每个步骤的输入、输出和状态,包括 profile_agent 09-02-ui-graph.png 进行的工具调用

以及来自 check_agent 和 CheckCondition 的状态更新。09-03-ui-state.png

在回调生成并返回最终摘要之前,此可视化轨迹对于了解和调试多代理工作流的运作方式非常有用。

浏览聊天回复和事件轨迹后,返回 Cloud Shell 终端,然后按 Ctrl+C 停止 ADK 开发者界面。

10. 代理到代理 (A2A) 通信

到目前为止,我们已经构建了专用代理,但它们在隔离状态下或在同一台机器上按照预定义的工作流运行。为了构建真正分布式且协作性的多代理系统,我们需要一种方法,让代理(可能作为单独的服务运行)能够相互发现并有效通信。这正是 Agent-to-Agent (A2A) 协议的用武之地。

A2A 协议是一种专为 AI 代理之间的互操作通信而设计的开放标准。MCP 侧重于代理与工具之间的互动,而 A2A 侧重于代理与代理之间的互动。代理可以通过此功能执行以下操作:

  • 探索:通过标准化的客服人员卡片查找其他客服人员并了解其能力。
  • 通信:安全地交换消息和数据。
  • 协作:委派任务和协调操作,以实现复杂的目标。

A2A 协议通过“代理卡片”等机制促进这种通信,代理可以使用这些机制宣传其功能和连接信息。

10-05-agent-card

A2A 利用熟悉的 Web 标准(HTTP、SSE、JSON-RPC),通常采用客户端-服务器模型,其中一个代理(客户端)将任务发送给另一个代理(远程代理/服务器)。这种标准化对于构建可扩缩的模块化系统至关重要,这样一来,独立开发的代理便可协同工作。

为 InstaVibe 代理启用 A2A

为了让其他智能客服可以通过 A2A 访问现有的 Planner、Platform Interaction 和 Social 智能客服,我们需要为每种智能客服封装一个 A2A 服务器组件。此服务器将:

  • 公开客服人员卡片:通过 HTTP 端点提供客服人员能力的标准说明。
  • 监听任务:根据 A2A 协议接受来自其他代理(A2A 客户端)的传入任务请求。
  • 管理任务执行:将收到的任务交给底层 ADK 代理逻辑进行处理。

规划者代理(启用了 A2A)

all-agent-planner

首先,我们将 A2A 服务器层添加到 Planner Agent。

首先,我们需要一个与 A2A 任务管理系统兼容的封装容器类。此类 PlannerAgent 本质上是将 A2A 服务器的任务处理与现有的 ADK 代理逻辑联系起来。

👉 在 ~/instavibe-bootstrap/agents/planner/planner_agent.py 中的 import 下方添加以下类定义:

class PlannerAgent(AgentWithTaskManager):
  """An agent to help user planning a night out with its desire location."""

 
SUPPORTED_CONTENT_TYPES = ["text", "text/plain"]

 
def __init__(self):
   
self._agent = self._build_agent()
   
self._user_id = "remote_agent"
   
self._runner = Runner(
       
app_name=self._agent.name,
       
agent=self._agent,
       
artifact_service=InMemoryArtifactService(),
       
session_service=InMemorySessionService(),
       
memory_service=InMemoryMemoryService(),
   
)

 
def get_processing_message(self) -> str:
     
return "Processing the planning request..."

 
def _build_agent(self) -> LoopAgent:
    """Builds the LLM agent for the night out planning agent."""
   
return agent.root_agent

接下来,我们定义 A2A 服务器启动逻辑。此代码定义了 AgentCard(代理的公开说明)、配置了 A2AServer 并启动了它,将其关联到我们的 PlannerAgent 封装容器。

👉 将以下代码添加到 ~/instavibe-bootstrap/agents/planner/a2a_server.py 的末尾:

def main():
   
try:
       
capabilities = AgentCapabilities(streaming=True)
       
skill = AgentSkill(
           
id="night_out_planner",
           
name="Night out planner",
           
description="""
            This agent generates multiple fun plan suggestions tailored to your specified location, dates, and interests,
            all designed for a moderate budget. It delivers detailed itineraries,
            including precise venue information (name, latitude, longitude, and description), in a structured JSON format.
            """,
           
tags=["instavibe"],
           
examples=["What about Bostona MA this weekend?"],
       
)
       
agent_card = AgentCard(
           
name="NightOut Planner Agent",
           
description="""
            This agent generates multiple fun plan suggestions tailored to your specified location, dates, and interests,
            all designed for a moderate budget. It delivers detailed itineraries,
            including precise venue information (name, latitude, longitude, and description), in a structured JSON format.
            """,
           
url=f"{PUBLIC_URL}",
           
version="1.0.0",
           
defaultInputModes=PlannerAgent.SUPPORTED_CONTENT_TYPES,
           
defaultOutputModes=PlannerAgent.SUPPORTED_CONTENT_TYPES,
           
capabilities=capabilities,
           
skills=[skill],
       
)
       
server = A2AServer(
           
agent_card=agent_card,
           
task_manager=AgentTaskManager(agent=PlannerAgent()),
           
host=host,
           
port=port,
       
)
       
logger.info(f"Attempting to start server with Agent Card: {agent_card.name}")
       
logger.info(f"Server object created: {server}")

       
server.start()
   
except Exception as e:
       
logger.error(f"An error occurred during server startup: {e}")
       
exit(1)

if __name__ == "__main__":
   
main()

👉 我们来快速测试一下 A2A 服务器是否在本地正确启动并提供其代理卡片。在第一个终端中,运行以下命令:

. ~/instavibe-bootstrap/set_env.sh
source ~/instavibe-bootstrap/env/bin/activate
cd ~/instavibe-bootstrap/agents/
python -m planner.a2a_server

👉 现在,打开另一个终端窗口。(点击终端面板中的加号)两个端子

👉 使用 curl 从本地运行的服务器请求代理卡片:

curl http://localhost:10003/agent-card | jq

您应该会看到我们定义的 AgentCard 的 JSON 表示法,确认服务器正在运行并广告 Planner 代理。

10-02-planner-a2a.png

返回第一个终端(服务器运行所在的终端),然后按 Ctrl+C 停止服务器。

👉 添加了 A2A 服务器逻辑后,我们现在可以构建容器映像了。

构建和部署 Planner 代理

. ~/instavibe-bootstrap/set_env.sh

cd ~/instavibe-bootstrap/agents

# Set variables specific to the PLANNER agent
export IMAGE_TAG="latest"
export AGENT_NAME="planner"
export IMAGE_NAME="planner-agent"
export IMAGE_PATH="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
export SERVICE_NAME="planner-agent"
export PUBLIC_URL="https://planner-agent-${PROJECT_NUMBER}.${REGION}.run.app"

echo "Building ${AGENT_NAME} agent..."
gcloud builds submit . \
 
--config=cloudbuild.yaml \
 
--project=${PROJECT_ID} \
 
--region=${REGION} \
 
--substitutions=_AGENT_NAME=${AGENT_NAME},_IMAGE_PATH=${IMAGE_PATH}

echo "Image built and pushed to: ${IMAGE_PATH}"

👉 并在 Cloud Run 上部署我们的 Planner Agent。

. ~/instavibe-bootstrap/set_env.sh

cd ~/instavibe-bootstrap/agents

# Set variables specific to the PLANNER agent
export IMAGE_TAG="latest"
export AGENT_NAME="planner"
export IMAGE_NAME="planner-agent"
export IMAGE_PATH="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
export SERVICE_NAME="planner-agent"
export PUBLIC_URL="https://planner-agent-${PROJECT_NUMBER}.${REGION}.run.app"


gcloud run deploy ${SERVICE_NAME} \
 
--image=${IMAGE_PATH} \
 
--platform=managed \
 
--region=${REGION} \
 
--set-env-vars="A2A_HOST=0.0.0.0" \
 
--set-env-vars="A2A_PORT=8080" \
 
--set-env-vars="GOOGLE_GENAI_USE_VERTEXAI=TRUE" \
 
--set-env-vars="GOOGLE_CLOUD_LOCATION=${REGION}" \
 
--set-env-vars="GOOGLE_CLOUD_PROJECT=${PROJECT_ID}" \
 
--set-env-vars="PUBLIC_URL=${PUBLIC_URL}" \
 
--allow-unauthenticated \
 
--project=${PROJECT_ID} \
 
--min-instances=1


export PLANNER_AGENT_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner-agent)

👉 我们来验证一下部署的服务是否正在运行,以及是否从云端正确提供其代理卡片。使用实际的服务网址提取卡片:

curl ${PLANNER_AGENT_URL}/agent-card | jq

这应该会显示相同的代理卡片 JSON,但这次是从您的实时 Cloud Run 实例提供。

平台互动代理(启用了 A2A)

all-agent-platform

接下来,我们将针对平台互动代理(使用 MCP 的代理)重复此过程。

👉 将 AgentWithTaskManager 封装容器类添加到导入内容下的 ~/instavibe-bootstrap/agents/platform_mcp_client/platform_agent.py

class PlatformAgent(AgentWithTaskManager):
  """An agent that post event and post to instavibe."""

 
SUPPORTED_CONTENT_TYPES = ["text", "text/plain"]

 
def __init__(self):
   
self._agent = self._build_agent()
   
self._user_id = "platform_agent"
   
self._runner = Runner(
       
app_name=self._agent.name,
       
agent=self._agent,
       
artifact_service=InMemoryArtifactService(),
       
session_service=InMemorySessionService(),
       
memory_service=InMemoryMemoryService(),
   
)

 
def get_processing_message(self) -> str:
     
return "Processing the social post and event request..."

 
def _build_agent(self) -> LlmAgent:
    """Builds the LLM agent for the Processing the social post and event request."""
   
return agent.root_agent

👉 在 ~/instavibe-bootstrap/agents/platform_mcp_client/a2a_server.py 的末尾定义 A2A 服务器设置,包括其唯一的 AgentCard:

def main():
   
try:
       
capabilities = AgentCapabilities(streaming=True)
       
skill = AgentSkill(
           
id="instavibe_posting",
           
name="Post social post and events on instavibe",
           
description="""
            This "Instavibe" agent helps you create posts (identifying author, text, and sentiment – inferred if unspecified) and register
            for events (gathering name, date, attendee). It efficiently collects required information and utilizes dedicated tools
            to perform these actions on your behalf, ensuring a smooth sharing experience.
            """,
           
tags=["instavibe"],
           
examples=["Create a post for me, the post is about my cute cat and make it positive, and I'm Alice"],
       
)
       
agent_card = AgentCard(
           
name="Instavibe Posting Agent",
           
description="""
            This "Instavibe" agent helps you create posts (identifying author, text, and sentiment – inferred if unspecified) and register
            for events (gathering name, date, attendee). It efficiently collects required information and utilizes dedicated tools
            to perform these actions on your behalf, ensuring a smooth sharing experience.
            """,
           
url=f"{PUBLIC_URL}",
           
version="1.0.0",
           
defaultInputModes=PlatformAgent.SUPPORTED_CONTENT_TYPES,
           
defaultOutputModes=PlatformAgent.SUPPORTED_CONTENT_TYPES,
           
capabilities=capabilities,
           
skills=[skill],
       
)
       
server = A2AServer(
           
agent_card=agent_card,
           
task_manager=AgentTaskManager(agent=PlatformAgent()),
           
host=host,
           
port=port,
       
)
       
server.start()
   
except Exception as e:
       
logger.error(f"An error occurred during server startup: {e}")
       
exit(1)

if __name__ == "__main__":
   
main()

构建和部署平台互动代理

此服务需要在部署期间设置 MCP_SERVER_URL 环境变量,以便连接到我们之前部署的 MCP 服务器。👉 现在,我们可以构建容器映像,并将代理部署为自己的 Cloud Run 服务:

. ~/instavibe-bootstrap/set_env.sh

cd ~/instavibe-bootstrap/agents

export IMAGE_TAG="latest"

export AGENT_NAME="platform_mcp_client"
export IMAGE_NAME="platform_mcp_client-agent"
export IMAGE_PATH="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
export MCP_SERVER_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep mcp-tool-server)/sse
export SERVICE_NAME="platform-mcp-client" # Define the service name
export PUBLIC_URL="https://platform-mcp-client-${PROJECT_NUMBER}.${REGION}.run.app"


echo "Building ${AGENT_NAME} agent..."
gcloud builds submit . \
 
--project=${PROJECT_ID} \
 
--region=${REGION} \
 
--config=cloudbuild.yaml \
 
--substitutions=_AGENT_NAME=${AGENT_NAME},_IMAGE_PATH=${IMAGE_PATH}


echo "Deploying ${SERVICE_NAME} to Cloud Run..."
gcloud run deploy ${SERVICE_NAME} \
 
--image=${IMAGE_PATH} \
 
--platform=managed \
 
--region=${REGION} \
 
--allow-unauthenticated \
 
--project=${PROJECT_ID} \
 
--set-env-vars="MCP_SERVER_URL=${MCP_SERVER_URL}"  \
 
--set-env-vars="A2A_HOST=0.0.0.0" \
 
--set-env-vars="A2A_PORT=8080" \
 
--set-env-vars="GOOGLE_GENAI_USE_VERTEXAI=TRUE" \
 
--set-env-vars="GOOGLE_CLOUD_LOCATION=${REGION}" \
 
--set-env-vars="GOOGLE_CLOUD_PROJECT=${PROJECT_ID}" \
 
--set-env-vars="PUBLIC_URL=${PUBLIC_URL}" \
 
--min-instances=1

👉 查看已部署的平台代理的代理卡片:

export PLATFORM_MPC_CLIENT_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep platform-mcp-client)

curl $PLATFORM_MPC_CLIENT_URL/agent-card | jq

10-03-platform-a2a.png

社交客服(启用了 A2A)

all-agent-social

最后,为社交媒体画像代理启用 A2A。

👉 将 AgentWithTaskManager 封装容器添加到 ~/instavibe-bootstrap/agents/social/social_agent.py 的导入内容下:

class SocialAgent(AgentWithTaskManager):
  """An agent that handles social profile analysis."""

 
SUPPORTED_CONTENT_TYPES = ["text", "text/plain"]

 
def __init__(self):
   
self._agent = self._build_agent()
   
self._user_id = "remote_agent"
   
self._runner = Runner(
       
app_name=self._agent.name,
       
agent=self._agent,
       
artifact_service=InMemoryArtifactService(),
       
session_service=InMemorySessionService(),
       
memory_service=InMemoryMemoryService(),
   
)

 
def get_processing_message(self) -> str:
     
return "Processing the social profile analysis request..."

 
def _build_agent(self) -> LoopAgent:
    """Builds the LLM agent for the social profile analysis agent."""
   
return agent.root_agent

👉 在 ~/instavibe-bootstrap/agents/social/a2a_server.py 的末尾定义 A2A 服务器设置和 AgentCard:

def main():
   
try:
       
capabilities = AgentCapabilities(streaming=True)
       
skill = AgentSkill(
           
id="social_profile_analysis",
           
name="Analyze Instavibe social profile",
           
description="""
            Using a provided list of names, this agent synthesizes Instavibe social profile information by analyzing posts, friends, and events.
            It delivers a comprehensive single-paragraph summary for individuals, and for groups, identifies commonalities in their social activities
            and connections based on profile data.
            """,
           
tags=["instavibe"],
           
examples=["Can you tell me about Bob and Alice?"],
       
)
       
agent_card = AgentCard(
           
name="Social Profile Agent",
           
description="""
            Using a provided list of names, this agent synthesizes Instavibe social profile information by analyzing posts, friends, and events.
            It delivers a comprehensive single-paragraph summary for individuals, and for groups, identifies commonalities in their social activities
            and connections based on profile data.
            """,
           
url=f"{PUBLIC_URL}",
           
version="1.0.0",
           
defaultInputModes=SocialAgent.SUPPORTED_CONTENT_TYPES,
           
defaultOutputModes=SocialAgent.SUPPORTED_CONTENT_TYPES,
           
capabilities=capabilities,
           
skills=[skill],
       
)
       
server = A2AServer(
           
agent_card=agent_card,
           
task_manager=AgentTaskManager(agent=SocialAgent()),
           
host=host,
           
port=port,
       
)
       
server.start()
   
except Exception as e:
       
logger.error(f"An error occurred during server startup: {e}")
       
exit(1)

if __name__ == "__main__":
   
main()

构建和部署社交代理服务

此代理需要访问 Spanner,因此请确保在部署期间正确传递 SPANNER_INSTANCE_ID 和 SPANNER_DATABASE_ID 变量。

👉 构建社交代理并部署到 Cloud Run:

. ~/instavibe-bootstrap/set_env.sh

cd ~/instavibe-bootstrap/agents

export IMAGE_TAG="latest"

export AGENT_NAME="social"
export IMAGE_NAME="social-agent"
export IMAGE_PATH="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
export SERVICE_NAME="social-agent"
export PUBLIC_URL="https://social-agent-${PROJECT_NUMBER}.${REGION}.run.app"


echo "Building ${AGENT_NAME} agent..."
gcloud builds submit . \
 
--config=cloudbuild.yaml \
 
--project=${PROJECT_ID} \
 
--region=${REGION} \
 
--substitutions=_AGENT_NAME=${AGENT_NAME},_IMAGE_PATH=${IMAGE_PATH}

echo "Deploy ${AGENT_NAME} agent..."
gcloud run deploy ${SERVICE_NAME} \
 
--image=${IMAGE_PATH} \
 
--platform=managed \
 
--region=${REGION} \
 
--set-env-vars="SPANNER_INSTANCE_ID=${SPANNER_INSTANCE_ID}" \
 
--set-env-vars="SPANNER_DATABASE_ID=${SPANNER_DATABASE_ID}" \
 
--set-env-vars="GOOGLE_CLOUD_PROJECT=${PROJECT_ID}" \
 
--set-env-vars="A2A_HOST=0.0.0.0" \
 
--set-env-vars="A2A_PORT=8080" \
 
--set-env-vars="PUBLIC_URL=${PUBLIC_URL}" \
 
--set-env-vars="GOOGLE_GENAI_USE_VERTEXAI=TRUE" \
 
--set-env-vars="GOOGLE_CLOUD_LOCATION=${REGION}" \
 
--allow-unauthenticated \
 
--project=${PROJECT_ID} \
 
--min-instances=1

👉 查看已部署的社交客服人员的客服人员卡片:

export SOCIAL_AGENT_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep social-agent)

curl $SOCIAL_AGENT_URL/agent-card | jq

10-04-social-a2a.png

11. Orchestrator Agent(A2A 客户端)

现在,我们有 3 个专用代理(Planner、Platform、Social)在 Cloud Run 上作为独立的支持 A2A 的服务运行。最后一部分是 Orchestrator 代理。此代理将充当中央协调者或 A2A 客户端。它会接收用户请求,确定执行请求所需的远程代理(可能按顺序),然后使用 A2A 协议将任务委托给这些远程代理。在本研讨会中,我们将使用 ADK 开发者界面在本地运行 Orchestrator 代理。

all-agent-orchestrator

首先,我们来增强 Orchestrator 的逻辑,以处理它发现的远程代理的注册。此函数 (register_agent_card) 会存储从提取的代理卡片中获取的连接详情。

👉 在 ~/instavibe-bootstrap/agents/orchestrate/host_agent.py 中,将 #REPLACE ME REG AGENT CARD 替换为:

  def register_agent_card(self, card: AgentCard):
   
remote_connection = RemoteAgentConnections(card)
   
self.remote_agent_connections[card.name] = remote_connection
   
self.cards[card.name] = card
   
agent_info = []
   
for ra in self.list_remote_agents():
     
agent_info.append(json.dumps(ra))
   
self.agents = '\n'.join(agent_info)

接下来,在 ADK 中定义编排器代理本身。请注意其工具:

  • list_remote_agents(了解 Planner、Platform、Social 代理)
  • send_task(用于委托工作的 A2A 函数)。

👉 将 ~/instavibe-bootstrap/agents/orchestrate/host_agent.py 中的 ##REPLACE ME CREATE AGENT 替换为:

  def create_agent(self) -> Agent:
   
return Agent(
       
model="gemini-2.0-flash",
       
name="orchestrate_agent",
       
instruction=self.root_instruction,
       
before_model_callback=self.before_model_callback,
       
description=(
           
"This agent orchestrates the decomposition of the user request into"
           
" tasks that can be performed by the child agents."
       
),
       
tools=[
           
self.list_remote_agents,
           
self.send_task,
       
],
   
)

编排器的核心逻辑在于其指令,这些指令会告知编排器如何使用 A2A。

👉 将 ~/instavibe-bootstrap/agents/orchestrate/host_agent.py 中的 #REPLACE ME INSTRUCTIONS 替换为此指令生成方法:

  def root_instruction(self, context: ReadonlyContext) -> str:
   
current_agent = self.check_state(context)
   
return f"""

    You are an expert AI Orchestrator. Your primary responsibility is to intelligently interpret user requests, plan the necessary sequence of actions if multiple steps are involved, and delegate them to the most appropriate specialized remote agents. You do not perform the tasks yourself but manage their assignment, sequence, and can monitor their status.

    Core Workflow & Decision Making:

    1.  **Understand User Intent & Complexity:**
        *   Carefully analyze the user's request to determine the core task(s) they want to achieve. Pay close attention to keywords and the overall goal.
        *   **Identify if the request requires a single agent or a sequence of actions from multiple agents.** For example, "Analyze John Doe's profile and then create a positive post about his recent event attendance" would require two agents in sequence.

    2.  **Agent Discovery & Selection:**
        *   Use `list_remote_agents` to get an up-to-date list of available remote agents and understand their specific capabilities (e.g., what kind of requests each agent is designed to handle and what data they output).
        *   Based on the user's intent:
            *   For **single-step requests**, select the single most appropriate agent.
            *   For **multi-step requests**, identify all necessary agents and determine the logical order of their execution.

    3.  **Task Planning & Sequencing (for Multi-Step Requests):**
        *   Before delegating, outline the sequence of agent tasks.
        *   Identify dependencies: Does Agent B need information from Agent A's completed task?
        *   Plan to execute tasks sequentially if there are dependencies, waiting for the completion of a prerequisite task before initiating the next one.

    4.  **Task Delegation & Management:**
        *   **For New Single Requests or the First Step in a Sequence:** Use `create_task`. Your `create_task` call MUST include:
            *   The `remote_agent_name` you've selected.
            *   The `user_request` or all necessary parameters extracted from the user's input, formatted in a way the target agent will understand.
        *   **For Subsequent Steps in a Sequence:**
            *   Wait for the preceding task to complete (you may need to use `check_pending_task_states` to confirm completion).
            *   Once the prerequisite task is done, gather any necessary output from it.
            *   Then, use `create_task` for the next agent in the sequence, providing it with the user's original relevant intent and any necessary data obtained from the previous agent's task.
        *   **For Ongoing Interactions with an Active Agent (within a single step):** If the user is providing follow-up information related to a task *currently assigned* to a specific agent, use the `update_task` tool.
        *   **Monitoring:** Use `check_pending_task_states` to check the status of any delegated tasks, especially when managing sequences or if the user asks for an update.

    **Communication with User:**

    *   When you delegate a task (or the first task in a sequence), clearly inform the user which remote agent is handling it.
    *   For multi-step requests, you can optionally inform the user of the planned sequence (e.g., "Okay, first I'll ask the 'Social Profile Agent' to analyze the profile, and then I'll have the 'Instavibe Posting Agent' create the post.").
    *   If waiting for a task in a sequence to complete, you can inform the user (e.g., "The 'Social Profile Agent' is currently processing. I'll proceed with the post once that's done.").
    *   If the user's request is ambiguous, if necessary information is missing for any agent in the sequence, or if you are unsure about the plan, proactively ask the user for clarification.
    *   Rely strictly on your tools and the information they provide.

    **Important Reminders:**
    *   Always prioritize selecting the correct agent(s) based on their documented purpose.
    *   Ensure all information required by the chosen remote agent is included in the `create_task` or `update_task` call, including outputs from previous agents if it's a sequential task.
    *   Focus on the most recent parts of the conversation for immediate context, but maintain awareness of the overall goal, especially for multi-step requests.

    Agents:
    {self.agents}

    Current agent: {current_agent['active_agent']}
    """

测试 Orchestrator 和完整的 A2A 系统

现在,我们来测试整个系统。我们将使用 ADK 开发者界面在本地运行 Orchestrator,它将与在 Cloud Run 上远程运行的 Planner、Platform 和 Social 代理进行通信。

👉 首先,确保环境变量 REMOTE_AGENT_ADDRESSES 包含已部署的支持 A2A 的代理的逗号分隔的网址。然后,为 Orchestrator 代理设置必要的环境变量,并启动 ADK 开发者界面:

. ~/instavibe-bootstrap/set_env.sh
source ~/instavibe-bootstrap/env/bin/activate

export PLATFORM_MPC_CLIENT_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep platform-mcp-client)

curl $PLATFORM_MPC_CLIENT_URL/agent-card | jq

export PLANNER_AGENT_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner-agent)
export SOCIAL_AGENT_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep social-agent)

export REMOTE_AGENT_ADDRESSES=${PLANNER_AGENT_URL},${PLATFORM_MPC_CLIENT_URL},${SOCIAL_AGENT_URL}

cd  ~/instavibe-bootstrap/agents
sed -i "s|^\(O\?GOOGLE_CLOUD_PROJECT\)=.*|GOOGLE_CLOUD_PROJECT=${PROJECT_ID}|" ~/instavibe-bootstrap/agents/orchestrate/.env
sed -i "s|^\(O\?REMOTE_AGENT_ADDRESSES\)=.*|REMOTE_AGENT_ADDRESSES=${REMOTE_AGENT_ADDRESSES}|" ~/instavibe-bootstrap/agents/orchestrate/.env
adk web

👉 打开 ADK 开发者界面(通过网页预览访问端口 8000)。在代理下拉菜单中,选择编排代理。

👉 现在,为其分配一项需要协调多个远程代理的复杂任务。请先试用以下示例,其中应涉及社交代理和规划者代理:

You are an expert event planner for a user named  Diana.
   
Your task is to design a fun and personalized night out.

   
Here are the details for the plan:
   
- Friends to invite: Ian, Nora
   
- Desired date: "2025-6-15"
   
- Location idea or general preference: "Chicago"

   
Your process should be:
   
1. Analyze the provided friend names. If you have access to a tool to get their InstaVibe profiles or summarized interests, please use it.
   
2. Based on their potential interests (or general good taste if profiles are unavailable), create a tailored plan for the outing.
   
3. Ensure the plan includes the original `planned_date` provided: "2025-6-15" .
   
4. Organize all details into a structured JSON format as specified below.

   
The user wants a comprehensive plan that includes:
   
- The list of invited friends.
   
- A catchy and descriptive name for the event.
   
- The exact planned date for the event (which is"2025-6-15").
   
- A summary of what the group will do.
   
- Specific recommended spots (e.g., restaurants, bars, activity venues) with their names, (if possible, approximate latitude/longitude for mapping, and address), and a brief description of why it fits the plan.
   
- A short, exciting message that {Diana} can send to {Ian, Nora} to get them excited about the event.

编排

在 ADK 开发者界面聊天窗口中观察互动。请密切关注 Orchestrator 的响应,其中应说明它将任务委托给了哪个远程代理(例如,“好的,我先向社交媒体个人资料客服人员询问一下 Ian 和 Nora...”)。

此外,请查看界面中的“事件”标签页,了解向远程代理网址发出的底层工具调用 (send_task)。

“发送任务”

👉 现在,请尝试第二个示例,该示例应直接涉及平台集成代理:

Hey, can you set up an event for Laura and Charlie? Let's call it 'Vienna Concert & Castles Day'.
here are more info
"event_name": "Vienna Concert & Castles Day",
 
"description": "A refined and unforgettable day in Vienna with Laura and Charlie. The day begins with a guided tour of the magnificent Schönbrunn Palace, showcasing imperial architecture and history. In the evening, enjoy a classical music concert in one of Vienna's most iconic concert halls.",
 
"event_date": "2025-06-14T10:00:00+02:00",
 
"locations": [
   
{
     
"name": "Schönbrunn Palace",
     
"description": "A UNESCO World Heritage Site and former imperial summer residence, Schönbrunn Palace offers opulent rooms, beautiful baroque gardens, and a glimpse into the life of the Habsburg monarchy. Visitors can stroll the grounds or take a guided historical tour.",
     
"latitude": 48.184516,
     
"longitude": 16.312222,
     
"address": "Schönbrunner Schloßstraße 47, 1130 Wien, Austria"
   
},
   
{
     
"name": "Musikverein Vienna",
     
"description": "Home to the world-renowned Vienna Philharmonic, the Musikverein is one of the finest concert halls in the world. Its 'Golden Hall' is famous for its acoustics and ornate design. Attendees can enjoy a powerful classical concert in an unforgettable setting.",
     
"latitude": 48.200132,
     
"longitude": 16.373777,
     
"address": "Musikvereinsplatz 1, 1010 Wien, Austria"
   
}
 
],
 
"attendee_names": ["Laura", "Charlie", "Oscar"] And I am Oscar

再次监控聊天和“事件”标签页。编排器应确定是否需要创建事件,并将任务(包括所有提供的详细信息)委托给“InstaVibe 平台互动代理”。发送事件

然后,您可以验证该事件是否显示在 InstaVibe Web 应用中。InstaVibe 活动

此图演示了使用 ADK 和 A2A 协议成功实现多代理系统,其中中央协调程序将任务委托给专门的远程代理。

请务必在测试完成后停止 ADK 开发者界面(终端中的 Ctrl+C)。

12. InstaVibe 中的客服人员引擎和远程通话

到目前为止,我们已在 Cloud Run 上运行专用代理,并使用 ADK 开发者界面在本地测试了 Orchestrator。对于生产场景,我们需要一个强大、可扩缩且受管理的环境来托管代理。这正是 Google Vertex AI Agent Engine 发挥作用之处。

Agent Engine 是 Vertex AI 上的一项全托管式服务,专为部署和扩缩 AI 代理而设计。它抽象化了基础架构管理、安全性和运营开销,让开发者(尤其是不太熟悉复杂云环境的开发者)可以专注于代理的逻辑和功能,而不是管理服务器。它提供了专为代理工作负载优化的专用运行时。

现在,我们将 Orchestrator 代理部署到 Agent Engine。(注意:下方所示的部署机制使用的是本研讨会材料中提供的自定义脚本 (agent_engine_app.py),因为官方的直接 ADK 到 Agent-Engine 部署工具可能仍在开发中。此脚本负责封装和部署使用必要远程代理地址进行配置的 Orchestrator 代理。)

执行以下命令将 Orchestrator 代理部署到 Agent Engine。确保上一部分中设置的 REMOTE_AGENT_ADDRESSES 环境变量(包含 Planner、平台和社交媒体代理在 Cloud Run 上的网址)仍然正确设置。

我们将 Orchestrate 代理部署到 Agent Engine(注意:这是我自己实现的部署,但我希望 Google 将来会推出官方部署)。

cd ~/instavibe-bootstrap/agents/
. ~/instavibe-bootstrap/set_env.sh
source ~/instavibe-bootstrap/env/bin/activate
export PLANNER_AGENT_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner-agent)
export PLATFORM_MPC_CLIENT_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep platform-mcp-client)
export SOCIAL_AGENT_URL=$(gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep social-agent)

export REMOTE_AGENT_ADDRESSES=${PLANNER_AGENT_URL},${PLATFORM_MPC_CLIENT_URL},${SOCIAL_AGENT_URL}

python -m app.agent_engine_app \
 
--set-env-vars "AGENT_BASE_URL=${REMOTE_AGENT_ADDRESSES}"

(等待脚本完成部署流程。这可能需要几分钟。)

现在,Orchestrator 托管在托管式 Agent Engine 平台上,我们的 InstaVibe Web 应用需要与其通信。该 Web 应用将远程调用 Agent Engine 端点,而不是通过 ADK 开发者界面进行交互。

10-agent-remote.png

首先,我们需要修改 InstaVibe 应用代码,以使用已部署的 Orchestrator 代理的唯一 ID 初始化 Agent Engine 客户端。此 ID 是定位平台上正确代理实例的必需条件。

打开 ~/instavibe-bootstrap/instavibe/introvertally.py 并将 #REPLACE ME initiate agent_engine 替换为以下代码。这会从环境变量(我们稍后会进行设置)中检索 Agent Engine ID,并获取客户端对象:

ORCHESTRATE_AGENT_ID = os.environ.get('ORCHESTRATE_AGENT_ID')
agent_engine = agent_engines.get(ORCHESTRATE_AGENT_ID)

我们在 InstaVibe 中规划的用户体验流程涉及与客服人员的两次互动:首先,生成推荐方案;其次,在客服人员实际将活动发布到平台之前,要求用户进行确认。

由于 InstaVibe Web 应用(在 Cloud Run 上运行)和 Orchestrator 代理(在 Agent Engine 上运行)现在是单独的服务,因此 Web 应用需要向 Agent Engine 端点发出远程调用才能与代理交互。

我们来更新用于进行初始调用以生成方案建议的代码。在同一 introvertally.py 文件中,将 #REPLACE ME Query remote agent get plan 替换为以下代码段,该代码段使用 agent_engine 客户端发送用户的请求:

agent_engine.stream_query(
               
user_id=user_id,
               
message=prompt_message,
           
)

接下来,更新用于处理用户确认(例如,用户点击“确认方案”时)的代码。这会向 Agent Engine 上的同一对话发送跟进消息,指示编排器继续发布事件(该事件将委托给平台集成代理)。将 introvertally.py 中的确认 #REPLACE ME Query remote agent for confirmation 替换为:

agent_engine.stream_query(
           
user_id=agent_session_user_id,
           
message=prompt_message,
       
)

网站应用的路由需要访问这些函数。确保在 Flask 路由文件中导入 introvertally.py 中的必要函数。

cd ~/instavibe-bootstrap/instavibe/ally_routes.py 中,我们先将 # REPLACE ME TO ADD IMPORT 替换为指向实例的代码:

from introvertally import call_agent_for_plan, post_plan_event

将原型功能添加到 InstaVibe,在 ~/instavibe-bootstrap/instavibe/templates/base.html 中,将 <!–REPLACE_ME_LINK_TO_INTROVERT_ALLY–> 替换为以下内容:

            <li class="nav-item">
             
<a class="nav-link" href="{{ url_for('ally.introvert_ally_page') }}">Introvert Ally</a>
           
</li>

在重新部署 InstaVibe 应用之前,我们需要获取部署到 Agent Engine 的 Orchestrator 代理的具体 Resource ID

目前,通过 gcloud 以编程方式检索此 ID 可能受到限制,因此我们将使用辅助 Python 脚本(在本研讨会中提供的 temp-endpoint.py)提取 ID 并将其存储在环境变量中。

执行以下命令以运行脚本,捕获代理引擎端点 ID

. ~/instavibe-bootstrap/set_env.sh
cd ~/instavibe-bootstrap/instavibe/
source ~/instavibe-bootstrap/env/bin/activate
python temp-endpoint.py
export ORCHESTRATE_AGENT_ID=$(cat temp_endpoint.txt)
echo "ORCHESTRATE_AGENT_ID set to: ${ORCHESTRATE_AGENT_ID}"

代理引擎端点 ID

最后,我们需要使用更新后的代码和新的 ORCHESTRATE_AGENT_ID 环境变量重新部署 InstaVibe Web 应用,以便它知道如何连接到在 Agent Engine 上运行的代理。

以下命令会重新构建 InstaVibe 应用映像,并将新版本部署到 Cloud Run:

. ~/instavibe-bootstrap/set_env.sh

cd ~/instavibe-bootstrap/instavibe/

export IMAGE_TAG="latest"
export APP_FOLDER_NAME="instavibe"
export IMAGE_NAME="instavibe-webapp"
export IMAGE_PATH="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${IMAGE_NAME}:${IMAGE_TAG}"
export SERVICE_NAME="instavibe"

echo "Building ${APP_FOLDER_NAME} webapp image..."
gcloud builds submit . \
 
--tag=${IMAGE_PATH} \
 
--project=${PROJECT_ID}

echo "Deploying ${SERVICE_NAME} to Cloud Run..."

gcloud run deploy ${SERVICE_NAME} \
 
--image=${IMAGE_PATH} \
 
--platform=managed \
 
--region=${REGION} \
 
--allow-unauthenticated \
 
--set-env-vars="SPANNER_INSTANCE_ID=${SPANNER_INSTANCE_ID}" \
 
--set-env-vars="SPANNER_DATABASE_ID=${SPANNER_DATABASE_ID}" \
 
--set-env-vars="APP_HOST=0.0.0.0" \
 
--set-env-vars="APP_PORT=8080" \
 
--set-env-vars="GOOGLE_CLOUD_LOCATION=${REGION}" \
 
--set-env-vars="GOOGLE_CLOUD_PROJECT=${PROJECT_ID}" \
 
--set-env-vars="GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY}" \
 
--set-env-vars="ORCHESTRATE_AGENT_ID=${ORCHESTRATE_AGENT_ID}" \
 
--project=${PROJECT_ID} \
 
--min-instances=1 \
 
--cpu=2 \
 
--memory=2Gi

最终部署完成后,在另一个浏览器标签页中打开您的 InstaVibe 应用网址。

测试依托 AI 技术的完整 InstaVibe 体验

“InstaVibe Ally”功能现已发布,由通过 Vertex AI Agent Engine 协调的多智能体系统提供支持,并通过 A2A 进行通信。

12-02-new.png

点击“InstaVibe Ally”,然后让它规划活动。

12-03-introvertally.png

在代理处理问题期间,观察右侧的活动日志(可能需要 90-120 秒)。方案显示后,请查看该方案,然后点击“确认此方案”以继续发布。

12-04-confirm.png

现在,编排器将指示平台代理在 InstaVibe 中创建帖子和活动。12-05-posting.png

请访问 InstaVibe 首页,查看新帖子和活动。12-06-instavibe.png

事件页面将反映代理生成的详细信息。

12-07-event.png

使用 Cloud Trace 分析性能

您可能会注意到此过程需要一些时间。Vertex AI Agent Engine 与 Cloud Trace 集成,可让我们分析多智能体系统的延迟时间。

前往 Google Cloud 控制台中的轨迹,在 Span 中选择 agent_run[orchestrate_agent],您应该会看到几个 Span,点击其中一个

12-08-trace.png

在轨迹详情中,您可以确定哪些部分花费的时间较长。例如,由于搜索着陆页和复杂的生成,对 Planner 代理的调用可能会显示较长的延迟时间。12-09-plan.png

同样,在创建帖子和事件时,您可能会看到 Orchestrator 处理数据和为平台代理准备工具调用所花费的时间。12-10-post.png

探索这些轨迹有助于了解和优化代理系统的性能。

celebrate.png

恭喜!您已使用 Google 的 ADK、A2A、MCP 和 Google Cloud 服务成功构建、部署和测试了复杂的多代理 AI 系统。您已解决代理编排、工具使用、状态管理和云部署问题,为 InstaVibe 创建了一项功能强大的 AI 赋能型功能。恭喜您完成本研讨会!

13. 清理

为避免系统持续向您的 Google Cloud 账号收取费用,请务必删除我们在本研讨会期间创建的资源。以下命令可帮助您移除 Spanner 实例、Cloud Run 服务、Artifact Registry 仓库、API 密钥、Vertex AI Agent Engine 以及关联的 IAM 权限。

重要提示:

  • 请确保您是在本研讨会所用的 Google Cloud 项目中运行这些命令。
  • 如果您关闭了 Cloud Shell 终端,则可能未设置 $PROJECT_ID、$SPANNER_INSTANCE_ID 等某些环境变量。您需要像在本研讨会设置期间所做的那样重新导出它们,或者将以下命令中的变量替换为它们的实际值。
  • 这些命令会永久删除您的资源。如果此项目中还有其他重要数据,请在运行前仔细检查。

重置环境变量

. ~/instavibe-bootstrap/set_env.sh

删除 Agent Engine:

cd ~/instavibe-bootstrap/runners
source ~/instavibe-bootstrap/env/bin/activate
export ORCHESTRATE_AGENT_ID=$(cat ~/instavibe-bootstrap/instavibe/temp_endpoint.txt)
echo "ORCHESTRATE_AGENT_ID set to: ${ORCHESTRATE_AGENT_ID}"
python remote_delete.py
deactivate
echo "Vertex AI Agent Engine deletion initiated."

删除 Cloud Run 服务:

我们将多项服务部署到了 Cloud Run。我们来逐一删除它们。

# InstaVibe Web Application
gcloud run services delete instavibe --platform=managed --region=${REGION} --project=${PROJECT_ID} --quiet

# MCP Tool Server
gcloud run services delete mcp-tool-server --platform=managed --region=${REGION} --project=${PROJECT_ID} --quiet

# Planner Agent (A2A Server)
gcloud run services delete planner-agent --platform=managed --region=${REGION} --project=${PROJECT_ID} --quiet

# Platform MCP Client Agent (A2A Server)
gcloud run services delete platform-mcp-client --platform=managed --region=${REGION} --project=${PROJECT_ID} --quiet

# Social Agent (A2A Server)
gcloud run services delete social-agent --platform=managed --region=${REGION} --project=${PROJECT_ID} --quiet

echo "Cloud Run services deletion initiated."

删除 Spanner 实例:

echo "Deleting Spanner instance: ${SPANNER_INSTANCE_ID}..."
gcloud spanner instances delete ${SPANNER_INSTANCE_ID} --project=${PROJECT_ID} --quiet
echo "Spanner instance deletion initiated."

删除 Artifact Registry 代码库:

echo "Deleting Artifact Registry repository: ${REPO_NAME}..."
gcloud artifacts repositories delete ${REPO_NAME} --location=${REGION} --project=${PROJECT_ID} --quiet
echo "Artifact Registry repository deletion initiated."

删除本地 Workshop 文件:

echo "Removing local workshop directory ~/instavibe-bootstrap..."
rm -rf ~/instavibe-bootstrap
rm -f ~/mapkey.txt
rm -f ~/project_id.txt
echo "Local directory removed."

清理