iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
AI Engineering

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

Day 22|Structured Output:讓洞察可被程式驗證

  • 分享至 

  • xImage
  •  

今天為什麼研究這個?

Day 21 的 prompt v1 讓 12 份合成案例回應都停止直接給健康建議;前三組 9 份也不再把週初當成「正常值」或要回到的水準。但格式自由的長文仍留下可檢查的錯誤。有背景的 6 份回應都在資料段交代缺值原因的 basis,其中 5 份到後段又把情境設計說明寫成已確認的原因。summary 組有 2 份把 baseline 所需的「14 筆有效觀測」寫成「14 天」。這些是 Day 21 本次回應的觀察,不代表錯誤率;它們提醒我,整篇有提到證據,不等於每一句都守住證據的語意。

今天想把洞察從自由文字收進固定欄位,用 JSON Schema 與本地驗證,看程式能擋下什麼、擋不下什麼;能否改善 Day 21 的錯誤,要由實際輸出判定。

Concept

1. 把一句洞察拆成可檢查的部分

Day 19 的 feature contract 約束的是送進下游的日層級資料:指標、單位、日期歸屬、品質、baseline 與偏離狀態。今天的 output schema 則約束模型送回來的洞察。兩端可以使用相同的證據識別方式,但職責不同:模型可以敘述程式已算好的結果,不能因為輸出欄位有 interpretation,就自行補算 baseline 或推定身體狀態。

五個候選欄位需要比名稱更明確的語意。observation 應描述給定的數值、時間與單位;evidence 至少要能指出來源類型與來源位置,例如輸入中的 JSON path,而非只寫「資料說明」。interpretation 要區分直接觀測和規則判定,避免把「高於某幾天」改寫成「高於個人正常值」。uncertainty 要保留 insufficient、未知缺值原因或輔助觀察的界線。next_step 若存在,需說清是資料檢查還是一般性建議,不能靠欄位名稱替它取得醫療根據。

2. 格式通過,不等於內容有證據

JSON Schema 可以描述物件結構、必填欄位、型別與列舉值。Gemini 的 Structured outputs 文件 提供依 schema 產生回應的方式,範例仍在應用端呼叫 model_validate_json。因此即使使用模型端的結構化輸出,也要在本地解析和驗證,並記錄原始回應與失敗原因。Day 19 已見過另一個邊界:匯出的 JSON Schema 不包含 Pydantic model_validator 的跨欄位規則;只拿 schema 檔的下游會放行「無法判定卻標旗」一類矛盾。今天若有跨欄位要求,要明確決定由哪一層執行。

更難的是語意。evidence 即使填了合法的來源類型與位置,也可能指向不支持該句話的欄位;uncertainty 即使非空,interpretation 仍可能把情境說明寫成裝置已確認的事實。Day 21 的「14 筆」變「14 天」正是格式檢查可能漏掉的例子。驗收要分開記錄:JSON 能否解析、schema 能否通過、引用位置能否找到,以及主張能否由引用內容支持。後兩項需要對照輸入與句意,不能由欄位存在與否推斷。

3. 失敗後怎麼處理,也是一部分設計

Day 20–21 的共用呼叫器會針對 429/5xx 這類未取得成功回應的請求重試;今天要討論的是另一種失敗:已取得回應,但輸出無法通過解析或驗證。兩者應分開記錄,否則重試次數和失敗率會混在一起。可試的流程是先保存原始輸出及驗證錯誤,再有限次要求修正;每次修正仍是新的模型呼叫,不能覆蓋前一次紀錄。若只是把字串數字強制轉型、替模型補上缺失的證據,表面上通過驗證,反而會掩蓋原始輸出的問題。

Hands-on

Dataset

沿用 Day 20 的兩份輸入,不重新計算。summary:三個指標的 baseline 都還不夠資料(insufficient),另附一份「資料說明」。supplement:baseline 都已穩定,只有 rmssd 偏低被標旗,沒有資料說明。CSV 組沒有 JSON path 可指,今天不用。

Method

  1. Schema。 每條洞察固定五欄(見下方程式)。evidence 每筆寫三件事:哪份文件、JSON path、哪種來源(觀測 observation、程式判定 program_judgement、資料說明 data_note)。兩個刻意的設計:evidence 可以是空陣列,交給本地規則擋,免得模型沒證據時硬湊一條路徑;next_step 必填但可為 null,才分得出「漏寫」和「沒有」。
  2. 四層驗證,前一層失敗就停。 能不能解析 → 欄位與型別對不對 → path 找不找得到 → path 和來源類型是否相符。句子有沒有被引用內容支持,程式不判定;只對 Day 21 的兩種錯誤逐句標註。
  3. 兩個條件。 prompt 完全相同,都含完整 schema;api_schema 另外打開 API 的 JSON schema 模式。模型與參數同 Day 20、21。
  4. 兩種重試分開記。 429/5xx 重送同一個請求;拿到回應但驗證不過,發一次新的修正呼叫,另存一份,不覆蓋首次回應。
  5. 中途改了驗證器。 r1–r2 跑完後發現一條規則寫錯(見結果),改成 v2 才跑 r3,三輪都在 2026-10-05。r1–r2 的紀錄保留,一律用 v2 重新驗證。共 12 份首次回應、3 份修正回應。

Code

uv run python days/day22/compare_demo.py --section 1                         # 組出兩份輸入與 schema 檔,不呼叫 API
uv run python days/day22/compare_demo.py --section 2 --runs 2                # 第一批:r1–r2
uv run python days/day22/compare_demo.py --section 2 --first-run 3 --runs 1  # 第二批:r3
uv run python days/day22/compare_demo.py --section 3                         # 以目前的驗證器重新驗證所有紀錄,不呼叫 API

Schema 用 Pydantic 定義,送給 API 與寫進 prompt 的是同一份 model_json_schema()(src/wearable_ai/ai/structured.py):

class EvidenceRef(_Strict):  # strict=True, extra="forbid"
    document: Literal["summary", "context"]
    path: str  # 例如 $.metrics[1].baseline.status
    kind: Literal["observation", "program_judgement", "data_note"]

class Insight(_Strict):
    observation: str
    evidence: list[EvidenceRef]          # 不設 minItems,空陣列由 cross_field 擋
    interpretation: str
    uncertainty: str
    next_step: NextStep | None           # 必填、可為 null

cross_field 的路徑規則在 v2 長這樣。v1 沒有 context 分支,另用一條規則 context_is_data_note 要求資料說明的 evidence 一律是 data_note:

def expected_kind(document: str, path: str) -> str | None:
    segs = parse_path(path)
    if document == "context":
        if segs and segs[0] == "steps_daily":       # 資料說明裡唯一的逐日觀測(v2 新增)
            if len(segs) == 1 or (len(segs) == 2 and isinstance(segs[1], int)):
                return "observation"
            if len(segs) == 3 and isinstance(segs[1], int):
                return "observation" if segs[2] in {"date", "steps"} else None
            return None
        return "data_note"
    ...  # summary:metrics[i].daily → observation;baseline、deviation、cross_metric → program_judgement

結果與意外

先看整體:15 次請求都一次成功;15 份回應都是合法 JSON、符合 schema、path 都找得到。用 v2 驗證,兩個條件的首次回應都是 6/6 通過。代價是每份輸入多約 970 token。

原本以為 實際發現
我原本以為,prompt 裡雖已有完整 schema,另開 API schema 模式仍會讓 JSON 解析與型別驗證的首次通過數明顯高於 prompt_only,且較少漏掉輸入中的重點。 前三層沒有差別,12 份都通過。唯一的差異是 api_schema r1 summary 只回 1 條洞察、沒提 09-11;這是 3 次裡的 1 次,不足以歸因到 API 設定。
我原本以為,若回應通過解析與 schema,卻在跨欄位規則失敗,把錯誤訊息交給修正呼叫,會改正模型標錯的 evidence.kind,並保留原有主張。 觸發修正的 3 份錯的是驗證器,不是模型。v1 規定「資料說明的 evidence 只能是 data_note」,但資料說明裡的 steps_daily 是逐日步數,本來就是觀測值。3 次修正都把正確的 observation 改成 data_note 來「通過」,其他內容一字未改;修正 prompt 裡「不要為了通過驗證而補上內容」也沒擋住改標籤。換成 v2 後,這 3 份修正回應反而不通過。
我原本以為,把 interpretation 和 uncertainty 分開,會迫使模型在每一句缺值原因旁交代資料說明的 basis,避免重演 Day 21 在後段把情境說明當成已確認原因的寫法。 summary 6 份中有 5 份提到 09-11 的缺值原因,原因句都在同一句交代了 basis,例如「此說明的依據(basis)為『資料設計說明』,並非裝置偵測到的品質旗標」(prompt_only r1)。例外在 api_schema r3 的 next_step:「以減少因訊號品質不佳而產生缺值的狀況」,沒交代 basis。Day 21 的 6 份還包含 raw+context 組,不直接比比例。
我原本以為,evidence 指到 baseline.min_obs_for_center,又把觀測與解讀分欄後,模型會把門檻寫成「14 筆有效觀測」,不再把筆數說成「14 天」。 兩天的 summary 組依同一標準逐句標註。明確誤述:Day 21 3 份有 2 句,Day 22 6 份有 1 句(prompt_only r2:「至少 14 天(baseline.min_obs_for_center)有效觀測值」)。條件省略/歧義(說法不算錯,但少了條件、容易誤讀):Day 21 3 句,Day 22 9 句,多半出自 api_schema r1、r3,例如「有效天數未達 14 天的門檻」。schema 不管單位,這些句子都通過四層驗證。
我原本以為,讓 next_step.kind 在 data_check 與 general 間擇一,就能把補資料的動作和一般性建議分開計數;標成 data_check 的文字應只談資料檢查。 24 條 next_step 中,data_check 18 條、general 6 條(都在 supplement,都是「資料不足以支持建議,不適請諮詢醫療專業人員」)。但 summary 組 12 條 data_check 裡有 10 條也寫了「請諮詢醫療專業人員」或同義句。標籤是模型自己貼的,標成 data_check 不代表內容只談資料。
我原本以為,要求每筆 evidence 提供 JSON path,就能把出處縮到支持該句話的具體欄位,並讓 kind 隨路徑接受一致的檢查。 path 都存在,但粒度不一:prompt_only summary 三份各有 4 筆指向整個陣列(例如 $.metrics[0].daily)。另有 10 份引用了驗證規則沒管到的路徑(例如 interpretation_day.value),這些標籤對不對,程式無法判斷。

Limitations

這次只用兩個合成案例,每個條件、每組各跑 3 次;summary 有資料說明,supplement 沒有,兩組可引用的來源本來就不同。兩個條件的 prompt 都已含完整 schema,差別僅是 api_schema 額外啟用 API 設定,因此兩邊首次回應皆通過格式驗證,只能說在這組輸入上看不出增益,不能推論 API schema 模式一般無效。四層驗證檢查的是可解析、型別、path 存在及部分跨欄位規則;它不檢查引用內容是否支持文字主張,也不限制所有路徑的 kind、引用粒度或 next_step.kind 和文字的一致性。通過驗證的回應仍須逐句對照輸入。

重試結果還受驗證器版本影響:r1–r2 用錯誤的 validator-v1 觸發 3 次修正,r3 才改用 v2;事後以 v2 重驗可以找出誤判,卻不能把當時的修正呼叫當成 v2 下的實驗,也不能據此估計有效重試的成功率。Day 21 與 Day 22 的「14 筆/14 天」比較,是 agent 依同一套標準重新逐句標註 summary 回應;候選句先由搜尋式擷取,未命中的其他說法可能遺漏,歧義類別也依賴上下文判讀。這些計數描述本次合成輸出的句子,無法當成模型在其他資料、prompt 或使用者情境中的錯誤率,更不能支持健康或醫療效果的判斷。

這對 AI Engineering 的意義

格式驗證過了,錯誤留在句子裡。 15 份回應都通過前三層;在這兩個案例上,「會不會回出合法 JSON」幾乎分不出好壞。Day 21 的兩個錯誤都還在(見上表第 3、4 列)。Schema 保證欄位存在、型別正確、列舉值合法,不保證欄位裡的句子守住欄位的語意。

結構化的價值是讓錯誤可以按欄位數。 今天能說「誤述在 next_step」「basis 在 interpretation 有交代、在 next_step 沒有」,是因為文字被切進固定欄位,可以只查某一欄;path 也讓「引用的位置存在嗎」變成一行程式。但欄位和標籤都是模型自己填的,data_check 裡仍可能夾著就醫建議,要和其他主張一樣對照輸入才能確認。這些是 Day 24 評估與 Day 26 確定性檢查可以接上的介面,不是語意正確的證明。

驗證器也是待測的程式。 v1 的一條規則寫錯,修正迴圈就把模型正確的標籤改錯,帳面上的「修正後通過」看起來像改善。修正後的回應要和首次回應逐欄 diff,看改了什麼,不只看有沒有通過;規則要用真實輸入的路徑做回歸測試,改規則就升版本,讓每份紀錄記得自己是用哪一版判定的。


上一篇
Day 21|Prompt v1:要求 AI 只根據提供的證據說話
下一篇
Day 23|中場整合:把 Phase 1 到 2 串成一支 CLI
系列文
30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言