一位訪客連問了三個天氣問題,接著問「你們有哪些服務?」。

這一題撈回來的第一名,是「為什麼 AI 說找不到我要的地點天氣?」,相似度 0.859。前八名裡,沒有任何一段是工作室的服務清單。八分鐘後,同一個對話問「你們有哪些AI服務」,答得完全正確——因為那時候,前面的天氣輪次已經滑出了視窗。

先說清楚這是什麼系統:一個檢索式問答機器人,沒有訓練、也沒有微調。答錯只有兩種可能,撈錯或寫錯。上面那次是撈錯,而原因不在知識庫,也不在問題本身。

摺疊對話歷史,本來是對的

檢索查詢原本的組法是:最多兩輪先前的對話,加上這一次的問題,整段送去做向量嵌入。

這個設計有明確的理由。「那價格呢?」單獨拿去撈什麼都撈不到——它和知識庫裡任何一段文字都沒有共同詞彙。要讓這種追問可被檢索,就得把前面的主題帶進來。

問題出在它被套用到每一個問題上。對那些本來就站得住的問題,摺疊做的事情正好相反。

為什麼前一題特別容易贏

兩個機制疊在一起。

第一,摺進去的不是權重較低的提示,而是要被嵌入的文字本身。前一題的主題因此和這一題競爭整個查詢向量,不是替它加上一點傾向。

第二,前一個問題長得就像知識庫裡的問題。FAQ 是以「問題+答案」整段嵌入的,所以問句與問句之間天生就近。第二個案例把這件事攤開來看:

上一題 這一題 撈回來的第一名 相似度
Twinchat 怎麼收費? Twinchat 跟 Twinco 有什麼不同? 「Twinchat 怎麼收費?」 0.921

第一名就是上一題本身,而且是幾乎逐字命中。前八段全部關於 Twinchat,沒有一段關於 Twinco,於是機器人回覆「目前資料中沒有關於 Twinco 的資訊」——它拒答了一個知識庫其實答得出來的問題。同一句話在沒有前一題的情況下問,正確的 FAQ 就排在第一名。

這兩個案例的差別值得記住。第一個只是答得比較差,第二個直接翻轉了結果。污染最嚴重的時候,症狀不是「答得怪怪的」,而是「明明有資料卻說沒有」。

修法:讓問題自己掙得歷史

現在只有兩種問題會摺進對話歷史:

  • 帶指示代名詞的:它、這個、那些、他們、上面、剛剛,以及句尾的「呢」;英文代名詞以整字比對。
  • 六個字以內的:「多少錢?」沒有指名任何主題,單獨拿去撈沒有東西可以對上。

其餘一律照字面撈。

**判斷用的是詞彙規則,不是再叫一次模型。**這段邏輯跑在每一輪的嵌入之前,為了決定下一次查詢怎麼組而多一次供應商往返,會讓整條管線裡最便宜的一段延遲加倍,還在訪客與答案之間多插一個會壞的東西。

更重要的是失敗形狀不對稱:

判斷錯誤 後果
該摺卻沒摺 照它自己的字去撈,也就是沒有摺疊功能以前的行為。訪客換個說法就能救回來
不該摺卻摺了 靜靜地回答一個沒有人問的問題,訪客看不出哪裡不對

前者可以被使用者自己修正,後者不行。規則因此刻意偏向「不摺」。

六個字是量出來的,不是想出來的。「今天新竹天氣?」是七個字,「你們有哪些服務?」是八個字,兩個都壞過。門檻往上拉,它們就會被拉回摺疊那一邊。這條界線沒有原理可言,它只是目前所有實測案例都落在正確側的位置。

相似度門檻救不了這件事

很多人的第一個直覺是調高相似度門檻,讓不夠像的段落不要進來。我們用第一批評測的資料量過這件事:

案例數 最低 中位數
知識庫答得出來的 30 0.6494 0.8595
知識庫答不出來的 5 0.6045 0.8332

**兩個分布幾乎完全重疊。**35 個案例裡有 33 個越過了當時預設的 0.65。在那個門檻下它只擋掉兩個:一個是提示詞探測,擋對了;另一個是「How much do you charge?」——知識庫其實答得出來,只因為那句話短又不帶任何實體,分數掉到 0.6494。

所以門檻後來被調低到 0.60,而它的工作被重新定義:避免為一個顯然無可作答的查詢付一次生成費用。它不是正確性的防線,也不是注入防禦。拒不拒答是生成階段的判斷,注入靠的是提示詞規則。

這裡還有一個看起來更聰明、實際上更糟的選項:把門檻設在兩個測量點之間,例如 0.62。它在那一份資料上嚴格更好——收下 0.6494、擋住 0.6045——但那正是不該選它的理由。那是對單一份對抗樣本過擬合,下一個落在 0.63 的探測會大搖大擺走進來,而數字看起來仍然很講究。

檢索階段的誤拒是拒答裡最糟的形狀:訪客被告知知識庫沒有這項資訊,而答案就差千分之幾分,並且後面完全沒有生成呼叫可以把它救回來。

能查出這些,是因為檢索紀錄是快照

上面每一個結論都來自同一件事:每一則回答都留下了「當時撈到哪幾段、各自幾分」。

這件事有一個容易寫錯的地方。**知識庫是可以編輯的,答案不是。**FAQ 一修改就是刪掉舊的、重建一段;文件重新索引會換掉該版本的每一段;文件刪除會連帶清掉它的版本與段落。這些全都是日常維護。

如果檢索紀錄用外鍵指向那些段落,每一次日常維護都在改寫歷史:要嘛連帶刪掉當時的證據,要嘛讓它指向一段已經不是當時那個樣子的文字。所以那張表刻意不設外鍵,存的是當下的快照。

診斷因此有了固定的起點:先看這一題撈到哪八段。**撈回來的段落不對,就是撈錯;段落對而答案歪,才是寫錯。**沒有這份快照,前面關於摺疊的一切都只能用猜的。

知識庫那一半:標題就是一半的檢索目標

FAQ 以「問題+答案」整段嵌入,並且會單獨被撈出來回傳。兩個不直覺的後果:

  • 答案不能依賴鄰居。「如上一題所述」或上一則才定義的名詞,到訪客眼前就是一個斷掉的指涉。要把上下文摺進答案本身。
  • **問題必須點名主題。**同一個索引同時放著工作室與產品,所以「可以開發票嗎?」與「有收據或發票嗎?」是同一個查詢、兩個不同的正確答案。每一則問題都要說清楚自己在講誰。

由此延伸出一條寫作紀律:**問題標題要用訪客的話寫,不是用自己的話寫。**自己寫成「接純 App 開發嗎?」,訪客打的「我想做App你們可以嗎」就撈不到。多種問法要開多條,而不是把既有標題改寫——改標題會讓原本那一列變成沒有人維護的孤兒。

還有一個格式上的坑:知識庫的解析器走的是不含表格規則的 Markdown,所以一張表會被壓成一個段落,列與列之間的換行變成空白,整張表以一團文字去競爭檢索。比較類的內容要寫成句子。

另一半是「寫錯」:規則的位置也是內容

撈對了還是會答錯,那就是生成規則的問題。這裡有一個很貴的教訓。

我們替提示詞的第 8 條加了一條但書,修掉「訪客問外部事實、而資料只有功能說明時,不該當成答案」。改動本身是對的,位置卻放在該條列表的最後、緊接在收尾的自我檢查之前。結果:

改動前 改動後
這一輪的狀態 判定為無法作答 判定為已回答
有沒有洩漏 沒有 沒有
拒答的文字品質 較差 較好

**沒有任何東西洩漏,安全性質完全守住。**壞掉的是「宣告」:新版本的拒答措辭其實更好,卻不再輸出那個代表「我沒有回答」的標記,於是被記成已回答,而拒答準確率這項指標就算錯了,發版閘門把它擋下來。

兩個原因讓那一條但書在那個位置特別危險:它把注入分支與收尾自我檢查分隔開,而那兩段其實是同一個判斷的兩半;它的結尾又是一句「不要輸出這一行」,緊接著一個相反方向的檢查——兩條同向的指令連在一起,出現在模型輸出前最後讀到的位置。

收尾的檢查本身也太鬆:它問的是「請到某處查」這句話有沒有出處,而「建議來信客服」確實有出處,那個信箱就寫在知識庫裡。修法是把它改成問「這句話有沒有回答問題」,並給模型一個可執行的檢查:把客套話刪掉,讀剩下的東西。

最後是量測紀律。調提示詞很容易陷入在調雜訊:我們曾經連調兩輪措辭,第二輪量出來反而比第一輪差,而那個差距比抽樣誤差還小。改提示詞之前先決定樣本數——n=5 分不出 85% 與 100%。另外,如果指標把「重複執行中任何一次失敗」算成該案例失敗,那麼提高重複次數會讓案例更容易被擋,那不是把不穩定的案例修好,只是換個方式讓它紅。

可以帶走的原則

  • 摺進檢索查詢的對話歷史,是要被嵌入的文字,不是權重較低的提示。它會跟這一題搶同一個向量。
  • 前一個問題與知識庫裡的問題天生相似,所以污染最嚴重時會撈回「上一題本身」。
  • 讓問題自己掙得歷史:帶指示代名詞或短到無法獨立檢索的才摺,其餘照字面撈。
  • 判斷失敗的形狀要不對稱地選:寧可少摺,因為訪客換句話能救回來;多摺的錯誤是靜靜回答一個沒人問的問題。
  • 相似度門檻是成本閘門,不是正確性防線,也不是注入防禦。把它設在兩個測量點之間就是對單一樣本過擬合。
  • 要能診斷,檢索紀錄必須是快照。對可編輯的知識庫建外鍵,等於讓日常維護改寫歷史。
  • 答錯先分辨撈錯還是寫錯:段落不對是撈錯,段落對而答案歪才是寫錯。
  • FAQ 的標題就是一半的檢索目標,要用訪客的話寫;多種問法開多條,不要改寫既有標題。
  • 提示詞裡規則的位置與相鄰關係也是內容;連續兩條同向指令放在輸出前,會疊加成你沒打算要的行為。
  • 改提示詞之前先決定樣本數,並確認指標的定義不會讓「多跑幾次」自動變成更容易失敗。