當自由文字變成 bug
和人聊天時,你要的是散文。但當 LLM 嵌在程式裡——餵資料庫、呼叫 API、填表單——自由格式的文字就成了負擔。一句多餘的開場白(「當然!這是你要的 JSON:」)就能讓你的解析器崩潰。解方是受限解碼(constrained decoding):與其指望模型乖乖聽話,不如改造那個選擇步驟,讓只有符合你格式的輸出才可能被產生。
再談停止序列
最簡單的約束是在哪裡停。停止序列(stop sequences)告訴解碼器:一旦產生某個字串就立刻停下。在用 `"###"` 分隔範例的少樣本(few-shot)設定裡,把它設成停止序列,就能阻止模型興高采烈地再捏造下一個假範例。停止序列修剪答案的尾巴,但不管前面的內容——那是下一步的工作。
logit bias 作為粗略約束
對於極小、固定的輸出空間,光用 logit bias 就夠。如果你永遠只想拿回五個類別標籤之一,就把其餘每個 token 的 logit 壓到很負來封殺它們;模型於是物理上無法說出別的東西。它很粗糙、無法擴展到豐富結構,但對一個 token 的分類器來說堅不可摧。
logit 偏置在 softmax 之前給每個詞元的 logit 加上一個固定值;把它推向負無窮即可徹底禁用該詞元。
文法:從構造上保證合法
真正的主力是以文法為基礎的受限解碼(constrained decoding)。你提供一份形式文法——一個上下文無關文法(context-free grammar)或一條正規表示式——描述所有合法輸出。在每一步,一個遮罩(mask)把任何會違反文法的 token 歸零,於是模型只在能維持輸出合法的 token 之間取樣。這樣做出的結構化輸出(structured output)在構造上就正確:結果保證能被解析,因為文法裡沒有任何路徑能產生不合法的東西。
基於文法的解碼會遮蔽文法不允許的每個詞元——把它的 logit 設為負無窮——因此每一步只能生成合法結構。
JSON 模式與函式呼叫
多數供應商把這包成 JSON 模式(JSON mode)。JSON 模式解碼套用內建的 JSON 文法(常再加上你的結構結構),所以回覆永遠是可解析的 JSON,沒有開場白。同一套機制也撐起函式呼叫(function calling):模型發出一個結構化呼叫——一個工具名稱,加上符合該工具簽名的 JSON 參數物件——你的程式可以直接執行。沒有受限解碼,這兩個功能都會淪為拜託你格式對一點的不穩賭局;有了它,它們才變得可靠。
# At each decode step, mask tokens that would break the JSON schema logits = model.next_logits(context) allowed = grammar.allowed_tokens(state) # valid next tokens only logits[~allowed] = -inf # mask everything else next_token = sample(softmax(logits / temperature)) state = grammar.advance(state, next_token)
約束下的信心與串流
即使結構合法,你往往還想知道模型有多確定。被選 token 的 logprobs 提供可用的信心訊號——某個欄位值機率偏低,就是該再次確認或交給人工的警示。而且因為串流在約束下依然有效,你可以在結構化物件成形時逐欄位渲染,等收尾大括號一到就立刻驗證。