前一版的 Cafe Bot,已經可以做到一件很實用的事:
使用者在 LINE 裡分享位置,Bot 會透過 Vertex AI 的 Google Maps Grounding,找出附近幾間咖啡廳,整理成繁體中文,再附上可以直接打開的 Google Maps 卡片。
後來我又替它加上「換一批」和「更適合工作」,讓使用者不用重新分享位置,也能接著上一輪繼續找店。
做到這裡,它其實已經是一個滿完整的咖啡廳推薦 Bot 了。
但我開始想一件事:
如果推薦結果裡真的出現一間喜歡的店,我還是得自己複製店名,再另外找地方記下來。
那 Bot 能不能不只告訴我「有哪些店」,而是真的聽懂:
收藏第二間。
然後幫我把後面的事情準備好?
這就是這次 line-cafe-action-agent 的起點。
不過,我一開始其實沒有直接決定要做收藏功能。我先請 Codex 盤點目前 Cafe Bot 已經完成的能力,再推薦五個還沒做過、又適合 Gemini API 的方向。
其中一個提案是「Function Calling 行動助理」:讓使用者用自然語言收藏店家、查看清單和刪除收藏。看到這個方向時,我第一個反應就是:「這個滿有趣的,我想做。」
確定方向後,我和 Codex 一起把想法整理成可以實際測試的 MVP。它不只是幫我寫幾段 Function Calling 範例,也一路參與了功能範圍、資料設計、測試與部署。
一路做 LINE Bot 的過程中,我陸續嘗試了 Message Action、Location Action、Postback、Quick Reply 和 Flex Message 等功能。這次我想在原本的基礎上,再多學一項 Gemini API 的能力:Function Calling。
我不只把一句話送給模型,再拿回一段文字,而是正式替模型定義可以使用的工具、參數格式與使用時機,觀察它怎麼把「收藏第二間」這種自然語言,轉成後端能執行的結構化指令。
技術上,這個專案使用 @google/genai SDK,透過 Vertex AI 呼叫 Gemini,並在請求裡提供 functionDeclarations。使用者仍然透過熟悉的 LINE 介面互動,而 Bot 則多了一層理解自然語言、選擇工具的新能力。
一般的聊天機器人很會回答,但回答完通常就結束了。
例如我說「收藏第二間」,模型當然可以回我一句:
好的,已為你收藏第二間咖啡廳。
問題是,它可能只是在「說」自己完成了,資料庫裡根本什麼都沒有。
這次使用的 Gemini Function Calling,解決的就是這個落差。
確定方向後,Codex 先沒有急著把所有功能塞進同一個 handler,而是把 Gemini 意圖判斷、Firestore 資料存取、LINE 訊息和 Postback 確認拆成不同模組。這讓模型的工作與真正執行操作的程式邏輯保持分離。
我先告訴 Gemini,目前有哪些工具可以使用:
save_cafe 收藏咖啡廳
list_saved_cafes 查看收藏
remove_saved_cafe 刪除收藏
當使用者輸入「收藏第二間」時,Gemini 不需要自己操作 Firestore,而是回傳一個結構化的工具呼叫:
{
"name": "save_cafe",
"args": {
"cafe_number": 2
}
}
這裡最重要的觀念是:
Gemini 負責理解使用者想做什麼,程式負責判斷這件事能不能做,以及真正把它做完。
我很喜歡這個分工。因為它保留了自然語言的彈性,又不需要把資料庫的控制權直接交給模型。

Function Calling 可以判斷 cafe_number: 2,但後端還有一個更基本的問題:第二間到底是哪一間?
這個答案只能來自使用者剛才看到的推薦結果。
所以每次 Maps Grounding 找完咖啡廳後,程式除了把推薦卡片送到 LINE,也會把這一批店家暫存在 Firestore:
使用者 ID
所在對話
本次推薦的咖啡廳清單
建立時間
到期時間
這份推薦紀錄有效 30 分鐘。
之後使用者說「收藏第二間」,後端就能依照同一位使用者、同一個對話,安全地找回剛才那份清單,再取得其中的第二間。
如果使用者還沒傳過位置、清單已經過期,或指定了不存在的「第 99 間」,程式都不會硬猜,而是請他重新分享位置。
這讓我再次感受到,很多看起來很自然的 AI 體驗,背後其實不是只靠一個 prompt。
模型負責理解「第二間」,但系統仍然要替它準備一份可信的上下文。
收藏店家看起來不是什麼危險操作,但只要開始做 Function Calling,就很容易繼續加上刪除或其他更敏感的功能。
所以這一版從一開始就沒有設計成「模型說做就做」。
我和 Codex 討論後,把「執行前確認」定成第一版就必須存在的規則,而不是等功能做大之後再補。因為今天只是收藏,明天很可能就會變成預約、通知,甚至其他更敏感的操作。
當 Gemini 判斷出工具之後,後端會先建立一筆有效 10 分鐘的 pending action,再回傳兩顆 LINE 按鈕:
確認執行
取消
這筆待確認操作會記住:
只有原本的使用者在原本的對話裡按下確認,後端才會真正寫入 Firestore。
確認時還會使用 Firestore Transaction,一次完成狀態檢查與資料寫入。這可以避免使用者連點兩次「確認執行」,結果收藏出現兩筆,或同一個刪除動作被重複執行。
整體流程變成:
使用者輸入自然語言
↓
Gemini 選擇 Function
↓
後端驗證店家與參數
↓
建立 pending action
↓
LINE 顯示確認/取消
↓
Firestore Transaction 正式執行
這多了一次點擊,卻換來一個很清楚的安全邊界。
Function Calling 讓 Bot 更有能力;確認機制則確保能力不會變成失控。
程式完成後,我先跑了 TypeScript build、單元測試和本機 /health。
六項測試全部通過,服務也能正常啟動,程式碼接著推上新的 GitHub repo。
我拿起手機輸入:
收藏第二間
結果 LINE 跳出來的,卻還是舊 Bot 的「傳送目前位置」。
第一眼很容易懷疑是不是 Function Calling 沒有成功,或 Firestore 沒讀到推薦紀錄。
我把現象告訴 Codex 後,它沒有立刻修改 prompt,而是先拿實際回覆和新版程式比對。新版在找不到推薦紀錄時,應該回答「找不到這間推薦,請重新傳送位置」,不會直接跳出舊版的歡迎訊息。
有了這個證據,Codex 接著查詢 Cloud Run 的服務清單,才發現 line-cafe-action-agent 根本還沒出現在已部署服務裡。也就是說,問題不在 Gemini,而在部署鏈路。
真正的原因很單純:
新程式雖然已經在 GitHub,卻還沒有部署;LINE Webhook 當然仍然連著舊服務。
這個插曲很小,卻是一個非常典型的雲端開發現場。
程式碼完成、測試通過、GitHub 有更新,都不代表手機上的 LINE 已經在跑那份程式。
要確認使用者實際碰到哪個版本,還是要沿著整條鏈路檢查:
GitHub repo
→ Cloud Build
→ Cloud Run revision
→ LINE Webhook endpoint
→ 手機上的實際互動

這次沒有直接覆蓋舊的 codex-postback-action,而是建立一個新的 Cloud Run service:
line-cafe-action-agent
部署前,Codex 先檢查既有 Cafe Bot 使用的 runtime service account,確認它已經具備:
也確認專案裡的 Firestore 是 Native mode,而且位置和 Cloud Run 一致。
新服務部署後,Codex 先測 /health,確認新的 revision 已經 Ready,再透過 LINE API 讀取目前真正使用中的舊 Webhook endpoint。
最後才執行切換:
保留舊服務
→ 部署新服務
→ Health Check
→ 切換 LINE Webhook
→ 執行 LINE 官方 Verify
→ 失敗就自動切回舊 endpoint
這次 LINE 官方測試回傳:
{
"success": true,
"statusCode": 200,
"reason": "OK"
}
Cloud Logging 也沒有出現 error,新 Webhook 才正式保留下來。
這段過程也讓我很明顯感受到 Codex 和單純貼程式碼給我的差別。它不只完成 repo 裡的程式,還能沿著 Cloud Run、IAM、Firestore、LINE API 和 logs 一層一層確認,直到手機真正連上新版本。
我在前一次 Postback 專案裡學到「發布前先想好怎麼回去」,這次就直接把同一個原則用在新的 Action Agent 上。
完成後的實際操作很簡單。
先在 LINE 裡傳送位置,等 Bot 回傳附近咖啡廳,再直接輸入:
收藏第二間
查看我的收藏
刪除收藏第一間
「收藏」和「刪除」都會先要求確認,確認後才會真正更新 Firestore 裡的資料。
這些句子不需要完全固定。
因為前面負責理解意圖的是 Gemini,所以使用者也可以用比較自然的說法。後端最後收到的,仍然會是穩定、可驗證的結構化參數。
這正是我覺得 Function Calling 最有趣的地方:
它不是只讓聊天機器人多背幾種指令,而是讓「人習慣的說法」和「系統需要的格式」之間,有一個真正能工作的翻譯層。
回頭看,這次表面上只是替 Cafe Bot 加了收藏功能。
對我來說,另一個很重要的收穫,是更清楚怎麼和 Codex 合作完成這類跨服務功能。我負責說明想解決的生活情境、選擇自己真正有興趣的方向;Codex 則把需求拆成可以實作和驗證的系統,遇到問題時再用實際程式與雲端狀態找原因。
但整個專案真正跨出去的一步,是讓 Bot 從「提供資訊」進入「執行操作」。
而這一步不能只靠模型變聰明,還需要把幾個角色分清楚:
我這次最想留下的一句話是:
好的 AI Agent,不是什麼都讓 AI 自己做,而是讓 AI 知道該叫哪個工具,再由系統安全地把事情完成。
收藏只是 Action Agent 的第一步。下一篇,我會把「安排行程」獨立拉出來,記錄 Gemini 怎麼理解自然語言時間,以及為什麼第一版先選擇 Google Calendar 預填連結,而不是直接要求使用者授權 OAuth。
GitHub:
https://github.com/zonawang/line-cafe-action-agent
更多 LINE Bot 與 AI 實作紀錄:
https://github.com/zonawang/zona-ai-learning-lab