Как снизить расход токенов без потери качества
В прошлом месяце я анализировал агента поддержки клиентов, который расходовал 18,000–42,000 входных токенов на обращение, прежде чем написать хотя бы слово. Сама проблема пользователя обычно занимала менее 300 токенов. Всё остальное было контекстом «на всякий случай»: полная история переписки, все страницы внутренних правил, найденные по ключевым словам, JSON результатов работы инструментов, скопированных дословно, и системный промпт объёмом 900 токенов, который разрастался на протяжении шести месяцев.
Решение состояло не в том, чтобы «использовать модель поменьше» или «снизить max_tokens и надеяться на лучшее». Мы сократили медианный расход токенов примерно на 68%, сохранив качество ответов, благодаря обычной инженерной работе: сокращению контекста, более точному поиску подтверждающих материалов, обобщению состояния, строгому соблюдению схем вывода и честному измерению расхода токенов.
Эта статья именно о такой работе. Не о соревновании в краткости промптов. Не о трюках, ухудшающих работу модели. Только о практических способах расходовать меньше токенов, не делая систему глупее.
Расход токенов — вопрос архитектуры
Большинство команд рассматривают затраты на токены как вопрос цен на модели. На практике это обычно вопрос маршрутизации информации.
В типичном вызове LLM в промышленной эксплуатации токены распределяются по четырём категориям:
- Токены инструкций: системный промпт, промпт разработчика, правила форматирования, ограничения безопасности.
- Токены контекста: история чата, найденные документы, результаты работы инструментов, записи базы данных.
- Токены рассуждений и задачи: сам запрос пользователя и промежуточные сведения о задаче.
- Выходные токены: сгенерированный ответ, объект JSON, SQL-запрос, сводка или вызов инструмента.
Когда расход токенов выходит из-под контроля, виноваты обычно контекст и вывод. Модель обходится дорого не потому, что пользователь задал сложный вопрос. Она обходится дорого потому, что приложение запихнуло в промпт весь мир, а затем позволило модели говорить без конца.
Полезный первый шаг — регистрировать расход токенов по разделам, а не только общий размер запроса:
{
"request_id": "ticket_81291",
"model": "sonnet-4.6",
"tokens": {
"system": 742,
"history": 3890,
"retrieval": 12640,
"tool_results": 2110,
"user": 184,
"output": 931
},
"latency_ms": 4820
}
Когда вы увидите эту разбивку, путь оптимизации станет очевиден. Не нужно спорить о том, какая модель абстрактно «лучше»: Claude Opus 4.8, Sonnet 4.6, GPT-5.5, Gemini 3, Fable 5 или открытая модель вроде Qwen или DeepSeek. Сначала нужно перестать отправлять 12,000 токенов нерелевантных материалов, полученных при поиске.
Начните с бюджета токенов для каждого сценария
Я предпочитаю задавать бюджеты для отдельных сценариев продукта, а не глобально. Анализ юридических договоров, агент для программирования и бот для ответов на частые вопросы не должны исходить из одинаковых предположений о расходе токенов.
Пример бюджета для ассистента поддержки:
| Вариант доступа | Бюджет входных токенов | Бюджет выходных токенов | Класс модели | Примечания |
|---|---|---|---|---|
| Классификация обращения | 600 | 50 | Недорогая и быстрая модель | Метки, срочность, язык |
| Поиск подтверждающих материалов | 1,500 | 0 | Эмбеддинги/поиск | Генерация не требуется |
| Подготовка ответа | 4,000 | 600 | Мощная модель среднего или высокого класса | Использует отобранные материалы |
| Сводка переписки | 1,000 | 250 | Недорогая и быстрая модель | Обновляет состояние памяти |
| Анализ необходимости эскалации | 6,000 | 900 | Мощная модель | Только для сложных случаев |
Смысл не в том, что эти числа подходят всем. Это намеренно скромные начальные ограничения. Полезный результат такого подхода — необходимость обосновывать расход токенов на каждом этапе.
Частая ловушка: если вы задаёте только max_tokens для вывода, входные данные всё равно могут незаметно разрастаться. Ограничения вывода предотвращают длинные ответы, но не мешают переполнять контекст найденными материалами.
Сокращайте контекст до поиска, а не после
Многие системы ищут документы по последнему сообщению пользователя, но при этом всё равно включают полную историю переписки в итоговый промпт. Это означает, что в контексте остаются старые отступления от темы, предыдущие неудачные вызовы инструментов и устаревшие предположения.
Вместо этого поддерживайте компактное рабочее состояние:
{
"user_goal": "Get refund for duplicate annual subscription charge",
"known_facts": [
"User paid twice on 2025-01-08",
"Invoice IDs: inv_1042 and inv_1077",
"Account email: user@example.com"
],
"constraints": [
"Refund policy allows duplicate-charge refunds within 90 days",
"User prefers email follow-up"
],
"open_questions": [
"Whether one charge has already been reversed"
],
"last_updated_turn": 12
}
Тогда ваш итоговый вызов включает:
- Компактное состояние.
- Последнее сообщение пользователя.
- Только релевантные фрагменты найденных правил.
- Возможно, последний обмен репликами или два, если важно сохранить тон общения.
Не включайте бездумно все предыдущие сообщения. История чата — это не память, а запись разговора. Память — это сжатое состояние, необходимое для продолжения задачи.
На практике я использую три слоя:
- Последние обмены репликами: последние 1–4 обменов репликами в зависимости от чувствительности задачи.
- Обновляемая сводка: устойчивые факты и принятые решения.
- Поиск во внешних источниках: документы, записи базы данных, результаты работы инструментов.
Обычно это работает лучше, чем заполнение контекста необработанной перепиской, поскольку модель видит меньше шума и противоречивых инструкций.
Поиск должен быть избирательным, а не декоративным
Генерация с дополнением из найденных материалов часто начинается как способ повысить качество, а затем превращается в источник перерасхода токенов. Проблему легко заметить: top-k установлен на 10 или 20, каждый фрагмент содержит 1,000 токенов, а итоговый промпт включает груду документов, лишь отдалённо связанных с вопросом.
Более качественный поиск обычно обходится дешевле.
Практический конвейер поиска:
def build_context(query, user_state):
search_query = rewrite_for_search(query, user_state)
candidates = vector_search(
query=search_query,
top_k=30,
filters={"product": user_state["product"]}
)
reranked = rerank_cross_encoder(
query=search_query,
docs=candidates,
top_k=6
)
selected = []
token_budget = 1800
for doc in reranked:
snippet = extract_relevant_span(doc.text, query=search_query, max_tokens=300)
if token_count(selected) + token_count(snippet) <= token_budget:
selected.append({
"doc_id": doc.id,
"title": doc.title,
"text": snippet
})
return selected
Важные детали:
- Находите больше кандидатов, чем планируете включить.
- Повторно ранжируйте результаты перед генерацией.
- Извлекайте релевантные отрывки вместо передачи полных фрагментов.
- Строго соблюдайте бюджет токенов для найденных материалов.
- Сохраняйте идентификаторы документов, чтобы модель могла цитировать материалы или ссылаться на них при внутренней обработке.
Размер фрагментов имеет значение. Если фрагменты слишком малы, модель теряет контекст. Если они слишком велики, каждое совпадение тянет за собой нерелевантный текст. Для документации продукта и страниц с правилами я часто начинаю с фрагментов примерно по 300–600 токенов, сохраняя заголовки, а затем корректирую размер на основе случаев, в которых система ошибается.
Что на самом деле происходит, когда вы перегружаете контекст найденными материалами? Модель часто становится менее точной. Она пытается согласовать близкие по теме, но нерелевантные правила, подхватывает устаревшие оговорки или отвечает не на тот вариант вопроса. Сокращение числа токенов может повысить качество, поскольку убирает отвлекающую информацию.
Обобщение — это сжатие с ответственностью за результат
Обобщение позволяет существенно сократить объём контекста, но небрежные сводки опасны. Если сводка упускает ограничение, ваш агент может уверенно сделать что-то неправильно.
Я предпочитаю структурированные сводки обычному тексту:
{
"task": "Debug failing OAuth callback in staging",
"environment": {
"service": "auth-api",
"branch": "release-2025-02-14",
"runtime": "node 20"
},
"confirmed": [
"Callback returns 302 to /login",
"Provider sends code and state",
"State validation passes"
],
"suspected": [
"Session cookie is not persisted after callback"
],
"rejected": [
"Provider credentials are invalid",
"Redirect URI mismatch"
],
"next_steps": [
"Inspect Set-Cookie attributes in staging response",
"Compare SameSite and Secure flags with production"
]
}
Это экономнее расходует токены и надёжнее, чем:
Пользователь отлаживает OAuth и уже пробовал разные подходы…
Хороший этап суммаризации должен сохранять:
- Цель пользователя.
- Точные факты.
- Уже принятые решения.
- Ограничения и предпочтения.
- Неудачные попытки.
- Открытые вопросы.
- Идентификаторы, имена файлов, временные метки и другие точные обозначения.
Не убирайте точные значения при суммаризации, если только они действительно не имеют значения. «У пользователя проблема со счётом» — дёшево, но бесполезно. «Повторное списание: inv_1042 и inv_1077 от 2025-01-08» — вот какое сжатие вам нужно.
Для агентов, работающих длительное время, я также сохраняю версии сводок. Если модель создаст неудачную сводку, вам понадобится возможность выяснить, когда состояние начало искажаться.
Ограничивайте многословие в ответах с помощью схем
Удивительно большая доля токенов тратится впустую на выходе. В промпте сказано «будь краток», но модель пишет вступление, оговорки, резюме и предложение помочь.
Для ответов, обрабатываемых программно, используйте схемы. Не ограничивайтесь просьбой «вернуть ответ в формате JSON»; задайте структуру и отклоняйте всё, что ей не соответствует.
Пример вызова для классификации:
{
"type": "object",
"additionalProperties": false,
"required": ["category", "priority", "needs_human", "reason"],
"properties": {
"category": {
"type": "string",
"enum": ["billing", "bug", "account", "how_to", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "normal", "high", "urgent"]
},
"needs_human": {
"type": "boolean"
},
"reason": {
"type": "string",
"maxLength": 160
}
}
}
Для ответов пользователю задайте формат с жёстким ограничением объёма:
Write the answer in this structure:
- First sentence: direct answer, max 25 words.
- Then 2-4 bullets with concrete steps.
- Do not include a greeting.
- Do not include "let me know if you need anything else."
- Stop after the final bullet.
Это работает лучше, чем «пиши коротко», потому что задаёт модели структуру.
Распространённая проблема: низкое значение max_tokens может оборвать полезный ответ на середине мысли. Схема или ограниченный формат сокращают длину ответа, сохраняя его полноту. Усечение — крайняя мера, а не стратегия форматирования.
Используйте стоп-последовательности осторожно
Стоп-последовательности полезны, когда у генерируемого текста есть естественная граница. Например, если модель создаёт один раздел документа по шаблону, можно остановить генерацию на маркере следующего раздела.
{
"model": "gpt-5.5",
"messages": [
{
"role": "user",
"content": "Generate only the Risk Summary section for this review..."
}
],
"max_tokens": 500,
"stop": ["\n## Mitigations", "\n## Appendix"]
}
Стоп-последовательности менее полезны для ответов ассистента со свободной структурой, поскольку могут обрезать корректное содержимое. Их поведение также различается между API и семействами моделей, особенно при вызове инструментов или использовании структурированных ответов. Рассматривайте их как средство контроля границ, а не основной способ сжатия.
Удачные варианты использования:
- Генерация одного раздела по известному шаблону.
- Предотвращение перехода модели к примерам.
- Остановка на разделителе при пакетной обработке.
- Ограничение устаревших промптов для продолжения текста.
Рискованные варианты использования:
- Остановка на распространённых знаках препинания.
- Остановка на словах, которые могут встречаться в обычных ответах.
- Использование стоп-последовательностей вместо полноценной схемы ответа.
- Предположение, что Claude, GPT, Gemini и шлюзы к открытым моделям ведут себя одинаково.
Распределяйте задачи между более дешёвыми моделями
Не каждый этап требует вашей самой мощной модели. В производственных системах я часто направляю подзадачи более компактным или дешёвым моделям:
| Подзадача | Типичное требование | Подходящий вариант |
|---|---|---|
| Определение языка | Простая классификация | Компактная или дешёвая модель |
| Категория обращения | Вывод метки в структурированном формате | Компактная или средняя модель |
| Переформулирование запроса | Короткое преобразование | Компактная или средняя модель |
| Повторное ранжирование документов | Специализированная модель повторного ранжирования или компактная модель | По возможности без генерации |
| Итоговый ответ | Рассуждение и тон | Более мощная модель |
| Исключение из правил | Нюансы и риски | Самая мощная модель |
Именно здесь доступ к нескольким моделям полезен на практике. Вам может понадобиться Sonnet 4.6 для большинства задач написания текста агентами, GPT-5.5 для сложной цепочки рассуждений, Gemini 3 для некоторых мультимодальных процессов или работы с длинным контекстом, Fable 5, когда действительно нужно контекстное окно 1M, и открытые модели, такие как Llama, Qwen, DeepSeek, MiniMax или Kimi, для контролируемых внутренних задач. С инженерной точки зрения главное — маршрутизация, а не верность бренду.
Ограничение: накладные расходы на оркестрацию вполне реальны. Каждый дополнительный вызов модели увеличивает задержку, добавляет сценарии отказа, усложняет журналирование и расширяет объём оценки. Если дешёвый классификатор экономит 300 токенов, но добавляет 600 мс и ещё один сценарий повторных попыток, это может не окупиться.
Подход, который мне нравится:
if route == "simple_faq":
model = "haiku-4.5"
elif route == "standard_support":
model = "sonnet-4.6"
elif route == "legal_or_financial_exception":
model = "opus-4.8"
else:
model = "sonnet-4.6"
Конкретные названия будут меняться. Принцип должен оставаться прежним: используйте самую дешёвую модель, которая надёжно обеспечивает требуемое для задачи качество.
Измеряйте входные и выходные токены честно
Оптимизация токенов не удаётся, когда команды смотрят только на среднюю стоимость запроса. Вам нужны распределения с разбивкой по маршрутам и этапам.
Отслеживайте как минимум:
- Входные токены по разделам промпта.
- Выходные токены.
- Количество найденных документов и токены извлечённого контекста.
- Название и версия модели.
- Перцентили задержки.
- Доля попаданий в кеш, если используется кеширование промптов или контекста.
- Количество повторных попыток.
- Показатели качества, заметные пользователю.
- Частота передачи задач человеку или внесения исправлений.
Полезная панель мониторинга показывает p50, p90 и p99. Проблемы с расходом токенов часто скрываются в хвосте распределения.
Пример строки журнала:
{
"route": "support.draft_answer",
"model": "sonnet-4.6",
"input_tokens": 3820,
"output_tokens": 514,
"retrieved_docs": 4,
"retrieval_tokens": 1320,
"latency_ms": 3100,
"cache_hit": false,
"quality": {
"user_thumb": "up",
"human_edited": false
}
}
Будьте осторожны с заявлениями «мы сократили расход токенов на 40%». Увеличилось ли число повторных попыток? Стало ли больше передач задач на следующий уровень поддержки? Начали ли пользователи задавать больше уточняющих вопросов, потому что ответы стали слишком краткими? Стала ли более дешёвая модель чаще выдавать некорректный JSON, вынуждая делать дополнительные вызовы для исправления?
Честная метрика — не токены на вызов, а токены на успешно выполненную задачу.
Длинный контекст — не повод лениться
Большие контекстные окна действительно полезны. Контекст класса 1M у Fable 5, сильные стороны Gemini в работе с длинным контекстом и режимы длинного контекста у передовых моделей позволяют реализовывать процессы, которые ещё несколько лет назад были неудобны: анализ кода всего репозитория, проверку больших документов, работу с многочасовыми расшифровками и насыщенными подборками исследовательских материалов.
Но длинный контекст — это возможность, а не архитектура по умолчанию.
Вызовы с длинным контекстом могут быть уместны, когда:
- Ответ зависит от множества удалённых друг от друга частей входных данных.
- Поиск не выявит связи между документами.
- Задача носит исследовательский характер, и вы ещё не знаете, что важно.
- Цена потери детали выше, чем стоимость чтения большего объёма.
Обычно они ведут к лишним затратам, когда:
- Пользователь задаёт узкий вопрос о конкретном факте.
- Достаточно нескольких фрагментов источников.
- Контекст содержит дублирующуюся или устаревшую информацию.
- Модель используется как фильтр базы данных.
Распространённый подход — использовать длинный контекст для анализа вне обработки запросов и создавать компактные артефакты для обслуживания запросов в реальном времени. Например, один раз проанализировать весь корпус правил, а затем создать структурированную карту правил, эмбеддинги, сводки и тестовые случаи. Ассистент во время работы не должен перечитывать весь корпус для каждого обращения.
Практический процесс оптимизации
Когда мне достаётся LLM-система с большим расходом токенов, я обычно действую в таком порядке:
-
Сначала настройте измерения
Добавьте учёт токенов по разделам. Не оптимизируйте вслепую. -
Ограничьте объём извлечённого контекста
Задайте жёсткий лимит токенов для извлечённого контекста. Выполните повторное ранжирование и извлеките нужные фрагменты. -
Замените заполнение контекста историей диалога на хранение состояния
Сохраняйте последние реплики и структурированную память. -
Ограничьте ответы
Используйте схемы для ответов, обрабатываемых программно, и явные форматы для ответов пользователю. -
Настройте маршрутизацию подзадач
Переносите классификацию, переформулирование и создание сводок на более дешёвые модели, когда они надёжно справляются с этими задачами. -
Оценивайте качество на уровне задачи
Сравнивайте успешное выполнение задач, а не только стоимость отдельного вызова. -
Следите за хвостом распределения
Исследуйте всплески потребления токенов на p95 и p99. Они часто указывают на сбои поиска или бесконтрольный рост истории.
Вот простой защитный механизм, который я использовал в сервисах:
TOKEN_LIMITS = {
"support.classify": {"input": 800, "output": 80},
"support.retrieve": {"input": 500, "output": 0},
"support.draft": {"input": 4500, "output": 700},
"support.summarize": {"input": 1800, "output": 300},
}
def assert_token_budget(route, prompt_sections, max_output_tokens):
input_tokens = {
name: count_tokens(text)
for name, text in prompt_sections.items()
}
total_input = sum(input_tokens.values())
limits = TOKEN_LIMITS[route]
if total_input > limits["input"]:
raise TokenBudgetError(route, total_input, input_tokens)
if max_output_tokens > limits["output"]:
raise TokenBudgetError(route, max_output_tokens, {"output": max_output_tokens})
Лучше сразу остановиться с ошибкой, чем через неделю обнаружить, что один неудачный поисковый запрос потратил ваш бюджет на нерелевантные PDF.
Практические рекомендации
- Снижение расхода токенов следует начинать с наблюдаемости: регистрируйте входные и выходные токены по разделам промпта, маршрутам и моделям.
- Поиск эффективнее заполнения контекста всем подряд, когда он избирателен: повторно ранжируйте результаты, извлекайте фрагменты и соблюдайте бюджет контекста.
- Сводки должны представлять собой структурированное состояние, а не расплывчатый текст. Сохраняйте точные факты, идентификаторы, ограничения и неудачные попытки.
- Схемы вывода сокращают многословие безопаснее, чем простое снижение
max_tokens. - Стоп-последовательности полезны для ограничения вывода, но не заменяют структурированные ответы.
- Направляйте простые подзадачи более дешёвым моделям, только если это оправдывает дополнительную задержку и возможные сбои.
- Оценивайте экономию по числу токенов на успешно выполненную задачу, а не на отдельный вызов.
- Модели с длинным контекстом обладают широкими возможностями, но не отменяют необходимости в информационной архитектуре.
Один ключ API для Claude Opus 5.5, Sonnet 5, Haiku 4.5 и Fable 5.1, а также GPT-6 моделей. Оплата по мере использования, без подписки.
Получить ключ API →