一、先に問題を正確に捉える:Agent にリポジトリを直させても、難しいのはコード生成ではない
いま主要なコードモデルは、どれも数十秒で動く単一ファイルの関数を書き上げ、さらにダイアログに貼り付けたコードも読めます。実際、本番の現場で詰まっているのは別のことです。現実のリポジトリと、複数ファイルにまたがる要件を渡して、それでもリポジトリがビルドでき、テストが通り、同僚の review で手抜きを見抜かれない状態にすること。この二つでは難易度が桁違いです。ボトルネックは生成能力ではなく、情報到達可能性と変更の爆発半径にあります。
現実のリポジトリで発生した失敗ログを分類すると、おおむね六つに収束します。そしてその六つのうち、本当に「モデルがコードを書けない」原因に該当するのは一つしかありません。
第一に「見えない」ケースです。モデルは、ある関数がどこから呼ばれているか、あるインターフェースに実装がいくつあるか、ある設定キーが誰に読み取られているかを知りません。プロンプトに現れた断片だけを見て局所的に推論し、見ていないものを存在しないものとして扱います。この種の失敗は最も多く、しかも数千行から数万行程度の中規模リポジトリで、もう発生し始めています。
第二に「自分自身が見えない」ケースです。モデルは、自分が読んだコードが完全なものか、途中で切られていないか、ソースファイルではなくビルド成果物を読んでいないかを知りません。欠けたコンテキストの上でも「きわめて自信のある」判断を下します。この種の失敗は最も質が悪いところで、即座にはエラーにならず、往々にして三時間後のどこかのテストで爆発します。
第三に「変更しすぎる」ケースです。要件は「フィールドをひとつ追加するだけ」なのに、モデルはインポートの並び順を勝手に並べ替え、ログの措辞を変え、関数の順序を入れ替え、十数個のファイルにスタイルの変化を残します。レビューする人は論理の正誤を確かめられず、diff が四百行あることしか見えないので、書き直すことを選びます。これは能力の問題ではなく、目的関数の問題です。システムに「最小」が制約や報酬として埋め込まれていないからです。
第四に「変更しこぼす」ケースです。引数をひとつ追加して、型の定義、シリアライズ層、ドキュメント文字列、別のモジュールの呼び出し側の更新を忘れます。強く型付けされた言語ではコンパイルエラーとして現れ、動的言語ではもっと隠蔽的で、たいてい実行時に爆発します。
第五に「検証がない」ケースです。モデルは変更を終えたのにテストを走らせずに完了を宣言します。あるいは全量テストを走らせて、赤いテストがいくつか出ても「もともとからあった問題だ」と判断してしまいます。前者はエラーを main に流し込み、後者は Agent にシグナルを無視することを学ばせてしまいます。これは最悪の学習です。
第六に「破壊的操作」ケースです。死んでいるように見えるファイルをまとめて削除する、依存のバージョンを上げてしまう、公開関数のシグネチャを変えてしまう。Agent の目にはいずれも局所的な変更ですが、メンテナの目には取り消せない出来事です。
この六つのうち、第一と第二は情報層、第三と第四は目的層、第五はフィードバック層、第六は権限層に属します。この後のチェーン全体——インデックス、予算、変更集合、パッチ、検証、ゲート、自己検査——は、本質的にこの四層の問題を一つずつ受け止めるものです。「より強いモデルに替える」ことで改善はできても根治しない理由も、ここにあります。モデルがいかに強くとも、見ることを許されていないものを、凭空で知ることはできないからです。
ここで一つ、現実的な観察を補足しておきます。モデル能力の向上で実際に改善するのは、第一の「見えない」のうち最も浅い層——正しい関数本体を書く力——だけです。第二の(自分が全部読めていないことを知らない)と第六の(自分が何を動かしたかを知らない)はメタ認知の欠落であり、これはパラメータ数や学習データで決まるものではなく、実行時の制約で決まります。同じモデルでも、完全なシンボル情報、明確な変更ホワイトリスト、実行可能な検証、そして不可逆操作のゲートが与えられた環境の方が、ファイル編集欄だけ渡された環境より圧倒的に良い性能を出します。その差はモデルの能力が足りないからではなく、何を知らないのかが伝えられていないからです。
1.1 純粋なテキストの grep では足りない理由:名前と構造は別物だ
AST インデックスを作る前に、純粋なテキスト検索の失敗地点を整理しておく価値があります。これがインデックス層に追加で何を供給しなければならないかを決めるからです。最も素朴な方法は、リポジトリ全体を横断してシンボル名を検索することです。多くの場面では確かに有効であり、インデックス之上での第一層の召回(リコール)として残しておくべきものでもあります。安価で頑健、コメントやドキュメントにも親切です。ただし六つの系統的な欠陥があります。
- 同名の混同。
getやrunやHandlerといった名前は、リポジトリ全体で数百回も出てきます。全文検索が返す並び順はマッチの質で決まるもので、意味的な関連性と名前の類似性にはまったく関係がありません。得られるのは関連性の並び順ではなく、ノイズの並び順です。 - オーバーロードと隠蔽。同じファイルに三つの
sendメソッドがあるとき、呼び出し側のself.send(x)がどれを指すかは、静的型と引数の個数で決まります。テキスト検索は三つの候補を返すだけで、モデルは自分で当てなければなりません。 - エイリアスと再エクスポート。
from .handlers import send as dispatchと書いたあと、dispatchという名前はリポジトリ全体を grep しても一度も登場しませんが、実在する呼び出し点です。逆に、公開エントリポイントのsendがパッケージレベルのapi/__init__.pyに再エクスポートされている場合、業務コードでapi.sendを grep すると、転送の一行しか出てきません。 - 動的構築。レジストリパターン、プラグインのロード、DI コンテナ、文字列で書かれたテンプレート名、デコレータによる挙動の書き換え——これらは構文上まったく正当で、テキスト検索でも素朴な AST 抽出でも見落とす可能性が残ります。
- 言語横断と生成物。IDL 定義から生成されるクライアント、フロントエンドのスキーマから生成される型、protobuf が生成するスタブコード、テンプレートが生成する設定。これらの「派生コード」とソースファイルの対応関係は、どの言語サーバーにも存在しません。ビルドルールを逆に辿るしかありません。
- 定義と使用の距離。200 箇所から参照されている定数をひとつ変更したとして、テキスト検索は 200 個のマッチを教えてくれます。しかし実際に到達可能なパスがどれか、どこが死んでいるコードか、どこはテストにしか現れないか、を示してくれるのは call graph だけです。
結論は「grep を AST で置き換える」ではありません。grep が召回を担当し、AST がフィルタリングと正規化を担当するということです。まず安価な方法で候補を回収し、次に構造によって「同名の異なるもの」を畳み込み、エイリアスを正式名に戻し、到達不可能な辺を重み付けで落とします。両者はカスケード関係であって、代替関係ではありません。この判断は実践の中で繰り返し裏付けられています。全文検索を飛ばしてインデックスだけで位置を特定しようとする方式は、歴史的負債を抱えた現実のリポジトリでは明らかに召回率が低くなります。インデックスが拾いきれない辺(文字列連結、動的インポート、テンプレート)が、かならず残ってしまうからです。
二、リポジトリインデックス:symbol table、call graph、そして依存関係
リポジトリのインデックスは、三種類の質問に答える必要があります。ただしこの三つでは問い合わせの形も更新の頻度もまったく違うため、ひとつのインデックスに押し込むと、通常はどちらの要求にも応えられません。
| インデックス層 | 答える質問 | 典型的なクエリ | 更新コスト |
|---|---|---|---|
| 字句層(全文検索) | この名前やこのテキストに触れている場所はどこか | 識別子、リテラル、コメント、エラーコード | 低い。差分更新に親切 |
| シンボル層(AST) | それは何で、どこに定義があり、どんなメンバーがあるか | 定義クエリ、メンバー列挙、型シグネチャ | 中。ファイル単位の解析 |
| 関係層(call graph) | 誰が依存しており,把它変更すると誰に影響が及ぶか | 逆参照、影響範囲、到達可能性 | 高い。ファイル横断の曖昧性解消が要る |
三層を並べて見ると、しばしば忘れられがちな第四の種類の情報があります。それがビルドとテストの構造です。どのテストファイルがどのソースファイルをカバーするか、どのコマンドで再コンパイルが走るか、このパッケージが CI 上で持つジョブ名。静的分析には属しませんが、第六節の「最小検証セット」を構成できるかどうかを直接左右します。インデックスがこの部分を含まなければ、検証段階は全テストを走らせるという泥臭い方向に退行してしまいます。
2.1 tree-sitter で構文レベルの決定性を得る
現在の主流はこうです。言語の構文解析層は tree-sitter に統一し、言語ごとに正規表現や手書きパーサを個別に保守するという方針です。理由は現実的で、tree-sitter が生成する構文木は差分更新でき、寛容で、完全な型検査を経ずに結果が出て、同一の走査コードをあらゆる言語で再利用できます。Agent システムにとって「寛容(フォールトトレラント)」という性質は何より重要です。リポジトリには、いつも編集中の、構文エラーのある、あるいはジェネレータが出力したばかりの未整形のファイルが混ざっています。パーサがファイルひとつの異常でインデックス全体を失敗させてはいけません。
次のコードは、シンボルと呼び出し辺の抽出における最小の骨格です。「走査」から「辺の正規化」までの主要なステップを示しています。現実のシステムはこの上にファイル横断の曖昧性解消を重ねますが、この構造自体は汎用のものです。
import tree_sitter
from dataclasses import dataclass
@dataclass
class Symbol:
name: str
kind: str # func / class / method / var / type
file: str
start_byte: int
end_byte: int
parent: str | None # 所属类或模块路径
@dataclass
class Edge:
src_file: str
src_line: int
callee_raw: str # 源码里写出来的名字
dynamic: bool # 是否经过 getattr / 反射 / 模板
LANGUAGE_BY_EXT = {".py": "python", ".ts": "typescript", ".go": "go"}
def extract(path: str, src: bytes) -> tuple[list[Symbol], list[Edge]]:
parser = tree_sitter.Parser(LANGUAGE_BY_EXT.get(path[path.rfind("."):], "python"))
tree = parser.parse(src)
symbols, edges = [], []
def walk(node, scope: str | None):
# 1) 定义:把每个绑定点登记为规范符号
if node.type in ("function_definition", "class_definition",
"method_definition", "type_alias_declaration"):
name_node = node.child_by_field_name("name")
if name_node is not None:
symbols.append(Symbol(
name=src[name_node.start_byte:name_node.end_byte].decode(),
kind=node.type, file=path,
start_byte=node.start_byte, end_byte=node.end_byte,
parent=scope,
))
scope = f"{scope}.{name_node.text.decode()}" if scope else name_node.text.decode()
# 2) 引用:调用、导入、类型标注
if node.type in ("call", "call_expression", "import_from_statement"):
raw = _callee_text(node, src)
if raw:
edges.append(Edge(path, node.start_point[0] + 1, raw,
dynamic=raw.startswith("getattr")))
for child in node.children:
walk(child, scope)
walk(tree.root_node, None)
return symbols, edges
このコードには強調すべき設計点が三つあります。第一に、dynamic フィールドが意図的に残されていることです。呼び出し辺は「静的に解析できる辺」と「実行時に初めて決まる辺」を区別しなければならず、前者は安心して影響範囲分析に使えますが、後者はヒントにしかなりません。正方向への伝播しか行わない失敗のほとんどは、ここが無視されていることが根本原因です。第二に、シンボルが parent のスコープチェーンを持つことで、同名のメソッドを区別できることです。第三に、走査の寛容さです。ファイルひとつの解析に失敗しても、影響を受けるのはそのファイルの辺だけで、インデックス全体が壊れることはありません。
2.2 ファイル横断の曖昧性解消:「名前」を「一意な同一性」に変える
局所的なシンボルを抽出したあと、実際に工数が集中しているのは曖昧性の解消です。中心となる手法は正規名(canonical name)です。a.b.c.method や obj.method や self.method や Other().method を、すべてパッケージパスとレシーバ型を伴う安定した識別子に戻すのです。曖昧性の解消は、優先順位の順に四種類の情報を用います。
- レシーバの静的型。言語サーバーが直接教えてくれるので、最強のシグナルです。型が不明なときは名前マッチングにフォールバックします。
- 字句スコープ。現在のファイル内の局所束縛が最優先で、次にクラスメンバー、モジュールグローバル、そして最後にインポート名です。
- インポート表。
*.importやfrom x import yから作ったエイリアスマッピングを使い、yを逆向きにx.yへ戻します。 - 一意性のフォールバック。どれでも判定できない場合でも、リポジトリ全体で同名の定義がひとつだけなら安全に確定できます。候補が複数ある場合は、任意のものを一つ選ぶのではなく、曖昧な辺としてマークして重みを下げます。
曖昧な辺は明示的にモデル化する必要があります。よくあるアンチパターンは「最初の候補を取る」ことで、これでは系統的に変更集合が小さく算出されます。大量の実際の呼び出し点が誤った対象へ帰属してしまい、影響範囲分析がそれらを見落とすからです。より保守的で、そして正しい方法は、曖昧な辺を「確実に影響を受けるもの」には数えず、捨てるのでもなく、独立した人手確認キューに入れることです。Agent は、変更がその辺に触れるときに自分から説明するようにします。
2.3 差分更新と無効化の伝播
インデックスで最も高くつくコストは「タスクごとにリポジトリ全体をスキャンし直す」ことです。1万行の小さなプロジェクトなら全走査もギリギリ許容できますが、数十万行になると破綻するため、差分更新は硬い要件になります。無効化の伝播粒度は、この方式の到達上限を左右します。
- ファイル粒度:ファイルの変更タイムスタンプが変わる → そのファイルを再解析する → 出力辺を再計算する → 入力辺の表と突き合わせ、影響を受けるシンボルをマークする。実装はもっとも簡単ですが、200 回 import されている中核モジュールが動くと、大範囲の再計算が走ってしまいます。
- シンボル粒度:各シンボルに内容フィンガープリント(正規化後のトークン列のハッシュ、空白とコメントを無視)を保存します。フィンガープリントが変わったシンボルだけが外向きに再伝播します。インデックスの構築時間はわずかに増える代わりに、伝播精度が大幅に上がり、変更が頻繁な場面ではそのほうが割に合います。
- 辺粒度:隣接表を差分的に保守し、古い辺を削除、新しい辺を追加します。精度は最も高い方式ですが、ファイル横断の曖昧性解消の扱いがもっとも複雑になります。ひとつのインポート変更が、数多くの辺の解析結果に同時に影響しうるからです。
実務で選ばれているのは、シンボル粒度のフィンガープリントとファイル粒度の再解析を組み合わせた混合方式です。解析は本来ファイル粒度が自然で、フィンガープリントはシンボル粒度の精度を持ち、どちらのコストも制御可能な範囲に収まります。もう一つ、技術とは無関係な制約にも注意が必要です。インデックスがサンドボックスの中で差分再構築できることもしばしば無視されています。インデックスが IDE の状態や、起動中のコンパイルデーモンに依存しているなら、ヘッドレスのコンテナでは動かず、自動化の流れ全体が途切れてしまいます。
2.4 インデックス之上:クエリインターフェースはどう設計すべきか
多くのインデックスプロジェクトが失敗するのは、「きれいに建成しても、クエリの使い方が明示されていない」という事情です。インデックスは検索エンジン向けの産物ではなく、制約された意思決定者が使うものです。インターフェースは、Agent が本当に問いたい問いを中心に設計するべきです。definitions(symbol)(どこで定義されているか。複数ありうる)、references(symbol, limit)(誰が参照しているか。静的か曖昧かを区別する)、impact(symbol, depth)(これを変更したら、一ホップから二ホップまでどこに影響が及ぶか)、members(type)(この型にはどんな実装があるか。未実装の抽象メソッドはあるか)、test_mapping(source_file)(どのテストがこれをカバーしているか)。
この中で impact がいちばん過小評価されています。「ここを変更したら何に触れるか」に答えるもので、しかも同層の影響(呼び出し側が適応を迫られる)と跨層の影響(この契約に依存する上位モジュールは再検証が必要)を区別できる必要があります。これがあって初めて、第四節の「最小変更集合」と第六節の「最小検証セット」に根拠が生まれます。なければ、チェーン全体をモデルの自由裁量に委ねることになります。
二.五、インデックスの計器:インデックスが十分かどうかをどう測るか
インデックスはチェーン全体の基盤です。しかし基盤に観測可能な指標がなければ、劣化は二つの悪い方向へ進みます。ひとつは、誰もそれがすでに壊れかけていることに気づかず、上位の判断がすべて欠けた情報の上で行われること。もうひとつは、誰もそれがどれだけ有用かを知らないまま、投資コストが際限なく膨らむことです。インデックスには健全性の指標が要り、しかもその指標はリポジトリ全体の総数だけでなく、具体的なファイル、具体的なシンボルまで辿れる必要があります。
2.5.1 絶対数よりカバレッジのほうが大事
最も価値のある二つの指標は定義カバレッジと辺の解決率です。定義カバレッジは「このリポジトリで何個の束縛点がシンボルとして登録されたか」に答え、辺の解決率は「抽出された参照のうち、どれだけが唯一の正規ターゲットへ解決できたか」に答えます。この二つの数字は、必ず drill down できる必要があります。辺の解決率が 0.72 なら、残りの 0.28 はどこか。あるディレクトリに集中していれば、そのディレクトリが本リポジトリでカバーされていない言語機能(テンプレート、マクロ、条件付きコンパイル)を使っている可能性が高いので、解析ルールを足す必要があります。散らばっているなら、動的言語に宿命的な曖昧性なので、解析をさらに加油するのではなく、明示的な注釈で補うべきです。
もうひとつ、忘れられがちな指標があります。生成物の割合です。多くのリポジトリでは生成コードが三分のの一以上を占めます。インデックスがそれらを普通のソースファイルとして扱うと、call graph は大量の派生辺で汚染され、シンボルの多重定義によって正規名の一意性が失われます。正しいやり方は、シンボルに出所タグ(手書き、生成物、テスト、サードパーティ依存)を付けることです。そして影響範囲分析のときに、出所ごとに信頼度を与えます。手書きコードの呼び出し側は必ず直す必要がありますが、生成コードの呼び出し側は、その生成元を直すべきです。
2.5.2 高リスクなシンボル集合が本当の見落とし地点を示す
リポジトリ全体の平均的な解決率を追い求めるより、高リスクなシンボル集合を維持するほうが実用的です。もっとも多く参照されている上位のシンボル、複数のモジュールからインポートされている公開型、インターフェース層とアダプタ層にある抽象。これらはインデックスが間違えると、被害が体系的になるからです。より実用的なのは逆方向の操作です。意図的に擾乱を注入します。既知で正しい呼び出し点に対して、ソースファイルに意図的に空行やコメントを挿入し、インデックスの結果が一切変わらなければ可靠だとします。さらに、実際に一度リネームを挿入して、どれだけの辺が追随して更新されるかを見ます。これは安価な自己点検で、普段は表面化しないキャッシュ無効化のバグを捕まえることができます。
| 指標 | 意味 | 健全なシグナル | 逸脱したときのアクション |
|---|---|---|---|
| 定義カバレッジ | 登録済みの束縛点 ÷ 構文解析で検出された束縛点 | 主要言語ではほぼ飽和している | 解析ルールを足す、または生成ディレクトリを除外する |
| 辺の解決率 | 解決に成功した参照 ÷ 抽出された参照 | 静的言語ではかなり高い。動的言語は drill down が要る | 曖昧性が集中するディレクトリを特定して個別に対応する |
| 陳腐度 | インデックスのスナショットとワークスペースの状態が異なるファイル数 | タスク開始時はゼロであること | タスク開始前に強制で差分更新する |
| 差分更新の遅延 | ファイルひとつの変更からインデックスに見えるまでの時間 | 1 ラウンドのテスト所要時間より遥かに小さい | シンボル粒度のフィンガープリントに粒度を落とす |
| 生成物の汚染度 | 生成ディレクトリのシンボルがリポジトリ全体に対して占める割合 | 既知であり、タグ付けされている | 影響範囲分析のときに出所ごとに重み付けする |
2.5.3 インデックスの剰余利得は段階的になる
すべてのリポジトリが完全なインデックスを作る価値があるわけではありません。務実的な段階分けはこうです。symbol table だけ(コストはほぼゼロ、どの言語でも作れる。「この関数はどこにあるか」を解決)。symbol table に加えて呼び出し辺(「誰がそれを呼んでいるか」を解決。曖昧性の解消が要るが、剰余利得は最大)。さらに型推論を足す(「オーバーロードはどれを選ぶか」を解決。コストが急上昇する)。さらにビルドとテストの対応を足す(「どのテストを動かすべきか」を解決。ビルドシステムの解析が要る)。
重要な判断は、呼び出し辺という層の投資対効果比率がほぼ常に最も高いということです。コンテキスト収集、変更集合の導出、影響範囲分析、検証集合の選択という四つの作業に同時に供給され、唯一が再利用するインデックスだからです。型推論の剰余利得は言語に強く依存し、動的言語のリポジトリでは採算が合わないことが多いからです。より割に合うのは、曖昧性解消の結果をモデルに明示的に見せることです。二三の候補から読ませて決めさせればいいのであり、曖昧性解消のために重たい型チェッカーを動かす必要はありません。
最後の判断基準は再構築可能性です。依存関係の変更、言語のアップグレード、ビルドシステムの調整のあとでインデックスを確実に再構築できないなら、それは資産ではなく負債です。健全なインデックスシステムには明示的なコマンドがひとつあり、きれいなコンテナの中でゼロから、本番とまったく同じインデックスをビルドできるはずです。そのコマンド自体が最好的検証手段です。依存変更のたびに実際に実行されなければならないからです。
二.六、マルチ言語と monorepo:インデックスがもっとも事故りやすい場所
単一言語で階層も平坦な構造のリポジトリなら、インデックスの正答率は比較的安定させやすく、許容できる水準に収まります。インデックスをもっとも事故らせてしまう三つの構造は個別に考える価値があります。それらの共通点はシンボルの同一性が一意でないことであり、ここまでの曖昧性解消のロジックはすべて「シンボルの名前が定義ひとつに対応する」という前提の上に成り立っているからです。
2.6.1 パスのエイリアスとモジュール解決
TypeScript/JavaScript のエコシステムでは、モジュール解決のルール自体が一つの独立した言語のようなものです。設定ファイル中のパスエイリアス、ベースパス、パッケージのエクスポートフィールド、条件付きエクスポート、ワークスペース上のパッケージと公開パッケージの二重の身元、型宣言と実体実装の分離。インデックスがこれの設定を読まずに処理すると、ひとつのファイルが二つの別モジュールとして数えられ、跨パッケージの参照を見落とし、型宣言は存在するが実装が書き換えられているときにはまったく誤った正規名を返してしまいます。この種の問題はモノリポでは「同じユーティリティ関数がインデックスにいくつもの複製として現れる」という形で表れ、影響範囲分析が完全に失真します。
Python の状況は少しマシですが、それにも固有の落とし穴があります。名前空間パッケージ、リファクタリング期に残った絶対インポートと相対インポートの混在、文字列で動的に足された検索パス、同名のトップレベルパッケージを標準ライブラリと取り違えること。ここで重要な判断があります。インポート文自体は辺ではなく、解釈すべきクエリだということです。本当の辺はモジュール解決を一度通して初めて決まるので、インポート名をそのまま対象にすれば大量の偽の辺が生まれます。
2.6.2 monorepo のパッケージ境界は意味論的な境界だ
モノリポでは、パッケージ間の依存方向の制約はしばしば本物のアーキテクチャ制約です(階層化、循環の禁止、ドメイン分離)。インデックスがこの制約を読めれば、影響範囲分析はアーキテクチャ分析へと昇格します。下位のパッケージを変更したが、同じ層のパッケージからしか依存されていないなら、そのまま通せます。上位のパッケージから依存されている公開インターフェースを変更するなら、高リスクとしてマークすべきです。
より重要なのは、パッケージ境界が検証集合を決めることです。ほとんどのビルドツールは、パッケージ単位のビルドやテストの機能を持っています。変更をパッケージの中に留めれば、大きな速度上の利得が得られます。ここでいう「影響を受けるテスト」の選択が速くかつ正確にできたのは、本質的にこの既存の依存グラフを再利用しているからです。call graph をこの用途のために作り直すのは、手間がかかるうえに、ビルドシステムほどの信頼性には到底およびません。 → 上の「影響を受けるテスト」の選択が速くかつ正確にできたのは、本質的にこの既存の依存グラフを再利用しているからです。call graph をこの用途のために作り直すのは、手間がかかるうえに、ビルドシステムほどの大した偽者也絶… FINAL: 「上の「影響を受けるテスト」の選択が速くかつ正確にできたのは、本質的にこの既存の依存グラフを再利用しているからです。call graph をこの用途のために作り直すのは、手間がかかるうえに、ビルドシステムほどの信頼性には決しておよびません。」
ここに明確な優先順位の提案があります。まずビルドシステムがすでに出している情報を最大限に使い、自前のインデックスはそのあとに考えるということです。依存グラフ、テストマッピング、型チェックの範囲、コンパイル成果物。これらはビルドツールがずっと前から正確に計算しており、しかも CI と一貫しています。自前で計算した依存グラフがビルドシステムと食い違うようにすると、よくあるうえに発見しにくい故障の原因になります。
2.6.3 言語横断の境界とコード生成
現実のリポジトリには、言語の境界ではないものが大量に存在します。IDL から複数言語のクライアントを生成する処理、データベーススキーマからモデルクラスを生成する処理、Protocol Buffers からスタブコードを生成する処理、フロントとバックで共有するスキーマファイル。処理の原則は三つです。源流と派生物を区別する。派生ファイルを直接直すより、源流を直して再生成するほうが常に正しい。生成コマンドをワークフローに組み込む。生成元を直したら自動で生成を走らせ、モデルが覚えておくことに期待しない。検証では生成結果を検証する。再生成した成果物が期待と違えば、そこで必ず止める。
もっともよくある失敗は、モデルが生成成果物のロジックを一处だけ直して、テストも通り、次の開発者が再生成した途端に変更がすべて消える、というものです。この種の問題は短期的にはまったく見えないので、生成コマンドを検証ループ(verification loop)に組み込むのは最適化項目ではなく必須項目です。
三、コンテキスト収集とウィンドウ予算の配分
インデックスがある状態で次に考えるべきは、ここです。リポジトリには関連するコードが数万行ある一方、ウィンドウは数十万 token しかありません。何を入れるべきなのか、という問題です。これはチェーン全体の中で費用対効果がいちばん高い一節であり、モデルが強くなる必要はありません。よいコレクタが一つあれば、変更の抜け漏れやハルシネーション参照を大きく減らせます。
3.1 要件から候補集合へ:全倉の流し込みではなく、シード拡張(seeded expansion)
コンテキスト収集の正しい出発点はシード(seed)であり、「要件に最も関連するいくつかのファイル」ではありません。後者の並び順の良さは、検索モデルがコードの意味を理解する能力に左右されます。前者は決定的な構造に完全に依存できるからです。典型的な手順はこうです。
- 要件からアンカー語を抽出する。識別子、ファイルパス、設定キー、エラーコード、ルート名。これらは通常、要件記述そのもの、あるいはユーザーが渡した issue のリンクから得られます。
- そのアンカー語でインデックスに対して厳密一致(あいまい検索ではなく)を行い、シードとなるシンボルの集合を得る。
- call graph に沿って制限付き拡散を行う。各シードから外向きに一到二ホップ広げ、辺を展開しながら加点し、到達したノードが多いほど点数を下げる。
- 拡散の結果を型ごとに層分けする。定義、実装本体、呼び出し点、テスト、設定、ドキュメント。この六つの層の価値密度はまったく異なり、後の予算配分は直接この層分けに依存します。
ここには重要な設計上のトレードオフがあります。拡散のホップ数は固定二ホップではなく、適応的にすべきだということです。固定二ホップは解析層(ツール、ルート、モデル)には向いていますが、リーフでは大量の無関係な内容を持ち込みます。判断の材料はノードの型です。上流へ向かうのは契約(インターフェース、型、プロトコル)なので、もっと深く辿るべきです。下流へ向かうのは実装の詳細(リーフ関数、ヘルパー関数)なので、一ホップで十分です。
3.2 予算配分:関連度で並べて切り詰めるのではなく、層ごとに配分する
候補集合をウィンドウに押し込むとき、よく使われるのは検索スコアで並べてから切り詰めるということです。この方法の欠点は、スコアの分布が長尾になることにあります。先頭の数十の断片が同じ関数の定義とそのコメントで埋まっていて、変更の正しさを決める呼び出し点が第 200 位で切られてしまう、といったことが起きます。
より安定したやり方は層ごとのクォータです。以下のこの設定の形は実際に使えるもので、コレクタ・モデル・人間のレビューが同じ事実を見ることを可能にします。
budget:
total_tokens: 90000
policy: layered_quota # 不用 top-k 截断,按层配额
layers:
signatures: { quota: 0.12, rule: "所有受影响符号的签名与文档串,含未展开的调用者列表" }
definitions: { quota: 0.28, rule: "将要修改的函数/类型的完整实现" }
call_sites: { quota: 0.22, rule: "每个调用点保留 ±3 行窗口 + 接收者静态类型" }
contracts: { quota: 0.15, rule: "上游接口、类型定义、序列化 schema" }
tests: { quota: 0.13, rule: "映射到受影响源文件的现有测试与断言" }
config_history: { quota: 0.10, rule: "相关配置键、依赖声明、最近 5 次改动摘要" }
seeds:
- symbol: PaymentGateway.charge
why: "需求点名要支持新的支付渠道"
confidence: high
ambiguities:
- symbol: notify
reason: "同名定义 7 处,无法静态确定接收者类型"
action: ask_before_edit
層どうしのあいだには調整可能なトレードオフがあります。ふつうの傾向はこうです。変更がファイル横断の一貫性に関わるなら contracts のクォータを上げる(モデルが同期すべき箇所をすべて見る必要があるため)。変更が純粋な追加なら tests のクォータを上げる(既存のテストはもっとも信頼できる振る舞いの仕様だからです)。変更がリファクタリングなら definitions のクォータを上げる(振る舞いの等価性は、実装を全部読まないと保証できないからです)。
3.3 構造による切り詰め:token 数で行を切るよりよい方法
ウィンドウが足りないときに、行単位で切り詰めるのはいちばん悪い戦略です。関数の途中で切れ、切り落とされるのはたいていエラー処理か境界判定です。本当に効く切り詰めは四層あり、意味を落とさないものから段階的に劣化させます。
- 要約層:関数の本体は、シグネチャと docstring、先頭の数行、return 文だけを残し、残りを
...(省略 N 行,行为未变)...という一行に置き換えます。重要なのは省略した行数を明示することで、モデルに自分が全部見ていないことを知らせる必要があるからです。これは第一節で言った「自分自身が見えない」にそのまま対応します。 - 骨格層:AST を使って、関数のシグネチャ、クラスのメンバー一覧、分岐の構造ラベル(if/except の分岐がいくつあるか)を取り出し、「構造アウトライン」を作ります。アウトラインは変更の影響範囲の判断にきわめて有効です。トークンをほとんど消費せずに、すべての制御フローをカバーできるからです。
- アサーション層:テストファイルからは、テスト関数名、fixture、アサーション式だけを残し、実装の詳細は削除します。アサーション式そのものが、もっとも精密な振る舞いの仕様だからです。
- 断片層:どうしても削る必要があるときは、スコアで全局的に取るのではなく、「同一ファイル内の近傍ウィンドウ」で残します。コードの意味単位は連続した塊なので、塊をまたいで切る被害は、捨てるより大きいからです。
さらに、忘れられがちな窍门が二つあります。ひとつは重複排除です。call graph の拡散は、同じ関数を複数の経路から繰り返し入れてくることがよくあります。内容のハッシュで重複排除すれば、すぐにも相当な予算を節約できます。もうひとつは反復拡大の抑止です。ある共通ユーティリティ関数が 30 個の候補コンテキストに出現するなら、完全な実装をひとつ残して「呼び出し元: ファイル: 行番号」の一覧を 30 件添えるだけにします。情報の密度はずっと高くなります。
3.4 コンテキストの並べ替え順序も結果に影響する
中身を選んだあとでも、置く順序にはまだ影響があります。経験的には安定した傾向がいくつかあります。タスクの指示は最初、受け入れ基準は最後。定義は変更する場所の近くに置く。すべての定義を冒頭に積み上げて、その合間に大量の呼び出し点を挟むのは避けたほうがいいです。関連する断片は隣り合わせに置く。同じシンボルの断片を連続した塊にまとめれば、注意層が長距離の突き合わせをする必要がなくなります。テストは、それが検証している実装の直後に置く。こうすれば、モデルが実装を読んでいる時点ですでに振る舞いの期待が頭に出来上がっています。
より攻めたやり方は二段階コンテキストです。第一段階では構造アウトラインとシグネチャだけを与えて、「どの実装の完全なコードが必要か」をモデルに判断させます。第二段階でそのリクエストに応じて補います。本質的に、コンテキスト選択が観測可能な推論の一歩として扱われるので、一度で全部を詰め込むより見落としを発見しやすく、予算も節約できます。
三.五、長時間にわたるタスクにおけるコンテキストの減衰と状態の固定化
一巡のタスクではコンテキストは消費財ですが、現実のエンジニアリングタスクは数十ターン、またセッションをまたいで続くことがあります。ここで見えにくい問題が生じます。初期の情報がコンテキスト圧縮のなかで徐々に具体性を失っていくという問題です。最初のシンボル定義が「決済ゲートウェイに関係するコード」という一文に要約されてしまい、モデルは第十ターンになってもその一文を土台に推論して、「我当时見た実装にはこの分岐がない」のような誤った判断をします。
3.5.1 圧縮は不確実性を静かに消してしまう
コンテキスト圧縮は通常「要点を残し、詳細を捨てる」というものです。物語的なタスクにはそれが妥当ですが、コードのタスクでは危険です。詳細の中に条件判定、境界チェック、例外分岐が隠れているからです。もっとも悪いのは、圧縮後の要約が意味的には通じた文のままで、モデルが自分が情報を失ったことに気づかないことです。ただ、欠けた前提を土台に、より自信を持って動くようになるだけなのです。
解決策は構造化された状態と、物語的なコンテキストを分離することです。長期的に高い忠実度で保ちたいもの——シンボル一覧、インターフェースシグネチャ、却下した案、未解決の疑問、検証結果、失敗の記録——をすべて構造化されたストレージに書き、必要に応じて検索します。対話のコンテキストは、そのときの推論を担うためだけに使う。この分業は、第五節で言う「テストの削除は許すが追加は許さない」と同じ発想です。必ず正確に保ちたいものは、信頼できない通道に依存させるべきではないからです。
3.5.2 失敗の記録は成功の記録より重要
状態の固定化のときに「何をしたか」だけを記録するシステムが多く、それによってきわめて悪いパターンが生まれます。モデルは第三ターンで案Aを試して失敗し、その失敗理由が捨てられます。第十ターンでもう一度案Aを試したくなります。忘れているからです。これは長いタスクでもっともよくある、しかももっとも愚かな作業の繰り返しです。
失敗の記録は三つの部分を持つべきです。何を試したか、どの段階で失敗したか、なぜ却下されたか。三つ目がもっとも価値があります。具体的な失敗を、再利用できる制約に一つ格上げできるからです。失敗の記録はログではなく、判断に圧縮する必要があります。「アンカーが合わなかった、再読したら修正で足りる」のほうが、完全なスタックトレースよりはるかに役に立ちます。
3.5.3 セッションをまたぐ継続
タスクがセッションをまたぐなら、状況を復元するには三つのものが必要です。タスクの目標と現在の進捗(どの能力点まで進んだか)、ワークスペースの状態(どのファイルを変えたか、いま clean か)、未解決の問題(どの曖昧性がまだ確認されていないか)。どれかひとつでも欠けると、継続は「もう一度コードを読んでゼロから判断し直す」ということに退化します。そして、その再判断は以前のときと違う結論を出す可能性があり、それは立ち止まるより悪いことです。
ここに避けたいアンチパターンがあります。復元の手順を「新しいセッションに自分でリポジトリを探索させて現状を知らせる」にするものです。賢そうに見えますが、実際にはモデルに,之前に犯したのと同じ過ちをもう一度犯させるだけです。正しい復元は、既存の構造化された記録を読み、ワークスペースとの一致を確かめ、不一致の部分だけを再探索することです。
3.4.1 リポジトリ間・サービス間のコンテキスト境界
変更が複数のリポジトリや複数のサービスに及ぶ場合,之前考えた「インデックスを一度作って、コンテキストも一度に集める」という前提は成立しなくなります。これらのリポジトリは、それぞれ独自のライフサイクル、ビルドシステム、リリースのペースを持っているからです。この種のタスクの中心は、インデックスを統合することではなく、依存関係の主従方向を先に確定することです。
绝大多数の跨リポジトリ変更には方向があります。インターフェース定義が上流にあり、実装や呼び出し側が下流にあるのです。方向を逆にすると、タスク全体が「下流を直して上流の新しい接口に適応させる」ことになってしまいます。正しいやり方は「まず上流に互換フィールドを追加し、下流はそのままにし、上流がリリースされてから次の段階へ進む」することです。これは 5.5 節で言う移行順序の話と同じ理屈ですが、尺度が一つのサービスから複数のサービスへ広がっただけです。
二つ目の要点は、リポジトリをまたぐとインデックスの鮮度がまったく違うことです。手元のリポジトリなら即座に再構築できますが、上流のリポジトリは数日分は古いかもしれません。このときの正しいやり方は、「この部分の根拠は N 日前の上流スナショットである」と明示し、上流の契約の部分(インターフェース定義、プロトコル、フィールド宣言)だけをコンテキストに入れることです。実装の詳細まで引き寄せるのは賢明ではありません。予算を無駄にするうえに、モデルに権限のない部分を修正するよう誘導してしまうからです。
三つ目の要点は、リポジトリをまたぐ検証はループを閉じるのが難しいことです。下流のテストが上流まで実際に到達することはあり得ないので、頼るのは契約テストと上流のモックだけです。両方がない場合、この種の変更の正しさは本質的に自動証明できません。このときシステムは「この部分は自動検証できません」と明言し、検証の責任を明示的に人へ返すべきです。これは第六節の最後にある原則の延長そのものです。
四、要件から「最小変更集合」へ
前の二節が「看清楚」を解決したのに対し、この節が解決するのは「直すべきところだけを直す」ことです。ここでの判断基準はかなり硬いものです。変更集合を、コンテキストを知らない人が十分弱で読んで正誤を判断できるか、という問いです。読み終えられなければ、変更集合が大きすぎるということです。
4.1 要件をコードではなく能力に変換する
要件の説明は通常、ユーザーの言語で与えられます(「注文時に在庫が足りなければ自動的に分割できるようにしてほしい」)。これを直接コードに変換すると、途中の大きな一段を飛ばしてしまいます。途中の这一步とは、要件を検証できる能力点へ分解することであり、それぞれの能力点はテスト可能な一文と一組のシンボルに対応します。
能力点:在庫が足りないときに注文を自動的に複数の子注文へ分割し、各子注文は独立に在庫を減算する。いずれかの子注文の減算が失敗した場合は、全体をロールバックする。
各能力点には、それが触れる層を标注します。ドメインロジック、永続化、インターフェース契約、設定、テスト。この一步の価値は、「曖昧な要件」を「有限で列挙可能な変更点リスト」に変えることにあります。リストに無いものは、直してはいけないものです。「最小」が実行可能になる根拠はここにあります。
4.2 影響範囲から変更集合を逆算する
能力点とシードシンボルを入手したら、変更集合はアルゴリズムで導出されるべきであり、モデルに直接書かせてはいけません。導出の規則はおおよそ四種類です。
- 定義の変更:能力点が直接乗る関数やクラス。
- 契約の同期:定義変更によってシグネチャが変わった場合、そのシンボルを参照し、適応が必要になる呼び出し点はすべて変更集合に含まれます。注意すべきは「適応が必要なもの」だけだということです。インデックスで確認して、古い形をハードコードしている呼び出し点だけが対象になり、引数順序が変わらないだけのものは触りません。
- 振る舞いのミラー:同じロジックが二か所で実装されている場合(フロントとバック、生成コードと手書きコード、キャッシュ層とソース層)、必ず同期しなければなりません。ファイル横断の一貫性でもっとも抜けやすい種類です。
- テストの補完:能力点に対応するテストがない場合、新しいテストファイルまたはテスト関数を追加します。既存のテストがそれによって失效する場合は、あわせて変更集合に含めます。
導出された変更集合には、通常信頼度の等級が付きます。インデックスが直接支持しているもの(高)、パターン照合から推測したもの(中)、純粋に意味から推測したもの(低)です。高信頼度ならそのまま直し、低信頼度なら先にユーザーに確認するか、探査的な読み取りを一步行います。この等級分けは保守のためではなく、モデルの注意力を不確かなところへ集めるためです。注意の予算は有限であり、平均して力を使うのはいちばん悪い戦略です。
4.3 探査的な読み取り:「書く」より先に「読む」
直感に反して何度も検証されている做法があります。変更の前に、変更するはずの数行をすでに読んでおり、なおかつその内容が、これから書くパッチの行コンテキストと一致している、というものです。これは馬鹿がましい要求に聞こえますが、実際には大量の失敗の根本原因です。典型的なシナリオはこうです。コンテキスト収集の段階で 20 分前のファイルスナップショット(あるいは要約で切り詰められた版)を入手し、パッチ生成の段階で要約の中の印象に基づいて書いた結果、アンカーがまったく合わず、パッチの適用が失敗するか、ずれた位置に挿入されてしまう、というものです。
エンジニアリング上の実現方法は行アンカー表明です。各パッチブロックは、それが基づく原文の断片を必ず持ち、ツールが適用前に厳密な照合を行い、一致しなければ拒否して再読み込みを求めます。この制約はほぼコストゼロですが、「ハルシネーションによる修正」という一類を不合格に変えてしまいます。
4.4 Agent に「いいえ」と言わせる
成熟したエンジニアリングのチェーンは、三種類の拒否を許さなければなりません。要件そのものに曖昧さがある(二つの合理的な解釈が異なる実装を導しまう場合)、明らかにもっと小さい手法が存在する(追加のロジックをコード変更ではなく設定にするなど)、要件が現在のリポジトリで検証できる範囲を超えている(変更対象の何もテストに覆われておらず、直しても正しさを証明できない場合)。三つ目がもっとも忘れられがちですが、まさにAgent が死角の中で大規模に改めるのを防ぐ鍵です。テストも、呼び出し点も、契約もなければ、変更の正しさはまったく評価できません。このときの正しいやり方はリスクを示してテストの追加を求めることであり、直してから報告することではないのです。
四.五、中間産物:読める変更計画
インデックスもコンテキスト収集も、機械内部の産物であり、人間には読めません。しかしシステムと人のあいだには、共通の引き渡し物が必要です。そうでなければ、人が関与する必要が生じるたびに、人はモデルの頭の中の推論をもう一度読み直すことになり、コストが非常に高くなります。その引き渡し物が変更計画です。これは人間向けの確認リストであると同時に、後続の実行ステップに対する契約でもあります。
4.5.1 計画に何を書くべきか
変更計画には少なくとも六種類の情報が含まれ、それぞれに明確な消費者がいます。
- 能力点とその受け入れ文。要件から切り離された、テスト可能な陳述。「何をするか」に答えるもので、最終的なカバレッジ検査の根拠になります。
- アクションと対象シンボル。「どの関数を追加し、どのシグネチャのフィールドを変え、どの分岐を調整するか」まで具体的に書くのであって、「決済モジュールを変更する」という書き方にはしません。「どこを触るか」に答えます。
- 起動の理由。このアクションがなぜリストに載ったのか。能力点が直接求めているのか、契約の同期なのか、振る舞いのミラーなのか。この欄は監査可能性の中核であり、ここが欠けると review は一行ずつコードを読むことに退化します。
- 信頼度と出所。インデックスから導出されたのか、パターンから推測されたのか、意味から推測されたのか。これが自動実行するか、先に一問するかを決めます。
- 検証の方法。どのテストがそれをカバーするか、どんなアサーションを追加すべきか、型チェックがバックアップとしてあるか。これを第六節の検証セットを変更より前に前倒しすることで、検証が事後の補填ではなくなります。
- 明示的にやらないこと。候補として認識されたが除外された変更と、その除外の理由。この欄はもっとも省略されやすく、もっとも価値があります。
| フィールド | 答える質問 | 誰が消費するか | 欠落の結果 |
|---|---|---|---|
| 能力点 | 要件は何を実現すべきか | モデル、受け入れ当事者 | できた機能が要件と一致しない |
| 対象シンボル | 具体的にどこを変えるか | パッチ生成 | 変更が散らばり、照合できない |
| 起動理由 | なぜ変更が必須か | 人間による review | review の一行ずつの読みになり効率が極端に落ちる |
| 信頼度 | インデックスがどこまで支持しているか | スケジューリングのロジック | 平均的に力を配り、信頼度の低いところで間違える |
| 検証方法 | どうやって正しいと証明するか | 検証段階 | テストは全て緑だが機能が実装されていない |
| やらないこと | 意図的にスキップしたもの | 人間が介入する判断 | 重要な判断が静かに飲み込まれる |
4.5.2 質問する三つのタイミング
計画の真の価値は、モデルを低コストで中断できる地点を定義していることにあります。割り込みに値するのは三箇所だけだと考え、それ以外はモデルに自分で走らせたほうがいいと思います。
第一に、要件の曖昧さです。同一の要件に二つの合理的な実装があり、異なるインターフェース設計に導かれる場合です(「タイムアウト後にリトライする」とは、三回リトライするのか、タイムアウト上限までリトライするのかという区別)。こういう場所で推測を誤ると、その区間の作業全体が無駄になり、しかもモデル自身は気付きません。
第二に、明らかにもっと小さい手法が存在する場合です。計画では新しい抽象層を追加しますが、実際にはリポジトリに設定項目やフックがあり、それで直接解決できます。ここで割り込む価値はきわめて高いです。節約されるのは作業量だけでなく、新しい抽象が長期的な保守コスト会带来点でもあります。
第三に、カバレッジがゼロの変更です。変更対象のシンボルをカバーするテストも、影響範囲を逆引きできる呼び出し点も存在しません。つまりその部分のコードはシステムの死角にあるということです。このときの正しいやり方は、まずリスクを示し、テストの追加を提案することです。黙って直して、検証できないパッチを納品することではないのです。
この三箇所以外にモデルを中断させるのは、騒音を製造しているだけです。特に警戒すべきなのは進捗を合わせるために質問することです。モデル自身が判断できる細部を問題としてユーザーに投げると、システムは慎重に見えますが、実際には判断のコストをユーザーに転嫁しているだけです。
4.5.3 多段階タスクの契約としての計画
タスクが複数段階を要する場合(まずデータ構造、次にシリアライズ、次に呼び出し側、最後にテスト追加)、計画はもう一つの役割を担います。各段階の完了定義と、次の段階への入力です。これは長いタスクで非常に重要です。コンテキストは途中で圧縮されるので、ある段階の成果物が明確な構造化フィールドに記されないままだと、圧縮のなかで消えてしまうからです。
実務では、各段階が終わった時点でその段階の結論を固定します。新しいシンボル一覧、新しいテスト名、却下した案、未解決の疑問。固定されたこの状態は、後続の段階における唯一信頼できる記憶の源であり、モデルに「前に何をしたか覚えさせる」よりもずっと信頼できます。同じ理屈です。複数の Agent がリレーして一つのタスクを完了させる場合、この計画はそれらのあいだの唯一のインターフェースになります。次のエージェントは前のエージェントの会話を読む必要がなく、計画を読むだけでよいのです。
五、パッチ生成:「コードを書く」から「変更をコミットする」へ
5.1 diff 形式の選択:三つの方式の境界
パッチは表現がいよいよよいほどよいのではなく、検証も適用も容易であるほどよいものです。三つの主要形式の適用境界はとてもはっきりしています。
| 形式 | 利点 | リスク | 適用対象 |
|---|---|---|---|
| 全文書き直し | モデルがもっとも得意で、生成品質も最も高い | 無関係な変更を生みやすく、大きいファイルでは上限を超えると使えない | 新規ファイル、短い関数、小さなファイル |
| Unified Diff | 差分だけを表現する。そのままレビュー可能で、変更量を数えられる | 行番号のずれに敏感。ファイル横断の一貫性は別途の保証が要る | 汎用の主力形式 |
| 構造化編集指示 | アンカーと厳密な行区間。意味に揃っており並べ替えに強い | 実装の複雑さが高い。ツール側の協力が要る | 成熟した編集ツールを持つコードベース |
実用的な結論は混合戦略です。新規ファイルと小さいファイルには全文書き直し、既存ファイルには unified diff を優先。ただし編集ツールが構造化アンカーをサポートしているなら、構造化編集を優先してください。その失敗モードが「明示的なエラー」であって「静かなずれ位置」ではないからです。
5.2 ファイル横断の一貫性:複数ファイルのパッチは一つのトランザクションでなければならない
ファイル横断の変更で最大の落とし穴は生成品質ではなく、部分適用です。パッチ A が関数シグネチャを変え、パッチ B が呼び出し点を変え、B がアンカーのずれで失敗したのにシステムが A だけを適用したなら、リポジトリはコンパイルの通らない中間状態に入ります。Agent システムにとって、中間状態は最終状態と同じくらい危険です。続く検証のステップが、不整合なリポジトリの上で走るからです。
解決策は三本あり、通常は三本を同時に使います。
- 先に検証してから適用する:すべてのパッチでまずアンカー照合と構文解析を行い、すべてを通してから適用段階へ進みます。どれか一つでも失敗したら、まとめてロールバックします。
- ファイル横断の契約を宣言する:パッチ集合の中に「新シグネチャ = X(…, idempotency_key: str)」と明示的に書き、それを参照するすべての呼び出し点を強結合ブロックとして列挙します。強結合ブロックは、すべて適用されるか、すべて適用されないかのどちらかでなければなりません。
- 適用直後にシンボル整合性を検査する:完全なテストは走らせず、軽量な「定義と呼び出しのシグネチャ照合」だけを行います(インデックスの symbol table と突き合わせる)。数秒あれば中間状態を検出できます。
もうひとつ、整合性として見落とされがちな問題があります。同じ変更に属する複数ファイルの間では、順序が内容に影響することがあるというものです。典型例はマイグレーションスクリプトとスキーマ、コードジェネレータと生成物、定数定義とその複数の使用箇所です。変更が生成物を含む場合、正しいやり方は生成物を手直しすることではなく、生成元を直して生成コマンドを再実行することです。この判定は Agent の内側で明示的でなければならず、そうしないと「.py は直したが protoc や orm の再生成を走らせ忘れた」という行き詰まりが起きます。
5.3 フォーマットノイズを抑える
Agent のパッチ品質を判断するときの、もっとも速い指標は論理の正誤ではなく、ノイズ比です。変更行のうち、要件と無関係な行がどれくらいあるか。ノイズの源はほぼいつもフォーマットです。インデントのスタイル変化、行末の空白、import の並び順、シングルクォートかダブルクォートか、末尾の改行、ファイルのエンコーディング。どれかひとつでも diff を 20 行から 300 行に変えてしまいます。
エンジニアリング上の抑制策を、強度の高い順に並べるとこうなります。
- リポジトリ自身のフォーマッタで書き戻す。もっとも手間の少ない方法は、パッチ適用後に
blackやgofmtやprettierを一度走らせることです。ただし、変更されたファイルにだけ実行すること、そしてフォーマットが触れていない行まで逆に書き換えていないかを確認することが必要です(フォーマッタのバージョンがリポジトリと食い違っているときに多く見られます)。 - コンテキストの保持:プロンプトの中で、既存のインデントスタイル、行幅、クォートスタイルを維持するよう明示的に要求し、対象ファイルのスタイル例を示します。効果は显著ですが、モデルが従うことに依存します。
- ノイズのフィルタと復元:パッチ適用後、パッチと無関係な行レベルの差分を自動的に戻します。これには「元の内容とパッチ結果」の差分解析が必要で、純粋な空白の差を識別して戻します。この方法がもっとも堅牢なのは、どちらか一方の自覚に依存しないからです。
- diff のサイズゲート:硬い閾値を設定します(たとえば単一ファイルの変更が 200 行を超える、または変更行数の総量が要件の複雑さの妥当な倍率を超える場合に、)、「最小変更集合で書き直す」というループを発火させます。これは受け皿であり、もっとも有効な自己点検でもあります。
unified diff の具体的な例です。アンカーのコンテキストが実際のファイルから得られなければならないことに注意してください。
--- a/payments/gateway.py
+++ b/payments/gateway.py
@@ -41,9 +41,11 @@ class PaymentGateway:
def charge(self, order: Order, amount: Decimal,
idempotency_key: str | None = None) -> ChargeResult:
- if amount <= 0:
- raise InvalidAmount(amount)
+ if amount <= 0:
+ raise InvalidAmount(amount)
+ key = idempotency_key or order.id
+ if cached := self._replay_cache.get(key):
+ return cached
with self._unit_of_work() as uow:
receipt = self._provider.authorize(order, amount)
+ self._replay_cache.put(key, receipt)
uow.add(receipt)
return receipt
--- a/payments/api.py
+++ b/payments/api.py
@@ -18,7 +18,7 @@
-def charge(order_id: str, amount: float) -> dict:
+def charge(order_id: str, amount: float, idempotency_key: str | None = None) -> dict:
order = _load(order_id)
- return _gw.charge(order, Decimal(str(amount))).as_dict()
+ return _gw.charge(order, Decimal(str(amount)), idempotency_key).as_dict()
二つのファイルの変更が強結合であることに注意してください。gateway.py は idempotency_key 引数を追加したので、api.py の呼び出し点も同期しなければなりません。この二行は同じトランザクションで適用されなければならず、中間状態は直接に型チェックの失敗を引き起こします。
編集ツールが構造化指示をサポートしているなら、同じ変更をよりずれに強い形で表現できます。アンカーが行番号ではなくシンボルなので、ファイルの上方に内容を挿入してもずれません。
{"ops": [
{"op": "replace_body",
"file": "payments/gateway.py",
"anchor": {"symbol": "PaymentGateway.charge", "kind": "method"},
"expect_contains": "with self._unit_of_work() as uow:",
"body": "..."},
{"op": "insert_line",
"file": "payments/api.py",
"anchor": {"symbol": "charge", "kind": "function", "line_contains": "return _gw.charge"},
"expect_contains": "_gw.charge(order, Decimal(str(amount)))",
"text": "return _gw.charge(order, Decimal(str(amount)), idempotency_key).as_dict()"}
]}
expect_contains はここでのアンカー表明で、4.3 節で言った行アンカー検証に対応します。「ファイルはこうなっているはず」という認識を検証可能な仮説に変え、合わなければ書き込みを拒否します。
5.5 移行系の変更:パッチ生成の別ルール
ここまでのパッチ生成は、変更が単一のデプロイ単位に収まり、デプロイすればそのまま効く、という前提を置いていました。しかし、この前提を満たさない変更が二種類あります——新旧二つのバージョンがしばらく併存しなければならない変更です。データスキーマの移行、API プロトコルの進化、設定形式のアップグレード、ストレージ形式の変換などがこれにあたります。コード Agent が本番事故を起こしやすいのは、まさにこの種類の変更です。「ローカルではテストがすべて緑」というシグナルは、この領域ではほとんど情報量がありません。
5.5.1 後方互換はデフォルト要件
移行系の変更における第一原則は、デプロイ順序が正しさに影響してはいけないということです。書き方に落とすと、通常は次のような要求になります。新しく書いたコードは、古いデータ形式と新しいデータ形式の両方を読み出せること。古いコードは、新しいコードが書き出したデータを処理できること(ローリングデプロイの窓の間には、古いインスタンスが新しく書かれたデータを読むは必ず起こります)。API に追加するフィールドには必ずデフォルト値があること。フィールドの削除は二段階で行うこと、まず書き込みを止め、次に読み取りを片付けることです。
これはつまり、「フィールドを削除する」という要望から実際に生まれてくるパッチは、ひとつのパッチの中の削除一行ではなく、複数のデプロイをまたぐ三回の独立した変更になるということです。モデルは要望を字義どおりに解釈する傾向があります——ユーザーが「このフィールドを削除して」と言えば、ただ一行を消す。正しいやり方は、この種の要望を移行タスクとして認識し、決まった移行パターンを当てはめることです。まず新しい形を導入して二重書き、次にバックフィル、次に読み取りを切り替え、次に書き込みを止め、最後に古いフィールドを片付ける。各ステップはそれぞれ検証し、それぞれデプロイします。
5.5.2 データのバックフィルは独立タスク
スキーマ変更のあとに残る履歴データにはバックフィルが必要です。これはコード変更とはまったく性質の異なるタスクです。本番環境で走るもの、影響する行数が巨大で、ロールバックの効きが悪く、しかも失敗が即座に表面化しないことが多い。合理的な扱い方は、コードパッチから完全に切り離して、独立したデータ操作タスクとして扱い、専用のチェックリストを持たせることです。影響行数の上限、バッチサイズ、冪等性の設計、失敗時の再開ポイント、シャドウ実行と突き合わせ。
Agent にバックフィルスクリプトをついでに生成させて実行させるのは危険です。スクリプト自体は正しく書ける(この部分は通常のコード変更と同じです)。しかし「いつ実行するか、何行実行するか、失敗したらどうするか」という判断は、ビジネス上のデータに対する理解と結びついたものです。スキーマだけを見たモデルが決められることではありません。
5.5.3 移行パッチの検証はやり方が違う
移行系の変更はユニットテストでは正当性を示せません。テストしているのは「二つのバージョンが併存しているときの振る舞いが正しいこと」だからです。現実的に使える検証手段は四種類あり、信頼度の高い順に並べるとこうなります。二バージョン互換テスト(同じテストデータを古いコードと新しいコードでそれぞれ走らせ、結果が一致すること)、契約テスト(新旧のバージョンが同じリクエストに対して返すレスポンスの差分が、想定の範囲内に収まること)、シャドウ比較(新しいロジックは計算するだけで書き込まず、古いロジックの結果と突き合わせる)、そしてプロパティテスト(不変条件に対するランダム入力での検証、たとえば往復変換のあとでデータが等価であること)。
この四種類に共通しているのは、いずれも二つのバージョンを突き合わせていることであり、ひとつのバージョンの中で表明しているわけではないということです。だからこそ、通常の最小検証セット戦略はここではほとんど完全に機能しません。あの戦略は call graph を見てテストを選ぶのですが、移行の真のリスクは時間軸上の共存にあります。誠実な検証レイヤは「同じ入力を、改前と改後にそれぞれ一度ずつ通す」という二重ルートの比較を表現できる必要があります。多くのテストフレームワークでは、専用に土台を組む必要があります。
5.3.1 全文書き直しにすべきなのはどんなときか
これまで繰り返し最小変更を強調してきましたが、ひとつだけ全文書き直しのほうが適切な場合があります。ファイルそのものが大きく変わり、その変更がファイルの大半を覆うときです。この場合にブロック単位でパッチを打つと、ブロック単位で生成的される diff のほうが、まとめて一度書き直すより汚くなります。どのような切り方をしても、全文書き直しの変更は意味的にはまとまった一塊だからです。
判断基準は数値化できます。想定される変更行数とファイル総行数の比が、ある閾値(経験的には三分の一程度)を超えたら、迷わず全文書き直しにするというものです。閾値より下ならパッチを使います。パッチの利点は diff に本当の変化しか現れず、review する人が何が変わったか一目で分かることです。閾値より上なら両者の差はほとんどありませんが、全文書き直しにはモデルが複数の小ブロックのあいだで全体の整合性を失うのを防げるという利点があります。diff 上ではそれは「数箇所の無関係に見える調整」として現れますが、実際はモデルが第五のブロックにいた時点で最初のブロックの意図を忘れ始めているからです。
全文書き直しには、必ずセットで使うべき二つの確認が要ります。一つ目は行末とインデントの一貫性です。モデルが長ファイルを書き直すとき 一部の段落だけスタイルが変わりやすく、この種のノイズは数百行の diff のなかでは非常に目立ち、目障りです。二つ目は変更していない段落は一字一句同一でなければならないということです。書き直しが終わったら機械的に突き合わせます。ある行が元テキストと違っていて、それがパッチで想定した差分でもなく、フォーマッタによるものでもないなら、それはモデルのドリフトであり、巻き戻してやり直す必要があります。この項目をツールで検査するのは数ミリ秒で済みますが、止められるのは review で最も見つけにくい種類のエラーです。
6 最小検証セットと高速なフィードバックループ
検証は、このチェーンのなかで最も遅く、しかも情報量の乏しい工程です。全量テストを一度走らせると数分かかり、Agent が五回ほど試したところでタイムアウトします。けれどユニットテストをひとつだけ走らせても、複数ファイルにまたがる変更はカバーできません。解決策は、検証をレイヤー分けし、各レイヤーに明確な時間予算を与えることです。
6.1 秒単位から分単位まで、四層の検証
| レイヤー | 何を確かめるか | 典型的な所要時間 | 失敗したときの行動 |
|---|---|---|---|
| L0 構文と構造 | パッチがパースできるか、import が閉じているか、書式が揃っているか | サブ秒 | ただちにパッチ生成へ戻り、やり直す |
| L1 シンボルの一貫性 | シグネチャ変更がすべての参照点で同期しているか、未解決の参照が残っていないか | 秒単位 | 漏れた呼び出し点を補完し、テストには進まない |
| L2 局所テスト | 変更ファイルに対応づくテスト(全量ではない) | 十秒から一分 | 帰属を判定する。パッチの誤りか、既存の問題か |
| L3 影響範囲の回帰 | call graph から逆引きした上流の契約テスト | 分単位 | 本物の回帰かどうかを見極め、必要ならロールバック |
L0 と L1 は、もっとも過小評価されている二層です。ほとんど時間を取らないのに、テストに入る前に「明显に誤った」変更の大半を止めてくれます。とくに L1 が重要です。シグネチャだけを変えて呼び出し点の更新を漏らした、という誤りをコンパイルやテストまで待たせてしまうと、Agent は丸一回の生成を無駄にします。シンボルの一貫性チェックは、インデックス内の symbol table と一度突き合わせるだけで、数秒で漏れた呼び出し点を特定できます。
6.2 変更セレクタ:影響しうるテストだけを走らせる
L2 の鍵は、テストからソースコードへの逆写像です。情報源が三つあり、質のよい順に並べると次のようになります。
- カバレッジからの逆引き:最新のカバレッジデータを使い、変更したファイルを、それをカバーするテストの集合へ写像します。もっとも正確ですが、カバレッジデータが新しいことに依存します。
- ビルド依存グラフ:テストが変更モジュールに依存しているなら、関連するパッケージ全体のテストを走らせる必要があります。パッケージ粒度なのでファイル粒度より粗いですが、常に安全です。
- call graph からの逆引き:変更したシンボルの呼び出し元に対応するテストです。意味に最も沿っていますが、call graph がテスト呼び出し辺を保持していなければならない、というコストがあります。
実践で最も安全なのは、和集合を取って、批量で回すことです。まずカバレッジからの逆引きで得た最小集合を走らせ、数十秒で最初のシグナルを手にします。その層が全部緑なら、次にパッケージ単位の集合を走らせます。「一度に十分かかるテスト」を「二十秒の速いテスト+十分の遅いテスト」に割ると、反復速度の差は桁違いになります。
# L1:符号一致性闸门(秒级,不依赖编译器)
from collections import defaultdict
def check_signature_consistency(index, patch_plan) -> list[str]:
problems = []
for change in patch_plan.changed_signatures:
for ref in index.references(change.symbol, limit=200):
if ref.dynamic: # 歧义边:不猜,升级为人工确认
problems.append(("ambiguous", change.symbol, ref))
continue
if ref.compatible_with(change.new_signature):
continue
if ref.file not in patch_plan.touched_files:
problems.append(("missing_sync", change.symbol, ref))
return problems
# L2:最小测试集选择
def select_tests(changed_files, coverage_map, build_deps, budget_s=25):
fast = set()
for f in changed_files:
fast |= coverage_map.get(f, set()) # 覆盖这些文件的测试
if fast and estimate_runtime(fast) <= budget_s:
return fast, "coverage"
return build_deps.affected_tests(changed_files), "package"
6.3 失敗の帰属:「もともとからあった問題」と「私が壊したもの」を分ける
検証工程で最も出错しやすく、被害も最も大きいステップです。Agent が赤いテストを見たときに出る道筋は二つで、自分が悪かったと判定するか、環境の問題だと判定するかです。前者は無意味な書き直しの反復を招き、後者は本物の bug をすり抜けます。正しいやり方はベースラインを確立することです。いかなるパッチを適用する前に、まずクリーンなツリーで同じ最小テストセットを走らせ、もともと赤かったテストケースを記録します。このベースラインのコストは L2 の半分ですが、帰属の問題が主観的な判断から決定的な照会に変わります。
- パッチ前は赤、パッチ後も赤 → 今回の変更とは無関係なので、記録してスキップする。
- パッチ前は緑、パッチ後は赤 → 本物の回帰。必ず直す。
- パッチ前は緑、パッチ後も緑、でも追加した能力に対応するアサーションがない → 検証不足。テストを足す。
最後の項目はよく飛ばされますが、実はそこが本当の問題です。「テストが全部緑」は「要件を満たした」ことを意味しません。idempotency_key を追加しても、重複呼び出しが同じ結果を返すというアサーションを足さなければ、機能的には何も保証になっていません。すべての能力ポイントに少なくとも一つのアサーションを対応させる必要があります。そうでなければ、その能力ポイントは検証されていないことになる。これは、L2 より前に行うべきカバレッジ上の検査です。
6.4.1 フィードバックループの反復規律
完全なループはこうです。読む → 位置を特定する → 直す → 検証する → 帰属させる → もう一度直す。エンジニアリング的には、このループに硬い制約を設けます。さもないと Agent は「直すたびに一度走らせる」という無限の試行錯誤に陥ります。
- 単一タスクあたりの最大反復回数:たとえば三回。三回やっても失敗するなら、微調整を続けるのではなく戦略を切り替えるべきです(仮説を変える、変更集合を狭める)。
- 毎回の反復で変える変数はひとつだけ:ロジックとテストを同時に変えると、帰属が不可能になります。失敗した場合は、まずテスト側の変更を巻き戻して、その失敗が本物かどうかを確認します。
- 失敗情報の再注入は圧縮する:十几個のスタックトレースをそのまま押し戻すのではなく、「失敗したテストケース+最初に関連しそうなアサーション+期待値と実際の値」まで圧縮します。ノイズがシグナルを覆い隠すと、Agent は当て推量を始めます。
- 行き止まりを記録する:試みて失敗した案は、実行不可であることを明示して記録し、次の反復で同じ探索を繰り返さないようにします。複数ターンのセッションではこれが特に重要です。
6.4.2 検証予算は最も効く場所に使う
検証にも予算配分が必要です。完全に走らせるだけのコストを払えるとは限らないからです。合理的な配分の原則は、「失敗確率 × 失敗の被害 × 検出の難しさ」で順位づけすることであり、検査項目の自然な並び順で決めることではありません。
投資する価値が最も高いのは通常、三種類の検査です。第一種類は検出が非常に難しい一方で、失敗の被害も非常に大きいもの。契約の同期漏れ、破壊的な削除、危険な操作。この種の変更は一度漏れると後続の検査がすべて無駄になるため、ゲートはここに置かなければなりません。第二種類は検出が非常に安いもの。構文、書式のノイズ、残ったデバッグコード、diff のサイズ。ほとんどコストがかからないので、毎回実施すべきです。第三種類はコストは高いがカバレッジが限定的なもの。完全な回帰テスト。必要ですが频繁に行う必要はないので、ループの末尾に置きます。変更のたびに実行するのではなく。
もうひとつ、見落とされがちな判断があります。反復回数が増えてきたら、検証の範囲を広げるのではなく変更集合を狭めるべきです。三回目でも失敗しているなら、どれほど大きな回帰テストをやっても問題は解決しません。問題はたいてい実装ではなく仮定の層にあるからです。正しい動作は、コードを読み返し、最初の判断をもう一度確かめることです。これは退行のように聞こえますが、実際には局所解から抜け出す唯一の経路です。
最後に、遅延予算についての反直感的な結論があります。フィードバックの遅延が有効な反復回数に与える影響は、乗算的に効きます。十秒のタスクなら同じ実時間の中に十五回試せますが、三分のタスクなら二度しか試せません。前者の十五回のなかで、モデルは複数の仮説を自然に探索して、そこから学びます。後者にはほとんど一度しか機会がありません。失敗情報を消化し終える前にタイムアウトします。「速い検査をサブ秒級まで詰め込む」価値は、「完全なテストをより網羅的にする」価値をはるかに上回ります。
6.5 信頼できない検証シグナル
ここまでは検証を四層に分けましたが、レイヤーの区分が解決するのは「何をするか」であって、「結果が何を意味するか」ではありません。いくつかのシグナルは、一見すると強いのに実際ほとんど情報量を持ちません。検証体系から明示的に除外しておかないと、虚偽な確信を出し続けます。
6.5.1 モデルにモデルを評価させる
もっとも警戒すべきは「言語モデルの出力を言語モデルでレビューする」という手法です。安価で、拡張しやすく、文字列マッチより賢く聞こえる、それが魅惑的なところです。しかし判定基準はテキスト表面が妥当かどうかであり、コードの欠陥はたいていテキスト表面の下に隠れています。シグネチャを変更して、すべての呼び出し点がテキスト上では矛盾がなく、読んでいてきわめて自然に感じるにもかかわらず、リフレクション経由で呼ばれるエントリをひとつ落としている——こういうことが起こります。
この種の判断の正しい使い方は、合否の根拠ではなく、仮説生成の源泉にすることです。「ありうる欠陥の仮説」のリストを作らせ、各仮説を実行可能な具体的な検査へ変換させます。あのリフレクションのエントリを探す、ある場所にアサーションが存在するか確かめる、ある設定項目を調べる、という具合です。判断そのものはシグナルになりません。決定的な検査に変換して初めてシグナルになります。同様に、否定の根拠にも使ってはいけません。否定は決定的な証拠でなければなりません。
6.5.2 「テストが全部緑」の三つのよくある偽り
第一がカバレッジの偽り。変更が触ったどの分岐にも、ひとつもアサーションが通っていない状態です。第二が環境の偽り。ネットワークがない、データベースがない、依存している任意のコンポーネントが欠けている、という理由でテストがフォールバックの分岐に走り、検証しているのは本番とは異なる挙動です。第三が弱いアサーションの偽り。アサーションが戻り値が空でないことしか確かめておらず、具体的な値を見ていない状態です。
この三つの偽りを見抜く方法は、より賢い検査ではありません。検証の前に、まず検証そのものが有効であることを確かめることです。重要な能力ポイントについて、意図的にパッチを壊し、対応する検証が落ちるかどうかを見ます。これはミューテーションテストの考え方です。コストは高くありませんが非常に効果的です。永久に発火しないテストは、最初のミューテーション検査の時点で露見します。多くのチームが一度もこれをやっていません。そしてこれこそ、「テストが全部緑」というシグナルを信頼あるものにする唯一の方法です。
6.5.3 型チェックの特殊な位置づけ
型チェックは検証体系の中で特殊な位置にあります。誤検出をほとんど生まず、速く、対象は選ばれたテストではなくリポジトリ全体です。L2 より前の位置に置くべきで、パッチ適用のたびに即時のゲートとして働かせることすらできます。その限界もはっきりさせておく必要があります。型チェックが発見できるのはシグネチャ層の不整合だけで、論理エラー、並行性の問題、境界条件には力えません。動的言語ではほぼ機能しません。
実践上よくある罠がもうひとつあります。「型チェックを通すため」に型無視やアサーション、緩い型変換を追加することは、検証手段の抜け道で変更の問題を隠すことです。これはテスト削除と同じ種類の行為であり、同じ禁止リストに入れるべきです。判定は単純です。無視の注釈や型変換の追加は、検査を通すために行った変更の中で必ず明示して注記し、別途一覧にまとめなければなりません。そうでなければ回避とみなします。
6.5.4 検証できない部分は明示的に宣言する
最後であり、いちばん誠実な項目です。この検証体系では覆いきれないものが有一定量あることを、認めることです。複数端末のあいだでの挙動の差、長時間動かしたあとのリソースリーク、競合条件、性能的特性、利用者に表示される文言や見た目、そして実際の外部システムに依存するあらゆる挙動です。システムはこれらが存在しないふりをしてはいけません。引き渡し説明のなかで「これらは検証されていない」ことと、その理由を明示的に列挙すべきです。
この宣言の実務上の価値は、人の行動を変えるところにあります。「並行経路は未検証」と書かれていれば、review する人は自分でその部分を確認します。逆に「すべて検証済み」と主張された引き渡しを受け取った人は、システムをそのまま信じて、検証を省いてしまいます。不確実性をありのままさらけ出すことは、結局、受け取る側の注意力を正しく配分することそのものです。いかなる自動検査よりも、それのほうが全体の品質を一段引き上げます。いかなる自動検査よりも、それのほうが全体の品質を一段引き上げます。
7 破壊的な変更を防ぐ
ここまでの六節が目指していたのは「正しい変更を小さく保つ」ことです。この節の目標は逆で、危険な変更が大きくなる前の迷いを長く引き延ばすことです。両者を分けるには、明快な危険操作リストと、それに対応するゲート戦略が必要です。
7.1 六種類の危険操作とその特徴
| 危険操作 | 識別の特徴 | デフォルト方針 |
|---|---|---|
| ファイル/ディレクトリの大量削除 | 一度の変更で N 個を超えるファイルを削除する、または import されていないファイルを削除する | 既定では拒否。明示的な確認が必要で、参照の証拠を添える |
| 依存バージョンの変更 | requirements、package.json、Cargo.toml、go.mod の修正 | 単独のタスクとし、ロジックの変更と同じ変更にまとめない |
| 公開インターフェースのシグネチャ変更 | 変更したシンボルの参照数が閾値を超える、または公開シンボルに属する | すべての呼び出し点の網羅と移行説明を求める |
| 一括リネーム/移動 | 大量のファイルにまたがる識別子の変更 | テキスト置換ではなく、IDE や LSP によるリファクタリングを優先する |
| 権限・秘密情報・CI 設定 | .env、secret、workflow、Dockerfile を含むパス | 書き込みを直接禁止する |
| テストの削除またはスキップ | テストの削除、skip の追加、アサーションのコメントアウト | 書き込みを直接禁止する。テストは追加のみ可能で、削減は不可 |
最後の二行は、個別に強調する価値があります。「既存のテストを緑にする」という誘惑は、すべての Agent システムにおける最大の内生リスクです。失敗したアサーションを削除したパッチと、bug を直したパッチは、「テストが通った」というシグナルのもとでは区別できません。したがって正しいルールはこうです。テストファイルには追加と、理由を明示したうえでの緩和だけを認め、削除・コメントアウト・スキップは認めない。このルールはツール層で強制しなければなりません。プロンプトで縛っておくだけでは信用できません。
7.2 ゲートの三段階の強度
危険操作への防護は「許可/禁止」の二段では足りません。強度ごとに三段階に分けると実用的です。
- 自動通過:影響範囲が小さく、巻き戻せ、テストで覆われている局所的な変更。
- 証拠の添付が必須:フローは止めないものの、パッチに根拠を添えることを求めます。たとえばファイルを削除するときには、「リポジトリ全体で参照がない」ことを示す独立した証拠が二件必要です(import グラフに入辺がないこと、全文検索のヒットが自身とドキュメントだけであること)。Agent がこれを提示できなければ、自動的に拒否へ降格させます。
- ハードブロック:秘密情報、CI、権限、そして取り消せずロールバック経路のないあらゆる操作。これらは会話のフローに載せず、ツール層で直接拒否します。
依存のアップグレードは、この三段階のなかで最も典型的な例外です。「たった一行を変えるだけ」に見えるからです。
# 危险操作闸门:在补丁应用前逐条检查
DANGEROUS_PATTERNS = [
(r"^-\s*(import|from)\s+[\w.]+", "删除 import:可能是死代码判断错误"),
(r"^\+\s*(import|from)\s+[\w.]+", "新增依赖:需要单独评估传递依赖"),
(r"^[+-].*\"[a-z_\-]+\"\s*:\s*\"[\^~]?[<>=]", "manifest 版本变更:锁文件必须同步"),
(r"^\+.*(password|secret|token|api[_-]?key)", "凭据字面量:直接拒绝"),
(r"^\-.*(pytest\.mark\.skip|@Ignore|xit|xtest)", "跳过测试:直接拒绝"),
]
def gate(diff_text: str, index, policy) -> list[tuple[str, str]]:
findings = []
for line in diff_text.splitlines():
if not line.startswith(("+", "-")) or line.startswith(("+++", "---")):
continue
for pattern, reason in DANGEROUS_PATTERNS:
if re.match(pattern, line, re.IGNORECASE):
findings.append(("block", reason))
deleted = re.findall(r"^--- a/(.+)$", diff_text, re.MULTILINE)
if len(deleted) >= policy.max_delete_files:
refs = {f: index.references_by_path(f) for f in deleted}
unproven = [f for f, r in refs.items() if r]
findings.append(("need_evidence",
f"批量删除 {len(deleted)} 个文件,其中 {len(unproven)} 个仍有引用"))
return findings
依存のアップグレードについては、よく見落とされる点が四つあります。ロックファイルは同時にコミットしなければならない。さもないと、ローカルでは通っても CI では落ちます。推移的依存の変化は見えない。直接の依存を一段上げるだけで、推移的依存が数十個変わることもあります。任意依存と peer 依存は、いちばん追跡しにくい破壊を生みます。加えて廃止されたバージョンとセキュリティ上の警告。更新が避けられない場合もあり、そのときは「最新に上げる」と「最小限の使えるバージョンに上げる」を区別しなければなりません。この四点はいずれも、依存の変更を単独のタスク、単独のパッチ、単独の検証として扱い、業務ロジックの中に混ぜないことを意味しています。
7.3 隔離された実行:検証はサンドボックスの中で行う
検証工程では実際にコードが実行されます。それ自体がリスクを含みます。最低限の隔離要件は次のとおりです。ソース領域は読み取り専用でマウントし、作業領域だけを書き込み可能にする(Agent が直接ソースを書き換えないようにする)、ネットワークは既定で遮断する(テストがネットワークを必要とする場合は、明示的にホワイトリストを開ける)、リソースの上限(CPU 時間、メモリ、プロセス数。暴走したテストがマシンを埋めてしまわないようにする)、そして本番の認証情報を注入しないこと。このうちネットワークの遮断はよく見落とされますが、同時に安全性と正しさの保障にもなります。ネットワークがないと多くのテストは mock の分岐を通るため、このときの「テストが通った」の意味はまったく違うものになるからです。
見落とされがちな隔離の観点がもうひとつあります。作業領域を破棄できるかどうか。検証全体は使い捨てのコンテナや一時的な worktree の中で行い、終わったら破棄します。こうすれば Agent の境界を越えた変更が本物のリポジトリを汚すことはなく、「Agent の変更をどう取り消してよいか」という難題も生まれません。逆に、システムが使い捨ての作業領域を提供できないなら、それと同等のパッチ単位のロールバック能力を持たなければなりません。
7.3.1 ロールバック能力:変更のあとにどう戻すか
ここまでの議論は、すべて「承受できない変更をしないこと」を中心に組み立ててきました。しかし自動的な書き込みがひとつでも存在するなら、もっと素朴な問いに答えなければなりません。本当に間違えてしまったとき、どうやって戻すのか。この能力は最後にになって初めて思い出すことが多く、しかしその形は、実は先に決めておくべき設計をいろいろ左右します。
ロールバックの方案は強度ごとに三段階に分かれます。パッチ単位のロールバックは、すべての変更を巻き戻せるパッチの形でコミットすることを求めます。取り消すとは逆向きに適用することだけで、変更の過程は完全に追跡できます。前提は、タスク開始時に作業領域がクリーンであることです。もともとコミットされていない変更があれば、パッチ単位のロールバックは安全に実行できません。その場合は先にクリーンアップか退避を求める必要があります。現実には多くのシステムがここで詰まっています。スナップショット単位のロールバックは、タスク開始前に完全なスナップショットを取り、問題があれば全体を戻すものです。実装は簡単で粒度は粗く、ひとつのタスクに人が手を入れた変更が混ざっていれば、それも一緒に消してしまいます。ブランチ単位のロールバックは、各タスクが自分のブランチまたは作業ツリーで作業し、完了後にマージで本流へ入る形です。問題があればブランチを捨てるだけで済みます。意味的にはもっとも綺麗で、タスクの中間状態は最初から最後まで本流を汚していないからです。
この三つの方式が前提となる設計に求めるものはまったく異なり、だからこそ事前に決めておかなければならないのです。ブランチ単位のロールバックは、システムが最初から作業ツリーの隔離をサポートしている必要があります。パッチ単位のロールバックは、変更を最初の書き込みから構造化されたパッチとして残すことを求めます。適当にファイルを上書きするのではなくて済みます。スナップショット単位のロールバックにはほとんど要求がありません。ただし復元するときに、人の変更まで一緒に失う危険があります。どれを選ぶかは、タスク開始時に作業領域がクリーンかどうか、人による操作が介在するかどうか、リポジトリの大きさが頻繁にスナップショットを取れるかどうか、によって決まります。実装のしやすさではありません。
ロールバックに似ていて、実際には違うものをもうひとつ区別しておく必要があります。モデルに「変更を元に戻して」と頼むことです。モデルが自分の変更を覚えていることに依存します。しかも、その記憶はあやふやであり、元に戻す過程そのものが新たな不整合を生むこともあります。ロールバックは決定的な操作でなければなりません。自然な言語の一文で済ませられるものではありません。
最後に、ロールバックできない操作は、ロールバックに守らせるのではなく単独で隔離する必要があります。すでに送信されたリクエスト、すでにリモートへ push されたコミット、すでに実行された移行、すでに削除されたファイル。これらは一度起こってしまえばロールバックの経路がありません。第八節の各種ゲートが事前に止めるしかありません。システムの誠実なやり方は、操作を二つに分類して明示することです。ロールバックできるものは自由にやる。ロールバックできないものは人間の確認チャンネルを通す。両者を混ぜて同じ手順で扱うのは、自動化システムでもっとも事故を起こしやすいところです。
8 変更が終わったあとの自己点検リスト
パイプラインを走らせ終えたことは、変更を交付してよいことを意味しません。最後の関門は、構造化された自己点検です。その役割は「モデルの主観的な自己評価」を「照合できる客観的事実」へ置き換えることにあります。下の表は、現実的に使える最小限の項目をまとめたものです。点検コストの低い順に並べていて、前のものほど、モデルに自制を委ねるのではなく自動化すべき項目です。
| 点検項目 | 判定のシグナル | 不合格のときの行動 |
|---|---|---|
| diff の大きさ | 総変更行数と変更ファイル数の比が、要件の複雑さから求めた上限を超えていないか | 最小の変更集合で書き直す |
| 書式のノイズ | 空白・引用符・import の順序だけの差がある行の割合 | ノイズの行を自動で復元する |
| 変更ファイルのホワイトリスト | どのファイルも、いずれかの能力ポイントか契約の同期に対応づけられるか | 対応づけられないファイルは巻き戻す |
| 契約の完全性 | シグネチャ変更に対応するすべての呼び出し点が更新されているか | 漏れた部分を補うか、任意引数にする |
| テストカバレッジの写像 | 能力点ごとに、少なくとも一つのアサーションがあるか | テストを足す |
| 危険操作の走査 | 秘密情報・CI・依存・テスト削除に触れていないか | ブロックして理由を説明する |
| 残骸 | デバッグの print、ブレークポイント、一時ファイル、コメントアウトされたコードが残っていないか | 片付けてから L2 を走らせ直す |
| 生成物との整合 | コード生成や移行の実行が必要かどうか | 生成コマンドを実行するか、生成元を変更する |
| ベースラインとの比較 | 新規に失敗したテストケース数がゼロであること | パッチ生成に戻る |
| 能力点のカバレッジ | 要件から分解した能力点に、対応する変更と検証があるか | 変更集合の導出に戻る |
この表のなかで、もっともシステムに無視されがちで、それでいて効果が最も高いものが三つあります。変更ファイルのホワイトリスト(最小という期待を、照合できる制約へ変えます)、残骸の走査(デバッグの残留は Agent がもっともよくある引き渡し側の欠陥で、review する人にパッチ全体への信頼を失わせます)、能力点のカバレッジ(これが「要件は本当に満たされたのか」に答えられる唯一の検査で、残りの検査は「変更は自己整合しているか」を尋ねているだけだからです)。
検証の仕組みについても、言っておきたいことがあります。それは問い直せ、再現できなければならないということです。誰かが独立に同じ結果を再現できないなら、その検証は引き渡しを形よく整えるためだけのものになり、正しさを守る就不是その役割です。もっとも基本的な要件はこうです。走らせたテストコマンド、実行の範囲、スキップしたテストケース、そしてスキップの理由。これらをすべて引き渡し書類に書いて、第三者が三十秒でその検証の重みをどう評価するか判断できるようにします。
検査の順序についても、ひとつ強調しておきたいことがあります。この表は上から下へ、おおむねコストの昇順に並んでいます。ただし実行のときに機械的にそのまま写すのは避けてください。純粋な論理エラーは絶対に書式検査では見つけられません。純粋な書式の問題に完全なテストを走らせる意味もありません。本当に合理的なやり方は、各レイヤーが自分の答えられる質問にだけ答えるようにすることです。あるレイヤーの検査に失敗したら、ただちに巻き戻します。次のレイヤーで補おうとは期待しません。「失敗したら即巻き戻す」というこの規律のほうが、チェックリストにさらに十項目を書き足すよりずっと有効です。
8.1 引き渡しに含めるべきもの
最後に、人へ渡すものは説明ひとつとパッチひとつで、「完了しました」という一文だけではありません。内容として十分な説明は、少なくとも次の項目を備えます。何を変えたか。要件の原文に対応づけられる形で。なぜそう変えたか。とくに明らかではない設計上の選択について。何を変えていないか。スキップした候補の変更を、その理由とともに明示すること。何を検証したか。どれを走らせたか、結果はどうだったか、どれを走らせなかったか、そしてその理由。そして残るリスク。インデックスが覆いきれていない辺、曖昧な呼び出し点、検証していないプラットフォーム差異。
「何を変えていないか」の項は価値が高い。モデルの判断過程を外部に晒すことで、人間はいくつかの重要な判断ポイントへ素早く介入できます。たとえばモデルがある呼び出し点は変更しなくてよいと判断したものの、人間はそれが外部システムの固定された契約だと知っている、という場合です。わずかに一度の人の介入で、本番障害をひとつ防げます。逆に、この欄が空なら受け取る側は、モデルの判断過程を組み立てるために diff を一行ずつ自分で読むしかありません。コストははるかに高く、たいてい読み取れません。
この説明には、見落とされがちな使い方があります。このリポジトリを次に変更するときにもっとも価値の高いコンテキストになるからです。三か月後に誰かがこのコードを引き継いだとき、いちばん知りたいのは当時の実装の細部ではありません。「どこは冗長に見えても実際には消せないのか」「どの境界条件は意図的にそうできているのか」ということです。もしこれらの判断が引き渡し書類に書いてあれば、次の変更は今よりずっと楽になります。書いていなければ、この知識はセッションの終了とともに消え、次回は同じ穴をまた踏むしかありません。この角度看ると、よい引き渡し書類は本質的に一度きりの修正を、一度きりの知識の蓄積に変えるものです。これが自動化された変更が、一度きりの人手による変更に対して持つ本当の優位点です。ただし、知識を書いていることが前提です。
8.5 複数エージェントの協業が持ち込む追加の制約
ここまでの八節が述べてきたのは、単一のエージェントが単一のスレッド上でたどるチェーンでした。并列の代理を一度導入すると、難易度はずっと跳ね上がります。そのまま線形に増えるのではなく。まったく新しい三種類の失敗が加わるからです。共有状態をめぐる競合、タスク分割による継ぎ目の隙間、そしてスタイルの不一致によるマージの悪夢です。
8.5.1 共有インデックスの並行性とタスク分離
複数の代理が同じ作業領域で同時にコードを書き換えると、もっとも直接的な問題はインデックスが互いの途中状態を見ることになることです。代理 A がシグネチャを変えたところで呼び出し点をまだ直していない。代理 B がその瞬間にインデックスを読むと、コンパイルの通らないリポジトリが見えてしまい、その影響範囲の分析全体がズレます。対策は三つあり、コストは増加する順に並べると次のようになります。
- スナップショット読み。各タスクの開始時にインデックスのスナップショットを固定し、タスクのあいだは他の代理の変更を認識しません。代償は、タスク終了時のシグネチャの一貫性がタスクをまたいで崩れることなので、最後に全体のマージ後検証を一度行う必要があります。
- 書き込み集合の分離。各代理が書き換えてよいのは、自分が担当があると宣言したシンボルだけです。ひとつのシンボルを複数の代理が担当することはありません。衝突は実行時からスケジュール時に前移し、計画の精度で安定性を買える、という取引です。
- ブランチ分離。各代理が自分のブランチまたは worktree で作業し、最後にマージの工程が衝突を解決します。もっとも徹底した方法であり、人間のチームの働き方にもっとも近い方法です。
強調しておきたいのは、書き込み集合の分離が思ったよりずっと簡単に実装できるということです。さっきまでの変更集合の導出が「誰がどのシンボルを変更するのか」をすでに計算しているからです。難しいのは分離そのものではなく、ひとつのシンボルを二つの能力点が本当に変更しなければならないときの扱いです。正しいやり方は、その状況を計画の段階で発見して、ひとつのタスクにまとめることです。パッチの段階でぶつかるのではなく。
8.5.2 分割の単位はファイルではなく能力点
タスクをファイルで分割するやり方は、もっとも一般的で、もっとも疑わしいやり方です。能力点は本質的にファイルにまたがるからです。「新しい決済チャネルに対応する」という能力点は、必然的にゲートウェイ層、API 層、テスト層のすべてにまたがります。ファイルで分割すると、各代理が三分之一の情報しか受け取らず、残りの二つがどうなっているかをそれぞれ推測し、最後にコンパイルは通るが意味として誤ったコードが束縛されます。
正しい分割の原則は三本あります。能力点で分割することで、各タスクの成果物が自己整合であることを保証します。依存関係に沿って直列化することで、依存される側の部分が先に完成し、検証されることを保証します。共有する型定義は単独のタスクとして最初に行うことで、他のすべてのタスクに対する入力の契約となるからです。并列にできるかの判断基準は単純です。タスク B の正しい実装がタスク A の成果物の内容に依存するなら、その二つはまとめるか直列に並べなければなりません。并列に推測させることはできません。
8.5.3 独立した検証者は、より多くの実行者より価値がある
二つ目の代理に同じコンテキスト、同じモデルで最初の代理のパッチを「チェック」させても、得られるものは見た目ほどの価値には遠く及びません。同じ盲点は同じ誤りを再現するからです。効果的なクロス検証にはコンテキストと役割の二重の隔離が必要です。
第一に、パッチからではなく要件から出発することです。検証者に見せるのは元の要件と最終的な diff であり、実行者の思考過程ではありません。そうすることで初めて「この変更は要件を満たしているか」と問えるのであって、「このパッチは自己整合しているか」と問うのではありません。
第二に、逆向きの検証です。各能力点について、検証者に「この要件が実装されていないなら、もっとも起きやすい現象は何か」と答えさせ、その現象に対応するアサーションがテストに存在するかを確かめてもらいます。安価で、しかも「テストは全部緑なのに機能がない」という状態を捕まえるのに有効な手段です。
第三に、破壊的な仮定の検証です。検証者に逆の役割を演じさせます。「このパッチにはバグがあると仮定すると、もっとも誤っているのはどこか」と問うのです。中立的な「ご確認ください」という質問よりも多くの問題を引き出せます。ただし、検証者が diff だけでなく十分なシンボル階層のコンテキストを受け取っていることが条件です。
8.5.4 マージ前のパッチの正規化
複数の代理が生み出したパッチは、スタイルに系統的な差を持ちます。名前の好みが違う。ログの言い回しが違う。エラーの扱い方が違う。フォーマットの度合いが違う。もしこの差をひとつのパッチ集にまとめて人間に review させれば、結果は review の即座の放棄です。パッチの質が悪いからではありません。diff のノイズが読み取れなくさせているからです。
対策は、マージの工程で一度、スタイルを正規化することです。リポジトリ自身のフォーマッタを通す。直近のコードスタイルに合わせて調整する。繰り返し現れるパターン、たとえばエラーログのテンプレートをそろえる。正規化はマージのあと、引き渡しの前に必ず行う必要があります。そしてもう一度検証を走らせる必要があります。フォーマッタもスタイルの揃えも、それ自体がコードを変える変更だからです。そのあとで検証しないのは、最初から検証しなかったのと同じです。
8.6 この能力をツールの契約として整える
ここまでの能力は、すべて最終的にツールとしてモデルに公開されます。つまりインターフェース設計そのものが、このエンジニアリングの一部です。包装の問題ではありません。粗いインターフェースではモデルの誤用が頻繁に起こり、丁寧に設計されたインターフェースでは、下層の能力がまったく同じでもモデルの正答率が明らかに上がります。
8.6.1 粒度の選び方:粗いツールと細かいプリミティブの配分
粒度には唯一の正解がありませんが、経験則がひとつあります。「判断」はプリミティブ層に、「编排」は粗いツール層に残すということです。プリミティブ層は精密で、検証可能で、副作用のない能力を提供します。シンボルから定義を引く。シンボルから参照を引く。パスでファイルを読み取る。アンカーを含むアサーション付きのパッチを適用する。粗いツール層は編制を担当します。「この能力点を実装する」のような上位の操作で、どのプリミティブを呼ぶか、何回呼ぶか、予算をどう配るかは内部で自分で決めます。
すべてを粗いツールにすると観測性が悪くなり、失敗のときにどの工程で失敗したのかが分かりません。すべてを細かいプリミティブにするとモデルの負担が重くなり、呼び出しの順序を自分で計画せざるを得ません。現実のタスクでは大量の往復を無駄にしながら、しかも手順を飛ばします。妥当な配分はこうです。プリミティブ層は豊かで安定しており、粗いツール層は少なくて組み合わせ可能。さらに、粗いツールのどの工程も単独で観測でき、単独で再実行できるべきです。
8.6.2 エラーメッセージはモデルのために書かれる
もっとも見落とされにくい、しかし効果が大きい要点がここにある。ツールが返すエラーメッセージは人に見せるログではなく、モデルの次のステップへの入力です。「エラー」の二文字をそのまま返せば、モデルは自分で復元方法を当てるしかなくなります。正しいやり方は、エラーを分類し、どの分類にも「いま何をすべきか」を明示することです。
| エラーの分類 | 典型的な発生源 | モデルが取るべき行動 | 出してはいけない情報 |
|---|---|---|---|
| アンカー不一致 | パッチの根拠にした原文がすでに変わっている | その領域を読み直し、新しい内容をもとにパッチを作り直す | 「apply failed」という一文だけ |
| インデックスを利用できない | インデックスが古い、またはそのシンボルが収録されていない | 全文検索へ降格し、今回の結果の信頼度が低いと注記する | シンボルが存在しないかのように振る舞う |
| 予算の超過 | 必要なコンテキストが残っている枠を超える | まず粒度を小さくして切り詰め、次に予算の拡張を要求する | 黙って切り詰める |
| 曖昧さが未解決 | 候補が複数あり、静的に決められない | 推測をやめ、候補をユーザーに示して確認を求める | どれかひとつを選んで続ける |
| 危険操作 | 秘密情報に触れた、テストを削除した、など | 停止し、発生の理由と代替案を添える | 拒否するだけで理由を示さない |
| 部分的な失敗 | 複数ファイルにまたがるトランザクションのどれかが適用されなかった | すでに巻き戻した。現在の状態をもとに一括で作り直す | 成功した分だけを報告する |
この表のなかでもっとも直感に反するのは、「インデックスが使えないときにシンボルが存在しないかのように振る舞わない」という項目です。誠実な降格と明示的な信頼度の注記があれば、モデルは自分でより保守的な戦略を選びます。逆に、成功を装った空の結果は、モデルに非常に高い確信を持たせて誤った全体判断を下させます。モデルはこのシンボルが本当にどこからも参照されていないと思いこみ、本来必要な同期の変更を飛ばしてしまうからです。
8.6.3 冪等性、再現できること、トランザクションの境界
この三つの性質は、システムが失敗に耐えられるかを直接決めます。冪等性は、再試行しても二重の破壊が起きないことを保証します。削除やリネームのような操作では、再試行が安全な no-op でなければなりません。再現できることは、どの段の入力と出力でも後から監査できることを保証します。インデックスのスナップショット、コンテキストの台帳、パッチ、検証の結果は一緒に保存しなければなりません。どれかひとつでも欠ければ再現できません。明確なトランザクション境界は、失敗したときに決定的な手順で整合した状態へ戻れることを保証します。
この三つに共通する前提がひとつあります。すべての判断は構造化されたデータとして残す、というものです。会話の履歴は圧縮されることも、要約されることも、切り詰められることもあります。信頼できる保管場所ではないのです。長いタスクではこれは生死を分ける問題です。途中まで進んだタスクが会話の記録だけを残し、構造化された判断ログを残さなかったなら、そのタスクは続きもできず、あとから読み返すこともできません。
8.6.4 観測可能性:コンテキストの判断を監査可能な軌跡に変える
最後に、チェーンの各段をひとつの軌跡として記録してください。この段はなぜこれらのファイルを呼び戻したのか。各レイヤーの予算を実際にどれだけ使ったか。パッチのどのブロックがどの能力点に対応するか。検証はどのテストケースを走らせたか。失敗をどう帰属させたか。この種の軌跡の価値は開発フェーズでもっとも大きく、「モデルの変更漏れは、コンテキスト収集の段階で呼び出し点が切り詰められていたから」という根本原因を発見できるからです。「モデルが十分に賢くない」という結論で終わらせずに済むからです。
そしてこのシステムが長期的に複数のリポジトリと複数の人を支えるようになれば、軌跡の価値は監査の側へ向かいます。誰が、いつの、何を根拠に、どのコードを書き換えたのか。当時のインデックスの状態はどうだったか。当時のテストはどこまで覆っていたか。実はこれは、コードそのものよりも完全な変更記録です。コードは結果しか記録しませんが、軌跡は推論の過程を記録するからです。複数エージェントのシステムのなかでは、この記録は「この変更の束がどうできあがったのか」を理解させてくれる唯一のものです。
9 全体を貫いて見る:これは予算の配分で、互いに独立した六つの手順ではない
チェーン全体を平たくして見ると、固定の総予算(モデルのコンテキスト、実時間、そして人の介入枠)のもとで行う一度の配分にほかなりません。
- 情報予算はインデックスとコンテキスト収集に配分します。ここに投資する価値がもっとも高いのは、誤ったコンテキストはより強い推論では埋められないからです。
- 推論予算は変更集合の導出とパッチ生成に配分します。ただし「最小」という制約で、無償限に膨らむのを抑さえなければなりません。
- フィードバック予算は検証に配分します。早くて安い検査ほど優先し、L0 と L1 の投資対効果は L3 とは比較になりません。
- 権限予算は危険操作のために残します。中核は「取り消せない」部分を会話の中から完全に追い出すことです。
この四つの予算は互いに競い合います。そこが本当のエンジニアリング上の難しさです。よくある不均衡は、大量の予算をコンテキスト収集に使い(ぎっしり詰め込んで)、全量テストは一度しか走らせないというものです。あるいはその逆で、grep による呼び戻しだけを使いインデックスを使わないまま、もっと多くの token をコンテキストへ詰め込むというものです。も失敗します。前者は反復できないほど遅く、後者は規模化できないほど高分です。
9.1 適用の境界:どの規模、どの言語、どのシーン
このやり方がどこで割に合わないのかを説明しておく必要があります。単一ファイルやごく小さいプロジェクト(数千行まで)であれば完全なインデックスは不要で、いっそ全文をコンテキストへ入れたほうが単純で確実です。インデックス構築の時間と失敗の危険は取り戻せません。初めて触るプロジェクトでもインデックスの価値は割引されます。履歴(誰が変更したか、なぜ変更したか、どの部分が安定しているか)が欠けており、いまの構造しか見えないからです。構造の外にある意味の情報こそ、Agent が判断を最も誤りやすい部分だからです。
言語の面では、静的型付き言語で効果が最も高くなります。受け手の型が既知なので曖昧性の解消はほぼ無料で、シンボルの一貫性検査に型の情報をそのまま使えます。動的言語でも実装はできますが、曖昧な辺が明らかに増えるので、不確実性をより多くユーザーへ明示する必要があります。生成コードの割合の高いリポジトリでは、インデックスの正確さに格別の注意が要ります。派生コードの辺は call graph を強く汚染するからです。
チームの成熟度も実際の制約です。リポジトリにテストがなく、ビルドがしばしば壊れていて、コードスタイルも統一されていないなら、ここまでのチェーンのうち「テスト写像」と「フォーマット正規化」に依存する工程は空回りします。このとき投資対効果のよい順序は逆になります。まずテストを足し、ビルドを直し、スタイルをそろえて、それからエージェントのチェーンを入れる。多くのチームはこの段階を飛ばして導入しようとし、結果としてチェーンは組み上がったものの L2 と L3 の層には一度も到達しません。検索とファイル書き込みだけが残り、使っても worse にはなりません。
組織面の現実的な制約も、はっきりさせておく価値があります。この類のシステムの保守コストは相当なもので、インデックス規則は言語のバージョンやビルドシステムに合わせて調整が必要で、ゲート方針は事故事例たびに追加されつづけ、検証集合の選び方はプロジェクトごとのテストフレームワークへ適合させなければなりません。これは箱から出してすぐ動く道具ではなく、人が絶えず努力し続けなければならないインフラです。チームにこの人員がいなければ、中途半端な版を作るくらいなら範囲を最小に絞ったほうがよいです。シンボルのインデックスと読み取り専用の検索だけを用意し、自動書き込みは開けない。そうすれば最低でも二節までの成果はすぐ得られ、書き込み操作がもたらすリスクも持ちこみません。
最後に場面の境界についてです。このチェーンが解いているのは局所から中程度の爆発半径をもつ変更です。機能の追加。バグの修正。パラメータへの対応調整。局所的な実装のリファクタリング。対象システムをまたぐアーキテクチャ級の変更(広範囲なインターフェースのバージョン化、ストレージエンジンの差し替え、複数言語のランタイムの統一)にも、インデックスと検証は依然として必要です。ただし、主導する要素は移行戦略と人の設計判断に移り、Agent はここで「影響範囲の分析者」と「案の起草者」のほうが向いており、「変更の実行者」ではありません。この境界を認めることは、この道具をあらゆるタスクへ無理に当てはめることよりも価値があります。
9.5 端から端まで一度通す:注文に「一部返金」機能を足す
抽象的な方法論では薄く見えるので、実際の要件を頭から最後まで一度通すほうが、各節どうしが噛み合うのが分かりやすくなります。以下のタスクは十分に典型的です。複数ファイルにまたがる変更があり、契約の変更があり、データへの影響があり、テストの穴があり、しかもどの段階でも失敗しうる。
要件の原文:「客服がバックオフィスで支払い済みの注文に対して一部返金を開始できる。返金額は注文の実支払額を超えてはならない。発送済みの注文は一部返金を認めない。この操作には監査ログを残す。」この一文のなかには実は五つの能力点が隠れており、どれか一つが欠けると後工程のどこかで表面化します。
9.5.1 インデックスとシード展開
アンカーの語から出発します。「注文」「返金」「支払い」。symbol table で正確に照合すると、いくつかの候補群が当たります。注文のドメインモデル。支払いゲートウェイの返金メソッド(すでに全額返金がある)。バックエンドの API 層の関連エンドポイント。そして監査ログのモジュール。ここでもっとも大切なのは「すでにあるが露出していないもの」と「まったく存在しないもの」を区別することです。全額返金はすでに存在します。一部返金はそれを拡張すべきで、ゼロから作り直すべきではありません。インデックスがこの経路の呼び出し鎖(コントローラ、サービス、ゲートウェイの実装、永続化、監査フック)を引き出せなかったなら、モデルは返金のロジックを 새로書き、既存の権限チェックと冪等処理を迂回してしまう可能性が高いのです。
call graph は、ここでも見落としやすい辺をひとつ暴きます。金額の型の変換です。実支払額はシステム内で整数と十進浮動小数点という二つの表現を同時に持っています。返金金額の型を間違えると、精度が失われます。これは「契約」の層のコンテキストで発見されたものです。refund_amount フィールドを追加したとき、金額に関わるすべての型の定義が配額に入ったからです。
9.5.2 予算の配分と、実際の切り詰め
候補集合は 180 個のシンボルになり、到底入る量ではありません。層ごとの配分を終わったあと、definitions 層には返金サービス、注文モデル、ゲートウェイのインターフェースの完全な実装が入りました。call_sites 層には 6 個すべての呼び出し点と、その受け手の型が入りました。contracts 層には注文状態の列挙型、金額の型の定義、そして監査ログの書き込みインターフェースのシグネチャが入りました。ここで一度の切り詰めが起きます。監査ログのモジュールには 40 個の書き込みメソッドがあり、すべて展開すると二万以上の token を消費します。注文に関係する三つだけを展開し、残りはメソッド名の一覧の一行に畳みました。
切り詰めの結果は妥当です。要件が求めているのは「監査ログを残すこと」であって「任意のログメソッドを呼ぶこと」ではないからです。ここでログのすべてを展開すると、予算を無駄にするうえ、モデルの注意が無関係なログの分類に薄まってしまいます。切り詰めの正しさは、能力点の分割が明瞭であることに依存します。だからこそ第四節で、まず要件を能力点へ翻訳しておく必要があるからです。能力点は変更の境界であると同時に、コンテキストを切り詰める正当性の根拠でもあります。
9.5.3 変更セットと、その一度の質問
変更集合の導出結果は 5 つのファイルになりました。返金サービス(一部返金のメソッドを追加)。ゲートウェイのインターフェース(引数を追加)。二つの呼び出し点(引数を渡す)。注文状態の列挙型(中間状態を追加)。そして監査ログの接続。システムはこの段階で、三回の質問のうち二回目を発火させました。既存の返金メソッドがすでに冪等性を処理している一方、要件のなかに「操作」という言葉が現れ、「同じものを二度押ししても二重返金になってはならない」という要求が暗に含まれているものの、要件の原文はそれを明示していないと分かったからです。これは典型的な「明らかにもっと小さい手順が存在する」ケースです。新しいロジックを書き足す必要はなく、冪等性キーが一部返金のシナリオを覆っていることを確認すればよいだけです。そこでユーザーに確認し、了承を得たうえで、この一件は新たな能力点として扱うのではなく、変更集合のなかに取り込まれました。
三つ目の質問(カバレッジのない変更)もこのタスクで現れました。追加した中間状態が状態の列挙型のなかにありますが、状態機械の遷移の合法性をカバーするテストがまったくありません。このシグナルは明示的に報告され、「テストを補わなければ、不正な状態の組み合わせを検証できない」という説明が付きました。最終的にパッチには状態機械の遷移を検証するテストファイルが一つ追加されます。これは検証の穴が逆向きに新しい変更を動かした事例です。
9.5.4 パッチと、一度のトランザクション巻き戻し
5 つのファイルは 3 つのトランザクションブロックに組織されました。インターフェースと実装は生死を共にする必要があります(強い結合)。二つの呼び出し点は独立に扱えますますが、どちらも同じ能力点に属します(弱い結合)。状態の列挙型とテストは単独です。適用の過程で、ある呼び出し点が別の場所のリファクタリングによってアンカー不一致を起こしました。これがまさに 2.3 節で上げた古いインデックスの問題です。タスク開始時のインデックスのスナップショットの中では、その呼び出し点は元の位置にあり、タスク途中のどこかの読み取りがフォーマットを起動し、行番号も内容も変わってしまったのです。
トランザクションブロック全体が巻き戻され、ツールが返したのは「apply failed」ではなく「アンカー不一致です。その領域を読み直してください」でした。モデルがこのエラーを読めば、次に何をするかが分かります。読み直す。その呼び出し点のパッチを作り直す。アンカーの検証をやり直す。そして最後に、パッチ全体は無事に応用されました。変更は 61 行、そのうち 4 行は書式のノイズで自動的に復元されました。
9.5.5 検証と、その不完全さ
L0 は通過しました。L1 のシンボルの一貫性検査は、二箇所の変更漏れを見つけました。リフレクション経由で登録された二つのハンドラのなかにも呼び出しがあり、インデックスはそれらを動的な辺として印づけ、モデルは規則どおり先に読み直して確認し、それから同期して直しました。これは本タスクで L1 が本当に一つ救った場面です。L2 は関連する 12 のテストを選び、そのうち 2 つはベースラインの時点で既に赤かった(リポジトリに未修正の問題が元からあった)ので、既存の問題として正しく帰属されました。新たに追加した能力点には 3 つのアサーションを追加し、それぞれ金額の上限、発送済みの拒否、冪等な重複呼び出しをカバーしています。L3 は返金と注文の二つのパッケージの回帰テストを走らせ、整合性のテストは通過しました。
引き渡し説明のなかには、検証していない部分が明示的に書かれていました。並行のシナリオで、二人が同時に同じ注文の一部返金をする場合は検証していません。既存のテストフレームワークにこの種の並行フィクスチャがないためです。監査ログがデータベースに残る際の形式も人が確認する必要があります。これは誠実で、かつ役立つ引き渡しです。「カバーされていない」を明示的なアクションアイテムに変えたのであって、「テストはすべて通った」という一文の下に隠したものではないからです。
この一連の練習を一文に圧縮するなら、こうなります。インデックスは正しい拡張点を見つけられるかを決め、予算は全体像を見通せるかを決め、変更集合の導出は境界を正しく引けるかを決め、トランザクション化されたパッチとアンカーによるアサーションは失敗時に半成品を残さずに済むかを決め、そして検証体系は、自分が実際に何証明したのかを知っているかを決めます。五つの工程のどれか一つでも欠ければ、残りの四つの成果はすべて割引かれます。これはまさに、このチェーンを部分的な技法ではなく全体として設計しなければならない理由です。
10 よくある逆向きの判断
最後に、この清単のなかで最も誤った選択として現れやすいものをいくつかレビューし、まとめます。これらは実践で与える損失が技術的な欠陥に劣りません。
ベクトル検索でシンボル検索を置き換える。セマンティック検索は「支払いが失敗したときの再試行をどこで処理しているか」を探すのに向いていますが、「charge がどこで定義されているか」に答えることには向きません。正確な問いを symbol table に任せ、曖昧な問いを検索に任せる、これは分業であって置き換えではありません。逆にやると、リポジトリが大きくなるほど破綻します。
一度きりの全量インデックスだけを張る。タスクの開始時に一度だけインデックスを張る做法は、きれいに見えますが、セッションのなかでリポジトリの状態は変化します(Agent 自身がファイルを書き換え、ユーザーが新しい要件を出します)。インデックスと作業領域がずれると、Agent の影響範囲の分析は体系的に誤り、そしてその誤りは極めて気付きにくい。インデックスはどれか一つの作業領域の状態に結び付けて、変更のたびに差分更新しなければなりません。
検証をモデルの自己報告に委ねる。「私は変更が正しいと思う」には情報量がまったくありません。使える検証シグナルは三つだけです。実行可能な検査の終了コード。構造化された整合性検査の結果。そしてコードとベースラインの機械的に判定できる差異。モデルの自然言語での結論はシグナルではありません。
「最小」をプロンプトに書くだけにする。プロンプトのなかの「変更を最小限にしてください」が守られる確率は、ファイル数が増えるにつれて急速に下がります。信頼できるやり方は構造的な制約に変えることです。変更ファイルのホワイトリスト。diff の行数の上限。ノイズ行の自動復元。制約はツール層に、願いはプロンプト層に置くのです。
賢さを早急に最適化すること。インデックスと検証のループがまだ健全でないシステムで、予算をすべてより良いプロンプトと長い思考に使っても、得られるものは「変更が終わったら自動で検証できる」という土台を先に固めることに注力するより、遥かに小さい。土台が弱いうえに、その上に何をどれだけ高く建てても無駄です。
冪等性、再試行、トランザクションをモデルの自制に委ねる。これらはツールとランタイムの属性でなければなりません。モデルに「冪等性キーを入れて」と伝えることと「毎回入れる」こととのあいだには、大きな差があります。モデルが三つ目のファイルを変更しているとき、最初の二つのファイルの制約はとっくに薄まっているからです。同じ理屈で、ロールバック能力、トランザクション境界、アンカー検証も、すべてランタイムが提供すべき能力であり、プロンプトで置き換えられるものではありません。
テストのベースラインを任意の手順として扱う。パッチを適用する前に同じテストを走らせるコストは、検証時間の半分です。しかしそのベースラインは、その後のすべての失敗が回帰なのか既存の問題なのかを決めるものです。ベースラインを省くシステムは、既存の問題によってパッチを書き直し続けるか、赤いテストを無視することを徐々に覚えていきます。そして後者は取り消せません。一度シグナルを無視するようになったら、どれほど厳密な規則があっても、それを止める手段がありません。
並列化を早く導入すること。並列化は実時間を短縮できますが、失敗の帰属を難しくし、スタイルの差を増やし、インデックスの競合を複雑にします。順序としては、まず単一スレッドのチェーンを安定させるべきです。ひとつのエージェントが複雑な変更を十回連続で完遂でき、検証のループが機能していることを確認してから、分割を考えます。逆の順序のコストは高く、並列化は単一スレッドなら発見できたはずの、API の継ぎ目の隙間を覆い隠してしまいます。
平均値で受け入れる。このチェーンの質の分布には長い尾があります。ほとんどのタスクは順調に終わります。少数のタスクはまったく違う方向へ遠くまで走ってしまいます。平均値はそれらしく見えても、外れたタスクは本番のコードを汚染し、信頼を削っています。だから指標は最も悪い등급を見るべきです。「一度も質問を発火しないまま納品された変更の割合」「人の手による書き直しが必要だったパッチの割合」を数えます。後者はとくに重要で、変更集合の導出が正確だったかを判断する最もよい指標です。
この数項目を逆から考えると、このエンジニアリングチェーンの優先順位ははっきりしています。まずシステムに「見える」ようにする(インデックス)。次に「必要なものだけ」を動かすようにする(変更集合の制約)。「動かしたものを戻せる」ようにする(トランザクション化されたパッチ)。「結果を証明できる」ようにする(層状の検証)。「危険なものを入れない」ようにする(ゲートとサンドボックス)。この五つをやり終えれば、モデル能力が向上すれば結果は自動的に改善します。逆に、先にモデルを上げてからチェーンを補うなら、その効果の大部分は、見えない黑洞、変更しすぎる黑洞、検証できない黑洞という三つの穴のなかに散逸します。