iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
AI Engineering

30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察系列 第 26 篇

Day 26|評測情境集、確定性檢查與 Prompt 版本回歸

  • 分享至 

  • xImage
  •  

今天為什麼研究這個?

Day 20 到 Day 25 每一天都有「這次輸出有沒有問題」的檢查,但都是當天為當天的實驗臨時做的:Day 21 由 agent 逐句判讀,Day 22 的驗證器 v1 出過規則錯誤,Day 24 的 citation checker 把 168 條 claim 分成通過、失敗與需人工。這些檢查各自有用,卻沒有放在同一個固定的測例集上,所以很難回答「改了 prompt、context 或檢查規則以後,以前出過的錯有沒有回來」。Day 24、25 的文章結尾都把具體反例留給今天的回歸:trailing[1] 與 7-day 的數字抽取、漏引欄位、免責句裡的方向詞、MAX_TOKENS 截斷。

今天要把 Day 14 的合成情境整理成固定測例,每個測例附上期望行為,再用確定性檢查對模型輸出打分,並比較 prompt 或輸出格式改版前後的結果。要先確認的是:哪些期望能寫成程式可判斷的規則、哪些仍要人工判讀;以及模型輸出每次不同,單次 pass/fail 能不能當回歸的依據。

Concept

1. 測例是「資料 + 期望」,期望要來自情境設定

一個測例由兩部分組成:模型實際會收到的輸入(Day 24 起是 summary-v2),以及對輸出的期望。類別沿用 evals/scenarios/README.md:normal、insufficient、conflicting、missing、deviation,sensitive 留給 Day 29。其他四類都能從 Day 14 的七份情境取適當的解讀日(insufficient 就是同一情境較早的一天);conflicting 則是掃過七份情境所有解讀日,沒有任何一天的 cross_metric.category 是 contradictory,所以另存新情境 conflicting_rhr_down,舊檔不改。它的效果量(rmssd −28 ms、resting_hr −10 bpm)是調到偵測門檻之上的設定,不是真實量級:先用 −14、−6 試過,沒有任何一天標旗。

Day 14 的 expected 只有 n_true_events 與 should_abstain,沒有「這個測例必須引用哪些欄位」。must_cite 之類的期望要另外寫。這裡有個容易出錯的地方:若是看完模型輸出再回頭決定期望,期望就會被輸出塑造,測例集和被測系統互相遷就;Day 3 的 padding bug 也是 fixture 的慣例反過來塑造了程式。所以期望必須依情境設定與 summary 的欄位內容決定,在看到任何模型輸出之前定稿。

2. 哪些期望能寫成確定性檢查

TOPIC_PLAN 列了四類。逐一對照現有程式:

  • schema 有效: grounding.validate_output 已依序檢查 parse、schema、evidence(path 存在)、cross_field,直接沿用。
  • 引用的 evidence id 存在: Day 24 決定不另造識別,evidence id 就是 document + path,所以這一項就是上面的 evidence 階段,不用新寫。它只證明引用找得到,不證明引用的內容支持那句話。
  • 低品質時 abstain: insight-schema-v2 沒有拒答欄位,Day 24 的 mostly_missing 輸出仍是一般的 observation、interpretation、uncertainty 與 next_step。拒答當成「指標」層級的範圍,不當整份輸出的開關:不可判定的指標(abstain_scope)不得被寫出偏離方向,也不得寫成「沒有偏離」,uncertainty 要引用它;可判定的指標(must_answer)仍須回答,防止整份都拒答而被判通過。只有 uncertainty 非空不算通過。schema 不改,範圍與原因記在測例檔。可自動檢查的只有結構(有沒有引用、方向詞出現在哪),「全文沒有補出不受支持的結論」大多只能判 needs_human。
  • 禁用語句: 字串比對有已知的誤判。「確定」會命中「無法確定」,「診斷」會命中「這不是診斷」;Day 24 複核 row 23 時,免責句裡的「偏短」也造成過支持程度的判斷分歧(days/day24/annotation_adjudication.md),可見否定與免責句不是單純的關鍵字問題。單純的子字串清單不夠,至少要記錄誤判案例,必要時改成需人工的三態,而不是直接當 pass/fail 的門檻。

max_claims 也有同樣的問題:claim 是模型自己切的,數量取決於切分習慣,上限只能擋住極端情況。

3. 回歸比較的是什麼,單次結果代表什麼

回歸有兩種,成本不同。第一種是檢查規則改版(validator、checker 或詞表升版本):把 Day 24、25 已存的模型回應重放一遍,比較前後裁決,不需要新的 API 呼叫,Day 24 的比較腳本就是這樣做的。第二種是 prompt 或輸出格式改版:要新的呼叫。目前 repo 只有一份洞察 prompt,改版過的是輸出格式(output-v1 到 output-v2)與 schema,不是 prompt 本文,所以另起 prompt-v2:只改規則 9(無法判定的範圍不下相對 baseline 的結論,可判定的照常回答),其餘逐字同 v1,測試核對。固定 summary、問題句、output-v2、schema、模型設定與檢查規則,只比較 prompt。v1 與 v2 都重新呼叫並穿插執行,Day 24、25 的舊輸出只用來重放檢查規則,不當本輪 v1 的對照組,因為跨日重跑會混入模型狀態的差異。

模型輸出每次不同。呼叫條件是 temperature 預設 1.0、每次獨立請求,所以同一個測例可能這次通過、下次失敗:Day 24 的 mostly_missing 三次分別是通過、失敗於 cross_field、失敗於 parse(MAX_TOKENS 截斷)。單次 pass/fail 因此不能直接當回歸的紅綠燈;要記錄每個測例的重複次數與通過比例,並且把截斷、API 失敗與內容不合格分開記,不然「模型變差」和「這次被截斷」會混在一起。每格重複 3 次,是事前訂的,不是看結果加減。

Hands-on

Dataset:9 個測例,7 份情境

每個測例是 evals/scenarios/S-NNN.yaml 一個檔:情境、人(都是 p01)、解讀日,加上期望。YAML 只存情境參照與期望;輸入由情境重新產生 summary-v2,和 Day 24 同一條路徑(days/day26/eval_demo.py 的 build_summary),送出的內容另存在 days/day26/inputs/ 與每份呼叫紀錄。

測例 類別 情境 解讀日 summary 的狀態 abstain_scope must_answer
S-001 normal sleep_debt 03-05 三項可判定、都沒標旗 — 三項
S-002 insufficient sleep_debt 01-12 三項 baseline 都是 insufficient 三項 —
S-003 missing mostly_missing 02-02 三項都不可判定 三項 —
S-004 missing suspected_illness 02-08 rmssd 當天缺值 rmssd minutesAsleep、resting_hr
S-005 deviation drift_vs_event 02-14 resting_hr 標旗 up — 三項
S-006 deviation sync_delay 02-28 minutesAsleep 標旗 down(情境沒有真實事件,是誤報) — 三項
S-007 deviation two_people_same_value 02-19 rmssd 標旗 down — 三項
S-008 deviation sleep_debt 02-04 事件首日,但三項都沒標旗(rmssd score −2.54,門檻 3.0) — 三項
S-009 conflicting conflicting_rhr_down 02-09 rmssd、resting_hr 都標旗 down,cross_metric 為 contradictory — 三項

「三項」是 minutesAsleep、rmssd、resting_hr,日期都是 2026 年(days/day26/results.md §2)。must_cite 只有 5 個測例有設,S-009 最多(兩個指標的 deviation 加上 cross_metric.category)。max_claims 都是 30。S-008 歸在 deviation,但它的期望是「照 summary 回答、不把事件說成已偵測到」,因為注入的事件沒有達到標旗門檻。

期望和 summary 的對照由程式做:eval_demo.py --section 1 檢查 abstain_scope 的指標在 summary 裡 decidable 都是 false、must_answer 的都是 true、must_cite 的路徑都存在,tests/test_day26.py 也核對同一件事。這只證明期望和輸入一致,不證明期望本身寫對。

Method:兩個 prompt 版本,54 次呼叫

兩個條件只差 prompt 的規則 9:prompt-v1 與 prompt-v2(src/wearable_ai/ai/prompts.py)。其餘相同:輸出格式 output-v2、insight-schema-v2、Day 20 起的同一句要求、summary-v2;gemini-3.8-flash、temperature 1.0、max_output_tokens 8192、response_json_schema,沒有修正迴圈。tests/test_day26.py 的 test_demo_inputs_differ_only_by_prompt 核對兩版輸入只差規則 9。v2 的規則 9 較長,實際送出的 prompt_token_count 是 v1 5,724–6,033、v2 5,888–6,197。

9 測例 × 2 版 × 3 輪 = 54 次,依輪次執行,同一輪內同一測例的 v1、v2 相鄰。54 次每次都是第一次嘗試就有回應,沒有遇到 429(days/day26/results.md §1)。

評分用 evalcheck-v2。呼叫時的版本是 v1,跑完後因為一個誤判把「缺乏」加進否定詞表才升到 v2(,所以紀錄裡的 meta.evalcheck 寫的是 v1。檢查分三層:

  1. run_status:api_error、truncated(MAX_TOKENS)、ok。只有 ok 的紀錄跑內容檢查。
  2. 結構:schema、evidence_exists、cross_field,沿用 Day 22 的 validate_output。
  3. 測例期望:must_cite、abstain_scope、must_answer、forbidden_phrases、max_claims。

每份紀錄的裁決取最嚴的一項:有任何 fail 就是 fail,否則有任何 needs_human 就是 needs_human,都沒有才是 pass。cross_field 失敗不會擋下後面的檢查,只要 parse 與 schema 通過,五項期望檢查照跑。Day 24 的逐句 checker(check_response)的三態計數也記在 results.csv,但不計入裁決。

確定性檢查判不了語意,所以另外做人工抽查,口徑在產生閱讀表之前寫下:有 abstain_scope 的 3 個測例(S-002~S-004)的可評分紀錄全抽,共 17 份;有 must_answer 的 7 個測例(含 S-004)各版本用固定 seed 抽 1 份,共 14 份。兩群在 S-004 重疊 2 份,合併後 29 張卡,打亂順序、不寫版本,共 312 列。

Code

測例檔(evals/scenarios/S-009.yaml,註解省略):

id: S-009
category: conflicting
description: rmssd 與 resting_hr 都標旗且向下,cross_metric 為 contradictory
case:
  scenario: conflicting_rhr_down
  person: p01
  date: "2026-02-09"
  scenario_file: evals/scenarios/synthetic/conflicting_rhr_down.yaml
expect:
  abstain_scope: []
  must_answer: [minutesAsleep, rmssd, resting_hr]
  must_cite:
    - {metric: rmssd, sub: deviation}
    - {metric: resting_hr, sub: deviation}
    - {metric: cross_metric, sub: category}
  max_claims: 30
tags: [phase3, regression]

檢查在 src/wearable_ai/ai/evalcheck.py,對一份呼叫紀錄跑一個測例:

from wearable_ai.ai import evalcheck as ec

specs = {s.id: s for s in ec.load_specs("evals/scenarios")}
report = ec.check_case(specs["S-009"], rec)   # rec:runs/ 裡的一份呼叫紀錄
report.run_status                             # ok/truncated/api_error
report.verdict                                # pass/needs_human/fail;非 ok 時為 None
[(c.name, c.status) for c in report.checks]   # 逐項,含 skipped

check_case 比對的是紀錄裡實際送出的 summary(從 request.contents 解析),不是重新產生的那份。詞表 DEFAULT_FORBIDDEN、NEGATORS、DIRECTION_WORDS 寫在模組頂端,改內容就升 VERSION。重現:

uv run python days/day26/eval_demo.py --section 1   # 組輸入、核對期望與 summary,不呼叫 API
uv run python days/day26/eval_demo.py --section 2 --first-run 1 --runs 1   # 一輪 18 次呼叫
uv run python days/day26/eval_demo.py --section 3   # 確定性檢查,寫 results.csv
uv run python days/day26/eval_demo.py --section 4   # 重放 Day 24 的 18 份紀錄,不呼叫 API
uv run python days/day26/results_tables.py          # 重產 results.md

結果一:v2 的 pass 較多,但同一格三次也不一致

prompt 次數 pass needs_human fail truncated
prompt-v1 27 8 13 5 1
prompt-v2 27 16 8 3 0

逐格(days/day26/results.md §3,days/day26/results.csv):

測例 prompt-v1:第 1/2/3 輪 prompt-v2:第 1/2/3 輪
S-001 needs_human / needs_human / needs_human needs_human / pass / pass
S-002 pass / truncated / fail pass / pass / pass
S-003 fail / pass / needs_human fail / pass / fail
S-004 pass / pass / pass pass / pass / pass
S-005 needs_human / needs_human / needs_human pass / needs_human / pass
S-006 fail / fail / needs_human pass / needs_human / needs_human
S-007 needs_human / pass / needs_human needs_human / pass / needs_human
S-008 needs_human / fail / needs_human pass / needs_human / needs_human
S-009 needs_human / pass / pass pass / pass / fail

表裡所有 fail 都是 cross_field。v1 的截斷是 S-002 第 2 輪:thinking 7,863 token,加上輸出 315,碰到上限 8192,和 Day 25 的截斷同一種樣子。

pass 增加的 8 份裡,6 份出在沒有 abstain_scope 的測例(v1 3 份、v2 9 份);規則 9 直接處理的 S-002~S-004,pass 從 5 份變 7 份。

結果二:裁決的差異來自兩項檢查

把 53 份可評分紀錄的裁決拆回逐項(evalcheck.check_case 重跑,days/day26/results.md §3、§5、§6):

檢查 pass needs_human fail skipped
schema、evidence_exists 53 0 0 0
cross_field 45 0 8 0
must_cite 53 0 0 0
abstain_scope 17 0 0 36
must_answer 42 0 0 11
forbidden_phrases 29 24 0 0
max_claims 53 0 0 0

為測例寫的五項期望,一次 fail 都沒有。所有 fail 來自沿用 Day 22 的 cross_field,所有 needs_human 來自 forbidden_phrases。

cross_field 的 8 次,都是模型把 evidence 的 kind 標成 data_note。 summary 裡那些欄位的 kind 是 program_judgement 或 observation:

被引用的欄位 summary 的 kind 紀錄
baseline.min_obs_for_center、min_cumulative_obs_for_status program_judgement S-002 v1 r3、S-003 v1 r1、S-003 v2 r1、S-003 v2 r3
daily[n].missing_kind observation S-006 v1 r1、S-006 v1 r2、S-008 v1 r2
cross_metric.category program_judgement S-009 v2 r3

前四筆在資料不足的測例裡,引用的是 baseline 需要幾筆觀測的門檻。兩版的規則 9 都要求說明「還缺什麼(依資料裡寫的規則)」,模型照做了,引用的欄位也存在,錯在把門檻標成資料備註。

forbidden_phrases 的 37 個命中,前面同一子句都有否定詞。 「診斷」22 個、「疲勞」15 個,例如「並非臨床醫療診斷標準」「資料不足以支持任何健康診斷、疲勞判定」。它們都是免責句,沒有一句是在下診斷,但依設計命中加上否定詞一律送 needs_human。有命中的紀錄 v1 是 15/26 份,v2 是 9/27 份。

所以兩版裁決的差距,是 v1 的免責句較常提到「診斷」或「疲勞」,加上 v1 多 2 次 kind 標錯、1 次截斷。這兩項都不是規則 9 要處理的事。

abstain_scope 的 17 次 pass 只代表結構:uncertainty 有引用範圍內的每個指標,非 uncertainty 的 claim 沒有出現「偏高」「沒有偏離」這類詞。uncertainty 裡在否定句內的方向詞有 21 處,依規則只計數、不送人工。claim 數 7–21 筆,中位數 10,離 max_claims 30 很遠。

結果三:檢查規則改版,用舊紀錄重放

evalcheck 從 v1 升到 v2 只多一個否定詞「缺乏」。觸發的是 S-009 v1 第 1 輪的一句「缺乏醫療臨床診斷與生活脈絡資訊」:意思是資料裡沒有診斷資訊,v1 卻因為「缺乏」不在否定詞表而判成 fail。這也是 54 份裡唯一不是 cross_field 的 fail。

同一批 54 份紀錄以兩個版本各評一次,只有這一份從 fail 變成 needs_human;v1 的 fail 從 6 變 5、needs_human 從 12 變 13,v2 的計數完全相同(days/day26/results.md §4)。Day 24 的 18 份紀錄重放兩版也沒有差異(days/day26/replay_day24.csv)。這兩次重放都沒有新的 API 呼叫。

Day 24 那 18 份對應 S-003~S-008,重放結果裡也有 cross_field 失敗與截斷(days/day26/results.md §7),今天新呼叫裡的這兩類問題不是新出現的。

結果四:人工抽查,agent 預審 312 列,人工確認 0 列

判讀者 列數 結果
兩位 agent 預審 312 全部 ok
一位 agent 盲複核(預先固定的 8 張卡) 90 全部 ok
主 agent 補充複核(C01、C12、C17) 22 全部 ok
人工確認 0 —

90 與 22 列都是 312 列的子集,不能相加;主 agent 的補充複核不獨立、也不是版本盲(days/day26/results.md §8)。所以這張表能說的只有「agents 沒有挑出 violation」,不能說「沒有越界」。

agent 另外跑了四項詞彙探測。探測不是判讀,沒有推翻 agents 的判讀,但找出一個 rubric 缺口。 next_step 只規定不能暗示拒答範圍內指標的位置、不能給診斷或因果結論,沒有規定要不要和 summary 的標旗一致。C16 的 resting_hr 標旗 up,next_step 卻寫「觀察心率是否維持在基準範圍」;照字面 rubric 判 ok 沒有錯,算不算矛盾要先定規則。29 句 next_step 裡提到基準範圍或穩定的只有 2 句,另一句所在的卡三項都沒標旗。

結果與意外

原本以為 實際發現
我原本以為,新增的五項測例期望會挑出主要問題,既有的 schema 與欄位一致性檢查只需要當基本門檻。 五項測例期望 0 次 fail;8 次 fail 全是沿用的 cross_field,都是 kind 標成 data_note。
我原本以為,只改拒答規則 9,若 pass 增加,差異應該主要出在有拒答範圍的測例,能直接看出新規則有沒有作用。 pass 8 → 16,但多出的 8 份有 6 份在沒有拒答範圍的測例;差距主要來自免責句的用字。
我原本以為,禁用語句檢查加上否定詞處理後,送到 needs_human 的會主要是難以判斷的健康主張,值得優先複核。 37 個命中都在否定句內、都是免責句;21 份 needs_human 全由它造成。
我原本以為,固定輸入、prompt 和模型設定後,同一格跑三次應該大致得到相同裁決,足以當成回歸的紅綠燈。 同一格三次常不一致(S-003 v2:fail、pass、fail);只有 S-004 兩版 6 次都 pass。
我原本以為,檢查規則改版後,要重新呼叫模型才能確認影響,舊紀錄主要是留著追查當時發生什麼事。 加一個否定詞後重放已存的 72 份紀錄(54 份新、18 份 Day 24),只有 1 份裁決改變,不需要新的 API 呼叫。
我原本以為,拒答範圍的結構檢查全部通過,再加上逐句預審與複核都沒挑出問題,就足以確認這批輸出沒有越界。 結構 17/17 通過、agents 312 列全 ok,但人工確認 0 列,還有一個 rubric 缺口(C16)。

Limitations

這次的裁決只涵蓋檢查規則能辨識的結構與用詞,不能當成語意正確的保證。must_cite、must_answer 與 abstain_scope 只檢查引用與結構;否定句內的方向詞只計數,「並非偏低」這類句子仍可能漏過,送 needs_human 的數量也不等於違規數量。語意抽查的 312 列只有同一模型體系的 agents 預審,人工確認為 0 列;主 agent 的補充複核也不獨立、不是版本盲,不能把一致的 ok 當成真值。rubric 尚未納入 next_step 與 summary 標旗的一致性,C16 的存疑句仍未裁決;8 次 kind 標錯也沒有查明是規則 9 的引導、kind 定義的模糊,還是兩者共同造成。

這是一組合成資料的回歸實驗,不是對未見資料的泛化測試。9 個測例中的 S-003~S-008 沿用 Day 21–25 已看過的情境,prompt 與檢查規則都受先前輸出影響;詞表又在開發、審稿與本輪結果之後共四次調整,最後一次是讀過 54 份紀錄後加入「缺乏」。重放能確認這些既有紀錄的裁決怎麼變,不能據此估計新輸出的準確率、誤殺率或漏放率。每個測例每版只跑 3 次,且只測一個模型、一句問題與 p01,sensitive 類沒有納入;pass 從 8 份增為 16 份只能描述這批呼叫,不能據此判定 prompt-v2 較好。conflicting 也只有一個測例,rmssd −28 ms、resting_hr −10 bpm 是為跨過偵測門檻而調整的合成效果量,不代表真實生理變化的量級;本次結果只能檢驗模型如何解讀這套 summary 與期望,不能延伸成對真實穿戴資料或健康判斷的驗證。

這對 AI Engineering 的意義

這次最直接的收穫不是哪個 prompt 比較好,而是看清楚一份裁決是由哪些檢查組成的。如果只看「pass 8 → 16」,很容易得出規則 9 有效的結論;拆回逐項後,差距大多來自免責句的用字與 kind 標註,而規則 9 要擋的拒答越界,在兩版都沒有被任何確定性檢查挑出來。回歸報告至少要能拆到逐項,最好再拆到哪些測例,才能把分數變化歸因到真正改過的東西。

第二,檢查規則本身也要版本化,並且可以用舊紀錄重放。evalcheck 升版時,用 54 份新紀錄與 18 份 Day 24 紀錄各重放一次,就知道只有 1 份裁決改變,而且改變的方向是把一個已證實的誤判移出 fail。這不需要新的 API 呼叫,所以檢查規則的回歸可以每次改版都跑;prompt 的回歸要重新呼叫,成本高,只在 prompt 改版時跑。分開這兩種回歸,也才分得清分數變化來自模型、prompt,還是評分規則。

第三,三態裁決的 needs_human 要看是誰造成的。這次裁決為 needs_human 的 21 份紀錄,全部來自否定句內的詞表命中,讀過都是免責句。三態的用意是不讓字串比對直接判違規,但它也讓人工複核的工作量被免責句占滿;真正要人看的拒答越界、標旗矛盾,反而要另外抽查。

Day 27 要把同一條路徑包成 CLI 與 API。這批測例與 evalcheck 可以直接當那個服務的回歸測試:輸入固定、期望在測例檔裡、裁決可以重放。Day 28 要擴大到約 20 個情境時,還要先補上 next_step 與標旗一致性的規則,以及 cross_field 的 kind 標錯該怎麼處理。


上一篇
Day 25|Context Engineering:長期資料該如何交給模型?
下一篇
Day 27|Prototype:CLI 與單一 API endpoint,以及模型、成本與快取的取捨
系列文
30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言