前兩天都在 debug。今天終於做一件期待很久的事
讓評測真的執行學生的程式,而不是叫 AI 猜。
回頭看,「評測是推測」這件事污染了整個系統:
all_passed 不能是布林,要做成 True / False / None 三態——這些都是為了不說謊而付出的複雜度。今天把源頭換掉,這些就都有根據了。
動手之前先問一句:Python 在 Windows 上到底能隔離到什麼程度?
Python : 3.14.6
平台 : Windows
resource 模組(記憶體/CPU 上限): 不可用(Unix only)
subprocess timeout: 可用(無窮迴圈在 1.0s 被砍掉)
這個答案很關鍵。resource 是 Unix 才有的,所以設不了記憶體上限。
無窮迴圈砍得掉,但吃光記憶體的程式攔不住。
我還是把模組取名 sandbox.py,但 docstring 第一段就把界線寫死:
擋得住:無窮迴圈(逾時中止)、程式崩潰、寫檔落到專案裡(跑在暫存目錄)。
擋不住:記憶體耗盡、網路連線、讀取使用者其他檔案、fork 炸彈。
為什麼這樣還能用?因為威脅模型取決於部署方式:
所以這個模組現在只給本機用,雲端部署之前不能開。
這其實是同一條原則的延伸:整個專案都在講「沒做到的事不要宣稱做到」。
不能因為它叫 sandbox 就當它安全——那和 UI 謊稱跑過測試是同一種錯。
def run_case(code, stdin, expected, timeout=5.0) -> CaseResult
每組測資跑一次獨立行程,寫進暫存目錄再執行,cwd 也設在那裡——
學生的程式如果寫檔,不會落到 repo 裡。
判定用的是 problems.outputs_match(),跟評測 prompt 裡講給模型聽的是同一個函式。
一條規則,不是兩條會各自漂移的規則。
狀態刻意分四種:
PASS = "pass"
WRONG_OUTPUT = "wrong-output"
TIMEOUT = "timeout"
CRASH = "crash"
timeout 和 crash 不混成「就是錯」——無窮迴圈和 KeyError 是完全不同的毛病,
導師之後要據此給不同的引導。stderr 也留著,因為 traceback 正是摘要的原料。
單元測試綠了不代表真的能用。所以我對題庫裡每一題寫了一份正確解,
整批丟進去:
uva-100 ✓✓✓✓ 最慢 0.15s
uva-118 ✓✓✓ 最慢 0.15s
uva-272 ✓✓✓ 最慢 0.16s
...
共 31 組測資,不符 0 組
然後反過來,把每份解答改壞一行(讓它只讀第一行輸入):
改壞的解(每題都該至少有一組失敗):
uva-100 抓到
uva-118 抓到
...
十題全抓到。最慢 0.51 秒,timeout 設 5 秒,餘裕很大。
這些參考解沒有進版控。它們是練習題的答案,留在 repo 裡整個系統就白做了。
怎麼設計一個能重複執行、又不洩題的驗證流程,是之後要解的題目。
這裡要補一件很久以前的決定。
改成 CPE 題的時候,我特地先把「測資必須是可直接執行的 Python」寫成契約,
當時的理由是:LLM 評測看得懂散文描述,沙箱只能執行程式碼。
如果當初讓出題 agent 產出「空字典時要回傳 Not Found」這種自然語言描述,
今天接執行器就得回頭改出題 prompt、改評測、改兩邊的測試。
而實際上今天我一行都沒動到出題和評測——只加了一個新模組。
執行器做好了,但還沒接上去。明天把它接進評測 agent。
目前還差: