在昨天的文章中,我們透過 LINE Flex Message Carousel 大幅升級了聊天室的互動體驗,讓推薦結果以圖文並茂的橫向滑動卡片呈現。
然而,目前推薦系統所依賴的依然是 Day 05 寫在程式內的靜態模擬資料(Mock DB)。在真實的日常生活中,使用者的需求往往跨越不同縣市商圈,且店家隨時可能歇業或變更評分。如果只能推薦預先寫好的幾家店,實用性將大打折扣。
今天 Day 12,我們將把微服務正式接軌真實世界:在 FastAPI 中整合 Google Maps Places API(New),讓推薦 Agent 具備即時搜尋周邊實體店家、讀取 Google 星級評分、抓取營業地址與照片的能力!
Google 在最新版的 Places API 中推行了 Text Search (New) 機制,特別適合自然語言驅動的推薦場景:
板橋 甜點)。X-Goog-FieldMask 明確指定只需回傳的欄位(如店名、評分、價格等級、地址),只抓取需要的資料,避免浪費 API 額度。Places API (New) 並點擊 啟用。Places API (New),避免金鑰遭濫用。為了發送外部非同步 HTTP 請求,我們先在 Python 環境安裝高效能的 httpx 套件。
確認虛擬環境已啟用,在終端機執行:
pip install httpx python-dotenv
在 agent-backend/ 專案根目錄下建立 .env 檔案,妥善管理機密金鑰:
GOOGLE_MAPS_API_KEY=AIzaSyYourActualGoogleMapsAPIKeyHere
在 app/config.py(若無則新增)中讀取環境變數:
import os
from dotenv import load_dotenv
load_dotenv()
GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY", "")
建立 app/services/places.py,專門負責呼叫 Google Places Text Search API:
import httpx
from typing import List
from app.config import GOOGLE_MAPS_API_KEY
from app.schemas.food import RestaurantItem, CuisineType
PLACES_API_URL = "[https://places.googleapis.com/v1/places:searchText](https://places.googleapis.com/v1/places:searchText)"
# 價格等級對應平均消費估計
PRICE_LEVEL_MAP = {
"PRICE_LEVEL_FREE": 0,
"PRICE_LEVEL_INEXPENSIVE": 150,
"PRICE_LEVEL_MODERATE": 350,
"PRICE_LEVEL_EXPENSIVE": 800,
"PRICE_LEVEL_VERY_EXPENSIVE": 1500,
}
async def search_places(query_location: str, query_cuisine: str, budget_max: int) -> List[RestaurantItem]:
"""
透過 Google Places API (New) 搜尋符合地點與料理分類的真實店家
"""
if not GOOGLE_MAPS_API_KEY:
return []
text_query = f"{query_location} {query_cuisine}" if query_cuisine != "全部" else f"{query_location} 美食"
headers = {
"Content-Type": "application/json",
"X-Goog-Api-Key": GOOGLE_MAPS_API_KEY,
# 僅索取所需欄位以節省額度
"X-Goog-FieldMask": "places.id,places.displayName,places.formattedAddress,places.rating,places.priceLevel,places.editorialSummary"
}
payload = {
"textQuery": text_query,
"languageCode": "zh-TW",
"maxResultCount": 5
}
results: List[RestaurantItem] = []
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(PLACES_API_URL, headers=headers, json=payload)
if response.status_code != 200:
return []
data = response.json()
places = data.get("places", [])
for p in places:
price_level_str = p.get("priceLevel", "PRICE_LEVEL_MODERATE")
estimated_price = PRICE_LEVEL_MAP.get(price_level_str, 300)
# 依預算過濾
if estimated_price > budget_max:
continue
summary_dict = p.get("editorialSummary", {})
reason_text = summary_dict.get("text", "Google Maps 評價優良的在地推薦店家!")
item = RestaurantItem(
id=p.get("id", "places_id"),
name=p.get("displayName", {}).get("text", "精選餐廳"),
cuisine=CuisineType(query_cuisine) if query_cuisine in [c.value for c in CuisineType] else CuisineType.ALL,
price_avg=estimated_price,
location=p.get("formattedAddress", query_location),
rating=float(p.get("rating", 4.0)),
signature_dish=f"{query_cuisine}人氣餐點",
reason=reason_text
)
results.append(item)
return results
將原本只查 Mock 靜態資料的邏輯,改為優先呼叫 Google Places API,若無回傳再降級(Fallback)使用 Mock 資料:
from fastapi import APIRouter
from app.schemas.food import UserPreference, RecommendationResponse
from app.services.places import search_places
router = APIRouter(
prefix="/api/v1/recommend",
tags=["Food Recommendation"]
)
@router.post("/", response_model=RecommendationResponse)
async def recommend_food(preference: UserPreference):
"""
接收使用者條件,動態調用 Google Maps Places API 取得即時店家推薦
"""
# 呼叫 Google Places API
real_places = await search_places(
query_location=preference.location,
query_cuisine=preference.cuisine.value,
budget_max=preference.budget_max
)
if real_places:
return RecommendationResponse(
status="success",
total_found=len(real_places),
recommendations=real_places
)
# 查無結果時回傳空清單,由前端工作流處理溫馨提示
return RecommendationResponse(
status="not_found",
total_found=0,
recommendations=[]
)
重新啟動 FastAPI 服務,確保環境變數已載入:
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
拿起手機,打開 LINE 美食小幫手聊天室,發送不同縣市商圈的自然語言提問:
使用者輸入:
「我現在在台北車站附近,預算 400 元,想吃日式拉麵,有推薦的嗎?」
驗收重點:
1.Dify 解析:自動精準抽取 location: "台北車站"、cuisine: "日式"、budget_max: 400。
2.FastAPI 呼叫:背景即時呼叫 Google Places API,取得「台北車站」周圍真實營業中的日式餐廳與真實 Google Maps 評分。
3.LINE 介面呈現:手機端立即收到對應店家的真實名稱、地址與 Google 導航按鈕,完整告別寫死假資料的限制!
今日結語與明日預告
今天我們為美食推薦 Agent 裝上了真實世界探測儀:
成功整合 Google Maps Places API (New)。
透過非同步 httpx 實現低延遲的外部端點請求。
讓系統具備跨區域、真實營業狀態與動態評分的即時推薦能力!
目前每次使用者詢問完,資訊就隨風而逝,系統並不知道使用者「去過哪裡」或「常吃什麼」。
明天 Day 13,我們將要建立長期記憶機制:整合輕量級資料庫(SQLite / PostgreSQL),儲存使用者歷史喜好與去過的餐廳足跡,打造具備專屬記憶的貼身助理! 敬請期待!