Структурированный вывод: JSON Schema и строгая типизация ответов
Введение: Конец эры регулярных выражений
Добро пожаловать на один из самых важных уроков этого модуля. Если вы когда-либо пытались интегрировать LLM (Large Language Models) в реальное программное обеспечение до появления нативной поддержки JSON, вы знаете эту боль. Вы пишете в промпте: «Пожалуйста, верни только JSON, без лишних слов». А модель отвечает: «Конечно! Вот ваш JSON: {...}».
И всё. Ваш парсер падает. Ваше приложение выдает ошибку. Вы пишете регулярные выражения (RegEx), чтобы вырезать фигурные скобки, но модель вдруг решает использовать одинарные кавычки вместо двойных или добавляет комментарий внутри JSON.
В Gemini 3 API мы переходим от «просьб» к контрактам. Мы больше не надеемся, что модель вернет правильную структуру. Мы гарантируем это на уровне API. Сегодня мы разберем, как использовать Structured Output, задействуем мощь JSON Schema и научимся связывать это с библиотекой Pydantic для создания надежных, типизированных приложений.
Зачем нам нужна строгая типизация ответов?
Текст — это интерфейс для людей. Данные — это интерфейс для машин. Когда вы строите агента, который должен сохранить запись в базу данных, вызвать внешний API или отрендерить интерфейс, вам нужна предсказуемость.
- Детерминизм структуры: Вы всегда знаете, какие поля придут и какого они будут типа.
- Экономия токенов: Модели не нужно генерировать вводные слова («Вот список, который вы просили...»), она сразу генерирует данные.
- Уменьшение галлюцинаций: Когда модель ограничена схемой, она с меньшей вероятностью придумает несуществующие поля или данные, не подходящие под формат.
В экосистеме Gemini это реализуется через параметр response_schema и установку MIME-типа ответа в application/json.
import os
import google.generativeai as genai
from google.ai.generativelanguage_v1beta.types import content
# Настройка API ключа
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
# Определение простой схемы через словарь (raw dict)
# Это базовый уровень, позже мы перейдем к Pydantic
response_schema = {
"type": "object",
"properties": {
"recipe_name": {"type": "string"},
"ingredients": {
"type": "array",
"items": {"type": "string"}
},
"calories": {"type": "integer"}
},
"required": ["recipe_name", "ingredients", "calories"]
}
# Инициализация модели с конфигурацией генерации
model = genai.GenerativeModel(
'gemini-1.5-pro', # Используем актуальную версию Pro для лучшего следования инструкциям
generation_config=genai.GenerationConfig(
response_mime_type="application/json",
response_schema=response_schema
)
)
response = model.generate_content(
"Придумай рецепт полезного завтрака с авокадо."
)
print(response.text)
Интеграция с Pydantic: Золотой стандарт Python-разработки
Использование словарей для описания схем (как в примере выше) работает, но это громоздко и чревато ошибками. В Python-сообществе стандартом де-факто для валидации данных является библиотека Pydantic.
Gemini API умеет принимать классы Pydantic напрямую. Это дает вам огромные преимущества:
- Чистота кода: Вы описываете структуру данных как Python-класс.
- Автодокументация: Поля
descriptionв Pydantic передаются модели как часть контекста. Это означает, что описание поля служит микро-промптом для модели. - Валидация на выходе: Вы сразу получаете объект Python, готовый к использованию в бизнес-логике, а не просто JSON-строку.
Давайте посмотрим, как это выглядит на практике при создании анализатора резюме.
import typing_extensions as typing
from pydantic import BaseModel, Field
# 1. Описываем структуру данных, которую хотим получить
class WorkExperience(BaseModel):
company: str = Field(description="Название компании")
role: str = Field(description="Должность")
years: int = Field(description="Количество полных лет работы")
skills_used: list[str] = Field(description="Список технологий или навыков, использованных на этом месте")
class CandidateProfile(BaseModel):
full_name: str = Field(description="Полное имя кандидата")
summary: str = Field(description="Краткое профессиональное резюме в 2 предложения")
experience: list[WorkExperience] = Field(description="История работы, отсортированная от последней к первой")
seniority_level: typing.Literal["Junior", "Middle", "Senior", "Lead"] = Field(
description="Оценка уровня сеньорности на основе опыта"
)
# 2. Передаем класс Pydantic прямо в generation_config
model = genai.GenerativeModel(
'gemini-1.5-pro',
generation_config=genai.GenerationConfig(
response_mime_type="application/json",
response_schema=CandidateProfile
)
)
# 3. Имитируем сырой текст резюме
raw_resume_text = """
Меня зовут Алекс Иванов. Я занимаюсь бэкендом уже 5 лет.
Последние 2 года работал в TechCorp, где лидил команду питонистов,
строили микросервисы на FastAPI. До этого 3 года пилил монолит на Django в OldSchool LLC.
"""
response = model.generate_content(f"Извлеки информацию из этого текста: {raw_resume_text}")
# Преобразуем JSON ответ обратно в объект Pydantic для проверки
# В реальном приложении можно использовать CandidateProfile.model_validate_json(response.text)
print(response.text)
Тонкости работы с Enums и вложенностью
Обратите внимание на поле seniority_level в примере выше. Мы использовали typing.Literal. Это критически важный момент.
Когда вы задаете Literal['Junior', 'Middle'...], вы жестко ограничиваете словарь модели. Она физически не сможет вернуть значение "Expert" или "Intern", если его нет в списке допустимых значений схемы. Это работает как классификатор «из коробки».
Советы по проектированию схем:
- Вложенность: Gemini отлично справляется с глубокой вложенностью, но старайтесь не делать её глубже 3-4 уровней, чтобы не размывать внимание модели.
- Описания (Descriptions): Никогда не оставляйте поля пустыми. Поле
age: intможет быть понято двояко (возраст человека или стаж работы?).Field(description="Возраст кандидата в годах")убирает двусмысленность. - Опциональные поля: Если поле не обязательно, используйте
Optional[type]. Модель может вернутьnull, если информации в тексте недостаточно.
Создайте структуру данных для 'Умного помощника покупок'.<br><br>Вам нужно:<br>1. Описать Pydantic-модели для `Product` (название, цена, категория, является ли экологичным).<br>2. Категория должна быть ограничена списком: 'Food', 'Electronics', 'Clothing'.<br>3. Главная модель `ShoppingList` должна содержать список продуктов и общую оценочную стоимость.<br>4. Напишите код, который принимает список покупок в свободной форме (строка) и возвращает структурированный объект JSON.
Обработка ошибок и крайних случаев
Даже при строгой схеме модель — это вероятностная машина. В 99.9% случаев с response_schema вы получите валидный JSON. Но что делать в оставшихся 0.1%?
В продакшене вы должны оборачивать парсинг ответа в блок try-except. Если model_validate_json (метод Pydantic) падает с ошибкой валидации, это означает, что модель нарушила контракт. В архитектуре агентов это сигнал для самокоррекции (Self-Correction).
Паттерн самокоррекции выглядит так:
1. Получили JSON.
2. Pydantic выдал ошибку (например, поле `price` — строка, а не число).
3. Вы отправляете эту ошибку обратно модели с текстом: «Ты вернула неверный формат. Ошибка: {error_message}. Исправь это.»
4. Gemini 3 отлично справляется с исправлением собственных синтаксических ошибок.
Почему использование typing.Literal в определении схемы Pydantic особенно полезно при работе с LLM?