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

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

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

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

前作「読まない技術」の第 5 回で、私はこの 13 体の事故を、出てきたログを読む側から書きました。今回は、サブエージェントに仕事を渡す側から見ます。

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

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

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

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

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

CC BY 4.0

条件文と契約は、別のテキスト

前作の第 4 回で、私は、条件文が並ぶほど、一番使ってほしいスキルも、AIに選ばれにくくなると書きました。私はいまもそのとおりにしていて、スクリプトが 84 本あるのに、スキル(カスタムコマンドやツールを登録しておくもの)として登録しているのは 1 つだけです。

同じ回で私は、Anthropic がツールの定義について出しているガイダンスも引きました。ガイダンスは、ツールの説明文を極めて詳細に書くよう求め、説明文を、ツールの性能を決めるいちばん重要な要素としています。ガイダンスが求める中身を並べると、マニュアルページの項目になります。何をするか、いつ使い、いつ使わないか、各パラメータが何を意味し、挙動をどう変えるか、注意点と制限、そして返さない情報です。説明文は最低でも 3〜4 文、とまで書いてあります。並ぶ数を減らすことと、説明を詳しく書くことは、別のテキストのことを言っています。一つは、AIがツールを呼び出すかどうかを決めるためのテキスト(どんなときに使うかを書いた条件文)で、もう一つは、呼び出されたあとの振る舞いを決めるためのテキスト(契約の中身)です。サブエージェントに仕事を渡すときに私が書いていなかったのは、後者でした。

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

私の手元には、typingtube という本番プロジェクトがあります。YouTube の音楽動画でタイピング練習ができる Web サービスです。このプロジェクトで翻訳を任せた 13 体が使ったトークンは、合わせて約 173 万です。

私が内訳を見ると、どのサブエージェントも、訳を差し込むスクリプトと、差し込んだ結果を確かめるスクリプトを、自分用に書いていました。作業用の一時フォルダ(スクラッチパッド)には、そうしたスクリプトが 12 本あり、ツールの呼び出しは、1 体あたり 68〜97 回でした。

これが起きたのは、私がプロンプトに契約を何も書いていなかったからです。使わせるスクリプトという渡すものも、どこまでがサブエージェントの仕事かという境界も、書いていませんでした。どんなときに使うかを書いた条件文の曖昧さは、原因ではありませんでした。

13 体のサブエージェントに同じ翻訳を 1 言語ずつ任せたら、13 体がそれぞれ、自分用のスクリプトを書いていました。このとき、あなたなら次のどちらを選びますか。

  • どんなときにサブエージェントを使うかの条件文を、もっと詳しく書き直す
  • 使わせるスクリプトなど、渡すものを用途ごとに書き、それがそろうまで起動させない

答えは、渡すものを書くほうです。13 体が別々のスクリプトを書いたのは、私が渡したのが起動のときのプロンプトだけで、使わせるスクリプトも、どこまでが仕事かという境界も書いていなかったからです。サブエージェントには、メインの会話の履歴も、読んだファイルも渡りません。プロンプトに書かなかったところは、13 体がそれぞれ自分で決めました。条件文は、サブエージェントを使うかどうかを決めるテキストで、書き直しても、呼び出したあとに何を使うかは渡りません。渡すものを書き、それがそろうまで起動させなければ、サブエージェントが自分で決めるところは、訳を書くことだけになります。

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

増やしてはいけないのは「使う場面」を並べた条件文で、詳しく書くべきは契約——渡すもの、使わない場面、越えてはいけない境界。

なぜ、プロンプトに書かなかったところを、13 体が別々のやり方で埋めるのか

13 体に私が渡したのは、起動のときに書いたプロンプトだけです。私の側のセッションには、そこまでのやり取りが全部残っています。どの言語の訳がどれだけ残っているか、前にどんな失敗をしたか、どのファイルのどこに差し込むか、です。どれも、13 体には 1 バイトも渡っていません。

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

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

プロンプトに書かなかったところは、サブエージェントが自分で埋めます。前回(第 3 回)は、色リテラル(#fff のように、色の値をコードに直に書いたもの)を禁止する一行に、禁止がどこまで及ぶかという範囲が書かれていませんでした。AIは、手元にあった hook(コマンド実行などの直前・直後に割り込むスクリプト)が数える範囲を、そのまま禁止の範囲として扱いました。今回のサブエージェントの手元には、前回のような、補う材料が何もありませんでした。訳をどう差し込むかは、13 体それぞれが、自分で決めました。似ていて、少しずつ違う 12 本のスクリプトは、こうしてできたものです。

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

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

置くのは、ファイル 1 枚です。サブエージェントに任せてよい仕事を「用途」として並べた一覧(用途カタログ)に、用途ごとに、起動の前に用意しておくファイルを書きます。サブエージェントを起動して仕事を割り振り、成果を受け取るメイン側の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 行のままで、コメントのほうが、設定の本体より長くなりました。コメントに書いたのは、使わない場面と、なぜ使わないかという理由です。使わない場面は、ガイダンスが説明文に求めていた中身です。第 2 回では、開発でやらないと決めてAIに守らせるルール(禁止事項)に、なぜ禁止するのかという理由を添えました。ツールの説明文でも、要ったのは理由でした。

このカタログは、起動の直前に割り込む hook(Claude Code の機能)が見ています。判定は 5 つだけです。

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

1 と 2 が見ているのは呼び出しを決めるテキストで、用途名を書けば通ります。回避できないのは、契約を見ている 3 と 4 です。requires には、仕事を単純作業に落とし込んだ人だけが作ることのできるファイルが並んでいます。だから、hook は、そのファイルが実在するかどうかで、仕事が単純作業に落とし込み済みかどうかを判定します。落とし込み済みなら、サブエージェントが自分で決めるところは、訳を書くことだけです。hook に分かるのは、誰かがそのファイルを書いたかどうかまでで、サブエージェントがそのファイルを読んだかどうかは分かりません。

一つ、見込みが外れたことがあります。私は契約を書く前、173 万トークンの大半は、サブエージェントがスクリプトを書くことに使われたと思っていました。契約を書いて、差し込みと受け入れテスト(サブエージェントの成果をメインエージェントの側で判定するスクリプト)をメインエージェントの側のスクリプトに寄せれば、そのぶんトークンが減るはずだと考えていました。

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

契約を書いて私が得たものは、2 つです。一つは、訳を差し込む操作を、メインエージェントの側のスクリプトだけが行うようになったことです。YAML(訳を収めたファイル)が壊れると、本番のサーバーが起動しません。YAML を壊すおそれのある経路は、メインエージェントの側のスクリプトの 1 本だけになりました。もう一つは、受け入れテストが全言語で同じものになったことです。

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

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

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

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

注意: 全部のスクリプトに契約を書くのではない

詳しい契約が要るのは、間違えると高くつく入口(AIの操作が必ず通る場所)だけです。私の環境では、テストの実行、コミット、サブエージェントの起動という、数か所の入口のスクリプトに限っています。

残りの 80 本近いスクリプトは、契約を書かずに、そのままにしています。AIが使い方を間違えても、すぐに分かり、直す手間も小さく済むからです。

入口を選ばずに、全部のスクリプトに契約を書くと、84 本ぶんのマニュアルページを書くことになります。契約を詳しく書く入口は、契約が破られたときの損害の大きさで選んでください。

検証手順: requires のファイルを 1 つ退避して、起動が止まることを確かめる

前提

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

所要時間: 10 分

手順

  1. あなたが、その用途の requires に並んでいるファイルを 1 つ、別の場所へ退避します。後始末で元の場所に戻すので、消さずに移しておきます
  2. メインエージェント(サブエージェントを指揮する側のAI)に、その用途のサブエージェントを起動させます

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

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

合格しなかったとき

  • 起動してしまった —— hook が見ているのは用途名だけで、requires の実在までは見ていません
  • 止まったが、カタログが案内されない —— メインエージェントは、次に何を用意すればよいか分からないまま止まります。hook が止めるときに出す文に、カタログの場所を案内する 1 行を足します

後始末

  • 退避したファイルを元の場所に戻し、同じ用途でもう一度起動させます。hook に止められずにサブエージェントが走れば、終わりです

起動を止める仕組みは、壊れると、止めるはずの起動を黙って通すようになります。一度も起動を止めたことのない仕組みは、働いていても壊れていても、あなたには同じに見えます。この手順で requires のファイルをわざと 1 つ退避するのは、起動が止まるところを一度見ておくためです。


CC BY 4.0 はここまで

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

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