跳到主要內容

【2026 最新】Coding Agent 測試驗證怎麼做?Hidden Oracle × Mutation Test 6 步實戰

最後更新: ·
Coding Agent 測試驗證 Hidden Oracle 與 Mutation Test 兩道 gate 首圖

你把需求交給 Coding Agent,幾分鐘後它不只改完程式,還補了一排綠燈測試。這看起來很安心,但 Coding Agent 測試驗證最容易踩的坑正是:寫程式與寫測試的是同一個人,它可能把同一份誤解同時寫進實作與測試。

這篇專為第一次做 Agent 評測的讀者而寫。你會在一個小型 Python 專案裡,建立看不到答案的 Hidden Oracle,再用 Mutation Test 檢查測試是否真的會抓錯;最後把預設、TDD、property-based testing 與測試稽核放進同一套可重跑實驗。

先說結論:驗收 Agent 不能只問「測試綠了嗎」,而要拆成兩道互不洩題的 gate。本文的記憶公式是:驗證力 = Hidden Oracle(答案不外洩)× Mutation Score(測試真能抓錯)。任一邊接近零,綠燈都不等於可信。

Coding Agent 測試驗證,究竟在驗證什麼?

Test oracle(測試判準)就是判斷一個輸出到底對不對的規則。軟體測試研究把「如何分辨預期行為與實際輸出」稱為 oracle problem。本文所說的 Hidden Oracle,不是一個新工具名稱,而是把關鍵判準、保留案例與預期答案放在 Agent 無法讀取的位置,只由外部驗收器執行。

Mutation testing(突變測試)則故意把正確程式改壞一點,例如把 < 改成 <=、把整數除法改成一般除法,再看測試會不會失敗。會讓測試變紅的 mutant 稱為「被殺死」;仍然全綠的 mutant 叫「存活」。換句話說,Hidden Oracle 在問「實作對不對」,Mutation Test 在問「你寫的測試有沒有牙齒」。

Hidden Oracle 與 Mutation Test 兩道驗收 gate 流程圖
同一份 patch 要分別通過外部正確性判準與測試殺錯能力;Agent 只看得到任務與自己的工作區。

為什麼 Agent 自己補測試仍可能全綠?

假設需求是「折扣後金額以整數分表示,小數無條件捨去」。Agent 卻用了浮點除法,並把測試寫成 99 × 67% = 66.33。實作與測試完全一致,因此 CI 會是綠色;但兩者一起違反了「回傳整數分」的契約。

這就是共享盲點:測試數量、覆蓋率,甚至一套 property-based tests,都可能只反覆證明錯誤假設內部一致。要打破它,答案必須由工作區外的另一個判準保存,測試套件也必須接受可控的反例攻擊。

一項 Agent 測試研究真正告訴了我們什麼?

Dan Luu 公開的 agentic testing 個人實驗用 Rust/Zstd 實作任務,比較 Codex+GPT-5.6 Sol 在 medium、xhigh effort 下的多種提示條件,每個 condition/effort 預定各跑 80 次。該研究的公開主圖把「完全正確」定義為一個 run 通過當時的 37 個 hidden tests;作者也明確提醒,不宜從結果替各方法排出強而普遍的名次。

最值得帶走的不是某個百分比,而是行為落差:要求 TDD,確實更常讓 Agent 先看到失敗測試,卻未自然換來更高的完成率;要求 QuickCheck 或 mutation testing,也不代表 Agent 真的會建立有系統的 property 或跑完突變流程。尤其在 mutation 條件中,Agent 多半仍做一般測試,而不是實際產生並處理 mutants。

這不是自動化 mutation gate 的效果實驗。研究是按 prompt 標籤分組,觀察特定 Codex harness、effort 與任務下的 Agent 行為;它沒有替每一組安裝並強制執行外部 mutation gate。本文的雙 gate 是從失敗模式推導出的工程假說,不是研究已證明會提高正確率的答案。

這項研究只涵蓋 Rust 與 Zstd 任務。截至 2026 年 9 月 10 日,作者原文與較早方法說明未直接附上完整 prompts、raw trajectories 與當時 37 題 hidden suite 的下載連結;僅憑這兩頁無法獨立重跑相同研究。因此本文不把它外推成「某方法一定較好」:不要相信提示名稱,要檢查 Agent 實際做了什麼。

Coding Agent 測試驗證:6 步建立 Hidden Oracle × Mutation Test harness

1. 先把需求改寫成可判定的契約

痛點:「折扣算對」太模糊,Agent 與驗收器可能各自解讀。解法:把輸入範圍、輸出型別、邊界行為與錯誤條件寫清楚,但不要把保留案例的答案塞進提示。這和 Specification-first 的核心相同:先固定可觀察契約,再讓實作競爭。

def final_price(cents: int, percent_off: int) -> int:
    """回傳折扣後的整數分;小數無條件捨去。

    cents 必須 >= 0;percent_off 必須介於 0 與 100。
    不合法輸入要拋出 ValueError。
    """

你要看到的結果:任何 reviewer 都能只看這段契約,獨立說出哪些輸出算成功、哪些算失敗。

2. 把 Hidden Oracle 移出 Agent 工作目錄

痛點:檔名叫 hidden_tests 不代表真的隱藏;能讀整台機器的 Agent 仍可能搜尋到它。解法:讓 Agent 只掛載臨時工作區,host-only evaluator 另外保存 oracle。至少隔離檔案、環境變數、命令列參數、Git 歷史與過往執行紀錄;需要更強邊界時,使用獨立容器或執行帳號。

harness/
├── seed/                 # 每次實驗都從同一份乾淨起點複製
├── oracle/               # 只給 host evaluator,絕不掛進 Agent
│   └── test_price_hidden.py
└── runs/
    └── <condition>-<run_id>/  # Agent 唯一可讀寫的位置

驗證方式:從 Agent 使用的帳號或容器執行 find、列出掛載與環境,確認看不到 oracle 路徑及答案。這是安全邊界,不是靠提示叫 Agent「不要偷看」。如果你還不熟悉執行層,可先讀 AI Agent Harness 是什麼

3. 建立四個只改一項條件的實驗組

痛點:不同 prompt、模型 effort、工具權限與時間上限一起變動,就無法知道差異從哪裡來。解法:固定任務、seed commit、Agent 版本、effort、工具、token/時間上限與重跑次數,只替換測試方法:

  1. Default:只給功能需求,不額外指定測試流程。
  2. TDD:要求先寫會失敗的測試,再寫最小實作讓它通過。
  3. Property-based:要求定義跨多組輸入都成立的性質,再由工具產生案例。
  4. Test audit:實作完成後開啟全新 context,只給規格、diff 與測試,要求找出測試盲點並補強。
Default、TDD、Property-based 與 Test audit 四組 Coding Agent 實驗設計
四組使用相同任務與預算;每次從乾淨 seed 開始,才能把差異歸因到測試策略。

你要看到的結果:每一組都有獨立 run ID、相同設定快照與完整事件紀錄。只跑一次只能算示範;若要比較方法,必須重跑並呈現分布,而不是挑最好的一次。

四組的主要結果都用同一套 Hidden Oracle 驗收候選實作。若還要比較「哪一組寫出的測試比較強」,則把各組測試套件移到同一份凍結的參考實作上,再攻擊同一批 mutants;直接突變四份不同候選程式,mutant 數量與結構不同,mutation score 便混入了實作差異。

4. 用 pytest+Hypothesis 寫可觀察的 visible suite

痛點:只測兩三個例子,很容易漏掉邊界;但亂生輸入也不會自動知道正確答案。解法:用例子鎖住明確行為,再用 property 描述不變條件。Hypothesis 官方 Quickstart示範以 @given 產生資料,並直接配合 pytest。

from hypothesis import given, strategies as st
import pytest
from price import final_price

def test_example():
    assert final_price(10_000, 20) == 8_000

@given(
    cents=st.integers(min_value=0, max_value=1_000_000),
    percent=st.integers(min_value=0, max_value=100),
)
def test_result_stays_in_range(cents, percent):
    result = final_price(cents, percent)
    assert 0 <= result <= cents

@given(cents=st.integers(min_value=0, max_value=1_000_000))
def test_boundary_identities(cents):
    assert final_price(cents, 0) == cents
    assert final_price(cents, 100) == 0

@given(
    cents=st.integers(min_value=0, max_value=1_000_000),
    lower=st.integers(min_value=0, max_value=100),
    higher=st.integers(min_value=0, max_value=100),
)
def test_more_discount_never_costs_more(cents, lower, higher):
    lo, hi = sorted((lower, higher))
    assert final_price(cents, hi) <= final_price(cents, lo)

@pytest.mark.parametrize("percent", [-1, 101])
def test_rejects_invalid_percent(percent):
    with pytest.raises(ValueError):
        final_price(100, percent)

驗證方式:先故意破壞一個已知行為,確認測試真的會紅,再還原。property 只能驗證你明確寫出的性質;上面的區間性質仍抓不到「結果必須是整數分」,所以 Hidden Oracle 仍不可省略。

5. 用 Mutmut 攻擊 Agent 寫出的測試

痛點:測試全綠只證明目前程式通過,沒有證明測試能辨識錯誤版本。解法:Mutmut 官方文件安裝並執行突變;截至 2026 年 9 月,本文範例固定使用 Python 3.12、pytest 9.1.1Hypothesis 6.168.0Mutmut 3.7.0,避免未來套件變動讓輸出難以對照。

為了讓四組面對同一批 mutants,host 另外保存下面這份 frozen reference implementation;它用來比較測試套件,不會在 Agent 解題前提供給 Agent。

def final_price(cents: int, percent_off: int) -> int:
    if cents < 0:
        raise ValueError("cents must be non-negative")
    if not 0 <= percent_off <= 100:
        raise ValueError("percent_off must be between 0 and 100")
    return cents * (100 - percent_off) // 100
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]

[tool.mutmut]
source_paths = ["src/"]
pytest_add_cli_args_test_selection = ["tests/"]
do_not_mutate_patterns = ["raise ValueError"]

# 建立隔離環境後執行
python3.12 -m venv .venv
. .venv/bin/activate
python -m pip install "pytest==9.1.1" "hypothesis==6.168.0" "mutmut==3.7.0"
pytest -q
# CI/正式比較一律從不存在 mutants/ 的乾淨工作區開始
if [ -e mutants ]; then
    echo "refuse stale mutation state" >&2
    exit 2
fi
mutmut run
mutmut results
mutmut export-cicd-stats
test -s mutants/mutmut-cicd-stats.json || exit 3

範例用 do_not_mutate_patterns 排除只改例外訊息的 6 個 mutants,因為公開契約只要求拋出 ValueError,沒有要求訊息字串。未排除時是 18 個 mutants、殺死 11 個、存活 7 個(61.1%);排除後測試沒有變強,分數只是機械式變成 11/12(91.7%)。所以設定與排除理由必須和分數一起揭露,並鎖在 Agent 無法修改的位置。

這個固定版本的最小範例共有 12 個納入計分的 mutants:visible suite 殺死 11 個,1 個存活,mutation score 是 11 ÷ 12 = 91.7%。存活者把 // 100 改成 / 100;visible tests 仍是 6 passed,但 host 另跑 oracle 時有 4 個案例失敗,包括 99, 33 應回傳整數 66,而不是浮點數 66.33

這段本地 trace 證明的是「同一個 mutant 能通過 visible suite、卻被另一套 oracle 抓到」,不是證明它已承受惡意程式的隔離攻擊。真正評測仍要使用第 2 步的權限或容器邊界;而 mutant ID 會隨排除設定改變,證據應綁定實際 diff 與 config,不要只記流水號。

重要:Mutmut 3.7.0 的執行邏輯與本文固定版本輸出都可確認:mutmut run 即使有存活 mutant 也可能以 exit code 0 結束,不能只看 shell exit code。下面這個最小 gate 讀取匯出的 JSON;正式專案可調整 policy,但不應默默放行跳過、未檢查、超時或存活的結果。

python - <<'PY'
import json
from pathlib import Path

stats = json.loads(
    Path("mutants/mutmut-cicd-stats.json").read_text()
)
blocking = [
    "survived", "no_tests", "skipped", "suspicious", "timeout",
    "check_was_interrupted_by_user", "segfault",
]
bad = {key: stats.get(key, 0) for key in blocking if stats.get(key, 0)}
total = stats.get("total")
valid_total = isinstance(total, int) and total > 0
all_killed = valid_total and stats.get("killed") == total
if bad or not all_killed:
    raise SystemExit(f"mutation gate failed: {bad or stats}")
print(f"mutation gate passed: {stats['killed']}/{stats['total']} killed")
PY

這裡額外要求 killed == total,可攔住 JSON 沒逐欄列出的非 killed 狀態;它是關鍵模組的保守範例,不是宣稱所有專案都該追求 100%。遇到等價 mutant 時,先由人判讀、留下理由,再用鎖定的 policy 處理。

整數除法突變存活後被 Hidden Oracle 抓出的最小範例
綠燈 visible suite 漏過了型別與捨去規則;同一 mutant 在外部 oracle 留下可定位的失敗證據。

別把 91.7% 當成通用門檻。mutant 可能等價、超時或落在不重要的程式碼;不同工具與設定的分母也可能不同。正確做法是把 score 當趨勢訊號,逐一審查存活者,並要求關鍵模組不能留下可觀察、非等價的高風險 mutation。

6. 用全新 context 複核,留下可比較的 run record

痛點:原 Agent 看過自己的推理與修補過程,容易沿用同一假設。解法:開一個沒有舊對話的 reviewer,只提供規格、最終 diff、visible test 報告、mutation report 與經過遮蔽的 oracle 失敗類型;不要交出 hidden inputs 或預期答案。這是 對抗式 reviewer 的同一原則:讓第二條推理路徑有機會反駁第一條。

下面只是 run-record schema 範例,值不是另一筆實驗結果:

{
  "run_id": "<condition>-<sequence>",
  "seed_commit": "<git-sha>",
  "condition": "<default|tdd|property-based|audit>",
  "agent_config": "<model-effort-tools snapshot>",
  "oracle_pass": "<boolean>",
  "killed": "<integer>",
  "survived": "<integer>",
  "tokens": "<provider usage record>",
  "wall_seconds": "<monotonic timer>",
  "reviewer_verdict": "<pass|needs-fix>"
}

你要看到的結果:同一 run 能回溯到 seed、設定、patch 與報告;跨組比較時,同時看 hidden-oracle pass rate、mutation outcome、token、時間與重跑變異。想把這類 gate 接進持續整合,可延伸到 CI root-cause Agent 校準的記錄方式。

三個最常見的失敗模式

坑 1:把 hidden tests 放在同一個 repo

即使 prompt 說「不要讀」,可存取的檔案仍可能被搜尋、索引或意外寫進 log。修法是用權限與執行邊界隔離,而不是靠自律。若 harness 還會接觸外部內容,也可借用 Prompt Injection 回歸測試的 canary 思路,檢查秘密是否外洩。

坑 2:把 mutation score 當品質分數

score 高不代表需求完整,也不代表每個 mutant 都有商業風險。先看關鍵路徑、存活 mutation 的語義與 oracle 結果,再看總分;對等價 mutation 要註記,而不是硬逼測試殺死不影響行為的改動。

坑 3:比較四組時偷偷換了預算

TDD 或 audit 可能自然消耗更多步驟。如果一組多拿 token、一組多拿工具,結果同時混入方法與資源差異。先做等預算比較;若你也想知道「多花資源值不值得」,再另開一個成本受控實驗。評估 Agent 任務難度時,可搭配 METR Time Horizon 教學理解成功率與任務長度不是同一件事。

Coding Agent 測試驗證 FAQ

1. 有了 Hidden Oracle,就不需要 Agent 寫測試嗎?

不對。Hidden Oracle 負責獨立驗收,Agent 寫的 visible tests 則提供快速回饋、避免回歸,也讓維護者看懂契約。兩者角色不同。

2. 測試覆蓋率 100%,還需要 mutation testing 嗎?

需要。覆蓋率只表示程式碼被跑到,不表示測試會在行為被改壞時失敗;mutation testing 正是在檢查這個差距。

3. Property-based testing 可以取代 Hidden Oracle 嗎?

通常不能。它擅長擴張輸入範圍,但仍需要你定義性質;若性質本身漏掉型別、捨入或跨系統契約,產生再多資料也不會自動補上答案。

4. Mutation score 越高就一定越好嗎?

不一定。你要先排除等價 mutation、確認分母一致,並優先看關鍵模組的存活者。跨專案、工具或版本的數字不宜直接比較。

5. Hidden Oracle 只能是隱藏單元測試嗎?

不是。它也可以是參考實作、形式規格、契約檢查、metamorphic relation、人工評分規準,或另一個可信系統的差分結果;重點是判準獨立且不向被測 Agent 洩題。

6. Test audit 一定要換另一個模型嗎?

不一定。最小要求是全新 context 與受控輸入;換模型可能增加推理多樣性,但也同時新增變因。若目的是比較測試方法,先保持模型設定一致。

7. 小專案也值得跑 mutation testing 嗎?

值得先跑在關鍵模組。不必一開始掃完整 repo;先選金額、權限、資料轉換或核心演算法等「錯了會痛」的程式碼,建立時間與價值基準。

8. 可以把 hidden failure 完整貼回 Agent 修正嗎?

不要在正式評測中直接貼答案。可以回傳失敗類別、違反哪一條公開契約,或限制次數的模糊訊號;一旦揭露輸入與預期輸出,該案例就不再是 holdout,應另換一組未曝光 oracle。

給新手的 5 個重點

  • 先寫可判定的契約,再談 prompt 技巧。
  • 把 oracle 放在 Agent 讀不到的執行邊界外。
  • 綠燈只代表目前案例通過;mutation 才會主動測試「抓錯能力」。
  • Default、TDD、property-based、audit 四組只改一項變因,並從乾淨 seed 重跑。
  • 同時保存 correctness、mutation、成本、時間與變異,不拿單次漂亮結果當結論。

若你想把這套驗收延伸成完整開發流程,可從 建立 AI Agent Harness 接著做;想系統化練習 Agent 與自動化工作流,也可查看 AlphaLab 課程

接著閱讀

左右滑動查看更多推薦

結語:把「會寫測試」升級成「能證明測試有效」

Coding Agent 的測試檔寫得再漂亮,也可能只是和實作共享同一個盲點。真正可攜的做法仍是那句公式:驗證力 = Hidden Oracle × Mutation Score。前者守住獨立答案,後者逼測試面對可控錯誤。

你的第一步不必評測整個大型 repo。今天就挑一個 20 行左右、契約清楚的純函式,藏起 5 個邊界案例,跑一次 mutmut run,再親手讀完每個存活 mutant。當 Agent 無法偷看答案,而它寫的測試又真的能把錯誤打紅,你才開始擁有可驗證的綠燈。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

每週最多三封:一封 Weekly 週報與最多兩封關鍵 Alpha Signal。

我們不會 spam,隨時可退訂。