JSON-схемы и fallback-модели AI-агентов: защита от сбоев с Python | AiManual
AiManual Logo Ai / Manual.
21 Июн 2026 Гайд

Как избежать сбоев в JSON-схемах при использовании fallback-моделей в AI-агентах: рецепт с Python-реализацией

Падает JSON при смене модели? Решение: слой валидации, repair и retry. Python-код SchemaGuard для устойчивых агентных пайплайнов.

Реклама
partv2

Когда я впервые столкнулся с этим, меня трясло. Не в переносном смысле — буквально: прод упал, алерты орали, а в логах было JSONDecodeError. Причина? Fallback-модель. Основная GPT-4 (тогда ещё актуальная) выдавала идеально отформатированный JSON. Как только она падала по лимитам, включался GPT-3.5-turbo — и начинался цирк: то лишняя запятая, то строки без кавычек, то целые поля пропадали. Схема разваливалась, агентный пайплайн летел в тартарары. Если вы строили многомодельные системы, вы знаете этот ад.

Проблема не в том, что модель "тупая". Проблема в том, что разные модели имеют разную токенизацию, разное внимание к инструкциям и разную склонность галлюцинировать даже в формате. System prompt со схемой — хорошая практика, но не панацея. Особенно когда цена ошибки высока: planner/executor агент (мы про это уже писали) ломается на этапе парсинга плана — и вся сцена обнуляется.

Не советую так делать: добавлять fallback-модель без защитного слоя схемы. Это как ставить запасное колесо, не проверив, подходит ли оно по диаметру.

Момент истины: твой fallback-агент возвращает мусор вместо JSON

Разберёмся, что конкретно идёт не так. Представьте классическую конструкцию:

  • Основная модель (дорогая, точная) генерирует JSON, строго соответствующий схеме.
  • При её недоступности включается дешёвая/локальная модель.
  • Она получает тот же промпт, но выдаёт невалидный JSON — например, не экранирует кавычки, использует одинарные вместо двойных, добавляет комментарии.

В итоге парсер json.loads() падает, агент не получает управляющие данные, и пайплайн либо останавливается, либо генерирует мусорные действия. Знакомо?

Звучит логично, но есть нюанс: даже если модель выдаёт синтаксически правильный JSON, она может нарушить семантику схемы (например, пропустить обязательное поле). Поэтому нужен не просто синтаксический парсер, а валидатор по JSON-схеме.

Я не дам тебе сломать схему: архитектура защитной обертки

Вместо того чтобы надеяться на модель, мы строим прослойку — SchemaGuard. Её задача: перехватывать ответ любой модели, проверять его на соответствие схеме, и если что-то не так — автоматически чинить или повторно запрашивать.

💡
Идея не нова — это вариация паттерна "Circuit Breaker + Retry с умным условием". Но в AI-агентах он критичен, как ORM для баз данных. Без него вы будете тонуть в багах, которые нельзя отловить на этапе разработки.

Компоненты SchemaGuard:

  1. Менеджер запросов — отправляет промпт к основной модели, ловит исключения (таймаут, HTTP-ошибка, пустой ответ).
  2. Валидатор схемы — принимает JSON (или строку) и проверяет по JSON Schema (Pydantic v2 или jsonschema).
  3. Repair-движок — если валидация не прошла, пытается "вылечить" JSON. Использует библиотеки json-repair (актуальная на 2026) или кастомные эвристики (замена одинарных кавычек, удаление комментариев).
  4. Fallback-шлюз — если repair не дал результата, отправляет запрос на запасную модель с усиленным промптом ("Верни ТОЛЬКО валидный JSON, без пояснений").
  5. 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. Те, кто не встроят их сейчас, будут переписывать архитектуру экстренно. Не будьте среди них.

Подписаться на канал