iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
ChatGPT & Codex

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

Day 21 - LINE Bot 為什麼不記得「第二間」?我把收藏功能優化成真正的想去清單

  • 分享至 

  • xImage
  •  

之前我其實做過收藏功能。在 line-cafe-action-agent 版本中,我曾用 Gemini Function Calling 實作:

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

但這次實際在目前版本輸入「收藏第二間」時,Bot 卻沒有把這句話和上方的推薦連起來。

問題不是 Gemini 突然忘記,而是原本的 Function Calling 收藏留在早期的獨立版本;後來 Bot 的主線改用新的 search session,文字訊息裡又沒有帶著 session ID,目前的 handler 也不會主動找回「最近一批推薦」。

對 Bot 來說,「第二間」因此只是一個失去上下文的編號。它知道使用者想收藏,卻不知道指的是哪一家店。

舊版 Gemini 會判斷使用者想執行哪個動作,再建立一筆等待確認的操作。使用者按下確認後,Bot 才把店家寫進 Firestore。當時也有暫存推薦內容,只是這套上下文銜接沒有跟著後續版本一起保留下來。

那一版已經證明自然語言可以控制收藏,不是假的功能,也不是只有畫面沒有資料。

所以這次更準確的說法,不是「替 Bot 第一次加入收藏」,而是針對這個斷掉的上下文重新優化:

把 Function Calling 階段的實驗版收藏,重新整理成正式的想去清單。

完整程式碼:
https://github.com/zonawang/line-cafe-wishlist

舊版 Function Calling 實驗:
https://github.com/zonawang/line-cafe-action-agent

舊版收藏解決的是「AI 能不能採取行動」

Action Agent 當時最重要的問題是:Gemini 能不能理解「收藏第二間」,並安全地把它轉成程式操作?

因此它的重點放在:

  • 用 Function Calling 判斷收藏、查看、刪除或排行程。
  • 建立一筆 10 分鐘內有效的 pending action。
  • 寫入或刪除前,再讓使用者確認一次。
  • 用 Firestore Transaction 避免同一個操作重複執行。

這些設計很適合驗證「自然語言操作」是否可行。

但當 Bot 後來陸續加入偏好、足跡和主動回訪,舊版收藏沒有一起進入新的主線。它留在 line-cafe-action-agent,而不是目前正式服務的功能之一。

這次要解決的問題也不同。我關心的不再只是 Gemini 有沒有選對 function,而是收藏能不能被找到、會不會重複,以及幾天後能不能自然地接著使用。

收藏不是造訪紀錄

一開始,我曾想過直接把喜歡的店放進既有的咖啡足跡。

但兩者代表的事情完全不同:

想去清單:我對這間店有興趣,但還沒去
咖啡足跡:我真的去過,而且留下了體驗

如果混在一起,之後查看紀錄時,就分不清哪些是實際經驗,哪些只是暫時收藏。

所以新版把收藏重新定義成一條清楚的想去流程:

看到推薦
   ↓
加入想去清單
   ↓
之後查看收藏
   ↓
開啟地圖/安排時間/移除

它不會假裝使用者已經去過,也不會要求使用者收藏時就決定日期。

在最接近決定的地方放按鈕

舊版雖然也有「收藏這間」按鈕,但按下後會先送出「收藏第幾間」,再交給 Gemini Function Calling 判斷。

新版的收藏入口同樣放在每一張咖啡廳推薦卡片上,但按鈕會直接送出結構化 Postback。收藏哪一批推薦、其中第幾間,都已經是明確資料,不需要再請模型理解一次。

原本的卡片可以開啟 Google Maps、安排時間或直接記錄造訪;現在多了一個「加入想去清單」。使用者不需要先記住店名,再另外輸入收藏指令。

點下按鈕後,Bot 會回覆:

⭐ 已把「某間咖啡廳」加入你的想去清單。

[查看想去清單]

如果同一間店已經收藏過,Bot 會直接告知,而不是建立第二筆一模一樣的資料。這是舊版沒有處理的部分:舊版每次確認收藏,都可能產生一個新的隨機文件 ID。

這個小差異很重要。收藏按鈕很容易被重複點擊,網路也可能讓使用者不確定第一次是否成功。如果每按一次就多一張卡片,清單很快就會失去整理的意義。

https://ithelp.ithome.com.tw/upload/images/20260901/2018355623nyc0D8XS.jpg

同一家店,不能只用店名判斷

要避免重複,首先得回答一個問題:Bot 怎麼知道兩筆推薦是不是同一家店?

只比較店名並不可靠。

不同地區可能有同名咖啡廳,同一家店的名稱也可能因為分店資訊、空格或語言而稍微改變。因此我採用推薦結果中的 Google Maps URI 作為識別來源。

程式會先把 URI 標準化,再計算 SHA-256,取其中一段作為 Firestore 文件 ID:

Google Maps URI
      ↓
標準化並計算雜湊
      ↓
固定的 wishlist item ID

同一位使用者再次收藏相同 URI 時,會落在同一份文件上;店名日後即使更新,也只會更新原本的收藏,不會多出重複項目。

新版資料放在:

cafe-user-wishlists/{ownerId}/entries/{wishlistItemId}

每位使用者都有自己的 entries,內容只保存這個功能真正需要的資料:

ownerId
cafe.title
cafe.uri
createdAt
updatedAt

沒有先替未來想像十幾個欄位,也沒有把完整搜尋結果全部複製進來。

這裡也有一個必須說清楚的現況:舊版使用 cafe-favorites,新版使用 cafe-user-wishlists,兩個 collection 目前不共用資料。

舊版:cafe-favorites/{ownerId}/items
新版:cafe-user-wishlists/{ownerId}/entries

因此以前 Action Agent 裡的收藏,不會自動出現在新版想去清單。如果舊 collection 裡已有需要保留的正式資料,下一步應該做一次 migration:讀取舊項目、用 Maps URI 產生新版 ID,再寫入新的 entries。

這不是兩套收藏可以永遠並存的優點,而是這次回頭比對後發現、需要明確記錄的資料銜接問題。

收藏按鈕也要確認是誰按的

推薦卡片上的按鈕帶有兩項資訊:搜尋 session ID,以及咖啡廳在這批結果中的編號。

Bot 收到操作後,不會直接相信按鈕傳回來的店名,而是重新讀取原本的搜尋 session,確認:

  • 這次搜尋仍然有效。
  • 操作者是原本的使用者。
  • 按鈕來自同一個聊天室。
  • 編號真的對應到這批推薦中的店家。

通過檢查後,才把咖啡廳寫入使用者自己的清單。

這樣做也讓按鈕資料保持很短。LINE Postback 不需要夾帶一整段店家 JSON,更不需要相信可以被重新組合的外部輸入。

不靠 Gemini,也能用一句話叫出想去清單

收藏完成後,使用者可以輸入:

我的想去清單
想去清單
我的收藏
收藏的咖啡廳
咖啡收藏

Bot 會把最近收藏的店做成 Flex Message 輪播。每張卡片提供三個下一步:

  1. 在 Google Maps 查看。
  2. 安排喝咖啡時間。
  3. 移出想去清單。

如果清單是空的,Bot 不只回答「沒有資料」,還會附上「傳送目前位置」按鈕,讓使用者能立刻回到找店流程。

我把單次顯示限制在最近 10 間。想去清單的目的,是幫助使用者做下一個決定,不是一次塞進無限長的歷史資料。

這些固定指令會先由 wishlist text handler 判斷,不必每次都呼叫 Gemini。和舊版相比,結果更固定、回覆更快,也不會為「查看清單」這類明確操作產生模型成本。

https://ithelp.ithome.com.tw/upload/images/20260901/20183556xKoqU5L3vV.jpg

從收藏安排,不需要重新搜尋

這次最重要的銜接,是收藏卡片上的「安排喝咖啡時間」。

使用者可能在幾天前收藏一間店。等到週末有空時,只要開啟想去清單、選擇日期時間,就能延續原本的行程流程,不必重新分享位置,也不必期待同一家店再次被推薦。

新的地方只有「時間是從 wishlist item 選出來的」。後續的 Calendar 與行程處理仍沿用既有能力,沒有為收藏另外複製一套排程系統。

這也是我這次刻意遵守的原則:

新功能負責補上缺口,不重新發明已經能工作的部分。

新版比較完整,但不是每一點都更好

新版在去重、卡片呈現、固定入口和後續行程上比較完整,但舊版仍有一個值得保留的設計:刪除前會二次確認。

目前新版點下「移出想去清單」後會直接刪除,操作比較快,卻也比較容易因為誤觸而失去收藏。

如果繼續改善,我會在兩種方向中選一個:

  • 刪除前重新顯示確認按鈕。
  • 先標記為刪除,短時間內提供「復原」。

產品化不代表新版本的每個決定都一定勝過舊版。把兩次實作放在一起比較,反而更容易看見被自己不小心拿掉的保護措施。

測試不只確認畫面有按鈕

這次新增的測試主要集中在收藏自己的規則:

  • 加入與移除的 Postback 能正確解析。
  • 不安全或超出範圍的 ID 會被拒絕。
  • 收藏卡片包含地圖、安排與移除操作。
  • 空清單會引導使用者重新找店。
  • 同一個 Google Maps URI 會產生相同且安全的文件 ID。
  • 所有 Postback 都維持在 LINE 的長度限制內。

加上既有測試,目前共有 40 項測試全數通過,TypeScript typecheck 與 GitHub Actions 也保持成功。

測試數量不是重點。真正要確認的是:重複點擊不會產生垃圾資料、過期按鈕不能亂寫入,以及舊功能沒有因為推薦卡多一個按鈕而壞掉。

收藏替「喜歡」和「行動」留下一點距離

這次重做的功能不大,甚至沒有新的 AI 模型。

但它讓 Bot 的使用方式自然了很多。使用者不需要在看到推薦時立刻做決定,也不會因為今天沒空去,就失去一間感興趣的店。

現在流程多了一個很符合生活的停靠點:

現在看到 → 先收藏 → 有空再決定

對我來說,這也是產品逐漸完整的一個訊號。不是每次更新都要讓 Bot 更聰明,也不是曾經做過的功能就不能重做。實驗版回答「做不做得到」,產品版則要回答「找不找得到、用不用得久」。

本篇完整程式碼

👉 GitHub:https://github.com/zonawang/line-cafe-wishlist

👉 更多 LINE Bot 與 AI 實作紀錄: https://github.com/zonawang/zona-ai-learning-lab


上一篇
Day 20 - 程式會動還不夠:我用手機完成 Cafe Bot 測試
下一篇
Day 22 - 「想去清單」做出來了,但少一個固定入口?
系列文
這隻 LINE Bot 不是我寫的:30 天讓 Codex 從零幫我做到上線23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言