iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
ChatGPT & Codex

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

Day 29 - LINE Cafe Bot 旅程尾聲,我把一路累積的足跡做成一本咖啡護照

  • 分享至 

  • xImage
  •  

這陣子,我陸續幫 LINE Cafe Bot 加上找咖啡廳、收藏、記錄造訪、群組投票和一起選時間等功能。

做到這裡,它已經可以從「今天想喝咖啡」一路陪我走到真正約成、出發,再記下喝完之後的感受。

我也替這個系列定了一個終點:再完成這一項,就讓這隻 LINE Bot 告一段落。

既然走到收尾,我不想再往外增加一條全新的流程,而是想回頭看看,前面做過的功能究竟留下了什麼。這時我才發現,資料已經散落在不同地方:

  • 想去的店放在收藏清單。
  • 去過的店放在咖啡足跡。
  • 每次造訪有評分和體驗標籤。
  • 同一間喜歡的店,可能已經去了好幾次。

這些資料都有保存下來,卻還不像一段真正屬於自己的咖啡紀錄。也因為這樣,我覺得這一站最適合做的,不是再幫 Bot 多完成一件事,而是把一路累積的內容收回來。

我想看的不只是「最近去過哪間」,而是:

這個月喝了幾次咖啡?
去了幾間不同的店?
我最常留下什麼印象?
收藏的店,後來真的去了幾間?

於是這次,我和 Codex 一起完成了 line-cafe-passport,把原本分散的資料整理成一張「我的咖啡護照」,也用它替整個功能系列收尾。

不是再新增一份紀錄,而是把舊資料讀懂

一開始,我對 Codex 說的需求很簡單:

我想做最後一個功能,把使用者去過的咖啡廳整理成個人咖啡護照。

但「咖啡護照」其實可以有很多種做法。

它可以只是把去過的店全部列出來,也可以做成集點卡,甚至可以另外建立一套新的打卡資料。

Codex 先讀了上一版 Bot 的程式和 Firestore 結構,確認目前已經有哪些資料可以使用。最後我們決定不要求使用者重新打卡,也不建立另一份重複紀錄,而是直接整理原有的咖啡足跡、評分、標籤和想去清單。

這個決定很重要,因為對使用者來說,護照不是從今天才開始。

只要以前已經透過 Bot 完成過造訪紀錄,現在輸入:

我的咖啡護照

那些足跡就會自動成為護照的一部分。

完成一筆新的咖啡紀錄後,Bot 也會直接留下「查看咖啡護照」的按鈕,讓整個流程自然接起來:

記錄這次造訪
      ↓
選擇 1~5 分
      ↓
加入安靜、有插座、適合工作等標籤
      ↓
完成紀錄
      ↓
查看咖啡護照

https://ithelp.ithome.com.tw/upload/images/20260909/20183556rOk8C6ANkY.jpg

一本護照要放什麼,才不會只是漂亮的流水帳?

如果只是把所有店名列出來,它其實和「我的咖啡足跡」沒有太大差別。

所以我和 Codex 先討論:哪些數字真的能讓人快速看懂自己的咖啡生活?

最後,每張護照會整理:

  • 完成的造訪次數。
  • 不重複的咖啡廳數量。
  • 平均評分。
  • 五星體驗次數。
  • 最常出現的三個體驗標籤。
  • 最常造訪的咖啡廳。
  • 最近一次咖啡足跡。
  • 目前還在收藏,而且收藏後真的去過的店家數。

我也做了三種期間:

我的咖啡護照 → 全部紀錄
本月咖啡護照 → 這個月
今年咖啡護照 → 今年

這樣既能看到長期累積,也能把本月的生活單獨拿出來回顧。

時間區間固定用 Asia/Taipei 計算。這個細節看起來很小,但如果直接使用 UTC,月底深夜留下的紀錄可能會被算到錯的月份,跨年時也可能跑到不同年度。

Codex 在寫統計邏輯時,還替這些容易忽略的日期邊界補上測試。對我來說,這也是使用 AI 協作很實用的地方:我想到的是「我要看本月」,它會繼續往下追問「本月是用哪個時區切分?」

https://ithelp.ithome.com.tw/upload/images/20260909/20183556TeZeuBlgqI.jpg

去同一間三次,不能算成三間店

護照裡同時有「造訪次數」和「不同店家數」,所以程式必須知道哪些紀錄其實是同一家店。

如果只比對店名,很容易遇到空格、大小寫或名稱更新造成的差異。因此 Codex 幫我把規則整理成:

優先使用標準化後的 Google Maps URI
                    ↓
沒有 URI 時,才退回使用店名判斷

假設我去了同一間店三次,護照會顯示三次造訪,但不同店家仍然只有一間。

體驗標籤也不能看到一個就直接加總。同一筆造訪裡即使意外出現重複標籤,也只會計算一次;標籤票數相同時則使用固定排序,避免每次打開卡片,顯示順序都不一樣。

這些規則不一定會被使用者注意到,卻決定了護照上的數字能不能相信。

「收藏後已造訪」比我原本想得更難定義

我很想知道一件事:放進想去清單的店,後來有多少真的去過?

但目前的想去清單只保存「現在仍在清單裡」的店,移除收藏後不會留下完整歷史。因此這個數字不能假裝是終身收藏轉換率。

最後,程式只會計算同時符合三個條件的店:

目前仍在想去清單
        +
足跡裡有相同的 Google Maps URI
        +
造訪時間晚於加入收藏的時間

畫面上也直接寫成「目前收藏後已造訪」,讓它如實反映現有資料,而不是替一個不完整的數字取太厲害的名字。

這段也是 Codex 幫我踩煞車的地方。AI 不只是幫忙把數字算出來,也可以協助確認:現有資料究竟足不足以支持我想說的結論。

數字交給程式,Gemini 只負責把回顧說得像人話

護照最上面的次數、平均分數和店家排名,都由程式直接計算。

Gemini 不負責算數字,只會收到一份精簡的統計資料,例如:

{
  "period": "2026 年 9 月",
  "visits": 5,
  "uniqueCafes": 4,
  "averageRating": 4.6,
  "fiveStarVisits": 3,
  "topTags": [
    { "label": "安靜", "count": 3 },
    { "label": "適合工作", "count": 2 }
  ]
}

接著,它會把這些已經確定的事實整理成兩句繁體中文回顧。

Prompt 也明確限制它不能猜測使用者的個性、消費金額、沒提供的地點,或統計裡根本沒有出現的偏好。

我的分工原則是:

程式 → 計算必須正確的事實
Gemini → 把事實整理成自然的短文

這樣即使生成式 AI 的文字每次有一點變化,護照上的核心數字仍然一致。

如果 Gemini 暫時無法回覆,程式也會使用根據同一份統計產生的備援文字,不會讓整張護照因為一小段文案失敗而消失。

第一次打開時,我卻只看到 Gemini 說了半句

功能完成並部署後,我真的在 LINE 裡輸入「我的咖啡護照」。

卡片正常出現,統計資料也都在,但「Gemini 咖啡回顧」看起來像話還沒說完:句子停在一半,最後沒有完整結尾。

我第一個懷疑的是 Flex Message。

會不會是文字框高度不夠?是不是忘了開啟換行?或 LINE 對顯示行數有限制?

Codex 沒有直接把卡片拉高,而是先沿著資料流往回檢查:

Flex Message 是否限制行數?
        ↓
summary 是否在顯示前被裁切?
        ↓
Gemini 原始回覆是否已經不完整?
        ↓
為什麼重新打開還是同一段?

結果發現,卡片的文字已經設定 wrap: true,也沒有 maxLines 限制。畫面其實沒有把文字藏起來,而是收到的摘要本來就只有半句。

180 tokens 不一定全部都用來回答

原本 Gemini 的設定是:

config: {
  temperature: 0.4,
  maxOutputTokens: 180
}

我原本以為兩句短文用 180 tokens 已經很多。

但 Gemini 2.5 Flash 具有 thinking 能力,思考過程也可能占用輸出預算。留給最後答案的額度不夠時,即使我要的只有短短兩句,還是可能在句子中間被截斷。

Codex 把這段設定調整為:

config: {
  temperature: 0.4,
  maxOutputTokens: 512,
  thinkingConfig: { thinkingBudget: 0 }
}

這裡不是覺得 thinking 沒有用,而是這個任務只需要把已經算好的 JSON 改寫成兩句話,不需要額外推理。關閉 thinking budget,可以把額度留給真正顯示的文字,也減少不必要的等待與消耗。

檢查 Prompt 時,我們還發現原本有一句寫成「溫暖但不誠懇」。這和我真正想表達的完全相反,所以也一起改成「溫暖自然且不浮誇」,並要求最後一句一定要用完整標點結束。

只提高 token 還不夠,因為舊的半句已經被記住了

這個功能為了避免每次打開護照都重新呼叫 Gemini,會替統計資料建立 fingerprint,並把摘要存在 Firestore。

大致概念是:

期間+次數+評分+標籤+店家+收藏後造訪
                      ↓
               summary fingerprint

只要統計沒有改變,下次就直接使用快取。

這本來是好事,但這次也帶來另一個問題:第一次產生的半句已經存進 Firestore。就算程式提高 token 上限,只要 fingerprint 沒變,使用者還是會繼續看到同一段舊摘要。

因此 Codex 在 fingerprint 裡加入摘要版本:

summaryVersion: 2

新版上線後,舊 fingerprint 會自然失效。使用者不必刪除資料,也不必新增一筆假的咖啡足跡,下一次打開護照就會重新產生摘要。

最後,我們又補上一層保護:Gemini 回覆若沒有以完整句號、驚嘆號或問號結束,就不把這段半句存進快取,而是改用完整的備援回顧。超過長度時,也只會保留到最近一個完整句子,不會直接從第 180 個字切下去。

修正後的流程變成:

Gemini 產生回顧
      ↓
清除 Markdown 與多餘空白
      ↓
確認最後一句完整
   ↙          ↘
完整            不完整
 ↓                ↓
寫入快取       使用備援文案

https://ithelp.ithome.com.tw/upload/images/20260909/20183556w8TEEq8tvt.jpg

Codex 幫我修的,不只是畫面上的半句話

如果只看表面,這次問題好像只是「文字沒有顯示完整」。

但 Codex 實際追到的是一整條原因:

輸出預算太小
   +
簡單任務仍啟用 thinking
   +
程式沒有檢查句子是否完整
   +
錯誤結果已經被快取

少修任何一層,問題都有可能再出現。

我們這次的協作方式很像:

我:在真實 LINE 畫面發現「這句話怪怪的」
Codex:從 Flex Message 一路追到 Gemini 設定與 Firestore 快取
我:確認回顧應該要自然、完整,而且不要亂猜
Codex:調整 Prompt、輸出設定、完整性檢查與快取版本

我不需要先知道 thinkingBudget 或 fingerprint 應該怎麼設計,只需要把真實使用時的不舒服說清楚。Codex 則負責把那個感覺轉成可以檢查的技術問題,再逐層修正。

這也讓我重新認識「AI 功能壞掉」這件事。有時不是模型能力不足,而是模型設定、畫面呈現、資料驗證和快取策略共同造成的結果。

護照也可以分享,但不替使用者決定要傳給誰

每張護照都能切換本月、今年與全部紀錄,也可以產生分享版。

從本月護照按下「分享這張護照」,分享版仍然會保留本月期間,不會突然變成全部紀錄。

分享卡片不包含 LINE 名稱、頭像、userId 或聊天室 ID。Bot 只會準備好可以轉傳的卡片,再由使用者自己選擇要分享給哪位朋友或哪個群組,不會在背景替使用者推送個人紀錄。

即使在群組輸入「我的咖啡護照」,Bot 也只會讀取主動發出指令者自己的資料,不會把整個群組的足跡混在一起。

用一張咖啡護照,替整個功能系列收尾

回頭看,我很慶幸最後沒有選擇再加一個和前面無關的新工具。

咖啡護照沒有要求使用者多做一個步驟,而是讓以前留下的收藏、評分和足跡終於有了新的意義。它不一定是整個系列裡流程最長的功能,卻是最能把前面所有功能串起來的一個。

找店解決「現在去哪裡」,想去清單記住「以後想去哪裡」,群組投票和選時間協助大家真的約成;咖啡護照則回頭回答:

這段時間,我究竟去了哪些地方,又留下了什麼樣的咖啡生活?

而這次最有趣的地方,是一段看起來像 UI 截字的問題,最後帶我走進 Gemini 的 token、thinking、輸出驗證和 Firestore 快取。

功能做完並不代表真的完成。當我把 Bot 放回日常對話裡使用,才會看見那些測試數字很難直接告訴我的事。

現在,這隻 Bot 已經能陪使用者找店、做選擇、安排出發、留下感受,再回頭整理一路走過的咖啡足跡。咖啡護照替這段流程補上最後一頁,也讓我的 LINE Cafe Bot 功能系列在這裡形成一個完整的句點。

而在真正收尾以前,幸好 Gemini 也終於學會把最後一句話好好說完了。

完整程式碼與參考資料

GitHub:
https://github.com/zonawang/line-cafe-passport

LINE Messaging API — Flex Message:
https://developers.line.biz/en/docs/messaging-api/using-flex-messages/

LINE Messaging API — Quick reply:
https://developers.line.biz/en/docs/messaging-api/using-quick-reply/

Gemini API — Thinking:
https://ai.google.dev/gemini-api/docs/thinking

Gemini API — Text generation:
https://ai.google.dev/gemini-api/docs/text-generation

Cloud Firestore — Data model:
https://firebase.google.com/docs/firestore/data-model


上一篇
Day 28 - 不是發起人按了截止,接下來呢?我修好 LINE Bot 裡消失的下一步
下一篇
Day 30 - 從空 Repo 到咖啡護照: 30 篇文章,走完 LINE Cafe Bot 的鐵人賽
系列文
這隻 LINE Bot 不是我寫的:30 天讓 Codex 從零幫我做到上線30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言