開発中の家計簿サービスmoneruにAIチャットを載せる設計を詰める中で分かったのは、「Agents SDK=高い・怖い」は誤解で、本当に見るべきはモデルのトークン単価だけだった、ということです。固定2段階+MCPの構成なら、そもそもSDKへの移行すら要りませんでした。
想定読者は、Cloudflare Workersで自社サービスにAIチャットを組み込もうとしている個人開発者や小規模チーム。特に「Durable Objectsの高額請求」の噂に怯えて、構成の選択肢を狭めてしまっている人です。この記事では、恐怖の対象を1つずつ計算で潰していきます。原価の積み方と月額プランの逆算まで書くので、値付けの参考にもなるはずです。
いまの構成:固定2段階のチャットをWorkers AIバインディングで
前提として、moneruのAIチャットは「エージェント」と呼ぶには単純な、固定2段階の構成です。
- モデルにツール選択を依頼する(非ストリーミング。どのデータ取得ツールを使うか決めるだけ)
- Workerがツールを1件実行し、結果を添えて回答をストリーミング生成する
実装はWorker内の c.env.AI.run(...) をWorkers AIバインディングで呼び、gateway.id を指定してAI Gateway経由にしています。外部REST APIは呼ばず、Agents SDKも未使用。ループも自律判断もない、いわば「決め打ちのツール呼び出し」です。「AIチャット」と聞くと自律的に何段も考えるエージェントを想像しがちですが、家計簿の質問はツール1回で答えが出るものがほとんど。段数を固定できる題材なら、固定した方が挙動もコストも読めます。
ここに機能を足したくなりました。候補はスキル(SKILL.md方式)とMCP。そしてどうせ足すならAgents SDKへ移行すべきか、という検討が始まりました。
「Agents SDKにするとバインディングが使えない」は逆だった
最初の思い込みがこれでした。Agents SDKに移行したら、Workers AIバインディングやAI Gatewayの経路を捨てて外部API呼び出しに変わるのでは、という不安です。
調べると逆でした。Cloudflare Agents SDKの Agent クラスはDurable Objectの上で動くWorkerなので、env.AI も env.DB(D1)も普通に触れます。createWorkersAI({ binding: this.env.AI, gateway: { id } }) と書けば、AI Gateway経由の呼び出しも現状と同一。モデルへの呼び出し経路は一切変わりません。
変わるのは「ループと状態管理をSDKに任せるかどうか」だけ。つまりAgents SDK移行は経路の交換ではなく、制御の委譲です。移行してもしなくても、請求の構造はほぼ同じということになります。
スキルのトークン消費は「仕組み」ではなく「本文の量」
次の思い込みは「スキルはトークン消費が激しい」でした。これも半分誤解で、消費しているのはスキルという仕組みではなく、プロンプトに入れた本文の量です。
- スキル一覧(名前+1行説明)は数十〜数百トークン。SKILL.mdの本文は要求されたときだけ読み込まれ、そこで数千トークン
- MCPのツール定義は毎回のリクエストに全ツール分のスキーマが乗る。10ツールあれば常時1〜2千トークン
つまりprogressive disclosure(一覧は1行だけ、本文は要求時だけ読み込む)を守るなら、常時コストはスキルの方が軽いことが多い。「スキル=重い」と一括りにして選択肢から外すのは、計算する場所を間違えています。
Durable Objectsの請求を実際に計算する
本丸の恐怖、Durable Objectsの課金です。噂ではなく単価表で計算します。執筆時点のCloudflare公式の料金ページによると、Workers Paidプランには月100万リクエストとduration 40万GB秒が含まれ、超過分はリクエスト100万件あたり$0.15、100万GB秒あたり$12.50です。durationはメモリ128MB固定換算で、WebSocketなどが「ハイバネート可能」な状態が10秒続くと課金が止まります。
Cloudflare公式: Durable Objects PricingDurable Objects compute and storage billing, including pricing examples and free tier limits.developers.cloudflare.comチャット1回に当てはめて、計算過程をそのまま置きます。
- 1チャットの占有時間: 回答の生成に10秒、その後ハイバネートに入るまで10秒と置いて、計20秒
- durationは128MB=0.125GB固定換算なので、20秒 × 0.125GB = 2.5GB秒/チャット
- 込み枠はduration 40万GB秒/月。40万 ÷ 2.5 = 月16万チャットまで無料枠内
- 無料枠を超えても、2.5GB秒 × $12.50/100万GB秒 ≒ $0.00003。リクエスト課金($0.15/100万件)は1件あたりでは桁が小さすぎて効かず、合わせて1チャット約0.003円
月額1,000円のプランを設計している身からすると、これは誤差というほかありません。仮に見積もりが甘くて占有時間が2倍になっても、結論が動かない桁差です。
単価は変わりうるので、採用判断の前に公式ページで最新値を確認してください。ただ、桁が2つ3つ動かない限り結論は変わらない計算です。
高額請求の事故は3パターンに決まっている
では世に聞く「DOの高額請求」は何なのか。事故のパターンは決まっています。
- Hibernation APIを使わないWebSocketの繋ぎっぱなし。接続数分のdurationが24時間課金され続ける
- alarmの無限ループ。処理失敗→再スケジュール→また失敗、の永久機関
- ループ内でのストレージput乱打。書き込み課金が積み上がる
moneruではこう防いでいます。WebSocketは使わずHTTPストリーミングのみ。alarmは不使用。エージェント的なループを将来入れる場合もmaxStepsを小さく固定。そのうえでCloudflareの通知機能で使用量アラートを設定しています。Cloudflareには支出のハードキャップがないので、このアラートが実質の防衛線です。
裏を返すと、HTTPリクエスト処理のときだけDOが起きる使い方なら、事故の入口が構造的にありません。
請求の本体はモデル代——Llamaで回らずKimi K2系へ
DOが誤差なら、何が請求の本体か。モデルのトークン代です。ここは試算の過程をそのまま書きます。
最初はLlama 3.3 70BクラスでOK、1チャット0.3〜0.5円と見積もっていました。ただ家計簿の文脈理解とツール選択の品質が実用に届かず、「Llamaじゃ回らん」と却下。Kimi K2系へ切り替えました。
単価の話は、試算の仮定を明示する書き方にします。モデルのバージョン表記も価格も動きが速く、Workers AI経由で使う場合の単価は未確認だからです。ここではK2系の中位クラスを想定し、100万トークンあたり入力$1前後/出力$3〜4と仮置きします。実際に採用するときは、Moonshot公式の料金ページとWorkers AIのモデルカタログで最新値を確認してください。この記事でモデル名を「K2系」と幅を持たせているのも同じ理由です。
ここで効いてくるのがthinkingトークンです。思考トレースも出力側のトークンとして課金される前提で、その量が可視回答の2〜5倍になると置くと、1チャットの原価は1.5〜3円の見積もりになりました。DOの約0.003円と比べれば、どちらを設計対象にすべきかは明らかです。
原価を下げる2つの手とプラン逆算
トークン代が本体だと分かれば、削る場所も決まります。moneruで採った手は2つ。
- thinkingを切る・短くする。家計簿の質問に深い推論は要りません。特にステップ1のツール選択は分類タスクなので、thinkingなしで十分。ステップ1だけ軽量モデル、ステップ2だけKimi上位、という分割も有効です
- プロンプトの先頭を固定してキャッシュに載せる。多くのLLM APIは、リクエスト先頭が前回と一致した分をキャッシュ扱いにして入力単価を割り引きます。システムプロンプトとツール定義を先頭に固定配置しておけば、どのモデルを採用してもこの割引を取りにいける構造になります。割引率はモデルごとに公式料金表で確認してください
そのうえでプランの逆算です。月額1,000円と置き、決済手数料を3.6%(約36円)と仮定すると、手元に残るのは約960円。1チャット2〜3円なら、月300チャット使われても原価600〜900円で赤字にはなりません。実際の平均は50〜100チャットと見ているので原価150〜300円、粗利7割前後。「トークン枠を原価1,000円分に設定すれば、使い切る人でトントン、平均では粗利が残る」という設計に着地しました。原価はすべて試算で実測ではありませんが、逆算の型はそのまま使えるはずです。
表はモデルに書かせない——設計の副産物
コストと品質の両方に効いた判断をもう1つ。moneruではモデルにMarkdownテーブルを書かせません。ツールの実行結果は構造化JSONとしてストリームの先頭に tool イベントで流し、フロントのReactコンポーネント(renderersマップ)が描画します。数字がモデルを経由しないので改ざんがゼロ、表を出力しないのでトークンも減る。メッセージは Part[](text | tool)の配列で持ち、履歴はPartごとD1に保存します。テキストだけ保存すると、再表示のときに表が消えるからです。
「toolイベントが先頭に来るのは確定なのか」という疑問には、確定と答えられます。モデルの都合ではなくWorkerが順番を決めているからです。ステップ1は非ストリーミングで完了を待ち、ツールを実行してからストリームを開始するので、開始時点で結果は手元にあります。ツール未選択の雑談だけはtextから始まるため、フロントは「toolは来ないかもしれない」前提で書きます。
UIの主役も自由入力ではなく定型質問ボタン(「先月と比べて増えたのは?」など)にしました。「ChatGPTにCSVを貼ればよくない?」と比較する層はそもそも想定ユーザーではなく、自分でそれをやらない人に届けるサービスだからです。定型ボタンはツール選択が安定し、トークンも減る。一石三鳥でした。
MCPの実装にも小技があります。自前MCPサーバーが同じWorker内にあるなら、HTTPを経由せずツール実装関数を直接importして呼ぶ。「定義は1つ、出口は2つ(MCP公開+チャット直呼び)」の構造にしておくと、MCPにツールを足した瞬間、チャットからも同じツールが使えます。
恐怖の対象を正しく選ぶ
結論として、moneruはAgents SDKへ移行せず、MCPのみ追加・現行の固定2段階を維持と決めました。理由は「SDKが高いから」ではありません。DOの課金は計算すると誤差で、移行してもしなくても請求の本体はモデルのトークン代。それなら制御の複雑さを増やす理由が今はない、という判断です。
「Durable Objectsは怖い」で思考を止めると、単価表を見れば消える恐怖のために構成を歪めることになります。怖がるべきはthinkingトークンの膨張と、キャッシュに載らないプロンプト。恐怖の対象を正しく選ぶこと自体が、コスト設計の第一歩でした。
同じmoneruを題材に、MCPとWebMCPの役割分担を検証した記事もあります。データ取得の経路設計はこちらでどうぞ。
WebMCPとは?llms.txt・リモートMCPとの違いを自分のサービスで実験したWebMCPとllms.txt・リモートMCPの役割の違いを整理します。自分の家計簿サービスでMCPを切断して生JSでのデータ取得を試し、Chromeのフラグを有効化してWebMCPツールを実行するまでの検証手順と、「データはMCP、画面の介助はWebMCP」という役割分担の判断軸をまとめます。www.tentspace.net




