🌅

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 を使います。

  1. 初回だけ人間が画面表示モードのブラウザでログインする
  2. その時点のCookie+localStorageを storageState.json として保存(600権限・リポジトリ管理外)
  3. 以降はヘッドレスで再利用。セッション切れは機械判定(ログイン画面リダイレクト検知)が捕まえ、その時だけ人間に更新を依頼する

保存されるのは有効期限付きセッションだけで、パスワードや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の向こう側」に悩んでいる方の参考になれば幸いです。

株式会社スプリックス IT戦略部・SPRIX Enginieering Lab

Discussion