コスト最適化ガイド
チャット履歴をトリミングしてコストを削減:スライディングウィンドウか要約か
毎ターン再送信する内容に上限を設けます。直近数メッセージまたは短い要約を保持します。50ターンのチャットのトークン計算とPythonヘルパー。

サポートボットが50ターンも会話していると、新しいメッセージのコストが前のものより高くなっていきます。APIは何も記憶しないため、コードは毎回会話全体を再送信します。解決策は送信する内容に上限を設けることです。システムプロンプトと直近数ターンだけを保持する(スライディングウィンドウ)か、古いターンを短い要約に置き換えます。以下に50ターンのチャットの計算と、コピーして使えるヘルパーを示します。
DeepSeekのマルチラウンドチャットガイド(api-docs.deepseek.com、2026年11月1日確認)は明確に述べています。サーバーは以前のリクエストのコンテキストを記録しないため、クライアントは毎回の呼び出しで完全なmessagesリストを送信します。CheapTokensのOpenAI互換フォーマットも同じように動作します。これが初めての場合は、長いチャットが高くつく理由とmessages配列の仕組みから始めてください。
ターン50が実際に送信するもの#
400トークンのシステムプロンプトを持つサポートボットを運用しているとします。典型的なターンでは、ユーザーが約60トークンを書き、ボットが約190トークンで答えます。1つの完全なターンで履歴に250トークンが追加されます。これらは仮定です。ご自身のレスポンスのusageフィールドの数値に置き換えてください。
ターン50では、リクエストはシステムプロンプト、49の以前のターン、新しいメッセージを運びます。190トークンの回答を得るために400 + 49 × 250 + 60 = 12,710入力トークンです。チャット全体を合計します。システムプロンプトと新しいメッセージは毎回送信されます。50 × 460 = 23,000。繰り返される履歴は250 × 1,225 = 306,250を追加します。合わせて、50 × 190 = 9,500出力トークンに対して23,000 + 306,250 = 329,250入力トークンです。
各リクエストの履歴は1ターンずつ増えるため、合計はチャットの長さの二乗で増加します。50から100ターンにすると、繰り返される履歴の部分はほぼ4倍になります。
自分で試す試してみましょう。システムプロンプト、直近4ターン、新しいメッセージだけを送信した場合、ターン50は何入力トークンを運ぶでしょうか?答えを表示
完全な履歴では12,710に対して1,460(400 + 4 × 250 + 60)です。同じ質問、同じ回答の長さで、入力はほんの一部です。
オプション1:スライディングウィンドウ#
考え方:常にシステムプロンプトを送信し、直近Nターン以外はすべて削除します。完全な履歴はご自身のストレージに保持し、送信するコピーだけをトリミングします。
def trim_history(messages, keep_pairs=4):
"""System prompt + the last keep_pairs turns + the new user message."""
system = [m for m in messages if m["role"] == "system"]
rest = [m for m in messages if m["role"] != "system"]
return system + rest[-(keep_pairs * 2 + 1):]ユーザーの新しいメッセージを追加した後に呼び出します。keep_pairs=4の場合、4つのユーザー/アシスタントのペアと新しいメッセージで9メッセージになり、スライスは常にユーザーメッセージから始まります。実際の呼び出しに組み込んだ例です:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.cheaptokens.dev/v1",
api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"
history = [{"role": "system", "content": "You are a concise support assistant."}]
def ask(text):
history.append({"role": "user", "content": text})
resp = client.chat.completions.create(
model=MODEL,
messages=trim_history(history),
max_tokens=500,
)
answer = resp.choices[0].message.content
history.append({"role": "assistant", "content": answer})
print(resp.usage.prompt_tokens, resp.usage.completion_tokens)
return answer有用なのはprintの行です。最初の数値に注目してください。トリミングしないと毎ターン上昇します。ウィンドウを使えば、チャットがターン4を超えると増加が止まります。

数値で見ると、ターン5以降、各リクエストは400 + 4 × 250 + 60 = 1,460入力トークンです。最初の4ターンは通常どおり増加し、合計3,340になります。チャット全体では3,340 + 46 × 1,460 = 70,500入力トークンで、完全な履歴で送信する329,250の約5分の1です。出力は50 × 190 = 9,500のままです。

コードでは、変更全体は呼び出しの1つの引数です:
比較する
1行の変更
変更前
messages=history変更後
messages=trim_history(history)注意点:ウィンドウは忘れます。ユーザーがターン40で「私の注文番号は何でしたか?」と尋ね、その番号がターン3にあった場合、モデルはそれを見たことがありません。安価な修正が2つあります。安定した事実(名前、注文番号、プラン)をご自身のコードでチャットから抽出し、システムプロンプトに追加します。そこはウィンドウが決して切り取らない場所です。または要約を追加します。これが次のオプションです。
オプション2:古いターンの要約#
古いターンを削除する代わりに、圧縮します。数ターンごとに、ウィンドウからちょうど外れたターンと現在の要約をモデルに送信し、新しい短い要約を求めます。そしてその要約をシステムメッセージに入れます。
def summarize(summary, old_messages):
transcript = "\n".join(f"{m['role']}: {m['content']}" for m in old_messages)
prompt = (
f"Summary so far:\n{summary}\n\nNew turns:\n{transcript}\n\n"
"Rewrite the summary in under 200 words. "
"Keep names, numbers, decisions and open questions."
)
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
max_tokens=400,
)
return resp.choices[0].message.content
def build_messages(history, summary, keep_pairs=4):
system = history[0]["content"]
if summary:
system += "\n\nEarlier in this chat: " + summary
recent = history[1:][-(keep_pairs * 2 + 1):]
return [{"role": "system", "content": system}] + recent要約は無料ではありません。追加のモデル呼び出しです。約300トークンの要約を5ターンごとに更新すると仮定します(50ターンで9回)。各更新は約1,600トークン(古い要約+5ターン+指示)を読み、300を書き込みます。安全側に倒して、ターン5以降のすべてのリクエストで要約をカウントします(実際には最初の更新後に現れるため、これはわずかな過大評価です):400 + 300 + 4 × 250 + 60 = 1,760入力トークン。
チャットの合計:メインリクエスト3,340 + 46 × 1,760 = 84,300入力トークン、要約呼び出し9 × 1,600 = 14,400入力と9 × 300 = 2,700出力。全体で84,300 + 14,400 = 98,700入力と9,500 + 2,700 = 12,200出力トークンです。
要約は素のウィンドウより少しコストがかかり、その代わりにモデルはターン3で何が起きたかをまだ知っています。
3つの構成のコスト#
同じ50ターンのチャットをDeepSeek-V4-Flashで、上記の合計からライブ価格で計算したものです:
価格はライブ価格表から取得しているため、料金が変われば数値も更新されます。順序は変わりません。ウィンドウが最も少なく送信し、要約がその中間、完全な履歴が最も高価です。
このようなチャットでは、CheapTokensで最初に試すモデルはDeepSeek-V4-Flashです。380Kのコンテキストウィンドウがあり(50ターンのチャットはトリミングしなくても収まります)、回答あたり最大32,768出力トークン、1日のどの時間でも固定価格で、入力と出力を合計すると全体でOpenRouterより安価です。モデルIDはdeepseek-です。ウィンドウ構成でモデルを比較したい場合(リクエストあたり平均1,410入力トークン、190出力、50リクエスト)、ライブテーブルはこちらです:
| モデル | 入力 $/1M | 出力 $/1M | リクエストあたり | 50 リクエスト | OpenRouter、50 リクエスト |
|---|---|---|---|---|---|
| GLM-5.3-Flash | $0.02 | $0.04 | $0.000036 | $0.0018 | $0.0032 |
| DeepSeek-V4-Flash | $0.015 | $0.10 | $0.00004 | $0.002 | $0.0063 |
| MiniMax-M2.7 | $0.05 | $0.15 | $0.000099 | $0.005 | $0.0594 |
リクエストあたり 1,410 入力トークンと 190 出力トークン。100万トークンあたりの価格は現行のもので、当社の料金体系に合わせて変動します。OpenRouter 列は同じワークロードの参考価格を示しています。
GLM-5.3-Flashは390Kのコンテキスト、MiniMax-M2.7は180Kで、トリミングしたチャットには十分です。現在の価格は価格ページにあり、ドキュメントでは最初のリクエストの送信方法を示しています。
どれを選ぶか#
ウィンドウがデフォルトです。3行のコード、追加の呼び出しなし、追加の失敗モードなし。サポートボット、チューター、ロールプレイに使用します。直近数ターンが会話を運び、システムプロンプトがルールを運ぶ場合です。ユーザーがずっと前に言ったことを参照する場合は要約を追加します。終わりのない1つの会話を持つパーソナルアシスタントのように。ウィンドウは、モデルが直近のコンテキストをまだ見られる程度に小さく、完全な質問と回答のやり取りを保持できる程度に大きく保ちます。そもそもどれだけ収まるかは別の制限で、コンテキストウィンドウとは何かで扱っています。
クイックチェック
ユーザーがターン40で、ターン3で示された注文番号について尋ねます。要約なしの4ターンウィンドウを使用しています。何が起こるでしょうか?
答えを表示
答え: ターン3がリクエストから切り取られたため、モデルはそれを見ることができない
APIはステートレスです。モデルはリクエストにあるものだけを見るため、まだ必要な事実はシステムプロンプトまたは要約に残しておく必要があります。



