🚀

Fable 5が使えなくなる前に、その「働き方」をOpus/Sonnetに引き継がせた

に公開

前回、Fable 5をトークン破産させずに使い倒す方法について書きました。

https://zenn.dev/yui/articles/740da24e9ee419

簡単にいうとオーケストレーターに Fable 5をおいて、スコープ切り、設計、委譲、最終レビューを任せ、手を動かす実装・テスト・シッピング・コードレビューは Opus と Sonnet のサブエージェントが担う形式です。高いモデルが計画と判断を、安いモデルが実行を受け持つ構成です。これはかなりのタスクでうまく回っていました。

ただ、みなさんご存知の通り、サブスクでのFable 5 アクセスは7月7日で終わります。(近い将来またサブスクに戻ってきてくれることを祈ってはいますが・・・🙏)
追記: サブスクで使えるようになりましたね!笑 けどこのやり方はオーケストラーをFableにおいたままでも役に立つので読んでいただけると嬉しいです🙏

オーケストレーションの仕組み自体はFable 5がなくても動くとはいえ、やはり最上位でスコープを切っていたオーケストレーターのモデルが下がれば、その分アウトプットの質も下がります。

けど、Opus, Sonnet自体は悪いモデルというわけではありません。正しく使えばかなり良いアウトプットが出ます。大事なのは何をどのように行うのか、失敗した時にどうするのかの明文化です。そこで、Fable 5が使えなくなる前にFable 5がどのように働いているのかをドキュメント化して、スキルとしてOpus/Sonnetに引き継がせることにしました。

この記事は今回Opus/SonnetをFable化するにあたって、私が行ったこととその方法についてです。Fable の代わりを探すのは難しかったので、チーム全体の能力を上げて、オーケストレーターに Opus が座っても崩れないようにすることを目標としました。

Fable 5とOpus/Sonnetの差は知能ではない

サブスクからFable 5にアクセスできなくなった後も擬似的にFable 5を使い続けるために、まずはFable 5とOpus/Sonnetとの差を検証するところから始めました。

Opus/Sonnetが日常のエンジニアリングで意図しないアウトプットが出てきた時にその中身を見てみると、ある程度決まった形式であることに気づきました。例えば・・

  • ボタン1個に onClick が要るだけで、Next.js のページ先頭に "use client" を貼る
    • その結果、ページ全体とその import ツリーがまるごとクライアントに落ちる
  • fetch にキャッシュ指定を書かない
    • Next.js 14 と 15 でデフォルトが逆なのに、「動いた」を dev サーバーだけで確認して本番でキャッシュにハマる
  • Server Action を「フォームからしか呼ばれない関数」だと思って入力検証を省く
    • 実際は誰でも任意の引数で叩ける公開 POST エンドポイント
  • 初期データを useEffect の中で取りに行く
    • Server Component で await すれば済むのに、リクエストのウォーターフォールとレイアウトのちらつきの原因になる
  • 落ちたテストを「直す」ためにアサーションの方を書き換える
    • 落ちた原因がコード側にあると証拠で確定するまで、アサーションは触らないべき
  • 何も実行せずに「これで動くはずです」と言う
    • 実行してから動くかどうかを結論づけるべき

ここで重要なのは、これらはどれも知識の欠落ではないということです。Sonnet に「Next.js の Server Action は入力検証なしだと危険ですか?」と直接聞けば、正しく答えます。つまり知識は入っているが、正しい手順が欠けているとわかります。

この点Fable 5だと頼まれなくても暗黙的に適切なチェックをして適度に差し戻してくれるので良いのですが、Opus/Sonnetだとその辺のチェック項目を明文化する必要があります。

生の推論能力の差はもちろん実在しますが、定型的な実装・レビュー・デバッグにおいて、観測される品質差の大半は規律・順序・検証の差です。そしてこの3つは、すべてスキルとして書くことが可能です。

「注意深く」というプロンプトは効かない

Opus/SonnetをFable 5化するにあたって、まずは最初に「注意深く、検証を怠らず、ベストプラクティスに従ってください」と書くことを試してみました。その結果、何も起きませんでした(それはそう)。モデルの「次の一手」を変えない文章は、挙動を変えないからです。

なので、今回Fable 5の働き方を明文化するにあたって、最初のルールは禁止リストを作ることから始めました。以下の表現を含む行はシステムプロンプトに含めないと決めました。

「注意深く」/「ベストプラクティスに従え」/「よく考えて」/「品質を担保して」

すべてのシステムプロンプトは実行可能または検証可能である必要があります。つまりコマンドで実行可能、閾値が定義されている、if/then、grep パターン等で何をするのか明確であるということです。

5つの軸でFable 5の働き方を明文化しました

さて、どのように「Fable 5化をしたか」を書く前に紹介ですが、私は ccteams という CLI をメンテしています。Claude Code のサブエージェントチームをコマンド一発で導入できるツールです。

そこでv0.2.xからすべてのチームに2つのスキルが同梱されるようにしました。共有の working method(働き方) と、チーム別の playbook(計画書) です。

ここではこのccteamsで実際に行ったことを順番に書いていきます。

1. 作業ルーティンを固定する

Opus/Sonnetでありがちなミスはいきなり書き始めることです。学習データ・又はこれまでのコンテキスト(「Go はだいたいこう書く」「このライブラリのAPIはこうだった」)を頼りに、目の前のリポジトリを確認せずに手を動かしてしまうということがあります。

そこでまずは番号付きの手順を書くことで書く前の行動を強制します。例えば・・

言語機能を使う前に go.mod(のバージョン)を読む
ライブラリのドキュメントを信じる前に、ロックファイルで実際に入っているバージョンを読む
パターンを選ぶ前に、隣接するファイルを2つ読む
ヘルパーを書く前に grep する(たいてい別名で既に存在している)

要は、記憶で判断するな、リポジトリを読んで判断しろということをあらかじめ書いておきます。

2. 失敗パターンとその解決策を定義

中位モデルに欠けているのは知識ではなく、誤った前提を持っていることです。もっともらしい実装を正しいという前提のもと進んでいってしまいます。まずはそこを徹底的に潰すことが重要です。

そこで、各チームごとに、固定フォーマットのよくある失敗パターンと対応策を定義しました。

症状 → ありがちな間違い → 正しい一手

を書くことで、Opus/Sonnetがその現象に直面した時にどのように修正すればいいのかがわかるという仕組みです。

例えばccteamsのNext.js playbookでは以下のように書いています。

ボタン1個に onClick が必要 → ページ先頭に "use client" → ボタンだけを葉コンポーネントに切り出し、そのファイルだけをクライアントにマークし、ページは Server Component のまま保て。

Go playbookでは以下のように書いています。

ハンドラーの中でエラーが起きたhttp.Error(w, msg, 500) を呼んでそのまま処理を続ける → 直後に return を足せ。http.Error は書き込んで戻るが、あなたのハンドラー自体は戻っていない。下にある成功時の処理が、確定済みの 500 レスポンスに2つ目のボディを書き込んでしまう。

これをccteamsの各チーム(goチーム、react-nativeチーム、railsチームなどなど)ごとに10〜15件定義しています。誤った前提を明示的に名指しすることで、モデルがそのミスを犯そうとする瞬間に自己認識できるようになっています。知識ベースではなく、意思決定ポイントにパッチを当てているわけです。

3. 意思決定の分岐を定義

フロンティアモデルの優位は判断力です。判断力そのものは移植できませんが、繰り返し発生する8〜9割は明示的な分岐に落とし込めます。例えばGo playbookでは以下のように書いています。

goroutine を起動する前の3つの質問 — 「誰もいない」という答えは禁止:

  1. 誰がそれをキャンセルするのか?
  2. 誰がそれを待つのか?
  3. そのエラーはどこへ行くのか?

どれかの答えが「誰もいない」なら、起動するな。

なぜこれを定義しているのかというと、goroutineは起動は一発で簡単なのに、後始末が難しいからです。中位モデルがgo doWork()と気軽に書くと、典型的に次が起きます:

  • 誰も止めない → プログラムが終わっても走り続ける、または永遠に残る(goroutine leak = メモリリーク)
  • 誰も待たない → main が先に終わって、その処理が完了しないまま終了する
  • エラーが行き場を失う → goroutine内で起きた失敗が握りつぶされ、誰にも気づかれない

これはVibeコーディングでGoを書くときに起こりがちな問題なので、Goチーム専用に定義しています。

また、全チーム共通で

コードに触る前に仮説を明示せよ。根本原因を確定していない修正は、当てずっぽうだ

ということを書いています。これにより、中位モデルがやりがちな、根本原因が確定していないのに、それらしい修正を当てるという問題をブロックすることができます。

デバッグに関しては「症状の場所を直して直った気になる」事故が頻発するので、debugチームでは専用の分岐をおいています。

修正を書く前に、「X が Y を引き起こす。なぜなら Z だから」と言えなければならない — X と Y は観測済みで、Z がこの症状がまさにこう出る理由を説明していること。Z が埋められないなら、それは因果ではなく相関である。

つまり、修正コードを書く前に「X が Y を引き起こす、なぜなら Z」という因果を言えなければ、fix のフェーズに進めずに、仮説の再検証に戻されます。

このようにccteamsでは各チームごとに起こりがちな問題に応じてそれを止めるための意思決定の分岐を明文化しています

4. 検証方法を定義する

検証は生成と比べると比較的安いので、中位モデル+強い検証ハーネスは、中位モデル単体に大差で勝ちます。だから各playbookには、正確な順序付きの検証レシピを書いています。

「テストしてね」ではなく、どのコマンドを、どの順で、各失敗が何を意味するかまで書いています。例えばGo なら go build ./...go vet ./...go test -race ./...(-race は妥協不可と明記)。フロントエンドの playbook には、linterだけのテストではなく、キーボードだけの操作ウォークスルーと、320/768/1280px でのリサイズテストを含むようにしています。

これに加えて、実行を根拠とするように共通のworking methodで書いています。
中位モデルの典型的なサボりなのですが、しばしば「これで動くはず」と言って、一度も実行していないコードを返してくることがあります。

このようなことが起きないように、

diff は主張であり、実行が証拠である。実際にビルド・テスト・lint を走らせて、その出力を見て初めて、その主張が裏付けられる。

ということを書いています。

このルールを強制するために、実際に実行した証拠としてレポート内のすべての主張には VERIFIED(実行した)/ REASONED(コードを読んだ)/ ASSUMED(未確認)のラベルを義務付け、無断の昇格を禁止しています。

5. 学習ループで失敗を1件ずつ吸収する

上記まではFable が先読みで書いたものです。「中位モデルはたぶんここで間違える」という予測に基づくルールです。悪くはないのですが、所詮は予測です。プロジェクトで実際に起きたミスに基づいてルールを強化できる方がもちろん良いです。

そこで、working method に学習ループを組み込みました。これにより、使えば使うほどエージェントチームがプロジェクトに応じて最適化されていく仕組みができます。
書いているのはシンプルで、playbook が想定していなかったミスに出くわしたら、それをカタログエントリ化して人間の承認後に追加ルールとして追加するようにしています。

ccteamsでは、v0.2.2以降のインストール後ccteams useを実行すると.claude/skills/team-lessons/SKILL.mdが自動で作成されるようになりました。このファイルは一度作成されたらその後ccteamsをアップデートしても上書きされることはありません。これはプロジェクト固有の失敗パターンとその解決策をまとめるために作っています。

想定していないミスがあった場合は、それに関して症状 → ありがちな間違い → 正しい一手をAIが確定した上で、ここに書くべきか人間に承認を求めるようになっています。承認したらエージェントがteam-lessons/SKILL.mdにエントリを追記します。

これにより、失敗を重ねれば重ねるほどどんどん賢くなっていく仕組みを実現しています。

とはいえ、プロジェクト開発が長くなれば肥大化は避けれないので、一次対策として重複チェックと承認ゲートは入れていますが、長期運用は今後の課題だと思っています。

それをサブエージェントに確実に届ける設定

Claude Code のスキルはオンデマンドです。つまり、モデルが「関連がある」と判断したときに呼ばれます。逆に言うと上記の働き方を定義したスキルは関連がない限りは使われません。また、実際に手を動かすサブエージェントは、スキルを自動では一切呼びません。

これを解決するために、二つの仕込みをしました。

1. エージェント定義に埋める

全エージェントのシステムプロンプトがFIRST ACTION: .claude/skills/<team>-playbook/SKILL.md を読み、従えで始まるようにしました。「読め」とは言っていますが、「読め」と書くだけだと従われないことがあるので、フォールバックとしてplaybook の最重要ルール5〜6行をシステムプロンプト自体にも埋め込んでいます。

2. オーケストレーターからサブエージェントを呼ぶときに埋める

また、オーケストレーションルールで、オーケストレーターに共有working-methodスキルの圧縮版を全委譲プロンプトへ逐語コピーせよと義務付けています。

そして両方がすり抜けた場合の最後の砦として、オーケストレーターはレポートを名前付きゲートに照らして差し戻すように定義しました。「build の出力が引用されていない? 差し戻し」と言う感じです。届ける仕組みが最悪失敗しても、規律に従っていない成果物は受理されません。

全文をあちこちにベタ貼りすると、その分命令は増え1本あたりの遵守率が下がるので、常時ロードは5〜6行とし、エージェントあたりは約10行、そして必要な際(オンデマンドで呼ばれる時)は約200行を読み込むようにしています。

とはいえ設定するの大変ですよね?

私が開発したccteamsではすでにFable 5の「働き方」をOpus/Sonnetに引き継がせることに成功しました。(v0.2.x以降)もし手っ取り早くとにかく良いチームを使いたい、雛形が知りたいと言う方はぜひ使ってみてください。

npm install -g ccteams
ccteams list
ccteams use go-api   # next-ts, python-fastapi, rails, django, debug などお好きなチームを選んでください
# Claude Code を再起動

ちなみにチーム内でも、役割でモデルを分けています。OpusとSonnet間でももちろん差はあるので、判断が必要な席(スコープ切り・設計・レビュー)は Opus、決まった手続きに従う実装・シッピングの席はSonnetに設定するようにしています。playbookが実装の規律を支えてくれる分、実行席は Sonnetで十分速く・安く回り、規律だけでは埋まらない「判断」が必要な席にだけ Opusを置く、という配分です。オーケストレーターと作業者を分けたのと同じ原理を、チームの内側でもう一段繰り返している形ですね。

もしこの設定で少しでも手間が省けましたら、リポジトリにスターをいただけると励みになります。

また、issueや使ってみた感想なども大歓迎です!

あとがき

最初はオーケストレーターとしてのルールだけを決めようと思ったのですが、弱くなるのがオーケストレーター自身のためそれは厳しそうでした。
Fableの場合は、サブエージェントが書いたミスをレポート段階で高精度に捕まえ、差し戻すことが可能だったんですが、この部分に関してはどれだけオーケストレーターとしてルールを定義してもFableと同じ動きにはなりませんでした。

また、予防よりも差し戻しの方がコストがかかるので、ミスを予防するということに重きを置いて、チーム全体の底上げを考えました。

実際にリバースエンジニアリングをして検証しましたが結果は満足できるものでした。今までOpusでは検出できなかったバグを検出してくれるようになりました。定型的な作業に関してはこの設定でFableとその他モデルとの差はほとんどなくなったと思います。

とはいえ、Fable独自の強みである予期できない場合の対応には対応できません。チェックリストのどこにも書いていないのに「何かおかしい」と気づくこと、まったく新しい問題の構造を一目で掴むことというのは、どうしてもスキルに書く程度のことでは埋まりません。

その差分を、上述の学習ループが失敗1件ずつ削っていくようになっています。

Opus/SonnetをFable 5化してみるアイデア自体は良く見るのですが、詳細まで書いている記事は少なかったので実際に行って書いてみました。
少しでも参考になれば嬉しいです。

Discussion