昨天講完輸入中動畫、Quick Reply、圖文選單跟白名單,line-notion-bot 的功能到這裡全部講完了。今天不拆新的函式,回頭把整支 src/index.js 掃一遍,只回答一個問題:這個 Bot 哪些地方用了 AI,哪些地方我刻意不用。
先講數字。整個檔案裡,真的呼叫模型的地方只有一個,就是 classify 裡面那一行:
const raw = await callAI(env, system, user);
我在檔案裡搜 callAI,除了函式定義,只會在這裡出現一次。也就是說,從收到 webhook 到回覆我,整條流程的 AI 呼叫就只有這一次。callAI 底下雖然有 Mistral 和 OpenRouter 兩層,那是 Day 12 講的備援,對外看起來還是同一次「請模型分類」。
這一次 AI 負責三件事:選分類、給 2 到 4 個標籤、寫 2 到 3 句摘要。這三件事的共同點是沒有標準答案。同一篇文章,我自己分類可能今天丟「技術開發」、明天丟「工具資源」,摘要更不用說。這種判斷我不想自己寫規則,寫了也寫不完,所以交給模型。
其他的地方,我一個一個列給你看。
先從進來的那一端開始。verifySignature 驗證簽章用的是 HMAC-SHA256,一個字元一個字元比對,Day 6 講過。handleEvents 的白名單比的是 userId,Day 16 講過。這兩關是安全,不是判斷,答案只有過或不過,沒有模型插手的空間。
再來是指令跟搜尋,Day 15 整篇都在講這塊。parseCommand 靠 startsWith 比對關鍵字,handleSearch 是一串 if,searchNotion 是抓最近 100 筆自己用 includes 比字串。我當時的理由是,刪除、標待辦、改分類這些動作,我要的是照字面執行,一個字都不想被詮釋。
然後是 AI 前後的那兩步。進 AI 之前,fetchReadable 把內文截在 6000 字,getExistingTags 去 Notion 撈最近 100 筆的既有標籤。這兩步都沒有用 AI,一個是切字串,一個是查資料庫去重。有意思的是,它們雖然不是 AI,卻是在決定 AI 看到什麼。內文給多少、既有標籤給哪些,都是我在 prompt 外面先排好的。
AI 回來之後,也是規則在把關。看 classify 最後回傳的那段:
return {
title: parsed.title || url,
category: CATEGORIES.includes(parsed.category) ? parsed.category : '生活其他',
tags: Array.isArray(parsed.tags) ? parsed.tags.slice(0, 4) : [],
summary: parsed.summary || '',
};
標題沒給就用網址,分類不在七個裡面就改成「生活其他」,標籤不是陣列就給空陣列,多了就只留前 4 個。模型說的每一個欄位,進 Notion 之前都先過一遍我的檢查。JSON 解析壞掉的三層容錯(Day 10)也是同一個思路:模型輸出的東西我不直接信。
最後寫進 Notion 那一步,重複網址查重(Day 14)靠的是 findPageByUrl,用 Notion API 的 url.equals 比對,不是請模型判斷「這兩篇是不是同一篇」。
這樣掃完,我自己歸納出一條分界線:答案有標準的,用規則;答案沒標準的,才用 AI,而且 AI 的輸出要被規則包起來。 前面幾天都是在講個別的功能,到這邊才算說得清楚為什麼它們長這樣。
不過這條線我也不是每個地方都守得很乾淨,有兩個地方要老實講。
第一個是重複網址。你看 handleTextMessage 的順序:先 classify,再 createNotionPage,查重是在 createNotionPage 裡面才做的。所以我同一個網址傳第二次,Bot 會先花一次 AI 呼叫重新分類,然後才發現這篇已經存過、改成更新。查重其實可以搬到 AI 前面,重複的網址直接省掉模型那一步。現在的寫法等於每次重傳都會重新分類一次,更新時還會在頁面底下補一段「(重新分類更新)」的摘要,算是副作用,只是它確實多花了一次額度。
第二個是規則的缺口。Day 15 提過,parseCommand 沒有轉小寫,「Todo」不會被認成指令,而搜尋那邊有轉。同樣是規則,兩邊的處理不一致。用規則換來的可預測性,前提是規則自己寫對,這點我沒辦法推給模型。
另外整份程式沒有任何自動化測試,這在 CLAUDE.md 的技術債裡有記。規則寫死的好處是行為看程式就知道,但沒有測試的話,改一個地方有沒有弄壞別的,還是得自己手動傳訊息確認。
回頭看這十幾天,我對「AI 應用」的感覺跟剛開始不太一樣。一開始我以為要做的是怎麼把 AI 用好,做完才發現花最多力氣的地方,是決定哪裡不用它,還有它說出來的東西要怎麼收口。模型那一行呼叫反而是整個檔案裡最短的部分。
第二部到這裡結束。明天開始進第三部,換 kid-english-ig-carousel,先從一個問題談起:我不會設計,為什麼還能發 IG,還把圖卡流程寫成一個 Skill。