Когда я впервые столкнулся с этим, меня трясло. Не в переносном смысле — буквально: прод упал, алерты орали, а в логах было JSONDecodeError. Причина? Fallback-модель. Основная GPT-4 (тогда ещё актуальная) выдавала идеально отформатированный JSON. Как только она падала по лимитам, включался GPT-3.5-turbo — и начинался цирк: то лишняя запятая, то строки без кавычек, то целые поля пропадали. Схема разваливалась, агентный пайплайн летел в тартарары. Если вы строили многомодельные системы, вы знаете этот ад.
Проблема не в том, что модель "тупая". Проблема в том, что разные модели имеют разную токенизацию, разное внимание к инструкциям и разную склонность галлюцинировать даже в формате. System prompt со схемой — хорошая практика, но не панацея. Особенно когда цена ошибки высока: planner/executor агент (мы про это уже писали) ломается на этапе парсинга плана — и вся сцена обнуляется.
Не советую так делать: добавлять fallback-модель без защитного слоя схемы. Это как ставить запасное колесо, не проверив, подходит ли оно по диаметру.
Момент истины: твой fallback-агент возвращает мусор вместо JSON
Разберёмся, что конкретно идёт не так. Представьте классическую конструкцию:
- Основная модель (дорогая, точная) генерирует JSON, строго соответствующий схеме.
- При её недоступности включается дешёвая/локальная модель.
- Она получает тот же промпт, но выдаёт невалидный JSON — например, не экранирует кавычки, использует одинарные вместо двойных, добавляет комментарии.
В итоге парсер json.loads() падает, агент не получает управляющие данные, и пайплайн либо останавливается, либо генерирует мусорные действия. Знакомо?
Звучит логично, но есть нюанс: даже если модель выдаёт синтаксически правильный JSON, она может нарушить семантику схемы (например, пропустить обязательное поле). Поэтому нужен не просто синтаксический парсер, а валидатор по JSON-схеме.
Я не дам тебе сломать схему: архитектура защитной обертки
Вместо того чтобы надеяться на модель, мы строим прослойку — SchemaGuard. Её задача: перехватывать ответ любой модели, проверять его на соответствие схеме, и если что-то не так — автоматически чинить или повторно запрашивать.
Компоненты SchemaGuard:
- Менеджер запросов — отправляет промпт к основной модели, ловит исключения (таймаут, HTTP-ошибка, пустой ответ).
- Валидатор схемы — принимает JSON (или строку) и проверяет по JSON Schema (Pydantic v2 или
jsonschema). - Repair-движок — если валидация не прошла, пытается "вылечить" JSON. Использует библиотеки
json-repair(актуальная на 2026) или кастомные эвристики (замена одинарных кавычек, удаление комментариев). - Fallback-шлюз — если repair не дал результата, отправляет запрос на запасную модель с усиленным промптом ("Верни ТОЛЬКО валидный JSON, без пояснений").
- Default-значения — если всё сломалось, возвращает заранее заданный дефолтный JSON, чтобы агент не упал.
Код, который выживает после GPT-4 → локальная модель
Начнём с базы. Сначала опишем схему с помощью Pydantic (версия 2.x, актуальная на 2026 год). Допустим, наш агент возвращает план действий.
from pydantic import BaseModel, Field
from typing import List, Optional
class ActionStep(BaseModel):
step_id: int
action: str
parameters: Optional[dict] = None
class Plan(BaseModel):
goal: str = Field(..., description="Цель плана")
steps: List[ActionStep] = Field(..., min_length=1)
Теперь сам SchemaGuard. Я покажу упрощённую версию, но в реальном коде добавьте логирование, метрики (счётчики падений), таймауты.
import json
import logging
from typing import Callable, Any, Optional
from pydantic import ValidationError
logger = logging.getLogger(__name__)
class SchemaGuard:
def __init__(self, schema_model: type[BaseModel],
primary_model: Callable[[str], str],
fallback_model: Callable[[str], str],
max_retries: int = 2,
repair_enabled: bool = True):
self.schema = schema_model
self.primary = primary_model
self.fallback = fallback_model
self.max_retries = max_retries
self.repair = repair_enabled
def generate(self, prompt: str) -> dict:
# попытка основной модели
for attempt in range(self.max_retries):
raw = self._try_call(self.primary, prompt)
if raw is None:
continue
parsed = self._parse_and_validate(raw)
if parsed is not None:
return parsed
# падаем на fallback
logger.warning("Primary failed, fallback")
return self._fallback_generate(prompt)
def _try_call(self, model, prompt) -> Optional[str]:
try:
return model(prompt)
except Exception as e:
logger.error(f"Model call failed: {e}")
return None
def _parse_and_validate(self, raw: str) -> Optional[dict]:
# сначала пытаемся распарсить
try:
data = json.loads(raw)
except json.JSONDecodeError:
if self.repair:
data = self._repair_json(raw)
if data is None:
return None
else:
return None
# валидация pydantic
try:
obj = self.schema.model_validate(data)
return obj.model_dump()
except ValidationError as e:
logger.warning(f"Schema validation failed: {e}")
return None
def _repair_json(self, raw: str) -> Optional[dict]:
# используем библиотеку json-repair (популярна с 2024)
try:
from json_repair import repair_json
fixed = repair_json(raw)
return json.loads(fixed)
except Exception:
# кастомная эвристика: обернуть в кавычки ключи
try:
# простейшая замена одинарных кавычек
fixed = raw.replace("'", '"')
return json.loads(fixed)
except:
return None
def _fallback_generate(self, prompt: str) -> dict:
# усиленный промпт: просим только JSON, никаких пояснений
strict_prompt = prompt + "\n\nВАЖНО: Верни ТОЛЬКО валидный JSON. Никакого текста до или после."
for attempt in range(self.max_retries):
raw = self._try_call(self.fallback, strict_prompt)
if raw is None:
continue
parsed = self._parse_and_validate(raw)
if parsed is not None:
return parsed
# если всё плохо — возвращаем дефолт
logger.error("All fallbacks failed, returning default")
return self.schema.model_construct(goal="default", steps=[ActionStep(step_id=0, action="fallback")]).model_dump()
Обратите внимание на _repair_json. Библиотека json-repair отлично справляется с 90% проблем: неэкранированные строки, лишние запятые, комментарии. Но она не панацея. Для семантических ошибок (пропущенное поле) — только повторный запрос с уточнением.
Как НЕ надо делать: типичные ошибки
Ошибка 1: менять схему в промпте при retry. "Пожалуйста, верни JSON с полями name, age, и обязательно email". Модель запутывается. Лучше один жёсткий промпт + внешняя валидация.
Ошибка 2: бесконечный retry. Если модель стабильно падает, вы просто спалите лимиты API. Поставьте жёсткий лимит (макс 2-3 попытки) и дефолт.
Ошибка 3: не проверять семантику схемы. JSON может быть синтаксически верным, но без обязательных полей. Используйте Pydantic или jsonschema.validate().
Ошибка 4: игнорировать контекст пайплайна. Если у вас агент с памятью (stateful memory), сломанный JSON может испортить всё состояние. В таких случаях лучше вернуть сигнал ошибки, чем мусор. Подробнее про memory — в нашей статье про planner/executor.
Предупреждение: SchemaGuard увеличивает латенти — каждый retry добавляет round-trip к API. Для real-time сцен (клиентский чат) лучше сразу отправлять на fallback с усиленным промптом, минуя ремонт.
Продвинутая тактика: обучение модели на своих ошибках
Раз мы говорим про 2026 год, стоит упомянуть более хитрый подход: если ваш агент часто падает на определённом паттерне ошибок (например, локальная модель всегда забывает поле parameters), вы можете автоматически генерировать few-shot примеры на основе прошлых успешных исправлений. Это как Enforcement layer, только для формата.
Соберите статистику: какие ошибки происходят, от каких моделей. Для каждой fallback-модели подготовьте отдельный prompt с характерными примерами. Это повысит процент попадания в схему с первой попытки на 30-40%.
Тестирование на грязных данных: единственный способ быть уверенным
Не верьте, что если модель один раз выдала правильный JSON — она будет так делать всегда. Напишите стресс-тест:
- Сгенерируйте 1000 запросов. В половине случаев насильно выключайте основную модель.
- Проверьте, что SchemaGuard возвращает валидный JSON (по схеме) в 99.9% случаев.
- Мониторьте, сколько уходит на retry vs repair. Если repair работает хуже — отключайте его для этой модели.
Кстати, о тестировании: у нас есть статья про 5 структурных ошибок AI-агентов в проде, где мы разбираем, почему даже идеальный JSON может убить агента. Там про циклические зависимости и deadlock — обязательно почитайте.
Прогноз: к 2027 году защитные слои для JSON-схем станут стандартным компонентом каждого агента, как сейчас middleware для HTTP. Те, кто не встроят их сейчас, будут переписывать архитектуру экстренно. Не будьте среди них.