Тема в исследовании. Повторяющийся прод-issue: Gemini посреди генерации иногда отдаёт обрезанный или мусорный output — либо честно дописал слишком длинный ответ и упёрся в maxOutputTokens, либо ушёл в дегенеративный repetition-loop (повторяет один токен — nnnn…, переводы строк — до лимита), либо завис так, что вызов не возвращается и не ошибается. На проде это видно как «много error-трейсов, Gemini очень много отвечает MAX_TOKENS, плюс не успевает вернуть ответ за 180 s → запрос фоллбэчим на GPT». Бьёт по Gemini-heavy structured-output вызовам в разных местах пайплайна (recognize, validity-классификатор и т.д. — не два примера, а класс). Известный тикет — BG-1066 (recognize: JSON-parse error на обрезанном ответе). Эта страница — что это за класс проблемы, чем с ней борются, и какие открытые вопросы; все подходы по сути про детекцию + перезапуск, и нюансы — именно в «как детектить» и «как перезапускать».

Проявления

  • (а) Честно слишком длинный ответ — модель не зациклена, просто ответ не влез в maxOutputTokens → обрезанный (часто невалидный) JSON. Лечится бо́льшим лимитом (BG-1066: DEFAULT_MAX_TOKENS 16k → 65536).
  • (б) Дегенеративный repetition-loop (он же «doom loop» в community-нотации1) — модель повторяет один токен (unit: nnnnnnn…The …) или короткую подстроку с минимальными вариациями пока не упрётся в лимит. Бо́льший maxOutputTokens НЕ лечит (просто больше n-ов). Известная болячка decoder-only LLM в целом, особенно на greedy / temperature 0 (что у нас и стоит); у Gemini-через-Vertex заметна. Google официально подтвердил эту связь — Jon_Matthews из Google в ответе на жалобу на «infinite reasoning loop / max token exhaustion» в Gemini 3 Flash Preview: «I’d recommend you set the temperature to 1.0. The reasoning process requires a certain degree of probabilistic freedom, if you set the temperature to 0.0, the model can become trapped in the single highest-probability path.»2. Workaround frequency_penalty нам недоступен — Google официально подтвердил, что параметр поддерживается только на Gemini 2.0 Flash, не на 2.5+1.
  • (в) Зависание mid-decode — вызов не возвращается и не ошибается; ловится только client-side таймаутом. Корреляция с «under load» (Vertex-side).

Все три читаются как finishReason != STOP (обычно MAX_TOKENS, иногда SAFETY) — если модель успела вернуть ответ; иначе как таймаут. Где встречается: recognize (analyzeBatchPdfsWithStructuredOutput — truncated JSON, BG-1066; и в .NET, и в Node.js), validity-классификатор (батчи — обычно одно «ядовитое» наблюдение, остальные элементы батча без него проходят), и вообще много где на проде (Gemini-heavy structured-output). Список не исчерпывающий — поиск по «MaxTokens» / MAX_TOKENS в Slack и Langfuse даёт ещё.

Детекция — как понять, что это случилось

  • finishReason != STOP — самый чистый сигнал: HTTP 200, но finishReason = MAX_TOKENS / SAFETY / … ⇒ контент обрезан/заблокирован. В .NET-стеке такой трейс теперь помечается level: ERROR + статус-сообщением, и бросается исключение (skip бессмысленной десериализации) → триггерит retry/fallback (blood-gpt-dotnet commit a9b75b27, Mar 2026). В Node.js-стеке finishReason от generateObject пока не проверяется на не-STOP — добавить.
  • Таймаут как неявный детектор — «Gemini завис с Max Tokens, не ждём больше». .NET: «Gemini operation canceled». Recognize (Node.js): для gemini-flash ~20 s → «считаем, что это MAX_TOKENS» (либо 503/499). test_overview / recognize_gpt / validity: ~180 s / 90 s.
  • JSON-parse error («Expected double-quoted property name in JSON at position …») — downstream-симптом обрезки, не сам детектор; сигнал, что вход прилетел уже битый (BG-1066).
  • Эвристик на repetition — длинный run одного символа / повторяющейся короткой подстроки в строковом поле вывода ⇒ подозрение на (б). Системно нигде не делается; validateClassifierOutput ловит структурные галлюцинации, но не unit: nnnn… (unit опционален, по содержимому не проверяется). Кандидат.

Перезапуск — как восстановиться

  • Просто retry — иногда хватает (transient).
  • Retry с пертурбацией промпта — добавить nonce в HTML-комменте → меняет префикс промпта → промах prefix-кэша Vertex → другая decoder-trajectory → часто выскакивает из цикла. Модель и temperature постоянны (transport-retry, не model-resample). Validity-классификатор делает это (callOnce).
  • Бисекция — батч N>1 терминально упал ⇒ делим пополам, гоняем каждую половину заново (меньше payload) — если триггер был в одном элементе, изолируется до 1-obs-фейла. Validity делает это (classifyBatchWithSplit, до VALIDITY_SPLIT_MAX_DEPTH). Применимо там, где вход — батч однотипных элементов.
  • Fallback на другую модель (GPT) — при finishReason != STOP или таймауте. Recognize / .NET делают это.
  • Бо́льший maxOutputTokens — только для проявления (а) «честно слишком длинно» (BG-1066). Проявление (б) не лечит.
  • «Дать дозациклиться до конца» — не решение. 180 s-таймаут на recognize = «не убивать раньше, чтобы хоть что-то вернулось» — но если вернулось nnnn…The …, это всё равно мусор, нужен один из перезапусков выше.

Механики validity-обходов (cleanSchema превентивно стрипает schema-артефакты, на которых Vertex чаще срывается в structured-output; nonce; бисекция) — подробно в validity-classifier § «Три уровня обработки» (один LLM-вызов / один прогон) и § «Классы сбоев».

Идея — модель-интроспекция как пост-мортем дебаг

Не real-time детекция и не перезапуск, а отдельный класс: прогнать ту же историю сообщений ещё раз и спросить у модели, почему она так ответила — какие именно строки промпта повлияли. Артур описывает это как рабочую технику для выяснения, где промпт вводит модель в заблуждение: «беру полностью её ответ, всю ту же самую историю сообщений, отправляю и спрашиваю, почему она приняла такой выбор, какие конкретно строки инструкции заставили её — и они очень часто конкретно отвечают».

Применимо к этому классу проблем (truncated / repetition-loop / hung) — но не как сигнал в проде, а как diagnostics-инструмент: взять зафейленный трейс из Langfuse, рестартовать его в evaluation-режиме с дополнительным вопросом «объясни». В сторону «как репортить Vertex/Google» это тоже полезно — конкретные строки промпта или инпута, которые модель сама называет триггером, лучше любого нашего guess’а.

Не systematic — нет workflow, нет инструментирования. Кандидат на маленький eval-скрипт «возьми trace-id → перезапусти с приклеенным ?why промптом»3.

Где это инкапсулировать

Это сквозная проблема всех Gemini-структурированных вызовов, не специфика одного агента. Сейчас обработка размазана и переизобретена в каждом месте: .NET-LLM-клиент (per-prompt maxTokens-конфиг + finishReason-check + timeout-as-detector + fallback); validity-классификатор (callOnce / classifyBatchWithSplit); recognize (свой timeout + fallback). Это всё самодельная in-app retry-логика — анти-паттерн (no-self-rolled-queues). Логично собрать в одном месте, инкапсулировать, и переиспользовать: либо на LLM-прокси-слое (bifrost / llm-proxy-choice) — как model-fallback и оркестрация уже вынесены на инфра-слой, — либо общим SDK-врапером поверх generateObject. Что для прокси нужно: почитать Bifrost docs / plugin-модель — позволяют ли pre/post-hooks: явный per-request maxTokens; reject-on-finishReason != STOP; per-request timeout; retry-with-prompt-perturbation; output-validator-плагин (repetition-эвристик); fallback на другую модель при detected-loop. Тогда recognize + validity + chat + всё остальное получают это даром, а не переизобретают.

Открытые вопросы

  • maxOutputTokens в Node.js-стеке не выставляется осознанно (validity generateObject — без maxTokens; recognize — частично, BG-1066 поднял DEFAULT_MAX_TOKENS до 65536). Выставить явно везде → детерминированный finishReason: MAX_TOKENS вместо «висит до таймаута». (В .NET — есть per-prompt конфиг, Realai-plus/.github-private/docs/models-policy.md.)
  • finishReason в Node.js не проверяется на не-STOP (в отличие от .NET после a9b75b27) — добавить как детектор.
  • temperature 0 vs официальная рекомендация Google 1.0 — мы держим temperature 0 ради детерминизма structured-output (одинаковый input → одинаковый output для evaluations), но Google прямо рекомендует 1.0 чтобы не упирать модель в repetition-loop (см. цитату выше). Trade-off: детерминизм для тестов vs устойчивость в проде. Сейчас работаем через transport-retry с nonce’ом (косвенно «расшатываем» детерминизм при стуке в loop), но это hack. Открытое: переходить ли на temperature > 0 для прод-вызовов и держать 0 только в eval; либо мириться с retry-механикой как есть.
  • Природа триггера — конкретные токены/строки во входе? «under load» / Vertex capacity? structured-output / responseSchema усугубляет? — добиться детерминированного репро (есть eval/validity/DEBUG_METHODOLOGY.md); баг Vertex/Gemini → зарепортить Google.
  • Vertex-specific или нет — GPT-фоллбэк зацикливается ли так же? Если нет — при detected-loop сразу на fallback, а не nonce-retry той же модели.
  • Насколько часто на проде сейчас (после BG-1066) — Langfuse: recognize-трейсы в lf.tryreal.tech project cmbc586n7004ruh07l25slrud (а также cm3n45n8w02dzsvo6ehqei3lt — поиск по MAX_TOKENS на recognize_gemini); validity — где трейсится TBD. Калибрует приоритет.
  • Что переносится из .NET-handling (per-prompt maxTokens config; finishReason-check a9b75b27; timeout-as-detector «operation canceled») в новый стек / на прокси.
  • nonce-пертурбация бьёт prompt-кэш — норм для застрявшего запроса, но при слишком мелкой Inngest-нарезке (per-batch шаги, каждый с nonce’ом) кэш-hit-rate падает по всему фронту (⊥ inngest-pipeline-orchestration-vs-choreography, biomarker-actuality-integration § Inngest-декомпозиция).

Связано

  • llm-call-failure-classes — общая таксономия классов сбоев LLM-вызова (транспорт / схема / семантика); это явление — конкретный кейс классов «транспорт» + «схема» для Gemini-через-Vertex
  • validity-classifier § «Три уровня обработки» (один LLM-вызов) / § «Классы сбоев» — где реализованы validity-обходы (cleanSchema / nonce / бисекция), константы
  • recognition — recognize-handling: ~20 s gemini-flash timeout, fallback на GPT, BG-1066 (truncation detection + DEFAULT_MAX_TOKENS 16k→65536)
  • bifrost / llm-proxy-choice — кандидатный «дом» для инкапсулированной detect+restart-machinery (как model-fallback — на прокси)
  • no-self-rolled-queues — обходы в коде агентов = self-rolled retry-логика, анти-паттерн как есть (и .NET, и validity, и recognize переизобрели по-своему)
  • inngest-pipeline-orchestration-vs-choreography / biomarker-actuality-integration — nonce-vs-prompt-кэш влияет на гранулярность Inngest-нарезки
  • gemini-flash-vs-pro-allocation — где какая модель; и recognize, и validity — на Gemini Flash через Vertex

Carry-over: posts/structured-output-llm-production.md (рисёрч-доку Ильдара) — смежная тема (надёжность structured output в проде), стоит свести сюда или в отдельную страницу.

  • Slack (Realai, #bg-qa / #dev / #ai-engineering, Aug 2025 – Apr 2026): «на проде много error-трейсов, Gemini очень много MAX_TOKENS … не успевает за 180 s → фоллбэк на GPT»; «Gemini operation canceled — таймаут, когда считаем, что Gemini завис с Max Tokens»; recognize — «за 20 s gemini-flash не справился → считаем MAX_TOKENS, либо 503/499»; исходный тред про unit: nnnn… в Bilirubin, Totalrealaicorp.slack.com/archives/C094GRT3CBY/p1759834543679359; Langfuse-ссылки на MAX_TOKENS-трейсы (lf.tryreal.tech projects cmbc586n7004ruh07l25slrud, cm3n45n8w02dzsvo6ehqei3lt).
  • Linear BG-1066 — recognize fails on some PDFs: JSON parse error from LLM (truncated/malformed JSON); fixes: detect truncation + maxTokens 16k→65536 (PR #191, #192).
  • blood-gpt-dotnet commit a9b75b27 (Mar 2026) — mark Gemini non-STOP finishReason as ERROR in Langfuse + throw + trigger retry/fallback.
  • Realai-plus/.github-private/docs/models-policy.md — per-prompt MaxTokens / Timeout / fallback-model конфиг (.NET-стек).
  • Код на origin/feat/v2-5: packages/analysis-core/src/agents/validity-classifier/classifier.mastra.ts (callOnce — nonce; classifyBatchWithSplit — бисекция), packages/analysis-core/src/lib/model.ts (cleanSchema, agentModel); packages/analysis-core/eval/validity/DEBUG_METHODOLOGY.md.

Сноски

  1. Тред «Doom loop, frequency penalty, 2.5-flash» на discuss.ai.google.dev, 2025-07-17, https://discuss.ai.google.dev/t/doom-loop-frequency-penalty-2-5-flash/94446 — community-репорт о «doom loop» (структурированный prompt → cyclical repetition similar entries до token exhaust); Google-ответ: «Currently, frequencyPenalty parameter is only supported for the gemini 2.0-flash model. Unfortunately, it is not available in 2.5 model.» — стандартный мulti-provider workaround frequency_penalty на Gemini 2.5 не работает. 2

  2. Jon_Matthews (Google), ответ в треде «Gemini 3 Flash Preview — Infinite Reasoning Loop Causing MAX_TOKEN Exhaustion + Raw Logic Leak» на discuss.ai.google.dev, 2026-01-28, https://discuss.ai.google.dev/t/gemini-3-flash-preview-infinite-reasoning-loop-causing-max-token-exhaustion-raw-logic-leak/114528/15 — официальная рекомендация Google использовать temperature: 1.0 чтобы избежать застревания модели в highest-probability path.

  3. 12 May 2026 sync с Артуром, «Сервис валидности-репрезентативности-релевантности биомаркеров», [Н4] — Метод интроспекции для дебага LLM-ответов. https://github.com/Realai-plus/meeting-digests/blob/main/data/digest/2026/05/2026-05-12T11%3A45%3A00.000Z_Сервис_валидности-репрезентативности-релевантности_биомаркеров_01KRE02872CTM2V6CEERAQ8EVD.md