AI 輔助開發

Whisper 的提示語會從前方無聲地截斷至 224 個權杖

Whisper 的 `prompt` 參數有一個隱藏的 224-token 上限。溢出的部分會在沒有警告的情況下從前面被截斷,這會讓關鍵字偏向的功能變成一個 bug。

本文由 AI 模型從英文原文翻譯而來,用字可能與原文有所出入。 閱讀英文原文

我們為 Whisper 語音轉錄功能推出了一個關鍵字偏向提示,結果卻讓我們的轉錄品質變得更差。不是稍微變差而已:這個提示主動地將模型推向錯誤的品牌名稱。根本原因是一個 API 從未回報的單位不匹配問題。Whisper 的 prompt 參數有一個 224 個 token 的有效上限,任何超過的部分都會被靜默捨棄,而且被捨棄的是前面的部分。這篇文章將逐步說明我們如何透過黑箱 A/B 測試證明了這個行為,以及用來修正此問題的預算算法。

prompt 參數的實際作用

Whisper 在每次轉錄請求中接受一個可選的 prompt。官方指南表示,它可以用於「引導模型的風格或指定不熟悉單詞的拼寫」。對於領域性強的音訊,例如醫療諮詢、法律口述或產品支援電話,這是您在使用託管 API 時的主要工具:在 prompt 中列出您的領域詞彙,模型就能更準確地拼寫這些術語。

在底層,prompt 並非模型用來推理的自由文本。它會被編碼並放置在解碼器的上下文視窗中,就像是前一個音訊片段的轉錄稿一樣。模型會從那裡繼續生成。這種架構解釋了兩個令我們驚訝的特性:

  • prompt 與輸出競爭上下文空間。Whisper 的文本上下文為 448 個 token,而實作最多為 prompt 保留其一半減一的空間:223 到 224 個 token。
  • prompt 過長時,參考實作會保留尾部,而非頭部。在原始原始碼中,這是一行切片:prompt_tokens[-(n_ctx // 2 - 1):]。邊界之前的所有內容在模型看到之前都會消失。

這兩個特性都沒有在託管 API 中顯現出來。我們使用的供應商會驗證一個獨立的限制,即 896 個字元,當您超過此限制時會返回錯誤。沒有任何東西會警告您 token 限制,因為截斷發生在模型執行期間,而不是在請求驗證中。

有幫助的提示如何變成有害的提示

我們的提示是從一個詞彙資料庫組合而成:首先是一個手寫的最重要品牌名稱的種子清單,然後附加資料庫詞彙,直到位元組預算用完為止。預算是 850 位元組,這是根據供應商錯誤訊息中的「896」限制所選擇的,而我們當時假設這個限制單位是位元組。

三個問題疊加在一起:

  1. 896 的限制是字元,而不是位元組。在 UTF-8 編碼中,韓文每個字元佔 3 個位元組,所以一個 850 位元組的提示大約只有 280 個字元。我們當時以為已經接近預算上限,但實際上使用的字元預算還不到三分之一。
  2. 這 280 個字元大約是 380 個 token。在 Whisper 的 BPE 詞彙庫中,中日韓(CJK)腳本的 token 化成本很高,根據我們的測量,每個字元大約需要 1.2 到 1.4 個 token。每個請求都超過了 224 個 token 的上限達 70%。
  3. 溢出的部分是從前面被截斷的,而那正是我們放置最重要詞彙的地方。

這個失敗模式非常惡劣。我們的種子清單開頭是使用者最常說的品牌名稱,而這些詞彙最先被犧牲掉。純粹因為資料庫中資料列順序的偶然性,存活下來的是一群在提示結尾、發音相似但錯誤的詞彙。所以當使用者說出像「Sonalift」這樣的詞時(本文中所有品牌名稱均為代稱),模型因為被存活下來的相似詞「Sonaline」引導,便充滿信心地轉錄出錯誤的品牌。一個完全沒有偏見引導的提示,表現得比我們的更好,因為我們的提示正是在引導模型犯錯。

還有一個值得注意的轉折:組合提示的查詢語句沒有 ORDER BY 子句。1,200 個候選詞彙中,哪 40 個能進入提示,取決於資料庫中實體的資料列順序,而這個順序在資料庫清理(vacuum)和大量寫入後會改變。同樣的程式碼、同樣的音訊,在不同天會產生不同的轉錄結果。如果你的偏見引導表現出不確定性,在責怪模型之前,請先檢查你的詞彙選擇過程是否真的是確定性的。

使用位置性 A/B 測試證明前端截斷

您不需要存取模型內部即可驗證截斷方向。您需要一個超限的提示以及它的兩種編排方式。

我們建立了一個約 460 個字元、大約 400 個詞元的提示,所以輕鬆超過了 224 個詞元的上限,但低於提供商的 896 個字元驗證。然後我們將同一個音訊檔案透過它執行了兩次:

  • 編排 A:將三個關鍵詞放在最前面,填充詞彙放在其後。
  • 編排 B:相同的填充詞彙在先,同樣的三個關鍵詞在最後。

結果明確無誤。編排 A 將三個詞都轉錄錯誤,完全重現了我們的生產環境錯誤。編排 B 將三個詞都轉錄正確。相同的內容、相同的詞元數量、相同的音訊;只有位置改變了。這就是從外部觀察到的前端截斷,無需存取原始碼。

實用規則立即顯現:在 Whisper 提示中,結尾是黃金地段。如果發生截斷,您放在最後的內容會被保留下來。將您必須保留的詞彙放在尾部,並將頭部視為犧牲區。

在不附帶權杖化工具的情況下進行權杖預算

根本的解決方案就是從一開始就絕不超過限制,這意味著在傳送前要計算權杖數量。我們不想只為了這個目的,就在 Go 服務中嵌入一個 BPE 權杖化工具,所以我們建立了一個估算器,它具有針對各種腳本的上限係數,並根據我們生產環境詞彙表中的實際權杖化工具進行測量:

func estimateTokens(s string) int {
    sum := 0.0
    for _, r := range s {
        switch {
        case r > 0xFFFF: // beyond BMP: surrogate pairs, emoji
            sum += 2.0
        case unicode.Is(unicode.Hangul, r) || unicode.Is(unicode.Han, r) ||
            unicode.Is(unicode.Hiragana, r) || unicode.Is(unicode.Katakana, r):
            sum += 1.4
        case unicode.Is(unicode.Thai, r):
            sum += 1.0
        case unicode.Is(unicode.Cyrillic, r) || unicode.Is(unicode.Arabic, r):
            sum += 0.55
        case (unicode.IsLetter(r) && unicode.Is(unicode.Latin, r)) ||
            unicode.IsDigit(r) || r == ' ':
            sum += 0.45
        default: // punctuation and symbols
            sum += 1.0
        }
    }
    return int(math.Ceil(sum))
}

有三個設計決策比確切的數字更重要:

  • 掃描 rune,而非分類整個字串。現實生活中的詞彙會在單一詞語中混合不同文字系統(例如 "V-line"、"3D lift"、帶有數字的品牌名稱)。單一的、針對每種語言的係數會錯誤估計你真正在乎的那些詞語。
  • 係數必須是上限值,而非平均值。一個低估了 15% 的平均估算器會悄悄地重新引入你試圖避免的截斷問題。我們將內部預算設定為 200 個 token,而上限是 224 個,因此估算器可以高估,但絕不能低估。
  • 標點符號不是「其他拉丁字元」。我們的初版草稿對所有非 CJK(中日韓)的字元都使用 0.45 的係數。像 "A/B/C-123" 這類的字串和商標符號,其 token 化後幾乎是一個字元對應一個 token,所以符號們獲得了自己專屬的 1.0 通道。

測試套件鎖定了固定的測試字串,其 token 數量由實際的 Whisper tokenizer 離線測量,然後對每個固定測試字串斷言 estimate >= actual。這個斷言在部署前捕獲到一個真正的錯誤:我們的泰文係數 0.7 來自於對普通字典詞彙的平均計算,但實際上填充偏向提示(biasing prompt)的音譯品牌名稱,其 token 化後約為每個字元 1.0 個 token。上限測試失敗了,我們提高了係數,從而避免了一整類針對泰國使用者的靜默截斷問題被發布出去。將斷言寫成一個嚴格的不等式;一個「上下 15% 以內」的容許誤差會讓那個錯誤溜過去。

在填滿其餘部分前,先保留尾部預算

一旦你能計算權杖,組合順序仍然很重要。兩條規則使最終的提示變得穩健:

首先,在接受任何其他內容之前,為你的關鍵術語保留預算。我們計算尾部區塊的成本,即種子範例加上釘選的必要術語,將其從 200 個權杖的預算中減去,然後才將排序後的詞彙表術語放入剩餘空間。天真的順序,即貪婪地填滿然後最後附加釘選項目,有一種失敗模式,即低價值術語消耗了預算,而釘選項目被你自己的防護機制捨棄。

其次,按重要性升序排列術語,這樣字串的結尾就是最關鍵的術語。在預算內,這不會改變任何事情。但如果管線中的任何組件計算錯誤,前端截斷會先刪除你最不重要的術語。它將最壞情況下的災難性故障轉變為優雅的失敗。

我們系統中最終組合的提示:25 個排序後的詞彙表術語,接著是種子範例,然後是三個釘選在最末端的品牌名稱,總計約 170 個估計權杖。啟動這次調查的兩個生產環境中的辨識錯誤都消失了,這已透過前後比對原始音訊片段進行了驗證。

針對託管式 Whisper API 進行提示詞偏向的檢查清單

  • 有效的提示詞上限為 224 個 token。供應商端的字元驗證(在我們使用的 API 上為 896 個字元)是一個獨立且寬鬆許多的限制。應根據 token 數量來規劃預算。
  • 超出的部分會從開頭被默默截斷。請將關鍵詞彙放在提示詞的結尾,絕不要放在開頭。
  • 透過位置性的 A/B 測試自行驗證截斷行為:使用一個超出上限的提示詞,將關鍵詞彙分別置於前端與後端,並搭配相同的音訊。
  • 如果在沒有 tokenizer 的情況下估算 token 數量,應使用各類字元(script)的係數來掃描每個 rune,並透過與實際 tokenizer 輸出的比對測試,將其設為上限。預期音譯的外國品牌名稱,其 tokenization 的成本會遠高於字典中的文字。
  • 首先為必須包含的詞彙保留預算,剩餘空間按排名填入,並保持選擇的確定性:一路使用穩定的排序鍵,直到唯一的 ID。
  • 記錄每個組合好的提示詞的估計 token 數,並在估計值接近上限時發出警報。API 絕不會告訴你這件事。

一個令人不自在的總結是,一個偏向性提示詞是個有預算限制的資料結構,而非一個字串。請以對待固定大小緩衝區的同等謹慎來處理它,因為模型就是把它變成那樣的東西。