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

今回は、サブエージェントの話です。サブエージェントというのは、メインの会話を引き継がずに、別に立ち上がって働く AI のことです。私はある日、十数言語ぶんの翻訳を、1 言語につき 1 体ずつ、13 体のサブエージェントに任せました。渡したつもりの仕事を、13 体はそれぞれ、別々のやり方で進めていました。渡していたのは呼び出しの文面だけで、契約 —— 渡すもの・使わない場面・越えてはいけない境界 —— を書いていなかったからです。

この連載は、AI の提案を検討しないという話をしてきました。

ただし、この回だけは逆のことを言います。

サブエージェントに渡っている入力だけは、一度、確かめてください。

前作「読まない技術」の第 5 回では、この 13 体の事故を、出てきたログを読む側から書きました。今回は、渡す側から見ます。

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

直近でサブエージェントに投げた仕事について、渡したプロンプトの全文です。

使わせるスクリプトの名前は、そこに書いてあるでしょうか。

成果物の合否を誰が決めるかは、書いてあるでしょうか。

たぶん、書いてありません。私も書いていませんでした。

CC BY 4.0

前作で、半分しか言えていなかったこと

前作の第 4 回で私はこう書きました。ツールの説明文は、似た条件文が並ぶほど互いを食い合い、一番使ってほしいものほど埋もれる。だから条件を並べるのをやめた、と。

現象としては合っています。実際、私の環境ではスクリプトが 84 本あって、スキル登録——カスタムコマンドやツールの登録——は 1 個です。

ただ、そこから「説明文は短いほどいい」と読める書き方をしてしまいました。これは間違いです。Anthropic がツールの定義について出しているガイダンスは、はっきり逆を言っています。極めて詳細な説明を書け。それがツールの性能を決める、いちばん重要な要素だと。求められている中身も具体的で、ほとんどマニュアルページです。何をするか、いつ使うか、そして使わないか、各パラメータの意味と挙動への影響、注意点や制限、そして返さない情報。最低でも 3〜4 文、複雑なツールならもっと、とまで書いてあります。

矛盾しているように見えて、していません。線引きはここに引けます。呼び出しを決めるためのテキスト(どんなときに使うかの見出し)と、振る舞いを決めるためのテキスト(契約の中身)は別物です。前作で私が「増やすな」と言ったのは前者で、後者は増やすべきだった。

13 体が、それぞれ別のスクリプトを書いていた

私の手元の本番プロジェクト——typingtube という、YouTube の音楽動画でタイピング練習ができる Web サービスです——で、十数言語ぶんの翻訳を 1 言語 1 体で任せ、13 体で約 173 万トークン使いました。

内訳を見ると、各サブエージェントが自分用の挿入スクリプトと検証スクリプトを書き直していました。スクラッチパッドに 12 本、ツール呼び出しは 1 体あたり 68〜97 回。

これは「使う条件」が曖昧だったから起きたのではありません。渡すもの、使うスクリプト、どこまでが自分の仕事かという契約が、何も書かれていなかったから起きました。

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

増やしてはいけないのは「使う場面」の列挙で、厚く書くべきは契約——渡すもの、使わない場面、越えてはいけない境界。

なぜ、書かなかったところが、13 体で違う形になるのか

13 体に私が渡したのは、呼び出しの文面だけです。私の側には、そこまでのやり取りが全部あります。どの言語がどれだけ残っているか、前に何を踏んだか、どのファイルのどこに差し込むか。どれも、13 体には 1 バイトも渡っていません。

Anthropic がサブエージェントについて出しているドキュメントは、渡るものを並べています。サブエージェントは新しい、隔離されたコンテキストで始まる。こちらの会話の履歴も、こちらが呼び出したスキルも、こちらが読んだファイルも、そこには見えない。渡るのは 6 つです。そのサブエージェント自身のシステムプロンプト、こちらが書いた委譲のプロンプト、プロジェクトの指示ファイル、git の状態、あらかじめ指定して読み込ませたスキル、同じセッションで動いている他のサブエージェントの名簿。

事故の日、私のプロジェクトの指示ファイルには、サブエージェントの節がありませんでした。私があらかじめ指定して読み込ませたスキルもありません。13 体に渡ったのは、呼び出しの文面 1 つでした。

書かなかったところは、空のままにはなりません。サブエージェントの側で埋まります。前回の禁止では、範囲の書いていない一行だけが届いて、手元で件数を数えているものの範囲が、そのまま禁止の範囲になりました。今回は手元に何もありません。翻訳をどう差し込むかは、13 体それぞれが学習で持っているほうから来ます。似ていて、少しずつ違う 12 本のスクリプトは、そうやって生まれました。

サブエージェントのドキュメントは、どこに何を書くかも指示しています。サブエージェントの説明文はコンテキストを食うので短く保て、詳細はそれぞれのシステムプロンプトへ移せ、そちらはそのサブエージェントが走るときだけ読み込まれる、と。呼び出しを決めるテキストと、振る舞いを決めるテキストは、置き場所まで別に指定されていました。

仕組み: 用途カタログに、契約を書く

置くのは 1 枚です。AI に投げてよい仕事の一覧に、起動前に用意しておくものを並べます。サブエージェントを起動して仕事を割り振り、成果を受け取るメイン側の AI を、ここでは「中央」と呼びます。指揮を執る側です。一般の用語ではオーケストレーターです。

# サブエージェントを起動してよい用途のカタログ(手元のコードを短くしたもの)
#
# ⚠️ ここに無い用途では起動しない。要るのは「単純作業に落とし込める大量処理」だけ。
#   落とし込めていない仕事を投げると、各エージェントが自分用のツールを書き直して溶かす。
# ⚠️ requires は起動前に用意しておくもの。1 つでも無ければ hook が起動を止める。
#   これが「単純作業に落とし込んだか」の機械的な判定になっている。
# ⚠️⚠️ サブエージェントにテストを走らせない。並走ガードが 1 本しか通さないので、
#   メイン側と取り合ってどちらかが必ず落ちる。テストはメインセッションで回す。

session_total_checkpoint: 25   # ここを超えたら用途に関係なく一度止めて人に報告する

missions:
  i18n_translation:
    description: Backfill locale translations (1 agent = 1 language)
    requires:
      - scripts/i18n_ledger.py                     # 作業の一覧(渡す唯一の入力)
      - scripts/i18n_apply.py                      # 差し込みは中央で行う
      - scripts/i18n_verify.py                     # 受け入れテストも中央
      - docs/reference/i18n_translation_notes.md   # 言語別の注意点
    model: [sonnet, haiku]     # ⚠️ opus を既定にしない(訳文そのものは全体の 1 割未満)
    max_parallel: 5
    max_total: 20

description は 1 行のままで、コメントのほうが本体より長くなりました。これが「使わない場面」で、さっきの man page の中身そのものです。第 2 回で禁止に添えたのも理由でした。ツールの説明文で要るものも、同じでした。

起動の直前に割り込むスクリプト(Claude Code なら hook)が、このカタログを見ています。判定は 5 つだけで、しかも回避できるかどうかが分かれています。

#判定回避できるか
1用途名の宣言がある名前を書けば通る
2カタログにある用途か同上
3requires のファイルが実在する⚠️ 回避できない
4model が用途ごとの許可リストにある⚠️ 回避できない
5起動の記録の数(並列・総数・同名の再起動)別の名前で起動すればすり抜ける

1 と 2 は呼び出しを決めるテキスト、3 と 4 は契約です。効いているのは契約のほうだと、この表が示しています。requires に並んでいるのは仕事を単純作業に落とし込んだ人にしか作れないファイルなので、その実在が落とし込み済みかの判定になります。「読んだか」は測れませんが、「書いたか」は測れる。

一つ、見込みが外れたことがあります。私はこのとき、173 万トークンの大半はスクリプトの書き直しに消えたと思っていました。契約を書いて、差し込みと受け入れテストを中央のスクリプトに寄せれば、そのぶん減るはずだと。

寄せたあとで 4 言語を測りました。前のやり方(各自がスクリプトを書く)は、10 言語の平均で 1 体あたり約 172,900 トークン。あとのやり方(中央のスクリプトを渡す)は、いちばんキー数の多い言語で 190,325 トークンでした。トークンは減っていません。決めていたのはスクリプトの重複ではなく、翻訳そのものでした。どちらのやり方でも、既存の訳を読んで用語を合わせ、まだ無い文章を書く手間は同じでした。

契約が買ったのは、速さではありませんでした。YAML を壊す操作がサブエージェントの手から消えたことです。この YAML が壊れると本番のサーバーが起動しないので、壊せる経路はここ 1 本だけでした。もう一つは、受け入れテストが全言語で同じものになったことです。

そして、もう確かめません

一度確かめて、カタログに書いたら、そこで終わりです。以後は hook が毎回、私の代わりに確かめます。起動のたびに requires が揃っているかを見て、欠けていれば止める。私は入力を開きません。

言わなくなったものも 1 つあります。「この作業はサブエージェントに向いていません」という説明です。以前は AI が起動しようとするたびに私が止めて、なぜ向かないかを話していました。いまはカタログに無ければ起動できず、無い理由はカタログの冒頭に書いてあります。同じ説明を、私が二度することはなくなりました。

確かめたのは一度だけです。

注意: 全部のツールに契約を書くのではない

厚い契約が要るのは、間違えると高くつく入口だけです。私の環境ではテスト実行、コミット、サブエージェント起動の数か所。

残りの 80 本近いスクリプトは素のままで、間違えてもすぐ分かって安く済みます。

ここを取り違えると、84 本ぶんの man page を書くことになり、前作が警告した「足すほど薄まる」を自分でやることになります。契約を厚くする対象は、契約が破られたときの損害で選んでください。

検証手順: requires を 1 つ消して、止まることを確かめる

前提

  • 用途カタログを 1 枚書き、起動の直前に割り込む hook を登録し終えていること
  • カタログに用途が 1 つ以上あり、その requires のファイルが実在していること

所要時間: 10 分

手順

  1. あなたが、その用途の requires に並んでいるファイルを 1 つ退避します。消すのではなく退避です。後始末で戻します
  2. 中央(サブエージェントを指揮する側の AI)に、その用途で起動させます

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

  • 起動する前に止まる。起動してから失敗するのでは遅すぎます。渡した仕事の分のトークンは、もう使われています
  • 止まった理由に、欠けたファイルの名前が出ている
  • カタログの場所が案内されている

合格しなかったとき

  • 起動してしまった —— hook が見ているのは用途名だけで、requires の実在までは見ていません
  • 止まったが、カタログが案内されない —— 中央の AI は、次に何を用意すればよいか分からないまま止まります。案内の 1 行を足します

後始末

  • 退避したファイルを戻し、同じ用途でもう一度起動させます。止まらずに走れば終わりです

止める仕組みは「止まらない」方向に壊れます。一度も止まったことのない判定は、動いている判定と同じ見え方をします。どちらも、黙って起動を通すからです。


CC BY 4.0 はここまで

次回は「自動テストを書かない技術」。禁止事項の半分以上に、私は自動テストを書いていません。それでも、書いてあるだけの禁止は 1 つもありません。

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