エージェントが「完了」と言うのに機能が動かない:原因と、AIエージェントの作業を検証する方法
エージェントが「完了」と報告するのは、自分で検証できた範囲、つまりほとんどの場合ユニットテストだけを根拠に評価するからです。ユニットテストはモジュール間の接続、マイグレーション、設定、E2Eのシナリオを見ません。対策はプロンプトではなく構造です。AGENTS.md に完了の定義を書き、E2Eの実行を必須にし、「passing」の印を付けられるのは検証スクリプトだけにし、独立した評価者をクリーンなコンテキストで動かします。
なぜそうなるのか
AIエージェントを使ったことがある人なら誰でも、この状況に覚えがあるはずです。エージェントは「完了」と言ったのに、機能は動かない。エージェントは意図的に嘘をついているわけではありません。自分が検証できたことだけを報告し、それで十分だと心から信じています。報告と現実のずれは、「実行者が自分の仕事を評価する」という仕組みそのものから生まれるもので、特定のモデルの欠陥ではありません。
理由は三つあります。第一に、モデルは過信しやすい。回答の確信度が、その正確さを上回っています。第二に、エージェントは仕事をしたのと同じコンテキストで自分の仕事を評価します。すでに解決策は正しいと納得してしまっているので、「もっと客観的に」と頼んでもコンテキストは変わりません。第三に、そして最も実際的な理由として、エージェントが行える検証はしばしば部分的です。ユニットテストはすぐに緑になりますが、エンドツーエンドのシナリオは誰も実行していないことがあります。
典型的な誤った「完了」の経緯
エージェントはパスワードリセット機能の実装を任されました。データベースのスキーマを変更し、エンドポイントを追加し、メールのテンプレートをつなぎ、ユニットテストを実行します。すべて緑です。チャットには自信満々の「完了しました」が表示されます。
しかし実際には次のことが起きていました。
- マイグレーションが途中で失敗し、スキーマが不整合な状態のまま残った。
- メールのテンプレートはあるが、環境にメールサービス(SMTP)が設定されておらず、メールが送信されない。
- 「ユーザーがリンクをクリック → 新しいパスワードを入力 → ログインできる」というシナリオは一度も実行されていなかった。
これらの問題はどれもユニットテストでは見つけられません。データベースはモックに置き換えられ、メール送信もモックに置き換えられ、そもそも通しで実行されていないからです。
緑のテストが機能の動作を証明しない理由
ユニットテストは、依存関係をモックに置き換え、外部環境を切り離した状態でモジュールを検査します。これによって速度と精度は得られますが、体系的な死角も生まれます。その死角はテストの欠陥ではなく、設計そのものです。緑のユニットテストが証明するのは、個々の部品がそれぞれ単独で動くことだけで、部品同士が連携して動くことではありません。
ユニットテストが原理的に見つけられない欠陥が四つのクラスに分かれます。
| 欠陥のクラス | 例 | ユニットテストで見つけられない理由 |
|---|---|---|
| インターフェースの不一致 | ある部品は相対パスを返し、別の部品は絶対パスを要求する | 各部品は、どんな形式でも受け入れてしまうモックを使ってテストされる |
| 状態の伝播 | データベースのスキーマが変わったのに、ORMのキャッシュが古いまま | ユニットテストには実際のデータベースもキャッシュもない |
| リソースのライフサイクル | ファイルハンドルや接続が解放されず、大きなファイルでメモリがリークする | テストは小さなデータで動き、リソースが尽きる前に終わる |
| 環境と設定 | 環境変数がない、SMTPが未設定、マイグレーションが未適用 | テストは本番に近い設定でアプリケーションを起動しない |
検証の層は入れ子になっています。ユニットテストで見つかるものは統合テストでも見つかり、統合テストで見つかるものはエンドツーエンド(E2E)テストでも見つかります。各層は前の層より厳密に多くを見ています。だからこそ「完了」と宣言できる権限は、最も上の層だけにあるべきです。
問題を測る:verification gap
直す前に数えてください。エージェントが最近完了させたタスクを10件取ります。それぞれについて二つの質問に答えます。エージェントは「完了」と言ったか。独立した検証(あなたが自分でシナリオを通す、E2Eテストが緑になる)はそれを確認したか。不一致の割合があなたの verification gap です。
この数字には二つの使い道があります。第一に、「まあ動いていると思う」という議論を抜きにして、問題の規模を示せます。第二に、基準値を与えてくれます。プロセスを変更するたびに、似たタスクの集合で数字を再計算し、効果があったかどうかを確認できます。
本当の検証:三つの層
分かりやすい例えは自動車の整備工場です。車は三つの工程を通り、どれも飛ばすことはできません。
- 診断(数秒):構文、型、リンター。タイプミスや明らかな誤りを見つけます。
- 台上試験(数分):単体テストと統合テスト。アプリケーションが起動します。エンジンは回っていますが、車はまだその場にあります。
- 試乗:ユーザーが実際に行う操作どおりのE2Eシナリオを、API、ブラウザ、CLIのいずれかを通じて、実際のデータベースと設定で実行します。
順序の規則は次のとおりです。診断で警告が出ているうちは試乗に出ず、試乗を通していない車を出荷してはいけません。緑の単体テストは、三つ目の工程ではなく、二つ目の工程を通過したことを意味するだけです。
これらをまとめて一つの入口にできます。
# Makefile
check:
ruff check .
mypy app
pytest tests/unit tests/integration
e2e:
docker compose up -d db mailhog
alembic upgrade head
pytest tests/e2e
verify: check e2e
ここでのE2Eは、実際のデータベースとテスト用メールサーバーを起動し、マイグレーションを適用して、シナリオを最後まで通します。マイグレーションの失敗やメールの未設定は、顧客に届く前にすぐ表面化します。
AGENTS.md の完了の定義
「完了」が何を意味するかを、エージェント自身に決めさせてはいけません。その基準を指示ファイルに書き、検証可能な条件として表現します。
## Definition of Done(完了の定義)
次のすべてを満たしたときだけ、機能は完了とみなす。
1. make check が終了コード0で終わる
2. make e2e が終了コード0で終わり、機能のシナリオが tests/e2e にある
3. マイグレーションが新しいデータベースに適用でき、ロールバックもできる
4. 新しい環境変数が .env.example に記載されている
5. PROGRESS.md に、完了した内容と検証方法が記録されている
次のものは完了の証拠にならない:
- 「コードは書けていて、正しく見える」
- 「ユニットテストは通っている」
- 「自分のローカルでは動いている」
「完了の証拠にならないもの」を明示すると、思いのほか効果があります。エージェントは最初の緑で立ち止まらなくなります。
「passing」を付けられるのは誰か
次の段階は、「完了」の状態をエージェントの手から完全に取り上げることです。機能は一覧(例えば feature_list.json)に保存し、各機能に「振る舞い」「検証コマンド」「状態」を持たせます。機能を passing に変えられるのはスクリプトだけです。スクリプトはまず共通の make check を実行し、次にその機能固有の検証コマンドを実行し、成功した場合にのみ状態を記録します。その際、証拠としてコミットのハッシュも残します。失敗すれば機能は active のままです。「ほぼ通った」という状態は存在しません。
{
"id": "F04",
"behavior": "ユーザーがリンク付きのメールを受け取り、パスワードを変更する",
"verification": "pytest tests/e2e/test_password_reset.py",
"state": "active"
}
ここには落とし穴があります。スクリプトは、verification フィールドに書かれたものだけを検証します。完了の定義に含まれるアーキテクチャのチェックが機能の検証コマンドに入っていなければ、その穴は見えないままです。だからこそ、共通の make check には、テストだけでなく必須のチェックすべてを含めなければなりません。
独立した評価者
実行者の自己報告が、独立した事実の源になることはありません。構造的な解決策は役割を分けることです。
- 計画者は要件を、受け入れ基準付きの機能に分解します。コードは書きません。
- 生成者は一つの機能を実装し、テストを実行しますが、完了かどうかは決めません。
- 評価者は、チェックリストの基準に沿って判定を下します。
評価者は生成者の推論を見てはいけません。見ると、その自信を引き継いでしまうからです。評価者が受け取るのは、要件、差分、実行結果だけです。Claude Code では、これをサブエージェントで実現できます。サブエージェントは独自のクリーンなコンテキストを持つからです。さらに良いのは、評価者に別のモデルを使うことです。モデルが違えば死角も違うからです。「問題なさそうに見える」という判定は認められません。すべての結論は、ファイル、行番号、またはログを参照しなければなりません。
エージェントが自分で直せるエラーメッセージ
検証は、その出力が修正につながるときにだけ価値があります。「Test failed」や、中身のないAssertionErrorはエージェントにとって何の意味もありません。良いメッセージには、何が壊れているか、なぜそのルールがあるのか、どう直すかが含まれます。
ERROR arch-check: app/services/refund.py が app/db/session.py をインポートしています
WHY: サービス層はデータベースに直接アクセスせず、リポジトリ経由でのみ行う
FIX: app/repositories/refund.py の RefundRepository を使ってください
この形式によって、検査は禁止事項から手がかりへと変わります。エージェントは、あなたが関与しなくても違反を修正できます。
人による検証がまだ必要な場面
自動化は人を不要にはしませんが、人の役割を狭めます。人による検証が必要なのは、基準を形式化しにくい場面です。画面の見た目、文言、シナリオの使いやすさなどです。ただし、「ちょっと触ってみる」のではなく、受け入れ基準のチェックリストに沿って行ってください。同じコメントをエージェントに二度書いたなら、それは自動検査に変えるべきサインです。テスト、リンターのルール、make check に入れた grep ガードのいずれかにします。
E2Eテストは、実行する前からエージェントの振る舞いを変える
エンドツーエンドテストの価値は、見つけたバグだけにあるのではありません。自分の仕事が E2E シナリオで検証されると分かっていれば、エージェントはコードの書き方を変えます。自分の関数だけでなく、隣接するコンポーネントとの契約を考えるようになり、エラーの経路を扱い、アーキテクチャの境界を破らなくなります。境界を破ればその違反が実行時に表面化するからです。検証は出口のフィルターであるだけでなく、入口での制約にもなるのです。
ただし前提があります。E2E が意味を持つのは、システムの境界が明確なときだけです。構造がスパゲッティなら、E2E テストはそのスパゲッティが動くことを証明するだけで、設計意図がどこで破られたかは示しません。だから、まず層と依存関係のルールを固定し、そのあとで E2E シナリオを作ります。エージェントはリポジトリ内の既存のパターンを喜んでコピーします。悪いパターンも含めてです。だからこそ、制約は初日から導入するのが望ましいのです。
よくある間違い
- エージェントに「もっと注意深く確認して」と頼む。コンテキストは変わらないので、評価は高いままです。
- カバレッジ100%を保証だと考える。ユニットテストのカバレッジは、モジュール同士の接続部分をカバーしません。
- 機能が確認される前にリファクタリングする。順序は厳密です。正しさ → 性能 → スタイル。
- 最も壊れやすい部分をE2Eでモックする。データベース、マイグレーション、設定はE2Eで本物を動かす必要があります。外部の有料APIだけはテストサーバーに置き換えて構いません。
まとめ
エージェントが「完了」と言うのに機能が動かないとき、問題はほとんどの場合モデルではなく検証の手順にあります。verification gap を計算し、AGENTS.md に完了の定義を書き、E2E を必須にし、「完了」の判定をスクリプトに渡し、独立した評価者を加えてください。この仕組みは、ハーネスエンジニアリングの講座で実際のプロジェクトを通して詳しく解説しています。
よくある質問
エージェントに E2E テストを自分で実行させることはできますか?
はい。コマンドが AGENTS.md の完了の定義に必須項目として書かれていて、必要なツールにエージェントがアクセスできれば可能です。ただし、エージェントの記憶に頼らないよう、フックや CI で追加的に実行するほうが確実です。
E2E テストは遅いので、毎回の修正で実行すべきですか?
いいえ。リンターと型チェック、単体テストは毎回実行し、E2E は複数の層にまたがる変更のときと、機能の完了を宣言する前に実行します。
フロントエンドの成果物はどう検証すればよいですか?
ブラウザの E2E テスト(Playwright など)で実際の画面をクリックして確かめます。見た目は、チェックリストに沿った人の確認か、評価者が確認するスクリーンショットで行います。
評価者の代わりに人のコードレビューでは不十分ですか?
人のレビューは有用ですが、コストが高く、数時間にわたる自律作業には拡張できません。評価基準を持つ評価者が明らかな問題を先に取り除き、判断の分かれる問題を人が決めるのが現実的です。
このテーマはコース「Harness Engineering: инженерия надёжности AI-агентов」で実践とともに詳しく解説しています。
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента