Принудительный JSON (JSON Mode) и валидация схем Pydantic

40 минут Урок 12

Введение: От хаоса текста к структуре данных

Добро пожаловать в третий модуль. Если вы работали с LLM достаточно долго, вы наверняка сталкивались с этой болью: вы просите модель вернуть список книг в формате JSON, а она вежливо отвечает: «Конечно! Вот ваш JSON: ...».

Для человека это вежливость. Для вашего парсера — это катастрофа. Вам приходилось писать регулярные выражения (Regex), чтобы вырезать фигурные скобки, удалять лишний текст и надеяться, что внутри валидный синтаксис.

В этом уроке мы разберем, как Gemini 3 API решает эту проблему на архитектурном уровне. Мы переходим от «просьб» к «требованиям». Мы изучим два ключевых инструмента:

  1. JSON Mode — принудительный вывод в формате JSON.
  2. Controlled Generation (Schema Validation) — использование Pydantic для жесткого ограничения структуры ответа.

Это превращает LLM из чат-бота в надежный компонент бэкенда.

Базовый уровень: Включение JSON Mode

Самый простой способ получить JSON от Gemini — это явно указать response_mime_type в конфигурации генерации. Это сигнал модели: «Забудь о свободном тексте, мне нужен только data-формат».

Однако есть нюанс: даже при включенном режиме JSON, в самом промпте обязательно должно быть упоминание того, что вы ожидаете JSON. Если вы этого не сделаете, модель может войти в бесконечный цикл генерации пробелов или выдать ошибку.

Ключевые параметры:

  • response_mime_type="application/json" — переключает режим декодера.
  • response_schema — (опционально) описывает структуру. Без этого параметра модель вернет какой-то валидный JSON, но структура останется на её усмотрение.

python
import google.generativeai as genai
import os

# Настройка API ключа
genai.configure(api_key=os.environ["GOOGLE_API_KEY"])

model = genai.GenerativeModel('gemini-1.5-pro')

# Простой пример использования JSON Mode без строгой схемы
prompt = """
Составь список из 3 популярных языков программирования.
Укажи название, год создания и автора.
Выведи ответ в формате JSON.
"""

response = model.generate_content(
    prompt,
    generation_config=genai.GenerationConfig(
        response_mime_type="application/json"
    )
)

print(response.text)
# Результат гарантированно будет валидным JSON-строкой,
# которую можно сразу парсить через json.loads()

Продвинутый уровень: Pydantic и типизация

Просто получить JSON — это только полдела. Часто нам нужно, чтобы поля назывались определенным образом (например, created_at, а не creationDate), а типы данных соответствовали нашим ожиданиям (число было числом, а не строкой).

Здесь на сцену выходит Pydantic. Это стандарт де-факто в экосистеме Python для валидации данных. Gemini SDK умеет нативно работать с моделями Pydantic, преобразуя их в JSON Schema, которую понимает модель.

Зачем это нужно?

  • Гарантия типов: Вы получите int там, где просили int.
  • Документация: Описания полей (docstrings или Field(description=...)) передаются модели как контекст, помогая ей понять, что именно писать в поле.
  • Отказ от парсинга: SDK может автоматически инстанцировать объекты вашего класса.

python
import typing_extensions as typing
from pydantic import BaseModel, Field

# 1. Определяем структуру данных, которую хотим получить
class Ingredient(BaseModel):
    name: str = Field(description="Название ингредиента")
    quantity: str = Field(description="Количество с единицами измерения, например '200 гр'")
    calories: int = Field(description="Примерная калорийность этого количества")

class Recipe(BaseModel):
    title: str = Field(description="Креативное название блюда")
    ingredients: list[Ingredient]
    difficulty: str = Field(description="Уровень сложности: Легко, Средне, Сложно")
    prep_time_minutes: int

# 2. Передаем класс схемы в конфигурацию
# Обратите внимание: мы передаем сам класс, а не объект
response = model.generate_content(
    "Придумай рецепт полезного завтрака из овсянки и ягод.",
    generation_config=genai.GenerationConfig(
        response_mime_type="application/json",
        response_schema=Recipe
    )
)

# 3. Работаем с результатом
import json

# Текст ответа — это JSON строка
data = json.loads(response.text)
print(f"Блюдо: {data['title']}")

# Можно валидировать через Pydantic для полной уверенности
recipe_obj = Recipe(**data)
print(f"Первый ингредиент: {recipe_obj.ingredients[0].name}")

Использование Enums для классификации

Один из самых мощных паттернов при работе со структурированным выводом — использование перечислений (Enum). Это заставляет модель выбирать значение из строго ограниченного списка. Это идеально подходит для задач классификации, сентимент-анализа или выбора действий агента.

Когда вы используете Enum в схеме Pydantic, Gemini видит допустимые значения и ограничивает свой вывод только ими. Это называется Constrained Decoding (ограниченное декодирование).

python
from enum import Enum

class Sentiment(str, Enum):
    POSITIVE = "positive"
    NEGATIVE = "negative"
    NEUTRAL = "neutral"

class CustomerFeedback(BaseModel):
    raw_text: str
    sentiment: Sentiment  # Модель вынуждена выбрать одно из трех значений
    priority_score: int = Field(description="Оценка срочности от 1 до 10")
    tags: list[str] = Field(description="Список из 3 ключевых тегов")

# Пример вызова
prompt = """
Проанализируй отзыв: 
'Купил этот тостер, а он сжег хлеб за 10 секунд! Ужасное качество сборки, хочу возврат!'
"""

response = model.generate_content(
    prompt,
    generation_config=genai.GenerationConfig(
        response_mime_type="application/json",
        response_schema=CustomerFeedback
    )
)

result = json.loads(response.text)
print(f"Sentiment: {result['sentiment']}") # Выведет: negative
Упражнение

Создайте систему анализа новостей. Вам нужно определить Pydantic-схему 'NewsAnalysis', которая будет извлекать из текста новости: 1) Заголовок (str), 2) Категорию (Enum: Политика, Технологии, Спорт, Экономика), 3) Список упомянутых персон (list[str]), 4) Флаг 'is_breaking_news' (bool). Напишите код запроса к модели для анализа короткого текста новости.

Тонкости и лучшие практики

При работе со структурированным выводом важно помнить несколько вещей, чтобы не столкнуться с неожиданным поведением:

  • Вложенность: Gemini хорошо справляется с глубокой вложенностью, но старайтесь не делать схемы глубже 3-4 уровней. Слишком сложные схемы повышают риск галлюцинаций в структуре.
  • Описания (Docstrings): Поле description в Field() Pydantic критически важно. Это ваша инструкция для модели. Если поле называется score, поясните: «Оценка от 1 до 5, где 5 — отлично». Без этого модель будет гадать.
  • Обработка ошибок: Хотя response_mime_type="application/json" гарантирует синтаксическую корректность JSON, он не гарантирует логическую корректность данных. Всегда оборачивайте создание Pydantic-объекта в блок try-except (ValidationError).

Вопрос

Какой параметр в generation_config является обязательным для того, чтобы Gemini гарантированно вернула ответ в формате JSON, а не просто текст?