NTTデータCCS で部門横断での技術支援を担当している riue です。普段は Codex でコードを書いています。
さて、GPT-5.6 系列を Codex で使う場合、普通に考えればモデルの使い分けは単純です。軽い仕事は Luna、普通の仕事は Terra、難しい仕事は Sol。モデルの能力に応じて仕事を振り分ける、いわゆるモデルルーティングです。
しかし、Luna と Terra には 10 倍のトークン単価差があります。ここまで価格が違うなら、「この仕事を Luna に任せても大丈夫だろうか」と真面目に分類すること自体がもったいないと思い始めました。
最近考えているのは、「とりあえず Luna に投げる。ダメだったら Terra に持っていく」という戦略です。Luna を「簡単な仕事を担当する廉価モデル」ではなく、「安いから投機的に実行できるモデル」として使います。本稿ではこれを "Luna-first" 戦略と称します。
Luna が失敗しても、たいして高くない
話を単純化して、Terra のコストを 1、Luna を 0.1 とします。
- 最初から Terra に仕事をさせればコストは 1 です
- 一方、まず Luna に仕事をさせて成功すれば 0.1 で終わります
- Luna が失敗して、その後 Terra に同じ仕事をやらせたとしても合計コストは 1.1 です
最初から Terra に任せた場合と比較して、余計に払ったコストは 0.1 にすぎません。一方、Luna が成功すれば 0.9 を節約できます。
この単純なモデルでは、Luna の成功確率が 10% を超えるだけで期待コスト上は (モデルコストだけを見れば) Luna-first が有利になります。「Luna なら確実にできそうだから Luna を使う」のではありません。「10 回に 1 回より成功しそうなら、とりあえず賭けてみる」という発想が成立します。
もちろんこれは、Luna の失敗を検出でき、失敗した試行を捨てて Terra が同じ条件からやり直せると仮定した単純なモデルです。現実には結果の検証、人間の待ち時間、失敗による影響もコストになります。それでも、10 倍という価格差は「適切なモデルを事前に選ぶ」という従来の前提を崩すには十分大きい、というのが出発点です。
「Luna に向いている仕事」を考える必要すらない
一般的なモデルルーティングでは、仕事を見てからモデルを選びます。変更が局所的なら Luna、ある程度の調査が必要なら Terra、複雑なアーキテクチャ判断なら Sol、といった具合です。
もちろんこの分類自体は合理的ですが、分類にもコストがかかります。issue を読み、既存コードを調べ、「これは Luna でいけるか」「いや Terra のほうが安全か」と人間が判断する時間は無料ではありません。だったら、その判断自体をやめてしまえばいい。Luna で解けるかどうかを考えるくらいなら、まず Luna に解かせてみるのです。
これは Luna を信頼しているからできることではありません。むしろ逆です。Luna を「失敗しても構わないほど安い計算資源」として扱っています。
Luna に向くのは「簡単な仕事」ではない
もちろん、Luna にも得意不得意はあります。長大なコンテキストから離れた情報を拾い集める仕事、不明点だらけの問題、何段階もの推論が必要な問題は、最初から Luna 向きとは言えません。逆に、直感的に分かる変更や、具体的な指示に従う仕事なら、かなり広い範囲を Luna に任せられます。この辺りは GPT-5.6 のベンチマーク結果に基づき『Luna だってもっと働きたい!! - GPT-5.6 のベンチマーク結果から考えるモデル分業論』として書きました。
Luna に与えやすいのは、規模の小さな仕事ではなく、判断の余地が小さな仕事です。従来なら、ここで人間や Terra が仕事を十分に具体化してから Luna に渡していました。しかし Luna が 1/10 なら、その具体化自体もまず Luna にやらせてみればいい。
できるかどうかの判断すら Luna にやらせる
まず Luna に要求アイテムを渡して実装計画を作らせます。たとえば次のような指示です。
対象ファイル、変更箇所、変更内容、必要なテストを具体化せよ。
確定できない事項は推測せず unresolved として列挙せよ。
ここで重要なのは、Luna に何としてでも計画を完成させるよう求めないことです。必要なファイルを特定できない、仕様上の判断が必要、複数の設計があり選択できない、といった不確実性があれば unresolved として外に出させます。unresolved が残ったなら、そこで Luna による調査は打ち切って Terra にエスカレーションします。
もちろん、弱いモデルが自分の不確実性を常に正しく認識できるとは限りません。したがって unresolved の自己申告だけを信用するのではなく、実装計画内の各項目について、どの仕様と確認済みコードを根拠にしたのかを追跡できる形にします。根拠を示せない判断は、もっともらしくても確定事項として扱いません。
また、Luna に追加調査を延々と続けさせ、リポジトリ全体を理解させるのも NG です。長大なコンテキストから離れた情報を拾い集め、暗黙の依存関係を推測する仕事は、まさに Luna にやらせたくない仕事です。Luna-first では弱いモデルに巨大な世界を理解させるのではなく、扱わせる世界そのものを狭く保つことが必要です。
その意味では、実装計画立案と実装を同じエージェントの長いセッションで続ける必要もありません。メインエージェントがリポジトリを探索して実装計画を確定したら、実装そのものはサブエージェントに渡します。サブエージェントが受け取るのは確定した計画と、そこで特定された関連ファイルだけです。探索中に蓄積した大量のコンテキストを実装まで引きずらず、一度実装計画に圧縮してから新しいエージェントに仕事を渡すわけです。
従来のモデルルーティングが、
タスク → 難易度判定 → モデル選択 → 実行
だとすれば、Luna-first は、
タスク → Luna で試行 → 問題を観測 → 必要なら Terra へエスカレーション
です。適切なモデルを事前に予測するのではなく、安いモデルから仕事を始め、問題が観測されたときだけ上位モデルへエスカレーションします。
待ち時間? 別の仕事をすればいい
もちろん、Luna が失敗したらその時間は無駄になります。しかし複数の独立したタスクを並列に進められる環境なら、タスク A を Luna に投げている間にタスク B を処理できます。タスク C の実装計画立案を走らせながらタスク D をレビューできます。複数の Codex エージェントを並列に動かせるなら、Luna の失敗によって生じる待ち時間の一部は隠蔽できます。もちろん並列化そのものにも管理コストはありますが、少なくとも「Luna が失敗するまで何もできない」と考える必要はありません。
すると Luna の失敗で失うものの多くは、0.1 Terra 分の計算資源になります。これはモデルルーティングというより、CPU 内部での投機実行に近い発想です。CPU は分岐の結果が確定するまで何もしないのではなく、「たぶんこちらだろう」と予測して先に命令を実行します。予測が当たれば高速化でき、外れれば結果を捨てます。
Luna でも似たことができます。正しいモデルを選んでから仕事を始めるのではなく、安いモデルに先に仕事をさせ、外れたら結果を捨てる。しかも Luna のコストは Terra の 1/10 です。
ただし、この比喩には重要な前提があります。
- 投機した結果が正しかったことを判定できること
- 判断が間違っていれば、その結果をコミットせず破棄できること
Luna-first にも、この二つが必要です。
実装とテストを一緒に作らせても、正しさは分からない
一つ目は、Luna の「できました」という自己申告とは別の方法で、結果の正否を判定できることです。とは言え、Luna に実装と共にテストも書かせても、それだけでは今回の要求を正しく実装したことの検証にはなりません。
たとえば、Luna が誤って要求を解釈したとします。同じ Luna がその解釈に基づいて実装と単体テストの両方を書けば、誤った実装に対して誤ったテストが通ります。結果として実装とテストは完全に整合しているのに、そもそも両方ともユーザー要求から外れている、ということが起こります。
もちろん、そのテストにも価値はあります。型チェックや lint は最低限の構造的な問題を検出できますし、既存の回帰テストは今回の変更が既存機能を壊していないかを確認できます。既に存在する OpenAPI Specification や JSON スキーマ、DB 制約、(自動検証が可能な) アーキテクチャルールなども、今回の実装とは独立した制約として強力です。
しかし、それらが保証するのは主として「既存の契約を壊していないこと」です。「今回新しく要求された振る舞いを正しく実装したこと」は、別に確認しなければなりません。
実装と同時にエージェントが作ったテストは独立した検証器ではなく、「自己一貫性のチェック (self-consistency check)」にすぎません。自分が理解した仕様と、自分が書いた実装が一致していることは確認できますが、その仕様理解自体が正しいことまでは確認できないからです。
正解を先に固定する
そこで、実装の前にもう一つ工程を置きます。ユーザー要求から、実装とは独立した「実行可能 (機械判定可能) な仕様」、たとえば Gherkin 記法で振る舞いを定義する工程です。
たとえば「管理者以外が /users/:id を DELETE した場合は 403 を返す」という要求なら、実装コードを書く前に、
Given: 一般ユーザーでログインしている
When: /users/123 に DELETE を送る
Then: HTTP 403
And: users.id=123 が DB に残っている
というように、外から観測できる合格条件を決めます。ここでいう「実行可能な仕様」は単なる自然言語の期待結果ではなく、最終的に自動テスト、API probe、DB assertion などへ機械的に落とせる粒度の合格条件を指します。
この仕様自体は Luna に作らせて構いません (ここでも Luna-first です) が、その結果は人間が確認する必要があります。「この条件をすべて満たせば今回の要求を満たしたと言ってよいか」を確認し、問題なければ承認します。要求の取り違えや重要なエッジケースの欠落があれば Luna に再検討を命じるか、諦めて Terra へエスカレーションします。
人間が保証するのは AI が書いたコードそのものではありません。「何をもって合格とするか」です。もちろん、人間が承認した仕様が絶対に正しいという意味ではありません。しかし少なくとも、エージェントが自分で問題を解釈し、自分で実装し、自分でテストを書いて、自分で「正解」と判定する循環は切れます。
仕様が承認されたらそれは固定しますが、その後の実装は捨てられるようにします。スローガン的に書けば Spec is durable, implementation must be disposable. (仕様は永続的でなければならない、実装は破棄できなければならない) という感じでしょうか。確認済みの仕様は残っていますから、Luna が実装に失敗したなら Terra にエスカレーションして再試行する。どの実装も、同じ合格条件に対して判定できます。
重要なのは、実装の失敗によって正解の定義までもが失われないことです。各エージェントに実装とテストをセットで作らせるというのは、自分で問題を作って自分で採点しているようなものです。合格条件を実装の外側に固定して初めて、実装を本当に投機的なものとして扱えます。
仕様 → 計画 → 実装 → 検証
ここまでをまとめると、Luna-first の実際の流れは次のようになります。
それぞれの工程の責務は異なります。
- 仕様立案は「何を満たせば正解か」を決めます
- 実装計画立案は「このリポジトリのどこをどう変更すれば、その正解に到達できるか」を具体化します
- 実装は計画だけに従ってコードを変更します
- そして検証は、承認済みの仕様と既存システムの制約に対して成果物を判定します
ここで、仕様は単にパイプラインを一度通過する中間成果物ではありません。後続の計画立案と実装の外側に残り続け、それらを判定する基準になります。一方で、実装は何度捨てても構いません。
Luna-first は、Luna に何でも解かせる戦略ではありません。Luna に安く試行させる一方、Luna に任せてはいけない不確実性が表面化したらそこで確実に打ち切れるようにする戦略です。
実装は破棄できなければならない
投機実行を成立させるもう一つの条件は、失敗した実装を本当に捨てられることです。
コード生成だけなら、失敗した diff を捨てれば済みます。しかしエージェントが共有 DB にマイグレーションを適用したり、外部 API に更新リクエストを送ったり、パッケージをデプロイしたりすれば、失敗した試行を単純に破棄することはできません。モデル料金が 0.1 でも、試行の副作用が高くつけば投機実行の経済性は崩れます。
したがって、投機的な実装は branch や worktree、コンテナ、一時環境など、失敗した試行をコミットせず破棄できる場所で実行する必要があります。不可逆な操作や共有環境への書き込みは、その境界の外に置きます。
「実装は破棄できなければならない」というのは、エージェントの出力に備わった性質ではありません。破棄可能な実行環境を事前に準備することで、初めて成立する性質です。
Luna-first に向くアーキテクチャ
ここまで来ると、Luna-first に向いているコードベースの性質もかなり具体的になります。
- 変更対象を局所化できること
- 結果を外から判定できること
- そして失敗した試行を捨てられること
です。
まず、変更に必要な世界を狭くできること。たとえばモノレポの packages/billing に対する変更なら、billing package と公開された依存関係だけを読めば計画を立案できる状態が望ましい。TypeScript なら ESLint の import restriction、dependency-cruiser、Nx の module boundary などで依存方向を機械的に制約できます。API 境界を OpenAPI Specification、JSON スキーマ、Protocol Buffer などで明示しておけば、エージェントが隣のモジュールの内部実装まで読んで暗黙の契約を推測する必要も減ります。
次に、変更結果を外から判定できることです。新しい要求については人間が承認した仕様、既存機能については回帰テスト、構造的な整合性については型チェックや lint、システム境界については API スキーマや DB 制約といった具合です。
pnpm --filter billing typecheck
pnpm --filter billing lint
pnpm --filter billing test
のように、変更対象だけを安く検証できればさらに都合がよいでしょう。Java なら ArchUnit のようなツールで依存関係をテストできますし、DB や外部サービスとの境界を結合テストで確認したければ Testcontainers のような仕組みも使えます。
そして三つ目が、失敗した試行を捨てられることです。変更とその副作用を隔離できれば、Luna による実装は本当に破棄可能になります。逆に、一度の失敗による影響が大きく、結果を安全に破棄できない領域では、いくらモデルが安くても Luna-first の経済性は悪くなります。
つまり Luna-first に向いているのは、局所化でき、外から判定でき、捨てられるコードベースです。
これは Luna のために特殊なアーキテクチャを用意しろ、という話ではありません。モジュール境界、型、スキーマ、契約、受入テスト、分離された環境といったものは、以前からソフトウェア工学で使われてきました。
人間なら暗黙の依存関係を経験で理解したり、コードレビューで仕様との差分を見つけたりできます。しかし安いエージェントを何度も投機的に投入するなら、その暗黙知を毎回エージェントに推論させるのではなく、機械的な境界や合格条件として外に出しておいたほうが安くなります。
巨大な既存モノリスを Luna のために全面的に再設計する必要もありません。
- 変更頻度の高い領域からパッケージローカルな検証コマンドを用意する
- 既存の依存関係を一気に整理するのではなく、新しい違反を自動化されたアーキテクチャ検証で禁止する
- 重要な API から契約を機械可読にする
といった漸進的な改善でも、Luna に安全に任せられる範囲は広がります。
安いモデルは「節約用モデル」ではない
Luna-first は、単に利用料金を節約する手段ではありません。価格が十分に安くなると、最適な戦略そのものが変わります。
3 割安い程度なら、失敗を避けるためにできるだけ適切なモデルを選ぶべきでしょう。しかし 10 倍違うなら話は別です。計画立案時に unresolved が出たら捨ててもいい。実装に失敗したら、ただ単にその仕様で Terra にエスカレーションすればいい。さらには、「Luna でいけるか」を判断する人間の時間すら節約してよい。
ただし、何もかも Luna に賭けるわけではありません。正解の定義まで破棄可能なままでは、誤った仕様解釈、誤った実装、誤ったテストが綺麗に整合してしまいます。だから人間は実装の一つ一つを監督するのではなく、何をもって正解とするかを承認します。一度そこを固定したら、その先の実装は安い計算資源で投機できます。
これはモデルルーティングではありません。投機実行です。そして投機実行を成立させるのは Luna の賢さそのものではなく、正解を実装の外側に固定し、変更対象を狭く切り出し、失敗した試行を安全に捨てられる仕組みです。
まとめ
7/31 に OpenAI が Terra / Luna の価格を値下げし、Luna のコストが Terra のコストの 1/10 になったときから漠然と「どう Luna を使い倒すか」を色々と考えていました。その中で思いついた「Luna での投機実行」というアイデアですが、実際に適用しているかというと、私自身もまだここまでは踏み込めていません。
一番の理由は、特に仕様立案のフェーズではレビューのコストが高いことが挙げられます。不明点の壁打ちの結果としてどんどん結果がずれていかないか、最終的に首尾一貫した内容になっているか、などを考えると、最初から Luna はさすがに心もとない。冒頭で「Luna が失敗しても、たいして高くない」と書きましたが、人間がレビューした結果失敗だと分かった場合は損失が大きすぎるのです。
結局のところ、これは「損失回避」というより「曖昧さ回避」という行動です。明らかな失敗の確率が分からない + 実際には失敗しているんだけど何だかそれっぽく出力されている、という状況は「本当に大丈夫なの?」という心理的不安感を増大させるというところに帰結します。そして心理的不安感が大きいとレビューもストレスになる。だったら最初からもっと信頼性の高い (高そう) なモデルでやったほうが…となるわけです。
一方で、実装計画立案以降はある程度のレベルまで機械的に検証が可能で、かつやり直しもさせやすいため、Luna-first でも進められつつあります。Luna-first かどうかにかかわらず、結果を外部から判断するための仕組み、失敗した試行を安全に捨てられる仕組みはどのモデルを使おうとも絶対に必要ですし。
補足: 同種の考え方
既に似たような考え方はないかと探してみたところ、以下のような事例が見つかりました。どちらも本質的には「安いモデルにまず仕事をさせ、高いモデルは検証あるいはエスカレーションに使う」というものです。下位モデルでもある程度の性能が担保できるようになった現在、このような考え方はより一般的になっていくのかもしれません。
RLM-Cascade: Response-Level Speculative Decoding for Cost-Efficient LLM API Serving
LLM API の運用コスト最適化を主目的とした研究です。「GPT-4 や Claude Opus は高性能だが非常に高価」「小型モデルは安いが品質が不安定」という問題に対して、大雑把に言えば
- 簡単な問題は安価な LLM (ex: DeepSeek) に直接ルーティング
- 難しい質問は安価な LLM に投げた後、その結果を高価な LLM (ex: Claude Opus) でレビュー
という仕組みを LLM の前段に設けるというものです。結論としては「安価なモデルで失敗して高価なモデルに修正させた場合だけは 15% ほど損するが、成功すれば大幅に安い」というものでした。
Speculative Cascades: Latency-Optimal Inference via Coupled Draft-Verify Routing
先行して下位モデルに出力を作成させ、上位モデルが検証するという Speculative Decoding という考え方と、上記で示したような Model Cascade は同じ枠組みなんじゃないか、という論。この二つの考え方は実際には連続しており、そこのトレードオフを導出できないか、という研究です。