Day 08 中出現的 verifier,本質上還只是改考卷的人,模型寫完答案,它負責把最後結果擷取出來,並判定和參考答案是否相同。而這套流程若改在可驗證獎勵強化學習 (Reinforcement Learning with Verifiable Rewards, RLVR) 機制下就變得不同了,模型會依照 verifier 給的 reward 更新權重,於是評分規則不再只是旁觀者或閱卷者,而成了模型努力達成的目標(關於 RLVR 的完整內容,我規劃在 Day 21-25 會有連續的實作)。
Sebastian Raschka 的原書第三章同樣建立數學 verifier,第六章再把可驗證答案變成 reinforcement learning 的獎勵,官方 Chapter 6 程式也沿用前面完成的答案判定。因此,我們今天的 verifier-first 的內容會延續其脈絡,確保答案是可驗證的,但 preflight、資源上限和相容性測試是我們為實際評估與後續訓練補上的工作。
所謂「先判對錯,再分析推理軌跡」意味著在既定規則下,檢驗最後答案是否與參考答案等價,若答案錯了,寫得再流暢的推導也不能被算為成功;答案對了,固然很棒,但依然需要檢視中間每一步是否正確。先用 final-answer verifier 把結果分開,之後再看哪些正解繞了遠路、哪些錯解在中途失去方向,至少不會讓一段很像推理的文字代替正確性。
Day 08 允許的數學表示式雖然已經限制字元和名稱,仍不代表每個能被 parser 接受的輸入都適合直接計算。__import__(os) 因為不太像數學表達式,模型很容易可識別並攔下;以下這些則長得完全像數學,風險反而比較容易被忽略。
9^999999
9^9^9
x^x
1e309
[1]*1000000
如果等 SymPy 建立或化簡巨大物件以後才說它太大,檢查已經來不及了。SymPy 的 parsing 文件也提醒,不應把未清理的字串直接送進 parse_expr()。因此這次先加一層 preflight:正規化之後只解析語法樹,不做數學求值;長度、括號深度、數字大小、運算數量和 AST 節點都在進入原本 verifier 以前檢查。
from dataclasses import dataclass
@dataclass(frozen=True)
class HardeningLimits:
max_expression_length: int = 256
max_parenthesis_depth: int = 16
max_numeric_literal_digits: int = 32
max_scientific_exponent: int = 308
max_operations: int = 64
max_ast_nodes: int = 256
max_power_exponent: float = 64
程式先用 Python 的 ast 把正規化後的式子解析成樹,再逐一檢查節點;基本四則、正負號、有限次方、sqrt、pi、oo、單字母變數和短 tuple 或 list 可以繼續,其他函式呼叫、attribute、subscript、floor division 或非固定指數則直接回傳拒絕原因。
def verify_with_preflight(prediction, reference, limits):
pred_check = preflight_expression(prediction, limits=limits)
ref_check = preflight_expression(reference, limits=limits)
if not pred_check.accepted:
return {
"correct": False,
"supported": False,
"reason": pred_check.reason,
"verifier_called": False,
}
if not ref_check.accepted:
return {
"correct": False,
"supported": False,
"reason": f"reference_{ref_check.reason}",
"verifier_called": False,
}
return verify_answer(prediction, reference)
9^999999 會在指數超過 64 時停下,理由是 power_exponent_too_large。9^9^9 更值得注意:次方是右結合,正規化後的 9**9**9 代表 9**(9**9),外層指數本身又是一棵運算樹,不能只看見三個不大的 9 就放行;它和 x^x 都會被歸為 dynamic_power_exponent。這些案例不需要真的交給舊 verifier 計算,因為測試危險輸入不該以真的承受危險為代價。
另外一些拒絕處理的是語意而非純粹大小。[1]*1000000 是容器運算,4%3 裡的 % 可能被理解成 modulo,也可能來自百分比記法;__import__(os) 包含不允許的識別字。Preflight 不猜模型原本想表達什麼,只留下 container_operation、ambiguous_percent_notation 或 dunder_identifier 這類可查的理由。
這次一共準備二十四個 probes。八個是應該繼續支援的有界表示式,包含分數、根號、符號等價、tuple 與剛好位於上限的 2^64;四個處理語法或注入,九個測試資源邊界,另外三個專門比較新舊規則的相容性。結果如下:
all probes 24 / 24
accepted by candidate 8
rejected before verifier 16
safe comparisons with frozen v1 11
candidate / v1 disagreements 6
dangerous v1 calls skipped 5
二十四個案例全部符合預期,六個 disagreement 正好說明更嚴格的驗證規則會改變評分。例如舊規則能把 2x 解讀成 2*x,也能計算 4//2;candidate 分別因隱含乘法與 floor division 拒絕。十七層括號和六十五個加法在舊規則裡也可能完成,新規則之下有可能因此停下。這些拒絕降低了部分風險,但同時也製造了 false rejection。
我們在今天除了知道 final answer 是否正確,還可分析推理軌跡,觀察正確答案如何計算出來的、錯誤答案又在哪一步偏離。此外,可以注意到,今天測試的版本沒有使用正式評分,也沒有回頭重算 Day 07 的 17.8%;畢竟若更換成新規則,同一批模型輸出就可能得到不同分數,後面看到的「進步」將混入 verifier 版本差異。
還有一點要說明的是,Preflight 並不是完整 sandbox,更不是形式驗證器,它沒有窮舉所有 parser exploit,也沒有證明通過的表示式一定便宜或數學上合理;真正去做大量 rollout 時,還要有程序隔離、timeout、輸出長度限制和逐項 reward 紀錄等,不過,本次鐵人賽後半段只會在 micro-GRPO 實驗中保留必要的 rollout 長度與 reward 分量紀錄;程序隔離、timeout 與完整 sandbox 等系統工程,則不會在這個系列裡深入展開。。