生成AIを機能に組み込むと、利用が増えるにつれ費用が無視できなくなります。
実際に計測して、効果が大きかった順に整理します。
前提:機能別に計測する
まず「どの機能にいくらかかっているか」を出します。これが無いと手を打てません。
class ApiUsage(models.Model):
feature = models.CharField(max_length=40) # 機能名
model = models.CharField(max_length=40)
prompt_tokens = models.IntegerField()
completion_tokens = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
大抵、1〜2機能が費用の大半を占めています。そこだけ対処すれば済みます。
1. キャッシュ(効果:最大)
同じ入力に同じ処理をしているケースが、想像以上に多くあります。
key = "llm:" + sha256(f"{feature}|{normalize(prompt)}".encode()).hexdigest()
if hit := cache.get(key):
return hit
result = call_llm(prompt)
cache.set(key, result, 60 * 60 * 24)
normalize() で空白や大小文字を揃えると、ヒット率が上がります。
分類やタグ付けのような決まった処理では、これだけで大幅に減りました。
2. モデルを使い分ける(効果:大)
すべてを高性能モデルで処理する必要はありません。
| 処理 | モデル |
|---|---|
| 分類・抽出・整形 | 軽量モデル |
| 要約 | 中間 |
| 複雑な推論・文章生成 | 高性能モデル |
軽量モデルで済む処理を高性能モデルに投げているのが、
最もよくある無駄でした。
3. 入力を短くする(効果:中〜大)
長い文脈を毎回渡すのは高くつきます。
- 会話履歴は直近N件に絞る
- 検索結果は上位k件だけ渡す(k を増やしても精度は頭打ちになりやすい)
- 定型の指示は簡潔に
messages = messages[-6:] # 直近3往復だけ
context = "\n".join(hits[:4]) # 上位4件だけ
実測すると、渡す件数を8→4にしても精度がほぼ変わらないことがよくあります。
4. 出力を短くする(効果:中)
出力トークンは入力より単価が高いのが一般的です。
max_tokens = 300 # 上限を設ける
「簡潔に」と指示するだけでなく、上限を機械的にかけるのが確実です。
5. 前処理で呼ばずに済ませる(効果:状況次第)
そもそも呼ばなくてよいケースを弾きます。
if len(text) < 20:
return None # 短すぎる入力は処理しない
if is_duplicate(text):
return previous_result
LLM を呼ばないのが、最も安いという当たり前の結論です。
上限を設ける
対策を入れても、想定外の使われ方で跳ねることがあります。
日次・月次の上限を設け、超えたら止めます。
if today_cost() > DAILY_LIMIT:
raise BudgetExceeded("本日の上限に達しました")
まとめ
- まず機能別に計測する(大半は1〜2機能が占めている)
- キャッシュ(正規化してヒット率を上げる)
- 処理の難易度でモデルを使い分ける
- 入力の件数・履歴を実測して削る
- 出力に上限をかける
- 日次の上限で暴走を止める