この記事は、連載「言わない技術」の第 1 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。
今回は、指示ファイル(CLAUDE.md や AGENTS.md)に書いた「必ず実行してください」という行の話です。5 か月前にが足したその行は、いまも指示ファイルに残っています。同じ種類の依頼をプロンプトに 1 文足しただけで、AIの手数(ツール呼び出しの回数)は、足さなかったときの倍でした。
は 1 作目「読まない技術」の第 6 回で、ルールを、AIが破ったら落ちる自動テストに置き換える、と書きました。今回は、その自動テストやhook(コミットのときなどに自動で走るスクリプト)を置いたあとに、指示ファイルに残った行の話です。
まず一つだけ、探してみてください。
指示ファイルを使っているなら、開いて「必ず実行」「最後に確認」「二重チェック」を検索してください。見つかった行のうち、その行を消したら本当に実行されなくなるものは、何行でしょう。
CC BY 4.0
検証を指示したら、手数が倍になり、正答は同じでした
は、検証を指示すると何が変わるかを測りました。2 体のAI(A と B)に同じ課題を渡し、A のプロンプトにだけ、次の 1 文を足しました。
最後に必ず検証の段階を設けてください。抜き出した 10 件を全件、原文と突き合わせて二重チェックし、確認できたことを報告してください。
課題は、ある連載記事 10 本から、決まった形の文を 1 つずつ抜き出して表にすることです。罠を一つ仕込んであります。10 本目にだけは、その形の文がありません。AIが、無い文を作り上げて表に入れたかどうかが、ここで分かります。正解表は、が先に機械的に作っておきました。2 体とも同じモデル(Claude Opus 5)を使い、ほかの条件もそろえました。
| A(検証を指示した) | B(指示なし) | |
|---|---|---|
| 正答(正解表と完全一致) | 10/10 | 10/10 |
| ツール呼び出し | 8 回 | 4 回 |
| 所要時間 | 93 秒 | 43 秒 |
正答は、A と B で同じでした。どちらも、10 件が原文と 1 文字まで一致しました。罠を仕込んだ 10 本目について、どちらも「該当なし」と報告しています。
違ったのは、手数と時間です。検証を指示した A は、B と比べてツール呼び出しが倍、時間が 2.2 倍でした。
検証を指示しなかったほうのAIが、自分で検証していました
が驚いたのは、検証を指示していない B の報告です。書き出しは、こうでした(原文は英語)。
Sanity check: 目印の語をディレクトリ全体で
grepした。9 ファイルにちょうど 1 回ずつ、10 本目には 0 回。
は B に、検証を指示していません。検証の指示があったのは A だけですが、検証そのものは A も B もしていました。
Anthropic のプロンプト指針は、この挙動を Claude Opus 5 のページに書いています。そのページによると、このモデルは頼まれなくても自分の誤りを見つけて直すので、モデルが既にしている確かめ直しを指示するのは避けるように、とあります。指示しても、モデル自身の挙動と重なってコストだけ増え、結果は良くならない、というのが理由です。手元の測定で起きたのも、このとおりのことでした。
あなたが指示ファイルを見直していたら、次の節がありました(短くしたもの)。
config/locales/*.ymlを編集した場合、コミット前に必ず実行:python3 scripts/validate_locales.pyYAML 構文エラーがあると Puma が起動不能になる(本番障害に直結)
このコマンドは、コミットのたびにhookも実行していて、チェックで落ちたコミットはhookが止めます。この節を、あなたなら次のどちらで直しますか。
- 節ごと消す
- 実行を頼む 1 行目だけを消し、2 行目の理由は残す
答えは、1 行目だけ消すほうです。1 行目の依頼は、hookが既にやっていることの上書きです。消しても、チェックはコミットのたびに走り、落ちたコミットは止まります。あなたが節ごと消すと、一緒に消えるのは 2 行目の理由です。hookも、チェックが落ちたときには理由を出すことができます。ただ、それが届くのは、チェックが落ちたコミットのときだけです。指示ファイルの理由の行は、セッションの始めに読み込まれます。1 行目だけ消せば、消えるのは上書きだけで、編集より前に届く理由は残ったままです。
理解したことは一文にできます。
実行の依頼は、モデルか仕組みのどちらかが既にやっていることの上書きになる。だから指示に残すのは、依頼ではなく理由だけにする。
モデルのほかに、仕組みが既にやっていることもあります
の手元には、typingtube という本番プロジェクトがあります。YouTube の音楽動画でタイピング練習ができる Web サービスです。その指示ファイルは 102 行で、「コミット前に必ず実行」と書いてある節が 2 つあります。翻訳ファイルの構文チェックの節と、デザインのラチェット(色を直に書いた箇所などの件数が、前より増えていないかを確かめるスクリプト)の節です。2 つの節が実行を頼んでいるコマンドは、どちらもpre-commit hookが実行しています。チェックで落ちるコミットは、AIに実行を頼んでも頼まなくても、hookが止めます。hookや自動テストのように、AIが読まなくても動くものが、この記事で言う仕組みです。
gitの履歴をたどると、が指示ファイルに「コミット前に必ず実行」を書き足したのは 2 回だけで、どちらも、そのチェックのスクリプトを足したのと同じコミットでした。hookがそのスクリプトを実行するようになったのは、デザインのほうはその日のうちで、翻訳ファイルのほうは、それから 5 か月あとです。
依頼の行は、hookを置いたあとも、すべて指示ファイルに残っています。
仕組み: 指示ファイルが実行を頼むコマンドを、hookも実行しているかを確かめる
が置くのは、自動テスト 1 本です。
# test/reference/claude_md_enforced_command_test.rb(手元のコードを短くしたもの)
class ClaudeMdEnforcedCommandTest < ActiveSupport::TestCase
test "commands the instruction file asks to run also exist in the commit hook" do
requested = requested_commands # 「必ず実行」の直後の囲みから scripts/... を拾う
# ⚠️ 対象 0 件で合格にしない。抽出が壊れたらここで落ちる
assert_operator requested.size, :>=, 2, "the extraction found nothing (is it broken?)"
enforcer = File.read(Rails.root.join(".git-hooks/pre-commit.sample"))
unenforced = requested.reject { |_lineno, script| enforcer.include?(script) }
assert_empty unenforced,
"a command is asked for in prose but missing from the commit hook. " \
"instructions only work when read, so put it in the hook first"
end
end
この自動テストを置くと、指示ファイルで実行を頼むには、その前に、同じコマンドを走らせる仕組みを置いておくことが必要になります。走らせる仕組みの無い依頼が実行されるのは、AIがその行を読んだときだけです。
hookがそのコマンドを実行していれば、依頼の行が指示ファイルに残っていても、この自動テストは通ります。この自動テストが通っているとき、指示ファイルに残っている実行の依頼は、どれもhookが既にやっていることの上書きです。
測る前、は「仕組みがあるなら、節ごと消すことができる」と思っていました。ところが翻訳ファイルの節には、問いで見た理由の行も並んでいました。
こうした理由の行は残します。のhookも、翻訳ファイルのチェックが落ちると、YAML の構文エラーはサーバーの起動不能に直結する、というメッセージを出します。ただ、そのメッセージが出るのは、チェックが落ちたコミットのときだけです。指示ファイルの理由の行は、セッションの始めに読み込まれます。この行は、5 か月あとに翻訳ファイルのチェックをhookへ入れたときの材料にもなりました。そのときの作業の記録は、依頼の行に仕組みが無かったことと並べてこの理由の行を挙げ、デザインより重いチェックが依頼の行のまま残っていた、と書いています。Claude Code には指示ファイルを点検する公式のコマンドがあり、そのコマンドも理由を残す側に置いています。削るのはコードベースから導くことのできる内容で、残すのは落とし穴、理由、ツールの既定と違う慣習です。だから、削るのは各節の依頼の 1 行で、行数は 1 節につき 1 行減るだけです。
CC BY 4.0 はここまで
頼まなくなったもの
は、「最後に確認してください」と「必ず実行してください」を、プロンプトでAIに頼まなくなりました。検証そのものは、いまも続いています。モデルは頼まなくても自分で検証し、hookは頼まなくても、チェックで落ちるコミットを止めます。やめたのは、モデルとhookが既にやっていることを、プロンプトで「やってください」と頼むことだけです。
CC BY 4.0
注意: 自動テストが語形しか見ていないこと、測ったのが 1 回であること
この回の死角を二つ、正直に書きます。
一つ目は、自動テストが見ているのが「必ず実行」という語形だけだということです。依頼の行が「実行しておいてください」と言い換えられていれば、自動テストはその行を見逃して合格にします。自動テストが判定することができるのは、決まった語形までです。1 作目の第 7 回でも、hookが見ているのはファイルの差分だけで、どの行がAI自身の書き込みかは、hookのプログラムでは判定することができませんでした。それと同じ構造です。
二つ目は、の測定が、1 つの課題で 1 回だけだということです。
この 1 回では、A と B が使ったトークンの差は 5% だけでした。倍になったのは、手数と時間のほうです。だから、「検証を指示するとコストが跳ね上がる」とは書くことができません。が観測したのは、正答が同じまま、手数が倍になったことだけです。
検証手順: 指示ファイルの側とhookの側から、一度ずつ食い違いを作る
前提
この回の自動テスト(指示ファイルの「必ず実行」と、pre-commit hookの中身を突き合わせるもの)を置き終えておきます。指示ファイルに「必ず実行」の行が 1 つ以上あり、その自動テストが今は通っていることも確かめておきます。
所要時間: 10 分
この自動テストは、指示ファイルとpre-commit hookという 2 つのファイルの食い違いを見ています。食い違いはどちらのファイルの側からも生まれるので、手順では、両方の側から一度ずつ食い違いを作ります。片方の側だけでは、自動テストが見ている範囲の半分しか確かめることができません。
手順
- あなたが、指示ファイルの側から食い違いを作ります。指示ファイルに「
scripts/nonexistent_check.shをコミット前に必ず実行」の 1 行を足します。実在しないコマンド名にしてください(hookが実行していないコマンドを、自動テストが拾うかを確かめるためです)。自動テストを走らせ、結果を控えます - あなたが、足した 1 行を消して指示ファイルを元に戻し、今度はhookの側から食い違いを作ります。pre-commit hookから、指示ファイルが「必ず実行」と書いているコマンドを 1 つコメントアウトします。自動テストを走らせ、結果を控えます
合格条件(すべて満たすこと)
合格の条件は 4 つです。手順 1 では、自動テストが落ち、失敗のメッセージに、指示ファイルの行番号とコマンド名が出ていること(CLAUDE.md:109: scripts/nonexistent_check.sh に当たる形)を確かめます。手順 2 でも、自動テストが落ち、失敗のメッセージに、コメントアウトしたコマンド名が出ていることを確かめます。
合格しなかったとき
- 手順 1 で落ちない —— 自動テストが、指示ファイルの「必ず実行」の語形を拾うことができていません
- 手順 2 で落ちない —— 突き合わせが片側だけです。hookの側からコマンドが消えても自動テストが気づかないので、指示ファイルに依頼だけが残った状態を見逃します
- 落ちたが場所が出ない —— 直す側(AIでも、あなたでも)が、依頼の行を指示ファイルの全行から探すことになります
後始末
コメントアウトを外し、自動テストが合格に戻ることと、git diff が空になることを確かめます。
落ちるべきときに自動テストが落ちることと、通るべきときに通ることを、両方とも一度ずつ確かめます。落ちることだけを確かめた自動テストでは、どんな指示ファイルでも落ちる壊れ方に気づくことができません。
CC BY 4.0 はここまで
次回は「具体的な指示を出さない技術」。は、やり方を書いていません。 指示ファイル 102 行に、番号付きの手順は 0 行です。それでも、頼んだ仕事はそのまま返ってきます。
連載「言わない技術」
- ← 前回: 序論
- → 次回: 第 2 回 具体的な指示を出さない技術
- 全回の一覧: 序論