Agent Mentor Learn
検証と品質保証: 「正しそう」をすり抜けさせない · 第 1 回 / 全 6 回

レッスン1: 「完了に見える」は「完了している」ではない

学習目標:

  • Claude が作業の見た目が完了したところで止まる理由と、そのとき検証を誰が引き受けることになるかを説明する
  • 決定的システムと非決定的システムの対比を使い、従来のテストの「入力X、経路Y、出力Z」という前提がエージェントで崩れる理由を説明する
  • 完了報告を渡されたとき、どの文が断言でどの文が証拠かを見分け、検収に何が足りないかを指摘する

前提: このシリーズの最初の9つのコースを完了していること。stop_reason 駆動のハーネスループを手で書け、チェックポイントとリカバリーを理解していること | 次: レッスン2 >>

火曜にリリースして、金曜に壊れる

火曜の午後、あなたは社内管理画面に「ユーザー一括インポート」機能を追加するようエージェントに頼みました。CSVをアップロードし、パースし、フィールドを検証し、データベースに書き込む。要件をはっきり伝えてから、会議に向かいます。

戻ってくると、セッションは停止しています。最後のメッセージはこうです。

text
完了しました。
- パースとフィールドマッピングのために src/importer/parseCsv.js を追加- メール形式と必須フィールドをチェックするために src/importer/validate.js を追加- POST /api/users/import でバッチ挿入を接続
実装は既存モジュールのコードスタイルに従っています。実行中にエラーはありませんでした。

差分にざっと目を通します。関数はきれいに分離され、命名は隣接モジュールと揃っていて、エッジケースも考慮されているように見えます——空ファイルは明示的なエラーを返すし、メールの正規表現も見るからに壊れてはいない。マージします。リリースします。

金曜の午後、運用チームがチャンネルに投稿します。「なんで空のユーザーが400件もインポートされてるの?」

原因は単純です。運用チームはそのCSVをExcelから「名前を付けて保存」で作りました。ExcelはUTF-8ファイルの先頭にBOM——目に見えない3バイト ——を付けたがります。その結果、最初の列名は email ではなく email としてパースされ、フィールドマッピング全体が空振りし、すべての行が「全フィールドが undefined」になりました。あの検証層はどうしたのか?あれは「メール形式が正しいか」をチェックしていましたが、undefined は別の分岐に落ちて「この列は入力されていない」として扱われ、通過してしまったのです。

ここで手を抜いた人は誰もいません。エージェントは動くコードを書きました。自分で生成したCSVで自分でテストもしました——もちろん自分のCSVにBOMは付きません。あなたが差分をレビューしたとき、見ていたのは「このコードは正しく書かれているか」であって、「このコードが現実の入力とぶつかったら何が起きるか」ではありませんでした。双方とも最善を尽くしました。それでもギャップは生まれました。

問題はそれが止まった瞬間にあります。エージェントが止まったとき、手元にあったのは「書いた、一度読み返した、問題なさそう」でした。「完了したことを確認した」で止まったのではありません。「完了に見える」で止まったのです。そして会話履歴からは、その違いが判別できません。

それは「完了に見える」ところで止まる

Claude Code のドキュメントははっきりこう述べています。Claude は作業が完了に見えたところで止まる。自分で実行できる検査がなければ「完了に見える」が唯一手に入る信号であり、あなた自身が検証ループになる——すべてのミスがあなたに気づかれるのを待つことになる1

この一文は、単語ごとに二度読む価値があります。「Claude はときどき手を抜く」でも「モデルの能力がまだ足りない」でもありません。これは構造的な事実を述べています。パイプライン全体のどこにも客観的な結果を出せるものがなければ、「完了に見える」がこのシステムに存在する唯一の信号になる。 モデルはその信号だけで判断するしかありません。他に何も持っていないのです。

同じドキュメントはこの現象に名前を与えています。the trust-then-verify gap——Claude はもっともらしく見えるがエッジケースを処理しない実装を生成する1。平たく言えば、先に信頼し(コードは良さそうに見える)、検証は起きないか、遅すぎるタイミングで起きる(金曜の午後、運用チャンネルで)。上のBOMの例は、このギャップの標準形です。コードが間違っていたのではなく、「Excelから書き出したファイルだったらどうなる?」と誰も問わなかったのです。

ここには見落としやすい第二の層があります。ドキュメントが提示する対策はこう締めくくられています。検証できないなら、リリースしない1。強調点は「検証する」ではなく「リリースしない」の側にあります。これは、どうしても検証できないものが存在することを認めているのです。検証できないとき、正しい一手は「今回だけは勘を信じる」ではありません。スコープを狭める、要件を変える、あるいはリリースを見送る、です。

断言と証拠: 何が違うのか

あの完了メッセージに戻りましょう。一文ずつに分解して、それぞれに同じ問いを投げます。コードを読まずに、示された情報だけでこの文を確認できるか?

  • src/importer/parseCsv.js を追加」——確認できます。ファイルが存在するかどうかは一目で確かめられます。これは証拠です(もっとも弱い種類ではありますが)。
  • 「実装は既存モジュールのコードスタイルに従っています」——確認できません。これはモデルの美的判断です。断言です。
  • 「実行中にエラーはありませんでした」——証拠に聞こえますが、実際には断言です。これが言っているのは、呼び出したツールが例外を投げなかったということであって、出力が正しいということではありません。すべてのツールが成功を返しつつ結果が完全に間違っている——十分にありえます。
  • 「メール形式と必須フィールドをチェック」——確認できません。これは意図を述べていて、挙動を述べていません。その正規表現が実際に何を許可し何を弾くのか?この文は何も語っていません。

境界線はどこにあるのか。証拠とは、第二の人物がまったく同じやり方で再実行できるものです。コマンドとその生の出力、終了コード、失敗したテスト名の一覧、スクリーンショット、変更前後の数値比較。断言とは、信じるか信じないかを選ぶしかないものです。「ロジックは正しい」「問題ないはず」「最適化済み」「もう起きません」。

公式ドキュメントはまさにこの線を引いています。成功を主張するのではなく、Claude に証拠を示させる——テスト出力、実行したコマンドとその返り値、あるいは結果のスクリーンショット。証拠をレビューするほうが自分で検証をやり直すより速く、見ていなかったセッションでも機能する1

最後の半文が肝です。ずっと見ていたのなら、「断言か証拠か」の区別はさほど役に立ちません——自分の目で見たのですから。しかし目を離した瞬間、会話履歴に残るのはテキストだけになります。そしてテキストの中では、断言は証拠と同じくらい自信ありげに見えるのです。

なぜエージェントで特にこの問題が起きるのか

「正しく見えるが間違っている」は従来のソフトウェアでも起きます。なぜエージェントには専用のレッスンが要るのでしょうか。

従来のテストが、エージェントには成り立たない前提の上に立っているからです。

定義から始めます。コンピューティングにおいて、決定的システムは同一の入力に対して毎回同じ出力を生成しますが、非決定的システム——エージェントのような——は同じ開始条件でも異なる応答を生成しうる2。これは「バグがあるから不安定」ではありません。そういう仕組みなのです。プロンプトを何ひとつ変えなくても、2回の実行で下される判断が一致する保証はありません3

そのため従来の評価の前提は崩れます。従来の評価はしばしば、AIが毎回同じ手順を踏むと仮定します。入力Xが与えられたら、システムは経路Yをたどって出力Zを生成するはずだ、と3。マルチエージェントシステムはそうは動きません。出発点が同一でも、エージェントは目標に到達するまでにまったく異なる有効な経路を取りうる——あるエージェントは3つのソースを検索し、別のエージェントは10検索するかもしれないし、同じ答えを見つけるのに異なるツールを使うかもしれません3

具体的にはこう見えます。

text
同じタスク、同じプロンプト、2回の実行
実行1: read_file(schema.sql) → grep("user_id") → edit(models/user.js)        → run_tests → 完了
実行2: list_dir(src/) → read_file(models/user.js) → read_file(models/order.js)        → edit(models/user.js) → edit(models/order.js) → run_tests        → run_tests → 完了

どちらの軌跡も「間違い」とは呼べません。2回目の実行はファイルを1つ余分に読み、1箇所余分に修正し、テストを2回走らせました——遠回りしたのかもしれないし、1回目が見逃した結合を捕まえたのかもしれません。「まず schema.sql を読まなければならない」というアサーションを書けば、2回目の実行は失格になります——しかし2回目のほうが良い仕事をしていた可能性があるのです。

あらかじめ決めた台本と軌跡を突き合わせる方式はここでは通用しません。正しい手順が何かを常に把握しているわけではないため、あらかじめ規定した「正しい」手順にエージェントが従ったかどうかを確認するだけでは、たいてい済まないのです3

さらにもう一層あります。エージェントシステムのエラーは複利で膨らむのです。従来のソフトウェアでは軽微なバグでも、エージェントにぶつかるとタスク全体を脱線させうる——1ステップの失敗がエージェントをまったく別の軌跡の探索に向かわせ、予測不能な結果につながる3。これは従来のプログラムの「1つの関数が悪い値を返し、それが上に伝播する」とは違います。エージェントは悪い結果を受け取り、その悪い結果を土台に新しい判断を下すのです。ファイルを読み違えれば「このモジュールは存在しない」と結論して新しく作るかもしれず、その後はその新しいモジュールを前提に作業を続けます。最終出力を見る頃には、エラーはもう元の場所にありません。別のものへと育っているのです。

Anthropic自身の結論もここに着地します。エージェントの自律的な性質はコストの増大と、エラーが複利で膨らむ可能性をもたらす。サンドボックス環境での広範なテストと、適切なガードレールを推奨する4。そしてもう一つの直接的な一文——LLMは多数のターンにわたって動作しうるため、その意思決定にある程度の信頼を置かなければならない4

「ある程度の信頼」という言い回しに注目してください。「信頼しなければならない」とは言っていません。この信頼はどこかから来なければならない、と言っているのです。そして信頼の出どころは2つしかありません。自分で見ていた(この場合エージェントはあなたの時間を何も節約していません)か、何かが代わりに見ていてくれたか。このコース全体は、後者についての話です。

出口: 自分で実行できる検査を渡す

ここまでの積み上げは一文に着地します。Claude に自分で実行できる検査を渡す——テスト、ビルド、比較用のスクリーンショット。それが、見張っているセッションと、席を外せるセッションの違いを生みます1

その違いはどこから生まれるのか。pass か fail を出すものを Claude に渡せば、ループは自分で閉じます。Claude が作業し、検査を実行し、結果を読み、検査が通るまで反復する1

この一文は、本シリーズのコース7で扱ったハーネスループに対応づけられます。まず、いまのループがどこで止まるかを見てください。

end_turn は何を意味するのか。このターンで話し終えたとモデルが考えている、という意味です。それだけです。 作業が正しいことを意味しませんし、応答が完結していることすら保証しません——このループは tool_use しか認識しないので、stop_reason が他の何かになれば抜けます。max_tokens で文の途中で出力が切られた場合も含めて、です。終了条件のどこにも「出力の品質」に関わるものはありません。

では検査を組み込むとはどういうことか。有効な位置は2つあります。

位置その1、検査をエージェントが呼べるツールにして、ループの内側で走らせる。

位置その2、ループが抜けた後にゲートを置く。自己申告を信じず、自分で走らせる。

コード自体に仕掛けはありません。肝心なのは終了条件の持ち主が変わったことです。「モデルがもうツールを呼びたくないと言った」から「決定的なコードが0を返した」へ。前者はモデルの自己評価です。後者は違います。

では「検査」は何でありうるのか。公式ドキュメントが示す範囲は思っているより広いです。検査とは、Claude が会話の中で読み取れる信号を返すもの全般——テストスイート、ビルドの終了コード、linter、出力を fixture と diff するスクリプト、あるいはデザインと比較するブラウザのスクリーンショット1

「fixture」を説明しておきます。事前に保存しておいた「標準解答ファイル」のことで、実行後に出力をそれと比較し、1文字も違ってはいけない、というものです。素朴に聞こえますが、「出力フォーマットが安定していなければならない」種類のタスクでは、これが最も手軽で最も信頼できる検査の形です。

この考え方は、エージェントの実行に関する Anthropic の推奨と一致しています。"During execution, it's crucial for the agents to gain “ground truth” from the environment at each step (such as tool call results or code execution) to assess its progress."(実行中、エージェントが各ステップで環境から「ground truth」——ツール呼び出しの結果やコード実行の結果など——を得て自らの進捗を評価することが決定的に重要である)4。「環境から」に注目してください——自分の推論からではありません。モデルの推論は自分で生成したものです。環境の返り値はそうではありません。

残り5つのレッスンで何を解決するか

「実行できる検査を渡す」を主線に据えると、残った問いは具体的になります。

レッスン2: 何を検証するか。 あらかじめ決めた台本と軌跡を突き合わせる方式が通用しないなら、どこを検証するのか。答えは終状態優先です——特定のプロセスに従ったかではなく、正しい最終状態に到達したかを評価する。複雑なワークフローでは、評価を「ここで特定の状態変化が起きているはず」という個別のチェックポイントに分割する3。このレッスンでは、曖昧な要件を測定可能な成功基準に変える方法も扱います。

レッスン3: 決定的な検証器。 pass/fail を出せる検査の選び方と書き方。完全一致、スクリプト比較、テストスイートがそれぞれどこに向くか、そして直感に反する落とし穴——検証器が厳しすぎると正しい答えを弾いてしまう。具体的な検証器のカタログと優先順位はそのレッスンで扱うので、ここでは広げません。

レッスン4: LLMを裁判官にする。 自由記述テキストは文字列比較が使えないので、モデルに採点を頼むしかありません。ルーブリックの書き方、出力フォーマットの制約、先に推論するか先に点を出すか、そしてなぜ作業したモデル自身に採点させてはいけないのか——これは先ほどの設問ですでに触れました。ルーブリックの具体的な設計はレッスン4です。

レッスン5: 評価セット。 1つの検査は1つのタスクを担当し、タスクの集まりが評価セットになります。実際の使われ方からどうケースを集めるか、エッジケースをどう補うか、ホールドアウトセットは何のためにあるか、そして「何件あれば十分か」——すべてレッスン5で答えます。その答えは、思っているより小さいかもしれません。

レッスン6: 自分で作る。 最初の5つのレッスンを配線します。1つの評価タスクに1つのハーネスループを与え、実行してレポートを出し、プロンプトのバージョンを1つ変えてスコアが動いたかを見ます。

釣り合い: 何もかもを検収プロセスで包まない

ここまで来ると、逆の極端に振れやすくなります。すべてのタスクにテストと裁判官と評価セットが要る、と思い込むことです。そうではありません。

Anthropic の原文はこうです。成功の鍵は、あらゆるLLM機能と同様、性能を測定して実装を反復すること。繰り返すが、複雑さの追加を検討すべきなのは、それが結果を明らかに改善する場合だけである4。同じ記事にはより具体的な道筋の推奨もあります——シンプルなプロンプトから始め、包括的な評価でそれを最適化し、より単純な解法では足りないときにだけ多段のエージェントシステムを追加する4

検証に当てはめると、判断基準は数行に収まります。

  • このタスクは繰り返し実行されるか? 使い捨てスクリプト、その場限りのデータ処理、自分で見張るつもりの3分の仕事——検収の仕組みを整えるのは差し引きマイナスです。繰り返し実行されるもの、他人が変更するもの、あなたが席にいない間に走るもの——それは割に合います。
  • エラーのコストを誰が負うか? タイプミスを直し間違えたら、自分で戻して終わりです。課金ロジックを壊したら、経理がコストを負います。コストが下流であるほど、そして巻き戻しが難しいほど、前段でゲートを置くべきです。
  • いまそれの検証にどれだけ時間を使っているか? 毎回手作業で3つのページを開いて突き合わせているなら、その3ページ比較をスクリプト化することこそ、最も自動化に値します——あなたはすでにこのコストを払っていて、気づいていないだけです。

もう1つ、別立てで挙げる価値のあるケースがあります。すでに持っている検査を、エージェントにつないでいないだけという場合です。プロジェクトにあるあのテストスイート、あの lint コマンド、あのビルドスクリプト——たいていは前から存在していたはずです。それらをタスク記述に書き込むか、ツールとして用意するコストはほぼゼロですが、セッションの性質は変わります。これは最もROIの高い一歩であり、このコースの以降数レッスンの出発点でもあります。

💻 演習

まとめ

  • Claude は作業が完了に見えるところで止まります。自分で実行できる検査がなければ「完了に見える」が唯一手に入る信号であり、あなた自身が検証ループになります——すべてのミスがあなたに気づかれるのを待つことになります1
  • 公式ドキュメントはこのギャップに名前を与えています。the trust-then-verify gap——Claude はもっともらしく見えるがエッジケースを処理しない実装を生成する。対になる対策の後半も同じくらい重要です。検証できないなら、リリースしない1
  • 断言と証拠の境界線は「第二の人物がまったく同じやり方で再実行できるか」です。成功の主張ではなく、Claude に証拠を示させましょう——テスト出力、実行したコマンドとその返り値、結果のスクリーンショット。証拠をレビューするほうが自分で検証をやり直すより速く、見ていなかったセッションでも機能します1
  • エージェントは非決定的システムです。同じ開始条件でも異なる応答を生成しうるし2、同じプロンプトでも実行ごとの判断が一致する保証はありません3。そのため従来の評価の前提「入力Xを与え、経路Yをたどり、出力Zを得る」は成り立ちません3——出発点が同一でも、まったく異なる、しかし有効な経路が生まれうるのです3
  • エージェントシステムのエラーは複利で膨らみます。1ステップの失敗がエージェントをまったく別の軌跡の探索に向かわせ、予測不能な結果につながります3。自律性はコストの増大とエラーが複利で膨らむ可能性をもたらすため、サンドボックス環境での広範なテストとガードレールが推奨されます4
  • 出口は、実行できる検査を渡すことです。pass か fail を出すものがあれば、ループは自分で閉じます。作業し、検査を実行し、結果を読み、通るまで反復する1。検査はテストスイート、ビルドの終了コード、linter、出力を fixture と diff するスクリプト、デザインと比較するブラウザのスクリーンショットのいずれでもありえます1
  • 実行中は、エージェントが各ステップで環境から「ground truth」(ツールの結果、コード実行の結果)を得て進捗を評価できるようにします。自分の推論からではありません4
  • LLMは多数のターンにわたって動作しうるため、その意思決定にある程度の信頼を置かなければなりません4——ただしその信頼はどこかから来なければなりません。
  • 何もかもを完全な検収の仕組みで包まないこと。複雑さの追加を検討すべきなのは、それが結果を明らかに改善する場合だけです4。まずは、このタスクが繰り返し実行されるか、エラーのコストを誰が負うか、いま手作業の検証にどれだけ時間を使っているかを確認してください。
  • 最もROIの高い一歩はたいていこれです。プロジェクトにあるあのテストスイート、あの lint コマンド、あのビルドスクリプトはすでに存在していて、エージェントにつないでいないだけ、というもの。

>> レッスン2: 何を検証するか: 終状態を優先し、プロセスは補助に

Footnotes

  1. Best practices for Claude Code — Claude Code official documentation — https://code.claude.com/docs/en/best-practices 2 3 4 5 6 7 8 9 10 11 12

  2. Writing effective tools for agents — with agents — Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents 2

  3. How we built our multi-agent research system — Anthropic Engineering — https://www.anthropic.com/engineering/multi-agent-research-system 2 3 4 5 6 7 8 9 10

  4. Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents 2 3 4 5 6 7 8 9

練習

01

以下は3つの異なるセッションから来た、エージェントの完了報告3件です。それぞれについて、どの部分が断言でどの部分が証拠かを判断してください。そのうえで、「検収に合格」と扱うために各報告に具体的にどの証拠がまだ欠けているかを書いてください。

レベル1: 断言と証拠を見分ける

文章での回答で構いません。コードは不要です。

報告A

text
完了しました。src/importer.js をリファクタリングし、フィールドマッピングを独立した関数 mapFields() に切り出しました。ロジックは以前よりずっと明確です。変更は既存の挙動に影響していません。

報告B

text
完了しました。npm test -- importer を実行した出力は次のとおりです:
  PASS  test/importer.test.js  Tests: 14 passed, 14 total  Time:  1.842 s
追加した3件のテストケースは "handles BOM in header"、"rejects duplicate email"、"errors on missing column" です。テストスイート全体は実行しておらず、このimporter グループのみです。

報告C

text
完了しました。BOMヘッダーによってフィールドマッピングが失敗する問題を修正しました。コードを再確認しましたが、ロジックは正しく、同様の問題はもう起きないはずです。ついでにパース性能も少し最適化しました。
完了基準 · ローカルでチェック
02

タスクはこれです。

レベル2: 小さなタスクの証拠チェックリストを設計する

data/contacts.csv から email 列を基準に重複行を除去し、各メールアドレスについて最初に出現した行だけを残して、結果を同じファイルに書き戻すスクリプトを書く。

このタスクをエージェントに渡し、実行され、「完了しました、重複を除去しました」と報告されたとします。

証拠チェックリストを設計してください。このタスクを検収するには何を見る必要があるか。各項目について、それがどの形を取るのか(コマンドとその出力か?変更前後の比較か?ファイルか?)をはっきり書いてください。そのうえで2つ目の問いに答えてください。このうちどれが、循環を自分で閉じさせられるか——つまり、あなたが居合わせなくても、エージェントが実行し、結果を読み、通るまで反復できるのはどれか。

擬似コードやコマンド例で構いません。完全なコードは不要です。

完了基準 · ローカルでチェック