月曜日の朝、5件の重大なバグレポートで目が覚める。レビュー監視ツールは役割を果たしている。クラッシュレポート、怒りに満ちた星1のレビュー、「保存ボタンを押すとアプリが固まる」といった報告をすべて捉えている。何が壊れているかは正確にわかっている。わからないのは、どこを探すべきかだ。

それが、最初のパイプラインを構築した後に直面した壁だった。それはアプリのレビューやクラッシュログを問題なく監視し、フィードバックを「バグ」「クラッシュ」「機能リクエスト」といった整然としたバケツに分類していた。ダッシュボードは健全に見えた。しかし、実際のデバッグプロセスはそうではなかった。

バグが存在することを知るのは、1マイルの道のりの最初の1インチに過ぎない。IDEを開き、モジュールをgrepで検索し、スタックトレースを現在のコードベースと照らし合わせ、頭の中で失敗の経路を再構築しなければならなかった。チケットが積み上がり、コーヒーがまだ熱いうちに、その手動の「考古学」作業は、本来使うべきではない時間を奪っていく。パイプラインには、単に問題をフラグ立てする以上のことが必要だった。問題を「調査」してほしかったのだ。

そこで、単一の目標を中心にシステムを再構築した。それは、生のバグレポートを受け取り、検証済みの診断結果を返すことだ。LLMによるとりとめのない考察の段落ではなく、ファイル名を特定し、行数を指し示し、リスクを推定し、修正案を提示する、構造化された調査結果だ。その構築プロセスを以下に記す。

チャットログよりも構造化が勝る理由

調査エージェントはPydanticAIを使って構築した。理由は単純だ。言語モデルにコードについて推論させる際、デフォルトの出力は親しみやすいテキストのストリームになる。それは人間の読者には役立つかもしれないが、後続のスクリプトにとっては役に立たない。私が必要としていたのは、機械判読可能な契約(contract)だった。

エージェントは、根本原因、影響を受けるファイル、提案される変更、複雑さとリスクの評価という4つの特定のフィールドを持つ、検証済みのデータモデルを返す。モデルにフィールドが欠けていたり、ファイルパスをハルシネーション(捏造)したりした場合、バリデーションが失敗し、即座に検知できる。この厳格さがパイプラインの信頼性を保つ。

実際の探偵作業を行うために、エージェントには4つの読み取り専用ツールのみを与え、それ以外は一切与えないようにした。grepによるコード検索、ファイル内の特定の行範囲の読み取り、ディレクトリ内容のリスト表示、そしてクラスや関数などのシンボルの特定ができる。重要なのは「読み取り専用」であることだ。午前2時に、書き込み権限を持つエージェントがリポジトリ内を徘徊するのは避けたい。まずは理解し、それから編集するのだ。

リポジトリマップ:ツールを使う前のコンテキスト

エージェントの最初のバージョンは正確だったが、コストが非常に高かった。まるで円を描いて歩き回る観光客のように、トークンを浪費していた。モデルはlist-dirを呼び、次にgrep、次にファイルを読み、またlist-dirを呼び……というように、高価なトークンを一つずつ消費しながら、プロジェクト構造のメンタルモデルをゆっくりと組み立てていた。

解決策は、エージェントが動き出す前にコンパクトなリポジトリマップを生成することだった。このマップは、リポジトリの要約を抽出したものだ。主要なファイル、それらの主な関数やクラス、そして主要なモジュールがどのように接続されているかを示す。エージェントに、試行錯誤して道を探索させるのではなく、GPSを手渡すようなものだと考えてほしい。

そのマップをコンテキストウィンドウに入れておくことで、エージェントはsrc/utils/parser.tsが存在するかどうかを確認するために無駄な呼び出しを行う必要がなくなる。すでに地形を把握しているからだ。エージェントは煙が上がっている尾根へと直行する。この一つの変更により、彷徨うフェーズを完全に排除することができた。

ツール・ファンネル:結論を強制する

マップがあっても、エージェントは躊躇することがあった。怪しいファイルを見つけては、自分を疑い、また検索し、別のファイルを読み……という、「もう一度だけ確認させて」という終わりのないループに陥ることがあった。推進力を強制する方法が必要だった。

私は、進行状況に応じてエージェントができることを制限する、3フェーズのツール・ファンネル(漏斗)を実装した。

フェーズ1は探索(Exploration)だ。エージェントは4つのツールすべてにフルアクセスできる。推論の中でバグを再現するために必要な検索、閲覧、読み取りが可能だ。

フェーズ2は深掘り(Deep-dive)だ。エージェントが原因と思われる箇所を特定すると、探索ツールは失われる。できることはファイルの読み取りのみだ。grepもディレクトリ一覧表示もできない。この段階では、既に見つけたコードを精査し、証拠の連鎖を構築しなければならない。

フェーズ3は出力(Output)だ。すべてのツールがロックされる。エージェントはもうコードベースにクエリを投げることはできない。座ってレポートを書かなければならない。これにより、「もう一つだけ確認させて」という無限ループを防ぐことができる。

このファンネルによって、1回の分析あたりの平均ツール呼び出し回数は、40回以上から約10回へと減少した。エージェントは高速化し、低コストになり、そして逆説的だが、結論を出すことを強制されたため、より確信を持って動作するようになった。

バックエンドの差し替え可能性を維持する

I did not want to hardcode the system against a single model provider. I use different engines depending on the task. Sometimes Claude Code, sometimes Grok Build, sometimes whatever is cheapest at the moment. To keep the core logic provider-agnostic, I split the work into two stages.

Stage one is exploration. The coding agent, which can be any capable model, reads the repo map, uses the tools, and produces a raw markdown report. This is the expensive thinking part.

Stage two is structuring. A cheap, fast LLM takes that markdown and reformats it into the strict Pydantic model. This stage requires almost no reasoning. It is just extraction and formatting, so it runs on lightweight hardware.

Because the boundary is clean, I can swap the backend without touching the validation logic. The markdown report acts as a universal adapter between the exploratory brain and the structured output I actually use.

What Actually Worked

This setup changed how I handle incoming issues. The classification layer still sorts bugs from feature requests, but now the analysis layer picks up immediately after. By the time I open my editor, I have a file path, a line range, and a proposed change waiting for me. I still review everything manually. This is assistance, not autopilot. But the context gathering that used to