0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コードエージェントにおけるファイルメンションの神話

0
Last updated at Posted at 2026-07-19

この記事は「The Mention File Myth in Code Agents」の日本語版です。原文は私自身が書いたものですが、文章の再構成と推敲には GLM 5.2 を使いました。元になった調査の一部も GLM 5.2 と一緒に行っています。

きっかけ

Claude Code が出たばかりの頃、公式ブログ1から二つの習慣を仕入れました。! でシェルコマンドをその場で実行することと、@ でファイルをメンションすることです。どちらも、使った瞬間に「これは便利だ」と感じました。

! のおかげで、自分の慣れたコマンドを実行して出力を Claude のコンテキストに流し込んだり、逆に経緯をすべて Claude に把握させたまま、続きの作業を自分の手で引き取ったりできました。@ も同じくらい快適でした。ファイルの場所を Claude に正確に伝えられるからです。ファイル名だけを渡すと、モデルはツールを呼ぶためのテキストを生成してファイルを探しに行かなければならず、場所を外すこともあれば、正しいファイルにたどり着くまで何往復もかかることもあります。

しばらくの間、これが私の当たり前の仕事のやり方でした。

「トークンの無駄」説

数か月後、X で @ に反対する意見を見かけるようになりました。理屈は単純で、当時は明らかに正しく思えました。@ は魔法ではない。クライアント側で動く決定的なコードがディスクからファイルを読み、その中身をユーザーのプロンプトに展開して、丸ごと LLM に送っているだけだ2。だから巨大なファイルを @ すれば、膨大なトークンを無駄にすることになる——。

なるほど、と腑に落ちました。ファイルメンションはそう実装するしかない。! がシェルを呼ぶのと同じで、本質的には決定的なプログラムなのだから、この手の問題は避けがたい。そう納得したのです。

バイブコーディングの時代

やがてバイブコーディングが流行り、モデルは目に見えて賢くなり、気づけば私はほとんど @ を使わなくなっていました。モデルの作業を眺めていると、必要になるたびにディレクトリ構造を確認するし、何かのファイルを読んでと頼めば、たいていはもう場所を知っています。コンテキストウィンドウも軒並み大きくなったので、ツール呼び出しと往復を数回余分にさせるくらいは大したコストではなくなりました。その代わりに、正確なパスを打ち込まずに曖昧な言い方でファイルを指せるようになったわけです(そう、ボトルネックはもはや私自身——私のタイピング速度——になっていたのです)。

それから長いこと、@ の実装のことは考えもしませんでした。

自分でエージェントを作ってみたら

状況が変わったのは、自作のコードエージェント paimon を作り始めてからです。ファイルメンションを実装する段になって、あの古い疑問が戻ってきました。改めてじっくり考えてみると、昔の結論は必ずしも正しくないように思えてきたのです。

  • @ は入力時のローカルな補助(ファイルパスの補完)にすぎず、プログラムは中身を一切プロンプトに読み込まない、という設計もありうるのでは?
  • もう一歩進めるなら、ファイルメンションは確かに決定的な処理なのだから、エージェントが一度ファイルを読んだ時点で、全文と一緒に内容の sha256 のような追加情報を送れるはずです。次に同じファイルを読むとき、送信済みで sha256 も変わっていないとわかれば、メタデータだけを送って「このファイルは読んだことがあって、中身も変わっていない」とモデルに伝えられるのでは?
  • そうだとして、本当に中身を展開する必要が出たとき、コンパクション(履歴の要約)はどう扱えばいい? 何か込み入った管理機構が要るのでは?

考えた末、メタデータを渡すことを軸にした設計にたどり着きました。メンションされたファイルは XML のエンベロープで包み、ファイルパス、内容の sha256、同封したのが全文か一部か、含まれる行範囲、実際の総行数をモデルに伝えます。おおよそこんな形です。

<file
  path="src/parser.py"
  sha256="3f6a…"
  content="partial"
  lines="1-200"
  total-lines="1240">
...
</file>

同じセッション内で既に全文を送っていてハッシュも変わっていなければ、参照用のメタデータだけを送り、「読了済みで変化なし」とモデルに知らせる。一定サイズを超えるファイルは先頭だけを送り、本当の長さはメタデータに書く。この取り決めを見たことのあるモデルは存在しないので、ルールはシステムプロンプトに明記する。裏側では、これまでに送ったすべてのファイル(ハッシュと、正確に何を送信したか)をメモリ上のテーブルで管理し、コンパクション後は「全文送信済み」が成り立たなくなるのでリセットする。積み上げていくと、なかなか複雑な状態遷移図になりました。

面倒そうに聞こえますが、ChatGPT 5.6 sol の助けを借りると、状態とフローはすぐに整理できました。それどころか、気を利かせて設計をひとつ上乗せしてくれました。ユーザーが @filename:10-20 の構文で行番号を固定した場合も、全文を送った範囲を記録しておき、後のメンションが以前送った範囲と重なって完全に覆われているなら、範囲をマージできる、というものです。この重なりの計算——どの範囲が送信済みか、どれが部分的に交差するか、どれをひとつにまとめるべきか——が、設計全体の中で本当に厄介な部分でした。

待てよ、それは Cursor がやったことでは?

そこでふと我に返りました。この仕組み、複雑になりすぎていないか?

思い出したのは、Cursor が登場したばかりの頃に、その内部の仕組みをみんなが分析し推測していたことです。曰く、LLM のコンテキストウィンドウは小さいから、コードは RAG で検索して関連する断片だけを送っている3。さらに踏み込んだ分析では、ツール呼び出しがファイルやコマンドの操作を終えると、呼び出し周辺のコンテキストは丸ごと捨てられ、結果だけが縫い戻される、という推測までありました。後半が本当だったかはさておき、当時信じられていたその設計は実に巧妙に見えました。ただ、私自身が使った範囲では、長めのエージェント的タスクでの結果はぱっとしませんでした。その後 Claude Code が現れて、ほぼ正反対のことをやりました。rg などの素朴なコマンドで力任せにコードを探し、コンテキストを巻き戻すことは決してせず、ウィンドウが埋まってコンパクションが走るまで、ひたすらすべてを前へ積んでいく。私にとっては、こちらのほうが劇的にうまく動いたのです。4

LLM の仕組みを完全に理解しているとは言いませんが、Claude Code のやり方を見て、あの噂の設計が本質的に何をしていたのかに気づきました。コンテキストウィンドウを一種のデータベースとして扱い、LLM に送ったものは確実に記憶されると仮定していたのです。しかし、いま主流の attention ベースの LLM はそんな保証をしてくれません。コンテキストに既にある内容が確実に使われるとは限らず、長い履歴の奥深くに沈んでいる場合はなおさらです5。だからこそモデルは同じものを読み直そうとする。

そして今、私はまさにその種の「巧妙な仕組み」を設計していました。範囲のマージ、ハッシュによる重複排除、「ハッシュが一致したらメタデータだけ送る」というトリック。どれもコンテキストウィンドウを正式な記録のデータベースとして扱うものです。同じ間違いを繰り返しているのかもしれない、と思いました。

事実確認

幸い、いまやオープンソースのコードエージェントはそこら中にあり、有名どころも少なくありません。理屈をこねる代わりに、去年たどり着いた結論が本当だったのか、実際に調べることにしました。opencode と GLM 5.6 を使って、よく知られたオープンソースエージェント 5 つ——piopencodegemini-cligrok-buildcodex——のコードを読みました。6

最初の問い——そもそも @ はファイルの中身を送るのか——の時点で、早くも足並みが揃いません。5 つのうち 4 つはファイルを読んでユーザーメッセージにインライン展開します。pi は <file name="..."> で包み(ただし CLI の起動引数として渡したファイルに限られ、対話的な TUI では @ は単なるパス補完で、パスの文字列がそのまま送られます)、grok-build は行番号付きの <file_contents path="...">、gemini-cli は --- Content from referenced files --- というマーカーの間に挟み、opencode はツール呼び出しを偽装します——モデルから見ると Called the Read tool with the following input: {...} に標準的な Read の出力が続き、まるで自分でツールを呼んだかのように見えるのです。そして codex だけはファイルをまったく読みません。@ はファイル名のあいまい検索で、候補を選ぶとパスがただのテキストとして挿入されるだけ。中身が気になれば、モデルが cat なり rg なりすればいい、という設計です。まさに私の最初の箇条書きにあった「@ はただの補完かもしれない」という可能性そのものでした。

エージェント @ が送るもの パス以外のメタデータ 行範囲の構文 送信済みの記録
pi 全文、切り詰めなし(CLI 引数のみ) なし
opencode 自前の Read ツール経由の内容(2000 行 / 50 KB 上限)、ツール呼び出しとして偽装 切り詰めの注記:「Use offset=N to continue」 @file#12-18
gemini-cli 全文(2000 行上限、20 MB で拒否) read_file へ誘導する切り詰め警告
grok-build 推定 ~5,000 トークンまで全文、超過分はメタデータのみのスタブ 巨大ファイルには skipped="true" と理由 @foo.rs:10-20
codex 何も送らない——パスをただのテキストとして挿入

続いて、paimon の設計をこれらの実装と一項目ずつ突き合わせてみました。

ハッシュを送るものは皆無で、ファイルの本当のサイズは切り詰めの注意書きにしか現れない。 エンベロープが運ぶのはパスと中身、実質それだけです(opencode の Read 風出力は末尾に総行数を付けます)。私のメタデータ設計に一番近いのは grok-build の巨大ファイルの扱いで、推定 5,000 トークンを超えると本文を丸ごと落としてスタブだけを送ります——<file_contents path="..." skipped="true" reason="file too large (~5800 estimated tokens, limit 5000). Use read_file tool to read specific sections."/>。切り詰めを行うエージェントはどれも同じパターンです。切れたことをモデルに伝え、自前の read ツールへ誘導する。メタデータは「自分で読みに行け」というヒントであって、重複排除のキーとしては決して使われません。

送信済みの内容を追跡するものも皆無。 ハッシュも mtime もメモリ上のテーブルも、「このファイルはもう持っている」という分岐も存在しません。メンションのたびにディスクを読み直し、全文を送り直します。唯一の重複排除は、1 つのメッセージ内の重複メンションをまとめる Set だけ。メンション経路で見つけた唯一の sha256(grok-build のもの)は、ディスク上の退避ファイルの命名に使われるだけで、モデルには届きません。唯一の本物の反例は gemini-cli の奥に埋まっていました。ContextCompressionService というクラスがあり、ファイル内容をハッシュ化し、小さなモデルに各ファイルを FULL / PARTIAL / SUMMARY / EXCLUDED へ振り分けさせる——私の設計の一部と驚くほど似ています。ただしデフォルト無効の実験フラグの裏にあり、私が読んだコミットの時点ではランタイムのどこからもインスタンス化されておらず、仮に動いたとしても処理するのは read ツールの応答だけで、@ メンションのインラインは対象外。同じことを考えた人はいた。けれど、世に出てはいないのです。

行範囲の構文はあるが、重なりの管理はない。 opencode は @file#12-18(offset と limit 付きの Read 呼び出しに変換されます)を、grok-build は @foo.rs:10-20 をサポートします。しかしどちらも送った範囲を記録せず、重なった範囲をマージもしません。すべてのメンションは独立した読み取りです。ChatGPT と私がスケッチしたあの重なりマージの機構は、どこにも存在しませんでした。

システムプロンプトで取り決めを説明するものもない。 5 つのシステムプロンプトはどれも、メンションがどんな形をしているかについて沈黙しています。pi と grok-build は XML が自己記述的であることに頼っていて、grok-build のソースのコメントは自分たちのフォーマットを「われわれがずっと使ってきた訓練フォーマット」と呼んでいます。opencode の偽 Read 呼び出しは、一番狡猾な回避策です。モデルは Read の出力がどんなものか既に知っているので、何も文書化する必要がない。そして codex には説明すべきものが何もありません。送るのはパスだけなのですから。

コンパクションはメンションを特別扱いしない。 5 つとも、履歴が要約されるとき、インライン展開されたファイル内容はただのユーザーテキストとして要約器に丸ごと渡されます。ハッシュによる振り分けも、プレースホルダーへの置き換えもありません。「状態はコンパクションをどう生き延びるのか」という私の心配は、きれいに消えました。生き延びるべき状態が、そもそも存在しないのです。

鮮度を監視するものもない。 メンションしたファイルがその後ディスク上で変更されても、コンテキスト内の古いコピーに期限切れの印を付ける仕組みはありません。モデルは、次にたまたまそのファイルを読んだときに気づくだけです。

そして一番示唆的な発見:codex はかつて逆のやり方をしていた。 TypeScript 版 CLI の時代、codex の @ は他のエージェントと同じ動きでした。導入時のプルリクエストの言葉を借りれば「ファイル内容は LLM に送られる前に自動的に XML ブロックへ展開」され、@path[50:80] という行選択が次のステップとして挙げられていました。Rust への書き直しでそれらはすべてパスのみのあいまい検索に置き換えられ、なぜ内容展開をやめたのかはコミット履歴のどこにも書かれておらず、TypeScript 実装は後に丸ごと削除されました。「展開して情報を足す」道を実際に歩いたことが確認できる唯一のエージェントが、引き返して、これ以上ないほどミニマルな設計まで戻っていったのです。

おわりに

というわけで、私の古い理解はほぼ全項目で間違っていました。一方では、いまのコードエージェントは想像よりも賢い。大半は、ファイル丸ごとを馬鹿正直に LLM へ押し込んだりしません。一定のサイズを超えれば切り詰めるか、拒否するか、「自分で読みに行け」とモデルに伝えるスタブへ差し替えます。もう一方では、私が得意になって設計したあの手の込んだシステムを必要としたエージェントは、ひとつもありませんでした。状態遷移図がどれだけ精緻に見えても、LLM はそのどれも欲しがっていない。モデルが失敗するのは、ファイルを二重に送ったときではなく、ファイルを見つけられないとき、あるいは必要な分より少なく送ったときです。あの複雑さは自分の満足のためのもので、モデルのためではなかった。

この先の話をすると、モデルは賢くなり続けていて、codex の設計はそこへの一点賭けです。ファイル名だけを送り、気になるものはモデルに取りに行かせる。エージェント的な検索が既にこれだけうまく機能していることを考えると、こちらのほうが筋のいいやり方ではないかと私は見ていて、他のエージェントが同じ方向へ寄っていっても驚きません。

そして、この小さな問題の裏には、もっと大きな困難が隠れていました。自分でエージェントを作って初めて実感したことです。「ファイルを展開するか、パスだけ送るか」といった問いは、コードを読んでも、好みで決めても、答えが出ません。実際のタスクで評価するしかなく、その評価は、これまでの趣味プロジェクトでは考えられなかった規模でトークンを燃やします。CLI ツールならローカルで数秒、タダで検証できる。エージェントの設計判断は、データポイント 1 つごとに実費がかかるのです。

さらに厄介なことに、その答えはモデル間で持ち運べないかもしれません。モデルの改善では強化学習が大きな役割を果たしていて、それは同時に、各モデルへ固有の仕事の流儀を刻み込みます。だから各ベンダーの CLI は、自社モデルが訓練で寄せられ、自社の評価で最高点を出す仕組みを自然と搭載することになる。grok-build のソースはそれを堂々と口にしています——あの XML フォーマットが存在するのは、それが「われわれがずっと使ってきた訓練フォーマット」だからだ、と。他人のモデルの上に汎用エージェントを作る人間にとって、これは静かで恒久的な悩みの種です。最良の仕組みは普遍的ではなく、かといって、すべてを測る余裕もないのですから。

  1. 「Claude Code: Best practices for agentic coding」。元は Anthropic のエンジニアリングブログの記事で、現在は公式ドキュメントの一部として維持されています。

  2. 公式ドキュメントはいまでも @ をこう説明しています:"Reference files with @ instead of describing where code lives. Claude reads the file before responding."

  3. この推測は「ローカルで」の部分を除けば、おおむね正しかったようです。Cursor はファイルの分割こそローカルで行いますが、埋め込みの計算はサーバー側で行い、リモートのベクトルデータベースに保存します。コード自体は手元のマシンに残ります。How Cursor Indexes Codebases Fast を参照。「ツール呼び出しのコンテキストを捨てる」の部分は、私の知る限り確認されたことがありません——そもそも初期の Cursor にはエージェント式のツール呼び出し自体がありませんでした。

  4. この記事のファクトチェック中に初めて知ったのですが、初期の Claude Code も RAG とローカルのベクトルデータベースを試していて、素朴なエージェント検索を選んで捨てたそうです。作者の Boris Cherny 曰く、"Early versions of Claude Code used RAG + a local vector db, but we found pretty quickly that agentic search generally works better."(初期の Claude Code は RAG とローカルのベクトル DB を使っていたが、エージェント検索のほうが概してよく機能することがすぐにわかった)。同じスレッドの別の Anthropic エンジニアも、"In our testing we found that agentic search outperformed [it] by a lot, and this was surprising."(テストではエージェント検索が大差で上回った。これは意外だった)と述べています。

  5. これは雰囲気の話ではなく、測定された現象です。モデルは長いコンテキストの中間にある情報を、冒頭や末尾の情報より明らかにうまく使えません。Lost in the Middle: How Language Models Use Long Contexts を参照。

  6. 5 つとも 2026 年 7 月中旬、次のコミットで確認しました:pi 87ad8243、opencode efb6cc2d4、gemini-cli 3ff5ba2、grok-build 98c3b24、codex 315195492c。どれも動く標的なので、この記事を読む頃には細部が変わっているかもしれません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?