這陣子,我陸續幫 LINE Cafe Bot 加上找咖啡廳、收藏、記錄造訪、群組投票和一起選時間等功能。
做到這裡,它已經可以從「今天想喝咖啡」一路陪我走到真正約成、出發,再記下喝完之後的感受。
我也替這個系列定了一個終點:再完成這一項,就讓這隻 LINE Bot 告一段落。
既然走到收尾,我不想再往外增加一條全新的流程,而是想回頭看看,前面做過的功能究竟留下了什麼。這時我才發現,資料已經散落在不同地方:
這些資料都有保存下來,卻還不像一段真正屬於自己的咖啡紀錄。也因為這樣,我覺得這一站最適合做的,不是再幫 Bot 多完成一件事,而是把一路累積的內容收回來。
我想看的不只是「最近去過哪間」,而是:
這個月喝了幾次咖啡?
去了幾間不同的店?
我最常留下什麼印象?
收藏的店,後來真的去了幾間?
於是這次,我和 Codex 一起完成了 line-cafe-passport,把原本分散的資料整理成一張「我的咖啡護照」,也用它替整個功能系列收尾。
一開始,我對 Codex 說的需求很簡單:
我想做最後一個功能,把使用者去過的咖啡廳整理成個人咖啡護照。
但「咖啡護照」其實可以有很多種做法。
它可以只是把去過的店全部列出來,也可以做成集點卡,甚至可以另外建立一套新的打卡資料。
Codex 先讀了上一版 Bot 的程式和 Firestore 結構,確認目前已經有哪些資料可以使用。最後我們決定不要求使用者重新打卡,也不建立另一份重複紀錄,而是直接整理原有的咖啡足跡、評分、標籤和想去清單。
這個決定很重要,因為對使用者來說,護照不是從今天才開始。
只要以前已經透過 Bot 完成過造訪紀錄,現在輸入:
我的咖啡護照
那些足跡就會自動成為護照的一部分。
完成一筆新的咖啡紀錄後,Bot 也會直接留下「查看咖啡護照」的按鈕,讓整個流程自然接起來:
記錄這次造訪
↓
選擇 1~5 分
↓
加入安靜、有插座、適合工作等標籤
↓
完成紀錄
↓
查看咖啡護照

如果只是把所有店名列出來,它其實和「我的咖啡足跡」沒有太大差別。
所以我和 Codex 先討論:哪些數字真的能讓人快速看懂自己的咖啡生活?
最後,每張護照會整理:
我也做了三種期間:
我的咖啡護照 → 全部紀錄
本月咖啡護照 → 這個月
今年咖啡護照 → 今年
這樣既能看到長期累積,也能把本月的生活單獨拿出來回顧。
時間區間固定用 Asia/Taipei 計算。這個細節看起來很小,但如果直接使用 UTC,月底深夜留下的紀錄可能會被算到錯的月份,跨年時也可能跑到不同年度。
Codex 在寫統計邏輯時,還替這些容易忽略的日期邊界補上測試。對我來說,這也是使用 AI 協作很實用的地方:我想到的是「我要看本月」,它會繼續往下追問「本月是用哪個時區切分?」

護照裡同時有「造訪次數」和「不同店家數」,所以程式必須知道哪些紀錄其實是同一家店。
如果只比對店名,很容易遇到空格、大小寫或名稱更新造成的差異。因此 Codex 幫我把規則整理成:
優先使用標準化後的 Google Maps URI
↓
沒有 URI 時,才退回使用店名判斷
假設我去了同一間店三次,護照會顯示三次造訪,但不同店家仍然只有一間。
體驗標籤也不能看到一個就直接加總。同一筆造訪裡即使意外出現重複標籤,也只會計算一次;標籤票數相同時則使用固定排序,避免每次打開卡片,顯示順序都不一樣。
這些規則不一定會被使用者注意到,卻決定了護照上的數字能不能相信。
我很想知道一件事:放進想去清單的店,後來有多少真的去過?
但目前的想去清單只保存「現在仍在清單裡」的店,移除收藏後不會留下完整歷史。因此這個數字不能假裝是終身收藏轉換率。
最後,程式只會計算同時符合三個條件的店:
目前仍在想去清單
+
足跡裡有相同的 Google Maps URI
+
造訪時間晚於加入收藏的時間
畫面上也直接寫成「目前收藏後已造訪」,讓它如實反映現有資料,而不是替一個不完整的數字取太厲害的名字。
這段也是 Codex 幫我踩煞車的地方。AI 不只是幫忙把數字算出來,也可以協助確認:現有資料究竟足不足以支持我想說的結論。
護照最上面的次數、平均分數和店家排名,都由程式直接計算。
Gemini 不負責算數字,只會收到一份精簡的統計資料,例如:
{
"period": "2026 年 9 月",
"visits": 5,
"uniqueCafes": 4,
"averageRating": 4.6,
"fiveStarVisits": 3,
"topTags": [
{ "label": "安靜", "count": 3 },
{ "label": "適合工作", "count": 2 }
]
}
接著,它會把這些已經確定的事實整理成兩句繁體中文回顧。
Prompt 也明確限制它不能猜測使用者的個性、消費金額、沒提供的地點,或統計裡根本沒有出現的偏好。
我的分工原則是:
程式 → 計算必須正確的事實
Gemini → 把事實整理成自然的短文
這樣即使生成式 AI 的文字每次有一點變化,護照上的核心數字仍然一致。
如果 Gemini 暫時無法回覆,程式也會使用根據同一份統計產生的備援文字,不會讓整張護照因為一小段文案失敗而消失。
功能完成並部署後,我真的在 LINE 裡輸入「我的咖啡護照」。
卡片正常出現,統計資料也都在,但「Gemini 咖啡回顧」看起來像話還沒說完:句子停在一半,最後沒有完整結尾。
我第一個懷疑的是 Flex Message。
會不會是文字框高度不夠?是不是忘了開啟換行?或 LINE 對顯示行數有限制?
Codex 沒有直接把卡片拉高,而是先沿著資料流往回檢查:
Flex Message 是否限制行數?
↓
summary 是否在顯示前被裁切?
↓
Gemini 原始回覆是否已經不完整?
↓
為什麼重新打開還是同一段?
結果發現,卡片的文字已經設定 wrap: true,也沒有 maxLines 限制。畫面其實沒有把文字藏起來,而是收到的摘要本來就只有半句。
原本 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 時,我們還發現原本有一句寫成「溫暖但不誠懇」。這和我真正想表達的完全相反,所以也一起改成「溫暖自然且不浮誇」,並要求最後一句一定要用完整標點結束。
這個功能為了避免每次打開護照都重新呼叫 Gemini,會替統計資料建立 fingerprint,並把摘要存在 Firestore。
大致概念是:
期間+次數+評分+標籤+店家+收藏後造訪
↓
summary fingerprint
只要統計沒有改變,下次就直接使用快取。
這本來是好事,但這次也帶來另一個問題:第一次產生的半句已經存進 Firestore。就算程式提高 token 上限,只要 fingerprint 沒變,使用者還是會繼續看到同一段舊摘要。
因此 Codex 在 fingerprint 裡加入摘要版本:
summaryVersion: 2
新版上線後,舊 fingerprint 會自然失效。使用者不必刪除資料,也不必新增一筆假的咖啡足跡,下一次打開護照就會重新產生摘要。
最後,我們又補上一層保護:Gemini 回覆若沒有以完整句號、驚嘆號或問號結束,就不把這段半句存進快取,而是改用完整的備援回顧。超過長度時,也只會保留到最近一個完整句子,不會直接從第 180 個字切下去。
修正後的流程變成:
Gemini 產生回顧
↓
清除 Markdown 與多餘空白
↓
確認最後一句完整
↙ ↘
完整 不完整
↓ ↓
寫入快取 使用備援文案

如果只看表面,這次問題好像只是「文字沒有顯示完整」。
但 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