Recap: 昨天定了評估規準,拿 4 組試跑,順便抓到一個引用驗證的 bug
試跑存下來的報告長這樣
{
"schema_version": "v1",
"path": "pipeline",
"model": "gemini-2.5-flash",
...
}
有模型名稱,有 schema 版本,但沒有任何一個欄位說它是哪一版 prompt 跑出來的
現在只有一份報告,還沒關係
但接下來每天都要「改一個地方、跑一次、跟上次比」,兩份報告分數不一樣的時候,第一個問題一定是:這兩次到底差在哪?
今天就是要讓這個問題答得出來
固定流程一次分析會呼叫模型的步驟有 6 個
| 步驟 | 哪一天加的 | 有版本號嗎 | 後來改過 |
|---|---|---|---|
| 解析履歷 | Day 07 | 沒有 | — |
| 解析職缺 | Day 07 | 沒有 | — |
| 證據對照 | Day 09 | 沒有 | Day 11 加了「使用者補充也可以當證據」 |
| 澄清問題 | Day 11 | 沒有 | — |
| 改寫 | Day 10 | 沒有 | — |
| 學習計畫 | Day 12 | 沒有 | Day 12 當天就改了一次 |
Day 04 寫過一個有版本號的 analyze_v1,但那是最早的單次分析,固定流程長出來之後就沒在用了
真正在跑的 6 個步驟,一個版本號都沒有
計畫的改動不只在 prompt 本文裡
Day 12 最有效的改動,是把落差清單的「缺:」改成「履歷沒有證據(不確定會不會):」
但那一行字是組合輸入時由程式加上去的,不在 prompt 本文裡
版本如果只看 prompt 本文,Day 12 最重要的那個改動就會被漏掉
schema 說自己是 v1,其實改過三次
Day 07 定了 SCHEMA_VERSION = "v1",當時還寫著「改欄位就升版本,不要原地改意思」
回頭對照這幾天的文章,之後改過三次
quote_source,新增澄清問題的 schemahours 拿掉 gt=0
extra="forbid")三次都沒升版,寫規則的人跟忘記規則的人是同一個
只靠人記得升版,就是會忘,昨天剛證明過
所以每個 prompt 同時記兩個東西:人決定的版本號,和內容算出來的 sha
@dataclass(frozen=True)
class Versioned:
name: str
version: str
text: str
parts: tuple[str, ...] = ()
@property
def sha(self) -> str:
h = hashlib.sha256(self.text.encode())
for p in self.parts:
h.update(b"\0" + p.encode())
return h.hexdigest()[:8]
@property
def tag(self) -> str:
return f"{self.version}@{self.sha}"
只記版本號,改了字忘了升版就分不出來
只記 sha,看不出哪個比較新,也看不出是不是刻意的改動
所以兩個一起記
parts 放的是「跟 prompt 一起送出去、但不寫在 prompt 裡」的字,計畫的落差字眼就放這裡,改了它一樣會改到 sha
_GAP_LINES_V1 = ("- 缺:{requirement}", "- 證據薄弱:{requirement}({reasoning})")
_GAP_LINES_V2 = ("- 履歷沒有證據(不確定會不會):{requirement}", _GAP_LINES_V1[1])
PROMPTS: dict[str, Versioned] = {
"v1": Versioned("plan", "v1", """...""", _GAP_LINES_V1),
"v2": Versioned("plan", "v2", """...""", _GAP_LINES_V2),
}
產生落差清單的程式也搬到計畫旁邊,照版本挑字眼
計畫的 v1 照 Day 12 改之前的樣子還原:落差寫「缺:」,沒有第 6、7 條
留著它,是為了等一下拿來跟 v2 比
測試裡記著每一步現在的版本號和 sha,改了 prompt 的任何一個字,測試就會失敗
不是要擋人改 prompt,是要讓「改了」這件事一定會被看見:要讓它過,只能自己動手更新測試裡記的版本,順手就會想到升版
schema 也一樣算 sha:把送給模型當 response_schema 的 6 個 class 的 JSON Schema 串起來雜湊
順便把 schema 升成 v2,承認 v1 被原地改過三次
另外再補兩個測試
版本記下來還不夠,要跟著結果一起存,之後拿到任何一份報告,都知道它是哪幾版跑出來的
report = AnalysisReport(
path="pipeline", model=r.model, resume=r.data, versions=versions.current(plan_version)
)
昨天的 pilot.py 刪掉,換成 evals/batch.py:index.json 的 24 組全部跑
cd backend
.venv/Scripts/python.exe ../evals/batch.py --label base
error 裡[ 1/24] sample-backend-8y.txt × sample-job-backend-senior.txt ok
...
[24/24] sample-devops-sre-6y.txt × sample-job-backend-senior.txt ok
540.0s 失敗 0 組 evals\reports\batch-20261010-153446-base.json
9 分鐘,399,660 tokens,一半是思考 token(199,285)
F1 吻合度與標準答案一致:17/24
M1 引用驗證通過:105/116
W1 改寫原句驗證通過:166/170
W2 編出來的數字:0
P1 計畫違規:11
W3 候選:59
M1 有 11 條沒過、W1 有 4 條沒過,一條一條看
1.0 'Go、Python、Java、PostgreSQL、MySQL、Redis、Kafka、Docker、Kubernetes、gRPC、Terraform...'
原文:'...Docker、Kubernetes、\ngRPC、Terraform...'
0.987 '...負責訂單與金流核心服務。 擅長在高流量情境下做效能調校...'
原文:'...負責訂單與金流核心服務。\n擅長在高流量情境下做效能調校...'
又是換行,而且是昨天的修正沒蓋到的兩種
W1 那 4 條是同一句話「提升至 4,500,\nP99 延遲」,四組都用到同一份八年後端的履歷
規則改成:空白只要有一邊是全形字,就整段拿掉
中文本來就不靠空白分詞,空白只有在兩邊都是英文、數字的時候才有意義
引用跟原文走同一個正規化,所以模型留不留空格都一樣
昨天只拿 4 組試跑,只看到一種換行;24 組才看到另外兩種
這就是要跑整份資料集的理由
改完不用重跑模型,rubric.py 本來就是拿引用原文重新驗證
M1 引用驗證通過:107/116
W1 改寫原句驗證通過:170/170
剩下 9 條,全部是真的違規
| 種類 | 條數 | 例子 |
|---|---|---|
| 自己加「技能:」 | 4 | 技能: Go、Python(原文是「【技能】」底下一行) |
| 把不相鄰的句子接成一段 | 5 | Python\n負責公司產品的系統開發與維護 |
Day 09 留下的「拿資料校準 0.85」,24 組跑完可以結案了
技能: Go、Python 這一條,修正前相似度 0.923,判定是「改過字」;修正後 0.846,判定變成「找不到」
判定換了,但兩個都是沒通過
回頭看程式
@property
def ok(self) -> bool:
return self.verdict is Verdict.EXACT
通過只認完全相符,0.85 只決定沒通過的時候訊息寫「改過字」還是「找不到」
它從來不影響誰過誰不過,真正決定判定對不對的是正規化
這一批 116 條引用加 170 條改寫原句,被冤枉的 6 條全部是正規化的問題,沒有一條是門檻的問題
現在可以比了,第一個想比的是 Day 12:計畫的 v1 跟 v2
Day 12 的結論是 v2 讓計畫變短、不再排背景條件,但那是一份履歷、跑一次看到的
它在 24 組上還成立嗎?
不能整份重跑
如果直接用 v1 再跑一次整批,解析、對照、改寫也全部重跑
Day 04 就看過同樣的輸入,思考 token 可以差到快一倍;對照的結果只要有一條判定不一樣,計畫的輸入就不一樣
最後計畫的差異,分不出是 prompt 造成的,還是上游變了
所以 batch.py 加了 --reuse:沿用上一份報告的解析、對照、改寫,只重跑計畫
.venv/Scripts/python.exe ../evals/batch.py --reuse ../evals/reports/batch-...-base.json --plan-version v1
也不能只跑一次 v1 就比
先做一件事:同一個 v2、同一份上游,計畫再跑一次
A batch-20261010-153446-base.json 沿用 None
B batch-20261010-153713-plan-v2-again.json 沿用 batch-20261010-153446-base.json
版本完全一樣:兩份的差異只能是模型本身的隨機性
A B
P1 計畫違規 11 7
計畫總項數 197 184
計畫總時數 1204.0 1250.0
總時數只差 4%,看起來很穩
但逐組看
| 組合 | 第一次 | 第二次 |
|---|---|---|
| 八年後端 × 資深後端 | 58 小時 | 25 小時 |
| 轉職者 × 初階後端 | 46 小時 | 16 小時 |
| 一年後端 × 初階後端 | 26 小時 | 46 小時 |
| 一年後端 × SRE | 50 小時 | 80 小時 |
同一個 prompt、同一份輸入,單一組的計畫可以差到快三倍
也就是說,什麼都沒改,某一組的計畫也可能從 58 小時變成 25 小時
如果改了 prompt 之後只看一組,很容易以為是自己改得好
這個雜訊,是接下來所有比較的底線:差異小於它,就什麼都不能說
一樣的上游,只換計畫的版本
A batch-20261010-153713-plan-v2-again.json 沿用 batch-20261010-153446-base.json
B batch-20261010-153818-plan-v1.json 沿用 batch-20261010-153446-base.json
plan: v2 → v1
A B
P1 計畫違規 7 34
計畫總項數 184 324
計畫總時數 1250.0 1878.0
P3 候選(背景條件排成課) 0 35
版本表只差 plan 一個地方,差異才能算在它頭上compare 會檢查這件事,差不只一個地方就印警告
| v2(兩次) | v1 | |
|---|---|---|
| 總時數 | 1204 / 1250 | 1878(+50%) |
| 排滿 8 週 80 小時的組數 | 0 / 4 | 22 / 24 |
| 計畫違規 | 11 / 7 | 34 |
| P3 候選 | 1 / 0 | 35 |
| 計畫這一步的 tokens | — / 84,718 | 120,974 |
這次的差距遠大於雜訊:v2 自己跑兩次,總時數差 4%;換成 v1,多了 50%
最明顯的是「排滿」:v1 有 22 組剛好是 8 週 × 10 小時 = 80 小時
Day 12 加第 7 條「不需要把週數排滿」的理由,就是當時兩份計畫都剛好 8 週;在 24 組上,這個傾向幾乎是每一組
最典型的是八年後端去看初階職缺,6 條必備全部有證據
v1:
5 自學轉職歷程整理與個人品牌建立 10.0 | 為補足「有非資訊本科但成功轉職的自學經歷」這項證據……
...(8 週,每週 10 小時,共 80 小時)
v2:22 小時 / 26 小時(兩次)
一個資工系、資工所畢業,八年經驗的 Tech Lead,被排了一週「自學轉職歷程整理」
這跟 Day 12 第一次看到的「為了非資訊本科排 8 週 Python 入門」是同一個問題,v1 在這裡原封不動地重現了
P3 的 35 條只是候選(用「本科」「年以上」「帶領」這些字眼挑出來的),要人看過才算數
但 0 對 35,方向已經很清楚
Day 12 的結論在 24 組上成立
不同的是,現在這句話後面有兩份報告、一張版本表、一個雜訊底線,誰都可以重跑一次
app/prompts.py:Versioned,版本號加內容雜湊Versioned,計畫保留 v1、v2 兩版app/versions.py:一次分析的版本表,包含 schema 的 shaapp/schemas/resume.py:SCHEMA_VERSION 升成 v2app/services/pipeline.py:報告帶版本表,落差清單的組法搬到計畫那邊app/services/evidence.py:空白只要有一邊是全形就拿掉evals/batch.py:整份資料集的批次執行,--reuse 只重跑計畫evals/rubric.py:compare 兩份報告並排,W1 也改成重新驗證evals/pilot.py:刪掉tests/test_versions.py、tests/test_evidence.py
測試從 114 個變成 121 個
今天一共跑了 3 批:整份一次(399,660 tokens),只跑計畫兩次(84,718 + 120,974 tokens)
版本:google-genai 2.22.0、pydantic 2.13.5、gemini-2.5-flash
24 組跑完,程式能打分的都打了
剩下的是程式打不了分的:59 條改寫被挑出來當加料候選,F1 有 7 組跟標準答案對不上,其中好幾組是「年資過高」這種 fit_scale 沒寫到的情況
59 條一條一條人工看,太慢
明天試試讓模型當評審(LLM-as-judge),同一批也自己看一輪,比對兩邊在哪裡意見不同,決定哪些可以交給自動評分、哪些一定要人看