iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

Day 12|Worker 2 知識專員:從 FAQ 文件找答案

今天要做的事:把 FAQ 放進向量資料庫,做出 W2,並且解決一個比建 RAG 更難的問題 ——
怎麼讓它在知識庫裡沒有答案時,老實說「沒有」。

先講為什麼不能繼續塞 Prompt

Day 04 我把 7 條 FAQ 塞在 System Prompt 裡。Day 06 的 T05 證明了這行不通:問到 FAQ 沒寫的事,它會編一條政策出來。

我一開始以為這是 prompt 寫得不夠嚴格。後來想清楚了,這是結構問題:

當 FAQ 是 prompt 裡的一段文字時,模型無法區分這兩種狀態:

  • 「我查過知識庫,裡面沒有這一條」
  • 「我的訓練知識裡剛好沒提到這個」

對模型來說,兩者都只是「context 裡沒看到」。而「context 裡沒看到」的預設反應是 —— 用生成能力補上。

檢索的價值不只是能放更多知識,而是它產生了一個「查無此項」的明確事件。 這個事件可以被程式捕捉、可以觸發不同的處理路徑。這才是 T05 的真正解法。

步驟一:把 FAQ 寫成可切塊的格式

這是我認為最被低估的一步。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)

這是一個獨立的、手動執行的 workflow,不是主流程的一部分。我命名 setup-faq-index:

Manual Trigger ──▶ Google Drive(下載 faq.md)──▶ Vector Store(Insert 模式)
                                                        │
                                          ┌─────────────┼─────────────┐
                                    Default Data      Embeddings   Text Splitter
                                      Loader

關鍵設定:Text Splitter 選什麼

這是我做錯過的地方。預設的 Recursive Character Text Splitter + chunk size 1000 對這份 FAQ 是錯的,它會把 [BEAN-01] 的後半段和 [BEAN-02] 的前半段切在同一塊,檢索結果會很混亂。

正確做法:用 Character Text Splitter,Separator 設成 ### (三個井號加空格)。

這樣每一個 FAQ 條目就是一個 chunk,乾淨對應。

如果你的知識是長篇文件(產品手冊、合約),那 Recursive 是對的。但條目式的 FAQ 要按條目切。 判斷法:問自己「檢索出一個 chunk,它自己能不能回答一個問題」。

  • Chunk Size:800(夠大,不會再被二次切開)
  • Chunk Overlap:0(條目之間不需要重疊)

Embeddings 與 Vector Store

  • Embeddings:任一 embedding 模型都行,重點是建索引和查詢必須用同一個模型(換了模型要整個重建,這是我第二個踩雷)。
  • Vector Store:
    • 開發測試:Simple Vector Store(in-memory,n8n 重啟就沒了)
    • 正式:Qdrant(自架一個 container 很簡單)或 PGVector(如果你已經有 Postgres)

我用 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 進去了。

步驟三:W2 的 workflow

新建 workflow W2-knowledge:

Execute Workflow Trigger ──▶ AI Agent ──▶ Code(守門員)──▶ (return)
                                │
                ┌───────────────┼──────────────┬──────────────┐
           Chat Model    Structured Output  Vector Store   (無 Memory)
             (中)          Parser            Tool

Vector Store Tool 的設定

掛一個 Vector Store Question Answer Tool,或直接用 Vector Store 節點的 retrieve-as-tool 模式。

  • Collection:haodou_faq
  • Limit(Top K):4
  • Tool Name:faq_search
  • Tool Description:
搜尋好豆選物的官方 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 節點對照一下合法編號清單就抓到了。

W2 的 System Prompt

你是「好豆選物」的知識查詢專員。你的唯一工作是:
從官方 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」那一段,我特別說明了「編造的後果」。這比單純說「不要編造」有效 —— 給模型一個理由,它遵守得比較好。

Code 節點:驗證條目編號真的存在

// 合法的 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 次會全部變成寄給客人的假政策。

實測:T05 那封信

輸入:

{
  "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 條目:商品品項清單"
}

這個輸出我很滿意,三個細節:

  1. 它說了 not_found,而不是掰一個答案
  2. 它在 answer 裡說明了自己檢索到什麼、為什麼判定不相關 —— 這對除錯非常有用
  3. 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 這種量級重建很快)。

今日小結

  • FAQ 塞 prompt 的問題不是容量,是沒有「查無此項」這個事件。
  • FAQ 要明確寫出「我們不能做什麼」,這比任何 prompt 技巧有效。
  • 條目式 FAQ 用 Character Text Splitter + ### 當 separator,不要用固定字數切。
  • 向量檢索永遠不會說「沒有」,它一定回傳最相似的 K 筆。解法是讓 LLM 明確做相關性判斷(prompt)+ 程式驗證條目編號存在(Code)。
  • Top K 設 3–4,設太大會降低判斷力。
  • _audit.fabricated_sources 是你唯一能看見幻覺的地方。
  • not_found 的 notes 可以變成知識庫成長的回饋迴路。

明天做 W3 回覆專員 —— 那位手上完全沒有工具的同事。我會測一件很有趣的事:給它假的 facts,看它會不會自己加料。


上一篇
Day 11|Worker 1 查單專員:讀訂單資料回答狀態
下一篇
Day 13|Worker 3 回覆專員:依語氣規範草擬回信
系列文
從單一 Agent 到 Supervisor 架構:用 n8n 實作會自己分工的 Multi-Agent AI 部門 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言