Назад в блог

Оптимизация расходовИнструкция

Как урезать историю чата: скользящее окно или сводка

Ограничьте то, что отправляете на каждом шаге: последние сообщения или короткая сводка. Расчёт токенов для чата из 50 ходов и код на Python.

Автор: CheapTokens TeamОпубликовано 5 мин чтения

Обложка: дешёвый длинный чат и хелпер trim_history
Содержание
  1. Что на самом деле уходит на 50-м ходу
  2. Вариант 1: скользящее окно
  3. Вариант 2: сводка старых ходов
  4. Сколько стоят три варианта
  5. Что выбрать

Ваш бот поддержки общается уже 50 ходов, и каждое новое сообщение обходится дороже предыдущего. API ничего не помнит, поэтому ваш код каждый раз отправляет весь разговор заново. Решение — ограничить отправляемое: оставить системный промпт и последние несколько ходов (скользящее окно) или заменить старые ходы короткой сводкой. Ниже — расчёт для чата из 50 ходов и хелпер, который можно скопировать.

Руководство DeepSeek по многошаговому чату (api-docs.deepseek.com, проверено 1 ноября 2026) говорит прямо: сервер не записывает контекст прошлых запросов, поэтому клиент отправляет полный список messages при каждом вызове. В OpenAI-совместимом формате CheapTokens всё так же. Если для вас это новость, начните со статей почему длинный чат стоит дороже и как устроен массив messages.

Что на самом деле уходит на 50-м ходу#

Допустим, у вас бот поддержки с системным промптом на 400 токенов. В обычном ходу пользователь пишет около 60 токенов, бот отвечает около 190. Один полный ход добавляет в историю 250 токенов. Это допущения: подставьте числа из поля usage ваших собственных ответов.

На 50-м ходу запрос несёт системный промпт, 49 прошлых ходов и новое сообщение: 400 + 49 × 250 + 60 = 12,710 входных токенов ради ответа в 190 токенов. Теперь сложим весь чат. Системный промпт и новое сообщение уходят каждый раз: 50 × 460 = 23,000. Повторяющаяся история добавляет 250 × 1,225 = 306,250. Вместе: 23,000 + 306,250 = 329,250 входных токенов против 50 × 190 = 9,500 выходных.

История в каждом запросе растёт на ход, поэтому сумма растёт как квадрат длины чата. Перейдите с 50 ходов на 100, и повторяющаяся часть истории вырастет примерно вчетверо.

Попробуйте самиПопробуйте сами: если отправить только системный промпт, последние 4 хода и новое сообщение, сколько входных токенов уйдёт на 50-м ходу?Показать ответ

1,460 (400 + 4 × 250 + 60) против 12,710 при полной истории. Тот же вопрос, та же длина ответа, малая часть входа.

Вариант 1: скользящее окно#

Идея: системный промпт отправляем всегда, остальное отбрасываем, кроме последних N ходов. Полную историю вы храните у себя, а обрезаете только копию, которую отправляете.

python
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 сообщений, а срез всегда начинается с сообщения пользователя. Вот как это подключено к настоящему вызову:

python
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-й ход.

Хелпер trim_history на Python: системный промпт, последние 4 хода, новое сообщение
Хелпер обрезки из этой статьи

В числах: начиная с пятого хода каждый запрос равен 400 + 4 × 250 + 60 = 1,460 входных токенов. Первые четыре хода растут как обычно и дают в сумме 3,340. Весь чат: 3,340 + 46 × 1,460 = 70,500 входных токенов, примерно пятая часть от 329,250 при полной истории. Выход остаётся 50 × 190 = 9,500.

Запрос 50-го хода: полная история и окно из 4 ходов
Из чего состоит запрос 50-го хода при допущениях выше

В коде вся правка — один аргумент в вызове:

Сравнение

Правка в одну строку

Было

python
messages=history

Стало

python
messages=trim_history(history)

Но есть подвох: окно забывает. Если на 40-м ходу пользователь спрашивает «какой у меня номер заказа?», а номер прозвучал на 3-м, модель его просто не видела. Два дешёвых выхода. Достаньте устойчивые факты (имя, номер заказа, тариф) своим кодом и дописывайте их в системный промпт: там окно их не отрежет. Или добавьте сводку — это следующий вариант.

Вариант 2: сводка старых ходов#

Вместо того чтобы выбрасывать старые ходы, вы их сжимаете. Раз в несколько ходов отправляйте модели ходы, которые только что выпали из окна, вместе с текущей сводкой и просите новую короткую сводку. Затем кладите её в системное сообщение.

python
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 ходов (9 раз за 50 ходов); каждое обновление читает около 1,600 токенов (старая сводка, пять ходов и инструкция) и пишет 300. Для запаса посчитаем сводку в каждом запросе начиная с пятого хода (на деле она появляется после первого обновления, так что оценка слегка завышена): 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-м ходу.

Сколько стоят три варианта#

Тот же чат из 50 ходов на DeepSeek-V4-Flash, цены считаются вживую по итогам выше:

Один чат из 50 ходов на DeepSeek-V4-Flash
  • Полная история (329,250 вход, 9,500 выход)$0.0059
  • Окно и сводка (98,700 вход, 12,200 выход)$0.0027
  • Скользящее окно, 4 хода (70,500 вход, 9,500 выход)$0.002

Цены берутся из актуального прайса, поэтому числа обновятся вместе с тарифом. Порядок не изменится: окно отправляет меньше всего, сводка посередине, полная история дороже всех.

Для такого чата в CheapTokens стоит начать с DeepSeek-V4-Flash. Контекст 380K (чат из 50 ходов помещается даже без обрезки), до 32 768 токенов в ответе, одна фиксированная цена в любое время суток, а в сумме, с учётом входа и выхода вместе, он дешевле OpenRouter. Id модели — deepseek-ai/DeepSeek-V4-Flash-0731. Если хотите сравнить модели на варианте с окном (в среднем 1,410 входных токенов на запрос, 190 выходных, 50 запросов), вот живая таблица:

Модель За запрос 50 запросов OpenRouter, 50 запросов
GLM-5.3-Flash $0.000036 $0.0018 $0.0032
DeepSeek-V4-Flash $0.00004 $0.002 $0.0063
MiniMax-M2.7 $0.000099 $0.005 $0.0594

1 410 входных и 190 выходных токенов на запрос. Цены за 1M токенов актуальны и меняются вместе с нашими тарифами; столбец OpenRouter показывает справочные цены для той же нагрузки.

У GLM-5.3-Flash контекст 390K, у MiniMax-M2.7 — 180K, этого хватает для обрезанного чата. Актуальные цены — на странице цен, а как отправить первый запрос, показано в документации.

Что выбрать#

Окно — вариант по умолчанию: три строки кода, никаких лишних вызовов и лишних поводов для сбоя. Берите его для ботов поддержки, репетиторов и ролевых игр, где разговор держится на последних ходах, а правила — в системном промпте. Сводку добавляйте, когда пользователи ссылаются на сказанное давно, как в личном ассистенте с одним бесконечным разговором. Окно делайте достаточно маленьким, чтобы платить меньше, и достаточно большим, чтобы в нём помещался целый обмен вопросом и ответом. Сколько вообще помещается в запрос — отдельный предел, о нём в статье что такое контекстное окно.

Быстрая проверка

На 40-м ходу пользователь спрашивает про номер заказа, названный на 3-м. У вас окно из 4 ходов без сводки. Что произойдёт?

Показать ответ

Ответ: Модель его не увидит, потому что 3-й ход вырезан из запроса

API не хранит состояние. Модель видит только то, что есть в запросе, поэтому нужные факты должны остаться в системном промпте или в сводке.