スキルは本当に上手く動いている? Evalsでエージェントのスキルを体系的に検証する

Dev.to / 2026/4/20

💬 オピニオンDeveloper Stack & InfrastructureIdeas & Deep AnalysisModels & Research

要点

  • この記事は、エージェントの「スキル」は多くの場合、手動での試験実行だけで検証されがちであり、その方法では実利用で初めて見える“静かな”失敗パターンを見落とすと主張しています。
  • 失敗は大きく4つの経路(未トリガー、トリガーされたが未完了(アウトカム失敗)、正しい結果だが誤った手順(プロセス失敗)、完了したが品質基準を下回る)に分類できると整理しています。
  • 特に、SKILL.mdの説明が曖昧で起きるトリガー不一致は、手動テストで使う“典型的な合図”ではほぼ検出できないと強調しています。
  • 解決策として、これらの失敗マップを使って「感覚」に頼らない体系的な検証・評価(evals)システムへ逆算する方針を示しています。

あなたはどのようにスキルを最後まで検証しましたか?

スキルの文章を書き、手動で数回トリガーしてみたところ、出力は妥当に見えました。——そしてそのままリリースしました。

おそらく、それが多くの人にとっての完全な検証ワークフローでしょう。認めるのが少し恥ずかしいですが、事実です。私たちはユニットテストを書き、通常のコードではCIを回します。しかしスキルとなると、なぜか「感覚でやる」時代に逆戻りしてしまうのです。

問題は怠慢ではありません。問題は、スキルが静かに失敗するどのようにについて明確な見取り図がなく、そもそも「良い」とは何を意味するのかを表す共通の語彙もないことです。

この記事では、この両方に対処します。まず、失敗の経路を整理します。次に、その失敗マップを使って検証システムを逆算します。

スキルはどのように失敗するのか?

テスト方法の話に入る前に、「何がうまくいかない可能性があるか」を考えてみましょう。スキルの失敗は典型的に4つの経路に従い、しかも起きていることが気づかれにくい傾向があります。大きなエラーは出ず、「ちょっと違う」結果だけが残ります。

経路1:スキルが一度もトリガーされない

最も見えない失敗です。ユーザーは「コードをフォーマットして」と言ったのに、エージェントはあなたのコードフォーマット用スキルを呼び出さず、自前の知識で、もっともらしい変更をいくつか行っただけです。

根本原因は通常、SKILL.mddescriptionフィールドが曖昧すぎることです。広すぎると他のスキルと衝突し、狭すぎると正当なトリガーの幅広いケースを見逃します。厄介なのは:この失敗は手動テストではほぼ検出不可能だという点です。なぜなら、あなたは最も典型的なトリガーフレーズでテストしますが、現実のユーザーは同じ意図を千通りもの方法で表現するからです。

経路2:トリガーされたが、タスクが完了しなかった

スキルは呼び出され、ツールも実行されましたが、仕事が終わりませんでした。たとえば、作成されるはずのファイルが3つなのに2つしか作られていない。あるいは移行スクリプトが開始されたものの、途中で早期終了した。

これは結果(Outcome)失敗——最も直接的でインパクトの大きいタイプです。ユーザーはその結果を見ます。もし結果が間違っているなら、スキルは存在しないのと同じです。

経路3:正しい結果だが、間違った経路

これはもう少し見えにくいものです。最終的な出力は問題なさそうに見えますが、実行経路が誤っています。誤ったツールが呼ばれていた、手順の順序が入れ替わっていた、あるいは目的地にたどり着くまでにエージェントが長い遠回りをした。

例:正しいシーケンスが「バックアップ → 移行 → 検証」であるデータベース移行スキル。エージェントが先に移行してから後でバックアップを取った場合、出力ファイルは次回の失敗時に同じように見えるかもしれませんが、次に移行が失敗したときに使えるバックアップがありませんこれはプロセス(Process)失敗であり、結果だけで行う検証では完全に見えません。

経路4:完了したが、品質基準を下回っている

タスクは完了しました。プロセスは正しかった。ですが、生成されたコードがプロジェクトのスタイル規約に合っていない。コミットメッセージのフォーマットがチームの標準に従っていない。タスクが必要以上に500トークン使っていて、本来は100で足りた。

これはスタイルと効率(Style and Efficiency)失敗です。エラーは投げられません。技術的負債、チームの摩擦、コストの増大として、静かに積み上がっていきます。

「成功」を定義する:4つの検証ディメンション

これら4つの失敗経路は、それぞれ4つの成功基準に直接対応しています。4つすべてを定義するまで、そのスキルが何をするべきなのかを実質的に指定できていません。

ディメンション 対応する失敗 中核となる問い
結果(Outcome) タスクが完了しない やるべきことをやったか?
プロセス(Process) 誤った実行経路 正しいツールが、正しい順序で使われたか?
スタイル(Style) 品質が基準未達 出力は規約に適合しているか?
効率(Efficiency) 無駄なリソース 不要な遠回りはないか?妥当なトークン使用量か?

それぞれを検証する方法は以下のとおりです。

結果(Outcome)の検証:決定論的チェック

結果は最も定量化しやすいディメンションであり、決定論的なグレーダーで検証するのが最適です。実行ログを解析するか、ファイルシステム状態を調べて、タスクが完了したかどうかを確認します。

まず小さなテストセットを用意する

大量のテストケースは不要です。10〜20件で十分ですが、次の3種類をカバーする必要があります。

明示的トリガー:  "/use code-formatter please format this file"
暗黙的トリガー:  "this code looks messy, can you clean it up?"
ネガティブコントロール: "write me a sorting algorithm"(※フォーマッターはトリガーされないはず

ネガティブコントロールは特に重要です。「スキルが呼ばれるべきではないのに呼ばれていないか」をチェックします。過剰トリガーは、トリガーされないのと同じくらい大きな問題です。

JSON出力に対する決定論的アサーション

codex exec --json は、実行ログをJSONL形式で構造化したもので、すべてのツール呼び出しの詳細が含まれます。結果(Outcome)検証では:

import json, sys

# 実行ログを読み込む
with open("run_output.jsonl") as f:
    events = [json.loads(line) for line in f]

# 対象ファイルが作成されたかを確認する
created_files = [
    e["path"] for e in events
    if e.get("type") == "file_write"
]

expected = ["src/index.ts", "src/types.ts", "README.md"]
missing = [f for f in expected if f not in created_files]

返却形式: {"translated": "翻訳されたHTML"}if missing:
    print(f"❌ FAIL: 作成されなかったファイルは以下です: {missing}")
    sys.exit(1)
else:
    print("✅ PASS: 期待されたすべてのファイルが作成されました")

ここでの利点は次のとおりです: これらのチェックは決定論的です — モデルの判断は介在せず、結果は安定して再現可能です。アウトカム(結果)レベルのEvalsはこのまま維持してください。主観的な評価は、後でルーブリックによる採点のために取っておきます。

プロセスの検証: ツール呼び出しシーケンスの確認

アウトカムのチェックは「ジョブは完了したか?」を教えてくれます。プロセスのチェックは「どのように実行したか?」を教えてくれます。

実行順序の要件が定義されているスキルでは、ツール呼び出しの順序を検証する必要があります:

# 発生したツール呼び出しを順番にすべて抽出
tool_calls = [
    e["tool"] for e in events
    if e.get("type") == "tool_use"
]

# 期待する呼び出しシーケンスを定義
expected_sequence = ["db_backup", "db_migrate", "db_verify"]

# 期待されたものが部分列として現れているかを確認(他のツールが途中に入っていてもよい)
def is_subsequence(expected, actual):
    it = iter(actual)
    return all(step in it for step in expected)

if not is_subsequence(expected_sequence, tool_calls):
    print(f"❌ FAIL: ツール呼び出しのシーケンスが期待と一致していません")
    print(f"   期待されたものを含む必要があります: {expected_sequence}")
    print(f"   実際の呼び出し: {tool_calls}")
    sys.exit(1)
else:
    print("✅ PASS: ツール呼び出しのシーケンスは正しいです")

プロセス検証には、さらに踏み込んだ用途があります: コマンドのスラッシング(thrashing)の検出 — エージェントが同じ操作を繰り返し再試行し、行ったり来たりすることです。これは通常、スキルの指示があいまいで、エージェントが迷走していることを示します。連続して繰り返される呼び出し回数を数えることで検出できます:

from itertools import groupby

for tool, group in groupby(tool_calls):
    count = sum(1 for _ in group)
    if count > 3:
        print(f"⚠️ WARNING: '{tool}'{count} 回連続で呼び出されました — 可能性としてスラッシング")

検証するスタイルと効率: ルーブリックに基づくモデル採点

アウトカムとプロセスは事実ベースのチェックです — 合格か不合格か、白か黒か。スタイルと効率は定性的な判断です: コードのスタイルは適切か? コミットメッセージの形式は正しいか? エージェントは不必要に遠回りしていないか? これらには単一の正解がありません。

ここでツールを切り替えます: 別のモデルに出力を採点させる — ただし明確なルーブリックを与え、--output-schema を使って構造化されたJSONレスポンスを強制し、実行ごとのスコアを比較可能にします。

ルーブリックを定義する

返却形式: {"translated": "翻訳されたHTML"}
# rubric.yaml
criteria:
  - name: code_style
    description: "生成された コードは プロジェクトの ESLint ルールに 準拠していますか?
    scale: [1, 5]
    anchor_1: "完全に 準拠していない、 多数の 違反"
    anchor_5: "完全に 準拠している、 違反が ゼロ"

  - name: commit_format
    description: "コミット メッセージは Conventional Commits の仕様に 従っていますか?
    scale: [1, 5]
    anchor_1: "形式が 完全に間違っている"
    anchor_5: "完全に正しい タイプ、 スコープ、 説明がすべて適切"

  - name: efficiency
    description: "Agent には、明らかな冗長な 手順や不要な ツール呼び出しがありましたか?
    scale: [1, 5]
    anchor_1: "冗長性が多く、 実行が混乱している"
    anchor_5: "クリーンで 効率的な 実行パス"

構造化された出力を --output-schema で強制する

import subprocess, json

output_schema = {
    "type": "object",
    "properties": {
        "code_style":    {"type": "integer", "minimum": 1, "maximum": 5},
        "commit_format": {"type": "integer", "minimum": 1, "maximum": 5},
        "efficiency":    {"type": "integer", "minimum": 1, "maximum": 5},
        "reasoning":     {"type": "string"}
    },
    "

返却形式: {"translated": "翻訳されたHTML"}required": ["code_style", "commit_format", "efficiency", "reasoning"]
}

result = subprocess.run(
    ["codex", "exec", "--output-schema", json.dumps(output_schema),
     "--", "Evaluate the following run output for code style compliance..."],
    capture_output=True, text=True
)

scores = json.loads(result.stdout)
print(f"Code style:    {scores['code_style']}/5")
print(f"Commit format: {scores['commit_format']}/5")
print(f"Efficiency:    {scores['efficiency']}/5")
print(f"Reasoning:     {scores['reasoning']}")

構造化された出力の中核的な価値は、バージョンをまたいだ比較可能性です。Skill の指示文の1行を調整し、Eval を再実行すると、スタイルスコアが 3.2 から 4.1 に上がりました。これは信頼できる改善のシグナルです — 「なんとなく今の方が良さそう」ではありません。

Progressive Stacking: Let Your Evals Grow with Your Skill

最初から4つの次元すべてを構築する必要はありません。Skill の検証は、Skill 自体と同じペースで反復していくべきです。

フェーズ1(Skill が書きたて):手動テストを実行し、基本的な Outcome が正しい見た目になっていることを確認します。

フェーズ2(共有準備完了):決定論的な Outcome チェックを追加し、テストケースを10件作ります。

フェーズ3(チームが使い始める):Process のシーケンス検証を追加し、Style のルーブリックによるスコアリングを追加します。

フェーズ4(本番のクリティカルパス):コマンドのスラッシング検出、トークン使用量の監視、バリデーションの構築、本番稼働時のスモークテストを追加します。

この段階的なアプローチには重要な利点が1つあります。Skill が最もシンプルな段階で Eval の習慣を作りつつ、フルのシステムを作るのを待ってブロックされることなく進められるのです。Outcome チェックが2つある Skill は、まったくない Skill よりも明確に安全性が高くなります。

Eval のテストスイートが成熟してきたら、それを CI パイプラインに組み込み、Skill の変更があるたびに自動実行されるようにします。そうなれば、すべての Skill 改善を確かな自信を持って進められます — 「たぶん大丈夫」という賭けではありません。

Summary

冒頭の問いに戻りましょう:あなたの Skill は本当に良いのでしょうか?

今、答えるための枠組みがあります:

知りたいことは… これを使う
タスクは完了した? 決定論的なチェック(JSONL をパースし、ファイル/状態を検証する)
手順は正しかった? ツール呼び出しのシーケンス検証 + スラッシング検出
品質は良かった? ルーブリックモデルによるスコアリング(構造化された JSON 出力)
無駄なものはあった? トークン使用量の追跡 + 重複ステップの検出

良い Evals は2つのことを行います。回帰を明確にする — どの変更によってスコアが下がったのかがはっきり分かること。そして 失敗を説明可能にする — 「何かおかしい」ではなく「3手目でツール呼び出しのシーケンスが間違っていた」になることです。

それが、すべての変更を疑いながら進めることなく、Skill を改善し続けるための自信をあなたにもたらします。

出典:OpenAI デベロッパーブログのコアとなる手法 — Testing Agent Skills Systematically with Evals.

読んでくれてありがとうございます — テクノロジーが提供してくれるものを楽しみましょう!

私が共有するすべてのリソースについては、個人のホームページへお越しくださいHomepage