iT邦幫忙

2026 iThome 鐵人賽

DAY 9
1
ChatGPT & Codex

這隻 LINE Bot 不是我寫的:30 天讓 Codex 從零幫我做到上線系列 第 9

Day 9 - 我和 Codex 讓 LINE Cafe Bot 真的會做事:從「收藏第二間」開始的 Gemini Function Calling 實作

  • 分享至 

  • xImage
  •  

前一版的 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 範例,也一路參與了功能範圍、資料設計、測試與部署。


這次的新嘗試:Gemini API 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 負責理解使用者想做什麼,程式負責判斷這件事能不能做,以及真正把它做完。

我很喜歡這個分工。因為它保留了自然語言的彈性,又不需要把資料庫的控制權直接交給模型。

https://ithelp.ithome.com.tw/upload/images/20260820/201835567dtysw5xbz.jpg


「第二間」看似簡單,Bot 其實要先記得上一輪

Function Calling 可以判斷 cafe_number: 2,但後端還有一個更基本的問題:第二間到底是哪一間?

這個答案只能來自使用者剛才看到的推薦結果。

所以每次 Maps Grounding 找完咖啡廳後,程式除了把推薦卡片送到 LINE,也會把這一批店家暫存在 Firestore:

使用者 ID
所在對話
本次推薦的咖啡廳清單
建立時間
到期時間

這份推薦紀錄有效 30 分鐘。

之後使用者說「收藏第二間」,後端就能依照同一位使用者、同一個對話,安全地找回剛才那份清單,再取得其中的第二間。

如果使用者還沒傳過位置、清單已經過期,或指定了不存在的「第 99 間」,程式都不會硬猜,而是請他重新分享位置。

這讓我再次感受到,很多看起來很自然的 AI 體驗,背後其實不是只靠一個 prompt。

模型負責理解「第二間」,但系統仍然要替它準備一份可信的上下文。


我不希望 Gemini 一判斷完,就直接改資料庫

收藏店家看起來不是什麼危險操作,但只要開始做 Function Calling,就很容易繼續加上刪除或其他更敏感的功能。

所以這一版從一開始就沒有設計成「模型說做就做」。

我和 Codex 討論後,把「執行前確認」定成第一版就必須存在的規則,而不是等功能做大之後再補。因為今天只是收藏,明天很可能就會變成預約、通知,甚至其他更敏感的操作。

當 Gemini 判斷出工具之後,後端會先建立一筆有效 10 分鐘的 pending action,再回傳兩顆 LINE 按鈕:

確認執行
取消

這筆待確認操作會記住:

  • 是哪一位使用者提出的
  • 發生在哪一個 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
  → 手機上的實際互動

https://ithelp.ithome.com.tw/upload/images/20260820/20183556UaDFVMIDTX.jpg

部署時,我還是選擇讓新舊服務先並存

這次沒有直接覆蓋舊的 codex-postback-action,而是建立一個新的 Cloud Run service:

line-cafe-action-agent

部署前,Codex 先檢查既有 Cafe Bot 使用的 runtime service account,確認它已經具備:

  • Vertex AI 權限
  • Firestore 權限
  • Service Usage 權限

也確認專案裡的 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 上。


現在,這個 Bot 可以怎麼用?

完成後的實際操作很簡單。

先在 LINE 裡傳送位置,等 Bot 回傳附近咖啡廳,再直接輸入:

收藏第二間
查看我的收藏
刪除收藏第一間

「收藏」和「刪除」都會先要求確認,確認後才會真正更新 Firestore 裡的資料。

這些句子不需要完全固定。

因為前面負責理解意圖的是 Gemini,所以使用者也可以用比較自然的說法。後端最後收到的,仍然會是穩定、可驗證的結構化參數。

這正是我覺得 Function Calling 最有趣的地方:

它不是只讓聊天機器人多背幾種指令,而是讓「人習慣的說法」和「系統需要的格式」之間,有一個真正能工作的翻譯層。


這次我真正做的,不只是收藏功能

回頭看,這次表面上只是替 Cafe Bot 加了收藏功能。

對我來說,另一個很重要的收穫,是更清楚怎麼和 Codex 合作完成這類跨服務功能。我負責說明想解決的生活情境、選擇自己真正有興趣的方向;Codex 則把需求拆成可以實作和驗證的系統,遇到問題時再用實際程式與雲端狀態找原因。

但整個專案真正跨出去的一步,是讓 Bot 從「提供資訊」進入「執行操作」。

而這一步不能只靠模型變聰明,還需要把幾個角色分清楚:

  • Gemini 負責理解自然語言與選擇工具
  • Codex 負責協助我設計、實作、測試與部署整套流程
  • Firestore 負責保存上下文、收藏與待確認操作
  • 後端負責驗證參數與控制權限
  • LINE Postback 負責取得使用者最後確認

我這次最想留下的一句話是:

好的 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


上一篇
Day 8 - 我不直接把 LINE Webhook 指向新版:先寫好失敗時怎麼切回去
下一篇
Day 10 - 收藏之後呢?我和 Codex 讓 Gemini 聽懂「下週六下午兩點」,再把咖啡行程放進 Google Calendar
系列文
這隻 LINE Bot 不是我寫的:30 天讓 Codex 從零幫我做到上線11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言