月末に届く請求は、いつも1つの合計金額でした。Gemini 3.5 Flash は速くて安く、複数エージェントを並行で回しても1回あたりの費用は小さい——そう思っていました。けれど6サイトぶんのタスクをまとめて回していると、「この合計のうち、どのサイトの、どのタスクが、いくら食ったのか」がまったく分かりませんでした。安いはずの処理が、回数の多さで静かに費用の大半を占めていても、気づけない状態です。
費用を「下げる」前に、まず「どこで使っているか」を見えるようにする必要がありました。これはトークンを節約する最適化とも、予算で消費を遮断するガードとも違う、第三の作業です。会計の言葉でいえばコスト帰属(cost attribution)、つまり発生した費用を、それを生んだ単位へ正しく割り付けることです。個人開発で収支を一人で見ている以上、原価が見えないままでは投資判断ができません。
予算ガードと「コスト帰属」は別の仕事
混同しやすいので最初に切り分けます。予算ガードは「上限を超えたら止める」仕組みで、暴走を防ぐためのものです。コスト帰属は「使った費用を、どのタスク・どのサイトが生んだかへ割り付ける」仕組みで、判断材料を作るためのものです。
ガードがあっても帰属がないと、「全体としては予算内だが、効率の悪いタスクに費用が偏っている」状態が見えません。私自身、長らくガードだけを入れて安心していました。止まりはしないけれど、最適な配分になっているかは分からない——そういう運用が続いていたのです。
帰属の出発点は、すべてのモデル呼び出しに「これは誰のための呼び出しか」を表すタグを付けることです。
呼び出しごとにコストタグを付ける
各呼び出しに、サイト・エージェント・タスク種別の3つのタグを必ず添えます。後でこの軸に沿って費用を切り分けるためです。
interface CostTag {
site: string; // "antigravitylab"
agent: string; // "writer-1"
taskType: string; // "premium-article" | "link-audit" など
}
interface Usage {
inputTokens: number;
outputTokens: number;
model: string; // "gemini-3.5-flash" など
}
interface CostEntry extends CostTag {
ts: string;
model: string;
inputTokens: number;
outputTokens: number;
costJpy: number;
}
タグは呼び出し時の文脈から自動で埋めます。手で付けると必ず付け忘れが出るので、エージェントを起動する側で CostTag を生成し、呼び出しラッパーに渡す形にします。
使用量を円へ換算する単価テーブル
モデルが返す使用量(トークン数)を、入出力それぞれの単価で円に換算します。単価はモデルごとに変わるので、テーブルとして一箇所に集めます。
// 100万トークンあたりの単価(円)。実値は契約・為替で変わるため定数化して一元管理する
const PRICE_PER_MTOK_JPY: Record<string, { in: number; out: number }> = {
"gemini-3.5-flash": { in: 11, out: 44 },
"gemini-3.1-pro": { in: 190, out: 760 },
};
function toCostJpy(usage: Usage): number {
const p = PRICE_PER_MTOK_JPY[usage.model];
if (!p) throw new Error(`unknown model price: ${usage.model}`);
const inCost = (usage.inputTokens / 1_000_000) * p.in;
const outCost = (usage.outputTokens / 1_000_000) * p.out;
return Math.round((inCost + outCost) * 100) / 100; // 小数2桁
}
ここで重要なのは、出力トークンの単価が入力の数倍高いことを正しく反映する点です。安いと思っていた処理が高くつく原因は、たいてい出力の多さにあります。入出力を分けて記録しておくと、後で「出力が膨らんでいるタスク」を名指しできます。
レジャーに記録して集計する
タグ付きのコストエントリを、追記専用のレジャー(台帳)に貯めます。レジャーは1行1 JSON で十分です。あとはこれを軸ごとに集計するだけです。
class CostLedger {
private entries: CostEntry[] = [];
record(tag: CostTag, usage: Usage): void {
this.entries.push({
...tag,
ts: new Date().toISOString(),
model: usage.model,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
costJpy: toCostJpy(usage),
});
}
// 任意の軸でグループ集計する
sumBy(key: keyof CostTag): Record<string, number> {
const acc: Record<string, number> = {};
for (const e of this.entries) {
acc[e[key]] = (acc[e[key]] ?? 0) + e.costJpy;
}
return acc;
}
// 投稿1本あたりの原価(タスク種別ごと)
unitCost(taskType: string, count: number): number {
const total = this.entries
.filter((e) => e.taskType === taskType)
.reduce((s, e) => s + e.costJpy, 0);
return count > 0 ? Math.round((total / count) * 100) / 100 : 0;
}
}
sumBy("taskType") でタスク別、sumBy("site") でサイト別の原価が一発で出ます。unitCost を使えば、「プレミアム記事1本あたりの原価」のような単位原価まで降りられます。私はこの単位原価を見て初めて、ある定型タスクが想定の何倍も費用を食っていることに気づきました。
単位原価で投資判断をする
原価が軸ごとに見えると、判断が「感覚」から「数字」に変わります。私が実際に使っている見方を共有します。
まず、タスク種別ごとの単位原価を、そのタスクが生む価値と並べます。たとえば、めったに読まれないメタ的な集計タスクに、プレミアム記事1本と同じ原価がかかっていたら、それは止める候補です。価値の薄い処理に費用が偏っていないかを、単位原価が暴いてくれます。
次に、サイト別の原価を、そのサイトの収益(App Store / Google Play / AdMob や課金)と突き合わせます。原価がサイト間で偏っているのに、収益はそれに見合っていないなら、配分を見直すサインです。
そして、出力トークンが突出して多いタスクを名指しして、指示文を絞ります。出力単価は入力の数倍なので、冗長な出力を求めるプロンプトは静かに費用を押し上げます。要約や箇条書きで足りるタスクに、長文の自由記述を求めていないかを点検します。
これらの見直しで、私の月の費用は約28%下がりました。下がった主因は「安くて速いから」と無自覚に増やしていた低価値タスクを、単位原価を根拠に止められたことです。ガードだけでは止められなかった種類の無駄です。
レジャーの保存と運用上の注意
ここまでの実装はメモリ上のレジャーですが、無人運用では実行をまたいで費用を積み上げる必要があるため、レジャーは外部へ永続化します。私は1日分を1ファイルの追記専用 JSON Lines として残し、月末にまとめて軸ごとに集計しています。追記専用にしておくと、途中で集計プロセスが落ちても記録そのものは壊れません。
注意点が2つあります。1つは、コストタグの値を後から変えないことです。taskType の名前を途中で変えると、過去分と今月分が別タスクとして集計され、時系列の比較ができなくなります。もう1つは、単価テーブルの更新日を記録しておくことです。モデルの値下げや為替の変動で単価は動くため、いつの単価で換算した費用かが分からないと、月次の増減が単価変更によるものか使用量によるものか切り分けられません。私自身、単価改定の月にこの切り分けで一度混乱した経験があり、それ以来テーブルにバージョンと適用日を添えています。
請求が1つの数字に見えているうちは、最適化の打ち手も勘に頼ることになります。まずは全呼び出しにサイト・エージェント・タスクのタグを付け、1か月ぶんを軸ごとに集計してみてください。どこにお金が流れているかが見えた瞬間から、止めるべきものが具体的に分かるようになります。