Когда LLM-вызов (особенно structured-output — generateObject / responseSchema) ломается, сбой попадает в один из трёх классов, и для каждого правильный слой обработки разный. Различение классов — это и есть критерий «чинить на инфра/прокси-слое (generic, инкапсулировать один раз) или в коде агента (про конкретную задачу)». Транспортный класс универсален; схема и семантика — про structured-output.
Три класса
1. Транспорт
Провайдер недоступен / завис mid-decode / таймаут / 5xx / ECONNRESET / socket hang up. Это сеть/инфра, не про контент. Часто детерминированно по входу (конкретная последовательность токенов вешает Vertex Gemini — см. gemini-doom-loop).
Слой: инфра. Обработка — retry (с пертурбацией промпта, если зависание детерминированное: nonce в HTML-комменте → промах prefix-кэша → другая decoder-trajectory → выскакивает из цикла), per-request timeout, fallback на другого провайдера / другую модель. Модель/temperature при retry постоянны — метрики честны.
Кандидат на вынос на LLM-прокси (llm-proxy-choice — Bifrost): generic, ни одна задача не должна это переизобретать. Сейчас переизобретено по-своему в .NET-LLM-клиенте, в validity-классификаторе (callOnce), в recognize — это анти-паттерн (no-self-rolled-queues).
2. Схема / structured-output
Провайдер криво держит structured-output контракт: возвращает невалидный / усечённый JSON, игнорирует nullability, срывается на JSON-schema-артефактах (additionalProperties, нестандартные nullable-комбинации, $schema/$ref-обвязка, остатки Zod-.refine()). Не про контент — про форму вывода.
Слой: инфра / pre-call. Обработка — нормализация схемы перед вызовом (cleanSchema-паттерн — стрипнуть артефакты до минимального подмножества, которое провайдер держит надёжно); явный maxOutputTokens (детерминированный обрыв вместо зависания); detect-on-finishReason != STOP (HTTP 200, но MAX_TOKENS/SAFETY ⇒ контент обрезан); и при срыве — retry/fallback.
Тоже кандидат на вынос на прокси (generic per-provider quirk). Граничит с транспортом: дегенеративный repetition-loop, докрутивший до MAX_TOKENS, сидит на стыке двух классов — см. gemini-doom-loop.
3. Семантика / контент
Провайдер вернул валидный по форме output, но контент не тот: модель «забыла» часть входа, поля противоречат друг другу, выдумала записи, нарушила инструкцию. Это про конкретную задачу — что значит «правильный» output, может проверить только тот, кто знает задачу.
Слой: app-level. Обработка — валидатор поверх output (cross-field consistency, verbatim-echo / set-equality проверки, anti-hallucination guard) → на reject: один retry с прицельной подсказкой («исправь ровно это и переэмить целиком») → на still-fail: degrade (синтезировать sentinel-значение типа unknown / fallback-вариант, не валить весь прогон).
Остаётся в коде агента — не generic, нельзя вынести на прокси: прокси не знает, что для этой задачи «консистентно». Конкретный пример — validateClassifierOutput в validity-классификаторе.
Принцип
Транспорт + схема — generic → инкапсулировать один раз (LLM-прокси-плагин / общий враппер поверх generateObject), не переизобретать в каждом агенте. Семантика — task-specific → остаётся при задаче. Это и есть ответ на «куда вешать обработку».
Где у нас эти классы
- validity-классификатор — все три: транспорт (Gemini завис → retry-with-nonce → GPT-fallback), схема (
cleanSchemaдля Vertex strict-nullable), семантика (validateClassifierOutput— verbatim state enumeration + index set-equality + cross-field → retry-hint →unknown). См. validity-classifier § «Классы сбоев». - recognize — транспорт + схема: truncated/malformed JSON (BG-1066 — detect truncation + поднять
DEFAULT_MAX_TOKENS16k→65536; retry/fallback на GPT), ~20 s gemini-flash timeout как transport-детектор. (Семантика recognize — отдельная история, валидация fact-extraction.) См. recognition, gemini-doom-loop. - .NET-LLM-клиент (
blood-gpt-dotnet) — транспорт + схема:finishReason != STOP→ пометить трейс ERROR + бросить исключение + retry/fallback (commita9b75b27); per-promptmaxTokens/timeout/fallback-модель конфиг (Realai-plus/.github-private/docs/models-policy.md). Самая зрелая обработка из всех — кандидат-источник логики для переноса. - Python normalization / LOINC-сервисы — транспорт: hand-rolled retry/fallback внутри Redis-queue воркеров — миграционные цели (no-self-rolled-queues).
Открытые вопросы
- Единый враппер / прокси-плагин для классов 1+2 — не сделано; что именно поддерживает Bifrost plugin-модель (per-request
maxTokens; reject-on-finishReason != STOP; per-request timeout; retry-with-perturbation; fallback при detected-loop) — почитать. - Что переносится из .NET-handling (per-prompt
maxTokensconfig; finishReason-checka9b75b27; timeout-as-detector «operation canceled») в новый стек / на прокси. - Границы между классами: дегенеративный loop до
MAX_TOKENS— транспорт или схема?finishReason != STOPловит и «честно слишком длинно» (схема-ish), и «зациклился» (транспорт-ish) — нужны ли разные реакции. - Стоит ли это сделать осознанным паттерном (детектор класса → диспетчер на нужный слой), а не «у каждого агента своё».
Связано
- gemini-doom-loop — конкретный кейс классов 1+2 для Gemini-через-Vertex (зависание / repetition-loop /
MAX_TOKENS); там же — три обхода (cleanSchema / nonce / бисекция) и «где это инкапсулировать» - llm-proxy-choice — Bifrost; куда логично уехать классам 1+2 (как model-fallback уже вынесен)
- no-self-rolled-queues — переизобретённые транспорт-обходы (worker-pool / ручной retry-loop) = анти-паттерн
- validity-classifier § «Классы сбоев» — носитель всех трёх классов; конкретные механики (
callOnce,cleanSchema,validateClassifierOutput, бисекция) - recognition — транспорт + схема (BG-1066, 20 s timeout, retry/fallback)
- structured-output-field-order-cot — смежное: что входит в «правильный» structured output (порядок полей = CoT); семантика-валидатор проверяет, что поздние поля conform к ранним
- llm-call-granularity — гранулярность вызовов (1 / N / батчами) и трёхуровневая декомпозиция; классы сбоев применяются на каждом уровне
- gemini-flash-vs-pro-allocation — где какая модель; fallback-цепочки Gemini→GPT
Источники
Источники: 1.
Сноски
-
Linear BG-1066, accessed 2026-05-17, https://linear.app/realai/issue/BG-1066 — Linear BG-1066 — recognize truncated-JSON; fixes: detect truncation +
DEFAULT_MAX_TOKENS16k→65536 (PR #191, #192). ↩