Когда 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_TOKENS 16k→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 (commit a9b75b27); per-prompt maxTokens/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 maxTokens config; finishReason-check a9b75b27; 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.

Сноски

  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_TOKENS 16k→65536 (PR #191, #192).