エージェントループによるAPI予算の浪費を防ぐ
具体的なエンジニアリング施策、比較表、実践的な例を盛り込み、記事を直接執筆します。実際のループ動作とコスト管理に即した内容にし、最後には実行に移せる要点をまとめます。## API予算を最速で溶かす方法
私はかつて、「単純な」コーディングエージェントが、400トークンのユーザーリクエストを、わずか二分足らずで28,000トークンの請求項目に変えてしまうのを目にしました。
そのパターンは、嫌になるほど見覚えのあるものでした。エージェントは検索ツールを呼び出し、結果を要約し、自分の要約を読み直し、表現を少し変えて同じツールに再び問い合わせました。その後もコンテキストを拡張し続け、各ターンに会話全体、リポジトリの半分、失敗した計画の履歴が含まれるようになりました。モデルに問題があったわけではありません。ループの設計が間違っていたのです。
これがエージェント型システムの根本的な問題です。コストはユーザーのプロンプトに比例するのではなく、次のものに比例します。 ループの動作。たった一つの不適切なループが、次のものを何倍にも増やしかねません。
- 増え続けるコンテキストによる入力トークン、
- 冗長な「思考」による出力トークン、
- 呼び出しの繰り返しによるツールの利用コスト、
- そして、逐次的な再試行による遅延です。
本番環境でエージェントを構築するなら、本当に取り組むべきことは「モデルをより賢くする」ことではありません。「ループをより安く、短く、暴走しにくくする」ことです。
エージェントのループがコストを急増させる理由
通常のチャットのターンには、入力と出力がそれぞれ一つあります。エージェントのループには、次のような流れがあります。
- ユーザーが質問する、
- モデルが計画を立てる、
- モデルがツールを呼び出す、
- ツールがデータを返す、
- モデルがデータを読む、
- モデルが次のステップを決める、
- これを繰り返す。
反復のたびに、次の反復のプロンプトに新たなトークンが追加されます。ツールの出力全文や過去の思考を追加し続けると、プロンプトはおおむね階段状に増えていきます。
実際には、これによってコストを増幅する三つの要因が生まれます。
- コンテキストの蓄積:各ステップが、それ以前のステップを引き継ぎます。
- ツール呼び出しの多発:引数を少しずつ変えて、同じツールが呼び出されます。
- 再試行の暴走:すでに十分な作業を終えているのに、モデルが「もう一つだけ試す」ことを続けます。
よくある落とし穴は、実行ごとのトレースを確認するまで、トークンの浪費がたいてい見えないことです。最終回答は簡潔でも、見えない処理の連鎖は、ユーザーに見える出力の10xにもなり得ます。
シンプルなコストモデル
ループの実行ステップ数を N とし、各ステップで次のものを扱う場合:
Cトークンの引き継がれるコンテキスト、Iトークンの再投入されるツール出力、Oトークンのモデル出力、
おおよその合計は次のようになります。
total_tokens ≈ N * (C + I + O)
これは楽観的な見積もりです。なぜなら、 C は通常、ループが続くにつれて増えていくからです。実際のエージェントでは、後半のターンほどコストが高くなることがよくあります。
本当に重要な制御手段
制御を加えられる箇所はいくつかあります。重要なのは、それらを組み合わせることです。
| 制御手段 | 削減できるもの | 最適な用途 | トレードオフ |
|---|---|---|---|
| ステップ数の上限 | ループの暴走 | ツールを使うあらゆるエージェント | 難しいタスクでは早期に停止する可能性 |
| コンテキストの削減 | 入力トークン | 長期にわたるタスク | 有用な履歴を失うリスク |
| 低コストのサブエージェント | 定型作業のコスト | 検索、抽出、分類 | 連携に伴うオーバーヘッド |
| ツール結果のキャッシュ | ツールの繰り返し利用によるコスト | 安定した情報源、繰り返されるクエリ | 情報の陳腐化とキャッシュの無効化 |
| コストの可観測性 | 予想外の請求 | 本番システム | 計測機能の実装が必要 |
| ループの停止条件 | 無限ループ、またはそれに近いループ | 信頼性の低いエージェント | 「十分によい」の定義が必要 |
最も効果的な本番構成では、これらをすべて活用しています。
1) ステップ数に厳格な上限を設ける
これは最もシンプルで、最も過小評価されている対策です。厳格な上限を設定しなければ、モデルが自力で停止することを信頼していることになります。
私は通常、次の設定から始めます。
3–5ステップを、範囲の狭いタスクに設定、6–8ステップを、中程度のタスクに設定、- それ以上にするのは、明確な理由がある場合だけです。
適切な上限は、単なる安全弁ではありません。よりよいタスク分割を促します。エージェントが四回しか行動できなければ、次のような傾向が生まれます。
- 適切なツールをより早い段階で求める、
- 探索的な寄り道を避ける、
- そして、使える回答により早く収束する。
ループ制御の例
MAX_STEPS = 5
for step in range(MAX_STEPS):
action = model.next_action(state)
if action.type == "final":
return action.final_answer
observation = run_tool(action.tool_name, action.arguments)
state = update_state(state, action, observation)
raise RuntimeError("Agent stopped after step cap")
よくある落とし穴として、ステップ数の上限は次の対象に適用すべきです。 計画全体。各サブルーチンに個別に適用するべきではありません。そうしないと、コストの急増を最上位のエージェントから入れ子の補助エージェントへ移すだけになります。
2) 毎ターン、コンテキストを削減する
ほとんどのエージェントは、過剰な履歴を引き継いでいます。過去のツール応答をすべて保持する必要はありません。特に、応答が大きく、そのうち一つの詳細しか重要でなかった場合はなおさらです。
実際に最もよい結果が得られたのは、情報を階層的に扱うメモリ方針です。
- 原文のまま保持:ユーザーのリクエスト、現在の目標、最終的な決定、未解決の制約。
- 要約:過去の推論、分岐ごとの試行、長いツール出力。
- 削除:行き詰まった仮説、重複するツール結果、古い中間草稿。
保持すべきもの
生の会話履歴を再投入するより、コンパクトな状態オブジェクトのほうが効果的です。
{
"goal": "Find the failing endpoint and propose a fix",
"constraints": [
"No schema changes",
"Must preserve backward compatibility"
],
"known_facts": [
"500s started after deploy 1842",
"Only /v1/search is affected"
],
"open_questions": [
"Is the new cache layer returning stale payloads?"
],
"step": 3
}
これは、モデルに「考えを逐一言葉にした」20ターン分の内容を入力するより、はるかに低コストです。
実用的な履歴削減パターン
次のバッファを使います。
working_set:現在のターン、ツール出力、実行中の計画。summary_memory:圧縮した履歴。archive:監査には必要になる可能性があるものの、プロンプトのコンテキストには不要な情報。
エージェントが参照するのは working_set + summary_memoryです。 working_set が大きくなりすぎたら、要約して格納する先は summary_memory.
ここでの落とし穴は、要約によって情報が少しずつ変質することです。要約処理で情報が失われると、制約が消え、エージェントが同じミスを繰り返す原因になります。私は、自由形式の要約とは別に構造化された事実を保持するようにしています。そうすれば、モデルが文章からそれらを推測する必要がなくなります。
3)軽い作業には低コストのサブエージェントを使う
すべてのステップに最高性能のモデルを使う必要はありません。
タスクが次のようなものなら、
- JSONからのフィールド抽出、
- チケットの分類、
- ファイルが変更されたかどうかの確認、
- または短い検索クエリの生成、
小型で低コストのモデルで十分な場合がよくあります。高コストのモデルは、計画、情報の統合、曖昧な状況への対応に使いましょう。
よくある役割分担
- メインエージェント:タスクについて推論し、いつ停止するかを判断します。
- サブエージェント:検索、抽出、正規化など、範囲を絞った作業を行います。
- 検証エージェント:出力の形式、完全性、明らかな誤りを確認します。
実用的なアーキテクチャ:
User
-> Orchestrator (strong model)
-> Search agent (cheap model)
-> Extract agent (cheap model)
-> Final synthesis (strong model)
これが有効な理由
メインモデルは、すべてを自分で行うのではなく、制御フローに集中できます。サブエージェントには、プロンプトで制約を設け、トークン数に厳格な上限を設定できます。
検索用サブエージェントのプロンプト例:
Return only:
1. the best search query,
2. 3 likely source categories,
3. no explanation.
このように役割を狭く定めることで、出力を短く、予測しやすく保てます。
利用しているプラットフォームで複数のモデルを手頃な価格で使えるなら、ここでその利点が生きます。高性能なモデルをオーケストレーションに、低コストのモデルを反復的なサブタスクに使えば、定型的な解析に高価なトークンを費やす必要はありません。
4)ツールの結果を徹底してキャッシュする
エージェントのループでは、同じ情報に何度もコストを払っていることがよくあります。
- ファイルの繰り返し読み取り、
- データベースへの繰り返し照会、
- Web検索の繰り返し、
- 埋め込み生成や検索の繰り返し呼び出し。
ツール出力のキャッシュは、実施できる改善の中でも特に費用対効果の高いものです。
生のテキストではなく、意味に基づくキーでキャッシュする
生のプロンプトを使うキャッシュは、表現が少し変わるだけでヒットしなくなります。正規化した意図に基づくキーのほうが適しています。
def cache_key(tool_name, args):
normalized = normalize_args(args)
return f"{tool_name}:{hash(normalized)}"
キャッシュエントリの例:
{
"tool": "repo_search",
"query": "auth middleware rate limit bug",
"result": {
"files": ["src/middleware/auth.py", "src/api/routes.py"],
"ts": 1710001123
}
}
キャッシュが特に役立つ場面
- リポジトリを調査するエージェント、
- ドキュメント検索、
- 内容が安定しているナレッジベース、
- 決定論的な変換処理、
- 複数ターンのワークフローで繰り返される検索。
キャッシュが悪影響を及ぼす場面
- 急速に変化するデータ、
- ユーザー固有の結果、
- 時間に敏感な鮮度要件があるもの。
よくある落とし穴は、ツール出力をキャッシュしても、その鮮度をエージェントに伝えないことです。キャッシュされたデータを受け取ったのに、モデルが最新のデータを取得したと思い込むと、判断を誤る可能性があります。ツールのメタデータにタイムスタンプや有効期間を含めてください。
5)ダッシュボードに支出を表示する
レイテンシと成功率だけを追跡していると、本当の問題、つまりコストの高すぎる成功を見逃します。
実行ごとに、次の項目を観測できるようにしましょう。
- 入力トークン数、
- 出力トークン数、
- ツール呼び出し数、
- ツールのレイテンシ、
- 総支出、
- ステップ数、
- 停止理由。
最小限のイベントスキーマ
{
"run_id": "r_8f2c",
"step": 4,
"model": "gpt-5.5",
"input_tokens": 1842,
"output_tokens": 221,
"tool_name": "repo_search",
"tool_latency_ms": 312,
"estimated_cost_usd": 0.087,
"stop_reason": "step_cap"
}
実際に注目すべき指標
- 成功したタスクあたりのトークン数:コストを最も明確に示す指標です。
- ステップあたりのトークン数:プロンプトの肥大化を検知します。
- ツール呼び出しの繰り返し:通常は計画処理の不具合です。
- 停止理由の分布:多くの実行が上限に達して終了しているなら、エージェントの計画が不十分です。
特に役立つグラフのひとつが、タスクの種類別のコストです。種類によっては、どうしてもコストが高くなります。それは問題ありません。見つけたいのは、本来は低コストで済むはずなのに、そうなっていないタスクです。
6)暴走するループを早めに止める
ループの停止理由は、「ステップ数の上限に達した」だけであってはいけません。
有用な停止条件には、次のようなものがあります。
- モデルが最終回答を生成した、
- 新しい情報が得られないまま、同じツール呼び出しが繰り返されている、
- 確信度または有用性の変化量がしきい値を下回っている、
- 検証エージェントがタスクは解決済みと判断した、
- 計画が停滞している。
ループを打ち切る処理の例
seen_calls = set()
for step in range(MAX_STEPS):
action = model.next_action(state)
signature = (action.tool_name, canonicalize(action.arguments))
if signature in seen_calls:
return fail("repeated tool call with no new information")
seen_calls.add(signature)
observation = run_tool(action.tool_name, action.arguments)
state = update_state(state, action, observation)
if verifier_says_done(state):
return finalize(state)
これにより、モデルが「役に立ちたい」と考え、同じクエリを言い換えるだけになるという、非常によくある問題を検知できます。
もうひとつの有用なガードレール:有用性に基づく続行判定
ステップを実行しても状態が十分に変わらないなら、続行しないでください。たとえば、次のような単純な条件でも構いません。
- 新しいファイルが見つからない、
- 新しいエンティティが抽出されない、
- 回答の確信度が向上しない、
- 新たな根拠が追加されない。
具体的な指標はタスクによって異なりますが、追加のステップによって得られる効果を何らかの形で捉えることが不可欠です。
適切に調整すると実際に何が起こるか
ループを引き締めると、エージェントは通常、最初は気づきにくい次のような点でも改善します。
- 冗長な発言が減り、判断が明確になります。
- 適切なツールをより早く要求します。
- 「まだ調査中です」というサイクルが減ります。
- コストが下がり、 と 速度も上がります。ターンが減ると、コンテキストが小さくなり、レイテンシも低下するためです。
重要なトレードオフは、厳格な上限や大幅な履歴削減によって、本当に難しいタスクに対するシステムの堅牢性が損なわれる可能性があることです。実際にはタイムアウトしているだけなのに、エージェントが問題を「解決している」かのように扱わないでください。ログやユーザー向けメッセージで、その状況を正直に伝えましょう。
私の経験則は、次を重視することです。 範囲を限定した能力 際限なく探索を続けることよりも。
実用的な本番環境の構成
明日エージェントをリリースするなら、私は次の構成から始めます。
MAX_STEPS = 5- 会話履歴全体の再送ではなく、構造化された状態
- 各ターン後の要約メモリ
- 検索や抽出を担当する低コストのサブエージェント
- 鮮度のメタデータを備えたツール結果キャッシュ
- 実行ごとのトークン数とコストの記録
- 繰り返し呼び出しの検出機能
- 明示的な停止理由
この構成は華やかではありませんが、請求額がプロダクトの成長を上回るペースで増えるのを防ぐのは、こうした仕組みです。
実践上のポイント
- まずステップ数に上限を設けましょう。暴走するループを防ぐうえで、最も低コストな対策です。
- コンテキストを積極的に削減し、構造化された状態は要約と分けて管理しましょう。
- あらゆる処理に高価なトークンを使うのではなく、範囲の狭いタスクには低コストのサブエージェントを使いましょう。
- 再利用できるツール出力をキャッシュしましょう。ただし、モデルが古いデータを無条件に信頼しないよう、鮮度を明示してください。
- 実行ごと、ステップごと、ツール呼び出しごとの支出を記録し、コストの悪化がすぐに見えるようにしましょう。
- 予算やステップ数を使い切ったときだけでなく、反復、停滞、または完了の確認を理由にループを停止しましょう。
- 範囲を限定した能力を前提に設計しましょう。十分な品質で、速く、予測可能な動作は、賢くても高コストな動作に常に勝ります。
Claude Opus 5.5、Sonnet 5、Haiku 4.5、Fable 5.1に加え、GPT-6のモデルを、単一のAPIキーで利用できます。従量課金制で、サブスクリプションは不要です。
APIキーを取得 →