この記事は、連載「動かさない技術」の第 2 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。

今回は、AI に自分たちのラッパーを使わせる話です。ラッパーというのは、標準のコマンドの代わりに置いたプロジェクト固有の入口のことで、rails test の代わりに置いた scripts/test.sh がその形です。置く理由はいくつもあります。並走を止めたい、環境変数を揃えたい、記録を残したい。そして置いたあと、たいていの人が指示ファイルに 1 行書きます。「テストは scripts/test.sh で走らせてください」。その 1 行は、AI に素のツールに戻る用事ができた瞬間に負けます。

私は 1 作目「読まない技術」の第 1 回で、その 1 行についてこう書きました。

「ラッパーを使うこと」と指示ファイルに書いても、テストを走らせようとした瞬間に出てくるのは、何億回と見てきた標準コマンドの側です。指示ファイルの 1 行は、AI の中で染み付いた習慣と優先順位を競うことになり、たいてい負けます。

この「たいてい」を、私は数えていませんでした。公式のドキュメントは逆の側に立っています。指示ファイルに書くものの例として挙がっているのは「AI が推測できない Bash コマンド」で、書けば埋まる前提で線が引かれています。

仮説が公式にあり、反証が自分の前作にある。今回はそれを測りに行きます。

まず一つだけ、探してみてください。

指示ファイルの中で、標準のコマンドの代わりに自分たちの入口を使え、と書いている行です。

「テストは scripts/test.sh で」「デプロイは bin/deploy で」。

見つかったら、その行が実際に何回守られたかを思い出してみてください。

思い出せないはずです。どちらで走らせても、出来上がるものは同じだからです。

CC BY 4.0

「置いてある」と「呼ばれる」は別の話でした

答えの分かる題材を作りました。logs/ にログを模したファイルを 12 本置き、各ファイルから 1 つの値を拾って answers.tsv にまとめる、というだけの作業です。行頭が RESULT: のものだけが対象で、コメント行の中や引用の中の同じ文字列は囮、有効な行が 1 つも無いファイルもあります。

そこに、このプロジェクトのラッパーを 1 本置きます。

#!/bin/sh
# このプロジェクトで logs/ の中を見るときの入口。
# 出力は <行番号>:<行> の形。
exec grep -n 'RESULT:' "$@"

中身は grep -n そのもので、出力も 1 バイト違いません。これは意図的です。ラッパーのほうが賢いと、「指示に従ったか」と「課題ができたか」が同じ数字に化けます。使っても得をしないラッパーにしておくと、残るのは「どちらの言葉を選んだか」だけになります。

bin/README.txt にも 1 行の説明を置きました。ラッパーは 3 条件とも同じ場所にあり、同じ説明が付いています。変えたのは指示ファイルの「進め方」の節だけです。

条件「進め方」に書いたもの
書かない⚠️ 節そのものを置かない(ラッパーは置いてあり、説明も読める)
1 行だけ「ログの中身を見るときは、./bin/scan <ファイル> を使ってください」
強く書く⭐ 前 4 作が「効く」と書いた書き方を全部使った指示(後述)

12 体に渡しました。課題の答えは 12 体とも 12/12、囮も 2/2、離れた 3 か所に置いた陽性対照も 12/12 です。差が出たのは、そこではありませんでした。

条件ラッパーを 1 度でも呼んだ最後まで素のコマンドに戻らなかった
書かない0/40/4
1 行だけ4/41/4
強く書く4/43/4

書かなかった 4 体は、bin/ を一度も開いていません。ls の結果に出ていて、説明ファイルも読める場所にあって、それでも 0 回です。ラッパーが「ある」ことと、ラッパーが「呼ばれる」ことのあいだには、何もつながりがありませんでした。

そして 1 行書くと、4 体とも呼びます。私が前作で書いた「1 行は標準コマンドに負ける」は、ここでは外れています。書いた 2 条件の 8 体は、例外なく、まず cat bin/scan でラッパーの中身を読んでから使いました。知らない言葉は、無視されるのではなく、確かめられていました。

負けるのはその後です。1 行だけの条件では、4 体のうち 3 体が途中で素のコマンドに戻りました。

理解したことは一文にできます。

ラッパーは、指示ファイルに書けば呼ばれる。呼ばれ続けるかを決めるのは指示の強さではなく、素のツールに戻る用事が残っているかどうかだ。

測り方: 同じラッパーを、指示の強さだけ変えて 3 通り置く

条件は 1 つ、指示の強さだけです。「強く書く」の指示には、前 4 作が「効く」と書いた書き方を全部入れました。

使った技術出典入れたもの
してほしい形で書く3 作目 第 8 回「素の検索コマンドを打つな」ではなく「./bin/scan を使ってください」
理由を書く2 作目 第 2 回「打つ人によって書き方が変わるので、同じ範囲を見たかを後から突き合わせられません」
範囲を割る2 作目 第 3 回「logs/ の下を見るときです。NOTES.md や answers.tsv には要りません」
肯定的な実例を出す公式の書式の指針./bin/scan logs/log_01.txt を 2 行

理由には、この題材の中で本当のことだけを書きました。「ラッパーのほうが正確です」のような利点を書くと、追従の理由が「指示に従った」ではなく「そのほうが正しいと判断した」に化けます。

揃えたものはスクリプトで確かめています。課題の仕様、測定対象 3 点の文面、指示ファイルの総行数(3 条件とも 216 行)、bin/ の中身、logs/ の中身。測定対象のうち 2 つは行番号まで同じにしました(削るのは前半の埋め草だけなので、ずれるのは先頭の 1 点だけです)。長さと位置は前回の実験で条件として否定済みなので、その 1 点のずれは差の説明になりません。

ラッパーを呼んだかどうかは、成果物に残りません。answers.tsv は、どちらの言葉で読んでも同じものが出ます。数えるのは作業者の記録のほうです。

# 作業者の記録から「ラッパーを通したか / 素のツールで見たか」を数える(手元の実装からの簡約版)
python3 - "$1" <<'PY'
import json, re, sys
SCAN = re.compile(r"(^|[;|&\n]|\bdo\s)(\s*(?:\./)?bin/scan\b[^;|&\n]*)")
RAW  = re.compile(r"(?<![\w./-])(grep|cat|head|tail|sed|awk)(?![\w-])")
scan = raw = 0
for line in open(sys.argv[1]):
    if "logs" not in line and "log_" not in line:
        continue
    if SCAN.search(line):
        scan += 1
    # ⚠️ ラッパーの呼び出しを消してから素のツールを探す(同じ 1 行に両方が入る)
    rest = SCAN.sub(r"\1 ", line)
    if any(RAW.search(seg) and ("logs" in seg or "$" in seg) for seg in re.split(r"[;|\n]+|&&", rest)):
        raw += 1
print(f"wrapper {scan} / raw tools {raw}")
PY

この「消してから探す」の 1 行が無いと、数は全部ずれます。私は最初これを書かず、ls logs && cat bin/scan(ラッパーの中身を読んだだけ)を「素のコマンドで logs を見た」に数えていました。1 回の呼び出しに複数のコマンドが入る形は、まとめて見ると必ず化けます。

CC BY 4.0 はここまで

書き直さなくなったもの

守られなかったラッパーの、その 1 行です。以前は、素のコマンドが打たれているのを見つけると 1 行のほうを直していました。位置を前に出す。理由を足す。禁止を太字にする。今回の実測では、そこまで全部やった書き方でも 3/4 でした。1 行だけの書き方の 1/4 と比べれば上がっていますが、母数 4 では偶然と区別できませんし、何より 4/4 にはなっていません。

いまは 1 行を直す代わりに、素のツールに戻る用事が残っていないかを見ます。戻った 3 体が何をしたかは、記録に出ていました。

条件戻る直前まで戻ったあとに打ったもの
1 行だけ./bin/scan で 12 本を通したcat -A / sed -n 'l'(行頭の空白を見るため)
強く書く./bin/scan で 12 本を通したsed -n '2p' | cat -A(囮の 1 行を確かめるため)

ラッパーは RESULT: を含む行を出しますが、行頭が RESULT: かどうかまでは見せません。囮を仕分けるにはその情報が要ります。足りないことに気づいた瞬間に素のツールへ戻り、戻ったきり帰ってきませんでした。

1 作目 第 1 回で私が出した答えは「標準コマンドの側を塞ぐ」でした。それは今も正しいと思っています。

ただし、塞ぐ前に見るところが 1 つ増えました。塞ぐと、戻る用事のある仕事が行き場を失います。ラッパーが仕事の一部しかできないまま入口を塞ぐと、AI は前回の記事と同じことをします。同じ成果物を、通れる別のやり方で作るだけです。

CC BY 4.0

注意: これは「1 行が効いた」の証明ではありません

読み方が 2 つ残ります。素に戻った理由は、ラッパーが仕事の一部しかできなかったことでした。

つまり「指示より事前分布が勝った」のか「ラッパーが足りなかったから合理的に別のツールを使った」のかを、この設計は分けていません。分けるには、囮の判定までラッパーにやらせた条件が要ります。

ただしその設計は課題を易しくするので、追従と正答が同じ数字に化けます。どちらかを捨てるしかなく、今回は「使っても得をしない」を優先しました。

課題の答えは 12 体とも満点です。3 回目まで続いていた天井が、4 回目でも破れていません。難度が張り付いている実験は、差が出ても出なくても読み方が狭くなります。

母数は条件あたり 4 です。0/4 と 4/4 の差は大きく見えますが、1/4 と 3/4 の差は偶然と区別できません。この記事で強く言えるのは、前者 —— 書かなければ 1 度も呼ばれない、のほうだけです。

そして、この実験は「どちらが良いか」を測っていません。素のコマンドで読んだ体も、ラッパーで読んだ体も、答えは全員満点でした。ラッパーを使わせたいのは、答えのためではなく、答え以外の何か(記録・並走の防止・環境の統一)のためのはずです。その「何か」が成果物に残らないなら、それは前回の話に戻ります。

実験手順: 同じラッパーを、指示の強さだけ変えて渡す

前提

  • 会話を引き継がない作業者を 12 体、条件を混ぜた波で同時に起動できること(逐次にすると、その日の混み具合が条件差に化けます)
  • 標準コマンドと出力が同じラッパーを 1 本用意できること
  • 作業者の記録を後から読めること。打ったコマンド行がそのまま残っている必要があります —— ./bin/scan logs/log_01.txt と grep -n 'RESULT:' logs/log_01.txt を見分けるためです。ファイルを読むツールを使った回は、読んだファイル名まで残っていること

所要時間: 25 分(題材の生成器と分類器を自分で書く場合。すでにあるなら 5 分)

手順

  1. 答えの分かる題材をスクリプトで作ります。12 本のファイルと、その正解表を同時に出力してください。正解表は、作業者に渡すディレクトリの外へ置きます
  2. ラッパーを 1 本置きます。標準コマンドと出力が 1 バイトも違わないものにしてください。賢いラッパーにすると、追従と正答が同じ数字になります
  3. 説明ファイルを、ラッパーの隣に置きます。3 条件とも同じものを置く(無いと「指示の効果」ではなく「存在を知らなかった」を測ることになります)
  4. 同じ指示ファイルから、条件を 3 つ作ります。書かない / 1 行だけ / 前作の技術を全部使う。総行数を揃える(埋め草で調整し、ハッシュではなく行数を印字して照合してください)
  5. 3 条件を混ぜた波で起動します。起動時のプロンプトにラッパーのことを書かないでください
  6. 課題の答えを採点します。ここにラッパーの話は出ません。出ないことが論点です
  7. 作業者の記録から 2 つ数えます。ラッパーを通した呼び出しと、素のツールで対象を見た呼び出し。ラッパーの呼び出しを消してから素のツールを探す(同じ 1 回に両方が入ります)

合格条件(すべて満たすこと)

  • 課題の答えが、3 条件とも満点(= 難度の差が追従の差に化けていない)
  • 陽性対照(指示ファイルの離れた 3 か所に置いた、手数 1 回の指示)が 3 条件とも全員正しい
  • 「書かない」条件で、ラッパーの呼び出しが 0 回(= ラッパーの存在だけでは呼ばれない、が取れている)
  • 「1 行だけ」条件で、素のコマンドに戻る作業者が 1 体以上出る

合格しなかったとき

  • 「書かない」条件でもラッパーが呼ばれた —— 説明ファイルが目立ちすぎています。ラッパーの名前を、課題の語から離してください
  • どの条件でもラッパーが呼ばれない —— ラッパーが標準コマンドより不便です。引数の形を標準コマンドに揃えてください
  • 3 条件とも全員がラッパーだけで通した —— 素のツールに戻る用事がありません。ラッパーが見せない情報を、課題の判定に 1 つ入れてください
  • 課題の答えが満点でない —— 難度が高すぎます。この状態では、追従の差と難度の差が同じ数字に化けます

後始末

  • 生成した題材・作業者の記録・正解表を削除します。正解表も一緒に削除する —— 残すと、次に題材を作り直したつもりのまま、古い答えを採点することになります

ラッパーを置くだけでは呼ばれず、1 行書けば呼ばれ、強く書いても通し切れない。この 3 つのうち、指示ファイルで動かせたのは最初の 1 つだけでした。


CC BY 4.0 はここまで

次回は「集めない技術」。私は、AI に毎回同じ答えを言わせるのをやめました。それでも、結論はいつも同じところに落ち着きます。