https://github.com/alan-nanpie/coffee-barista-agent/tree/main
在實際的生產環境中,將菜單寫死在本地的 menu.json 檔案中並不是理想的做法,因為只要菜單有任何更動,你就必須重新建立並部署整個應用程式。
為了解決這個問題,我們可以加入 選用:使用 Vector Search,以 Firestore 為代理建立基準 的進階挑戰。這個步驟會將菜單資料遷移至雲端資料庫 Cloud Firestore,並運用 Vector Search(向量搜尋) 技術,透過語意相似度讓 AI 精準撈取最相關的品項。
以下是結合 Firestore 向量搜尋的完整 CLI 建置步驟與詳細中文說明:
首先,我們需要啟用 Firestore 服務,並建立一個名為 coffee-menu 的原生模式資料庫。
# 1. 啟用 Firestore API
gcloud services enable firestore.googleapis.com
# 2. 建立資料庫 (請確保您已設定 $REGION 變數,例如 export REGION="asia-east1")
gcloud firestore databases create --database="coffee-menu" --location=$REGION
💡 提示:啟用 API 後可能需要 1~2 分鐘才會生效。如果出現錯誤,請稍候再試或輸入 Y 繼續。
我們需要寫一支 Python 腳本,將原本 menu.json 的資料轉換成向量(Embeddings)並存入 Firestore。
# 1. 安裝 Firestore 與 GenAI 的 Python 用戶端套件
pip3 install google-cloud-firestore==2.27.0 google-genai==2.11.0
# 2. 建立 seed.py 檔案
cat << 'EOF' > 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:
# 使用名稱做為文件 ID
doc_id = item["name"].lower().replace(" ", "-")
# 使用 Vertex AI text-embedding-004 模型生成文字向量 (Embedding)
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
# 將向量資料加入菜單項目中
item["embedding"] = Vector(embedding)
db.collection("menu").document(doc_id).set(item)
print("Firestore menu collection seeded with vector embeddings successfully!")
EOF
# 3. 執行播種腳本
python3 seed.py
為了讓資料庫能進行向量搜尋,必須針對 embedding 欄位建立複合向量索引。
gcloud firestore indexes composite create \
--collection-group=menu \
--query-scope=COLLECTION \
--database="coffee-menu" \
--field-config=field-path=embedding,vector-config='{"dimension":"768", "flat": "{}"}'
💡 提示:這個索引會在背景建立,可能需要幾分鐘的時間,你可以直接進行下一步。
你的 Cloud Run 應用程式需要有權限讀取 Firestore,因此要賦予其服務帳戶 datastore.user 的角色。
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:barista-agent-sa@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/datastore.user"
接下來,我們要修改原本的程式碼,讓 AI 代理程式改從 Firestore 讀取資料。
1. 更新 requirements.txt
將這兩行加入你的 requirements.txt 中:
google-cloud-firestore==2.27.0
google-genai==2.11.0
2. 更新 agent.py (AI 代理邏輯)
將讀取菜單的邏輯改為向量搜尋:
# [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
import json
def get_menu(query: str) -> str:
"""檢索與使用者查詢相符的咖啡廳菜單項目。"""
try:
db = firestore.Client(database="coffee-menu")
client = genai.Client()
# 將使用者的問題轉換成向量
response = client.models.embed_content(
model="text-embedding-004",
contents=query,
)
query_vector = response.embeddings[0].values
# 在 Firestore 中使用向量搜尋尋找最相近的 3 個品項 (Cosine 相似度)
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()
item.pop("embedding", None) # 移除向量欄位以節省 Token 消耗
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]
3. 更新 app.py (Streamlit 介面)
讓 Streamlit 側邊欄從 Firestore 抓取現有菜單:
# [START load_menu]
from google.cloud import firestore
import streamlit as st
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]
程式碼修改完成後,我們將新版本重新部署上去。
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
為了證明資料庫現在是動態的,我們透過 Python 腳本直接在 Firestore 塞入一個新產品「抹茶拿鐵」,而完全不需要重新部署 Cloud Run!
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 網頁。你會發現側邊欄自動出現了「抹茶拿鐵」,並且在聊天室詢問「你們有抹茶飲品嗎?」時,AI 代理程式就能透過 Vector Search 準確地推薦它給你!