2026 年 3 月、運営している癒し系の壁紙アプリに組み込んだ Claude API のチャット機能で、ある夜から input_tokens の請求が静かに膨らみ始めました。会話の品質には何の問題もなく、ユーザーレビューはむしろ好評です。だからこそ、私はそのまま 10 日ほど放置してしまいました。
10 日目に Anthropic Console を開いて気づいたのは、messages 配列ではなく system と tools 側のトークンが想定の倍近く膨らんでいた、という事実でした。チャットの履歴を圧縮する仕組みは前月に組み込んでいたのですが、コンテキストウィンドウ全体を「無制限の置き場」として扱っていたため、別の入り口からトークンが流れ込んでいたのです。
2014 年から個人でアプリを作り続けて累計 5,000 万ダウンロードに到達するあいだ、サーバー費用で痛い目を見たことは何度かあります。けれども、Claude API の場合は履歴だけを見ていると見落とすコスト経路が複数あります。今回は、200K コンテキストを「予算」として 5 つの枠に切り分け、それぞれに静的・動的なアロケーションポリシーを適用する設計の実装記録を残します。Cloudflare Workers + TypeScript で動かしている本番版から再構成しました。
200K は「ウィンドウ」ではなく「予算」
Sonnet 4.6 や Opus 4.6 の 200,000 トークンというサイズは、初見では十分すぎるほど大きく感じます。しかし本番運用では、ここに 5 つの異なる需要が同時に流れ込みます。
- system プロンプト(指示・ペルソナ・ガードレール)
- tools 定義(JSON Schema・description・parameter)
- memory 層(過去エピソード・エンティティ・ユーザー嗜好の検索結果)
- conversation history(直近の
messages 配列)
- answer headroom(モデルが出力に使う
max_tokens 分の確保)
私が壁紙アプリで犯した失敗は、5 つを区別せず「全部 200K に収まればよい」と考えていたことです。実際には、ツール定義だけで 18,000 トークンを毎リクエスト送り続け、そこに RAG 由来のメモリ層が平均 22,000 トークン、履歴が平均 9,000 トークン、システムプロンプトが 4,800 トークン入っていました。残りの 145,000 はほぼ未使用です。問題は「使い切れていない」ことではなく、「予算枠を意識していないので、どこから削るべきかの判断軸がない」ことでした。
5 カテゴリの「枠」を最初に決める
私が現在運用しているのは、静的予算をベースに動的に再配分する 2 段構えの方式です。Sonnet 4.6 / Opus 4.6 を前提に、200K のうち回答ヘッドルームを最大 8,000 トークン引いた 192K をリクエスト側の予算とみなします。
カテゴリ別の枠配分は次のとおりです。
- system: 静的 5,000 / 上限 6,000 — ペルソナ・ガードレール・出力フォーマット指示
- tools: 静的 15,000 / 上限 22,000 — JSON Schema を含む全ツール定義
- memory: 静的 30,000 / 上限 60,000 — RAG・エンティティ・嗜好の検索結果
- history: 静的 40,000 / 上限 80,000 — 直近 N ターンの会話
- reserve: 24,000 固定 — フェイルセーフ用のバッファ
「上限」は他の枠が削れている場合にだけ広げてよい値で、reserve は絶対に触りません。これは、ある日突然ツール定義に MCP サーバーが追加されたり、検索ヒット数が想定より多かったりした際に、ヘッドルームを食いつぶさないための安全弁です。
実装:BudgetAllocator クラス
TypeScript で書いた実装の骨子をそのまま載せます。Cloudflare Workers でも Node でも動きます。
type Category = "system" | "tools" | "memory" | "history" | "reserve";
interface BudgetPolicy {
base: Record<Category, number>;
max: Record<Category, number>;
totalBudget: number; // 192_000 for Sonnet 4.6
}
const DEFAULT_POLICY: BudgetPolicy = {
base: { system: 5000, tools: 15000, memory: 30000, history: 40000, reserve: 24000 },
max: { system: 6000, tools: 22000, memory: 60000, history: 80000, reserve: 24000 },
totalBudget: 192_000,
};
export class BudgetAllocator {
constructor(private policy: BudgetPolicy = DEFAULT_POLICY) {}
// 静的予算をそのまま返す(高速パス)
staticAllocate(): Record<Category, number> {
return { ...this.policy.base };
}
// 実コスト(calculated)と需要(requested)を見て再配分する
dynamicAllocate(
requested: Partial<Record<Category, number>>,
): Record<Category, number> {
const out = { ...this.policy.base };
const { base, max, totalBudget } = this.policy;
// 1) system と tools は確定値(最大上限まで)
for (const k of ["system", "tools"] as const) {
out[k] = Math.min(requested[k] ?? base[k], max[k]);
}
// 2) memory と history は弾力枠。残り予算から再配分
const fixed = out.system + out.tools + base.reserve;
const flexBudget = totalBudget - fixed;
const memReq = Math.min(requested.memory ?? base.memory, max.memory);
const histReq = Math.min(requested.history ?? base.history, max.history);
if (memReq + histReq <= flexBudget) {
out.memory = memReq;
out.history = histReq;
} else {
// 競合時は history を優先(直近の文脈は捨てにくい)
out.history = Math.min(histReq, flexBudget - base.memory);
out.memory = flexBudget - out.history;
}
return out;
}
}
ポイントは 2 つあります。1 つ目は system と tools を必ず先に確定させること。これらはリクエストごとにあまり変動しないので、需要側で先取りできる方が後段の判断が単純になります。2 つ目は弾力枠で競合が起きたときに history を優先する規約です。会話のコンテキストが切れるとユーザー体験が大きく落ちる一方、メモリ層は欠落しても回答品質の劣化はゆるやかでした。
ツール定義トークンの見落とし
冒頭で触れた事故の犯人はここでした。私は MCP サーバーを 3 つ繋いでいて、その総ツール定義が 18,200 トークンに達していました。Anthropic Console の Token Usage は input_tokens の合計しか出さないので、内訳を出すには自前で計測する必要があります。
import Anthropic from "@anthropic-ai/sdk";
async function measureToolTokens(client: Anthropic, tools: Anthropic.Tool[]) {
const probe = await client.messages.countTokens({
model: "claude-sonnet-4-6",
system: "probe",
tools,
messages: [{ role: "user", content: "x" }],
});
const baseline = await client.messages.countTokens({
model: "claude-sonnet-4-6",
system: "probe",
messages: [{ role: "user", content: "x" }],
});
return probe.input_tokens - baseline.input_tokens;
}
countTokens をペアで呼び、ツール定義の差分を計算します。私のケースでは Slack 用 MCP の description が冗長で、1 ツールあたり 700〜1,200 トークン消費していました。description を要点だけに刈り込み、parameter の enum を実際に使うものだけに絞ったところ、定義総量が 18,200 → 9,400 トークンまで落ちました。これだけで月額 API 費用の 23% を回収できています。
メモリ層を 2 モデルに分業させる
メモリ枠 30,000 トークンの中身は均質ではありません。私の壁紙アプリでは、ユーザーの過去発話・抽出済みエンティティ・嗜好タグの 3 種を入れていますが、それぞれに必要な「精度」が違います。
- 直近 5 セッション分のサマリ(中精度・要素抽出)— Haiku 4.5 で要約
- ユーザー嗜好タグ(高精度・構造化済み)— KV から直接取得(モデル不要)
- 長期エピソード(低精度・主題保持のみ)— Sonnet 4.6 で抽象化
Haiku 4.5 を中精度層に当てたのは、Sonnet 4.6 と比較して入力トークン単価が約 1/4 で、要約タスクなら品質劣化が体感できないレベルだったからです。一方、長期エピソードの抽象化は意味の取りこぼしが致命的になるので Sonnet 4.6 を使い、prompt_caching で再計算コストを抑えています。
この分業に切り替えた結果、月額 API 費用は ¥86,000 から ¥38,000 まで下がりました。DAU は変動なし、Day7 リテンションは +1.8% で、品質劣化の兆候は出ていません。
つまずいたところと回避策
実運用で踏んだ落とし穴を 3 つ挙げます。どれも本番デプロイ後にしか気づけませんでした。
(1) prompt_caching のヒット率が予算配分の前提を狂わせる
system と tools をキャッシュさせる前提で枠を切っていたのに、キャッシュ TTL が 5 分しか持たないため低頻度ユーザーで毎回ミスしていました。cache_creation_input_tokens を別カウントしないと請求書で初めて気づきます。私は 1 時間 TTL のキャッシュを併用するように切り替え、キャッシュヒット率を 41% → 78% まで上げました。
(2) tools 定義が動的に増減すると履歴の整合性が壊れる
過去ターンで使ったツールが現リクエストで欠落していると、Claude が tool_use を参照したアシスタントターンが宙に浮きます。私はツール集合を「セッション内で固定」する規約に変えて回避しました。本番運用では、ツール削減はセッション境界でのみ行います。
(3) max_tokens を 8,000 で固定するとレスポンス品質が落ちるケースがある
長い説明を期待されるシーンで max_tokens を絞り過ぎると、回答の末尾が切れます。ヘッドルームは予算化しつつ、max_tokens 自体は 16,000 まで動的に上げる仕組みを併用しています。reserve から借りる形にし、借りた分は history を縮めて返済します。
状況別の推奨アロケーション
固定値の正解はありません。3 つの典型ワークロード別に、私が現場で採用している配分をお勧めします。
シナリオ A:短い問い合わせ中心のサポート bot(DAU 200〜500)
- system 4,000 / tools 6,000 / memory 10,000 / history 20,000 / reserve 152,000
- ツールは少数で固定、履歴の保持を最優先。reserve を厚めにして突発的な長文回答に備える配分を推奨します。
シナリオ B:マルチエージェント・コーディング支援
- system 8,000 / tools 25,000 / memory 50,000 / history 70,000 / reserve 39,000
- ツール定義が重く、コードベース検索結果が memory に流れ込む。
prompt_caching を必ず有効化する設計を推奨します。
シナリオ C:パーソナライズチャット(壁紙アプリの実例)
- system 5,000 / tools 9,000 / memory 30,000 / history 40,000 / reserve 108,000
- memory のうち 18,000 を Haiku 4.5 要約、12,000 を KV からの構造化嗜好に当てる場合に採用する配分です。
どのシナリオでも、reserve を最後の砦として動かさないことが共通の原則です。これを削ると、予期せぬ tool 追加や検索ヒット過多の日にコンテキストオーバーフローを起こします。
個人開発者の現実的な落としどころ
私が運営しているアプリは、AdMob と少額の課金で回っている小さな経済圏です。API 費用が読めないと粗利率が一気に揺らぎます。一方で、コンテキスト枠を縮め過ぎると体験が劣化し、レビューが落ち、結果的に AdMob の eCPM も下がります。
そのバランスを取るために、私は「予算は静的に決め、需要は動的に再配分する」という二段構えを定着させました。アーティストとして 17 の国際芸術賞をいただいてきた感覚に近いものがあって、制作前に「ここまでは絶対に守る」と決めた骨格があるからこそ、現場での即興判断が暴走しないのだと思います。コードでもそれは変わりません。
明日からの 1 アクションとしては、自分のアプリの system と tools のトークン量を countTokens で測ってみることをお勧めします。多くの場合、削れる余地は履歴ではなくここに眠っています。お読みいただきありがとうございました。