AIエージェントの動作確認を「curlの200 OK」から「画面の視認」へ拡張した話
📌 はじめに
AIエージェント(Claude Code)と一緒にインフラ作業をしていると、Webアプリの動作確認は自然と「curlでステータスコードを見る」に落ち着きます。手軽で速く、たいていはそれで十分だからです。
しかし、あるSaaSのAWS移行プロジェクトで痛感したのは、本番障害の多くはHTTP 200のままブラウザ層で起きるということでした。curlが「200 OK」と言っているのに、ユーザーの画面は真っ白——そんな障害クラスに、エージェントの目は届いていませんでした。
この記事では、ヘッドレスブラウザ・運用手順書・サブエージェント委譲を組み合わせて、トークン消費を抑えながらエージェントに「視認」までやらせる仕組みを作り、実際のカットオーバー前夜の障害対応で使った話を書きます。
🎯 課題:「200 OK ≠ 正常」という盲点
実際に遭遇した障害は、いずれもcurlでは原理的に検知できないものでした。
- 白画面(HTTP 200) — DB移行時にビュー定義が破損し、管理画面が中身のない200を返し続けた。ステータス監視はすべて正常のまま
-
HTTP/2プロトコルエラー — 音声コンテンツのXHRだけがブラウザ内で
ERR_HTTP2_PROTOCOL_ERRORとなり再生不能に。サーバー側ログは「200・フルサイズ配信済み」を記録しており、サーバー側からは不可視 - CORSブロック — レスポンスヘッダーの欠落により、ブラウザのみがリクエストを遮断。curl単体では常に成功する
共通するのは、障害が「サーバーとブラウザの間」で起きていることです。検証の観測点をブラウザ側に置かない限り、この障害クラスは捕まえられません。
💭 素朴な解と、その対立軸:「トークンを食い過ぎないか?」
「ならブラウザ操作系のMCPをエージェントに繋げばいい」というのが素朴な解です。実際それで視認はできます。
ただ、チーム内で真っ先に出たのがトークン消費への懸念でした。ブラウザ操作MCPは操作1ステップごとにDOMスナップショット(アクセシビリティツリー)をコンテキストへ送り込みます。ページ規模によりますが1回あたり数千〜数万トークンで、しかもメインの会話履歴に滞留し続けます。10画面の確認フローを対話的に回すと、その後のやり取り全てがこの重みを引きずることになります。
「視認はさせたい。でもエージェントの本来の主作業(切替作業やインフラ操作)の文脈を、動作確認ごときで汚したくない」——これが設計の出発点でした。
🔨 設計:4つの原則
1. 重い処理は機械に、判断はエージェントに
ページ遷移・フォーム入力・スクリーンショット取得・console/network収集は、ローカルのPlaywrightスクリプトがエージェント非介在で完結させます。エージェントが受け取るのは成果物(数百トークンのJSONレポート+選択的に読む画像)だけ。ナビゲーションの過程がエージェントのコンテキストに乗ることはなくなり、消費は成果物の受領分だけになります。
スクリプトは「JSON形式の検証プラン」を受け取る汎用設計で、各ページで以下を機械判定します。
| 判定 | 捕まえるもの |
|---|---|
| 本文テキスト長ほぼゼロ | 白画面疑い(HTTP 200のまま起きる) |
| consoleエラー / pageerror | JSエラー・CORSブロック等、ブラウザ内でだけ起きる異常 |
| ネットワーク失敗・4xx以上 | XHRの失敗(HTTP/2エラー含む) |
| alertダイアログ | 音声ロード失敗等のエラーアラート |
| レスポンスヘッダー | CDN経由の証拠ヘッダー等、経路切替の判定 |
| ログイン画面リダイレクト | セッション切れ(検知したら人間に更新を依頼して停止) |
| 危険文言 |
Fatal error・Database Error・Unknown column 等の本文出現 |
2. スコープは2層で管理する
第1層はリスクベースで選んだ固定スモークセット(過去に壊れた画面・壊れやすい依存を持つ画面・影響最大の導線)。再現性を担保する層で、実行のたびに結果がブレません。第2層はエージェントの探索。第1層の出力に現れた「異常の匂い」(consoleの警告・レイアウトの違和感)を起点に、エージェントが仮説を立てて動的に枝を掘ります。網羅性ではなく感度で勝負する層です。
3. 手順書がSingle Source of Truth — そして機械可読化は不要
検証スコープを専用設定ファイルに書くと、運用手順書といずれ乖離します。そこで手順書そのものをスコープの正としました。鍵は、スクリプトが手順書をパースするのではなく、エージェントが手順書を読んでスクリプトへの指示(プランJSON)に変換する構造にしたことです。手順書は人間向けの自然言語のままでよく、曖昧さの吸収はエージェントが担います。障害が起きたら手順書に確認項目を足す——それだけで検証スコープも自動的に育ちます。
4. パスボール型並列 — メイン作業を止めない
検証の実行と一次判定(スクショの視認含む)はサブエージェントに委譲します。メインエージェントは主作業を継続し、完了通知で要約だけを受け取ります。スクリーンショットの画像トークンはサブ側の使い捨てコンテキストが吸収するため、メインの文脈は数百トークンのレポートしか増えません。
SSOはパスワードを貯めずに「セッションを貯める」
この方式の最大の関門はSSOログインです。ログインフォームを認証情報でスクリプト突破する案は、2FAで崩れる・IdPのbot検知に弾かれる・平文パスワードをディスクに置くことになる、と三重に筋が悪い。代わりにPlaywrightの storageState を使います。
- 初回だけ人間が画面表示モードのブラウザでログインする
- その時点のCookie+localStorageを
storageState.jsonとして保存(600権限・リポジトリ管理外) - 以降はヘッドレスで再利用。セッション切れは機械判定(ログイン画面リダイレクト検知)が捕まえ、その時だけ人間に更新を依頼する
保存されるのは有効期限付きセッションだけで、パスワードやTOTPシードは一切ディスクに置きません。認証という最もデリケートな部分だけ人間、以降は全自動です。素のID/PASS認証のアプリは、600権限のcredentialsファイル+プラン内の ${KEY} プレースホルダー置換で足ります(プランJSONに平文を書かない)。
🔥 実戦①:“HTTP 200なのに白画面”の切り分け→原因特定→復旧確認
本番カットオーバーの前夜、生徒向け画面で「ログイン後に画面が真っ白」という報告が上がりました。curlは200 OKを返し、従来の確認手法では「正常」に見えます。作ったばかりのこの仕組みを初投入しました。
機械判定が出した事実(人間はまだ画面を見ていない段階):
- 認証は成功している(ログインは通り、遷移先URLはマイページ)
- 遷移先の本文が0文字の200(=白画面の実体)
- console/networkエラーはゼロ → クライアント要因を一撃で除外でき、調査をサーバー側へ絞り込めた
次に効いたのが対照実験です。同一アカウントを移行前の旧環境(エラー表示が有効な設定)で実行したところ、フレームワークのDBエラー画面が表示され、エラー全文(SQL・ファイル名・行番号)をスクリーンショットから読み取れました。これが真因特定の決定打になりました。
真因は、テストユーザーのデータに紛れた不正な値が未クオートのままSQLに直接展開されて構文破壊(Unknown column)を起こしていたこと。本番相当環境は db_debug=FALSE でエラー表示を抑止するため白画面に見えていた、というオチです。つまり環境障害ではなく、データ×コードの既存バグでした。
復旧確認では、検証環境で先行修正→再検証→本番ソースに適用→再検証、と同じプランJSONを3回使い回して、マイページの完全描画をスクショ視認で確認しました。「直った」を客観化できたのが精神衛生上とても良かった。
学び: 機械判定は感度優先(誤検知を許容する)、最終判断はエージェントの視認。初回実行はSUSPICIOUS判定に誤検知も混ざりましたが、視認で「実態は正常/異常」を仕分けできました。この役割分担こそ設計の核心だと実戦で確信しました。
🕸️ 実戦②:管理画面280ページの一括確認(リンク自動追跡クロール)
もうひとつの実戦は「管理画面が多岐にわたり、一人で全部開いて確認するのは骨が折れる」問題です。単一ページ検証の応用として、リンク自動追跡クローラーを追加しました。
-
安全設計: 参照系GET遷移のみ(hrefを辿る)。変更系URL(delete/edit/save/upload/logout/csv/export/ajax等)とフォーム送信は正規表現で自動除外。同一オリジン+スコープパターン(例
^/admin)内に限定。ただし除外は正規表現ベースなので漏れの可能性は残り、またGETでも状態が変わるエンドポイント(既読化・トークン消費型のURL等)は世の中に存在します。クロール対象のアプリの作りを把握した上でスコープを絞ってください - データの扱い: 読者が再現する場合、本番相当のデータを扱うならマスキング済みのデータで実行することをおすすめします
- トークン設計: 各ページの機械判定は全実施しつつ、スクショは「怪しい」ページだけ保存
-
テンプレート畳み込み: URLの数字セグメント→
:id、クエリ値を落として「同じ画面テンプレートは1回」で巡回。個別レコードの総当たりを避けて画面種別を効率よく網羅
結果は2回の巡回(第1弾80ページ+第2弾200ページ)合わせて280ページ ALL_OK。全コントローラーの画面テンプレートについて、機械判定では異常を検出しませんでした。異常0件だったため、エージェントが視認した画像は0枚。トークン消費は機械判定分だけです。
$ node bin/crawl.js plans/admin-crawl.json
RESULT: ALL_OK ok=200/200 queueRemaining=2
(出力は第2弾のもの。ok=200/200 は上限 maxPages=200 まで巡回して全ページOK、queueRemaining=2 は上限到達時点で未訪問キューに2件残っていた=ほぼ巡回し切ったことを示します)
正直な併記: このクロールのカバレッジは「リンクで辿れる範囲」で、フォーム送信やボタン経由でしか到達できない画面は含まれません。また「異常0件」は機械判定の結果であって、視認まで含めた保証ではありません(この仕組みの原則は「最終判断は視認」です。機械判定がOKなら視認を省く、という感度とコストのトレードオフを選んでいます)。報告規律として「見た範囲/見ていない範囲」を必ず明示させています。“網羅した”という錯覚を作らないことは、ツールの精度と同じくらい大事だと考えています。
📊 トークン経済:懸念への決着
冒頭の対立軸「トークンを食い過ぎないか?」への、実測を交えた回答です。
| 方式 | ナビゲーション | 検証成果物 | メイン文脈への残留 |
|---|---|---|---|
| ブラウザMCP完全依存 | 2,000〜20,000 tok/歩 | スナップショットに包含 | 全ステップ分が滞留 |
| 本方式(スクリプト+サブ委譲) | 0 tok(コンテキスト非経由) | レポート約500 tok+スクショ1,000〜1,500 tok/枚×選択分 | 要約のみ(数百tok) |
※ MCP側の数値は、DOMスナップショットの一般的なサイズ感からの推定です(ページ規模に大きく依存します)。本方式側は今回の実測です。
実測では、1検証あたりサブエージェント側で約4〜5万トークン(視認あり/なしで数千トークン差)、メイン側は要約のみでした。280ページ巡回でも異常0件なら視認0枚で済みます。
公平のために書くと、アカウント全体の消費で見ればサブエージェント起動自体のオーバーヘッドがあるため、劇的な差にならないケースもあります。本質的な利点は「メインの文脈を汚さない」ことと「対話ステップごとのスナップショット再送を構造的に排除した」ことにあります。
⚖️ 使いどころの勘所(運用して分かったこと)
数日運用しての、正直な使い分けです。
- 単純な画面遷移を2〜3枚確認するだけなら、人間が直接見る方が速い。プラン化のオーバーヘッドが勝ちます
- 一方、枝分かれの多い画面群のクローリングは圧倒的にこの仕組みが優位です。一人では現実的に確認しきれない画面数を、機械判定+選択的視認で肩代わりできます
- 「200なのに何か変」という切り分けにも強い。クライアント要因の除外・環境間の対照実験・修正後の再検証(同一プランの再利用)が効きます
- CIの網羅的E2Eスイートの代替ではありません。「人間のテスターのように、少ないサンプルから異常を嗅ぎ分ける」探索的テストの自動化という位置づけです
🪤 実装のツボ・落とし穴
実装〜実戦で踏んだものを列挙します。これから作る方はどうぞ。
-
動的コンテンツの待機: スクショが「描画途中」を撮ると偽陽性になる。待機は要素出現待ち(web assertions)を中心に組むのがおすすめ。
networkidleはPlaywright公式がテスト用途で非推奨としており("rely on web assertions instead")、ポーリングや常時接続があるページでは永遠に完了しないこともある -
画像ベースのログインフォーム: 本文テキストが数文字しかなく、白画面判定の閾値(
minBodyLength)を画面ごとの実測に合わせる必要がある。画像内の文字はテキスト存在判定では拾えない -
計測タグのノイズ: GA等はヘッドレスで
ERR_ABORTEDになる。無視パターン(networkIgnorePattern)で判定から除外する - credentialsファイルの書式ゆらぎ: 区切り文字や所有者権限のミスが起きがち。初回にキー名・権限の検査を挟む
-
パスワード画面のスクショを残さない: fill系ステップは
screenshot: falseを徹底 - Node標準出力のバッファリング: クローラーのstdoutをファイルにパイプすると完了までRESULT行が出ない。完了検知の設計に注意
- サブエージェントへの指示漏れ: サブはメインの会話文脈を持たない。期待値・接続先・判定基準を毎回自己完結で渡す規律をスキル側に固定する
-
送信ボタンのセレクタ:
button[type=submit]とは限らない(input[type=submit]の環境も普通にある)
🚀 おわりに
「curlの200確認」と「人間が全画面を目視」の間には、思っていたより広い空白地帯がありました。そこを埋めたのが機械判定(感度優先)×エージェントの視認(最終判断)×手順書(スコープの正)、この三点です。
カットオーバー前夜の白画面障害では切り分けの主力になり、翌日の本番切替後は管理画面280ページを機械判定で一括確認してくれました。「動作確認をお願いします」の一言で、エージェントがブラウザの向こう側まで見てくれる——この体験は一度味わうと戻れません。
同じような「200 OKの向こう側」に悩んでいる方の参考になれば幸いです。
Discussion