Гардрейлы для стриминга LLM: отправленный токен не вернуть

Гардрейлы для стриминга LLM: токен не отозвать. Как NeMo, Guardrails AI и Bedrock балансируют скорость и безопасность и как шлюз проверяет каждое утверждение.

Токен, который LLM уже отправила в поток, отозвать нельзя. Когда текст дошёл до читателя, гардрейл (guardrail), который против него возражает, может лишь остановить то, что идёт дальше. Поэтому у гардрейла на потоковом ответе по сути одно решение: проверять до выдачи и заставлять читателя ждать или сначала выдавать и мириться с тем, что заблокированный ответ уже прочитан. Рабочих схем три: проверять после выдачи, пропускать через шлюз чанки фиксированного размера или целые предложения. Ассистент, которого я сделал для этого сайта, работает со шлюзом по предложениям (sentence gate). Цена — ожидание длиной в одно предложение. Ограничение — шлюз проверяет, подтверждено ли утверждение источниками, а не истинно ли оно.

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

Почему отправленный токен нельзя вернуть

Валидатор, работающий постфактум, видит ответ целиком только после того, как читатель его прочёл. Если в третьем предложении выдумана дата, удаление задним числом меняет запись, а не то, что уже прочитано. Недавний препринт Кристофера М. Фроста (Christopher M. Frost) на arXiv точно формулирует эту границу: «механизм контроля, который обнаруживает запрещённый шаблон после выдачи завершающего его чанка, не может его отозвать».

Вендоры документируют то же ограничение. Асинхронный фильтр Azure OpenAI, который включается отдельно, возвращает текст сразу и гарантирует сигнал о нарушении лишь в пределах примерно 1 000 символов от нарушающего контента. В разделе об устранении неполадок совет такой: «реализовать в приложении скрытие контента, чтобы убирать уже показанный контент». Запись LLM09:2025 в списке OWASP рекомендует инструменты и процессы, позволяющие «автоматически проверять ключевые выходные данные». Открытым остаётся вопрос, когда проверка выполняется относительно выдачи.

Три способа защитить поток: постфактум, по чанкам, по предложениям

Подход

Когда читатель видит текст

Заблокированный ответ

Примеры

Постфактум (сначала выдача)

До проверки (NeMo: каждый чанк, как только он забуферизован; асинхронные режимы Azure и Bedrock: по мере генерации)

Частично прочитан до остановки

NeMo с включённым stream_first (по умолчанию); асинхронные режимы Azure и Bedrock

Шлюз по чанкам

После проверки каждого чанка

Заблокированный чанк никогда не показывается

Режимы по умолчанию в Azure и Bedrock; NeMo с выключенным stream_first

Шлюз по предложениям

После проверки каждого предложения

Заблокированное предложение никогда не показывается

Ассистент этого сайта

В чём компромисс stream_first в NeMo Guardrails

Стриминг для output rails в NeMo Guardrails по умолчанию выключен. Если его включить, output rails (проверки NeMo для сгенерированного текста) по умолчанию запускаются на чанках по 200 токенов, а 50 токенов контекста переносятся в следующий чанк. Флаг stream_first по умолчанию равен True: «Клиент получает каждый чанк токенов до того, как output rails обработают этот чанк. Это сокращает время до первого токена, но если rail блокирует контент, пользователь уже получил токены». После этого поток завершается ошибкой в формате JSON.

При False «пользователь никогда не видит заблокированный контент, но время до первого токена увеличивается на время выполнения rail для каждого чанка». Вот и весь компромисс: время до первого токена (TTFT) против заблокированного текста, который уже дошёл до читателя. Если стриминг для output rails включён, NeMo по умолчанию сначала выдаёт текст, а два облачных фильтра, о которых речь ниже, сначала проверяют. Если rail нужен, чтобы остановить то, что никогда не должно быть показано, измените значение по умолчанию.

В чём компромисс валидаторов по предложениям в Guardrails AI

Guardrails AI выбирает предложение: «По умолчанию валидаторы ждут, пока от LLM не накопится контент объёмом в одно предложение, и только затем запускают валидацию». Проверка отдельных предложений, а не всего ответа, «помогает экономить на валидаторах, которым нужен дорогой инференс».

Не все типы on_fail поддерживаются при стриминге, а исправления от нескольких валидаторов ждут, «пока все валидаторы не накопят достаточно чанков», и только потом объединяются. Документация прямо не говорит, доходят ли сырые токены до пользователя до валидации, поэтому проверьте это в своей интеграции.

Буферизация по умолчанию: Azure OpenAI и Amazon Bedrock

Azure OpenAI по умолчанию буферизует текст ответа модели, фильтрует буфер и возвращает «чанки контента», а не отдельные токены. Amazon Bedrock Guardrails в синхронном режиме, который используется по умолчанию, сканирует чанки перед отправкой; это «вносит некоторую задержку», но «обеспечивает более высокую точность». В асинхронном режиме Bedrock чанки «могут содержать неприемлемый контент, пока сканирование гардрейлами не завершится», а маскировать конфиденциальную информацию нельзя.

Как шлюз по предложениям проверяет поток

Ассистент на этом сайте отвечает только на основе контента самого сайта, а вопрос, который не касается меня и который не подтверждает ни один фрагмент, до модели вообще не доходит. Обо мне он говорит в третьем лице. На сайте, где стоит моё имя, худший сбой — выдуманная квалификация, а квалификация — это утверждение. Наименьшая единица, в которой помещается утверждение, — предложение, поэтому шлюз буферизует текст до границы предложения, проверяет и только после этого выдаёт. Демо «RAG против без RAG» в Лаборатории ИИ обращается к тому же потоковому эндпоинту.

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

Где кончается предложение, пока токены ещё поступают

  • Знаки конца предложения: . ! ? …, а также вопросительный знак ؟ и точка ۔ арабского письма, для китайского и хинди — 。 ! ?, данда । и двойная данда ॥. Последовательность вроде ?! считается одной границей. Точка с запятой арабского письма ؛ соединяет части предложения, поэтому её намеренно не учитывают.

  • После знака должен идти пробельный символ. Одно это правило сохраняет 3.14 и example.com целыми. Исключение — китайские точка, восклицательный и вопросительный знаки: после них пробел не ставится. Внутри цитаты они завершают предложение там, где цитата закрывается, и не завершают вовсе, если следом идёт запятая.

  • Не всякая точка завершает предложение. Точка перед строчной буквой его не завершает (et al. и), как и точка сокращения из короткого списка, например e.g., т. е. или немецкого bzw., и точка после числа в немецкой дате (am 17. September).

  • Решает следующий символ. Граница становится окончательной, только когда приходит символ, который не является ни пробелом, ни маркером ссылки, потому что маркеры после знака конца относятся к предыдущему предложению: все, [S1][S2] или [S1] [S2], а не только первый.

  • Маркер списка после границы — не предложение. Иначе 1. в начале ответа или нового предложения считался бы утверждением без ссылки и останавливал бы опирающийся на источники ответ на первом же пункте списка.

  • Ограничение буфера. Если за тысячу символов граница так и не встретилась, буфер обрезается по последнему пробелу и всё равно проверяется.

  • Пустая строка сохраняется. Предложение, перед которым стояла пустая строка, выдаётся тоже после пустой строки, так что ответ из двух абзацев остаётся двумя абзацами.

Ожидание следующего символа обычно даёт задержку в один символ и чуть больше, если после точки идут маркеры. Последнее предложение проверяется, когда поток завершается. При сравнении к единой форме приводятся персидские цифры и разделители (۳٫۱۴ совпадает с 3.14) и арабские начертания букв, арабские знаки кратких гласных удаляются, а несоединяющий символ нулевой ширины (ZWNJ) считается пробелом: это те же варианты, с которыми на стороне поиска работает гибридный поиск для персидского RAG.

Четыре детерминированные проверки: ссылки, значения и лицо

  1. Диапазон ссылок. Маркер вроде [S7], когда передано шесть источников, указывает на выдуманный источник.

  2. Обязательная ссылка. Предложению из восьми и более слов или с любым числом, URL или адресом электронной почты нужен маркер. Короткие связующие предложения проходят.

  3. Дословное подтверждение. Каждое число, URL и адрес электронной почты должны присутствовать в источнике, на который ссылается предложение. Сопоставление учитывает границы цифр, поэтому год в источнике не может подтвердить процент, совпадающий с его последними цифрами, а десятичные числа, отличающиеся только нулями в конце, считаются одним значением: 20.00 — это 20. Запятая, которая не отделяет тысячи, — десятичная: 2,5 — это 2.5, а не 25.

  4. Грамматическое лицо. Слово в первом лице останавливает поток. Английский список короткий и фиксированный: местоимения первого лица и их сокращённые формы. Персидский список составлен вручную: местоимения и несколько глагольных форм с высоким риском, например персидские формы со значением «являюсь» и «имею». В персидском лицо выражается глагольными окончаниями, которые могут совпадать с окончаниями обычных существительных, поэтому правило, достаточно широкое, чтобы ловить любое такое окончание, ловило бы и существительные. Списки для остальных языков сайта содержат личные и притяжательные местоимения, а списки для испанского, португальского, русского, арабского и хинди добавляют к ним несколько глаголов, потому что в этих языках «я» обычно выражается одним глаголом; в китайском 我 ищется в любом месте предложения, кроме сложных слов с 自我, «само-», таких как 自我修正 («самокоррекция»). Какие списки применяются, зависит от языка, на котором был запрошен ответ. Английский, персидский, арабский, русский, хинди и китайский списки применяются к каждому ответу. Английский — язык инструкций модели, поэтому ответ, ушедший от запрошенного языка, скорее всего перейдёт именно на него; остальные пять используют письменности, в которых никакое латинское имя или аббревиатура не может совпасть с их словами. Исключение — ответ, запрошенный на арабском: в нём не ищутся персидские «я» и «мы», потому что по-арабски те же слова значат «из» и «что». Немецкий, французский, испанский и португальский списки применяются только к ответам, запрошенным на их языке: их короткие слова бывают и именами, и обычными словами других языков, как «Ma» в «Ma et al.». Аббревиатуры из заглавных букв, фамилия перед «et al.» и римская цифра в «Приложении I» отбрасываются до того, как читается любой список.

Что происходит, если предложение не прошло проверку

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

  • Шлюз останавливается и больше не открывается: одно неподтверждённое предложение дискредитирует всё остальное.

  • Событие guard передаёт причины нарушения и фиксированный локализованный текст о том, что остаток ответа удержан, а финальное событие done фиксирует остановку. Заблокированное предложение никогда не отправляется; сервер записывает в лог только его длину.

  • Поток провайдера закрывается: именно так в этой архитектуре отменяется запрос к провайдеру. То, что успело сгенерироваться до остановки, всё равно попадает в счёт, поэтому учитывается и в месячном лимите расходов. Оба протокола провайдеров сообщают расход выходных токенов в финальном событии, до которого оборванный поток не доходит, поэтому сервис сохраняет полученный текст и дополняет то, о чём провайдер не отчитался, по одному токену на байт UTF-8 — счёт, который, если и ошибается, то в большую сторону. Токены, которые модель с рассуждениями тратит на размышление, никогда не приходят в виде текста, поэтому в эту оценку они не попадают. Лимит разобран в статье о пятиуровневой схеме.

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

Обрезка ответа — другой сбой, и ранняя версия этого ассистента их путала. Когда у модели заканчивалось место для вывода, шлюз считал обрезанный фрагмент законченным предложением и удерживал его как утверждение без ссылки. Читателю сообщали, что ответ, который всего лишь прервали, источниками не подтверждается. А если текста не приходило вовсе, реплика исчезала без всякого сообщения. Теперь незаконченный фрагмент отбрасывается без проверки и показывается уведомление об обрыве. Прерванное предложение — не то же самое, что неподтверждённое. Если случилось и то и другое, приоритет у остановки шлюзом.

Маркеры ссылок остаются внутренними

Промпт требует маркер [Sn] в каждом предложении, которое сообщает факт, а шлюз применяет правило обязательной ссылки к каждому содержательному предложению. Когда предложение прошло проверку, маркеры удаляются, а пробелы исправляются, в том числе перед персидскими знаками препинания, так что читатель видит связный текст, а не маркеры. Если вместо этого убрать маркеры из промпта, каждое содержательное предложение будет отклонено по правилу обязательной ссылки.

Когда модели нечего сказать

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

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

Проверить «подтверждено» не значит проверить «верно»

В исследовании под руководством Стэнфорда Магеш (Magesh) и соавторы провели, по их собственному описанию, первую предварительно зарегистрированную оценку ИИ-инструментов для юридических исследований. Каждый из ИИ-инструментов для исследований от LexisNexis и Thomson Reuters галлюцинировал в диапазоне от 17% до 33% случаев. Ответ считался галлюцинацией, если он был «либо неверным, либо с неверной опорой на источник (misgrounded)», а неверная опора означает, что «ключевые фактические утверждения снабжены ссылками, но неверно истолковывают источник или ссылаются на неприменимый источник». У ответов с неверной опорой на источник ссылки есть. О том, что это значит для юридических ИИ-продуктов, есть отдельная статья.

Шлюз, проверяющий строки, неверную опору на источник не видит, и у этого шлюза есть известные слепые зоны:

  • Предложение со ссылками на два источника может взять значение из любого из них. Проверка идёт по предложению, а не по его частям.

  • Десятичная точка считается границей. 14.52 в источнике может подтвердить 52% или 14, и разделитель разрядов действует так же: 1,200 подтверждает 200.

  • Слова не проверяются. Предложение без чисел, в котором подменены роль или организация, проходит с любым маркером из допустимого диапазона.

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

  • Фиксированные списки слов неполны. В английском проходят we'd и let's, а также местоимение, написанное целиком заглавными буквами, например WE или US: оно читается как аббревиатура. Охват первого лица в персидском держится на промпте: глагол в первом лице, которого нет в составленном вручную списке, проходит. Списки для остальных языков тоже составлены вручную, а первое лицо на немецком, французском, испанском или португальском в ответе, запрошенном на другом языке, остаётся заботой промпта.

  • Закрывающие знаки могут склеивать предложения. После кавычки, закрывающейся на знаке конца предложения, как в ?» или .", предложение завершает только заглавная буква или перевод строки, а после скобки, как в .), — только перевод строки или маркер ссылки. Поэтому два предложения могут оцениваться как одно: после .) перед заглавной буквой, после ؟» в арабском, персидском и хинди, где нет заглавных букв, и после немецких кавычек „…?“ и »…?«, которые сплиттер вообще не считает закрывающими. Для китайских знаков действует своё правило, описанное выше.

Ошибка бывает и обратной. Сокращение не из списка, после которого идёт заглавная буква, преждевременно завершает «предложение», как Mr. перед именем, и так же действует вопрос в кавычках, за которым идёт имя, а в немецком — любое существительное, и если в этом фрагменте восемь или больше слов и нет маркера, ответ, опирающийся на источники, удерживается. У списка, который вводится строкой с двоеточием на конце, нет границы перед маркером первого пункта. Сплиттер режет сразу после 1., и этот фрагмент не проходит правило обязательной ссылки. В обоих случаях шлюз ошибается в безопасную сторону (fail closed): он скорее удержит ответ, опирающийся на источники, чем выдаст непроверенный.

Корпус тоже меняется. Опубликованные статьи индексируются для ассистента, поэтому их цифры, включая 17% и 33% из этой статьи, становятся значениями, которые шлюз сочтёт подтверждёнными. «Подтверждено» означает «встречается где-то в том, на что ссылается предложение», а не «относится именно к этому».

Шлюз — последняя проверка перед выдачей, но не единственная. Джорен (Joren) и соавторы обнаружили, что более крупные модели при недостаточном контексте «часто выдают неверные ответы вместо того, чтобы воздержаться», а Google Research отмечает, что RAG в целом улучшает качество работы системы, но «парадоксальным образом снижает способность модели воздерживаться от ответа, когда это уместно». Поэтому сначала идут отказ и порог оценки, как описано в пятиуровневой схеме. Препринт Фроста формулирует ограничение собственного результата словами, которые подходят и к этому шлюзу: «соответствие политике для детерминированного последнего рубежа, а не свидетельство семантической безопасности или полноты политики».

SSE для стриминга LLM: fetch, прокси и отмена запроса

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

Почему fetch и ReadableStream, а не EventSource

Спецификация WHATWG даёт конструктору EventSource URL и одну опцию, withCredentials: ни метода, ни тела запроса. Вопрос вместе с локалью и историей — это тело запроса, поэтому эндпоинт принимает POST и возвращает Server-Sent Events. Браузер читает ответ через fetch, reader потока ReadableStream и AbortController, чей abort(), по формулировке MDN, «может прерывать fetch-запросы, потребление любых тел ответов и потоки».

Парсеру, написанному вручную, нужны такие правила:

  • Буферизуйте данные между чтениями. Чанк — это не сообщение.

  • Передавайте событие дальше по пустой строке. Пока она не пришла, событие не завершено.

  • Игнорируйте строки-комментарии — те, что начинаются с :.

  • Склеивайте многострочные данные: все строки data: внутри одного события соединяются через перевод строки.

  • Принимайте CRLF, LF и CR как концы строк.

  • Удаляйте один пробел после двоеточия, и только один.

Декодируйте через TextDecoder с {stream: true} — эту настройку MDN указывает для случая, «если данные обрабатываются чанками». Иначе персидский символ, разделённый между двумя чанками, декодируется в символы замены.

Буферизация в прокси: nginx и Caddy

nginx по умолчанию буферизует проксируемые ответы, а заголовок ответа X-Accel-Buffering позволяет это отключить. Поток ассистента отправляет X-Accel-Buffering: no и Cache-Control: no-cache, no-store, no-transform; no-transform нужен потому, что прокси, который пережимает тело ответа, может склеивать чанки. Caddy сразу сбрасывает (flush) ответы text/event-stream, а в конфигурации Caddy этого сайта на маршруте чата дополнительно задано flush_interval -1. Service worker не регистрирует маршрут для потока, потому что стратегия кэширования удерживала бы ответ до его окончания.

Keep-alive и ошибки внутри открытого потока

Спецификация SSE предлагает отправлять строку-комментарий «примерно каждые 15 секунд», чтобы защититься от устаревших прокси, которые разрывают соединение после короткого таймаута, и сервер шлёт пинги с таким интервалом. Когда уходит первый байт, статус уже 200, поэтому сбои передаются событием error внутри потока.

Отмена, которая останавливает и расходы

Остановка в браузере прерывает fetch. Сервер при поступлении каждого токена проверяет, не отключился ли клиент. Обнаружив разрыв, он выходит из цикла и закрывает поток провайдера, что в этой архитектуре отменяет запрос к провайдеру. Две вещи стоит проверить тестом, а не принимать на веру. Во-первых, что каждый прокси и каждый CDN между браузером и вашим API передаёт разрыв соединения дальше. Проверьте этот путь тестом; не полагайтесь на конфигурацию. Во-вторых, в какой момент ваш провайдер сообщает usage: если данные о выходных токенах приходят только с последним чанком, досрочно закрытый поток их так и не получит, и лимит расходов, который считает заявленное провайдером потребление, занизит объём сгенерированного.

Как тестировать гардрейл для потокового ответа

Тесты покрывают шлюз на трёх уровнях:

  • Модульные тесты шлюза на фиксированных источниках. Выдуманные значения, маркеры вне диапазона, утверждения без ссылок и слова в первом лице (на каждом из языков сайта) останавливают поток; подтверждённые значения и короткие связующие предложения проходят; ранее прошедшие хорошие предложения сохраняются, даже если следом идёт плохое.

  • Тесты сплиттера. Десятичные дроби и домены не считаются границами, маркеры в конце остаются со своим предложением, а ؟ завершает предложение. Китайский текст делится на предложения без пробелов. Тест, который стоит перенять: текст, поданный кусками по три символа, должен разбиваться точно так же, как весь текст целиком.

  • Тесты потока с провайдером-заглушкой, который по сценарию воспроизводит ответ мелкими кусками: выдуманное значение никогда не попадает в сеть, остановка шлюзом или разрыв соединения закрывают поток провайдера, а обрезка ответа сообщается именно как обрезка.

Набросок двух ключевых тестов, написанный для этой статьи, а не взятый из кода сайта:

# Sketch only: SentenceSplitter and SentenceGate stand for your own classes.
import pytest

ANSWER = "The reading room opens early [S1]. Is the café open? Yes, on weekdays [S2]."

def pieces(text, size):
    return [text[i:i + size] for i in range(0, len(text), size)]

def split_streamed(text, size):
    splitter = SentenceSplitter()
    sentences = [s for piece in pieces(text, size) for s in splitter.feed(piece)]
    tail = splitter.flush()
    return sentences + ([tail] if tail else [])

@pytest.mark.parametrize("size", [1, 2, 3, 7])
def test_piece_size_does_not_change_sentences(size):
    assert split_streamed(ANSWER, size) == split_streamed(ANSWER, len(ANSWER))

def test_unsupported_value_never_reaches_the_reader():
    gate = SentenceGate(sources=["The library opens at nine."])
    released = ""
    for piece in pieces("It opens at nine [S1]. Late fees ended in 2019 [S1]. More", 7):
        outcome = gate.feed(piece)
        released += outcome.text
        if outcome.stopped:
            break
    assert "2019" not in released and gate.stopped

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

Чек-лист: гардрейл для потокового ответа перед запуском

  • Решите, что никогда не должно быть показано. От этого зависит выбор между «сначала проверка» и «сначала выдача», каким бы ни было значение по умолчанию в вашем фреймворке.

  • Выберите единицу проверки и назовите цену ожидания: чанк фиксированного размера или предложение.

  • Используйте детерминированные проверки там, где достаточно строк, предварительно приведя цифры и начертания букв к единой форме. Rail на основе модели добавляет время своего выполнения к каждой единице.

  • Считайте границу определённой только после прихода следующего символа и ограничьте размер буфера.

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

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

  • Транспорт: POST через fetch, reader потока и AbortController; потоковый декодер; пинги строками-комментариями; ошибки как события.

  • Прокси: X-Accel-Buffering: no, no-transform, немедленный сброс event-stream, проверенный тестом путь разрыва соединения и никакого service worker на потоке.

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

Если вы проектируете ассистента, который отвечает с опорой на источники, и хотите, чтобы на путь выдачи его ответов посмотрел ещё один специалист, этот разговор начинается на странице «Диагностика проекта».

Частые вопросы

Можно ли проверять потоковый вывод LLM, не отказываясь от стриминга?

Да. Буферизуйте текст до единицы, которую можно проверить, и выдавайте каждую единицу, как только она прошла проверку. Читатель видит, как текст появляется по предложению или по чанку, а не по токену.

Увеличивает ли буферизация до конца предложения время до первого токена?

Она задерживает видимый вывод на одно предложение: текст появляется, когда первое предложение завершено и проверено. Строковые проверки дёшевы по сравнению с генерацией, но на этом сайте я эту разницу не измерял, поэтому цифры не привожу.

Стоит ли отключать stream_first в NeMo Guardrails?

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

Почему не стоит использовать EventSource для стриминга LLM?

EventSource принимает URL и флаг учётных данных, поэтому не может отправить тело запроса. Запрос к чату с историей — это POST, так что используйте fetch с reader потока: заодно он даёт отмену через AbortController.

Останавливает ли проверка ссылок галлюцинации?

Нет. Она останавливает выдуманные номера источников, содержательные предложения без ссылок и значения, которых нет в источниках, на которые ссылается предложение. Она не может определить, подтверждает ли процитированный источник утверждение, а это и есть сбой с неверной опорой на источник. Считайте её последним рубежом, который стоит за отказом и порогом релевантности поиска.

Источники