Toss Bank の ML エンジニアが書いたこの記事は、著者がコーディングエージェントのコードの読み方に起因すると考える問題から出発する。エージェントはコードベース全体の文脈を取り込むのではなく、変更を加えるのに必要な部分だけを読み、その範囲内で書く。そのため、プロジェクト全体の規模で効いている規約を守り通すことができない。著者はこれを、人間の開発者が規約を身につける方法と対比している。人間は他人が書いたコードを読み、レビューコメントをやり取りするうちに、記事の言葉を借りれば、何も書き残さなくても時間が全員の足並みをそろえていく。
それ以前の 2 つの試みが紹介され、退けられている。規約をレビューコメントとして残すやり方では、チームの合意がたまたまそれを覚えている人に依存してしまう。プロジェクトルートの指示ファイル(記事はそうしたファイルの例として AGENTS.md を挙げている)に書くやり方は、別の形で失敗する。エージェントはセッションの開始時にファイルを読み、序盤はそれに従うが、セッションが長くなるにつれて従わなくなる。著者はこれを Lost in the Middle に帰している。さらに 2 つ目の反論として、細かなルールはそもそもそうしたファイルに入れるとコストが高いと述べる。当てはまるかどうかにかかわらず、すべてのプロジェクトがそのルールの分のコンテキストを負担することになるからである。
記事自身の答えは、セッションの冒頭を、チームのルールが入り込める唯一の場所として扱うのをやめることである。エージェントループについての記事の見立ては、1 つのユーザーリクエストが数十回のツール呼び出しを引き起こし、そのループの繰り返し部分には何も注入されていなかった、というものである。チームは、従来のリンターのフィードバックループ(コードを書き、リンターを実行し、エラーをフィードバックし、修正し、繰り返す)の形を借り、リンターの位置にチームの規約を置いた。規約は、チームが pfmls-stylepack と呼ぶプラグインによって、エージェントフック を通じて届けられる。
要点
- エージェントはコードベース全体ではなく、自分が加えている変更の局所的な文脈から作業する。そのため、守れなくなるのはプロジェクトレベルで効いている規約である。
- セッションの冒頭にだけ置かれたルールは、序盤は守られ、後になると落とされる。著者はこれを Lost in the Middle に帰し、会話が長くなるにつれて、ルールはモデルが最も注意を向けない位置である中間部分に追いやられると説明している。
- 問題は、エージェントに何を伝えるかではなく、ルールをいつ、いくつ提示するかの問題として捉え直されている。
- リクエストごとに数十回のツール呼び出しが行われるエージェントループの繰り返し部分こそが、チームの規約が欠けていた場所だとされる。
- チームの 2 つの注入ポイントは、意図的に異なるものにしてある。1 つはファイルが書き込まれた直後で(そのファイルの本文のみを対象とし、高速で、ルールは最大 2 つ、その場でコードを直させることが狙い)、もう 1 つはエージェントが作業を終える直前である(変更全体を
git diffとして対象とし、より遅く、ルールは最大 4 つで、最後のセーフティネットと説明されている)。 - ルールの選択は、モデルを呼び出すのではなく、ファイル名のパターンと正規表現で行う。フックがファイルの書き込みのたびに実行されるためである。著者は、AI ベースの関連性チェックを加えるとリクエストごとに約 10 秒かかったと報告し、関連性の最終判断はエージェントが下すのだから、フックは候補を絞り込むだけでよいと論じている。
- 単一ファイルを対象とするフックでは、複数のファイルにまたがる欠陥は見えない。記事の例は、サービスクラスがリポジトリのインターフェースではなく実装に依存しているケースで、各ファイルは単体では正しい。ループ終了時の注入ポイントは、これを捕まえるために存在する。
- トリガーが緩すぎるルールは、ルールがないより悪い。著者は、辞書リテラルだけでなく Python の f-string の書式指定子にもマッチしてしまった正規表現が、21 のセッションで発火し、その結果のコード変更がゼロだった例を報告し、誤ったフィードバックはエージェントにルールを無視することを覚えさせると論じている。この数値は、チームが自らのプラグインに対して行った計測によるものである。
- 著者は、そもそも何をルールにすべきかについて線を引いている。コードの形から認識できる欠陥はトリガー付きのルールにし、実行時にしか現れない欠陥(例として N+1 クエリ)は、ループの書き方が増え続けるため、代わりに常に読み込まれるコンテキストに置くべきだとしている。
- テンプレートリポジトリをコピーして規約を配布する方法は、テンプレートはコピーされた瞬間から古くなり始めるという理由で退けられている。チームは代わりに、ルールを 1 つの中央リポジトリで管理し、プラグインがセッション開始時にバックグラウンドでそれを取り込むようにしている。
背景
この記事は、エージェントフック を、モデルが従うかどうかわからない指示に対する決定論的な対抗手段として扱う他の文章と並ぶものだが、その仕組みを別の目的に用いている。ツール呼び出しを許可したりブロックしたりするのではなく、ここでのフックは、エージェントが従っても無視してもよい文章を注入する。ループ終了時のテキストはそのことを明示しており、自らを厳格な失敗ではなく、レビューを促すリマインダーだと称している。
この記事は全体を通して、1 つのチームが直面した問題とその解決方法を一人称で語ったものであり、裏付けとなる数値もそのチーム自身のものである。約 10 秒のオーバーヘッドは、AI による関連性チェックを一時的にフックに付けたときに計測されたもので、コード変更につながらなかった 21 のセッションは、チームが後からプラグイン自体に追加したロギングによるものである。記事は、このプラグインが社外で利用可能かどうかは述べていない。あるコメント投稿者はヒット率の問題を直接取り上げ、enum の例のように広いトリガーでは、コンテキストを注入する頻度が高すぎて割に合わないと論じている。