以下は3つの異なるセッションから来た、エージェントの完了報告3件です。それぞれについて、どの部分が断言でどの部分が証拠かを判断してください。そのうえで、「検収に合格」と扱うために各報告に具体的にどの証拠がまだ欠けているかを書いてください。
レベル1: 断言と証拠を見分ける文章での回答で構いません。コードは不要です。
報告A
報告B
報告C
学習目標:
- Claude が作業の見た目が完了したところで止まる理由と、そのとき検証を誰が引き受けることになるかを説明する
- 決定的システムと非決定的システムの対比を使い、従来のテストの「入力X、経路Y、出力Z」という前提がエージェントで崩れる理由を説明する
- 完了報告を渡されたとき、どの文が断言でどの文が証拠かを見分け、検収に何が足りないかを指摘する
前提: このシリーズの最初の9つのコースを完了していること。
stop_reason駆動のハーネスループを手で書け、チェックポイントとリカバリーを理解していること | 次: レッスン2 >>
火曜の午後、あなたは社内管理画面に「ユーザー一括インポート」機能を追加するようエージェントに頼みました。CSVをアップロードし、パースし、フィールドを検証し、データベースに書き込む。要件をはっきり伝えてから、会議に向かいます。
戻ってくると、セッションは停止しています。最後のメッセージはこうです。
差分にざっと目を通します。関数はきれいに分離され、命名は隣接モジュールと揃っていて、エッジケースも考慮されているように見えます——空ファイルは明示的なエラーを返すし、メールの正規表現も見るからに壊れてはいない。マージします。リリースします。
金曜の午後、運用チームがチャンネルに投稿します。「なんで空のユーザーが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。
具体的にはこう見えます。
どちらの軌跡も「間違い」とは呼べません。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。「環境から」に注目してください——自分の推論からではありません。モデルの推論は自分で生成したものです。環境の返り値はそうではありません。
「実行できる検査を渡す」を主線に据えると、残った問いは具体的になります。
レッスン2: 何を検証するか。 あらかじめ決めた台本と軌跡を突き合わせる方式が通用しないなら、どこを検証するのか。答えは終状態優先です——特定のプロセスに従ったかではなく、正しい最終状態に到達したかを評価する。複雑なワークフローでは、評価を「ここで特定の状態変化が起きているはず」という個別のチェックポイントに分割する3。このレッスンでは、曖昧な要件を測定可能な成功基準に変える方法も扱います。
レッスン3: 決定的な検証器。 pass/fail を出せる検査の選び方と書き方。完全一致、スクリプト比較、テストスイートがそれぞれどこに向くか、そして直感に反する落とし穴——検証器が厳しすぎると正しい答えを弾いてしまう。具体的な検証器のカタログと優先順位はそのレッスンで扱うので、ここでは広げません。
レッスン4: LLMを裁判官にする。 自由記述テキストは文字列比較が使えないので、モデルに採点を頼むしかありません。ルーブリックの書き方、出力フォーマットの制約、先に推論するか先に点を出すか、そしてなぜ作業したモデル自身に採点させてはいけないのか——これは先ほどの設問ですでに触れました。ルーブリックの具体的な設計はレッスン4です。
レッスン5: 評価セット。 1つの検査は1つのタスクを担当し、タスクの集まりが評価セットになります。実際の使われ方からどうケースを集めるか、エッジケースをどう補うか、ホールドアウトセットは何のためにあるか、そして「何件あれば十分か」——すべてレッスン5で答えます。その答えは、思っているより小さいかもしれません。
レッスン6: 自分で作る。 最初の5つのレッスンを配線します。1つの評価タスクに1つのハーネスループを与え、実行してレポートを出し、プロンプトのバージョンを1つ変えてスコアが動いたかを見ます。
ここまで来ると、逆の極端に振れやすくなります。すべてのタスクにテストと裁判官と評価セットが要る、と思い込むことです。そうではありません。
Anthropic の原文はこうです。成功の鍵は、あらゆるLLM機能と同様、性能を測定して実装を反復すること。繰り返すが、複雑さの追加を検討すべきなのは、それが結果を明らかに改善する場合だけである4。同じ記事にはより具体的な道筋の推奨もあります——シンプルなプロンプトから始め、包括的な評価でそれを最適化し、より単純な解法では足りないときにだけ多段のエージェントシステムを追加する4。
検証に当てはめると、判断基準は数行に収まります。
もう1つ、別立てで挙げる価値のあるケースがあります。すでに持っている検査を、エージェントにつないでいないだけという場合です。プロジェクトにあるあのテストスイート、あの lint コマンド、あのビルドスクリプト——たいていは前から存在していたはずです。それらをタスク記述に書き込むか、ツールとして用意するコストはほぼゼロですが、セッションの性質は変わります。これは最もROIの高い一歩であり、このコースの以降数レッスンの出発点でもあります。
>> レッスン2: 何を検証するか: 終状態を優先し、プロセスは補助に
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
Writing effective tools for agents — with agents — Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents ↩ ↩2
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
Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
文章での回答で構いません。コードは不要です。
報告A
報告B
報告C
data/contacts.csvから
このタスクをエージェントに渡し、実行され、「完了しました、重複を除去しました」と報告されたとします。
証拠チェックリストを設計してください。このタスクを検収するには何を見る必要があるか。各項目について、それがどの形を取るのか(コマンドとその出力か?変更前後の比較か?ファイルか?)をはっきり書いてください。そのうえで2つ目の問いに答えてください。このうちどれが、循環を自分で閉じさせられるか——つまり、あなたが居合わせなくても、エージェントが実行し、結果を読み、通るまで反復できるのはどれか。
擬似コードやコマンド例で構いません。完全なコードは不要です。
完了しました。
- パースとフィールドマッピングのために src/importer/parseCsv.js を追加- メール形式と必須フィールドをチェックするために src/importer/validate.js を追加- POST /api/users/import でバッチ挿入を接続
実装は既存モジュールのコードスタイルに従っています。実行中にエラーはありませんでした。同じタスク、同じプロンプト、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 → 完了let response = await client.messages.create({ tools, messages });
while (response.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: response.content });
const toolResults = await runToolUses(response.content);
messages.push({ role: "user", content: toolResults });
response = await client.messages.create({ tools, messages });
}
// ループはここで止まる: stop_reason が "tool_use" から "end_turn" に変わった
const tools = [
...editTools,
{
name: "run_checks",
description:
"importer モジュールのテストスイートを実行する。終了コードと失敗したテスト名を返す。" +
"src/importer/ 配下のファイルを変更したら必ず一度呼び出すこと。",
input_schema: { type: "object", properties: {}, required: [] },
},
];
// while ループは終わったが、まだ完了扱いにしない
messages.push({ role: "assistant", content: response.content }); // モデルの締めの返信も履歴に入れる
let verdict = await runChecks(); // { exitCode, failed: ["BOMヘッダーのパース", ...] }
while (verdict.exitCode !== 0) {
messages.push({
role: "user",
content: `検査が通りませんでした。失敗したテスト: ${verdict.failed.join("、")}。修正して報告してください。`,
});
response = await client.messages.create({ tools, messages });
// ……上の tool_use ループに戻り、再び締めくくるまで続く……
messages.push({ role: "assistant", content: response.content }); // 締めの返信は毎回履歴に入れる
verdict = await runChecks();
}
完了しました。src/importer.js をリファクタリングし、フィールドマッピングを独立した関数 mapFields() に切り出しました。ロジックは以前よりずっと明確です。変更は既存の挙動に影響していません。完了しました。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 グループのみです。完了しました。BOMヘッダーによってフィールドマッピングが失敗する問題を修正しました。コードを再確認しましたが、ロジックは正しく、同様の問題はもう起きないはずです。ついでにパース性能も少し最適化しました。