昨天講完全免費選型的取捨,今天正式進 line-notion-bot 的細節,第一站是 Notion 資料庫設計。這件事聽起來像是開個 Notion 建幾個欄位而已,但實際做下去會發現,欄位怎麼設計,其實是在幫 AI 定輸出規格——給 AI 多大的自由度、AI 的輸出會不會被 Notion 擋下來,答案都寫在資料庫的欄位型態裡。
先看資料庫長什麼樣,六個欄位:標題(Title)、分類(Select)、標籤(Multi-select)、連結(URL)、摘要(Text)、待辦(Checkbox)。程式碼裡對應一個 PROP 物件:
const PROP = {
title: '標題',
category: '分類',
tags: '標籤',
url: '連結',
summary: '摘要',
todo: '待辦',
};
這個對應完全沒有模糊空間,因為程式是用中文欄位名稱去查 Notion API,資料庫裡打錯一個字,寫入時就是整包失敗。
再看 AI 要輸出什麼。分類函式 classify() 丟給 AI 的 system prompt 最後一行寫死了格式:
格式:{"title": string, "category": string, "tags": string[], "summary": string}
這四個欄位,剛好對到 Notion 資料庫裡四個型態不一樣的欄位,而且型態直接決定了 AI 能寫什麼。分類(category)是 Select,Select 的本質是「單選」,所以 AI 只能從固定的七個分類裡挑一個,prompt 裡也明講「必須從以下選項擇一」。標籤(tags)是 Multi-select,可以複選也可以自由創造新選項,所以 prompt 對標籤的要求完全不一樣:「請給 2-4 個精準的關鍵字」,沒有限制在固定清單裡——這也是為什麼分類固定七種、標籤卻能越長越多,兩個欄位型態不同,管理策略天生就不一樣。
寫進 Notion 的地方也看得出這層對應:
const properties = {
[PROP.title]: { title: [{ text: { content: title.slice(0, 200) } }] },
[PROP.category]: { select: { name: category } },
[PROP.tags]: { multi_select: tags.map((t) => ({ name: t.slice(0, 50) })) },
[PROP.url]: { url },
[PROP.summary]: { rich_text: [{ text: { content: summary.slice(0, 1900) } }] },
};
每個欄位都帶了 slice 長度限制:標題最多 200 字、每個標籤最多 50 字、摘要最多 1900 字。這些數字不是隨便抓的,是照著 Notion API 對各欄位型態的實際上限抓保守值,防止 AI 一興奮寫太長,整個 API request 直接被 Notion 退回。換句話說,欄位設計不只是定了「AI 要填什麼」,也定了「AI 最多能填多少」。
分類還有一個防呆:AI 回傳的 category 如果不在 CATEGORIES 清單裡,程式會退回預設值「生活其他」,而不是讓一個奇怪的分類直接寫進 Select 欄位。免費模型偶爾會亂回東西,這種時候寧可分類分得不準,也不要讓資料庫裡混進一堆一次性的怪分類,之後要整理會很痛苦。
最後是待辦(Checkbox)這個欄位,它完全不在 classify() 的輸出規格裡,AI 從頭到尾不會碰它。這是留給我事後手動標記的欄位,用 LINE 傳「待辦 <連結>」才會被改成 true。設計資料庫的時候,這種「不是給 AI 填、是給人事後用」的欄位也要一起想進去,不然日後想加這類功能,就得回頭改資料庫結構,舊資料還要一筆一筆補。
所以這一步看起來只是拉幾個 Notion 欄位,實際上是把後面所有東西的骨架定下來了:AI 的 prompt 要照著欄位型態寫、程式的容錯要照著欄位限制做、之後要加的功能也要先想好放哪個欄位。明天來看下一步:怎麼申請 LINE Messaging API Channel,還有 webhook 簽章驗證這關要注意什麼。