ブログの未参照画像を削除したくて、Claude Code の Explore subagent に調査を依頼しました。返ってきた要約は 「未参照は2枚だけです」。
念のため自分で直接 grep を打ったら、107枚が未参照でした。実際に削除したのは87枚。Agent の報告と桁が2つズレていたわけです。
これは Agent が悪いのではなく、AI agent に「該当なし」を判定させると構造的に起きる現象です。本記事は、この事故から導き出した「検証 Trinity(三層検証)」という運用ルールの記録です。
事の発端:「未参照画像を調査して削除コマンドを教えて」
ブログの public/images/ 配下には記事ごとのキャッチ画像が135枚溜まっていました。OGP 画像を代表画像1枚に統一する変更を入れたあと、過去記事ごとの個別画像が宙に浮いた状態でした。
リポジトリの掃除をしたくて、Claude Code に依頼しました。
1ユーザー: ブログで参照していない画像を調査して削除コマンドを教えてClaude Code は内部で Explore subagent を起動して調査を始めました。
Agent の第一報:「未参照は2枚」
Explore agent からの要約は、こんな趣旨でした。
1Agent: 135枚を調査した結果、明らかに未参照と思われるのは2枚のみです。2 - aidriven_prevention_of_error_recurrence.jpg3 - aidriven_windows_task_schedule.jpg4
5その他の画像はファイル名と記事スラッグが一致しているため、参照されていると推定されます。この瞬間、私は危うく「2枚だけなら手で消すか」と納得しかけました。しかし違和感がありました。
最近、私は OGP 画像を全記事 bg.jpg に統一する変更をコミットしたばかりです。記事ごとの個別 OGP を廃止したのに、なぜ「ファイル名と記事スラッグが一致 = 参照されている」と判定されるのか?
直接検証してみたら桁が違った
Agent の summary を信じず、自分で grep を打ち直しました。
Step 1: 全画像と「実際の参照」を抽出
1ls public/images/ > /tmp/all_images.txt2
3# ソースコード内の /images/... 参照を全部拾う4grep -rhoE '/images/[a-zA-Z0-9_.-]+' src/ public/ astro.config.mjs 2>/dev/null \5 | sed 's|/images/||' | sort -u > /tmp/referenced.txt結果は 26件 しかありませんでした。
Step 2: 記事 frontmatter の image: 指定もチェック
Astro の記事は frontmatter で image: を指定すると個別 OGP 画像を持てます。
1grep -c '^image:' src/content/blog/*.md結果は 0件。誰も使っていませんでした。
Step 3: fallback を template から読む
1const defaultOgImage = 'bg.jpg'2const resolvedImage = image || defaultOgImageつまり、全記事の OGP は bg.jpg にフォールバックしていました。「ファイル名 = 記事スラッグ」というルールは存在しなかったのです。
Step 4: 差集合で未参照を確定
1sort -u /tmp/referenced.txt固定keepリスト > /tmp/keep.txt2comm -23 /tmp/all_images.txt /tmp/keep.txt > /tmp/unreferenced.txt3wc -l /tmp/unreferenced.txt4# → 107107枚。Agent 報告の 53.5倍でした。
なぜ Agent は間違えたのか
Explore agent の判断ロジックを推測すると、こうだったはずです。
ファイル名
aidriven_xxx.jpgと記事スラッグaidriven_xxx.mdが対応しているから、おそらく参照されているだろう
これは ヒューリスティック(経験則による推定) です。一般的な Astro ブログではこのパターンが多いので、確率的には正しい推論です。
しかし、私のブログは 少し前に OGP 統一の変更を入れていたため、その経験則が成立しなくなっていました。Agent はその「最近の変更」を知らないまま、一般則で判断したのです。
検証Trinity:三層で「該当なし」を裏取りする
この事故から、AI agent の「網羅した/該当なし」報告に対して、3つの層で裏取りする運用を作りました。
層1: Agent summary(最速・最も誤りやすい)
Agent の要約。手軽だが、特に 「該当なし」「未参照」「網羅完了」 系の判定は信頼度が低い。手がかりとしては使うが、最終判断には使わない。
層2: 直接 grep / 差集合(中速・確実)
ソースコードを自分で grep する。Agent と同じ場所を見ているように思えるが、全件突合のロジックを自分で書くことで、ヒューリスティック判定の余地を排除できる。
1# 全件 - 参照 = 未参照、を機械的に計算2comm -23 <(sort 全件) <(sort 参照件) > 未参照層3: ビルド後生成物の grep(最遅・最も確実)
Astro/Next.js のような静的サイトジェネレータなら、ビルド後の dist/ を見れば 実際に HTML に焼かれた参照 がわかります。
1pnpm build2grep -rhoE '/images/[a-zA-Z0-9_.-]+' dist/ | sort -u > /tmp/dist_refs.txt3
4# 削除予定の画像が dist に出てこないか確認5while IFS= read -r ref; do6 [ -f "public/images/${ref#/images/}" ] || echo "MISSING: $ref"7done < /tmp/dist_refs.txtMISSING: 行が 0 なら、ビルド成果物として 404 にならないことが保証されます。
適用シーン:いつ Trinity を使うべきか
毎回これをやる必要はありません。コストが高くなる判定にだけ適用します。
| 判定タイプ | Trinity 必要? | 理由 |
|---|---|---|
| 「このバグの原因は X です」 | 不要 | バグ修正は逆に検証可能(再現テスト) |
| 「この関数の使い方は Y です」 | 不要 | 動かして確かめられる |
| 「未参照ファイルは N 個です」 | 必須 | 削除後に取り戻せない |
| 「該当する設定は見つかりません」 | 必須 | 「存在しない」の証明は本質的に難しい |
| 「全ファイルをチェックしました」 | 必須 | 網羅性は自己申告では検証不能 |
ポイントは 「false negative(見落とし)が致命的になる操作」 で必須化することです。削除・マイグレーション・一括置換が典型例です。
結局、何枚削除したか
最終的な作業ログ。
- 調査: 135枚中 107枚が未参照 と判明
- 削除: そのうち 87枚をコミット(残り20枚は未追跡ファイルで別途整理)
- 検証:
pnpm buildで 405ページ生成、画像404はゼロ - 残: 47枚(必要な固定アセット + 最新記事用)
Agent が言っていた「2枚」のままだったら、133枚のゴミが永遠に残るところでした。
まとめ:Agent の沈黙誤判定を消す
AI agent は 「ある」を見つけるのは得意ですが、「ない」を証明するのは苦手です。これは AI の限界というより、「網羅性は自己申告では検証できない」という論理的な性質です。
だから、削除・移行・棚卸しのような false negative が致命的な操作 では、Agent の要約を一次情報として使いつつ、自分で機械的な突合を書いて裏取りする。これが検証 Trinity の本質です。
Agent を使わない、ではなく、Agent の出力を「仮説」として扱い、自分で証拠を集める。AI 駆動開発で最も効くのは、この一段階の懐疑です。