使用 Google ADK 和 Cloud Run,在 Streamlit 中部署 RAG AI 代理程式

1. 簡介

在本程式碼研究室中,您將為咖啡廳建構互動式 AI Barista 代理。您將使用 Google 的開放原始碼 Agent Development Kit (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 有基本的瞭解。

本程式碼研究室適合各種程度的開發人員,包括初學者。

預估費用:不到 $1.00 美元。

2. 事前準備

建立 Google Cloud 專案

  1. Google Cloud 控制台, 選擇或建立 Google Cloud 雲端專案
  2. 確認 Cloud 專案已啟用計費功能。瞭解如何確認專案已啟用計費功能

啟動 Cloud Shell

  1. 點選 Google Cloud 控制台頂部的 啟動 Cloud Shell

啟動 Cloud\nShell

  1. 驗證:

授權 Cloud Shell

gcloud auth list
  1. 確認已設定有效專案:
gcloud config get project

如果顯示的專案 ID 不正確或未設定,請執行:

gcloud config set project <YOUR_PROJECT_ID>

啟用 API

執行下列指令,啟用所有必要的 API:

gcloud services enable \
 run.googleapis.com \
 aiplatform.googleapis.com \
 cloudbuild.googleapis.com

3. 設定專案

在這個步驟中,您會初始化專案環境變數,並為專案建立目前使用的目錄。

  1. 在現行 Cloud Shell 工作階段中,初始化下列專案環境變數:
export PROJECT_ID=$(gcloud config get-value project)

注意:使用最近的區域

找出最接近的區域,並在下列指令中將 insert-region-here 替換為該區域:

export REGION=[insert-region-here]
  1. 建立並變更為名為 coffee-barista-agent 的新專案目錄:
mkdir coffee-barista-agent && cd coffee-barista-agent

4. 建立模擬選單資料來源

為避免 AI Barista 產生不存在的項目,您將建立本機菜單資料集。代理會在執行階段透過自訂工具讀取這個檔案。

  1. 在 Cloud Shell 編輯器中建立並開啟 menu.json
cloudshell edit menu.json
  1. 將下列 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"]
  }
]
  1. 確認 JSON 檔案格式正確:
cat menu.json | python3 -m json.tool > /dev/null && echo "Valid JSON!"

💬 討論:本機 JSON 與即時資料庫

為什麼我們使用簡單的本機 menu.json 檔案,而不是即時資料庫?

如果是快速教學課程或原型,本機 JSON 檔案可省去初始資料庫設定時間和複雜度。不過,在實際的企業正式環境應用程式中,您會將代理程式連線至 Cloud Firestore、AlloyDB 或 Cloud SQL 等代管資料庫。

咖啡店經理使用即時資料庫,即可動態新增季節性商品、更新價格或調整過敏原標記,不必重建容器映像檔或重新部署應用程式程式碼。稍後在程式碼研究室中,我們會使用即時資料庫做為選用步驟。

5. 建構 ADK 代理

現在,您要安裝必要套件,並建構核心 ADK 代理程式邏輯。您將定義 get_menu() 工具,並傳遞至 LlmAgent

  1. 在 Cloud Shell 編輯器中建立並開啟 requirements.txt
cloudshell edit requirements.txt
  1. 將下列依附元件貼到編輯器中,然後儲存檔案:
google-adk==2.2.0
streamlit==1.56.0
  1. 在 Cloud Shell 編輯器中建立並開啟 agent.py
cloudshell edit agent.py
  1. 將下列程式碼貼到 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
)
  1. 在 Cloud Shell 編輯器中建立並開啟 app.py
cloudshell edit app.py
  1. 將下列程式碼貼到 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 個項目,包括自訂食材,該怎麼辦?直接將大型資料集貼到系統提示中,會增加提示詞元數,導致每次查詢的交易費用提高,API 回應延遲時間也會變長。

透過使用 ADK 工具,代理可以動態地僅在需要時請求讀取選單。LLM 只會收到相關的菜單資料做為背景資訊,盡量縮小提示符記大小。

💬 討論:記憶體狀態和產生作業儲存區

當使用者關閉瀏覽器分頁時,Streamlit 的 st.session_state 中儲存的聊天記錄是否會保留?

不會。st.session_state 完全位於記憶體中,且專屬於有效的瀏覽器連線。如果使用者重新整理頁面或關閉分頁,與咖啡師的對話記錄就會遺失。

如果是正式版應用程式,您會將 ADK 執行器連線至 Cloud Firestore 或 Redis 等永久儲存空間後端。ADK 提供內建的服務抽象化 (例如 SessionService),可輕鬆儲存及接續對話記錄,即使重新載入頁面或更換裝置也不受影響。

6. 將代理程式部署至 Cloud Run

您將使用 Cloud Run 內建的建置包直接從原始程式碼部署 Streamlit 應用程式。為遵循最小權限原則,您將使用專用的自訂服務帳戶建立及部署,而非使用預設的 Compute Engine 服務帳戶。

  1. 建立專用服務帳戶:
  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"
  1. 將 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"
  1. 使用 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_ENTERPRISE=TRUE,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global
  1. 部署完成後,請在指令輸出內容中找出服務網址。

💬 討論:容器部署與原始碼部署,以及 IAM 安全性

我們使用 gcloud run deploy –source 部署至 Cloud Run,未建立 Dockerfile 或 Procfile。Cloud Run 是如何知道如何編譯和執行我們的 Python 應用程式的?

Cloud Run 會在幕後使用 Buildpacks 分析存放區,偵測到 requirements.txt 和 Python 原始碼檔案後,引擎會自動編譯並封裝 Python 執行階段容器。

撰寫自訂 Dockerfile 可讓您完全控管容器的系統套件和基礎層。Procfile 是較簡單的啟動指令宣告方式,不必完整設定容器。但對於快速部署而言,從原始碼部署(--source)效率非常高。

為什麼我們要多花一步,建立一個自訂服務帳戶 barista-agent-sa,而不是直接使用預設的 Compute Engine 服務帳戶?

安全第一!根據預設,預設的 Compute Engine 服務帳戶具備極為廣泛的編輯者權限。在預設服務帳戶下執行 Cloud Run 容器,表示如果應用程式有安全性錯誤,攻擊者可能會讀取、寫入或刪除 Google Cloud 專案中的其他資源。

建立專屬服務帳戶並只指派 roles/aiplatform.user 角色,可確保應用程式只具備呼叫 Gemini 的存取權,符合最小權限原則。

7. 測試 RAG 行為

在網頁瀏覽器中開啟 Cloud Run 服務 URL,並向 AI Barista 提問,以測試其接地和安全限制。

  1. 菜單內請求: 問: 「推薦一些濃鬱溫暖的飲品。」預期: 服務生推薦了濃縮咖啡。
  2. 菜單外陷阱:問:「你們有抹茶星冰樂嗎?」預期:代理會禮貌地拒絕,並說明菜單上沒有這項產品。
  3. 過敏原意識請求: 問: 「我乳糖不耐受,我能點什麼?」預期: 服務生只推薦不含乳製品的菜單項目(如燕麥奶拿鐵、濃縮咖啡、冷萃咖啡)。但「不」建議卡布奇諾或可頌。

測試 RAG 行為

8. 選用:使用向量搜尋為代理程式建立 Firestore 基準

在實際工作環境中,將菜單項目儲存在本機 menu.json 檔案中並不理想,因為菜單的任何變更都需要重建容器映像檔,並重新部署 Cloud Run 服務。

如要讓應用程式動態且可擴充,您可以將菜單資料遷移至 Cloud Firestore,並使用 向量搜尋 根據語意相似度,只擷取最相關的菜單項目。

整合使用向量搜尋的 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 指令碼。

  1. 在 Cloud Shell 本機安裝 Firestore 和 GenAI 用戶端程式庫,執行播種指令碼:
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
  1. 建立播種指令碼 seed.py
cloudshell edit seed.py
  1. 將下列程式碼貼到 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 Gemini Enterprise Agent Platform text-embedding-005 model
   text_to_embed = f"{item['name']}: {item['description']}"
   response = client.models.embed_content(
       model="text-embedding-005",
       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!")
  1. 執行指令碼:
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 索引建立在背景運行,可能需要幾分鐘才能完成。在索引建置期間,您可以繼續進行程式碼實驗室的後續步驟。

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 讀取。

  1. 在 Cloud Shell 編輯器中開啟 requirements.txt
cloudshell edit requirements.txt
  1. 將 Firestore 和 GenAI 用戶端庫新增至檔案末端並儲存:
google-cloud-firestore==2.27.0
google-genai==2.11.0
  1. 在 Cloud Shell 編輯器中開啟 agent.py
cloudshell edit agent.py
  1. 找到 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-005",
           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]
  1. 在 Cloud Shell 編輯器中開啟 app.py
cloudshell edit app.py
  1. 找到 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 \
  --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_ENTERPRISE=TRUE,GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=global

7. 驗證 Firestore 集成

若要測試代理程式與 Firestore 的連接,請直接在 Firestore 中新增一個全新的選單項,並驗證代理程式是否推薦該選單項目。

  1. 在 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 oat milk infused with premium Japanese matcha powder.'
res = client.models.embed_content(
   model='text-embedding-005',
   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!')
"
  1. 在瀏覽器中刷新 Streamlit 應用,以清除聊天會話並載入新的資料庫狀態。
  2. 註:
    • 抹茶綠茶拿鐵會自動出現在側邊欄選單中。
    • 問聊天機器人:「你們有抹茶飲料嗎?」
    • 服務專員應會成功推薦新產品「抹茶拿鐵」,並顯示你剛新增的說明和價格。這表示代理程式直接在運作中的 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 LlmAgentInMemoryRunner
  • 在 Streamlit 中建立有狀態的即時通訊體驗。
  • 使用基於原始程式碼的建置將 Streamlit 部署到 Cloud Run。

參考文件