Перейти к содержимому
25 нояб. 2025 г.·5 мин чтения

Стоп-последовательности в продакшене без мусора после JSON

Стоп-последовательности в продакшене помогают вовремя обрывать ответ модели после JSON, письма или цитаты без лишнего текста и поломки формата.

Стоп-последовательности в продакшене без мусора после JSON

Почему модель продолжает ответ после нужного места

Модель не видит границу ответа так, как ее видит ваш код. Для нее JSON, письмо или цитата - это просто продолжение текста. Пока генерация не упрется в лимит, stop-строку или явный сигнал остановки, она часто пишет дальше.

Одна инструкция в промпте помогает, но редко решает все. Фраза "верни только JSON" повышает шанс на чистый ответ, но не обрывает вывод сама по себе. Модель может закрыть объект и тут же добавить "Готово" или короткое пояснение. Человек это пропустит. Парсер - нет.

Хуже всего то, что ошибка долго прячется. На коротких ответах все выглядит нормально. Потом приходит длинный запрос, температура чуть выше, и после нужной скобки появляется хвост. Если вы гоняете несколько моделей через один API или через шлюз вроде AI Router, это видно сразу: одна модель останавливается после }, другая на той же инструкции дописывает еще строку. Значит, дело не только в промпте. Конец ответа надо задавать явно.

Где stop-токены действительно нужны

Stop-строки нужны там, где ответ модели сразу уходит в код, форму или другой строгий формат. Если текст читает человек, лишняя фраза обычно не страшна. Если ответ читает парсер, база данных, CRM или почтовый шаблон, одна лишняя строка уже ломает процесс.

Чаще всего это случается в четырех случаях:

  • JSON без лишнего текста
  • письма и шаблоны ответов
  • цитаты или короткие строки в интерфейсе
  • SQL, YAML, HTML и код без пояснений

Для JSON проблема банальна: объект уже закрыт, но модель добавляет "Готово". Для письма она может приписать служебную заметку или совет. Для цитаты достаточно одной строки после закрывающей кавычки, чтобы интерфейс показал мусор. В продакшене такие хвосты бьют по самым простым сценариям: падает парсинг, растут ретраи, очередь начинает тормозить.

Как stop-токены работают

Механика простая. Вы заранее передаете API одну или несколько строк, на которых ответ должен закончиться. Во время генерации сервис сверяет уже собранный текст с этим списком. Как только видит точное совпадение, обрывает вывод.

Если вы поставили stop на \nEND_JSON, модель может дойти до этой строки, но клиент получит только текст до нее. Сама стоп-строка в ответ обычно не попадает. Поэтому такой подход хорошо работает там, где после нужного блока не должно быть вообще ничего.

Есть важная тонкость. Символы и токены не совпадают один к одному. Короткая строка может состоять из нескольких токенов, а один токен может включать сразу несколько символов. На практике stop-токен удобнее считать не "спецтокеном", а правилом остановки по строке.

Хорошая stop-строка дает три вещи:

  • редко встречается в обычном тексте
  • однозначно показывает конец блока
  • ее легко проверить в тестах

Слишком короткие варианты вроде одной кавычки, одной скобки или общего слова END почти всегда дают ложные срабатывания.

Как выбрать stop-строку под ваш формат

Stop-строка должна совпадать не с красивым символом, а с реальной границей ответа. Для JSON одна } - плохой выбор. Она встречается внутри вложенных объектов и может оборвать ответ раньше времени.

Надежнее добавить отдельный маркер конца, который не входит в сам объект.

Верни только JSON.
После JSON выведи строку END_JSON

Тогда в API имеет смысл ставить stop на \nEND_JSON. Вы ловите конец всего блока, а не случайную скобку в середине.

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

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

Редкий маркер почти всегда надежнее знака препинания. Практичные варианты выглядят не очень красиво, зато работают предсказуемо: END_JSON_7X2, [[END_REPLY]], <END_QUOTE>. И обязательно проверьте пробелы и переводы строк. Ошибка в одном пробеле ломает остановку так же легко, как ошибка в коде.

Как настроить все по шагам

Проверьте stop на моделях
Прогоните один набор запросов через разные модели AI Router и найдите хвосты после JSON.

Начните с формата ответа, а не со списка stop-строк. Если вам нужен только JSON, так и пишите: один объект JSON, без пояснений, без приветствия, без текста после закрывающей скобки. Если нужен шаблон письма или одна цитата, задайте границы прямо в промпте.

Потом добавьте stop в API. Обычно хватает одной строки, которая явно означает конец ответа. Если вы отправляете запросы через OpenAI-совместимый шлюз, схема не меняется: в запросе есть промпт и массив stop. В случае AI Router логика такая же, потому что сервис использует OpenAI-совместимый эндпоинт.

Дальше полезен короткий рабочий порядок:

  1. Возьмите 20-30 реальных запросов, а не один удачный пример.
  2. Для каждого запроса отметьте, где ответ должен закончиться.
  3. Поставьте низкую temperature, если модель часто продолжает текст после нужного места.
  4. Прогоните ответы через тот же парсер, который будет работать в продакшене.
  5. Смотрите не только на удачные ответы, но и на редкие сбои.

Один хороший тест почти ничего не значит. Проблемы обычно всплывают на длинных входах, смешанных языках, пустых полях и неожиданных символах. Если модель иногда возвращает чистый JSON, а иногда добавляет строку после него, парсер сломается именно в бою.

Последняя проверка простая: ваш код должен принимать ответ без ручной обрезки, регулярных выражений и случайных "костылей" после инференса. Если без постобработки все еще плохо, причина обычно в слабом промпте, неудачной stop-строке или слишком свободных параметрах генерации.

Пример из продакшена

Обычный сценарий: сервис проверяет заявку и просит модель вернуть JSON с двумя полями - status и reason. Дальше этот ответ сразу читает код. Если JSON чистый, система идет дальше без участия человека.

Проблема возникает сразу после закрывающей скобки. Модель честно отдает объект, а потом дописывает еще одну строку для оператора. Для человека это мелочь. Для парсера - ошибка.

{"status":"reject","reason":"неверный формат документа"}
Пояснение для оператора: попросите клиента загрузить фото без бликов.

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

Обычно это чинят приземленно. Команда добавляет служебный маркер конца, например просит модель после объекта начать строку <END_JSON>, а в API ставит stop на этот маркер. Модель доходит до конца объекта, пытается продолжить, натыкается на правило остановки, и клиент получает только JSON.

Схема простая:

  • промпт требует один JSON-объект
  • после объекта модель должна начать строку <END_JSON>
  • API останавливает вывод на <END_JSON>
  • парсер видит только объект

Это не изящный трюк, а обычная защита. Ошибок парсинга становится меньше, ретраев тоже. Если вы переключаете модели через один шлюз, разница заметна еще сильнее: часть моделей чаще добавляет "пояснения от себя", а stop-строки выравнивают это поведение без переписывания клиентской логики.

Что ломает результат чаще всего

Тестируйте без смены клиента
Оставьте текущий код и меняйте только эндпоинт, чтобы быстро сравнить поведение моделей.

Большинство сбоев появляются из-за мелочей в формате.

Самая частая ошибка - слишком короткая stop-строка. Если поставить } как сигнал остановки, модель закроет первый попавшийся объект и не допишет остальной JSON. То же бывает с одной кавычкой, пустой строкой или общим словом вроде END.

Вторая проблема - стоп-маркер встречается внутри полезных данных. Если значение в JSON может содержать END или ###, модель остановится там, где не должна. Это часто всплывает в письмах, цитатах и карточках товаров.

Третья проблема - пробелы и переносы строк. Одна модель честно останавливается на \n\n###, другая печатает лишний пробел перед ###, и правило уже не срабатывает. Если команда использует несколько моделей, такие различия быстро вылезают наружу.

И, наконец, банальная вещь: в чате все проверили, а в боевой код забыли передать тот же параметр stop. Такое случается постоянно.

Перед релизом стоит быстро проверить пять вещей:

  • не слишком ли короткая stop-строка
  • не встречается ли она внутри данных
  • одинаково ли ведут себя пробелы и переносы строк на разных моделях
  • передает ли API тот же stop, что и тестовый стенд
  • не просит ли промпт объяснение после финального ответа

Если формат строгий, лучше взять редкую метку, которую нельзя встретить в данных, и прогнать ее на десятке реальных примеров. Это надежнее, чем один красивый тест.

Когда одной stop-строки мало

Одна stop-строка редко закрывает все случаи. Модель может закончить JSON на }, а может после него добавить перенос, комментарий или второй блок текста. Поэтому stop-правила лучше подбирать по реальным ответам, а не по одному удачному примеру.

Часто помогает набор из двух или трех маркеров. Для JSON это может быть отдельный конец блока и запасной маркер на случай, если модель пытается перейти к пояснению. Для письма полезны подпись, служебный разделитель и начало постскриптума. Для цитаты иногда нужен стоп по закрывающей кавычке и переносу строки.

Разделяйте режимы генерации

Не используйте один и тот же набор stop-строк для всех форматов. Если сервис иногда возвращает JSON, иногда письмо, а иногда короткую цитату, держите для каждого режима свой набор правил. Иначе маркер, который хорошо режет письмо, оборвет JSON в середине поля.

На практике удобно разнести режимы так:

  • json - только stop для структурированного ответа
  • email - stop для подписи и лишних блоков после письма
  • quote - stop для закрытия цитаты
  • free_text - минимум ограничений

Это особенно полезно, если вы меняете модели под цену, задержку или требования к данным. Через AI Router удобно прогонять один и тот же набор запросов на нескольких моделях и быстро видеть, где одинаковый промпт ведет себя по-разному.

Если вы используете стриминг, останавливайте вывод сразу, как только нашли совпадение. Не ждите полной сборки ответа на сервере или клиенте. И держите запасную защиту: валидируйте JSON, режьте хвост после допустимого конца, проверяйте закрывающие кавычки и скобки. Это не костыль, а нормальная страховка.

Быстрые проверки перед релизом

Запустите локальную модель
Проверьте open-weight модели на инфраструктуре AI Router, если важны задержка и контроль данных.

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

Что проверить перед выкладкой

  • Прогоните короткие, пустые, длинные и шумные входы.
  • Убедитесь, что stop-маркер не может случайно попасть в текст пользователя.
  • Смотрите не только на ответ модели, но и на поведение парсера.
  • Пишите в логи причину остановки: по какому маркеру модель закончила вывод и на каком шаге это произошло.
  • Держите набор stop-строк в конфиге и документации команды, чтобы staging и production не жили с разными правилами.

Один практичный тест экономит много времени. Возьмите шаблон вроде {"status":"ok","message":"..."} и прогоните его на реальных входах: пустой запрос, длинное письмо, кусок лога, текст с кавычками, текст с кодом. Если хотя бы в одном случае модель пишет комментарий после закрывающей скобки, выпускать такую настройку рано.

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

Что сделать дальше

Начните с небольшого набора живых примеров. Возьмите 15-30 запросов из вашего потока, где формат ответа уже важен: JSON для интеграции, письмо для CRM, цитату для интерфейса, короткий шаблон для внутреннего процесса. Не ограничивайтесь "идеальными" примерами. Добавьте случаи, где модель уже дописывала хвост после скобки, подписи или кавычки.

Потом прогоните этот набор хотя бы на двух или трех моделях. Одна остановится ровно, другая добавит пустую строку или короткое пояснение. Если вы сравниваете модели через airouter.kz или AI Router, удобно держать одинаковые stop-настройки и один набор тестов для всех прогонов. Так легче увидеть, где ломается формат и нужна другая stop-строка.

Проверьте четыре вещи:

  • останавливается ли ответ точно в нужном месте
  • не режется ли полезный текст раньше времени
  • одинаково ли ведут себя короткие и длинные ответы
  • меняется ли результат при смене модели

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

Этого обычно хватает, чтобы убрать большую часть сбоев. Один набор реальных примеров, сравнение на нескольких моделях и простой CI-тест дают больше пользы, чем бесконечная ручная правка промпта.