Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

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

Дата публикации: 24-09-2026 20:53:55

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

Основное содержимое страницы с новостью.

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

Уровень сложностиСредний

Время на прочтение3 мин

Охват и читатели8K

Кейс

Мы делаем русскоязычный сервис с несколькими языковыми моделями в одном чате. Однажды партнёр прислал скриншот: модель начала отвечать списком из ста пунктов и оборвалась на середине слова. В админке лежал тот же обрывок. Ни ошибки, ни предупреждения.

Почему ответ обрывается. У каждого запроса есть потолок max_tokens. У нас он был 1200 токенов. На английском это около 900 слов, а на русском кириллица съедает больше токенов, и 1200 токенов оказались примерно полутора тысячами знаков. Длинный список просто не помещался. Модель при этом возвращает finish_reason: "length" (у некоторых поставщиков max_tokens), и это единственный признак обрыва.

Поднять потолок — полумера: ответ всё равно может оказаться длиннее, а каждый лишний токен в потолке увеличивает сумму, которую мы резервируем у человека до ответа. Мы подняли его до 2400 и сделали продолжение.

Продолжение. Сервер отдаёт клиенту флаг truncated, если finish_reason сказал, что ответ упёрся в потолок. Клиент сам отправляет модели служебную просьбу продолжить с того же места и прикладывает конец уже написанного. Весь ответ приложить нельзя: сервер режет сообщение до 6000 знаков, поэтому берём последние 5800. Модели нужен именно конец, начало ей для продолжения не важно. Продолжений не больше четырёх, чтобы один вопрос не превратился в бесконечную переписку за счёт человека.

Самое интересное — склейка. Первая версия просто прибавляла продолжение к тексту, и сразу полезли артефакты.

Первый: модель повторяет оборванную строку. Было «102. ии», продолжение начиналось с «102. ии и нейросети…», и в чате появлялось «102. ии102. ии и нейросети».

Второй: модель начинает строку заново. Оборванный хвост «Лучшие серви» и продолжение «Лучшие сервисы для…» склеивались в «Лучшие сервиЛучшие сервисы».

Третий: модель начинает со следующего пункта, пропустив перенос. «…последний пункт» и «205. Следующий» давали «последний пункт205. Следующий», и разметка списка ломалась.

Получилась функция из трёх правил:

function mergeContinuation(prev, next) {
  const a = String(prev || '');
  const b = String(next || '');
  if (!a) return b;
  const lead = b.match(/^\s*/)[0];
  const body = b.slice(lead.length);
  // 1) продолжение повторяет конец написанного: повтор убираем
  for (let k = Math.min(400, a.length, body.length); k >= 3; k -= 1) {
    const piece = body.slice(0, k);
    if (!a.endsWith(piece)) continue;
    const before = a[a.length - k - 1];
    // короткое совпадение — только с границы слова
    if (k >= 12 || before === undefined || /\s/.test(before)) return a + body.slice(k);
  }
  // 2) модель начала оборванную строку заново: хвост заменяем новой строкой
  const cut = a.lastIndexOf('\n');
  const lastLine = a.slice(cut + 1);
  if (lastLine.trim().length >= 4 && body.startsWith(lastLine.trim())) return a.slice(0, cut + 1) + body;
  // 3) модель начала со следующего пункта, таблицы или заголовка без переноса строки
  if (!lead.includes('\n') && !a.endsWith('\n') && /^(\d+[.)]\s|[-*•]\s|\||#{1,6}\s|>\s)/.test(body)) return a + '\n' + body;
  return a + b;
}

Тонкое место — правило 1 на коротких совпадениях. Если искать любое совпадение конца и начала от трёх символов, «мейк» + «ейкеры» съест буквы: «ейк» совпадёт случайно. Поэтому короткие совпадения (меньше 12 символов) принимаем только с границы слова, а длинные — всегда: случайно совпасть 12 символам почти невозможно.

Мы прогнали функцию на 13 случаях из реальных обрывов и придуманных краевых: пустое начало, продолжение с переносом, таблица, заголовок, совпадение внутри слова. Та же функция стоит и на сервере, чтобы в истории чатов ответ лежал одной записью, а не четырьмя кусками.

Что ещё пришлось сделать.

  • Деньги. Каждое продолжение — отдельный платный запрос. Мы резервируем сумму до вызова модели, списываем фактическую стоимость по usage, который вернул поставщик, и возвращаем резерв, если модель ответила ошибкой. Продолжение списывается так же и дописывается к той же записи в журнале.

  • Модели с обязательными рассуждениями. Они тратят часть потолка на рассуждение, и обрыв у них случается раньше, чем кажется по длине видимого ответа.

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

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Промпт — не контракт: замерил, как часто модель нарушает жёсткие правила, и вынес их в код07.3222-09-2026
2RuntimeNodes: как мы, ML‑щики, стали писать рантайм07.7424-09-2026
3Каждый LLM-фреймворк описывает инструмент по-своему09.8930-09-2026
4Омнимодель для Алисы AI: как нам удалось подружить VLM и LLM07.403-09-2026
5AGI как когнитивная ОС: что если искать программы, а не веса06.3225-09-2026
6FAIRouter. Мы научили ИИ выбирать лучшую LLM модель для каждой задачи08.0902-10-2026
7Зачем нейросети «обвязка»: как превратить LLM в рабочий AI-сервис09.9730-09-2026
8Rust на вырост. Почему мы изучаем язык, который не входит в наш основной стек-17.2410-09-2026
9LLM уверенно называет шахматные ходы, которых на доске нет. Как я проверяю каждый ее ответ кодом05.8726-09-2026
10Положил http:// в строку, и ассемблер молча собрал 0 байт07.7909-09-2026

Классификация: Мнения. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 6. Источник: habr.com.