iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Build on Google AI

把履歷修改工程化:從「聽起來有道理」到用 Gemini 打造可驗證的 AI 職涯助理系列 第 27 篇

Day 26 - 把改動變成可以比較的東西:整理這 25 天的 prompt 與 schema 版本

  • 分享至 

  • xImage
  •  

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",當時還寫著「改欄位就升版本,不要原地改意思」
回頭對照這幾天的文章,之後改過三次

  • Day 11:對照結果加了 quote_source,新增澄清問題的 schema
  • Day 12:hours 拿掉 gt=0
  • Day 15:計畫改成不收不認得的欄位(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 被原地改過三次

另外再補兩個測試

  • 只改落差字眼,sha 要變(不然 Day 12 那種改動又會漏掉)
  • 留著做比較的舊版本(計畫 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
  • 3 個 worker 平行跑,Day 24 的重試規則照樣生效
  • 一組失敗不拖垮整批,錯誤記在那一組的 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擅長在高流量情境下做效能調校...'

又是換行,而且是昨天的修正沒蓋到的兩種

  • 第一種:換行的一邊是全形「、」,另一邊是英文「gRPC」。昨天的規則要求兩邊都是全形才吃掉換行
  • 第二種:模型自己留了一個空格「服務。 擅長」。昨天的規則把原文的換行吃掉了,引用反而多一個空格

W1 那 4 條是同一句話「提升至 4,500,\nP99 延遲」,四組都用到同一份八年後端的履歷

規則改成:空白只要有一邊是全形字,就整段拿掉
中文本來就不靠空白分詞,空白只有在兩邊都是英文、數字的時候才有意義
引用跟原文走同一個正規化,所以模型留不留空格都一樣

昨天只拿 4 組試跑,只看到一種換行;24 組才看到另外兩種
這就是要跑整份資料集的理由

改完不用重跑模型,rubric.py 本來就是拿引用原文重新驗證

M1 引用驗證通過:107/116
W1 改寫原句驗證通過:170/170

剩下 9 條,全部是真的違規

種類 條數 例子
自己加「技能:」 4 技能: Go、Python(原文是「【技能】」底下一行)
把不相鄰的句子接成一段 5 Python\n負責公司產品的系統開發與維護

0.85 不用校了

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 之後只看一組,很容易以為是自己改得好

這個雜訊,是接下來所有比較的底線:差異小於它,就什麼都不能說

計畫 v1 vs v2

一樣的上游,只換計畫的版本

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,版本號加內容雜湊
  • 固定流程的 6 個 prompt 都改成 Versioned,計畫保留 v1、v2 兩版
  • app/versions.py:一次分析的版本表,包含 schema 的 sha
  • app/schemas/resume.py:SCHEMA_VERSION 升成 v2
  • app/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),同一批也自己看一輪,比對兩邊在哪裡意見不同,決定哪些可以交給自動評分、哪些一定要人看


上一篇
Day 26 - 把改動變成可以比較的東西:整理這 25 天的 prompt 與 schema 版本
下一篇
Day 28 - 只換姓名,建議就變了?檢查偏誤、穩定性與未知資訊處理
系列文
把履歷修改工程化:從「聽起來有道理」到用 Gemini 打造可驗證的 AI 職涯助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言