この記事は、連載「動かさない技術」の第 1 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。
今回は、AI に手順を守らせる話です。手順というのは、成果物(出来上がるもの)を指定する代わりに、過程(進め方)を指定した指示のことで、「各ファイルは 1 本ずつ開いて確かめてください」がその形です。この種の指示には共通点があります。AI が守っても破っても、成果物が同じなので、指示を書いた人は、守られたかどうかを後から見分けることができません。見分けたいなら、過程は成果物に残す必要があります。
まず一つだけ、開いてみてください。
開いてほしいのは、指示ファイル(CLAUDE.md や AGENTS.md)の中で、進め方を指定している行です。たとえば「1 つずつ」「順番に」「必ず確認してから」と書いた行です。
AI がその行を守った回と守らなかった回を、成果物から見分けることができるでしょうか。
CC BY 4.0
見分けることができないなら、それは測っていないのと同じです
は長いあいだ、大事な指示は目立つ位置に、短く書けば守られると思っていました。公式のドキュメントにも「指示を足すほど個々の指示は薄まる」という趣旨の記述があります。はそれを、位置と長さの問題として読んでいました。大事な行を前に出し、太字にし、余計な行を削ればよい、という読み方です。
本番のプロダクトでは、この読み方が正しいかどうかを確かめることができません。指示が守られたかを見分けるには、答えが先に分かっている課題が要るからです。
そこでは、プロダクトとは別に、答えの分かる実験用の題材を作りました。
logs/ に、ログを模したファイルを 12 本置きます。サブエージェントに頼むのは、各ファイルから 1 つの値を拾って answers.tsv にまとめる、というだけの作業です。拾う対象は、行頭が RESULT: の行だけで、コメント行の中や引用の中にある同じ文字列は囮です。有効な行が 1 つも無いファイルも混ぜました。正解表は題材と同時にスクリプトが作り、サブエージェントに渡すディレクトリの外へ置きます(正解表を人が後から作ると、作る人がサブエージェントの答えに引きずられるからです)。
サブエージェントへの指示は、NOTES.md 1 枚に書きました。は、その中に進め方の 1 文を入れました。
ファイルは1 本ずつ開いて中身を確かめてください。
この NOTES.md を、会話を引き継がないサブエージェントに渡します。渡すのはこの 1 枚だけです。起動時のプロンプトには、はこの 1 文のことを書きません。プロンプトに書くと、NOTES.md の中の 1 文が守られるかどうかを、測ることができなくなります。
1 回目の実験は、「1 つのプロンプトに何行積むか」を条件にしました。サブエージェントは、377 行を積んでも 1 回のコマンドで読み切ります。指示の追従には、条件どうしで差が出ませんでした。1 回に読む行数は、指示が守られなくなる原因ではありませんでした。
2 回目は、題材の量を条件にしました(12 本 / 60 本。NOTES.md は両条件で 1 バイトも同じで、sha256 の頭 12 桁を照合しています)。8 回とも、課題の答えは満点で、3 か所に置いた「数を 1 つ書く」指示の追従も 24/24 でした。往復の数も、題材の量では決まりませんでした。中央値は 12 本の側が 13.5、60 本の側が 12.0 です。最短の 7 往復と最長の 67 往復は、どちらも 60 本の条件のサブエージェントでした。サブエージェントは grep やスクリプトで全ファイルをまとめて処理するので、題材が増えても往復の数は増えません。
題材の量による差は出ませんでしたが、その代わりに、破られた指示が 1 つだけ見つかりました。「1 本ずつ開いて確かめる」です。
| 指示 | 位置 | 守るのに要る手数 | 追従 |
|---|---|---|---|
| 数を 1 つ書く(3 か所) | 31 / 127 / 219 行目 | 1 回書くだけ | 24/24 |
| 1 本ずつ開いて確かめる | 27 行目 | 12 回 / 60 回のツールの呼び出し | 3/8 |
67 往復かかったセッションでも、31 行目の指示は守られています。守られなかったのは、位置や長さに関わらず、守るのに要る手数の多い指示のほうでした。ここでは、「手数の多い指示から守られなくなる」と結論しかけました。3 回目の実験はそれを確かめるためのもので、その結論は外れました。
AI に、関数 fetch_user の名前を load_user に変えさせているとします。同じ文字列は、コメントの中や、似た名前の別の関数 fetch_user_list の中にも出てきます。あなたは指示ファイルの隣り合う行に、「名前を変える前に、使っている箇所を 1 つずつ開いて確かめてください」と「直したファイルの一覧を edited.txt に書いてください」の 2 つを書きました。差分は数十ファイルに及ぶので、あなたは差分を全部は読まず、1 つずつ確かめてもらったことを頼りに受け取っていました。
ある日、あなたが作業の記録を開くと、AI は 1 つ目の指示を守っていませんでした。どの箇所も開かずに、1 回の sed で全ファイルの文字列をまとめて置き換えていたのです。置き換えてはいけない箇所まで置き換わっていないかは、差分を全部読まない限り分かりません。2 つ目の指示は毎回守られていて、edited.txt も毎回ありました。あなたなら、1 つ目の指示について次のどちらで対処しますか。
- 1 つ目の指示を先頭へ動かし、「必ず」を足して目立たせる
- 箇所ごとに、置き換えたか残したかを 1 行ずつ書くよう頼む
答えは、置き換えたか残したかを書くよう頼むほうです。記録を開くまで気づかなかったのは、1 つずつ開いて置き換えても、まとめて置き換えても、差分からはどちらのやり方で作ったかが分からないからです。まとめて処理するやり方は、指示に何も書かなくてもAIが選んだやり方で、の実験では、12 本のファイルから値を拾う作業で 8 回のうち 5 回で、サブエージェントは 1 本ずつ開かずにまとめて読み、その 5 回とも答えは満点でした。1 つ目の指示を先頭へ動かして「必ず」を足しても、差分の形は変わらないので、守った回と守らなかった回を、差分から見分けることはできません。隣の行の 2 つ目の指示は毎回守られていたので、位置で決まっていたのでもありません。違いは、2 つ目の指示は守れば edited.txt が残ることでした。箇所ごとに 1 行書き出させれば、守ったかどうかで成果物が変わるので、守られた回数を成果物から数えることができます。
理解したことは一文にできます。
過程を指定する指示は、成果物が同じなら守られない。守らせたければ、その過程が成果物に残る形にする。
なぜ、成果物が同じなら守られないのか
Claude Code のドキュメントが指示ファイルとメモリについて書いているのは、文脈であって、強制される設定ではないということです。同じページには、AI は指示を読んで従おうとするものの、厳密に守られる保証は無く、曖昧な指示や矛盾する指示ではとくにそうだ、と書かれています。NOTES.md に書いた進め方の 1 文も、追従率は最初から 1 より低いことになります。
冒頭で書いた「位置と長さ」の読み方にも、出どころはありました。Claude Code の best practices は、肥大した指示ファイルは、実際の指示を無視させると書き、多くの行を強調すれば、どれも目立たなくなるとも書いています。Claude Code のドキュメントは、1 ファイル 200 行未満を目安に挙げ、ファイルが長いほど追従率が下がるとしています。は、短く前に出せば守られる、と読みました。どちらのページも、書いているのは読む量のことです。何行でどれだけ追従率が落ちるかの表は、公開されていません。は、測った結果が公開されていない範囲にまで、その記述を当てはめて読んでいました。
成果物の側から見ると、指示が守られない理由を別の形で説明することができます。12 本のファイルを 1 本ずつ開いても、まとめて 1 回で読んでも、answers.tsv の中身は同じです。採点される成果物が同じなら、どちらのやり方でも課題の答えは満点になります。が NOTES.md に書いた進め方の 1 文は、成果物を定義していませんでした。
サブエージェントが手数の少ないやり方を選ぶことにも、出どころがあります。Claude Code の best practices が指示ファイルに書けと言っているのは、推測できない Bash コマンドのほうです。AI が既に知っている標準的な慣習は書くな、と続きます。1 回のコマンドでまとめて読むやり方は、が何も書いていないのに、2 回目の実験の 5 回で、サブエージェントが選んだやり方です。
過程を成果物に残させるやり方の出どころも、公式の指針です。Claude Code の best practices は、成功を主張させる代わりに、証拠を見せさせよと書き、スキルを書くときの公式の指針は、もう一歩進めて、検証できる形の途中の成果物を、先に出させることを勧めています。守った跡が残るものを成果物として要求しておけば、守られた回数を、成果物から数えることができます。
測り方: 同じ手数の指示を 2 本、隣り合わせに置く
3 回目の実験では、NOTES.md の「進め方」の節に、守るのに要る手数が同じ指示を 2 本、隣り合わせて置きました。
| 名前 | 位置 | 範囲 | 守った跡 | 中身 |
|---|---|---|---|---|
| P | 27 行目 | 12 件 | 残らない | 各ファイルは 1 本ずつ開いて確かめる |
| A | 28 行目 | 12 件 | 残る | 各ファイルについて checked/log_NN.txt にそのファイルの行数を書く |
位置は隣どうしで、範囲はどちらも 12 件、要る手数もどちらも 12 回です。違うのは、守った跡が残るかどうかだけです。渡した logs/ は、2 回目の実験と 1 バイトも同じです(sha256 で照合しています)。NOTES.md のほうは、A の 2 行を足して P を箇条書きにしたぶんだけ、2 行長くなっています。
がサブエージェントを 4 回起動して渡した結果です。
| サブエージェント | 課題の答え | 囮 | A(跡が残る) | P(跡が残らない) | 往復 | 秒 |
|---|---|---|---|---|---|---|
| 1 | 12/12 | 2/2 | 12/12(行数も一致) | 12/12 守った | 34 | 80.0 |
| 2 | 12/12 | 2/2 | 12/12(行数も一致) | 12/12 守った | 20 | 78.1 |
| 3 | 12/12 | 2/2 | 12/12(行数も一致) | 12/12 守った | 17 | 67.6 |
| 4 | 12/12 | 2/2 | 12/12(行数も一致) | 12/12 守った | 21 | 69.3 |
実験ごとに並べると、同じ 1 文(P)の追従はこうなります。
| 実験 | 置き方 | P の追従 |
|---|---|---|
| 2 回目 | P だけ(題材 12 本) | 2/4 |
| 2 回目 | P だけ(題材 60 本) | 1/4 |
| 3 回目 | P の隣に A を置いた | 4/4 |
P の文面と位置は、そのままです。P の隣に、同じ範囲の成果物を要求する指示(A)を 1 行足しただけで、P の追従は 3/8 から 4/4 になりました。
P を守ったかどうかは、採点器からは見えません(見えないことが、論点そのものです)。P を数えるのに、はサブエージェントの記録を使います。1 回のツールの呼び出しが log_NN.txt をちょうど 1 本だけ指定していたら、1 本ずつ開いたとして数えます。grep や for でまとめて処理した呼び出しは、指定するファイルが 0 本か複数本になるので、数に入りません。
# サブエージェントの記録から「1 本ずつ開いたか」を数える(手元の実装からの簡約版)
python3 - "$1" <<'PY'
import json, re, sys
single = 0
for line in open(sys.argv[1]):
hit = re.findall(r"log_\d+\.txt", line)
if len(set(hit)) == 1:
single += 1
print(f"one-at-a-time reads: {single}")
PYCC BY 4.0 はここまで
書き直さなくなったもの
書き直さなくなったのは、守られない指示です。以前のは、AI が守らなかった行を見つけると、その行を書き直していました。行を前に動かし、太字にし、「必ず」を足し、周りの行を削るやり方です。
いまのは、守られない行を書き直す代わりに、その指示を守った跡が残る成果物を要求する行を、隣に 1 行足すことができないかを考えます。足すことができたら、書き方の工夫は要りません。足すことができなかったら、その指示は守られないものとして、指示ファイルの残りを組みます。
CC BY 4.0
注意: P の追従が上がった差は、偶然と区別することができません
この実験から言うことができるのは、「A を置いた 4 回は全部守った」までです。3/8 から 4/4 への差そのものは、母数が 4 では、偶然と区別することができません。A を置くと P が守られた理由も、この設計では分かりません。4 回のうち 3 回は、A の 12 本を 1 回のコマンドでまとめて書いていて、A を満たすのに 1 本ずつ開く必要はありませんでした。
この実験では言うことができない点が、もう一つあります。課題の答えは 3 回とも全員満点でした(3 回の実験を合わせて 16 回)。囮を 5 種類入れても、全員が満点のままでした。課題が易しすぎて全員が満点になる実験では、追従に差が出ても出なくても、そこから読み取ることのできる範囲が狭くなります。
成果物が同じなら守られない、という向きは、実測の差の代わりに、サブエージェントがしたことから言うことができます。2 回目の実験で、1 本ずつ開く代わりに手数の少ないやり方を選んだ 5 回は、課題の答えがどれも満点でした。この 5 回でサブエージェントがしたのは、同じ成果物を、より手数の少ないやり方で作ることでした。
実験手順: 同じ 1 文を、跡が残る指示と残らない指示で比べる
前提
会話を引き継がないサブエージェントを 4 回以上、同時に起動する手段が要ります(順に起動すると、起動した時間帯の混み具合の違いが、条件の差に見えてしまいます)。サブエージェントに渡すディレクトリは、条件ごとに分けて用意します。サブエージェントの記録は、後から読む形で手元に残します。記録に要るのは、ツールを呼んだ 1 回ごとの「呼んだツールの名前」と「その対象」です。対象は、シェルを走らせた回なら打ったコマンド行(cat logs/log_01.txt)、ファイルを読むツールなら読んだファイル名(logs/log_01.txt)です。ツールの名前しか残らない記録では、P を測ることができません。
所要時間: 20 分(題材の生成器と採点器を自分で書く場合。すでにあるなら 5 分)
手順
- 答えの分かる題材をスクリプトで作ります。12 本のファイルと、その正解表を同時に出力してください。正解表は、サブエージェントに渡すディレクトリの外へ置きます
- 指示ファイルを 1 枚作ります。課題の説明と、離れた 3 か所に「数を 1 つ書く」指示を置きます(陽性対照、つまり、守られるはずのものが守られていることを確かめるための指示です。これが守られなかったら、問題は作業そのものにあります)
- 同じ指示ファイルから、条件を 2 つ作ります。片方は、進め方の節に P(跡が残らない指示)だけを置きます。もう片方は、P の隣に A(同じ範囲・同じ手数で、跡が残る指示)を置きます。それ以外は 1 バイトも変えず、ハッシュを印字して照合してください
- 両条件を同時に起動します。起動時のプロンプトに P と A のことを書かないでください
- 課題の答えを採点します。採点で確かめることができるのは、A までです。P を守ったかどうかは、採点の結果には出ません
- サブエージェントの記録から P を数えます。1 回のツールの呼び出しが対象ファイルをちょうど 1 本だけ指定したものを数えてください
合格条件(すべて満たすこと)
合格の条件は 4 つです。1 つ目は、手順 2 の 3 か所の指示を、両条件とも全員が守っていることです。これで、条件の差に見える作業の失敗を除くことができます。2 つ目は、課題の答えが両条件とも満点であることです。満点なら、難度の差は追従の差に見えません。3 つ目は、P だけを渡した条件で、P を守らない回が 1 回以上出ることです。4 つ目は、P と A を並べた条件で、A が全件満たされることです。
合格しなかったとき
- P だけを渡した条件でも全員が守った —— 守るのに要る手数が少なすぎます。題材の本数を増やして、1 本ずつ開く手数を増やしてください
- A が全件満たされない —— A の成果物が課題の答えと重なっている疑いがあります。答えを作る過程で自然に出てしまう値を、A に選ばないでください
- 両条件で P の追従が変わらない —— P と A の範囲がずれています。範囲(何件ぶんか)と位置を、先にそろえ直してください
- 課題の答えが満点でない —— 難度が高すぎます。この状態では、追従の差と難度の差が、同じ数字に混ざります
後始末
生成した題材、サブエージェントの記録、正解表を削除します。正解表を残すと、次に題材を作り直したつもりで、古い正解表のまま採点することになります。
守った跡が残らない指示は、守られたことにも、破られたことにもなりません。この実験で最後まで残った問いは、追従率の高さより、守ったかどうかを成果物から見分ける手段があるかどうかでした。
CC BY 4.0 はここまで
次回は「ラッパーを使わせない技術」。は、自分たちで作ったコマンドを「必ず使ってください」と書くのをやめました。 それでもAIは、自分からそのコマンドを使います。