自分で書いたプロンプトは自分で評価できない — Empirical Prompt Tuning 実践記
TL;DR
mizchi氏の empirical-prompt-tuning スキルを使って、自分の Claude Code スキル 6 本を最適化した。まず中核 3 本(bounded-context, minerua-codegen, tdd-cycle)で手法を確立し、その後 3 本(jj-workspace, living-documentation, minerua-fill)に展開。37 のサブエージェントをディスパッチし、全スキルの skill-quality unclear を 0 に収束させた。最大の発見: accuracy 100% でも品質の穴は見える。そして 自分の fix が新しいバグを生む瞬間をバイアスフリーの executor が捕まえた。
動機: なぜスキルのテストが必要なのか
Claude Code のスキル(SKILL.md)は、エージェントへの「業務指示書」だ。人間がドキュメントを書くのと同じで、書いた本人は「明瞭に書けた」と思い込む。しかし別の人が読むと解釈がブレる。
エージェントも同じだ。自分が書いたスキルを自分で読み返しても、バイアスが入って「ちゃんと書けている」と感じてしまう。empirical-prompt-tuning の核心はシンプル:
書いた本人ではなく、何も知らない新しいエージェントに実際に実行させ、二面評価する。
セットアップ
2段階のアプローチ
全体を2段階で進めた:
- Phase 1(深掘り): 中核 3 スキルで手法のすべての機能を検証(Iteration 1-3 + variant exploration + hold-out)
- Phase 2(展開): 残り 3 スキルにバッチ適用 + Hook による自動化
Phase 1 対象スキル
| スキル | 用途 | 行数 |
|---|---|---|
bounded-context |
Wittgenstein 言語ゲーム + DDD による境界分析 | 535行 |
minerua-codegen |
自然言語仕様からコード生成ワークスペースを作成 | 153行 |
tdd-cycle |
t-wada式 Red-Green-Refactor ワークフロー | 698行 |
Phase 2 対象スキル(後半で追加)
| スキル | 用途 | 選定理由 |
|---|---|---|
jj-workspace |
Jujutsu VCS 操作 | 毎回使用、高頻度 |
living-documentation |
ドキュメント生成・レビュー | 複数モード、高複雑度 |
minerua-fill |
codegen スケルトンの関数充填 | minerua-codegen と対 |
シナリオ設計
各スキルに 2 シナリオ(median + edge)を用意。Phase 1 のシナリオ:
| スキル | Scenario A (median) | Scenario B (edge) |
|---|---|---|
| bounded-context | フードデリバリー「注文」の多義性 | IoT — 技術コンポーネントから Actor を発見できるか |
| minerua-codegen | 認証モジュール生成 | 既存型参照 (--project-context) |
| tdd-cycle | パリンドローム判定の TDD | セグフォルトバグの修正 |
各シナリオに [critical] 付きの要件チェックリスト(3-6 項目)を設定。
Phase 2 のシナリオは「スケールアウト」セクションで紹介する。
Iteration 0: 静的整合性チェック
サブエージェントを動かす前に、description と body の gap を確認する。
| スキル | Gap |
|---|---|
| bounded-context | 軽微 — description にコード接地が未記載 |
| minerua-codegen | 要注意 — "IG inference" と謳うが実際は Claude Code が JSON 生成 |
| tdd-cycle | 軽微 — description が body の豊富さに対して控えめ |
この静的チェックを飛ばすと、サブエージェントが body を description に合わせて「再解釈」し、accuracy が不当に高く出る(false positive)。
Iteration 1: Baseline
6 つのサブエージェント(Sonnet)を並行ディスパッチ。
結果
| Scenario | Success | Accuracy | Steps | Duration | Unclear pts |
|---|---|---|---|---|---|
| bc-A | ○ | 100% | 1 | 133.8s | 0 |
| bc-B | ○ | 100% | 1 | 174.1s | 2 |
| mc-A | ○ | 100% | 4 | 71.8s | 3 |
| mc-B | ○ | 100% | 3 | 69.2s | 3 |
| tdd-A | ○ | 100% | 1 | 99.1s | 2 |
| tdd-B | ○ | 100% | 1 | 67.7s | 0 |
全シナリオ accuracy 100%。しかし unclear points は計 10 件。
発見 1: accuracy 100% でも品質の穴は見える
定量メトリクス(accuracy, steps, duration)だけを見ていたら「完璧」で終わっていた。しかし executor の self-report から:
- minerua-codegen の JSON スキーマに
importsフィールドがない(生成ルールは import を要求するのに) - skeleton stub に
discardを使っている(Error honesty 原則に違反) - tdd-cycle で「既にパスするテストを書くな」と「Obvious Implementation 後の回帰ガード」が矛盾
これらは 定性的評価(unclear points / discretionary fill-ins)が主、定量が補 という設計思想の正しさを裏付ける。
発見 2: tool_uses のスキュー
minerua-codegen の steps(3-4)が他スキル(1)の 3-4 倍。これはスキルの 自己完結性の低さ を示す構造的シグナル。CLI バイナリ確認やファイル I/O が多く、executor が外部参照に追われている。
Iteration 2: Fix 適用と再評価
Fix propagation patterns — 適用前に予測する
empirical-prompt-tuning の独自機能。fix を当てる前に「この fix はどう伝播するか」を予測する:
| Fix | 予測 | 実測 |
|---|---|---|
JSON スキーマに imports 追加 |
Overshoot | Overshoot ✓ — steps 4→1 |
| project-context エラーパス追加 | Conservative | Conservative ✓ |
skeleton stub を raise に変更 |
Overshoot | Zero-shoot ✗ |
| tdd 回帰ガード例外明記 | Conservative | Conservative ✓ |
| bc 入力分類ステップ追加 | Conservative | Conservative ✓ |
5 つ中 4 つ的中。しかし 1 つが Zero-shoot — fix がまったく効かないどころか、新しいバグを生んだ。
発見 3: 自分の fix がバグを生む(Zero-shoot)
discard を raise newException(NotImplementedDefect, ...) に変更した。一見正しい。しかしバイアスフリーの executor が即座に指摘:
NotImplementedDefectは Nim stdlib に存在しない-
{.raises: [].}とraiseが矛盾する(Exception 系は effect tracking される)
自分で書いた fix を自分でレビューしても、この矛盾には気づけなかっただろう。「raise にしたから OK」という確証バイアスが働くからだ。
Hold-out シナリオ(overfitting check)
収束判定の前に、これまで使っていない新シナリオで検証:
| Skill | Hold-out scenario | Accuracy | Overfitting? |
|---|---|---|---|
| bounded-context | 病院「処方」4 Actor | 100% | No |
| minerua-codegen | Blog CRUD | ~96% | Minor — デフォルト値未対応 |
| tdd-cycle | Stack[T] 実装 | 100% | No |
minerua-codegen だけ minor signal。パラメータのデフォルト値 (limit: int = 10) を JSON スキーマで表現できない問題が浮上。
Iteration 3: Variant Exploration
minerua-codegen の skeleton stub 問題が未収束。局所最適から脱出するため、2 variant を並行テスト。
| Variant A | Variant B | |
|---|---|---|
| 方式 | doAssert false, "stub" |
type StubDefect = object of Defect + raise
|
| 利点 | 追加型不要、Nim 標準 | grep 可能、明確なエラー名 |
4 エージェント(2 variant × 2 scenario)を並行ディスパッチ。
比較(客観軸のみ)
| Metric | Variant A | Variant B |
|---|---|---|
| mc-A accuracy | 90% | 100% |
| mc-B accuracy | 100% | 100% |
| Total steps | 8 | 10 |
| Unclear points | 3 | 3 |
Variant B を採用。 accuracy が高い方を選ぶ。tie なら steps が少ない方を選ぶ — というルールが決まっているので、主観的な「どちらが好きか」は入らない。
最終収束状態
| Skill | Iterations | Skill-quality unclear | Status |
|---|---|---|---|
| bounded-context | 2 | 0 | 収束 |
| tdd-cycle | 2 | 0 | 収束 |
| minerua-codegen | 3 (variant 含む) | 0 | 収束 |
数字で見る全体像
| 指標 | 値 |
|---|---|
| ディスパッチしたサブエージェント | 19 |
| 総 iteration | 3 + variant exploration |
| iter1 の unclear points | 10 |
| 最終 unclear points (skill-quality) | 0 |
| 発見した fix バグ | 1 (Zero-shoot) |
| Hold-out で発見した新問題 | 2 |
| 使用した全機能 | 14/14 (100%) |
学んだこと
1. 「書いた本人が読み返す」は構造的に無意味
これが empirical-prompt-tuning の根本思想であり、実践して身に染みた。自分の fix にバグがあることに気づけなかったのが最大の証拠。
2. accuracy 100% は安心材料にならない
Iteration 1 で全シナリオ 100% だったのに、10 件の unclear points が出た。メトリクスだけ見て「完璧」と判断するのは危険。定性評価(self-report)が主。
3. Fix propagation patterns で「効く fix」と「効かない fix」を予測できる
5 つ中 4 つ的中。外した 1 つが Zero-shoot で、これが最も学びが大きかった。fix を当てる前に「これはどの判定文言を満たすか」を明示する習慣が、空振りを防ぐ。
4. tool_uses のスキューは構造的欠陥のシグナル
accuracy が同じでも、あるシナリオだけ steps が 3-4 倍なら、そのスキルは自己完結性が低い。「accuracy だけで切る」と構造的欠陥を見逃す。
5. Variant exploration は「迷ったら両方試す」の形式化
doAssert vs StubDefect で迷ったとき、主観で選ばず並行テストして客観軸で比較した。これは個人のプロンプト改善でも組織のプロンプト運用でも使えるパターン。
6. Hold-out シナリオは overfitting の安全弁
2 シナリオだけでチューニングすると、そのシナリオに特化した改善になりがち。hold-out で検証したら minerua-codegen にだけ minor signal が出た(デフォルト値問題)。
スケールアウト: 残り 3 スキルのバッチチューニング
最初の 3 スキルで手法を確立した後、残りの高優先度スキル(jj-workspace, living-documentation, minerua-fill)をバッチで回した。
自動化: PostToolUse Hook
SKILL.md を編集するたびに empirical-prompt-tuning を提案する hook を settings.json に追加:
# ~/.claude/hooks/suggest-empirical-tuning.sh
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_response.filePath // ""')
if echo "$FILE_PATH" | grep -q 'skills/.*SKILL\.md$'; then
SKILL_NAME=$(echo "$FILE_PATH" | sed -E 's|.*/skills/([^/]+)/SKILL\.md$|\1|')
echo "{\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\",
\"additionalContext\": \"SKILL.md for '${SKILL_NAME}' was modified.
Consider running /empirical-prompt-tuning.\"}}"
fi
実際に SKILL.md を Edit した瞬間、即座に提案が表示された。ワークフローに組み込まれた。
バッチ結果
| Skill | iter1 unclear | iter2 unclear | Status |
|---|---|---|---|
| jj-workspace | 0 | — | 即収束(1 iteration) |
| living-documentation | 2 | 0 | 2 iteration で収束 |
| minerua-fill | 3 | 0 | 2 iteration で収束 |
jj-workspace は Iteration 1 で both scenarios unclear 0 — 最も成熟したスキルだった。
living-documentation の fix は 2 件:
- ADR の自動採番ルール(
docs/adr/内の最大番号 + 1) - 出力言語の決定ルール(既存 ADR の言語に合わせる)
minerua-fill の fix は 3 件:
-
--functionsで部分充填した場合のstages.fill状態遷移(ssInProgress) - 共有ファイルの deploy 時に未充填 stub を含む警告表示
- skeleton-relative → project-relative の import パス書き換えアルゴリズム
最も印象的だった出力: living-documentation の ADR 生成
executor が実際のコードベース(types.nim, evaluation.nim, operators.nim, 既存 ADR-0001〜0088)を読み込み、ADR-0089 として既存の ADR-0001 を Supersede する形で生成した。スキルの「ADR を更新するな — 新しい ADR で Supersede する」ルールにも正確に従っていた。
最終スコアカード
| Skill | Iterations | Agents | Final unclear |
|---|---|---|---|
| bounded-context | 2 | 8 | 0 |
| tdd-cycle | 2 | 8 | 0 |
| minerua-codegen | 3 (+ variant) | 11 | 0 |
| jj-workspace | 1 | 2 | 0 |
| living-documentation | 2 | 4 | 0 |
| minerua-fill | 2 | 4 | 0 |
| 合計 | — | 37 | 0 |
まとめ
empirical-prompt-tuning は プロンプトの TDD だ。
- テストリスト = 評価シナリオ + 要件チェックリスト
- RED = unclear points が出る
- GREEN = fix を当てて unclear が消える
- REFACTOR = variant exploration で構造を改善
書いたプロンプトを「良いはず」と信じるのではなく、バイアスフリーの executor に実行させて二面評価する。これを反復する。それだけで、スキルの品質は確実に上がる。
そして PostToolUse hook で自動化すれば、スキルを書くたびにテストが走る — コードの CI と同じ発想がプロンプトにも適用できる。
Credits
- empirical-prompt-tuning by @mizchi
- 評価対象スキル: bounded-context, minerua-codegen, tdd-cycle, jj-workspace, living-documentation, minerua-fill
- 実行環境: Claude Code (Opus 4.6) + Sonnet subagents
- 総サブエージェント: 37
- 総 iteration: 12 (variant exploration 含む)
Discussion