iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Claude AI

把 Claude 練成專家:30 天打造可驗證的 Agent Skills系列 第 12 篇

Day 12|把 pytest 與 eval 接進 CI:讓規格成為合併門檻

  • 分享至 

  • xImage
  •  

昨日回顧

Day 11 把完整行為案例做成 JSONL eval,runner 會驗證 status、normalized_host、evidence 與 next_step,並區分產品不符與設定損壞。今天把 pytest 和 eval runner 接進 CI,讓每次修改腳本、來源 registry 或 SKILL.md 都必須通過同一套門檻。

CI 不是把本機指令搬上雲

一個可靠的 CI job 還要回答:

  • 哪些檔案改動會觸發驗證
  • Python 與依賴版本是否固定
  • pytest 與 eval 是否分開呈現
  • 失敗時能否直接看到原因
  • 分支保護是否真的要求這個 check

只寫一行 pytest 不代表規格已經成為合併門檻。

先固定執行環境

用 requirements-dev.txt 明確鎖住測試與 schema 驗證依賴:

pytest==8.3.5
PyYAML==6.0.2
jsonschema==4.23.0

若專案已有 lockfile,就使用同一套管理工具,不要同時維護兩份真相。重點是 CI 不應在每次執行時悄悄抓到新的 major 或不相容版本。

Workflow 的觸發範圍

.github/workflows/verify-ticket-guard.yml:

name: verify-ticket-guard

on:
  pull_request:
    paths:
      - "SKILL.md"
      - "scripts/**"
      - "references/**"
      - "tests/**"
      - "evals/**"
      - "requirements-dev.txt"
      - ".github/workflows/verify-ticket-guard.yml"
  push:
    branches: [main]
    paths:
      - "SKILL.md"
      - "scripts/**"
      - "references/**"
      - "tests/**"
      - "evals/**"
      - "requirements-dev.txt"
      - ".github/workflows/verify-ticket-guard.yml"

路徑要涵蓋規格、實作、來源、測試、eval 與 workflow 自身。只監控 scripts/** 會讓人改了 sources.md 或 expected cases 卻不跑驗證。

最小權限和取消舊執行

permissions:
  contents: read

concurrency:
  group: verify-ticket-guard-${{ github.ref }}
  cancel-in-progress: true

驗證工作只需要讀 repository。相同 branch 推新 commit 時取消舊 run,避免過期結果占用 runner,也避免 reviewer 看錯版本。

固定 Python 並快取套件

jobs:
  verify:
    runs-on: ubuntu-24.04
    timeout-minutes: 10

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
          cache-dependency-path: requirements-dev.txt

      - name: Install dependencies
        run: python -m pip install --require-hashes -r requirements-dev.txt

ubuntu-24.04 與 Python 3.12 明確指定,不依賴 latest。若要使用 --require-hashes,requirements 必須真的包含每個 wheel/source 的 hash;否則先移除這個 flag,不能貼上一個看似安全但必定失敗的指令。

第一版也可以用:

      - name: Install dependencies
        run: python -m pip install -r requirements-dev.txt

然後在下一次維護加入可重現 hash。文章中的安全選項必須和 repository 現況一致。

pytest 和 eval 分成兩個 step

      - name: Run unit tests
        run: python -m pytest -q

      - name: Run behavior evals
        run: |
          mkdir -p artifacts
          python evals/run_evals.py \
            --cases evals/cases.jsonl \
            --schema evals/schema.json \
            --sources references/sources.md \
            --json-output artifacts/eval-summary.json

分開的 step 讓失敗位置一眼可見。不要用:

python -m pytest -q && python evals/run_evals.py ...

那會把兩層契約混成一段 log,也讓第二個命令因第一個失敗而完全沒有狀態。

產品失敗和設定失敗要保留

Day 11 已定義:

  • exit 0:全部通過
  • exit 1:runner 正常,但產品輸出與 expected 不符
  • exit 2:cases、schema 或 sources 損壞

CI 不需要改寫這些 code。GitHub Actions 會把非零視為失敗,而 log 中保留 1/2 的診斷語意。不要寫 || true,也不要用 continue-on-error: true 把紅燈改成綠燈。

即使失敗也上傳摘要

若 runner 在 exit 1 前寫出 JSON summary,可以用 if: always() 保存 artifact:

      - name: Upload eval summary
        if: always() && hashFiles('artifacts/eval-summary.json') != ''
        uses: actions/upload-artifact@v4
        with:
          name: eval-summary
          path: artifacts/eval-summary.json
          if-no-files-found: error
          retention-days: 14

Artifact 不應包含來源頁面內容、憑證、環境變數或完整 traceback。只保留 case id、不一致欄位和通過/失敗計數。

設定失敗時沒有 summary 是正常的

Exit 2 可能發生在 runner 尚未建立 summary 之前。hashFiles(...) != '' 讓 upload step 在檔案不存在時跳過,而不是用第二個錯誤蓋掉原始原因。

若想讓所有失敗都有 artifact,runner 可以先建立最小 envelope,再開始載入:

{
  "status": "configuration_error",
  "message": "cases.jsonl:4: invalid JSON",
  "total": 0,
  "passed": 0,
  "failed": 0
}

訊息要可診斷,但不能把整份來源 registry 複製進 artifact。

完整 workflow

name: verify-ticket-guard

on:
  pull_request:
    paths:
      - "SKILL.md"
      - "scripts/**"
      - "references/**"
      - "tests/**"
      - "evals/**"
      - "requirements-dev.txt"
      - ".github/workflows/verify-ticket-guard.yml"
  push:
    branches: [main]
    paths:
      - "SKILL.md"
      - "scripts/**"
      - "references/**"
      - "tests/**"
      - "evals/**"
      - "requirements-dev.txt"
      - ".github/workflows/verify-ticket-guard.yml"

permissions:
  contents: read

concurrency:
  group: verify-ticket-guard-${{ github.ref }}
  cancel-in-progress: true

jobs:
  verify:
    runs-on: ubuntu-24.04
    timeout-minutes: 10

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
          cache-dependency-path: requirements-dev.txt

      - name: Install dependencies
        run: python -m pip install -r requirements-dev.txt

      - name: Run unit tests
        run: python -m pytest -q

      - name: Run behavior evals
        run: |
          mkdir -p artifacts
          python evals/run_evals.py \
            --cases evals/cases.jsonl \
            --schema evals/schema.json \
            --sources references/sources.md \
            --json-output artifacts/eval-summary.json

      - name: Upload eval summary
        if: always() && hashFiles('artifacts/eval-summary.json') != ''
        uses: actions/upload-artifact@v4
        with:
          name: eval-summary
          path: artifacts/eval-summary.json
          if-no-files-found: error
          retention-days: 14

這份 workflow 不需要 repository secret,也沒有寫入權限。若驗證流程要求 token 才能讀測試資料,表示 fixture 或架構邊界需要重新檢查。

不要用外部網路完成核心 eval

域名分類的 eval 應使用本地來源 fixture,不應在 CI 中抓 organizer 網站或 DNS:

  • 外部服務可能暫時失敗
  • 內容可能在同一 commit 下改變
  • rate limit 會讓結果漂移
  • PR 作者不應能藉由 URL 讓 runner 讀取任意內網資源

來源證據的定期更新是另一個 workflow;核心回歸測試必須離線、確定、快速。

Action 版本也要維護

actions/checkout@v4、setup-python@v5、upload-artifact@v4 比追 main 穩定,但 tag 仍可能移動。高敏感 repository 可以 pin 到完整 commit SHA,再用 Dependabot 更新:

dependabot:
  updates:
    - package-ecosystem: github-actions
      directory: /
      schedule:
        interval: monthly

不要手寫一個假的 SHA。真正 pin 時,從 action 官方 repository 的 release/commit 取得並 code review。

Branch protection 才是真正的門檻

Workflow 成功不代表合併一定被阻擋。Repository 設定還要把 verify-ticket-guard / verify 設為 required status check,並要求 branch 是最新版本後才能合併。

確認步驟:

  1. 開一個測試 PR,故意改壞 expected evidence。
  2. 確認 eval step 紅燈、artifact 可讀。
  3. 確認 GitHub 不允許合併。
  4. 修正 expected 或實作後,確認同一 check 轉綠。
  5. 再破壞 cases JSON,確認 exit 2 也會阻擋合併。

Required check 名稱是 API 與設定的一部分。不要頻繁改 job id,否則 branch protection 可能指向不存在的 check。

Path filter 的盲點

如果 PR 只改 README,workflow 不會執行。這是刻意的節省,但 required check 必須配合 GitHub 的 path-filter 行為測試。若平台讓被跳過的 required workflow 永遠 pending,改用永遠觸發 workflow,再在 job 內決定是否執行重測。

最安全的第一版其實是所有 PR 都跑。這組測試應該快速,省下幾十秒不值得換一個合併死鎖。

今天完成的驗收標準

[ ] 本機 pytest 通過
[ ] 本機 eval 通過
[ ] PR 會觸發 verify job
[ ] 產品 mismatch 顯示可讀 diff,exit 1
[ ] 設定損壞顯示行號/欄位,exit 2
[ ] eval summary 在失敗時仍可取得
[ ] workflow 只有 contents: read
[ ] required check 真正阻擋紅燈 PR
[ ] 核心 eval 不依賴外部網路

CI 的價值不是「有一個綠色勾勾」,而是把可驗證規格變成每次合併都無法跳過的工程約束。

明天預告

Day 13 回到 SKILL.md,設計分流與失敗政策:哪些輸入直接拒絕、哪些標成 UNCONFIRMED、哪些情況可以繼續到後續流程。


上一篇
Day 11|把 pytest 升級成 eval:不只通過,還要說對理由
下一篇
Day 13|把失敗政策寫回 SKILL.md:先分流,再決定能不能繼續
系列文
把 Claude 練成專家:30 天打造可驗證的 Agent Skills 共 14 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言