blog
開発メモAIが「未使用だから消せる」と言ったCSSは、全ページで使われていた——エージェント監査の落とし穴
Claude Codeには、複数のサブエージェントを並列に走らせて調査や実装を分担させる機能(Workflow)があります。他社から請け負った、あるサイトの集客・CV監査で、この機能を大きめに使ってみました。「7つの観点(技術SEO・パフォーマンスなど)を並列で監査するエージェント7体」+「それぞれの所見を懐疑的に裏取りする検証エージェント7体」の計14体を、オーケストレーター役のエージェント(Fable)が束ねる構成です。トークン消費は約170万、所要時間は31分でした。
先に断っておくと、これは2026年7月時点で実際に一度あった監査事例の記録です。マルチエージェントでの調査・監査が常にこの失敗をすると言いたいわけではありません。ただ、複数のエージェントに分析や監査を任せるときに踏みやすい地雷と、逆に「2段構えにしてよかった」と思えた収穫が、この一回の中に詰まっていたので、記録として残します。
先に結論から
AIエージェントにコードやサイトをまとめて調べさせたとき、いちばん危ないのは「該当0件」「未使用」「リスクなし」という断定です。この断定の中身は、たいてい「どこまで探したか」だけで決まっています。
今回は「このCSSは未使用なので削除してよい」という報告を受け取りました。実際には全ページ共通のナビゲーションが、まるごとそのCSSに依存していました。指示どおり消していれば、サイト全体の見た目が壊れていたことになります。原因は難しい話ではなく、調査対象のファイル種別に、そのサイトが使っていた拡張子が入っていなかった——それだけでした。
AIに「未使用だから消していい」と言われたとき、人が確かめる4点
- 調査対象の拡張子に、そのサイトが実際に使っているテンプレート(
.inc/.tpl/.liquid/.twigなど)が含まれているか。 - 探した範囲は手元のソースだけか。本番サーバーにしか置かれていないファイルを見落としていないか。
- 「棄却0件」を安心材料にしない。検証が働いた証拠ではなく、検証が届いていない兆候かもしれない。
- 消す前に本番のページを1枚取得し、そのクラス名が配信結果に現れないかを自分の目で確認する。
渡したはずの保存先が、指示文では「undefined」になっていた
Workflowのスクリプト側で、各エージェントへの指示に「保存先パス」をオブジェクトとして渡す設計にしていました。ところが実行時、渡したはずの値が展開されず、文字列のまま各エージェントに届いてしまいました。
✗ 実際の届き方:値が展開されず、エージェントへの指示文が「
undefined/01_tech-seo.md に保存せよ」になっていた興味深かったのは、このあとの各エージェントの振る舞いです。全滅して止まるかと思いきや、エージェント(Sonnet)は「undefinedを含むパスはおかしい」と自律的に判断し、スクラッチパッドやプロジェクト直下など、思い思いの場所に成果物を保存して作業を続行しました。止まらなかったこと自体は助かった一方、成果物が複数箇所に散らばって回収の手間が発生し、検証役の1体は他エージェントのレポートを見つけられず検証をスキップする実害も出ました。
効いた対処は、パスなどの定数を実行時の受け渡し値に頼らず、スクリプト文字列に直接埋め込むことでした。テンプレートリテラルで組み立てる場合も、実行前に確定値へ焼き込みます。受け渡し値を使う設計にする場合は、冒頭で文字列化されている可能性を疑い、オブジェクトとして復元する防御を入れておくと安全です。
「使っている場所は0件」——その調査は、ナビの実体を見ていなかった
パフォーマンスを担当した監査エージェントが、こう報告してきました。「あるCSSファイル(114KB)は、サイト全体で使用箇所0件。該当ページから削除すべき。リスクはゼロ」。根拠は、HTML/PHPファイルを対象にしたgrepで、そのCSSが定義するクラス名が1件もヒットしなかったことでした。
ところが、このサイトの一部はSSI(Server Side Includes)構成で、全ページ共通のグローバルナビゲーションは、監査対象に含めていなかった.inc拡張子のファイルにありました。そこを確認すると、削除対象とされたCSSのクラスが21箇所で使われていて、全ページのナビゲーションのホバー演出が、まるごとそのCSSに依存していました。指示どおりに削除していたら、全ページのUIが壊れていたことになります。
気づけたのは、オーケストレーター役のエージェントが、別の文脈でその.incファイルをすでに読んでいたという、ある意味たまたまの経緯でした。しかも、この所見を裏取りするはずだった検証エージェントは、地雷1の余波でレポートの保存場所がずれていたために元の報告書を見つけられず、検証されないまま通っていました。最終的に発覚したのは、人の手で裏を取ったからです。地雷1(値の受け渡しの失敗)が、地雷2(誤った断定の見逃し)を素通りさせる導火線になっていたわけです。
この件以来、「使用箇所0件だから削除してよい」系の提案を受け取ったら、grepの--includeフィルタが、そのプロジェクトのファイル種別(.inc・.tpl・.liquidなど拡張子が特殊なテンプレート)を網羅しているかを必ず見るようにしています。監査→検証と段を重ねても、検証側が同じスコープの見落としを引き継いだり、そもそも検証に届いていなかったりすれば、誤りはそのまま素通りします。「複数の観点で検証したのに棄却が0件だった」は安心材料ではなく、検証そのものが甘い兆候かもしれない。今回いちばん重く受け止めたのは、この点でした。
裏取り役を付けて効いたのは、棄却ではなく事実の精密化だった
7体の検証エージェントは、所見そのものを棄却した件数こそ0件でしたが、「行番号が3ファイルとも実際とずれている」「レスポンシブのブレークポイントは767pxではなく479pxだった」「該当ファイルは2ファイルではなく1ファイルだった」といった、付随する事実の訂正を複数出してきました。
この中で479pxの訂正は、所見の深刻度を上げる方向に働きました。当初「電話番号のCTAが768〜1000px幅で消える」としていた報告が、実際には「480〜1000px幅」で消えることが検証で判明し、影響を受けるビューポート幅の範囲が広がったからです。監査→検証という2段構えは、今回のケースでは誤指摘をゼロにする効果よりも、事実そのものを精密化する効果のほうが大きく出ました。
手元のソースだけで「入っていない」と判定してはいけない
.gitignoreで画像・PDF・JSファイルなどを一律で除外しているリポジトリでは、ローカルのファイルだけを見た「リンク切れ」「ファイル欠落」判定は、偽陽性だらけになります。本番へのHTTP実測(curl -sIでLast-Modified・404・Content-Encodingなどを確認)を一次証拠にする必要がありました。
今回も、本番サーバーにだけ存在するincludeファイルがあり、ローカル起点のgrepでは「該当タグが未設置」に見えるのに、実際には本番では設置済み、という誤診が起きかけました。SSI・インクルード構成のサイトを監査するときは、ローカルのソースコードだけで判定を確定させず、本番HTMLの実取得を必須の手順にすることが要ります。
後日、別の案件で同じ症状が出て、原因を思い違えていたと分かった(2026-07-29 追記)
地雷1を書いてからしばらくして、別の受託案件のサイト分析で、39体規模のエージェントを並列実行させる機会がありました。そこでも同じ症状が出ました。返ってきたパスにundefinedが2箇所混じっていたのです。
最初は「今回も、呼び出し側がargsをJSON文字列として渡してしまったのだろう」と考えました。Workflowツールのドキュメントには「配列やオブジェクトは実際のJSON値として渡せ、JSON文字列化して渡すな」という注意書きがあり、症状がそれとよく似ていたからです。そこで次の実行では、argsを正真正銘のJSONオブジェクトとして渡し、念のためスクリプトの冒頭にif (typeof args !== 'object') throw new Error(...)という防御を入れました。
結果、その防御にいきなり引っかかりました。「argsが不正: string」。オブジェクトとして渡したはずなのに、スクリプト側には文字列で届いていたのです。原因は呼び出し方ではなく、この実行環境ではargsが常に文字列化されて渡ってくるという、もっと手前の話でした。ドキュメントの注意書きと症状が似ていたので早合点しましたが、実際に検証するまで本当の原因には辿り着けませんでした。
効いた対処は、渡し方を疑うのをやめて、受け取る側で必ず正規化することです。
if (!A || !A.repo) throw new Error(`argsが不正: ${typeof args}`)
// 以降は args ではなく A を使う。args.xxx を1箇所でも残すと同じ穴に落ちる
今回は規模が大きい分、地雷1と同じ現象がよりはっきり出ました。6体の偵察役エージェントがいずれも「undefinedを含むパスはおかしい」と判断し、リポジトリ直下・別ディレクトリ・自分のスクラッチパッドと、6体が6通りの場所に着地しました。さらに厄介だったのは、後続のエージェントに「偵察役が書いたファイルを必ずReadせよ」と指示していた点です。指定したパスが存在しないためそのReadは全部失敗していたのですが、プロンプトには偵察役の返り値の要約もあわせて埋め込んでいたため、後続は要約だけで作業を進めてしまい、これもエラーとしては表面化しませんでした。地雷2で書いた「エラーが出ないので気づけない」という壊れ方は、入力側が壊れているケースでも同じように起きます。
おまけの地雷もひとつありました。スクリプトを直接編集して実行し直す運用で、Pythonでファイルを書き戻したところ、次の実行がscript contains control characters that would be hidden in the approval dialogという理由で拒否されました。原因は改行コードです。Windows上でio.open(path, 'w', encoding='utf-8')と書くと\nが\r\nに自動変換され、余分な文字が承認ダイアログに表示できないとみなされていました。newline=''を付けて改行を変換させないようにすると直ります。
今度は例外すら出ないまま、全エージェントの指示が静かに汚れていた(2026-08-06 追記)
地雷1と07-29の追記は、どちらもargsの型や渡し方が壊れて、スクリプトが例外で止まる、あるいは止まらなくても症状が比較的分かりやすく表面化するケースでした。さらに別の受託案件(ECサイトの社内向け資料作成、Workflowで15体を並列実行)で、今度は例外が一度も出ないまま、全エージェントの指示が静かに汚染されるケースに遭遇しました。
スクリプトの冒頭は素直に受けていました。
const OUT = args.out
各エージェントへの指示はテンプレートリテラルで組み立てていました。
リポジトリルート: ${ROOT}
…
【出力】調査結果を ${OUT}/research/xxx.md に Write せよ。`)
実行後、進捗ログのプロンプトプレビューには「リポジトリルート: undefined」とあり、返り値のサマリもoutDir: "undefined/research"を返していました。argsがスクリプトに正しく届いていなかったのです。
07-29の追記で入れた防御——typeof args !== 'object'なら例外を投げる——は、今回はまったく無力でした。args自体はちゃんとオブジェクトとして届いており、壊れていたのは個々のキーの中身だったからです。args.rootがundefinedでも、それはプロパティアクセスの失敗ではなく単に値がundefinedというだけなので、例外にはなりません。そして${undefined}はテンプレートリテラルの中で構文エラーにも例外にもならず、ただの文字列"undefined"に化けるだけです。結果、構文的には完全に正常な、しかし中身は嘘のプロンプトが15体全員に配られました。
さらに厄介だったのは、優秀なエージェントほど壊れた指示を自力で辻褄合わせしてしまう点でした。調査系のエージェントはルートがundefinedでも、カレントディレクトリから正しいリポジトリを自力で見つけて調査を完遂しました。保存先がundefined/research/...と解決不能だったエージェントは、自分の判断で検証対象と同じディレクトリ配下に保存し、「指示されたパスが解決不能だったため、検証対象と同じ配下に保存した」とレポート冒頭に一言添えていました。この注記を読まなければ気づけず、返り値のサマリだけ見ていたら「15体全員成功」で終わっていたはずです。
今回効いた検知は3つでした。①返り値に、渡したはずの値を必ず1つ含めて返させる——outDirを返させていたおかげで"undefined"という文字列が目に入り一発で気づけました。②進捗のプロンプトプレビューを1本だけ確認する——15本全部を読む必要はなく、1本目冒頭のundefinedの有無だけ見れば十分でした。③スクリプト冒頭でキー単位のガードを入れる。
if (!args || typeof args[k] !== ‘string’) {
throw new Error(`args.${k} が未指定。全エージェントのプロンプトが汚染されるので中断する`)
}
}
07-29の教訓は「argsの型を受け取り側で正規化する」でした。今回分かったのは、型が正しくても、キー単位で中身を検算しないと同じ穴に落ちるということです。「落ちなかった=正しく渡った」ではありません。動的に組み立てるプロンプトは、値が欠けても構文的には常に成立してしまいます。設定値・パス・日付・IDをテンプレートに埋め込むあらゆる自動化で、同じ壊れ方が起こり得ます。
エージェントの数を増やしても、断定を鵜呑みにする癖は消えない
今回のように多数のエージェントを並列稼働させると、「これだけの体制で監査したのだから網羅できているはず」という安心感が生まれやすいのですが、実際には完了報告を信じたら本番には何も反映されていなかった話で書いたのと同じ構図が、監査結果の断定でも起きうることを、今回のケースで確認しました。報告する側のエージェント数を増やすことは、報告の「もっともらしさ」を積み増しはしても、それ自体が検証の網羅性を保証してはくれません。所見が具体的な数値や「0件」という断定を含むほど、その根拠のスコープ(何を対象に調べたか)を人間が一度は確認する価値があります。
並列実行の実行環境まわりでは、サブエージェント並列実行で結果が消える・止まる話でも別の症状を書きました。同じ並列実行の仕組みを使い続けているからこそ積み上がる地雷が、実行環境側にも設計側にも確かにあります。ここまでに踏んだものを一通りまとめたClaude Code実務運用ガイドもあわせてどうぞ。
※本記事の記述は2026年7月時点のものです。


