前一天收尾時我記下一個「已知但今天不碰」的問題。今天回來處理它,
因為它其實不是小瑕疵,是這個系統目前最嚴重的一次誤報。
all_passed: True
那是今天真的跑出來的回覆,不是我編的。
原因在 state 的生命週期:grade 一旦寫進去就留到換題為止,
而 code 每輪都以 UI 為真、重送最新的(這是第 1 期就定好的分工,本身沒錯)。
於是打字提問那一輪的 prompt 長這樣:
【學生目前的程式碼】 ← 改壞的新版
【評測結果】以下是**實際執行**學生的程式…
- 已確認全部通過。
這一題已經通過了。直接告訴學生**已答對**…
兩個區塊各自都是真的,並排在一起就成了一句謊話。
這個專案從第 2 期開始反覆撞到同一件事:
前兩次講的是「這個結論從哪來」,今天這次講的是「這個結論關於什麼」。
一份沒有繫在對象上的判定,時間一久就會漂到另一個對象身上——
而且漂的過程完全沒有徵兆。
grade 裡多記一個欄位:
"graded_code": code, # 這份判定是對哪份程式碼下的
然後導師的 prompt 與左欄各自比對一次。不一致就明講:
注意:這份評測是對上一版程式碼跑的,學生之後又改過了,
它不代表眼前這份程式碼的對錯。
而且 all_passed 那道「直接告訴學生已答對」的指令,
從此只在判定對得上眼前這份程式碼時才下。
兩個判準都是往同一個方向倒:
graded_code → 當成過期。 跟 source 欄位缺失時左欄(評測結果)在版面上排在中欄(編輯器)之前,
所以渲染左欄的時候,user_code 這個變數還不存在。
解法是繞過變數,直接讀 widget 的狀態:
current_code = st.session_state.get(f"code:{problem['title']}", INITIAL_CODE)
Streamlit 的 widget 值存在 session_state 裡,key 是我們自己給的
(這個 key 綁著題目標題,本來是為了換題時重置編輯器)。
rerun 一開始它就已經在那裡了——它是上一次 rerun 結束時學生眼前的值,
正是我們要比對的東西。
排版順序造成的限制,靠「狀態不跟著版面走」繞開。這也算 Streamlit rerun
模型的一個實用推論:想拿某個 widget 在版面後面的值,讀它的 key,不要讀變數。
昨天學到的教訓(prompt 改動要對真模型 A/B)今天就用上。同一個情境跑兩次:
| 導師的回覆 | |
|---|---|
| 改動前 | 「這段程式碼已經順利通過所有測試了,做得很好!你可以直接點擊『換一題』」 |
| 改動後 | 「我目前看不出這段程式碼有什麼明顯的問題,建議你直接點擊『送出程式碼』讓系統評測看看」 |
誤報消失了。
值得誠實記一筆的是:改動後導師也沒看出那份程式碼把 int(a) + int(b)
改成了 a + b(字串相加)。但那是提示階梯的設計使然——level 0 在沒有判定時
只能說「我看不出問題」,不能自己宣稱程式碼是錯的(第 1 期的 NO_FAULT_RULE)。
修掉一個誤報,換來的是一句誠實但沒那麼有用的話。 這是划算的:
說錯話的代價是學生帶著壞掉的程式碼走人,說得保守的代價只是多按一次送出。
source 就當推測,沒有 graded_code 就當過期。測試 317 → 324 支,全綠。