1. 简介
在此 Codelab 中,您将为咖啡店构建一个交互式 AI Barista 代理。您将使用 Google 的开源智能体开发套件 (ADK) 和 Gemini 3.5 Flash 模型,实现检索增强生成 (RAG),以便根据模拟菜单数据集为智能体推荐内容提供依据。最后,您将使用 Streamlit 界面封装智能体,并将其部署到 Cloud Run。
您将执行的操作
- 创建包含咖啡商品、标签和过敏原的 RAG 数据源 (
menu.json)。 - 使用 ADK
LlmAgent构建 AI 智能体,并连接 Python 工具以加载菜单数据。 - 将代理封装在管理对话历史记录的 Streamlit 聊天应用中。
- 使用基于源代码的部署将 Streamlit 应用部署到 Cloud Run。
- 测试 RAG 接地和过敏原意识。

所需条件
- 网络浏览器,例如 Chrome。
- 启用了结算功能的 Google Cloud 项目。
- 基本熟悉 Python。
本 Codelab 适合各种水平的开发者,包括新手。
预计费用:不到 1.00 美元。
2. 准备工作
创建 Google Cloud 项目
- 在 Google Cloud 控制台中,选择或创建 Google Cloud 项目。
- 确保您的 Cloud 项目已启用结算功能。
启动 Cloud Shell
- 点击 Google Cloud 控制台顶部的激活 Cloud Shell。

- 验证身份验证:

gcloud auth list
- 确认已设置活跃项目:
gcloud config get project
如果显示的项目 ID 不正确或未设置任何项目 ID,请运行:
gcloud config set project <YOUR_PROJECT_ID>
启用 API
运行以下命令可启用所有必需的 API:
gcloud services enable \
run.googleapis.com \
aiplatform.googleapis.com \
cloudbuild.googleapis.com
3. 设置项目
在此步骤中,您将初始化项目环境变量并为项目创建工作目录。
- 在活跃的 Cloud Shell 会话中,初始化以下项目环境变量:
export PROJECT_ID=$(gcloud config get-value project)
注意:使用最近的区域
找到最近的区域,并在以下命令中将 insert-region-here 替换为该区域:
export REGION=[insert-region-here]
- 创建并切换到名为
coffee-barista-agent的新项目目录:
mkdir coffee-barista-agent && cd coffee-barista-agent
4. 创建模拟菜单数据源
为了让 AI Barista 能够根据实际情况提供服务,并防止其虚构不存在的商品,您需要创建一个本地菜单数据集。代理将在运行时通过自定义工具读取此文件。
- 在 Cloud Shell 编辑器中创建并打开
menu.json:
cloudshell edit menu.json
- 将以下 JSON 内容粘贴到编辑器中,然后保存文件:
[
{
"name": "Espresso Solo",
"description": "A single shot of rich, bold espresso.",
"price": 2.50,
"tags": ["strong", "hot", "dairy-free", "sugar-free"],
"allergens": []
},
{
"name": "Oat Milk Honey Latte",
"description": "Creamy steamed oat milk with espresso and a touch of honey.",
"price": 5.00,
"tags": ["sweet", "hot", "dairy-free"],
"allergens": []
},
{
"name": "Cold Brew Coffee",
"description": "Smooth, slow-steeped cold brew served over ice.",
"price": 4.00,
"tags": ["strong", "cold", "dairy-free", "sugar-free"],
"allergens": []
},
{
"name": "Seasonal Pumpkin Latte",
"description": "Spiced pumpkin sauce, espresso, and steamed milk, topped with whipped cream.",
"price": 5.50,
"tags": ["sweet", "hot", "seasonal"],
"allergens": ["dairy"]
},
{
"name": "Classic Croissant",
"description": "Flaky, buttery traditional French pastry.",
"price": 3.50,
"tags": ["bakery", "savory"],
"allergens": ["wheat", "dairy"]
},
{
"name": "Vegan Blueberry Muffin",
"description": "Soft, sweet muffin packed with real blueberries, entirely plant-based.",
"price": 3.75,
"tags": ["bakery", "sweet", "dairy-free", "vegan"],
"allergens": ["wheat"]
},
{
"name": "Nitro Cold Brew",
"description": "Cold brew infused with nitrogen for a super smooth, creamy head.",
"price": 4.50,
"tags": ["strong", "cold", "dairy-free", "sugar-free"],
"allergens": []
},
{
"name": "Iced Caramel Macchiato",
"description": "Chilled milk and vanilla syrup marked with espresso and caramel drizzle.",
"price": 5.25,
"tags": ["sweet", "cold"],
"allergens": ["dairy"]
}
]
- 验证 JSON 文件是否格式正确:
cat menu.json | python3 -m json.tool > /dev/null && echo "Valid JSON!"
💬 讨论:本地 JSON 与实时数据库
为什么我们使用的是简单的本地 menu.json 文件,而不是实时数据库?
对于快速教程或原型,本地 JSON 文件可省去初始数据库设置的时间和复杂性。不过,在实际的企业生产应用中,您需要将智能体连接到 Cloud Firestore、AlloyDB 或 Cloud SQL 等托管式数据库。
借助实时数据库,咖啡店经理可以动态添加季节性商品、更新价格或调整过敏原标记,而无需重新构建容器映像或重新部署应用代码。我们将在本 Codelab 的后面部分使用实时数据库(可选步骤)。
5. 构建 ADK 智能体
现在,您将安装所需的软件包并构建核心 ADK 代理逻辑。您将定义一个 get_menu() 工具并将其传递给 LlmAgent。
- 在 Cloud Shell 编辑器中创建并打开
requirements.txt:
cloudshell edit requirements.txt
- 将以下依赖项粘贴到编辑器中,然后保存该文件:
google-adk==2.2.0
streamlit==1.58.0
- 在 Cloud Shell 编辑器中创建并打开
agent.py:
cloudshell edit agent.py
- 将以下代码粘贴到
agent.py:
# agent.py
import json
from google.adk.agents import LlmAgent
# [START get_menu]
def get_menu() -> str:
"""Retrieves the coffee shop menu from menu.json.
Returns:
str: A JSON string representing the list of menu items.
"""
try:
with open("menu.json", "r") as f:
menu_data = json.load(f)
return json.dumps(menu_data)
except Exception as e:
return json.dumps({"error": f"Could not retrieve menu: {str(e)}"})
# [END get_menu]
# Create the barista agent
barista_agent = LlmAgent(
name="barista_agent",
model="gemini-3.5-flash",
instruction="""You are a friendly barista at ☕ Coffee Shop.
Your job is to recommend drinks and pastries to customers based on their preferences.
Rules you MUST follow:
1. You must recommend items ONLY from the menu returned by get_menu().
2. Do NOT recommend or suggest any item that is not present in the menu.
3. If a user's preference is vague or unclear, ask exactly ONE friendly clarifying question to narrow down what they want (e.g., cold or hot, sweet or strong, coffee or pastry).
4. Be warm and welcoming, but remain professional.
5. Ground your recommendations in the actual tags, descriptions, and allergens listed in the menu (e.g., if a user is dairy-free, recommend ONLY items tagged 'dairy-free' or with no dairy allergens).
""",
tools=[get_menu]
)
from google.adk.apps import App
# Define the App object
app = App(
name="coffee_barista_app",
root_agent=barista_agent
)
- 在 Cloud Shell 编辑器中创建并打开
app.py:
cloudshell edit app.py
- 将以下代码粘贴到
app.py:
# app.py
import streamlit as st
import json
# Set page config for a premium look
st.set_page_config(
page_title="☕ Coffee Shop - Barista Bot",
page_icon="☕",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS to make the header sticky (adapts to light/dark themes)
st.markdown("""
<style>
div[data-testid="element-container"]:has(.header-container),
div.element-container:has(.header-container) {
position: sticky;
top: 2.875rem;
z-index: 999;
background-color: transparent;
padding-bottom: 10px;
}
</style>
""", unsafe_allow_html=True)
# App Header (using inline styles for the permanent coffee theme look)
st.markdown("""
<div class="header-container" style="text-align: center; padding: 20px; background: linear-gradient(135deg, #8B5E3C, #6F4E37); color: white; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h1 style="margin: 0; font-size: 2.5rem; font-weight: 700; color: white;">☕ ☕ Coffee Shop</h1>
<p style="margin: 5px 0 0 0; font-size: 1.1rem; opacity: 0.9; color: white;">Your friendly AI Barista is ready to help you find the perfect drink or pastry!</p>
</div>
""", unsafe_allow_html=True)
# Load Menu for the sidebar
# [START load_menu]
try:
with open("menu.json", "r") as f:
menu_items = json.load(f)
except Exception as e:
st.error(f"Error loading menu: {e}")
menu_items = []
# [END load_menu]
# Sidebar Menu & Configuration
with st.sidebar:
st.markdown("## ☕ Coffee Shop Menu")
st.markdown("Explore our offerings and ask the barista for recommendations.")
st.markdown("---")
for item in menu_items:
with st.container(border=True):
st.markdown(f"**{item['name']}** • **${item['price']:.2f}**")
st.caption(item['description'])
# Tags & Allergens as native badges
tags = " ".join([f"`{t}`" for t in item.get("tags", [])])
if tags:
st.markdown(tags)
allergens = ", ".join(item.get("allergens", []))
if allergens:
st.markdown(f"⚠️ *Allergens: {allergens}*")
# Chat Interface
if "session_id" not in st.session_state:
import uuid
st.session_state.session_id = str(uuid.uuid4())
if "runner" not in st.session_state:
from google.adk.runners import InMemoryRunner
from agent import app
st.session_state.runner = InMemoryRunner(app=app)
if "messages" not in st.session_state:
st.session_state.messages = [
{"role": "assistant", "content": "Welcome to ☕ Coffee Shop! What can I get started for you today?"}
]
# Display existing messages
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# User Input
if prompt := st.chat_input("Ask for recommendations (e.g., 'What dairy-free pastries do you have?')"):
# Display user message
with st.chat_message("user"):
st.markdown(prompt)
st.session_state.messages.append({"role": "user", "content": prompt})
# Generate response
with st.chat_message("assistant"):
try:
import asyncio
# Run the ADK runner asynchronously using asyncio.run
async def fetch_response():
return await st.session_state.runner.run_debug(
prompt,
session_id=st.session_state.session_id
)
res_events = asyncio.run(fetch_response())
response_text = "".join([
part.text
for event in res_events
if event.content and event.content.parts
for part in event.content.parts
if part.text
])
st.markdown(response_text)
st.session_state.messages.append({"role": "assistant", "content": response_text})
except Exception as e:
st.error(f"Apologies, I ran into an error: {e}")
💬 讨论:模型权衡与检索令牌效率
为什么调用函数工具来检索菜单,而不是直接将整个菜单文本粘贴到智能体的系统指令中?
代币经济!在提示中放入 8 件商品成本不高,但如果咖啡店的商品扩展到 500 件,包括自定义配料,该怎么办?将大型数据集直接粘贴到系统提示中会增加提示 token 数量,从而提高每次查询的交易成本和 API 响应延迟时间。
通过使用 ADK 工具,智能体仅在需要时才动态请求读取菜单。LLM 仅接收相关菜单数据作为上下文,从而最大限度地减少提示令牌大小。
💬 讨论:内存状态和生产存储区
当用户关闭浏览器标签页时,存储在 Streamlit 的 st.session_state 中的聊天记录是否会保留?
不是。st.session_state 完全在内存中,并且对于活跃的浏览器连接是唯一的。如果用户刷新页面或关闭标签页,与咖啡师的对话记录会丢失。
对于生产应用,您需要将 ADK Runner 连接到 Cloud Firestore 或 Redis 等持久性存储后端。ADK 提供内置的服务抽象(例如 SessionService),可让您轻松地在页面重新加载和设备之间保存和恢复聊天记录。
6. 将代理部署到 Cloud Run
您将使用 Cloud Run 的内置 buildpack 直接从源代码部署 Streamlit 应用。为了遵循最小权限原则,您将使用专用自定义服务账号创建和部署,而不是使用默认的 Compute Engine 服务账号。
- 创建专用服务账号:
gcloud iam service-accounts create barista-agent-sa \
--description="Service account for Coffee Barista ADK agent on Cloud Run" \
--display-name="Barista Agent Service Account"
- 向新服务账号授予 Gemini Enterprise Agent Platform 用户角色 (
roles/aiplatform.user):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/aiplatform.user"
- 使用
gcloud run deploy部署服务,并通过--service-account标志传递新的服务账号电子邮件地址:
gcloud run deploy coffee-barista \
--source . \
--region $REGION \
--allow-unauthenticated \
--labels dev-tutorial=codelab-streamlit-rag-adk \
--command "/cnb/lifecycle/launcher" \
--args "sh,-c,python3 -m streamlit run app.py --server.port=\$PORT --server.address=0.0.0.0 --server.enableCORS=false --server.enableXsrfProtection=false" \
--service-account "barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
--set-env-vars GOOGLE_GENAI_USE_VERTEXAI=TRUE,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global
- 部署完成后,在命令输出中找到服务网址。
💬 讨论:部署容器与源代码,以及 IAM 安全性
我们使用 gcloud run deploy --source 部署到 Cloud Run,而无需创建 Dockerfile 或 Procfile。Cloud Run 是如何确定如何编译和执行我们的 Python 应用的?
Cloud Run 在后台使用 Buildpack 分析您的代码库。检测到requirements.txt和 Python 源文件后,引擎会自动编译并打包 Python 运行时容器。
编写自定义 Dockerfile 可让您完全控制容器的系统软件包和基础层。Procfile 是一种更简单的方式来声明启动命令,而无需完全配置容器。不过,对于快速部署,从源代码 (--source) 进行部署非常高效。
为什么我们多执行了一个步骤,创建了自定义服务账号 barista-agent-sa,而不是直接使用默认的 Compute Engine 服务账号?
安全第一!默认情况下,默认 Compute Engine 服务账号具有非常宽泛的 Editor 权限。在默认服务账号下运行 Cloud Run 容器意味着,如果我们的应用存在安全漏洞,攻击者可能会读取、写入或删除我们 Google Cloud 项目中的其他资源。
通过创建专用服务账号并仅为其分配 roles/aiplatform.user 角色,我们遵循了最小权限原则:应用仅具有调用 Gemini 所需的访问权限,没有其他权限。
7. 测试 RAG 行为
在网络浏览器中打开 Cloud Run 服务网址,并向 AI Barista 提问,以测试其事实依据和安全限制。
- 菜单内请求:提问:“推荐一些浓郁而温暖的饮品。”预期:代理推荐浓缩咖啡。
- 菜单外陷阱:提问:“你们有抹茶星冰乐吗?”预期:智能体礼貌地拒绝,并说明菜单中没有此饮品。
- 过敏感知请求:问:“我有乳糖不耐受,可以点什么?”预期:代理仅推荐不含乳制品的菜单项(例如燕麦拿铁、浓缩咖啡、冷萃咖啡)。它不会推荐 Cappuccino 或 Croissant。

8. 可选:使用 Vector Search 在 Firestore 中为智能体建立依据
在生产场景中,将菜单项存储在本地 menu.json 文件中并不理想,因为对菜单的任何更改都需要重建容器映像并重新部署 Cloud Run 服务。
为了使应用具有动态性和可伸缩性,您可以将菜单数据迁移到 Cloud Firestore,并使用向量搜索功能仅检索语义相似度最高的菜单项。

1. 启用 Firestore API 并初始化数据库
运行以下命令以启用 Firestore API 并创建名为 coffee-menu 的原生模式 Firestore 数据库:
gcloud services enable firestore.googleapis.com
gcloud firestore databases create --database="coffee-menu" --location=$REGION
注意:API 启用状态可能需要 1-2 分钟才能传播。如果数据库创建命令提示您输入 API [firestore.googleapis.com] not enabled on project... Would you like to enable and retry?,请键入 Y 以继续,或等待一分钟,然后重新运行该命令。
2. 使用菜单数据为 Firestore 播种
如需使用 menu.json 文件中的菜单项快速为 Firestore 数据库提供初始数据,您可以在 Cloud Shell 中本地运行 Python 脚本。
- 在 Cloud Shell 中本地安装 Firestore 和 GenAI 客户端库,以运行初始填充脚本:
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
- 创建初始脚本
seed.py:
cloudshell edit seed.py
- 将以下代码粘贴到
seed.py:
# seed.py
import json
import os
from google import genai
from google.cloud import firestore
from google.cloud.firestore_v1.vector import Vector
db = firestore.Client(database="coffee-menu")
client = genai.Client(
vertexai=True,
project=os.environ.get("PROJECT_ID"),
location=os.environ.get("REGION", "us-central1")
)
with open("menu.json", "r") as f:
menu_items = json.load(f)
for item in menu_items:
# Use the name as the document ID
doc_id = item["name"].lower().replace(" ", "-")
# Generate text embedding using Vertex AI text-embedding-004 model
text_to_embed = f"{item['name']}: {item['description']}"
response = client.models.embed_content(
model="text-embedding-004",
contents=text_to_embed,
)
embedding = response.embeddings[0].values
# Add embedding vector to the menu item data
item["embedding"] = Vector(embedding)
db.collection("menu").document(doc_id).set(item)
print("Firestore menu collection seeded with vector embeddings successfully!")
- 运行脚本:
python3 seed.py
3. 创建 Firestore 向量索引
如需对菜单项执行向量搜索,您必须在 Firestore 数据库中的 embedding 字段上创建复合向量索引。
在 Cloud Shell 终端中运行以下命令:
gcloud firestore indexes composite create \
--collection-group=menu \
--query-scope=COLLECTION \
--database="coffee-menu" \
--field-config=field-path=embedding,vector-config='{"dimension":"768", "flat": "{}"}'
注意:Firestore 索引创建在后台运行,可能需要几分钟才能完成。在构建索引期间,您可以继续执行 Codelab 的后续步骤。
4. 向服务账号授予 Firestore 访问权限
为了让 Cloud Run 服务能够查询 Firestore,您必须向其服务账号授予 Cloud Datastore User (roles/datastore.user) 角色:
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/datastore.user"
注意:虽然我们使用的是原生模式 Cloud Firestore,但 Google Cloud 会使用统一的 Cloud Datastore IAM 角色(roles/datastore.viewer 或 roles/datastore.user)来管理访问权限控制。
5. 更新代码
现在,更新代码以从 Firestore 中检索菜单,而不是从 menu.json 中读取菜单。
- 在 Cloud Shell Editor 中打开
requirements.txt:
cloudshell edit requirements.txt
- 将 Firestore 和 GenAI 客户端库附加到文件末尾,然后保存该文件:
google-cloud-firestore==2.27.0
google-genai==2.11.0
- 在 Cloud Shell Editor 中打开
agent.py:
cloudshell edit agent.py
- 在
agent.py中找到# [START get_menu]代码块,并将其完全替换(从# [START get_menu]到# [END get_menu])为以下 Firestore 实现:
# [START get_menu]
from google import genai
from google.cloud import firestore
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from google.cloud.firestore_v1.vector import Vector
def get_menu(query: str) -> str:
"""Retrieves coffee shop menu items matching the user's query.
Args:
query: The search query or preference to find matching menu items.
Returns:
str: A JSON string representing the list of top matching menu items.
"""
try:
# Initialize clients
db = firestore.Client(database="coffee-menu")
client = genai.Client()
# Generate embedding for the search query
response = client.models.embed_content(
model="text-embedding-004",
contents=query,
)
query_vector = response.embeddings[0].values
# Search the Firestore database using Vector Search
results = db.collection("menu").find_nearest(
vector_field="embedding",
query_vector=Vector(query_vector),
distance_measure=DistanceMeasure.COSINE,
limit=3,
).stream()
menu_data = []
for doc in results:
item = doc.to_dict()
# Remove embedding field to save tokens
item.pop("embedding", None)
menu_data.append(item)
return json.dumps(menu_data)
except Exception as e:
return json.dumps({"error": f"Could not retrieve menu: {str(e)}"})
# [END get_menu]
- 在 Cloud Shell Editor 中打开
app.py:
cloudshell edit app.py
- 在
app.py中找到# [START load_menu]代码块,并将其完全替换(从# [START load_menu]到# [END load_menu])为以下 Firestore 加载逻辑:
# [START load_menu]
from google.cloud import firestore
try:
db = firestore.Client(database="coffee-menu")
docs = db.collection("menu").stream()
menu_items = []
for doc in docs:
item = doc.to_dict()
item.pop("embedding", None)
menu_items.append(item)
except Exception as e:
st.error(f"Error loading menu from Firestore: {e}")
menu_items = []
# [END load_menu]
6. 重新部署到 Cloud Run
部署更新后的应用:
gcloud run deploy coffee-barista \
--source . \
--region $REGION \
--allow-unauthenticated \
--command "/cnb/lifecycle/launcher" \
--args "sh,-c,python3 -m streamlit run app.py --server.port=\$PORT --server.address=0.0.0.0 --server.enableCORS=false --server.enableXsrfProtection=false" \
--service-account "barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
--set-env-vars GOOGLE_GENAI_USE_VERTEXAI=TRUE,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global
7. 验证 Firestore 集成
如需测试代理与 Firestore 的连接,请直接在 Firestore 中添加一个全新的菜单项,并验证代理是否会推荐该菜单项。
- 在 Cloud Shell 中运行以下命令,以使用 Python 将新文档写入 Firestore 中的
menu集合:
python3 -c "
import os
from google import genai
from google.cloud import firestore
from google.cloud.firestore_v1.vector import Vector
db = firestore.Client(database='coffee-menu')
client = genai.Client(
vertexai=True,
project=os.environ.get('PROJECT_ID'),
location=os.environ.get('REGION', 'us-central1')
)
name = 'Matcha Green Tea Latte'
desc = 'Creamy steamed milk infused with premium Japanese matcha powder.'
res = client.models.embed_content(
model='text-embedding-004',
contents=f'{name}: {desc}'
)
embedding = res.embeddings[0].values
db.collection('menu').document('matcha-latte').set({
'name': name,
'description': desc,
'price': 5.50,
'tags': ['sweet', 'hot', 'dairy-free'],
'allergens': [],
'embedding': Vector(embedding)
})
print('Successfully added Matcha Latte with vector embeddings!')
"
- 在浏览器中刷新 Streamlit 应用,以清除聊天会话并加载新的数据库状态。
- 请注意:
- 抹茶拿铁会自动显示在边栏菜单中。
- 向聊天机器人提问:“Do you have any matcha drinks?”
- 客服人员应成功推荐您刚刚添加了说明和价格的新抹茶拿铁。这确认了智能体直接在您的实时 Firestore 数据库中进行查询!
9. 清理
为避免 Google Cloud 结算账号持续产生费用,请删除已部署的 Cloud Run 服务和自定义服务账号。
删除 Cloud Run 服务:
gcloud run services delete coffee-barista --region $REGION --quiet
删除自定义服务账号:
gcloud iam service-accounts delete barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com --quiet
(可选)删除 Firestore 数据库(如果已创建):
gcloud firestore databases delete --database="coffee-menu" --quiet
可选步骤:删除整个项目。⚠️仅当您为本实验创建了专用项目时才执行此操作
gcloud projects delete $PROJECT_ID
10. 恭喜
恭喜!您已使用 Google 的 ADK 和 Cloud Run 构建并部署了检索增强生成 (RAG) AI Barista 智能体。
您学到的内容
- 使用 Python 构建简单的 RAG 工具。
- 利用 ADK
LlmAgent和InMemoryRunner。 - 在 Streamlit 中创建有状态的聊天体验。
- 使用基于源代码的 build 将 Streamlit 部署到 Cloud Run。