powered by TechFeed
表示モード
Deep Dive

AIエージェントのコスト暴走を実行単位でリアルタイム遮断 — Microsoftが公開した「TokenOps」の設計思想と踏んだ落とし穴

8月14日、Microsoft Command Line(commandline.microsoft.com)が「TokenOps: Real-time, run-scoped cost control for AI agents」と題した記事を公開した。この記事では、AIエージェントの実行単位でトークンコストをリアルタイムに計測・制御するシステム「TokenOps」の設計と実装について詳しく紹介されている。実装コードおよびポリシー定義は**GitHub(theagentplane/tokenops)でオープンソースとして公開されており**、すぐに試せる状態になっている点も重要だ。

8月14日、Microsoft Command Line(commandline.microsoft.com)が「TokenOps: Real-time, run-scoped cost control for AI agents」と題した記事を公開した。この記事では、AIエージェントの実行単位でトークンコストをリアルタイムに計測・制御するシステム「TokenOps」の設計と実装について詳しく紹介されている。実装コードおよびポリシー定義は**GitHub(theagentplane/tokenops)でオープンソースとして公開されており**、すぐに試せる状態になっている点も重要だ。


月次バジェット上限では止められない

AIエージェントは1回のタスク実行(「ラン」)で、モデル呼び出しを何度も繰り返す。ツールの結果が不十分であれば再試行し、さらに再試行する。1回1回の呼び出しは安価でも、積み上がれば青天井だ。しかも問題が発覚するのは、月次請求書が届いてからになる。

一般的なコスト管理手段はAPIキーやチームへの月次バジェット上限だが、これは「事後の合計値」にしか作用しない。ランが暴走し終えた後でトリップする仕組みであり、リアルタイムの制御にはなっていない。記事ではこの問題を4つの前提で整理している。

  1. トークンがコストの単位である — コスト/トークンと価値/トークンの両方を追跡する
  2. コストは各呼び出し時点で発生する — 呼び出しが見えなければ制御できない
  3. 帰属なくして制御なし — 全呼び出しはランとエージェントのコンポーネントに紐付けられるべきだ
  4. 請求は遅れてくる — 過剰支出を止めるには、ラン中に動かなければならない

ゲートウェイでは足りない理由

LiteLLMPortkeyCloudflare AI Gatewayなどのゲートウェイツールは、1リクエスト=1単位として制御する。APIキーを軸に計上するため、同じランから発行された複数の呼び出しが「同じラン」に属するとは認識できない。特に複数プロセス・複数プロバイダーにまたがるマルチエージェント構成では、この限界が顕著に出る。

記事では既存ツールを2軸(「リクエスト単位 vs ラン単位」と「インパス強制 vs 帯域外観測」)で整理している。

リクエスト単位 ラン単位
インパス強制 LiteLLM / Portkey / Cloudflare AI Gateway TokenOps
帯域外観測 Langfuse

TokenOpsが狙うのは右上の「インパス+ラン単位」という空白地帯だ。


TokenOpsの核心:「決定」と「実行」を分離する

TokenOpsの設計で最も面白いのは、ポリシー評価(決定)をリクエストのクリティカルパス外で行い、実行(ラップ)だけをインパスに置く点だ。

各モデル呼び出しの前後に薄いラッパーを挟む。ラッパーは呼び出し前に共有レジャー(台帳)からランの累積消費を読み取り、ポリシーの判断を適用する。ポリシー評価自体は事前に計算済みのため、インパス処理はカウンタの読み取りと比較だけで済む。

# プロバイダークライアントを一度ラップするだけ。エージェントのコードは変更不要。
from tokenops.control.integration import wrap_complete
from tokenops.control.config import build_governor

governor = build_governor(store.governance_config_for(agent), price, ApplyControls())
complete = wrap_complete(
    governor, controls, attr,
    provider=provider, model=model,
    dispatch=provider.complete,
    service="planner",
)

def governed(p, m, messages):
    controls.begin_call()
    request = CallRequest(
        attr=attr, provider=provider, model=m,
        estimated_input_tokens=estimate(messages),
        max_output_tokens=controls.call.max_output_tokens
    )
    governor.pre_call(request)
    use_model = controls.call.model_override or m
    messages = consume_carry(controls, messages)
    governor.ledger.admit(seg)
    try:
        response = traced(p, use_model, messages,
                          max_output_tokens=controls.call.max_output_tokens)
        return response
    finally:
        governor.ledger.complete(seg)

governor.pre_call(request) が共有台帳から累積消費を読み取りSTEER/HALT判断を行う関門であり、governor.ledger.complete(seg) が実コストを台帳に書き戻す。この2ステップがインパスに置かれた全処理の核心だ。エージェントのコードを変更せずにラップできるのが実用上の利点だ。

強制アクションは2種類に集約される。

  • STEER:ランを継続させながら次の呼び出しを変更する(安いモデルへの切り替え、プロンプト短縮、ループ修正など)
  • HALT:サーキットブレーカー。以降のすべての呼び出しをオペレーターが再開するまで拒否する

STEERが先に走り、HALTは最終手段として位置づけられている。正常なランを止めることはそれ自体が障害だからだ。


実装中に踏んだバグ:分散台帳の落とし穴

記事が特に価値を持つのは、実際に踏んだバグを詳細に開示している点だ。

テストベンチでは2つのエージェント(リサーチエージェントとサマライザーエージェント)を別プロセスで動かし、同一ランIDと共通バジェットを持たせた。全テストをパスしたが、バグがあった。

各プロセスが消費カウンタをメモリ内に独立して持っていたため、$2のバジェット上限が実質$4になっていた。 リサーチ側で$2、サマライザー側でさらに$2消費できる状態だ。これは典型的な分散システムの問題で、「単一の真実の源(single source of truth)」がなかったことが原因だ。

修正は共有レジャーへの一元化。リサーチエージェントの消費が即座に共有台帳に反映されるため、サマライザー開始時点で残バジェットが正確に算出される。


10のポリシーでコストの爆発をガードする

ランのコストは以下の式で表される。

ランのコスト = Σ(入力トークン × 入力単価 + 出力トークン × 出力単価)
1ランあたりの呼び出し数 = ループ深度(連続ステップ)× ファンアウト幅(並列サブエージェント)

コストを爆発させる要因は「1呼び出しあたりのトークン数」と「呼び出し回数」の2つ。TokenOpsはこれらをガードする10のポリシーを提供する。

ポリシー ガード対象 発動条件 アクション
cost_budget 合計 バジェット上限到達 ランをHALT
pre_call_worst_case 呼び出し単位 次の呼び出しの最悪ケースが残バジェットを超える 出力キャップまたはHALT
cost_guard 呼び出し単位 消費がバジェットの約80%超過 安いモデルへSTEER(1回限り)
step_cap ループ深度 呼び出し数がラン上限を超過 HALT
progress_guard ループ深度 同じアクションと結果が繰り返される 修正をインジェクト、その後HALT
output_runaway 呼び出し単位 縮退した繰り返しストリーム キャンセル後にバウンド付き再試行
tool_fix ループ深度 不正または不明なツール呼び出し 合成エラーをインジェクト、繰り返しでHALT
context_compaction 呼び出し単位 プロンプトがコンテキスト上限に近づく 重複排除とピン止め
tool_output_cap 呼び出し単位 ツールが過大なペイロードを返す 保存してハンドルを返す
concurrency_cap ファンアウト 同時並行呼び出しが過多 キューまたは拒否

強制はすべて決定論的に動作し、ポリシー評価パスにモデルは使わない。制御対象が自分を止めるかどうかを決める構造は避けるべきという設計思想による。CaMeLがプロンプトインジェクション対策に用いたアーキテクチャと同じアプローチだ。

なお、記事では推論トークンの上限もポリシーで管理できると述べており、根拠として「自明な質問に対して推論モデルが通常モデルの約20倍のトークンを消費しながら精度は変わらなかった(Chen et al., ICML 2025)」という研究結果を引いている。


ソースコードと参照先

TokenOpsの実装・ポリシー定義・テストベンチはすべて**GitHub(theagentplane/tokenops)** で公開されている。マルチエージェント構成での共有レジャーの実装例や、各ポリシーの設定パラメータも確認できるため、自前のエージェント基盤に組み込む際の出発点として有用だ。

詳細はTokenOps: Real-time, run-scoped cost control for AI agentsを参照していただきたい。