参考にしたAnthropicのエンジニアリングブログ:
ハーネス設計についての記事で、AIコーディングエージェントを単体で使うのではなく複数のエージェントに役割を分担させることで、単独エージェントでは解決できない問題に対処できるという内容だった。
記事の中で特に気になったのが自己評価の問題。エージェントは自分が作ったものを評価させると必ず甘くなるという指摘で、Evaluatorを別エージェントとして分離して懐疑的に振る舞うよう調整する方が、Generatorに自分のコードを批判させようとするよりずっと効きが良いと書かれていた。
実装するエージェント(Generator)と、それをレビューするエージェント(Evaluator)に分けることで自己評価の甘さを回避できるはず、と思って試してみた。最終的に検証と、これをClaude CodeのSkillとして実装することをゴールに置いた。
実験に使ったリポジトリ:https://github.com/naokami3/harness_review_example
実験の設計
単独エージェントによるレビューとハーネスを使ったレビューの差を比較するため、サンプルコードを用意した。
対象はRuby on Rails製のタスク管理REST API(Docker対応)。JWT認証・タスクのCRUD・論理削除・ページネーションを実装した規模で、意図的にバグを仕込んである。仕込んだバグには「表面的なもの(コードを読めばわかる)」と「深いもの(実際に動かさないと気づきにくい)」を混在させた。
サンプルコードはClaude Codeに生成させた。バグを「意図的に仕込んでください」とは指示していない。設計上バグが生まれやすい条件(権限チェックなし・JWT検証スキップなど)を仕様として指示した。
実験の手順:
- 単独レビュー:「このコードをレビューしてください」と普通に依頼
- ハーネスレビュー:別セッションのEvaluatorに独立したレビューを依頼
- 結果を照合
使ったプロンプトは記事の末尾に全文を載せた。
ハーネスレビューは必ず別セッションで実行すること。同じセッションで続けてしまうと、EvaluatorがGeneratorのコードを書いたときの文脈を引き継いでしまい、評価が甘くなる。Claude Codeを新しいウィンドウで起動するか、セッションをリセットした上でEvaluatorプロンプトを渡すこと。
仕込んだバグ
レビュー結果の照合前に、何を仕込んだかを先に書いておく。
深い問題(動かさないと気づきにくいもの)
JWT署名・有効期限の検証スキップ
JWT.decode(token, secret_key, false) を使っている。第3引数が false のため署名検証と有効期限の検証が両方スキップされる。偽造したトークンでも期限切れのトークンでも認証が通る。
IDOR(他ユーザーのタスクを操作できる)
タスクの単件取得・更新・削除は Task.find(params[:id]) だけでフィルタしている。current_user によるスコープがないため、タスクIDを知っていれば他ユーザーのタスクを自由に操作できる。
論理削除が一覧に反映されない
削除は deleted_at にタイムスタンプをセットする論理削除だが、一覧取得のクエリに where(deleted_at: nil) が入っていない。削除済みのタスクが一覧に出続ける。
ページネーションのオフセットずれ
offset = page * per_page で計算している。page が1始まりという仕様なので page=1 のとき offset=20 になり、最初の20件が取得できない。
表面的な問題
priority と status のバリデーションがなく、どんな値でも保存できる。テストは正常系のみで権限・異常系のテストがない。
単独レビューの結果
プロンプト:
以下のRails APIのコードをレビューしてください。タスク管理APIです。JWT認証、タスクのCRUD、論理削除、ページネーションを実装しています。セキュリティ・バグ・設計の問題点があれば指摘してください。
結果のサマリー:Critical 2件(JWT署名未検証、IDOR脆弱性)、High 2件(シークレットキーのフォールバック、論理削除の不備)、Medium 3件、Low 3件。
主要なバグはほぼ全て拾えていた。ただし全ての指摘が「〜のリスクがある」「〜できる可能性がある」という形で、コードを読んで推測した結果だった。実際にAPIを叩いた形跡はない。
ハーネスレビューの結果
EvaluatorはPhase 1でプロジェクトの種類・起動コマンド・エンドポイント一覧・認証方式をコードから自律的に把握したうえで、Phase 2で実際にDockerを起動してAPIを叩きながら確認した。
指摘の中からいくつかを引用する。
指摘3: 他ユーザーのタスクを取得・更新・削除できる [Critical]
ユーザーA(id=4)でタスク(id=30)を作成後、ユーザーB(id=5)のトークンで以下を実行した:GET /tasks/30 → 200、ユーザーAのタスク内容が全て返った。PATCH /tasks/30 → 200、タイトルが「HACKED BY USER B」に書き換わった。DELETE /tasks/30 → 200、deleted_atがセットされた。
指摘5: ページネーションが1ページ目を読み飛ばす [High]
27件のタスクを作成し、GET /tasks?page=1 → 7件返った(offset=20、最初の20件がスキップされた)。GET /tasks?page=2 → 0件。
単独レビューが「0始まりで混乱する」(Medium)とした問題が、実際に試すと「1ページ目が完全に消失する」(High)だったことがわかった。
比較表
全文はリポジトリの solo_review.md と harness_review.md で確認できる。
| バグ | 単独レビュー | ハーネスレビュー |
|---|---|---|
| JWT署名検証が無効 | Critical(推測) | Critical(偽署名トークンで実証) |
| JWT有効期限検証が無効 | 署名問題に含む | Critical(個別実証) |
| 他ユーザーのタスクが操作できる | Critical(推測) | Critical(タイトルを書き換えて実証) |
| 論理削除が一覧に反映されない | High | High(by_statusでも追加発見) |
| ページネーションのズレ | Medium「混乱する」 | High「1ページ目が消失」実証 |
| テストが正常系のみ | 見逃し | High(未テストと実際のバグを対応表で指摘) |
| スタックトレースが露出 | 見逃し | Medium(404を叩いて初めて発見) |
分かったこと
単独エージェントも主要なバグはほぼ拾えていた。正直予想より良い結果だった。
ただし差は2つの軸で出た。
重要度の精度:ページネーションの問題を単独レビューは「0始まりで混乱する」(Medium)とした。実際に動かすと「1ページ目が完全に消失する」(High)だった。コードを読んだだけでは影響度を過小評価する。
見逃しの内容:単独レビューが見逃したのはテストカバレッジとスタックトレース露出の2件。どちらも実際に動かすことで初めて表面化する問題だった。
単独レビューの全ての指摘が「〜のリスクがある」という形だったのに対して、ハーネスの指摘は全て「〜を試したら〜になった」という形だった。PRのレビューコメントとしてどちらが説得力があるかは明らかで、ここが実用上一番大きな差だと思う。
Skillとしての実装
今回使ったEvaluatorプロンプトはRails APIに特化した内容ではなく、どんなプロジェクトでも使える設計にした。Phase 1でEvaluator自身がプロジェクトの種類・起動方法・エンドポイント・認証方式をコードから把握する。
このEvaluatorをClaude CodeのSkillとして実装した。ai_dotfilesリポジトリのplugins配下に code-review-harness として追加している。
使ったプロンプト
サンプルコード生成(Claude Code)
単独レビュー依頼(Claude Code)
以下のRails APIのコードをレビューしてください。タスク管理APIです。JWT認証、タスクのCRUD、論理削除、ページネーションを実装しています。セキュリティ・バグ・設計の問題点があれば指摘してください。
ハーネス用Evaluatorプロンプト(Claude Code・別セッション)
感想
一番効いたのは「懐疑的に振る舞え」という姿勢の指示だったかもしれない。
また、Anthropicのブログに書いてあった通り、EvaluatorにGeneratorとは独立した第三者として振る舞うよう明示することで、自己評価の甘さを回避できた。
もう一つ、「〜の可能性がある」は使うなという制約が効いた。コードを読んだだけで書けるレビューより、実際に動かして確認しないと書けないレビューを要求したことで、自然と確認の深さが変わった。
Anthropicのブログには「新しいモデルが出たらハーネスを見直して不要な部品を外せ」とも書かれていた。今回のEvaluatorは現状のモデルでの実験なので、新モデルに切り替えたときにどの部分が不要になるかを確認するのが次のステップだと感じている。