企業客服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或FAISS | numpy暴力內積手刻,小庫檢索<1ms |
| 生成LLM | OpenRouter | 付費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照假規定行動的兩種攻擊。
第二是「查不到」的收尾方式要看場景。客服場景查不到就導去真人客服,誠實又止損;節目場景查不到,則放下知識庫,讓角色照人設自由聊,不用硬答。同一套檢索邏輯,出口行為由呼叫端決定要走哪一種。
技術細節:門檻數值、工程邊界與embedding的坑
- 門檻:hard negative「支援Apple Watch嗎」0.680 > 庫內改寫題0.649 > 離題上限0.597;路由門檻由0.55調到0.62。
- injection評測2/2守住(不洩漏system prompt、不照做假規定)。「查不到」的節目場景走
miss_mode="chat"。- 依賴注入劃邊界:
rag/套件不importmain.py,LLM函式由呼叫端傳入(llm: Callable[[messages], (text, model)]),兩條開發線在整合點前零檔案交集,事後驗證duo_talk.py一行未動。- 小庫不用框架:16 chunks用numpy內積<1ms,LlamaIndex與Chroma都是多餘依賴。
index.json記model@dim做載入校驗,防「換embedding模型忘了重建索引」。- Gemini embedding截斷維度後要自己L2 normalize(768維截斷不會自動歸一化);文件端taskType=RETRIEVAL_DOCUMENT加title,查詢端RETRIEVAL_QUERY,兩端要分開。
- 檢索不是延遲元兇:端到端4.6到7秒裡,檢索加embedding只佔約0.4秒,大頭是模型生成。
剩下要做的是接留言路由,以及換上真的nailora資料
目前跑完的是假FAQ的建庫、檢索、雙層護欄問答驗證。還沒做的兩件事:把方針1的留言路由接進來,讓客服角色能從雜談台的留言流裡分辨出哪些是客服問題;以及把假資料換成真正的nailora app說明與客服問答記錄。這兩塊都還沒做,結果尚未驗證。
相關
- 全貌入門:AI-VTuber 是什麼-白話入門
- 首個驗證目標:雙AI雜談台 架構與執行
- 記憶設計,RAG是長期記憶的一種:AI VTuber 的記憶與自主性設計