AIレビューで道具を使わせる指示——読む・探す・実行で取れる指摘が違う
AIレビューで道具を使わせる指示——読む・探す・実行で取れる指摘が違う
リポジトリ全体に触れる環境でレビューさせるとき、道具の許可は「必要なら使ってよい」の1行で済ませることになる。10ファイルの題材で3段を別々に実行して数えたところ、読む・探す・実行するは強さの順に並んでいなかった。差分を読めば分かる欠陥は、テストを実行しても出てこない。実行で出た欠陥は、1回では半分しか出てこない。段ごとに取れる材料が違うので、許可は段ごとに書く。
この記事の提案を使うのは、モデルが自分でファイルを開いたり検索したりできる環境でレビューさせるときである。
題材にした10ファイル
在庫と明細金額を扱う Python のパッケージを作った。10ファイル・120行で、テストは python3 -m unittest discover -p 'test_*.py' で走る。README.md に「このリポジトリの約束」を3条置き、判定基準の出所にした。
16 README.md 約束3条
3 run_tests.sh
0 inventory/__init__.py
9 inventory/models.py 商品の定義
9 inventory/pricing.py 明細金額の計算
18 inventory/store.py 在庫の出庫
13 inventory/report.py 注文票の文字列化
18 test_pricing.py
20 test_store.py
14 test_report.py
120 合計 / 10 ファイルレビューに渡す差分は6ファイル・57行で、そのうち変更された行は16行(追加12・削除4)である。差分は2つの変更を含む。割引率の単位を百分率の整数から比率の小数へ変え(約束2の書き換えを含む)、Store に reserve を足したものである。
仕込んだ欠陥は2件で、材料のありかが対照になるように置いた。
- 欠陥1: 足した
reserveが、残数を確認せずに在庫を減らす。同じクラスのshipには確認があり、約束1(在庫を減らす操作は、減らす前に残数を確認する)に反する。根拠は差分と、差分に出たファイルの全文で足りる - 欠陥2: 割引率の単位を変えたのに、差分に入っていない
inventory/report.pyが旧単位のまま%を付けて表示し、test_report.pyのテストデータも旧単位のままである。根拠は差分の外にある
同じものを作るなら、10ファイル以内のパッケージに約束を3条書き、差分の中で判定できる欠陥と、差分の外のファイルを読まないと判定できない欠陥を1件ずつ入れればよい。以下の数値はこの題材に対するもので、Python 3.12.3、2026-10-12 に実行した。モデルは走らせていない。測ったのは段ごとに手に入る材料である。
段1 差分と、差分に出たファイルを読む
差分を読むと、reserve の本体が1行で出てくる。
@@ -13,3 +13,6 @@
if qty > self.available(sku):
raise OutOfStock(sku)
self._stock[sku] = self.available(sku) - qty
+
+ def reserve(self, sku: str, qty: int) -> None:
+ self._stock[sku] = self.available(sku) - qty
文脈行に if qty > self.available(sku) が入っているので、確認を伴う経路が同じ画面にある。新しい reserve にその確認が無いことは、この6行を並べて読めば判定できる。読む量は差分の57行で、うち変更行は16行である。
文脈行の範囲には限りがある。def ship(self, ...) の行は差分に入っていないので、上の確認がどのメソッドの中にあるかは差分だけでは決まらない。欠陥1を「ship には確認があるのに reserve には無い」という形で書くには、inventory/store.py の全文を開く必要がある。
欠陥2のほうは、差分に bulk_rate: int から bulk_rate: float への変更と、約束2の書き換えが出てくるだけである。旧単位のまま残っているファイルは差分に入っていないので、差分だけからは「追随していない箇所があるかどうか」が言えない。
段2 識別子で検索する
差分に現れる4文字以上の識別子を取ると18語になる。そのうち bulk_rate の1語でリポジトリ全体を検索すると6件出て、2件が差分の外にある。
README.md:15 2. `Item.bulk_rate` は比率の小数で持つ(0.2 は 20% を指す)
inventory/models.py:9 bulk_rate: float
inventory/pricing.py:8 return int(item.unit_price * qty * (1 - item.bulk_rate))
外 inventory/report.py:13 return f"{item.bulk_threshold}個以上で{item.bulk_rate}%引き"
test_pricing.py:6 WIDGET = Item(..., bulk_rate=0.2)
外 test_report.py:6 CATALOG = {"widget": Item(..., bulk_rate=20)}欠陥2の2箇所が、1コマンドで両方出た。 表示の側(% を付けたまま)とテストデータの側(旧単位の 20)が同じ一覧に並ぶので、片方だけを直すと壊れる関係もその場で見える。
段2で欠陥1に届くかどうかは別である。reserve で検索すると3件出るが、確認の有無は検索結果の行だけでは分からない。available で検索すると6件出て、そのうち store.py:13 の if qty > self.available(sku) が確認にあたる。検索の件数からは「確認がある箇所が1つある」までしか言えず、それが ship のものか reserve のものかは、ファイルを開いて前後を読む段に戻る。
段3 テストを実行する
変更後の題材でテストを走らせると、8件のうち1件が失敗する。
FAIL: test_order_lines (test_report.ReportTest.test_order_lines)
AssertionError: Lists differ: ['widget x10 = -19000'] != ['widget x10 = 800']
Ran 8 tests in 0.001s
FAILED (failures=1)変更前は7件すべて成功する。失敗した1件は欠陥2のテストデータ側を指している。
ここで2つのことが同時に起きている。1つ目は、欠陥1がテストに出ないことである。差分は reserve のテストを1件足していて、在庫5から2を予約して残数3を確かめる。残数より多い数を予約する場合を試していないので、確認が無いことはテストの成功に影響しない。実行の段で欠陥1に届かせるには、まだ無いテストを書く段がもう1つ必要になる。
2つ目は、実行が1回では欠陥2の全体に届かないことである。失敗した1件を素直に直して走らせ直すと、別の1件が失敗する。
1回目(そのまま) : 8 件実行 / FAILED (failures=1) / 失敗: test_order_lines
2回目(テストデータを直す): 8 件実行 / FAILED (failures=1) / 失敗: test_discount_note
AssertionError: '10個以上で0.2%引き' != '10個以上で20%引き'欠陥2の2箇所は互いを隠していた。テストデータが旧単位のままだったので、表示側の % のテストは通っていた。段2の検索は2箇所を1回で並べたが、段3の実行は1回あたり1箇所しか指さない。
段ごとに届いた欠陥
| 段 | 操作 | 欠陥1(読めば分かる) | 欠陥2(差分の外にある) |
|---|---|---|---|
| 1 読む | 差分57行と、差分に出たファイルの全文 | 届く | 届かない |
| 2 探す | bulk_rate で1回検索 |
届かない | 2箇所とも届く |
| 3 実行する | 既にあるテストを走らせる | 届かない | 1回目は1箇所だけ |
段を上げると前の段の結果が含まれる、という形になっていない。許可を1段だけ書くと、その段で届かない欠陥は出力に現れないまま終わる。公式ドキュメントにも、どの道具を呼ぶかの判定が指示文の側で動かせると書かれている(Tool use with Claude、https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview、取得日 2026-10-11)。
Claude determines on each turn whether to call a tool or respond directly. … This boundary is steerable through your system prompt.
3段の許可の宣言
段ごとに許す操作を並べて、段ごとに何を出力に書くかを決める。山括弧は各自の環境に置き換える。
## 道具の許可
次の3段を、この順に許す。段を飛ばさない。各段で何をしたかを出力の末尾に書く。
1. 読む: 渡した差分と、差分に含まれるファイルの全文を開いてよい。
書くこと: 開いたファイルのパス
2. 探す: リポジトリ全体に対する文字列検索を許す。検索は差分に現れる識別子に限る。
書くこと: 検索した語と、一致した件数
3. 実行する: `<テストを走らせるコマンド>` だけを許す。他のコマンドは実行しない。
書くこと: 実行したコマンドと、終了コード
段3で失敗が出た場合、その失敗を直す変更は提案してよいが、
直した状態での再実行はしない(1回の失敗が別の失敗を隠している場合があるため、
隠れている側は段2の検索結果から挙げる)。
## 段ごとの扱い
- 差分の中だけで判定できる指摘は、段1で出す。段2と段3の結果を待たない
- 差分の外のファイルを根拠にする指摘は、段2の検索語と件数を添える
- 実行の結果を根拠にする指摘は、段3のコマンドと終了コードを添える段3を許さない場合は、3番の段落ごと削って「実行はしない」と1行書く。道具が1つも無い環境では、段1は差分を貼る、段2は検索結果を人が貼る、段3はテストの出力を人が貼る、という形に落とせる。段の宣言は、道具があるかどうかとは別に材料の取り方を決める。 段が指しているのは材料の取り方であって、取る主体ではない。
段を分けると何を使うか
払うのは人の確認時間である。段ごとの報告を出力に足すので、1回のレビューに対して読む項目が3つ増える。段1の「開いたファイルのパス」は差分のファイル数ぶん(この題材では6行)、段2の「検索した語と件数」は検索語の数ぶん、段3は1行である。採否を判定する前に、この3項目が実際の操作と合っているかを人が見る。検索語と件数は手元で同じコマンドを打てば数秒で突き合わせられるが、突き合わせを省くなら段2の欄を足す意味は無い。
段を分けることで実行回数が増える場合もある。この題材では、段3の失敗を直して走らせ直す経路を禁じたので、実行は1回で止まる。禁じないと、隠れていた欠陥に届くまで実行と修正が繰り返され、1回のレビューの所要時間が読めなくなる。
この宣言を落としていい条件は、差分の外に根拠が無いときである。 1ファイルだけの差分、設定値を1行変えただけの差分、翻訳ファイルの更新では、段2で検索しても差分の外に一致は出ない。渡す前に、差分に現れる識別子を1つか2つ選んで検索してみて、差分の外に一致が0件なら段1だけを許せばよい。段を3つ書くこと自体が、材料が差分の外にあるという見込みの表明である。
指示文の点検
- 道具の許可を「使ってよい」ではなく、段ごとに書いたか
- 段ごとに、許す操作を具体的な名前(コマンド、検索の対象範囲)で書いたか
- 段ごとに、何を出力に書くかを決めたか
- 実行を許す段で、実行してよいコマンドを名指ししたか
- 差分の中だけで判定できる指摘を、探索の結果を待たずに出させる形になっているか
- 検索を許す段で、検索語を差分に現れる識別子に限ったか
- 1回の失敗を直して再実行する経路を、許すか禁じるかを決めたか
- 段3を許さない場合に、「実行はしない」と書いたか
- 渡す前に、差分の識別子を検索して差分の外に一致があるかを見たか
- 段ごとの報告を人が突き合わせる時間を見積もったか
おわりに
10ファイル・120行の題材に欠陥を2件仕込んで3段を別々に実行すると、差分を読んで判定できる欠陥はテストの実行では出ず、差分の外に根拠がある欠陥は1回の検索で2箇所そろうのに対して1回の実行では1箇所しか指さなかった。3段は強さの順ではなく、取れる材料の種類が違う。
欠陥が互いを隠す形は実行の段でだけ起きた。テストデータが旧単位のままだったので表示側のテストが通っていて、片方を直すともう片方が失敗した。検索はこの関係を1回で並べる。
次に取る行動を1つ挙げる。手元の差分から識別子を1つ選び、リポジトリ全体を検索して、一致が差分の外に何件あるかを数える。0件なら段1だけを許す指示文で足り、1件以上あるなら段2の検索語と件数の欄を足す理由がその場で立つ。