今天要做的事:把 FAQ 放進向量資料庫,做出 W2,並且解決一個比建 RAG 更難的問題 ——
怎麼讓它在知識庫裡沒有答案時,老實說「沒有」。
Day 04 我把 7 條 FAQ 塞在 System Prompt 裡。Day 06 的 T05 證明了這行不通:問到 FAQ 沒寫的事,它會編一條政策出來。
我一開始以為這是 prompt 寫得不夠嚴格。後來想清楚了,這是結構問題:
當 FAQ 是 prompt 裡的一段文字時,模型無法區分這兩種狀態:
對模型來說,兩者都只是「context 裡沒看到」。而「context 裡沒看到」的預設反應是 —— 用生成能力補上。
檢索的價值不只是能放更多知識,而是它產生了一個「查無此項」的明確事件。 這個事件可以被程式捕捉、可以觸發不同的處理路徑。這才是 T05 的真正解法。
這是我認為最被低估的一步。FAQ 的寫法直接決定檢索品質,比選哪個向量資料庫重要一百倍。
我的 faq.md 寫成這樣,每一條都是一個自我完整的段落:
### [SHIP-01] 出貨時間
一般訂單於付款完成後 1–3 個工作日內出貨,例假日與國定假日不計入。
預購商品與烘焙日指定商品另有說明,依商品頁標示為準。
### [SHIP-02] 運費與免運門檻
單筆訂單運費 NT$80。單筆金額滿 NT$1500 免運費。
運費以結帳頁顯示金額為準。
### [SHIP-03] 配送範圍
目前配送台灣本島與離島(澎湖、金門、馬祖)。不提供海外配送。
離島訂單運送時間較本島多 1–2 個工作日。
### [SHIP-04] 指定配送時段
無法指定配送日期或時段。物流配送時間由宅配業者安排。
如需調整配送,請於收到出貨通知後直接聯繫物流業者。
### [BEAN-01] 咖啡豆保存方式
建議於烘焙日後 30 天內飲用完畢。開封後請密封並冷藏,避免陽光直射。
冷凍保存可延長至 60 天,但每次取出後需回溫至室溫再研磨。
### [BEAN-02] 烘焙度與沖煮方式建議
淺焙:酸質明亮,適合手沖、冰滴、冷萃。
中焙:平衡,適合手沖與虹吸。
中深焙/深焙:適合義式濃縮、摩卡壺、奶類飲品。
### [RET-01] 退貨期限
商品到貨後 7 天內可申請退貨(以物流簽收日為起算日)。
申請方式:來信客服或於會員中心提出。
### [RET-02] 咖啡豆退貨限制
已開封的咖啡豆因食品衛生規範,不接受退貨或換貨。
未開封且外包裝完整者,可在 7 天內辦理退貨。
### [RET-03] 器材保固
器材類商品保固一年,自到貨日起算。
人為損壞、自行拆解、正常耗損(如濾網老化)不在保固範圍。
四個寫法原則,每一條都是我踩過雷才學到的:
| 原則 | 為什麼 |
|---|---|
| 每條都自我完整 | 檢索出來的是單一 chunk,如果它寫「同上」「參考前述」,模型看不到前面那段 |
| 每條有唯一編號 | [SHIP-04] 這種 ID 就是 source,讓答案可以追溯 |
| 標題含關鍵字 | 「指定配送時段」這個標題本身就會被向量抓到 |
| 刻意寫出「不能做的事」 | 這是重點,見下方 |
最後一條特別講。看 [SHIP-04]:我刻意新增了一條「無法指定配送時段」。
Day 06 的 T05 之所以會幻覺,是因為原本的 FAQ 只寫了能做的事,沒寫不能做的事。檢索當然撈不到,然後模型就編了。
你的 FAQ 要明確寫出「我們不提供什麼」。 這比任何 prompt 技巧都有效。我後來檢查了一輪真實 FAQ,補了 6 條「不能做的事」,幻覺率直接掉了一大半。
這是一個獨立的、手動執行的 workflow,不是主流程的一部分。我命名 setup-faq-index:
Manual Trigger ──▶ Google Drive(下載 faq.md)──▶ Vector Store(Insert 模式)
│
┌─────────────┼─────────────┐
Default Data Embeddings Text Splitter
Loader
這是我做錯過的地方。預設的 Recursive Character Text Splitter + chunk size 1000 對這份 FAQ 是錯的,它會把 [BEAN-01] 的後半段和 [BEAN-02] 的前半段切在同一塊,檢索結果會很混亂。
正確做法:用 Character Text Splitter,Separator 設成 ### (三個井號加空格)。
這樣每一個 FAQ 條目就是一個 chunk,乾淨對應。
如果你的知識是長篇文件(產品手冊、合約),那 Recursive 是對的。但條目式的 FAQ 要按條目切。 判斷法:問自己「檢索出一個 chunk,它自己能不能回答一個問題」。
800(夠大,不會再被二次切開)0(條目之間不需要重疊)我用 Qdrant,Docker 起來:
docker run -d --name qdrant -p 6333:6333 -v qdrant_data:/qdrant/storage qdrant/qdrant
Vector Store 節點設定:Operation Insert Documents、Collection Name haodou_faq。
Metadata 記得帶上。在 Default Data Loader 裡加 metadata 欄位 doc: faq、version: 2026-04。這在你有多份知識文件時是救命的。
手動執行一次,去 Qdrant 的 dashboard(http://localhost:6333/dashboard)確認有 9 個 point 進去了。
新建 workflow W2-knowledge:
Execute Workflow Trigger ──▶ AI Agent ──▶ Code(守門員)──▶ (return)
│
┌───────────────┼──────────────┬──────────────┐
Chat Model Structured Output Vector Store (無 Memory)
(中) Parser Tool
掛一個 Vector Store Question Answer Tool,或直接用 Vector Store 節點的 retrieve-as-tool 模式。
haodou_faq
4
faq_search
搜尋好豆選物的官方 FAQ 知識庫,回傳最相關的條目原文。
使用時機:任何關於商品、保存、沖煮、出貨政策、退換貨政策、保固的問題。
輸入:用客戶問題的關鍵字組成的查詢句。
注意:這個工具「總是」會回傳最相似的幾條,即使它們與問題無關。
你必須自己判斷回傳的內容是否真的回答了問題。
Tool Description 最後那兩句是今天最重要的三行。 為什麼?
這件事必須講清楚。向量檢索的運作方式是「計算相似度,回傳最相似的 K 筆」。
當你問「你們有賣濾掛包嗎?」而 FAQ 裡完全沒有濾掛包的資料,檢索不會回傳空的。它會回傳相似度最高的 4 筆 —— 可能是 [BEAN-01] 保存方式、[SHIP-01] 出貨時間之類的東西。
然後模型看到 4 筆看起來像 FAQ 的東西,就會覺得「我查到資料了」,然後硬掰。
所以「建好 RAG」只解了一半的 T05。另一半是「判斷檢索結果到底相不相關」。
我試了三種做法:
| 做法 | 怎麼做 | 結果 |
|---|---|---|
| A. 相似度門檻 | 在 Code 節點過濾 score < 0.7 的結果 | 有效,但門檻值很難調;同一個門檻在不同問題上表現差很多 |
| B. 讓 LLM 判斷相關性 | prompt 明確要求「先判斷檢索結果是否回答了問題」 | 有效且穩定 ← 我用這個 |
| C. 強制引用條目編號 | 要求 source 必須是 [XXX-NN] 格式,程式驗證編號存在 |
有效,當作第二道防線 ← 也用 |
B + C 一起用,幻覺率降到很低。C 的價值在於它是程式可驗證的:如果模型編了一個 [SHIP-99],我的 Code 節點對照一下合法編號清單就抓到了。
你是「好豆選物」的知識查詢專員。你的唯一工作是:
從官方 FAQ 知識庫中找出能回答主管問題的條目,並回報。
## 你的職責邊界
- 你只回報 FAQ 裡「確實寫了」的內容。
- 你不寫給客人看的回信,那是別人的工作。
- 你不使用自己的通用知識回答。就算你知道答案,FAQ 沒寫就是沒寫。
## 可用工具
- faq_search:搜尋 FAQ 知識庫
## 工作流程(務必照做)
1. 把主管的問題轉成關鍵字查詢,呼叫 faq_search。
2. 【最重要的一步】逐條檢查回傳的結果,問自己:
「這一條的內容,有直接回答主管的問題嗎?」
faq_search 總是會回傳最相似的幾條,即使它們無關。
3. 如果有條目直接回答了問題 → status = ok,
facts 的 source 填該條目的編號,格式 faq![編號]
4. 如果回傳的條目都只是「話題相近」但沒有回答問題
→ status = not_found
5. 如果只回答了問題的一部分 → status = partial,
在 missing 列出沒被回答的部分
6. 最多呼叫 faq_search 2 次(可換關鍵字重試一次)
## 關於 not_found
回報 not_found 是完全正確、被鼓勵的行為。
FAQ 沒寫的事,公司會由人工補上答案,不需要你猜。
編造一條不存在的政策,會造成客戶實際損失,這是最嚴重的錯誤。
## 回報規則
- facts 中每一筆的 source 必須是 faq! 加上真實存在的條目編號
例:faq![SHIP-04]
- 不要自己發明條目編號
- answer 欄位用中文摘要 FAQ 的內容,但不要加入 FAQ 沒寫的資訊
- 不要在 answer 裡寫客套話,這是給主管看的,不是給客人看的
「關於 not_found」那一段,我特別說明了「編造的後果」。這比單純說「不要編造」有效 —— 給模型一個理由,它遵守得比較好。
// 合法的 FAQ 條目編號清單。維護 FAQ 時同步更新這份清單。
const VALID_IDS = new Set([
'SHIP-01','SHIP-02','SHIP-03','SHIP-04',
'BEAN-01','BEAN-02',
'RET-01','RET-02','RET-03'
]);
const r = $input.first().json.output ?? $input.first().json;
const taskId = $('Execute Workflow Trigger').first().json.task_id;
const facts = Array.isArray(r.facts) ? r.facts : [];
const checked = [];
const fakeIds = [];
for (const f of facts) {
if (!f || !f.source) continue;
// 期望格式:faq![SHIP-04]
const m = String(f.source).match(/^faq!\[([A-Z]+-\d+)\]$/);
if (m && VALID_IDS.has(m[1])) {
checked.push(f);
} else {
fakeIds.push(f.source);
}
}
let status = r.status ?? 'error';
if (status === 'ok' && checked.length === 0) {
status = 'not_found'; // 沒有任何一筆引用了真實條目
}
return [{
json: {
task_id: taskId,
worker: 'knowledge',
status,
confidence: typeof r.confidence === 'number' ? r.confidence : 0,
facts: checked,
answer: r.answer ?? '',
missing: Array.isArray(r.missing) ? r.missing : [],
notes: r.notes ?? '',
_audit: {
fabricated_sources: fakeIds, // ← 這裡就是幻覺證據
downgraded: status !== r.status
}
}
}];
_audit.fabricated_sources 這個欄位在我第一週的測試裡抓到了 11 次捏造的條目編號。如果沒有這個檢查,那 11 次會全部變成寄給客人的假政策。
輸入:
{
"task_id": "t_test_5",
"question": "客戶詢問下單時是否可以指定星期六上午配送",
"attempt": 1
}
輸出:
{
"status": "ok",
"confidence": 0.93,
"facts": [
{
"key": "designated_delivery_time",
"value": "無法指定配送日期或時段,配送時間由宅配業者安排;如需調整可於收到出貨通知後聯繫物流業者",
"source": "faq![SHIP-04]"
}
],
"answer": "依 FAQ SHIP-04,不提供指定配送日期或時段。客戶可於收到出貨通知後直接聯繫物流業者調整。",
"missing": [],
"notes": ""
}
T05 解掉了,而且是「正確回答」而不只是「拒絕回答」 —— 因為我在 FAQ 裡補了那條「不能做的事」。
再測一個 FAQ 真的沒寫的:「你們有賣濾掛包嗎?」
{
"status": "not_found",
"confidence": 0.9,
"facts": [],
"answer": "FAQ 中沒有關於濾掛包(掛耳包)商品的資訊。檢索到的條目為 BEAN-02(烘焙度建議)與 SHIP-02(運費),均未回答此問題。",
"missing": ["是否販售濾掛包"],
"notes": "建議補充 FAQ 條目:商品品項清單"
}
這個輸出我很滿意,三個細節:
not_found,而不是掰一個答案answer 裡說明了自己檢索到什麼、為什麼判定不相關 —— 這對除錯非常有用notes 建議補 FAQ —— 這變成一個知識庫成長的回饋迴路。我後來真的做了一個小流程,把所有 not_found 的 notes 收集到一張 Sheet,每週看一次該補什麼1. Simple Vector Store 在 n8n 重啟後空了,但流程不會報錯。 它只是每次都檢索到 0 筆,然後 W2 一直回 not_found。我找了半小時才發現是索引沒了。正式環境一定用外部向量庫。
2. 換 embedding 模型忘記重建索引。 查詢向量和索引向量來自不同模型,相似度計算出來的結果是亂的 —— 而且不會報錯,只是檢索品質莫名很差。
3. Top K 設太大反而更糟。 我一開始設 K=8,想著「多給一點總比少給好」。結果模型看到 8 條東西,其中 6 條無關,它的判斷力下降了,開始把相近的條目當成答案。K=3 或 4 最好。
4. FAQ 更新後忘記重跑索引。 這是營運層面的坑。建議做法:把 setup-faq-index 改成 Schedule Trigger 每天跑一次,Vector Store 用 upsert 模式(或先清 collection 再重建,FAQ 這種量級重建很快)。
### 當 separator,不要用固定字數切。_audit.fabricated_sources 是你唯一能看見幻覺的地方。not_found 的 notes 可以變成知識庫成長的回饋迴路。明天做 W3 回覆專員 —— 那位手上完全沒有工具的同事。我會測一件很有趣的事:給它假的 facts,看它會不會自己加料。