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

今回は、AIの答えが返ってきた後の話です。前回は、答えが返ってくる前に、人間がAIに渡してしまう前提を見ました。今回扱うのは、返ってきた答えに「そうじゃなくて」とダメ出しをするときです。ダメ出しは書き換えられる —— してほしい形に、です。書き換えることができなかったダメ出しは、そもそも言葉でAIに守らせるものではありません。

前作「AIの意見を聞かない技術」の第 2 回で、禁止には理由を添えろと書きました。あれは、一覧に残す禁止の話でした。今回は、その場で口に出す禁止の話です。

まず一つだけ、数えてみてください。

数えてほしいのは、あなたが直近のセッション(AIとの、始まりから終わりまで一続きのやり取り)で、AIに「そうじゃなくて」「それは違う」と打った箇所です。そのうち、代わりにどうするかまで打ったのは、何回でしょう。

CC BY 4.0

行動ルールの禁止 16 件のうち、15 件には「代わりにどうするか」が書いてありました

私が数えたのは、AIに渡している行動ルールのほうです。行動ルールは 43 件あり、1 件ごとに 1 ファイルに書いてあります。セッションの最初に読み込まれるのは、その索引だけです。索引には、ファイル名と一行の要約が並んでいます。ルールの本文をAIが読むのは、そのファイルを開いたときです(「AIの意見を聞かない技術」の第 1 回で測ったとおりです)。

数える前、私は、一覧には否定形の禁止ばかりが並んでいると思っていました。

件数
行動ルール(総数)43
そのうち、要約が否定形(〜禁止 / 〜しない / 〜してはならない)16
そのうち、**How to apply:**(代わりにどうするか)を持つ15

例えば、要約が否定形のルールの 1 件は、次のように書いてあります。

description: エラーを握りつぶさない。不完全データで機能させず、明確にエラーを出す。

データが期待フォーマットでない場合、フォールバックで動作させてはならない。

**Why:** ユーザーが問題に気づけない / 開発者にエラーが届かない / 不完全なデータで
        中途半端に動作し、体験を悪化させる

**How to apply:**
- 期待するデータがなければ console.error + ユーザーに見えるメッセージで通知
- 「何も表示しない」より「エラーを表示する」方が常に良い

上半分が禁止で、下半分が「代わりにどうするか」です。Anthropic のプロンプトの指針は、応答の書式を指示するときの勧めとして、同じ向きの書き方を挙げています。してほしくないことではなく、してほしいことを書く、というものです(「markdown を使わないで」の代わりに「滑らかにつながる散文の段落で」)。手元の一覧の 15 件は、その向きの書き方に、すでになっていました。

禁止の一行だけが届くと、何が禁止に入るかと、代わりに何をするかは、AIがその場で決めます。前作の第 2 回では、「思いつきや『ついでの改善』は禁止」という SEO の禁止事項の一行を、AIが、使われていない翻訳キーを削除する作業に当てて、「削除をためらったほうがいい」と提案してきました。禁止事項の文面には無い言葉です。「代わりにどうするか」が同じファイルにあれば、AIはそのファイルを開いたときに、禁止と一緒に、してほしい形を読みます。

「代わりにどうするか」を書くことができなかった 1 件は、自動テストへ移してありました

残る 1 件のルールを開くと、本文は 1 行だけでした。その 1 行は、自動テストのファイル名です。このルールは、「代わりにどうするか」を書くことができなかったので、自動テストへ移してありました。

行動ルール全 43 件で見ても、同じでした。「代わりにどうするか」を持たない 6 件のうち、5 件には「検査へ移した」と書いてあります。5 件の形は、どれもこうです。

description: 非 GET の fetch には X-CSRF-Token が必須。検査へ移した

**検査が見ている**: test/reference/memory_rule_csrf_token_test.rb

言葉で渡したルールは、AIが破っても、何も止めません。第 5 回では、私が「最重要」と書いた節のルールが、残っていた 5 週間の記録で 6 回破られていました。6 回とも作業はそのまま先へ進み、私が回数を知ったのは、記録を検索したときです。自動テストへ移したルールは、AIがその行を読んだかどうかとは別に、破られれば落ちます。

私は、AIの答えが、してほしいものと違っていても、「そうじゃなくて」「〜しないでください」とは言わなくなりました。行動ルールの一覧にも、否定形の一行だけで残っている禁止はありません。否定形をやめて済んでいる理由として、あなたなら次のどちらを考えますか。

  • AIは否定形を読み取りにくいので、肯定形で書いたほうが伝わるから
  • 否定形の一行だけだと、代わりに何をするかを、AIがその場で決めることになるから

答えは、後者です。前作の第 2 回のAIは、禁止の一行を読んで、翻訳キーを削除する作業に当てていました。否定形は読み取れています。欠けていたのは、代わりに何をするかで、AIはそこを「削除をためらったほうがいい」という、禁止事項の文面に無い言葉で埋めました。読み取りにくさが理由なら、「〜しない」を「〜を避ける」と言い換えれば足ります。けれども、言い換えても、代わりに何をするかは書かれないままです。「代わりにどうするか」が書いてあれば、AIはそれに沿って進みます。書くことのできない禁止を言葉のまま残すと、破られても何も止まりません。第 5 回の 6 回も、作業はそのまま先へ進みました。自動テストへ移せば、破った変更が落ちます。否定で言っていた役を受け持つのは、してほしい形と自動テストです。

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

ダメ出しは、してほしい形に書き換えられる。書き換えることができなかったダメ出しは、言葉でAIに渡す代わりに、自動テストへ移す。

「代わりにどうするか」が無いと数えた 6 件のうち、1 件は書式が違うだけでした

正直に書きます。私は、上の「6 件」をコマンドで数えたあと、6 件のファイルを開いて中身を読みました。そのうち 1 件は、数え間違いでした。

そのファイルには、**How to apply:** という節がありません。その代わりに、「E2E でしか担保できない領域」「単体テストで完結させる領域」という 2 つの節があります。ファイル全体が、テストをE2Eと単体テストのどちらに書くかを決める表でした。中身は最初から最後まで「代わりにどうするか」で、違ったのは書式だけです。

コマンドが数えることができるのは、節の名前までです。「代わりにどうするか」も自動テストも持たないルールは、0 件だったことになります。

仕組み: 「代わりにどうするか」の無い禁止を、保存の直後にAIへ突きつける

手元では、AIが行動ルールを保存すると、その直後に差分をAIへ突きつける監査が動いています(「読まない技術」の第 2 回で置いたものです)。今回置くのは、この監査に足すコードです。

# .claude/hooks/audit_memory_write.py(手元のコードを短くしたもの)
PROHIBITION = re.compile(r"禁止|するな|してはならない|使わない|出さない|べきではない|しないこと")
REPLACEMENT = "**How to apply"

# ⚠️ 差分ではなく保存後のファイル全体を見る。How to apply は前のセッションで
#    書かれていることがあり、差分だけ見ると「無い」と誤検出する
unreplaced = sorted({
    os.path.basename(path)
    for path, _ in changes
    if PROHIBITION.search(read(path) or "") and REPLACEMENT not in (read(path) or "")
})

if unreplaced:
    parts += [
        f"⚠️ **prohibition with no replacement**: {', '.join(unreplaced)}",
        "   there is a prohibition, but no `**How to apply:**` (what to do instead).",
        "   if you cannot write one, move it to a check. left as a prohibition, nobody sees whether it was kept.",
    ]

このコードが見るのは、禁止と同じファイルに「代わりにどうするか」、つまり置き換え先があるかどうかだけです。

この警告は、保存の後に出しています。「読まない技術」の第 8 回では、同じ方針でも、差し込む瞬間によってAIの動きが違い、「方針の中身は同じで、違ったのは、読ませた瞬間です」と書きました。ここでの方針は、この警告の文です。

この監査は、初めは保存の前に割り込んで、保存を止めていました。そのときのAIは、語を言い換えるだけで、その hook を素通りしました(「読まない技術」の第 2 回)。保存を止める hook は、止めた呼び出しを通す条件として、方針を見せることになります。保存の後には、止める呼び出しがありません。AIの前に並ぶのは、保存されたファイルと、その禁止に「代わりにどうするか」が無いという警告です。残るのは、「代わりにどうするか」を書き足すか、自動テストへ移すかです。

flowchart LR
  P["保存の前に差し込む"] --> P1["保存を止め、通す条件として方針を見せる<br/>方針は、通るための攻略対象になる"] --> P2["語を言い換えて素通り"]
  Q["保存の後に差し込む"] --> Q1["保存は済んでいて、<br/>止める呼び出しが無い"] --> Q2["書き足すか、自動テストへ移すか"]

CC BY 4.0 はここまで

言わなくなったもの

私は、「そうじゃなくて」「それは違う」「〜しないでください」を言わなくなりました。AIの答えが、してほしいものと違っていることは、前と同じだけ見つかります。変わったのは、書くものが、否定形から、してほしい形になったことと、してほしい形を書くことができないときに、言葉で書くのをやめたことです。

CC BY 4.0

注意: 監査が見ているのは 7 語だけです

一つ、この監査が見ているのは、禁止の語形だけです。語形を見ているのは 7 語の正規表現なので、AIが禁止を「〜は好ましくありません」と言い換えれば、監査を抜けます。第 1 回と第 5 回で置いた自動テストも、語形だけを見ていて、言い換えた行を見逃します。同じ構造の抜け穴は、これで 3 度目です。

今回は、コマンドも監査も節の名前だけを見ているので、誤検出が出ることも分かりました(上の、書式が違うだけだった 1 件です)。

二つ、行動ルール 43 件は、多いほうです。小さなプロジェクトなら 5 件で足ります。問われるのは、1 件目のルールを書くときに、否定形のまま書き終えるかどうかです。

三つ、AIは、書いた「代わりにどうするか」に沿って進みます。それが最善でなければ、最善との差が、そのまま出力の差になります(第 2 回)。

「代わりにどうするか」を書くのは、自分が本当に知っている場所に限る必要があります。知らない場所なら、書くのは制約と確かめ方だけにします。

四つ、「代わりにどうするか」を消したときにAIがその場で何を埋めるかは、この一覧では測っていません。測ったのは、前作の第 2 回の、結論の一行だけが届いた 1 件です。

検証手順: 禁止だけを保存して、保存の直後に警告が出ることを確かめる

前提

保存の直後に、差分をAIへ突きつける監査(PostToolUse に当たるhook)が、既に動いている状態から始めます。その監査には、「代わりにどうするか」の無い禁止を見るコードを足し終えておきます。試すのは使い捨ての場所にしてください。本物のルール置き場は、複数のセッションが共有しているので、試し書きが隣のセッションに届きます。

所要時間: 10 分

手順

  1. AIに、行動ルールを 1 件、禁止だけ書いて保存させます(「〜を作るのは禁止」の 1 行だけ。「代わりにどうするか」は付けません)。禁止の語(禁止 / しないこと / してはならない など)を、必ず 1 つ入れてください。監査が見ているのは、その語形です
  2. AIに、同じルールへ **How to apply:**(代わりにどうするか)の節を足して、保存し直させます

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

合格の条件は 3 つです。手順 1 では、保存が済んだ後に警告が出て、その警告に prohibition with no replacement の行と、そのファイル名が出ていることを確かめます。手順 2 では、警告が消えることを確かめます。

合格しなかったとき

  • 保存の前に止まった —— 登録したのは保存前のhookです。割り込む位置が違います
  • 手順 2 でも警告が出たまま —— 監査が探している節の名前と、AIが書いた節の名前が食い違っています

後始末

手順 1・2 で作ったルールのファイルを消します。


CC BY 4.0 はここまで

次回は「AI に考えさせない技術」。私は、AI に考えさせていません。 それでも、AI の答えは正しいままです。

連載「言わない技術」