Тема в исследовании. Повторяющийся прод-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_TOKENS16k → 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. Workaroundfrequency_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-dotnetcommita9b75b27, 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-стеке не выставляется осознанно (validitygenerateObject— без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 0vs официальная рекомендация Google1.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.techprojectcmbc586n7004ruh07l25slrud(а такжеcm3n45n8w02dzsvo6ehqei3lt— поиск поMAX_TOKENSнаrecognize_gemini); validity — где трейсится TBD. Калибрует приоритет. - Что переносится из .NET-handling (per-prompt
maxTokensconfig;finishReason-checka9b75b27; 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_TOKENS16k→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, Total—realaicorp.slack.com/archives/C094GRT3CBY/p1759834543679359; Langfuse-ссылки наMAX_TOKENS-трейсы (lf.tryreal.techprojectscmbc586n7004ruh07l25slrud,cm3n45n8w02dzsvo6ehqei3lt). - Linear BG-1066 — recognize fails on some PDFs: JSON parse error from LLM (truncated/malformed JSON); fixes: detect truncation +
maxTokens16k→65536 (PR #191, #192). blood-gpt-dotnetcommita9b75b27(Mar 2026) — mark Gemini non-STOPfinishReasonasERRORin 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.
Сноски
-
Тред «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,
frequencyPenaltyparameter is only supported for the gemini 2.0-flash model. Unfortunately, it is not available in 2.5 model.» — стандартный мulti-provider workaroundfrequency_penaltyна Gemini 2.5 не работает. ↩ ↩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. ↩ -
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 ↩