企業客服RAG的原理與nailora客服的實作進度

這篇整理的是「方針3」的技術細節:讓單一AI角色讀懂公司自己的客服資料,回答用戶問題時不瞎編。適合已經懂LLM基本概念、想知道RAG具體怎麼串起來的人讀,不需要先懂向量資料庫。讀完能知道nailora客服雛形怎麼把假FAQ建成庫、怎麼擋掉編造答案,以及目前卡在哪裡還沒做完。

方針3是唯一有清楚B2B變現路徑的做法

方針1是雙AI互動的雜談節目,方針3走的是不同路線:單一角色,回答內容完全依據「公司的客服資料」——app說明書、FAQ、過去的客服問答記錄。延遲不是問題,用戶問答等個幾秒可以接受。這個方針之所以值得做,是因為它不卡YouTube訂閱門檻,直接就是B2B產品:公司要的是一個能回答自家app怎麼用的客服機器人,核心技術就是RAG(Retrieval-Augmented Generation,檢索增強生成)。

LLM不知道你的app怎麼用,硬塞整本說明書進prompt又太貴太慢

LLM的知識停在訓練截止那天,它沒看過nailora的app說明書。直覺的解法是每次提問都把整本說明書塞進prompt,但這樣做context會爆掉,速度變慢,錢也燒得快,而且資料一多,模型反而容易抓錯重點。

RAG的做法是反過來:平常不塞任何資料,只有被問到的時候,才去資料庫裡撈出最相關的幾段,塞進那一次的prompt。模型永遠只看到跟這個問題相關的那幾段文字,不用背整本書。

四個步驟:切塊、向量化、檢索、生成,讓AI只讀被問到的那幾頁

整套流程其實就是一個「會自動翻到正確那一頁的說明書」:AI不用背整本,被問到才翻相關那幾頁照著念。

 公司文件 ──切塊──一段段小文字 ──embedding──向量 ──存──向量資料庫
                                                                  ▲
 用戶提問 ──embedding──問題向量 ──查最近的前K段──────────────────┘
                                       │
                                       ▼
        把「找到的K段 + 問題 + 人設」組成 prompt ──LLM ──答案

拆開來看四步:

  • 切塊:文件切成一段段小文字,每段大約200到500字。
  • 向量化:每一塊丟給embedding模型,轉成一串數字,代表它的語意座標。語意相近的文字,座標距離也近,存進向量資料庫。
  • 檢索:用戶提問時,問題也轉成向量,去庫裡找距離最近的前K段,也就是最相關的那幾段。
  • 生成:把「這K段資料+問題+角色人設」組成prompt餵給LLM,並且明確要求它只根據提供的資料回答,沒寫的就說不知道。

nailora客服要走的六步,已經做完前三步的驗證

對應到nailora,實際製作流程是:蒐集app使用說明、FAQ、客服問答記錄,整理成乾淨文字;切塊加embedding建成向量庫;寫檢索層,把問題轉向量、查庫、取前K段;改prompt模板,system注入人設並要求只依據檢索到的資料回答;設嚴格護欄,查不到就誠實說「這部分我不確定,建議聯繫真人客服」,絕不編造;進階再加來源引用,讓答案可以追溯到是哪份文件寫的。

目前用假FAQ完整跑過建庫、檢索、雙層護欄問答的驗證,評測全數通過。實作報告記在專案的docs/RAG-PLAN.md,衝刺心得記在這次衝刺的實作紀錄。剩下的是接方針1的留言路由,以及把假資料換成真的nailora客服資料。

技術選型從免費本地方案,改成雲端embedding配numpy手刻向量庫

早期規劃是走全免費路線:embedding用本地的bge-small-zh或multilingual-e5,向量資料庫用Chroma或FAISS,生成LLM沿用OpenRouter。實際做下去之後全部換了。

層原本設想實際採用
Embedding本地bge-small-zh / multilingual-e5雲端gemini-embedding-001,共用同一把GEMINI_API_KEY,不吃VRAM,不加新依賴
向量資料庫Chroma或FAISSnumpy暴力內積手刻,小庫檢索<1ms
生成LLMOpenRouter付費Gemini 3.5 Flash,OpenRouter留作實驗備援

本地embedding方案沒有丟掉,留作RAG_EMBED_PROVIDER=local的備案。向量庫如果之後chunks數量破萬,再換成sqlite-vec,現階段的資料量用numpy就夠。這一整套RAG模組跟方針1的編排層、VTS都是獨立的,是外接給單一角色的「知識大腦」,未來方針3的骨架就是單角色加RAG模組加嚴格護欄,底下仍然沿用同一套LLM+TTS+VTS地基。

相似度門檻擋不住「切題但庫外」的問題

原本以為設一個相似度門檻,低於門檻就判定答不出來,問題就解決了。實測發現不是這麼回事。

問題其實分三種:庫內題、離題、以及最麻煩的「切題但庫外」——問的方向完全對,但資料庫裡剛好沒寫。實測拿「支援Apple Watch嗎」當測試題,它的相似度分數是0.680,反而比改寫過的庫內題(0.649)還高,而離題題目的分數上限是0.597。門檻只能擋掉那些明顯離題的,擋不住切題但庫外的問題。

所以護欄不能只靠一個數字,得加第二層:讓模型看完檢索到的資料之後,自己判斷「這裡面沒寫,我就直說沒有」。這組數字逼出雙層護欄的設計。

判斷要不要查資料庫的門檻,得比回答門檻更高

第二個被推翻的直覺,是「判斷該不該去查資料庫」這一關的門檻。原本以為寧可多查一點比較保險,門檻應該設低一點。實際上兩條路的退路完全不一樣:回答那一關查不到資料,還有安全網可以說不知道;但路由那一關一旦判定要查,查到的產品資料就會直接塞進角色嘴裡講出去,沒有退路。

結果雜談留言「你們覺得這新聞怎樣」分數0.564,誤觸了路由,角色就硬扯產品,場面很尷尬。修正做法是把路由門檻從0.55拉高到0.62,取離題分數上限跟庫內分數下限的中點。

graph TD
留言進來 --> 判斷是否過路由門檻{相似度是否超過0.62}
判斷是否過路由門檻 -- 否 --> 走一般聊天
判斷是否過路由門檻 -- 是 --> 查向量庫
查向量庫 --> 模型讀取檢索到的段落
模型讀取檢索到的段落 --> 判斷資料裡有沒有寫{資料裡有沒有寫答案}
判斷資料裡有沒有寫 -- 有 --> 照資料回答
判斷資料裡有沒有寫 -- 沒有 --> 誠實說不知道

護欄要擋住「把留言當指令」的攻擊

除了門檻,還有兩個容易被忽略的地方。第一是防止有人在留言裡塞指令,騙AI照做,便宜的防法是在system prompt裡把「檢索到的參考資料」跟「觀眾留言」都明確宣告成資料,而不是指令,實測擋下了嘗試洩漏system prompt、以及嘗試讓AI照假規定行動的兩種攻擊。

第二是「查不到」的收尾方式要看場景。客服場景查不到就導去真人客服,誠實又止損;節目場景查不到,則放下知識庫,讓角色照人設自由聊,不用硬答。同一套檢索邏輯,出口行為由呼叫端決定要走哪一種。

剩下要做的是接留言路由,以及換上真的nailora資料

目前跑完的是假FAQ的建庫、檢索、雙層護欄問答驗證。還沒做的兩件事:把方針1的留言路由接進來,讓客服角色能從雜談台的留言流裡分辨出哪些是客服問題;以及把假資料換成真正的nailora app說明與客服問答記錄。這兩塊都還沒做,結果尚未驗證。

相關