前幾天的 SubWise 已經可以透過 LINE 接收文字訊息,並利用 Gemini 判斷使用者意圖,再把記帳、查詢、訂閱與消費分析等資料寫入或讀取 Google Sheets。但如果真的要做到「智慧記帳」,光靠文字輸入還不夠——現實生活中的消費紀錄,很多時候其實是一張發票、一張收據。
所以今天,我決定讓 SubWise 跨出重要的一步:直接把發票照片丟進 LINE,讓 Gemini 看懂圖片,辨識消費資料,再自動完成記帳。 今天的目標,就是完成這條完整流程:
LINE 發票照片 → LINE Webhook → 取得圖片內容 → Gemini 多模態 AI
→ 解析成 Expense JSON → save_expense() → Google Sheets → LINE 回覆記帳結果
這也是 SubWise 從「文字型 AI Bot」逐漸變成「多模態智慧記帳工具」的重要一天。今天主要完成五件事:新增 LINE 圖片訊息處理 Handler、從 LINE 取得使用者上傳的圖片、將圖片傳給 Gemini 多模態模型、把辨識結果交給既有的 save_expense()、成功把真實發票資料寫入 Google Sheets 並回傳 LINE 結果。
之前的文字訊息 Handler 只處理 MessageEvent + TextMessageContent,使用者傳送圖片時完全不會被觸發。因此新增獨立的 Handler:
@handler.add(MessageEvent, message=ImageMessageContent)
def handle_image_message(event):
SubWise 開始可以區分不同類型的 LINE 訊息:文字進 handle_message(),圖片進 handle_image_message()。
LINE 不會直接把整張圖片放進 Webhook request,只提供 event.message.id。因此需要用 MessagingApiBlob 搭配 line_bot_api.get_message_content(event.message.id),才能把圖片內容真正抓回來,實際測試成功取得 191903 bytes 的圖片資料,代表「LINE → Flask → 圖片內容」這關已經打通。
在 gemini_client.py 新增圖片辨識流程,讓圖片可以和文字 Prompt 一起送進 Gemini:
data = ask_gemini_with_image(image_bytes, "image/jpeg")
以前的流程是「文字 → Gemini → JSON」,現在變成「圖片 → Gemini Vision → JSON」,SubWise 正式開始具備多模態輸入能力。
今天最重要的地方之一,是 Gemini 並不是回答「這是一張發票」,而是直接產生 SubWise 可以使用的 JSON,例如:
{'type': 'expense', 'category': 'Entertainment', 'amount': 1089, 'item': '午晚5hr入場票與清潔費', 'date': '2026-08-19'}
Gemini 的工作只負責「看懂圖片 → 結構化成 Expense JSON」,後面的資料驗證與寫入仍然交給 SubWise 自己的 Service,這樣的架構比讓 AI 直接負責所有事情更容易維護。
這是今天架構設計上很重要的成果:圖片辨識後的資料,直接交給原本就存在的 save_expense(data)。文字記帳與圖片記帳兩條路徑,最後在同一個地方會合:
┌→ 文字 → Gemini ─┐
LINE ────────┤ ├→ save_expense() → Google Sheets
└→ 圖片 → Gemini ─┘
不同輸入方式,共用同一套資料寫入邏輯,這是今天最值得保留的設計。
坑一:'bytes' object has no attribute 'content'
第一次測試時,Render Log 出現這個錯誤。原本的寫法預期 get_message_content() 回傳類似 HTTP Response 的物件,因此用了 response.content,但實際上目前的 LINE SDK 已經直接回傳圖片的 bytes。修正成直接使用 image_bytes = line_bot_api.get_message_content(event.message.id) 後就成功了。這也讓我實際理解到:「程式可以通過語法檢查,不代表整條 API 流程真的正確。」
坑二:Gemini 辨識成功,卻沒有成功記帳
Gemini 判斷出 type = expense、amount = 100、date = 2026-08-20,但 category 是 None,LINE 卻收到「發票辨識成功,但記帳失敗」。查看 expense_service.py 才發現,save_expense() 本來就有必要欄位驗證,category 缺失會直接阻止寫入——這其實不是 Bug,反而代表原本的資料驗證機制正常運作。這也讓我更清楚看到多模態 AI 開發的一個實際問題:AI 辨識成功,不代表資料一定完整。發票上看得到金額、日期、店家,但「分類」不一定能從圖片直接推導出來,因此系統需要 AI 層(圖片 → 結構化資料)與業務邏輯層(檢查必要欄位 → 確認合理性 → 寫入)分開,AI 不應該直接等於業務邏輯。
坑三:AFC Warning 不是失敗原因
執行時看到 Gemini SDK 顯示「不建議直接在 generate_content 使用自動函式呼叫」的警告,但後續 Gemini 仍然成功回傳正確的 JSON,代表這不是造成圖片辨識失敗的原因。這提醒我:看到 Warning 不代表功能一定失敗,還是要看實際執行結果與資料流,今天真正造成失敗的是前面的 bytes.content 問題。
再次測試發票,Gemini 成功辨識並完成完整流程,LINE 回覆:
✅ 發票辨識成功,已完成記帳!
📅 日期:2026-08-19
📂 分類:Entertainment
💵 金額:NT$1089
📝 項目:午晚5hr入場票與清潔費
代表完整流程真的跑通了:📷 LINE 發票 → 📨 Webhook → 🖼️ 圖片 bytes → 🤖 Gemini 多模態辨識 → 📦 Expense JSON → 🛡️ save_expense() 驗證 → 📊 Google Sheets → 💬 LINE 回覆。


使用者只需要將發票或收據照片傳給 LINE Bot,SubWise 就會自動擷取消費日期、金額、分類與品項,完成記帳。
| 測試項目 | 結果 |
|---|---|
| Python 語法檢查 | ✅ |
app.py Import |
✅ |
| Render 部署 | ✅ |
| LINE Webhook | ✅ |
| LINE 圖片事件接收 | ✅ |
| LINE 圖片下載 | ✅ |
| 圖片 bytes 取得 | ✅ |
| Gemini 圖片辨識 | ✅ |
| Expense JSON 產生 | ✅ |
save_expense() 驗證 |
✅ |
| Google Sheets 寫入 | ✅ |
| LINE 成功回覆 | ✅ |
| 真實發票測試 | ✅ |
| Git Push | ✅ |
| 工作目錄保持乾淨 | ✅ |
Git 版本管理:完成 git commit -m "Fix LINE image bytes handling" 並成功 push。另外也把不小心被追蹤的測試發票 receipt.jpg 移出版本控制(git rm --cached receipt.jpg)並加入 .gitignore,確認 git check-ignore -v receipt.jpg 顯示已被正確忽略。
目前 SubWise 的多模態流程:
今天已經完成「看懂發票並記帳」,明天 Day 21 可以開始思考:如果 Gemini 辨識出來的資料不完整,SubWise 能不能不要直接失敗,而是主動詢問使用者?
例如辨識出金額、日期、店家,但不確定消費分類時,主動提供選項讓使用者補齊。如此一來,SubWise 就會從單純的「AI 看懂 → 寫入資料」,進一步變成「AI 看懂 → 發現缺漏 → 主動詢問 → 補齊資料 → 完成記帳」,這也會是 SubWise 從「多模態 AI 工具」繼續往「真正的 AI Agent」前進的重要一步。我們明天見!