发布于2026年5月1日

我建了一個 AI 知識機器人,結果發現它一直在對社群成員說謊

作者:Frank Yao

> **重點摘要(TLDR)** > - 我的 RAG 聊天機器人對明明存在的內容回答「我沒有這方面的資料」——索引(index)裡有 4,290 個向量(vector),3,836 個文字片段(chunk)確認已嵌入 > - 根本原因:本地 `.env` 檔案缺少 `HF_TOKEN`,導致嵌入管道(embedding pipeline)在建立索引時悄悄切換到量化 ONNX 模型,而生產環境(production)查詢時卻使用 Hugging Face API——兩套模型走的是不同的向量空間(vector space) > - 一堂中文課程《如何註冊美國公司》在英文查詢下相似度只有 0.3762,而我的閾值(threshold)設定為 0.55,機器人直接把正確答案丟掉 > - 三個修復步驟:補上 token、重新嵌入全部 3,836 個 chunk、把最低分數降至 0.35 並加入 LLM 層的相關性過濾器

rag chatbot 部署 — FrankYao.com
Frank Yao

---

我對社群許下的承諾

大約六個月前,我對自己的線上社群說了一句我當時完全相信的話:「有什麼問題都問機器人,我教過的東西全部都在裡面。」

我是認真的。我花了整整三週建了一個 RAG(Retrieval-Augmented Generation,檢索增強生成)聊天機器人,讓它作為永不下線的知識庫——一個可搜尋的大腦,涵蓋從移民如何在美國成立 LLC(Limited Liability Company,有限責任公司)、供應鏈物流,到 AI 自動化工作流的所有內容。

技術架構在紙面上看起來無懈可擊:把原始內容切成文字片段(chunk),用 `paraphrase-multilingual-MiniLM-L12-v2`(一個來自 Hugging Face 的 384 維多語言嵌入模型)生成嵌入向量(embedding),把所有向量推進 Pinecone 向量資料庫(vector database),接上 Next.js 前端,部署到 Vercel。

數字上看,一切都完整了。上線前我跑了驗證腳本。Pinecone 索引裡確認有 4,290 個向量。3,836 個內容 chunk 已嵌入並儲存。資料在那裡,索引是真實的,數學沒問題。

我許了承諾,把它上線了。接下來好幾週,我完全不知道它正在對所有人說謊。

---

社群成員開始說奇怪的話

投訴一開始只是涓涓細流,是那種對方不想讓你難堪的客氣語氣。

「Frank,我搜尋 LLC 那部分——你有教過外國人如何在美國成立公司,但機器人說它沒有這方面的資料。我是不是找錯地方了?」

我第一反應是使用者問題。也許他們問的方式跟我預期不同。我叫他們換個方式提問。

結果一樣。「我沒有關於這個主題的資料。」

然後第二個成員。然後第三個,問的是物流採購——我在多堂課中詳細講過的主題。機器人同樣沉默,同樣給出空洞的回答,在本應有完整答案的地方一片空白。

到了這一步,我停止替機器人找藉口,打開了終端機。

---

開始排查

第一步是最明顯的:確認索引裡真的有這些內容。

我直接呼叫 Pinecone 的統計 API。數字回傳乾淨——4,290 個向量,與我索引的數量吻合。資料確實物理性地存在於索引之中。這不是資料遺失的問題。

所以我跑了一次直接向量查詢——不透過聊天機器人介面,而是寫了一個測試腳本,取一位成員輸入過的原始問題(關於外國人如何在美國成立 LLC),在我本地機器上把它轉換成嵌入向量,然後直接打到 Pinecone,不設任何閾值過濾,要求返回前 10 名結果。

LLC 課程確實出現了。那堂標題為《如何註冊美國公司》的課程確實在結果裡。但它附帶的相似度分數(similarity score)是 **0.3762**。

我的生產環境 `MIN_SCORE` 閾值設定的是 `0.55`。

答案存在。機器人找到了它。然後因為 0.3762 太低,過不了篩選,機器人把它扔掉了。

這解釋了表面症狀。但它沒有解釋**為什麼**一堂明確相關的課程分數會這麼低。多語言模型本來就應該能處理英文查詢對應中文內容的情況——這正是 `paraphrase-multilingual-MiniLM-L12-v2` 的賣點所在。為什麼它給出的是 0.3762 而不是我預期的 0.60 以上?

我開始深挖嵌入管道(embedding pipeline)本身。

---

三條嵌入路徑,一個破掉的索引

這裡有一件我在建系統時沒有充分認識到的事:`paraphrase-multilingual-MiniLM-L12-v2` 在程式碼裡有三種不同的執行方式,而且它們並不等價。

**路徑一:Hugging Face 推理 API(Inference API)。** 你把文字傳送到 `api-inference.huggingface.co`,帶上 Bearer token,取回一個 384 維向量。這是我的 Vercel 生產環境在查詢時配置使用的方式。

**路徑二:SentenceTransformer 本地執行。** 你在 Python 本地執行 `SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')`,它下載模型權重並在你的機器上跑推理。這在計算上等同於 API——同樣的權重,同樣的數學運算。

**路徑三:fastembed 搭配 ONNX 量化(ONNX-Q)。** 這是快速、低記憶體消耗的選項。`fastembed` 函式庫內建了許多熱門嵌入模型的量化 ONNX(Open Neural Network Exchange,開放神經網路交換格式)版本。它不需要完整的 PyTorch 堆疊,速度快,使用方便。但它產生的是**與路徑一和路徑二不同的向量空間**。

我跑了一個快速測試來驗證我的猜測。取一個範例句子,用 HF API 嵌入,再用 SentenceTransformer 本地嵌入,計算兩個輸出向量的餘弦相似度(cosine similarity)。

結果:**1.0000**。完全相同。路徑一和路徑二是同一個模型、同樣的權重、同樣的向量。不管計算發生在 Hugging Face 的伺服器還是我的筆電上,輸出的 384 維向量是一致的。

然後我用 fastembed 的 ONNX-Q 版本嵌入同一個句子,和 HF API 的輸出做比較。

不同。可測量地不同。不是災難性的差異——向量不是隨機雜訊,它們大體上佔據相近的向量空間區域——但差異足以讓 ONNX-Q 索引的向量和 HF API 查詢向量之間的餘弦相似度分數,持續比預期低。差距不到讓結果從前 10 名完全消失的程度,但已足夠把邊緣匹配推到 0.55 閾值以下。

現在我理解了機制。問題是:我的生產索引是怎麼用 ONNX-Q 建出來的,明明我的程式碼應該使用 HF API?

答案尷尬得出乎意料。我的本地 `.env` 檔案缺少 `HF_TOKEN`。沒有 API 金鑰,就無法呼叫 HF API。我的嵌入腳本有回退邏輯:如果 token 不存在,就用 fastembed 的 ONNX-Q 實作當作「優雅回退(graceful fallback)」。快速、靜默、在這個使用場景中完全錯誤。

我用 ONNX-Q 向量索引了 3,836 個內容 chunk。我的 Vercel 環境把 `HF_TOKEN` 正確設定為環境變數。查詢時,Vercel 生成的是 HF API 嵌入。兩套不同的向量空間,同一個 Pinecone 索引,閾值設在 0.55——我的知識庫每次有人提問,就在悄悄地掩埋一半的答案。

---

rag chatbot 部署 — FrankYao.com
Frank Yao

為什麼跨語言查詢讓問題更嚴重

這個問題還有另一個層次,讓 bug 更難被發現,實際危害更大。

即使嵌入路徑完全一致——沒有漂移(drift)、沒有模型不匹配、一切都正確——英文查詢對應中文內容,本來就會比同語言查詢得到更低的分數。多語言模型在跨語言語義對齊(cross-lingual semantic alignment)上做得確實不錯,但「確實不錯」意味著相比同語言的英對英或中對中查詢,餘弦相似度大約有 0.10 至 0.15 的固有損耗。

這在實踐中意味著什麼:

  • 一個用英文索引的 chunk,面對英文查詢,可能得到 0.72 的分數
  • 同樣的概念內容用中文儲存,在理想的一致嵌入路徑下用英文查詢,可能只有 0.58 至 0.62
  • 再疊加上嵌入漂移(embedding drift),那個中文 chunk 就掉到 0.37

《如何註冊美國公司》這堂課同時被兩個問題擊中:ONNX-Q 索引導致的向量空間不匹配,加上跨語言相似度本就有的固有損耗。兩個效果疊加,最終分數是 0.3762——遠低於 0.55 的閾值,對任何用英文提問的成員來說完全隱形。

這裡有個特別值得台灣和華語圈創業者注意的地方:如果你正在建一個同時服務中英文用戶的 AI 知識庫,或者你的課程、手冊有部分是中文內容,這個跨語言相似度損耗問題幾乎是必然會碰到的。不是「可能遇到」,是「一定會遇到」,差別只在於你是在上線前發現,還是等到用戶投訴才知道。

如果我的知識庫全部是英文內容,漂移帶來的影響會更小,也許我還能被這個 bug 瞞更久。多語言內容讓問題更早浮出水面——這是整件事唯一的一線曙光。

---

修復方案(三個步驟)

我沒有繞過問題打補丁。我從根本修好了它,從頭重建。

**修復一:把 `HF_TOKEN` 加進本地 `.env`——並且把它的缺失設定為硬性錯誤(hard error)。**

不只是把 token 加進去而已。我完全移除了靜默回退邏輯。所有原本在 `HF_TOKEN` 缺失時回退到 fastembed 的程式碼路徑,現在都會拋出一個帶有明確訊息的硬性錯誤:「HF_TOKEN 是必要的。請設定後重新執行索引管道。」如果你沒有 token,腳本在觸碰索引之前就會退出。靜默回退到不同模型路徑,是一種在生產嵌入管道中不應存在的 bug 類別。

**修復二:對全部 3,836 個 chunk 重新執行嵌入管道。**

我刪除了舊的 Pinecone 命名空間(namespace),全程使用 HF API 從零重新索引。重建花了大約 40 分鐘。當我對重建後的索引跑同樣的 LLC 查詢,《如何註冊美國公司》得到了 **0.5741** 的分數。高於原本的閾值。機器人正確地把它找出來了。

其他成員一直問起的物流內容也得到了類似分數,全部超過 0.55。向量空間不匹配消失了,索引現在和查詢路徑在內部是一致的。

**修復三:把 `MIN_SCORE` 降至 0.35,並加入 LLM 層的相關性過濾器。**

即使在修復漂移之後,我意識到 0.55 對一個多語言知識庫來說仍然太嚴苛——跨語言相似度損耗是真實存在且無法消除的。我把最低分數降至 0.35,停止丟棄邊緣匹配。但降低閾值而不做補償,會把更多雜訊傳遞給語言模型。

解決方案是:在向量檢索之後,讓語言模型評估每個候選 chunk,對每個 chunk 做二元相關性判斷——相關或不相關——再組成最終答案。這用更聰明的語義判斷,取代了粗糙的向量閾值過濾,在真正重要的環節發揮作用。較低的檢索底線加上 LLM 層過濾的組合,讓覆蓋率更好,同時不增加幻覺(hallucination)的風險。

---

rag chatbot 部署 — FrankYao.com
Frank Yao

如果你也在跑 RAG,這三個檢查今天就該做

不管你的 RAG 聊天機器人跑在 Vercel、Railway、Fly.io 還是自架環境,以下三項檢查值得現在就去做。

**檢查一:模型路徑一致性測試(Model Path Consistency Test)。**

寫一個五行腳本。取一個句子,用你系統使用的每一條程式碼路徑去嵌入它:本地開發環境、CI 環境、生產環境。計算所有配對之間的餘弦相似度。結果應該都在 1.0000 附近。如果任何一對出現明顯偏差,你就有嵌入漂移問題,你的索引不可靠。這個測試大約花 10 分鐘寫,應該在每次重新索引前作為預飛檢查(pre-flight check)執行。

**檢查二:跨語言閾值校準(Cross-Lingual Threshold Calibration)。**

如果你的知識庫包含多種語言的內容,在生產環境設定閾值之前,要明確地針對跨語言配對測試你的相似度閾值。取一段語言 A 的已知相關內容,用語言 B 查詢,測量實際分數。如果你的閾值在截斷這些結果,就降低閾值,並在下游用 LLM 相關性過濾器補償。不要假設你的多語言模型對跨語言匹配和同語言匹配給出同樣的分數——它不會,而且只用同語言配對校準閾值,對多語言知識庫來說會產生過度嚴苛的結果。

**檢查三:LLM 層的相關性過濾器(LLM-Level Relevance Filter)。**

低檢索閾值帶來更多候選 chunk,也就是更多雜訊。正確的回應不是把閾值調回去——而是在檢索之後加入語義過濾。把候選 chunk 傳給語言模型,用一個簡單的提示詞讓它判斷每個 chunk 是否與查詢相關,在組成最終答案之前過濾掉不相關的。這能捕捉向量相似度無法可靠區分的語義近似錯誤(semantic near-misses),並在檢索網撒得更廣的情況下維持答案品質。

---

常見問題(FAQ)

**RAG 聊天機器人裡的嵌入漂移(embedding drift)是什麼意思?**

嵌入漂移是指在索引建立時和查詢時所產生的向量表示(vector representation)之間的不匹配。當嵌入模型、模型版本,或模型執行路徑在索引和查詢之間有所不同,產生的向量就會佔據向量空間的不同區域。查詢向量和索引向量之間的餘弦相似度分數會比預期更低,導致相關內容跌落檢索閾值以下,從機器人的回答中消失——即使這些內容確實物理性地存在於索引之中。

**我怎麼知道我的 RAG 系統是否有嵌入一致性問題?**

最可靠的測試是:取一個你知道在知識庫裡的句子,用你的生產查詢路徑將它嵌入,然後直接和索引裡那個句子的已索引版本計算餘弦相似度。分數應該非常接近 1.0。如果是 0.85 或更低,你的索引和查詢路徑使用的是不同的向量表示。你也可以間接判斷:如果你的機器人持續說它沒有你明確涵蓋的主題的資料,而直接 Pinecone 查詢顯示那些向量確實存在,但分數比預期低,漂移很可能就是原因。

**為什麼量化 ONNX 模型會產生和全精度模型不同的向量?**

量化(quantization)降低了模型權重的數值精度——通常從 32 位元浮點數壓縮到 8 位元整數——以加快推理速度並減少記憶體使用。輸出向量是透過原始模型算術的壓縮近似計算出來的。對分類或聚類等大多數下游任務來說,品質差距小到可以接受。但在 RAG 系統中,當你要計算一個 ONNX-Q 索引向量和一個全精度查詢向量之間的餘弦相似度時,差距是可測量的,並且可能把相似度分數推到檢索閾值以下,讓相關內容變得不可見。

**多語言模型能處理英文查詢對應中文內容的情況嗎?**

可以,而且實際上效果不錯——但有固定的分數損耗。使用 `paraphrase-multilingual-MiniLM-L12-v2` 進行跨語言檢索,與語義上同等相關的同語言檢索相比,餘弦相似度分數通常低 0.10 至 0.15。只用英對英測試配對校準的閾值,對多語言知識庫來說會過於嚴苛。永遠要明確地針對跨語言配對測試你的閾值,並根據實際測量分數設定,而不是借用單一語言測試的假設。

**防止嵌入管道出現靜默回退行為,最安全的方式是什麼?**

大聲失敗,立即失敗。任何在依賴項缺失時會回退到不同模型、API 或執行環境的程式碼路徑,都應該拋出帶有具體、可行動訊息的硬性錯誤——而不是靜默地用不同實作繼續執行。在資料攝入管道(data ingestion pipeline)中,靜默回退是最危險的失敗模式,因為它產生看起來合理的輸出:向量被生成了,索引被填充了,沒有錯誤被記錄,一切看起來正常運作。等你注意到問題時,你可能已經有一個完整的生產索引建立在錯誤的向量空間上。把嵌入 API token 缺失當作資料庫連線缺失一樣對待:立即停止,明確告訴操作者在管道能夠執行之前需要修復什麼。

**修復嵌入漂移之後,我需要重建整個 Pinecone 索引嗎?**

需要。如果你的索引是用和查詢環境不同的嵌入路徑建立的,索引裡的每一個向量相對於你的查詢都在錯誤的向量空間裡。對「受影響文件」進行選擇性重新索引是不夠的,因為問題是系統性的——漂移期間索引的每一個 chunk 都受到影響。刪除命名空間,在兩端都確認正確的憑證和模型路徑之後,重新完整執行嵌入管道,用模型路徑一致性測試驗證,然後恢復生產環境。在我的案例中,重建 3,836 個 chunk 花了大約 40 分鐘。那是因為太晚發現 bug 而必須付出的一次性代價;前面描述的預防性檢查花 10 分鐘,而且是在索引建立之前就能抓到問題。

准备好付诸行动?

让我们聊聊 AI 自动化和智能数字策略如何为你的业务带来实际成果。