肝心なのは、reserve を「ネットワークに出る直前の関所」にすることです。Workers が投げる Too many subrequests は処理のどこで発生するか読みにくく、後始末も難しい。自分の予算で先に止めれば、どのフェーズで枯渇したかが state に残り、ユーザーには「いまは要求が複雑すぎたので、条件を絞って再度お試しください」という分かりやすい応答を返せます。沈黙して 500 を返すより、はるかに運用しやすくなります。
予算の初期値は、フレームワークが裏で使う分を引いた「自分が使ってよい枠」にします。私は Standard プラン(上限 1,000)でも、フレームワーク予約を 200 とみなして実効 800 を初期値に置いています。Free の 50 で Function Calling を多用するのは、早晩壁に当たるので本番では避けています。
ツール連鎖は深さではなく予算で止める
Google AI SDK にはツールループの組み込み制御が無いので、ループは自前で書きます。よくある実装は「最大ループ数」で打ち切る形ですが、私は予算と連動させるほうを好みます。連鎖の途中で外部 API も叩くため、回数より残量で見るほうが実態に合うからです。
async function runToolLoop( ai: GoogleGenAI, budget: SubrequestBudget, initialMessages: Content[], tools: Tool[],): Promise<string> { let messages = [...initialMessages]; // モデル1往復 + ツール実行ぶんの余白を残せる間だけ続ける。 while (budget.remaining >= 3) { if (!budget.reserve(1)) break; // モデル呼び出しぶんを確保 const response = await ai.models.generateContent({ model: "gemini-3.1-pro", contents: messages, config: { tools }, }); const calls = response.functionCalls ?? []; if (calls.length === 0) return response.text ?? ""; for (const call of calls) { if (!budget.reserve(1)) { // ツール実行ぶんの残量が無い → ここまでの文脈で締める return summarizePartial(messages); } const result = await executeTool(call); // 外部 API 1件を消費 messages.push({ role: "user", parts: [{ functionResponse: { name: call.name, response: result } }], }); } } return summarizePartial(messages);}
while (budget.remaining >= 3) の「3」は、モデル1往復とツール1件、締めの1件を残すための安全余白です。残量で止めると、深い連鎖でも浅い連鎖でも同じ予算観で扱え、上限の手前で確実に着地できます。打ち切り時に例外を投げず summarizePartial で「ここまで分かったこと」を返す設計にしておくと、ユーザー体験の劣化が一段なだらかになります。