一位訪客連問了三個天氣問題,接著問「你們有哪些服務?」。
這一題撈回來的第一名,是「為什麼 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 的標題就是一半的檢索目標,要用訪客的話寫;多種問法開多條,不要改寫既有標題。
- 提示詞裡規則的位置與相鄰關係也是內容;連續兩條同向指令放在輸出前,會疊加成你沒打算要的行為。
- 改提示詞之前先決定樣本數,並確認指標的定義不會讓「多跑幾次」自動變成更容易失敗。