Сначала рассчитайте лимиты
Три числа определяют всё:
- Контекстное окно на 100 000 токенов, сумма промпта и completion.
max_tokensпо умолчанию равен 2048, максимум на один запрос — 16,000.- Тело запроса не должно превышать 8 МБ. Этот лимит редко становится узким местом — обычно вы упираетесь в лимит токенов.
Формула бюджета проста: prompt available = 100000 - max_tokens. Вы планируете вывод на 4000 токенов, значит, для промпта остаётся максимум 60,000. При превышении API возвращает 400, не обрезая данные автоматически. Поэтому «просто впихнуть всю книгу» — не стратегия, а лотерея.
Еще одно заблуждение: чем больше max_tokens, тем надежнее. На самом деле это зарезервированное место под вывод. Установка 16,000 означает, что под промпт останется только 48,000. Устанавливайте значение по необходимости, не ставьте максимум наугад.
Пример расчёта: у вас есть расшифровка интервью на 300 000 знаков для суммаризации. При грубой оценке 1.3 токена на знак это около 390 000 токенов — более чем в шесть раз превышает лимит, отправить целиком нельзя. Если требуется вывод на 800 токенов, то на блок ввода остаётся максимум 63,000, с учётом 10% запаса — около 50 000. Но это не оптимально: при больших блоках модель хуже фокусируется на середине текста. Поэтому лучше разбивать блоки на меньшие и вызывать модель несколько раз.
Оценка токенов: приблизительный алгоритм
Если нет локального токенизатора, используйте следующие эмпирические значения для грубой оценки. Ниже приведены приблизительные данные, а не точные, погрешность может достигать 10–20% в зависимости от текста:
| Тип текста | Приблизительный коэффициент |
|---|---|
| Основной текст на китайском | Около 1–1.5 токена на иероглиф |
| Английский язык | Около 1 токена на 4 символа или 1.3 токена на слово |
| Код, JSON, текст с обилием спецсимволов | Требует больше токенов, чем обычный текст. Рекомендуется использовать консервативную верхнюю оценку. |
Пример функции для оценки и расчета бюджета с резервом:
CONTEXT = 100000
def est_tokens(text: str) -> int:
"""粗估:中文按每字 1.3 token,其余字符按每 4 个字符 1 token。仅为近似。"""
zh = sum(1 for c in text if "\u4e00" <= c <= "\u9fff")
other = len(text) - zh
return int(zh * 1.3 + other / 4) + 1
def max_prompt_budget(max_tokens: int, margin: float = 0.1) -> int:
"""给定计划的 max_tokens,返回 prompt 最多能用多少 token(留 margin 余量)。"""
return int((CONTEXT - max_tokens) * (1 - margin))
# 例:计划让模型写 3000 token,prompt 预算
print(max_prompt_budget(3000)) # 54900Метод калибровки: отправьте типичный ввод, прочитайте usage.prompt_tokens в ответе, сравните с оценкой и вычислите реальный коэффициент для ваших данных. Затем измените коэффициент на измеренное значение. Оценка нужна только для решения «делить или нет», а для сверки смотрите usage.
Суммаризация длинных документов: разбиение и слияние
Если документ превышает бюджет, используйте подход map-reduce: сначала суммируйте фрагменты, затем объедините их в общий итог. Обратите внимание на следующие детали:
- Разбивайте по границам абзацев, а не по фиксированному количеству символов, чтобы не разрезать предложения.
- Оставляйте достаточный бюджет для каждого фрагмента. Рекомендуется не превышать треть от лимита, чтобы оставить место для инструкций и вывода.
- При суммаризации фрагментов требуйте сохранения имен, цифр и ключевых событий, иначе информация будет потеряна при объединении.
- Если итог суммаризации все еще слишком длинный, выполните рекурсивное сжатие.
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.wushenchaapi.com/v1", api_key=os.environ["API_KEY"])
def split_paragraphs(text, budget):
"""按段落切块,每块估算不超过 budget token。"""
chunks, cur, used = [], [], 0
for p in text.split("\n"):
t = int(len(p) * 1.3) + 1 # 中文粗估
if used + t > budget and cur:
chunks.append("\n".join(cur))
cur, used = [], 0
cur.append(p)
used += t
if cur:
chunks.append("\n".join(cur))
return chunks
def ask(prompt, max_tokens=800):
r = client.chat.completions.create(
model="uncensored",
temperature=0.3,
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
)
return r.choices[0].message.content
def summarize_long(text, budget=12000):
parts = split_paragraphs(text, budget)
partial = [ask("用不超过 200 字概括下面这段,保留人名和关键事件:\n\n" + p) for p in parts]
merged = "\n".join(f"第{i+1}部分:{s}" for i, s in enumerate(partial))
return ask("下面是分段摘要,请合并成一份 400 字以内的整体摘要:\n\n" + merged, max_tokens=900)Здесь temperature равно 0.3 для стабильности задач суммирования. Каждый вызов независим, их можно выполнять параллельно, но помните о лимите в 300 запросов в минуту на ключ; для документов из нескольких десятков частей это ограничение не станет проблемой.
Нет универсального ответа на вопрос о размере фрагмента. Опытным путем оптимальным считается размер от 10 000 до 15 000 токенов. Протестируйте фрагменты размером 5000, 12000 и 25000 токенов и сравните количество упущенных фактов. Для документов с высокой плотностью информации (договоры, техдокументация) фрагменты должны быть меньше, для диалогов с большим количеством повторов — больше.
Продолжение романа: скользящее окно и динамический план
К десятой главе весь текст уже не помещается в контекст, и это не нужно. Используйте двухуровневую память:
- Близкий план: последние 2–3 главы оригинала для сохранения стиля, ритма диалогов и деталей сцены.
- Дальний план:ранние главы сжимаются в outline, сохраняя связи, закладки и нерешённые нити.
После написания главы просите модель сжать её до 3–5 строк и добавить в план. Если план превышает 3000 токенов, сжимайте его целиком.
def continue_story(chapters, outline, new_hint, keep_last=3):
"""chapters: 已写章节列表。只带最近 keep_last 章原文,更早的用 outline(滚动大纲)代替。"""
recent = "\n\n".join(chapters[-keep_last:])
prompt = (
f"【全书大纲(早期章节摘要)】\n{outline}\n\n"
f"【最近章节原文】\n{recent}\n\n"
f"【下一章要求】\n{new_hint}\n\n请写下一章,约 2000 字。"
)
return ask(prompt, max_tokens=4000)Закладки — то, что最容易 теряется. Выделите в плане отдельный раздел «Нераскрытые закладки» и требуйте обрабатывать одну из них в каждой главе. Также выведите список имен и локаций в system prompt, чтобы модель не меняла их при выходе за пределы окна.
Еще одна проблема при продолжении — дрейф стиля. Модель начинает подражать последним главам, что может исказить предыдущий стиль. Добавьте в system prompt «эталонный фрагмент» — отрывок текста, который вам нравится. Он будет служить якорем и не будет смещаться при движении окна.
max_tokens и обрезание
Обрезание ответа происходит по двум причинам:
- Если
finish_reasonравенlength, значит, вы уперлись в установленный max_tokens. Решение: увеличить max_tokens или попросить модель писать частями: «остановись здесь, продолжай, когда я скажу». - Прямая ошибка 400: сумма промпта и max_tokens превысила 100,000. Решение: сократите ввод или уменьшите max_tokens.
Практическая таблица соответствия бюджета:
| Задача | Рекомендуемое значение max_tokens | доступный промпт (приблизительно) |
|---|---|---|
| саммари, извлечение | 800 | 63,200 |
| продолжение главы (около 2000 слов) | 4000 | 60,000 |
| длинный вывод на уровне десятков тысяч слов | 16000 (лимит) | 48,000 |
Длинный вывод также зависит от времени ответа; рекомендуется использовать потоковую передачу, чтобы отображать результат по мере генерации.
Когда продолжение обрезается, не стоит просто передавать модели обрезанный фрагмент с командой «продолжить». Более надёжный подход — вернуть сгенерированную часть в сообщения в качестве assistant и добавить новое user-сообщение «продолжи с последнего предложения, не повторяя предыдущее». Это обеспечит наиболее естественную связность. Обратите внимание, что этот шаг увеличивает длину промпта, поэтому необходимо пересчитать бюджет.
Чек-лист для работы с длинным контекстом
- Перед каждым запросом рассчитывайте длину промпта с помощью функции оценки; если лимит превышен, используйте ветвление с разбивкой текста.
- Фиксируйте usage в каждом ответе и постоянно калибруйте коэффициенты оценки.
- Размещайте system-инструкции и план в самом начале, а ключевые требования из инструкций повторяйте в конце.
- Проверяйте finish_reason: если это length, выдавайте предупреждение или инициируйте продолжение.
- Установите лимит на историю диалога; при его превышении удаляйте самые старые сообщения, не дожидаясь ошибки 400 от API.
Похожие материалы: структура промптов см. в Гид по промптам, обработка ошибок — в Справочнике по кодам ошибок, тарификация — на Странице с ценами.
Помните: длинный контекст не означает долговременную память. Модель читает предоставленные вами данные с нуля при каждом запросе и не запоминает результаты предыдущих вызовов. Для сохранения непрерывности вы должны самостоятельно передавать историю.