遮罩前 vs 遮罩後(逐 token:原始 → 遮罩後未正規化 → 重新正規化)
| token | 遮罩前 | 遮罩後 | 正規化後 | 合法? |
|---|
你有沒有遇過:叫 LLM「回一段 JSON」,它卻多寫了一句「好的,這是你要的資料:」,或漏了一個引號、括號沒收好, 害你的程式解析失敗?受限解碼就是根治這件事的技術。它的想法很直接:模型每一步吐出下一個字(token)之前, 先問一個「文法規則」——「照目前已經寫出來的內容,接下來哪些 token 合法?」——然後把所有不合法的 token 機率直接歸零(遮罩,mask),只在剩下的合法 token 上重新分配機率再選字。這樣一來,模型物理上不可能寫出 違反文法的輸出,而不是「事後解析失敗再重試」。
「哪些 token 合法」用一個 DFA(有限狀態自動機)來記:節點是「目前寫到哪個狀態」,邊是「這個狀態允許接哪個
token、接了會走到哪個新狀態」。本頁用一個小而具體的文法 a(b|c)d(語言只有兩個合法字串:
abd 和 acd)當骨架,詞表是 [a, b, c, d, z],其中 z 是文法永遠不接受的
干擾 token。我們刻意讓模型的「原始偏好」在每一步都想選非法的 token(q0 最想選 z、q1 最想選 a、q2b 最想
選 z)——正好凸顯:無約束解碼會產生非法輸出,有約束解碼保證合法。
response_format: json_schema(Structured Outputs)、outlines、llama.cpp 的
GBNF grammar、guidance、vLLM 的 guided decoding,底層都是同一招:在每一步用一個自動機遮罩掉會讓
輸出違反 schema/文法的 token。看懂這頁的「遮罩 → 重新正規化 → 沿 DFA 轉移」三步,就看懂了為什麼這些工具
能「保證合法 JSON」,以及它們的代價(token 與文法符號邊界對齊的複雜度)。
DFA 狀態圖:橘色(current)= 當前狀態;綠色邊(合法出邊)= 當前狀態允許的 token 轉移; 粗色邊(relaxing)= 這一步剛走過(選中)的邊;綠色節點(finalized)= 到達的接受狀態。 右側對照表逐 token 呈現「遮罩前(原始)→ 遮罩後(未正規化)→ 重新正規化(受約束)」三欄機率,非法 token 被標灰、機率歸零;合法 token 標綠、被選中者整列反白。每一步還同時秀出「無約束 argmax(模型原本想選什麼)」 對照「受約束 argmax(文法逼它選什麼)」——多數步驟兩者不同,正是受限解碼的價值所在。最後一段(line 8) 示範:即使把 argmax 換成固定種子 LCG 抽樣,因非法 token 機率已被歸零,抽樣同樣不可能選到 z。
| token | 遮罩前 | 遮罩後 | 正規化後 | 合法? |
|---|