この記事は、連載「AIの意見を聞かない技術」の第 3 回です。各回はファイルやスクリプトを一つ置けば完結します。連載の全体像と各回の一覧は序論にあります。

今回は、禁止のルールそのものの話です。禁止事項とは、開発のときに許さないと決めたことを書き並べたルールで、AI にも自分にも守らせるものです。前回、その禁止に理由を添えました。理由まで書いた禁止は、拡大にも縮小にも振れなくなります。それでも、正しく書いてあるのに守られない禁止が、私の手元にはまだ残っていました。

前作「読まない技術」の第 6 回で、自動テストに移せるのは、機械が真偽を決められるルールだけだと書きました。今回取り上げる禁止は、機械どころか、人が読んでも真偽を決められないものでした。

まず一つだけ、試してみてください。あなたの禁止事項を 1 つ選んで、「これは違反なのかどうか、自分でも一瞬迷う」というケースを 1 つ作ってみます。作れたでしょうか。作れたなら、次の問いです。AI がそのケースを踏んだとき、どちらを選ぶかを、あなたは答えられますか。

CC BY 4.0

「色リテラルを書かない」と決めて 0 にしたのに、254 件残っていた

私の手元の本番プロジェクト——typingtube という、YouTube の音楽動画でタイピング練習ができる Web サービスです——には、「色はテーマの変数で書く。色リテラルを書かない」という禁止があります。明るいものから暗いものまで 10 のテーマを切り替えられるので、#fff と直に書いた文字は、どれかのテーマで必ず背景に沈みます。理由もはっきりした、譲れない類のものです。

4 月に私は pre-commit hook を置いて、1 週間で 1,775 件を 0 にしました。「達成済」と設計書に書いて、終わったつもりでした。

8 月に、hook が見ていなかった場所を初めて数えました。254 件ありました。 HTML テンプレートの style="color:#fff" と、JavaScript の el.style.color = "#fff" です。CSS ファイルには 1 件も無く、その隣に 254 件が並んでいました。文字色を直書きしていた箇所を 10 テーマで測ると、白い面に白い文字が出ているトースト(コントラスト比 1.00)が、10 テーマ中 5 つで見つかりました。

いちばんこたえたのは、同じ月に AI が書いた計画書に、こうあったことです。

CSS ファイルに色リテラルを増やさないよう、inline style で流す

AI は禁止を守ろうとして、迂回していました。 書いた側からすれば、hook に引っかからない場所へ置いた親切な処理です。文面は正しかった。「色リテラルを書かない」と書いてあった。それでも守られなかった。style 属性の中の色が「CSS の色」に入るかどうかを、誰も——私も、AI も、hook も——決めていませんでした。hook は CSS ファイルだけを数えていて、AI はその範囲を禁止の範囲だと読みました。

決めていないものは、その場の都合で決まります。テーマの変数に無い色をいま出したい、という目の前の要求のほうが、範囲の書いていない禁止より強い。善意の迂回路は、いつも範囲の隙間に通ります。

「どっちの判定軸で数えるのか」が決まっていなかった

もう一つ、もっと大きいものを。このサービスには「歌詞タイピングと単語帳の実績・ミッション・統計は完全に分離する」という禁止があります。難易度もプレイコストも違うので、混ぜると片方の実績価値が壊れる。理由もはっきりしています。

ここに 3 つ目の面——記事——が増えたとき、答えられない問いが出ました。記事から入って単語帳をプレイしたユーザーは、どっちの判定軸で数えるのか。

流入元で数えるなら記事軸です。行為で数えるなら単語帳軸です。どちらの読み方も「相互乗り入れ禁止」に反していません。禁止は正しく書いてあるのに、判定できない。 この状態で AI に実装を任せると、そのときの文脈でどちらかを選び、後から辻褄合わせが始まります。

決めた線引きは 1 行です。

判定軸は行為で決まる(打った = 単語帳 / 読んだ = 記事)のであって、どこから来たかでは決まらない。流入元は導線クリックとして記事軸が別に数える。

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

禁止は、範囲が割れていないと守られない。割れているかどうかは、境界のケースを一つ作れるかで分かる。

仕組み: 適用範囲を「判定軸」で書き、境界のケースを 1 行ずつ置く

禁止事項に「適用範囲」の見出しを作り、禁止の文言をそのまま繰り返さないことだけ守ります。書くのは、迷ったときにどちらを選ぶかです。

### NG13 歌詞・単語帳・記事の実績/ミッション/統計の相互乗り入れ禁止(3 軸)

**適用範囲**:

- 単語帳のプレイを歌詞側の集計テーブルに計上しない(単語帳は独立系統)
- ⚠️ 記事から入った単語帳プレイは**単語帳軸**に計上する(記事軸ではない)。
  軸は行為で決まる(打った = 単語帳 / 読んだ = 記事)のであって、どこから来たかでは決まらない
- ⚠️ 記事軸に実績・ミッションは当面作らない。持つのはアクセス分析だけ。
  ★ 作りたくなったときは**この行を書き換えてから**作る(黙って足さない)

3 行目の ★ は解除の手順です。禁止だけだと「必要だから作る」か「書いてあるから作らない」の二択になりますが、解除条件があると第三の道が開きます。 この形が実際に使われることは、第 1 回の測定で確認できました。

そして、判定軸は後から増えます。この禁止は 2 つの判定軸で始まって、3 つの判定軸になりました。増えるたびに境界のケースが新しく生まれるので、判定軸を増やす作業と境界を 1 行足す作業はセットです。

Anthropic がスキルの書き方について出しているガイダンスに、これを一般化した言い方があります。指示の具体度は、その操作の壊れやすさに合わせよ——判断が要る開けた領域には散文の目安を、一つの手順しか安全でない狭い橋には正確なコマンドを。禁止の「結論」は判断が要るので散文でよく、「適用範囲」は狭い橋なのでそこだけ正確に書く。同じ 1 項目の中で、書き方を変えていいということです。

CC BY 4.0 はここまで

相談されなくなったもの

「これは違反に当たりますか」です。以前はその都度こちらが判定していました——AI が聞いてくることもあれば、聞かずに越えていることもあり、後者に気づくのは実装が終わった後です。境界の行を書いてからは、迷いどころが先に文字になっているので、判定を求められません。

CC BY 4.0

注意: 判定軸を増やすと、組み合わせが増える

2 つの判定軸なら境界は 1 本ですが、3 つの判定軸なら 3 本です。判定軸を足すコストは、判定軸の数ではなく組み合わせの数で効きます。 私の場合、3 軸目を足した日にいちばん時間を使ったのは、記事軸の定義ではなく「記事 × 単語帳」の境界を決めることでした。

検証手順: 境界を、両方向から破る

前提

  • 禁止に「適用範囲」を書き、境界の行が 1 つ以上あること
  • その境界を見ている自動テストがあり、今は通っていること
  • 作業中の変更が無い状態(git status が空)から始めること

所要時間: 20 分

境界には向きが 2 つあります。片方だけ確かめると、半分しか見ていない自動テストを、両方見ていると思い込みます。

手順

  1. あなたが、境界の行を 1 つ選びます(本文の例なら「記事から入って単語帳をプレイしたユーザーを、どちらの判定軸で数えるか」)
  2. あなたが、一方の向きに破ります。境界が「行為で数える」と決めているなら、流入元で数える実装をわざと 1 行書きます。自動テストを走らせ、結果を控えます
  3. あなたが、その 1 行を戻し、逆の向きに破ります。今度は反対側から越える実装を書きます。自動テストを走らせ、結果を控えます

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

  • 手順 2 で、自動テストが落ちる
  • 手順 2 の失敗のメッセージに、どちらの向きで破ったかが出ている
  • 手順 3 でも、自動テストが落ちる

合格しなかったとき

  • 手順 3 で落ちない —— ⚠️ その自動テストは片側しか見ていません。 見つけたのがこの検証の収穫です。逆向きの表明を 1 つ足します
  • どちらでも落ちない —— 自動テストが、その境界を走査の対象にしていません

後始末

  • 手順 3 で書いた 1 行を戻し、自動テストが合格に戻ることと、git diff が空になることを確かめます

片方向だけの自動テストは、境界の半分しか見ていません。私の環境でこの禁止を見ているテストが「双方向の回帰」という名前になっているのは、片側だけ塞いで安心した過去があるからです。


CC BY 4.0 はここまで

次回は「サブエージェントの入力だけは、確かめろ」。サブエージェントの入力だけは、確かめないととんでもないことになります。

連載「AIの意見を聞かない技術」