跳至主要內容
所有文章

可信的不是模型,是它周圍那層工序

客服助理答錯的時候,你沒辦法知道它是從哪裡開始錯的。我們把站上的頁面切成三十五段、逐段對問題計分、只送分數最高的幾段進模型,再把模型寫回來的每一個引用對回真正送出去的來源。這篇講那三步各自量了什麼、為什麼三個數字都印在畫面上,以及為什麼沒有接任何 RAG 框架。

閱讀約 6 分鐘分類檢索工程產品

「它會不會亂講?」問到客服助理的客戶,第一句幾乎都是這個。

第二句通常是:「那我怎麼知道它有沒有亂講?」

第二句才是難的。第一句還可以用模型解決——換大一點的模型、把提示詞寫嚴一點,錯誤率會往下掉。第二句不行:可信不是模型的性質,是它周圍那層工序的性質。資料是怎麼挑進去的、答案出來之後有沒有東西去對,這兩件事跟模型多聰明沒有關係。所以我們把那層工序做在自己站上,而且把它擺到畫面上。下面每個數字都是對線上的模型跑出來的,不是估的。

一次回答走過的路

  1. 01切段35 段
    站上的頁面按它本來的結構切開。每一段記得自己來自哪一頁——引用能不能點得開,從這一步就決定了。
  2. 02計分
    中文按相鄰兩字比對、英文按詞,每個詞用「它出現在幾段裡」加權,再對長度做輕微懲罰。取分數最高的幾段。

    一段都對不上

    直接回「不知道」並給出人的信箱,完全不呼叫模型。
  3. 03生成
    只有選中的那幾段進得了提示。模型被要求只用它們回答,並在用到的句子後面標上編號。
  4. 04驗證
    答案裡的每個編號,對回這次真正送出去的那幾段。對得上的變成連結,對不上的留成純文字。
四步裡只有第三步有模型。其餘三步是程式,所以它們的結果可以被數、可以被看、可以被吵。

站上的頁面,被切成三十五段

這個站的文案本來就是結構化的:一個產品一個物件、一則常見問題一個物件、隱私政策每一節一個物件。所以語料不用另外準備,直接從那些物件切,切出三十五段。

每一段記得自己來自哪一頁。這一步決定了後面全部的事情——引用點不點得開,就看切的時候有沒有把路由帶著走。

切法是「按子樹拿」,不是列舉欄位:把那棵子樹底下所有字串攤平就是這一段的內容。差別在改文案的時候。列舉欄位的話,新增一個欄位就會有一句話悄悄不在語料裡,而且沒有任何地方會報錯;按子樹拿,只有動到結構才會掉,而動結構在別的地方會先變成型別錯誤。

有個洞要先講:部落格內文不在語料裡。文章是 MDX,build 的時候讀,不會進到 serverless 的 bundle,要索引它得先產生一份檔案。在那之前,問文章的事它會說不知道。

計分是四十四行算術,不是一個框架

檢索總共四十四行。中文按相鄰兩字切(「統一編號」變成 統一、一編、編號),英文按詞切;每個詞用它出現在幾段裡來加權——出現在兩段裡的詞,比出現在二十段裡的詞更能決定該送哪一段;最後對長度做一個很輕的懲罰,不然隱私權政策每一節都夠長,長到什麼問題都包含得到。

這裡沒有接 LangChain,是想過之後決定不接的:

框架幫你做的這裡的狀況
抽換 embedding 模型沒有 embedding
接向量資料庫沒有索引要存,語料是記憶體裡的物件
編排 agent 迴圈一次檢索、一次生成、一次驗證,就結束了
包好的串流我們要在中間插自己的事件,包好的反而擋路

而且引用驗證那段,框架本來就沒有現成的可以借。真正會讓這個答案改變的那天是語料長到詞面比對不夠用、需要 embedding 的時候——那時候要接的是向量資料庫,不是框架。

引用是驗過的,不是模型標的

模型會標引用。它也會標一個不存在的編號。

所以答案串完之後,程式把裡面每一個引用編號抓出來,跟這次真正送出去的那幾段對一次,分成兩堆:對得上的,和對不上的。這一步沒有模型參與——它就是比對一組編號,所以它不會有意見,也不會漏掉。

對得上的變成可以點的連結,對不上的留成純文字。不刪掉,是因為刪掉等於幫模型把痕跡擦乾淨;不做成連結,是因為一個看起來像引用、點下去卻沒有東西的編號,比沒有引用更糟。

還有一種要停得下來的情況:問題跟站上一個詞都對不上的時候,路由直接回「不知道」,完全不呼叫模型。拿一份空的來源清單去問模型,它會很流暢地寫出一段,那正是這條路要防的事。

三個數字,放在畫面上

每個回答下面有三個數字,各量不同的東西。下面是問「工具支援哪些語言?」那一次真的跑出來的值:

工序那次量的是什麼
檢索2/35站上有多少內容進了提示
生成1/2模型實際靠了其中幾段
驗證1/1它寫的編號有沒有編造的

再往下是「依據」,每一筆列出標題和它相對於最高分的百分比——那次是 100% 和 37%——以及一個收合起來的「有分數,但沒送出去」,把落選的幾段連同分數劃掉列出來。

一個沒人看得到落選名單的排序,跟手挑的清單沒有分別。

這是整個檢索設計唯一的原則

秀這些不是為了好看。「我們用 RAG,答案有出處」跟「三十五段裡送了兩段、用掉一段、標了一個引用、驗過一個」是兩種不同的東西:前者要人信,後者可以被查。你可以自己在 Desk 的產品頁 上問一題,數字會跟著你那一次跑出來的變。

這樣做的代價

詞面比對抓不到換句話說。「我可以把東西帶走嗎」跟頁面上寫的「匯出」之間沒有共同的字。這種問法會落到弱來源,然後靠模型拒答——多花一次呼叫,而且結果取決於模型當下的判斷。這是保留召回率換來的。

語料有洞。 部落格內文不在裡面,包括這一篇。

沒有框架就要自己維護。 換 embedding 的那天,那四十四行要自己改。這是有意識付的價,換到的是三個數字都講得出來怎麼算的。

節流在記憶體裡。 serverless 上每個 instance 一份,認真要打的人繞得過去。搬到 KV 之前,這個 demo 不該放到會被大量訪問的地方。

還有一個比較軟的代價:每個回答下面多了三行數字。對只想要一個答案的人,那是噪音。所以支援頁上它擺在常見問題上面,而常見問題整份留在下面沒有拿掉——能用眼睛掃完的答案,還是比較好的答案。

接下來

這條規矩在這個站上是第二次用了。歌詞那邊是數字數:模型不會數,所以程式數,沒中的退回去重寫。這邊是驗引用:模型會編號碼,所以程式對,編造的留成純文字。同一句話的兩個版本——模型負責寫,程式負責說它算不算數。

下一步是把部落格內文放進語料。那會逼出 embedding,因為一篇兩千字的文章不能整篇塞進提示,而詞面比對在長文上會開始漏。到時候「檢索」那一格的算法會換掉,從詞頻換成向量距離。會換的是那一格;把它攤開來給人看這件事不會換。

接著讀

心裡已經有想法了嗎?

說說你想做什麼,我們會老實告訴你,這件事適不適合交給我們。