「実装できました」が、ブラウザで開くと動いていない ── AI エージェントの E2E QA を「実装者と評価者の分離」で自律ループ化した話
IT技術
この記事で分かること
- AI エージェントに実装を任せたとき、なぜ「実装できました」を鵜呑みにできないのか
- そこで自動 QA をワークフローに組み込んだら、今度は人間が毎回はさまる面倒 が生まれた話
- その面倒を消すために作った、実装したら勝手に検証→修正→再検証が回る自律 QA ループの設計
- 実際に 1 画面でパイロットして分かった、ハマりどころと教訓(失敗込みの実録)
この記事は、私たちが実際にたどった道のりを 4幕の物語 として書いています。「動かない」→「自動 QA を入れた」→「でも人間が毎回はさまる」→「自動で回るようにした」という、私たちがたどった順序そのものです。
- 第1幕:「実装できました」が動かない → ブラウザで動作確認させる(最初の自動 QA)
- 第2幕:QA は入った。でも人間が毎回はさまる → 中核の判断「作り手と採点者を分ける」
- 第3幕:その判断を Claude Code で組む(実装リファレンス)
- 第4幕:1画面でパイロット → ハマりどころと教訓(失敗込みの実録)
対象読者・前提・動作環境
📌 想定する読者
- Claude Code などの AI コーディングエージェントを業務開発で使っている方
- エージェントの「実装できました」を信用しきれず、QA の自動化を考えている方
- サブエージェント/スキルといった Claude Code の構成要素に一通り触れたことがある方
第1幕:「実装できました」を、ブラウザで開くと動いていない
私たちのチームでは、機能実装を AI エージェント(Claude Code)に任せる場面が増えました。エージェントは「実装できました」と言ってきます。型チェックも Lint も通っている。でも、実際にブラウザで開いてみると、期待通りに動いていない ── これが頻繁に起きました。
- 画面のレイアウトが崩れている
- フォームが送信できない
- コンソールにエラーが出ている
- ある操作をすると 500 が返る
不思議なことではありません。型チェックや Lint が見ているのは「どう書かれているか」であって、「実際に動かしたとき、ユーザーが必要とする振る舞いをするか」ではないからです。
この「振る舞いの正しさ(Behaviour)の検証」が、実はソフトウェア検証の中で最も遅れている領域です。Thoughtworks の Birgitta Böckeler は AI エージェント向けのハーネス(=エージェントを取り巻く仕組みの総体。後述します)を論じる中で、ハーネスが規制する対象を保守性 / アーキテクチャ適合性 / 振る舞いの3つに分け、振る舞いの検証こそが一番未成熟だと指摘し、これを「部屋の中の象」と呼びました。保守性や型は機械で速く検証できるのに、「本当にユーザーの役に立つ動きをするか」は誰も自動で確かめられていない、というわけです。
さらに厄介なのが、実装した本人のセルフチェックは当てにならないという点です。人間のセルフレビューに「自分のコードは正しく見える」バイアスがあるのと同じで、「こう実装したのだから、こう動くはず」という前提が確認をすり抜けさせます。これは人間でもエージェントでも同じです。
そこで私たちは、Claude に実際にブラウザを操作させて E2E 動作確認させる仕組みをワークフローに組み込みました。これが最初の一歩です。
自動 QA の土台:エージェントがブラウザを操作できる状態を作る
AI エージェントにブラウザ E2E を任せるには、その下に「このアプリで E2E をどう動かすか」を教える基盤が要ります。要点はこうです。
- テスト専用ログイン。 本番は OAuth ログインですが、自動テストでこれを通すのは非現実的です。そこで開発/テスト環境でのみ有効なテストログイン用エンドポイントを用意し、本番では絶対に有効化されないよう環境変数でガードします。
- テスト DB の分離とリセット。 導入初期の最大の敵はテストデータ汚染でした。前のテストが残したデータで次が落ちる・通るが不安定になる、という典型です。対策は単純で、専用のテスト DB を本番/開発から分離し、各検証の前に必ずリセットすることです。
- factory CLI でのデータ投入。リセット後、シナリオに必要なデータを投入する CLI ツールを作成しました。作成したエンティティの ID を JSON で返すのが肝で、エージェントが ID をパースして URL 組み立てやクリーンアップに使えます。
- Reconnaissance-Then-Action(観察してから操作)。「見えない状態で操作する」と必ず失敗します。毎アクションの前に DOM スナップショットで現在の状態を確認し、正しい要素にだけ操作します。セレクタは
data-testidを最優先にして、スタイル変更に強くしておきます。
これらをプロジェクト固有のスキルとして「手順」だけでなく「なぜそうするか」まで明文化し、汎用の QA コマンドと組み合わせます。こうして「実装直後にブラウザで動作確認する」が、ようやく回るようになりました。
| 💡第1幕の結論:自動 QA は前進だった。「実装できました」を、機械が実際に動かして裏取りできるようになったのです。── ただ、運用してみると、今度は別の問題が前面に出てきました。 |
第2幕:QA は入った。だが、人間が毎回はさまる ── 中核の判断「作り手と採点者を分ける」
運用してみると、すぐに別の不満が生まれました。「自動 QA」と呼ぶには、人間が何度も手で介在していたのです。
具体的には、実装が1つ終わるたびに、こういう往復が発生しました。
- 起動の手間。 実装が完了しても、人間が明示的に「QA を実行して」とコマンドを打たないと、検証が始まらない。
- 修正依頼の手間。検証結果が返ってきたら、人間がそれを読んで「このバグを直して」と都度エージェントに指示する。直ったら、また「もう一回 QA して」と打つ。
つまり実装 → QA → 修正 → 再 QA というフィードバックループが、毎周、人間の手で分断されていたのです。小さな変更ほど、この往復のオーバーヘッドが本体の作業より重く感じられました。「動作確認、して」「直して」「もう一回確認して」を一日に何度も打つのは、地味に消耗します。
加えて、構造的な問題もありました。検証を実装と同じ会話(同じ文脈) で走らせると、実装したエージェントが自分の成果物を採点する形になりがちです。第1幕で触れたセルフ評価バイアスが、ここで再び顔を出します。「こう実装したのだから正しいはず」という前提が、検証に混入してしまうのです。
ここまでで、消すべき介在が3つ見えてきます。
- 起動の介在 … 人間が
/qa(ブラウザ上でQAを実行するSkill)を打たないと検証が始まらない - 修正依頼の介在 … 人間が「直して」と都度頼まないと修正が進まない
- セルフ評価バイアス … 実装した本人(の文脈)が自分を採点し、バイアスを混入させる
この「人間が毎周はさまる」状態は、実はもっと大きな潮流から見ると不自然です。OpenAI はエージェントファーストな開発について、人間の役割は「コードを書く人」から「環境・意図・フィードバックループを設計する人」へ移ると述べています。そしてエージェントは、自分の変更をレビューし、追加のレビューを要求し、フィードバックに対応し、全レビュアーが満足するまで自律的にループ反復して仕事を仕上げる ── と。
同じ記事にある「修正は安価、待機は高コスト」という言葉も刺さりました。人間が間に立ってエージェントを待たせるより、エージェントにフィードバックループを回させた方がずっと速いのです。
つまり、起動も、修正依頼も、採点も、人間の手から外したい。ところが、ただ「人間を抜く」だけでは評価者が消えません。実装したエージェントにそのまま採点させれば、バイアスが残るからです。ここで、このシステム全体を決めるたった一つの設計判断 が要ります。
中核の判断:作り手と採点者を分ける
一番大事な判断は、これだけです。
コードを書くエージェント(Generator)と、それを採点するエージェント(Evaluator)を、別々の文脈に分離する。
これは Anthropic が長時間実行アプリ向けのハーネス設計で提唱する、GAN(敵対的生成ネットワーク)に着想を得た生成者 (Generator) – 評価者 (Evaluator) 分離の考え方そのものです。彼らは、エージェントの自己評価は信頼できない ── 自分の成果物を評価させると、人間の目には明らかに平凡な出力でも自信たっぷりに称賛しがちだ ── と指摘し、外部の評価エージェントが実際にライブのページを操作してから採点し、生成者に具体的な反復対象を与える設計を示しています。
これを QA に当てはめ、評価者(Evaluator)はこう動かします。
- 実装役(Generator)とは別の隔離されたコンテキスト で起動する
- 実装の差分も、「なぜこう直したか」という意図も渡さない
- 受け入れ基準と、対象画面・URL だけを渡す
- 「実装者の意図」ではなく「受け入れ基準」だけに照らして、初見のユーザーのように動くアプリを評価する
意図を渡さなければ、バイアスは混入しようがありません。これがセルフ評価バイアスを構造的に消す仕掛けです。そして両者の受け渡しは、会話の往復ではなく構造化されたレポート(JSON)1枚 に固定します(Anthropic の「ファイルベース通信」)。Evaluator は書くだけ、Generator は読んで直すだけです。
| 💡第2幕の結論:中核の判断は決まった。作り手と採点者を別々の文脈に分け、受け渡しは JSON レポート1枚に固定する ── これで3つの介在のうちセルフ評価バイアスは、構造的に消えます。残る「起動の介在」「修正依頼の介在」を実際に消すのは、この判断を動く形にする実装の仕事です。次の幕で組み立てます。ちなみに0の行のpageのレンダリング結果以外のところや他の行には、MetaデータやNext.jsの必須のClient Componentの情報などが入ってるようでした。 |
第3幕:その判断を Claude Code で組む(実装リファレンス)
第2幕で決めた「作り手と採点者の分離」を、Claude Code の機能(スキル/サブエージェント)で実際に組みます。やることは、第2幕で残った2つの介在 ──起動と修正依頼── を実装で消し、分離を支える周辺の仕掛け(受け入れ基準の供給/受け渡し契約/暴走防止)を整えることです。設計にあたっては、公開されているハーネスエンジニアリングの議論(OpenAI / Anthropic / Böckeler)を参考にしました。
アーキテクチャ全体像
まず設計の全体像です。第2幕の「分離」を、実装と検証が1本のループで回る形に落とすと、こうなります。
1 ┌─────────────────────────────────────────────┐
2 │ Orchestrator = Generator(実装&修正役) │
3 │ ・実装が完了したら自動で QA を起動 │
4 │ ・レポートを読み、バグを修正 │
5 │ ・合格 or 反復上限到達まで回す │
6 └───────────────┬─────────────────────────────┘
7 │ 対象画面 / URL / 受け入れ基準 / 周回番号
8 │ (※実装 diff・意図は渡さない)
9 ▼
10 ┌─────────────────────────────────────────────┐
11 │ Evaluator(評価者・隔離コンテキスト・毎周フレッシュ)│
12 │ ・テスト DB リセット → データ投入 │
13 │ ・テストログイン → ブラウザで対象を操作 │
14 │ ・page-load / console / 要素 / network を検証 │
15 │ ・結果を JSON レポート1枚に書く(コードは直さない)│
16 └───────────────┬─────────────────────────────┘
17 │ qa-report-<feature>-<iteration>.json
18 ▼
19 status == "pass" ?
20 ├─ yes → ループ終了(合格)
21 └─ no → Generator がバグを修正して再検証
22 (ただし反復上限 MAX_ITERATIONS まで)役割を Claude Code の機能に対応させると、こうなります。
| 役割 | 実体 | Claude Code 上の表現 |
|---|---|---|
| 入口(ワークフロー組み込み) | 実装+検証を 1 呼び出しで連続実行 | プロジェクトスキル(/implementing-with-verification) |
| Orchestrator / Generator | 上記スキルの本体(実装は executing-plans skill に委譲、検証ループは自分で回す) | スキル内のフロー |
| Evaluator | 隔離された評価者 | サブエージェント(独立コンテキストで毎周新規起動) |
| 受け入れ基準の源 | 仕様書を真実の源に直結 | 実装前に作成した仕様書(このプロジェクトでは OpenSpec を使用)をそのまま渡す |
| 受け渡し契約 | 検証結果 | JSON レポート(スキーマを文書で固定) |
| 起動判定 | 変更規模の自動分類 | 小さなシェルスクリプト |
ポイントは Evaluator をサブエージェントとして毎周新しく起動すること。サブエージェントは親と独立した文脈を持つので、「前回こう直した」「実装側はこういう意図」といった情報が自然に遮断され、判定の独立性がタダで手に入ります。
Generator と Evaluator の詳細
では Generator と Evaluator はどのように実装しているのでしょうか。
Evaluator は、サブエージェント定義ファイル(.claude/agents/qa-evaluator.md)です。Claude Code のサブエージェントは、フロントマターと本文プロンプトで定義された「独立した実行コンテキスト」 ── 簡単に言えば、親セッションから切り離された別の小さな Claude です。私たちの qa-evaluator は、おおよそ次のように制約しています。
.claude/agents/qa-evaluator.md1---
2name: qa-evaluator
3description: >
4 機能完成後、独立した文脈でブラウザ E2E 検証を行い、
5 構造化 QA レポートを 1 つだけ出力する。コードは直さない。
6model: opus
7tools:
8 - Read, Write, Bash, Grep, Glob # ファイル・コマンド・レポート出力
9 - mcp__playwright__browser_navigate # ブラウザ操作群
10 - mcp__playwright__browser_snapshot
11 - mcp__playwright__browser_click
12 - ...
13 # 注: コードを書き換える Edit ツールは渡さない
14---
15(本文: 採点の手順、出力契約=レポートスキーマ、ハードルール)押さえどころは 3 つです。
- 隔離された実行コンテキスト。親セッションの会話履歴・実装の diff・author の意図は届きません。毎周フレッシュに起動します。
- ツール権限を絞ってある。 レポートを書く
Write、ブラウザを操作する Playwright MCP、観察用のRead/Grep/Bashまでは渡しますが、コードを書き換えるEditは渡しません。「評価者は採点に徹する」を権限レベルで強制しています。 - 本文プロンプトで、採点の手順・出力契約(後述のレポートスキーマ)・ハードルール(コード修正禁止/証跡必須/感想ではなく構造化 JSON で返せ)を明示します。
Generator は、メインセッションそのもの。専用のサブエージェントファイルはありません。スキル(.claude/skills/implementing-with-verification/SKILL.md)を実行している、いつもの開発セッションが Generator として振る舞います。実装に必要な編集権限・テスト実行権限・既存の開発文脈(プラン、TDD、リファクタの判断)をフルに持っています。
スキル本体は、その Generator の振る舞いを規定する手順書です。「(仕様があれば)受け入れ基準を導出する」「executing-plans スキルに実装を委譲する」「qa-scope で規模を分類する」「qa-evaluator を fresh で起動する」「レポートを読んで修正する」「反復上限を超えたら capability gap を書く」 ── これをスキル本体に書いておけば、メインセッションはその手順どおりに動きます。
「起動の介在」を消す:実装と検証を 1 呼び出しで連結し、規模に応じた深さで起動
1つ目の課題「人間が /qa を打たないと始まらない」を消します。これには2つを組み合わせます。
(1) 実装と検証を、ワークフローのレベルで 1 つの呼び出しに統合する。
ここが落とし穴です。実装フェーズと検証ループを「別々の手動コマンド」のままにしてはいけません。「実装は executing-plans スキル、検証は別途専用の QA コマンド」と分けると、コマンドの名前が変わっただけで、実装の終了から検証の起動までの間に人間が1回はさまる構造は残ったままです。起動の介在は名前を変えて生き残るのです。
そこで、プロジェクト固有のスキル(/implementing-with-verification)を1つ用意し、その中で
- (OpenSpec change-id があれば)spec を読んで受け入れ基準を導出(次節「受け入れ基準は手で書かない」)
- 実装は既存の
executing-plansスキルに委譲 - 完了後、変更規模を機械分類(このすぐ後)
- 規模に応じた深さで検証ループを起動
を 連続実行 します。Claude Code のスキルは「この change を実装して」のような自然なリクエストで auto-trigger できる(明示的に /implementing-with-verification と呼ぶこともできます)ので、ワークフローのどこにも「実装後に人間が QA コマンドを叩く」ステップが存在しなくなります。実装と検証を分けてしまった瞬間に手動の隙間が生まれる、というのが今回の最大の学びでした。
(2) 全変更にフルループを課さない。変更規模で深さを分ける。
もう一つ重要な判断があります。すべての変更にブラウザ E2E のフルループを回すのは過剰です。型修正やコメントの直しにまで重い検証を挟むのは、ただのコストです。これは Anthropic が、モデルが十分に強くなって境界内に収まるタスクでは評価器がただのオーバーヘッドになる(だから外せる)と述べているのと同じ発想で、ハーネスは足すだけでなく、当てる深さを使い分けるのが要点です。
そこで変更規模で深さを分けます。
| 変更規模 | QA の深さ |
|---|---|
| 数行・型・コメント・整形のみ | ループなし(型チェック/Lint/テストの機械ゲートだけ) |
| サーバー側ロジックの小さな修正(1〜3ファイル) | 1周だけ検証(single-pass) |
| UI・振る舞いを変える変更/新規画面 | フルループ(検証→修正→再検証) |
この分類を小さなシェルスクリプト で機械的に行います。差分が UI 関連のパスを含むなら「フルループ」、ごく小さければ「ループなし」、それ以外は「single-pass」。迷ったら深い方に倒す(UI に触れているか不明ならフルループ)のが安全側の設計です。
「修正依頼の介在」を消す:レポートを読んで自分で直す
2つ目の課題「人間が毎回『直して』と頼む」は、Orchestrator(Generator)の責務にするだけで消えます。
Evaluator が返す JSON レポートには、各バグについて再現手順・期待・実際・証跡 が入っています。Generator はこれを重大度の高い順に読み、根拠に基づいて最小修正を行います。テストを通すための握りつぶしはしません。修正したら周回番号を1つ進めて、また Evaluator を起動 ── これを合格まで(反復上限内で)繰り返します。人間が間に立つ必要は、もうどこにもありません。
受け入れ基準は手で書かない:仕様書を真実の源に直結する
「起動」と「修正依頼」の介在を消したあとに残るのが、もう一つの密かな手作業 ── 「Evaluator に渡す受け入れ基準を、毎回人間が手書きする」です。
最初は、人間が「期待挙動はこう」と prose で Evaluator に伝えていました。しかしこのやり方だと、
- 受け入れ基準が会話ごとにバラついて再現性がない
- 仕様書(私たちは OpenSpec を使っています)と、検証側が見る基準がじわじわズレる
- 「何を作るか」と「何を確かめるか」が常に二重管理になる
ことになります。理想は、仕様書 1 つを真実の源にして、実装も検証もそこから派生する形です。
そこで wrapper スキルは、OpenSpec の change-id が指定または推定できる場合、
openspec/changes/<id>/proposal.mdを読んで目的・影響範囲を把握openspec/changes/<id>/specs/**/*.mdの### Requirement:/#### Scenario:を抽出- そのスペックのパスを
specRefとして Evaluator に渡す
という流れを取ります。Evaluator は受け取った specRef を直接読み、Requirement / Scenario をそのまま判定基準にして実アプリを動かします。バグを報告するときは「どの Scenario を満たせなかったか」を specCriterionRef として書き残します。
仕様が変われば検証基準が自動で変わります。仕様 → 実装 → 検証が同じ change を介して直結し、ズレようがありません。これは Böckeler の behaviour harness でいう、仕様を feedforward の guide(=基準の源)に据え、検証を feedback の sensor に担わせる構図を、業務システムにそのまま落とし込んだものです。
受け渡しの契約:レポートスキーマ
Generator と Evaluator の唯一のインターフェースが JSON レポートです。フィールドは「判定」と「修正」に必要な最小限に絞ります(過剰なスキーマは保守コストになるだけです)。
qa-report-<feature>-<iteration>.json1{
2 "schemaVersion": "1.0",
3 "target": { "feature": "基本情報ページ", "url": "…", "hogeId": 1, "fugaId": 1 },
4 "iteration": 1,
5 "status": "fail", // "pass" | "fail"
6 "summary": "1〜3文の総評",
7 "bugs": [
8 {
9 "id": "BUG-001",
10 "severity": "high", // critical | high | medium | low
11 "repro": ["再現手順を Generator が辿れる粒度で"],
12 "expected": "受け入れ基準ベースの期待",
13 "actual": "実際の挙動",
14 "evidence": { // 証跡が無ければバグとして報告しない
15 "consoleErrors": ["…"],
16 "networkFailures": [{ "url": "…", "status": 400 }],
17 "screenshotRef": "…"
18 }
19 }
20 ],
21 "checksPerformed": ["page-load", "console-errors", "expected-elements", "network"],
22 "rubric": { "functionality": "fail", "errors": "fail", "expectedElements": "pass", "network": "fail" }
23}設計上のルールはこうです。
- 証跡のないバグは報告しない。コンソールエラー、失敗したネットワークリクエスト(URL+ステータス)、スクリーンショットのいずれかを必ず添える。
- status と bugs は整合させる。 critical / high が1件でもあれば必ず
fail。 - 正直な pass。 基準を満たすなら
bugs: []でpassを返す。網羅的に見せたいがための「でっち上げバグ」を作らない。 - レポートは毎周、上書きせず新規ファイルで残す。失敗の履歴が、後述の診断の根拠になる。
採点を主観に流さないため、ルーブリックは機能性・エラー有無・期待要素・ネットワークの4観点に絞ります。「デザインの良し悪し」のような主観項目は、現状は QA の採点項目から外しています。
「収束しないとき」の規律:もっと頑張れ、ではなく、何が足りない?
自律ループには暴走防止の反復上限(MAX_ITERATIONS)を置きます。問題は「上限まで回しても合格しなかったとき、どうするか」です。
一番やってはいけないのが「もう一周だけ試す」です。上限超過時は「同じやり方では収束しない」というシグナルで、回数を増やせば解決する話ではありません。
そこで、収束しなかったら再試行せず、capability gap(足りない能力)を1枚のノートに書き出して人間に渡します。
- 残ったバグはどの受け入れ基準を満たせなかったか
- 各周回で何を試して、なぜ不十分だったか
- 推定される「不足している能力」は何か(テスト用の
data-testidが無い? factory にコマンドが無い?受け入れ基準・仕様が空洞? スキルに手順が欠けている?)
そして同じ種類の gap が2回以上起きたものだけを恒久対応(基盤の改善)に昇華します。1回限りのものに先回りで抽象化しません(YAGNI)。
これは OpenAI の「不足している能力(missing capability)」診断 ──「いま足りない能力は何か、それをどうすればエージェントにとって読み取り可能(legible)で強制可能(enforceable)にできるか」を問う ── をループの撤退条件に組み込んだものです。失敗を、根性論ではなく恒久的な品質向上に変換する。これが、ループを無限リトライ地獄に陥らせない肝になります。
ここまでで設計は出そろいました。── ただ、「設計として正しい」と「実際に回る」は別物です。次の幕で、1画面に当てて確かめます。
第4幕:1画面でパイロットしてみた(失敗込みの実録)
設計だけ立派でも仕方ないので、変更頻度が高く UI 要素が明確な業務入力ページ1枚(仮に「基本情報ページ」と呼びます)で実際に回しました。検証方法は「小さく可逆な既知バグを1つ仕込み、ループが人間のトリガーなしに検出 → 修正 → 再検証 pass するか」を観察するものです。
結論から言うと、ループは機能しました。そしておまけに、仕込んでいない本物のバグと、基盤自身の欠陥まで炙り出しました。
iteration 1:仕込んだバグを、頼まれずに検出
ページのデータ取得呼び出しを、わざと無効な ID(NaN)で叩くように改変し、自律ループを起動。Evaluator は隔離コンテキストで、私が「何を仕込んだか」を一切知らないまま動きました。結果は status: fail(high 1件)。
/api/hoge/NaN/detailが 400 を返している(ネットワーク証跡)- コンソールに error 3件
- 画面に赤いエラーアラートが出たまま残る
人間は /qa を一度も打っていません。規模判定が「UI 変更=フルループ」と分類し、ループが自動で Evaluator を起動した結果です。第2幕の「起動の介在」、消えました。
iteration 1 → 修正:頼まれずに直す
Generator はレポートの再現手順・期待・実際を読み、「無効な ID で取得している」が原因と判断して、正しい ID を渡すよう修正しました。人間は「直して」と一言も言っていません。第2幕の「修正依頼の介在」も消えました。周回を進めて再検証へ。
iteration 2:直したのに、まだ落ちる ── 本物のバグの発覚
iteration 2 は status: fail(今度は medium)。まだ NaN リクエストが残っているというのです。私が仕込んだ箇所は完全に元へ戻したはず。調べると ── 対象ファイルは元のコミットと完全に一致していました。にもかかわらず、dev サーバーのアクセスログには iteration 2 セッション中に GET /api/hoge/NaN/detail 400 が確かに 1 回記録されていました。私の仕込みではなく、素のままの元コードが NaN を撃ったその瞬間がログとして残っていたのです。
該当箇所は page.tsx の初期化 useEffect。長期間こう書かれていました。
app/.../page.tsx1useEffect(() => {
2 loadInitialData();
3 loadHogeDetail(parseInt(hoge_id as string)); // ← NaN ガード無し
4}, [hoge_id, loadInitialData, loadHogeDetail]);hoge_id から parseInt で数値を取り出した結果が NaN になりうる経路に対して、ガードがありませんでした。NaN がそのまま API クライアントに渡ると、URL が /api/hoge/NaN/detail として組み立てられ、サーバーは 400 を返します。
ガードを追加する小さな修正を提案し、再検証へ。自律 QA ループが、仕込みバグの検証のついでに、既存の潜在バグをセンサーとして拾い上げた瞬間でした。
iteration 3:合格するはずが…偽陽性に殴られる
ガードを入れたので今度こそ合格 ── と思いきや、iteration 3 も status: fail(high)。レポートはまた「コールドロードで NaN リクエストが出ている」と言います。
ここで立ち止まりました。ガードは確かに入っている。コードは正しい。なのに Evaluator は NaN を見たと言う。そこで嘘をつけない証拠 ── 開発サーバーのアクセスログ ── を突き合わせました。サーバーは受信した全リクエストを記録します。タイムラインを追うと、
- 修正がホットリロードされた後の、iteration 3 のコールドロード以降のリクエストはすべて正常な ID(200)で、
NaNは1件も無い - レポートに出ていた
NaNは、iteration 3 より前 ── iteration 2 のセッションのリクエストだった
iteration 3 の fail は偽陽性 でした。原因は、ブラウザ自動化ツールのブラウザ状態が Evaluator の起動をまたいで永続していたこと。ネットワーク履歴の取得が過去の周回の累積 を返し、Evaluator はそこに残っていた古い NaN を「今のバグ」として報告していたのです。毎周フレッシュなはずの Evaluator が、ブラウザという外部状態を経由して前周回と繋がっていた ── 分離の穴でした。
iteration 4:計測手順を正してクリーン合格
原因が分かれば対処は簡単です。計測の前にブラウザセッションをリセットし、当該周回の遷移だけを計測対象にする。その手順で再検証したところ status: pass。NaN は本周回中に一切観測されず。ガード修正の有効性と、iteration 3 が偽陽性だったことが同時に裏付けられました。ループは(計測手順を正したうえで)pass に収束しました。
パイロットは基盤自身の欠陥も炙り出した
この1回のパイロットは、検証対象のバグだけでなく基盤(ハーネス)自身の欠陥 も露呈させました。
欠陥:評価器の計測汚染(上記 iteration 3 の偽陽性)。ブラウザ状態が周回をまたいで残り、ネットワーク履歴の累積が偽陽性を生んでいた。→ Evaluator のスキルに「計測前にブラウザ状態をリセットする」手順を明記。
明白なバグだったので、その場で恒久対応しました。自律 QA ループは、業務コードのバグを見つけるだけでなく、使われながら自分自身の欠陥を可視化し、改善のフィードバックを生む ── この「ハーネスが自己改善していく」性質こそ、パイロット最大の収穫でした。
🔧 設計でつまずいた点と、そこから得た教訓
実録を一般的な教訓に圧縮します。同じ構成を作るチームは、おそらく同じ穴に落ちます。
- 「分離」はコンテキストだけでは足りない。外部状態も分離せよ。サブエージェントで文脈を分けても、ブラウザ・DB・ファイルといった外部状態が周回をまたいで残ると、そこから依存が漏れて偽陽性になります。計測の前に状態をリセットすることを、手順として明文化しましょう。
- エージェントの報告より、サーバーログのような一次証拠を信じよ。「Evaluator が fail と言った」と「アプリが実際に失敗した」は別物です。判断が割れたら、嘘をつけない一次ソースで突き合わせます。
- セルフ評価バイアスは、気合いではなく構造で消す。「客観的に見て」と頼むのではなく、採点者に実装の意図をそもそも渡さない 設計にします。
✅ まとめ
私たちがたどった道のりは、シンプルな4幕でした。
| 幕 | 状況 | 打ち手 |
|---|---|---|
| 第1幕 | 「実装できました」がブラウザで動いていない | ブラウザ E2E で動作確認させる(自動 QA を導入) |
| 第2幕 | だが起動・修正依頼・採点を人間が毎周はさむ | 中核の判断「作り手と採点者を分ける」を据える |
| 第3幕 | その判断を、実際に動く形にする | 分離した Evaluator + 実装統合スキルで自律ループを実装 |
| 第4幕 | 設計は正しい。でも本当に回るか | 1画面パイロットで実バグと基盤自身の欠陥を炙り出し、pass に収束 |
この自律ループの核心は、「実装した本人に、自分の成果物を採点させない」という一点に尽きます。作り手と採点者を別々の文脈に分け、そこへ「機械可読な契約」「規模に応じた起動」「収束しないときの規律」を重ねると、検証は頼まなくても回る ようになります。
そして実際に回してみると、ループは業務コードのバグを見つけるだけでなく、自分自身の欠陥まで可視化して、改善のループを生みます。自律 QA は「作って終わり」ではなく、使うほど賢くなる基盤として育てていける ── というのが、最初のパイロットで得た一番の手応えでした。
📚 参考にした記事
- Birgitta Böckeler「Harness engineering for coding agent users」(martinfowler.com 掲載) — https://martinfowler.com/articles/harness-engineering.html
- OpenAI「ハーネスエンジニアリング:エージェントファーストの世界における Codex の活用」(2026-02-11, Ryan Lopopolo) — https://openai.com/ja-JP/index/harness-engineering/
- Anthropic「Harness design for long-running apps」 — https://www.anthropic.com/engineering/harness-design-long-running-apps
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!カジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
Udemy信奉者系フロントエンジニア(バックエンドもちょっと)。 現在はNextjsを用いた不動産情報サイトのフロントエンド開発担当中。 映画好きで基本毎日Netflixしてます。




