Урок курса

Structured output: JSON/Pydantic, строгие схемы и валидация

LangChain 1.0.x: c нуля до продакшн LLM-приложений

🔹 Зачем нужен структурированный вывод

Иногда важно, чтобы модель возвращала не просто текст, а строго структурированный результат — например, в формате JSON. Такой подход облегчает интеграцию с другими системами и делает взаимодействие с моделью более надёжным.

 

🔸 Почему это важно?

  1. Удобная обработка ответов. Если модель возвращает JSON-объект с заранее определёнными полями, ваш код может сразу его разобрать (json.loads) и использовать данные без парсинга текста на естественном языке.
  2. Интеграция с фронтендом и API. Структурированные ответы проще передавать в базу данных, веб-сервис или другую функцию. Например, чатбот может вернуть ответ с полем score (уверенность), а интерфейс решит — показывать результат пользователю или нет.
  3. Валидация и предсказуемость. При фиксированной схеме можно проверять соответствие ответа требованиям. Это помогает быстро выявлять ошибки, если модель отклоняется от заданного формата.
  4. Передача данных между компонентами. Когда результат одной модели используется как вход для другой, структурированный формат упрощает настройку всей цепочки обработки.

 

🔸 Пример использования

Представьте, что вы создаёте систему анализа отзывов. Модель должна вернуть оценку тональности и выделить ключевые темы. Вместо свободного текста можно уточнить формат:

Верни JSON с полями: sentiment (positive/negative) и topics (list of strings)

Результат может выглядеть так:

{ "sentiment": "positive", "topics": ["цена", "качество"] }

 

🔸 Возможные проблемы

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

 

🔸 Рекомендации

  • Если требуется фиксированный набор данных, явно задавайте формат в промпте.

  • Модели любят добавлять свои коментарии и форматирования. чтобы этого избежать добавляйте уточнение вроде: Никаких комментариев — только валидный JSON.

  • Обрабатывайте ошибки формата: выбрасывайте исключение, пробуйте исправить результат или запросите ответ повторно.

🔹 Output Parsers

Сейчас мы познакомимся с инструментами LangChain для обработки структурированных ответов — output parser-ами. Вы научитесь использовать встроенные парсеры (например, для JSON) и формулировать инструкцию в промпте, чтобы повысить надёжность получения нужного формата. В предыдущем модуле, при кратком описании языка LCEL, мы уже упоминали один из подклассов parser-ов — класс, преобразующий объекта AIMessage в строку. Однако процесс может идти и в обратную сторону: модель выдаёт строку, а мы преобразуем её в объект и далее обрабатываем.

 

🔸 Что такое Output Parser?

Output Parser — это компонент LangChain, отвечающий за преобразование вывода модели (обычно объекта AIMessage или LLMResult) в нужный формат: строку, словарь, JSON, Pydantic-модель и т.д.

Его задача — стандартизировать и упростить работу с результатами моделей.
В зависимости от конкретного парсера направление преобразования может быть разным:

  • StrOutputParser извлекает текстовое содержимое (content) из AIMessage и возвращает обычную строку.

  • CommaSeparatedListOutputParser, JsonOutputParser, ListOutputParser, PydanticOutputParser, XMLOutputParser   и другие — наоборот, извлекают некую структуру из строки ответа модели и преобразуют её в объект (словарь, JSON, Pydantic-модель и т.д.)

Таким образом, Output Parser служит связующим звеном между моделью и последующими этапами обработки данных, обеспечивая единый интерфейс для работы как с текстом, так и со структурированной информацией.

 

🔸 Преобразование ответа в JSON

Разберём на примере JSON, как можно работать с парсерами. Если нужен простой JSON-объект без строгой схемы, можно конечно просто в промпте вручную сформулировать это как запрос:

Ответь строго в формате JSON. Пример: { "result": ..., "details": [...] }

Но в LangChain есть для этого более гибкие инструменты, как например JsonOutputParser и другие парсеры для стандартных случаев. Вот базовый пример как они работают:

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers.json import JsonOutputParser

load_dotenv()
MODEL = os.getenv("OPENAI_API_MODEL", "gpt-5")

llm = ChatOpenAI(model=MODEL)

parser = JsonOutputParser()
format_instructions = parser.get_format_instructions()

prompt = PromptTemplate(
    template="Пожалуйста, верни данные в формате JSON.\n{format_instructions}\n{question}",
    input_variables=["question"],
    partial_variables={"format_instructions": format_instructions}
)

chain = prompt | llm | parser

result = chain.invoke({"question": "Что такое LangChain?"})
print(result)  # вывод: {'question': 'Что такое LangChain?', 'answer': 'LangChain — это ...'}

 

🔸 Обработка ошибок

Парсинг может дать сбой — например, модель возвращает:

{"name": "Иван", "age": "двадцать"}

json.loads пройдёт, но тип данных неверен. Хотя JsonOutputParser проверяет ключи и их наличие, типы могут не соответствовать строгим требованиям. Риск таких ошибок необходимо иметь ввиду и осуществлять проверки типа данных и проходится другими условными операторами в зависимости от типа данных и специфики вашей задачи.

💡 Для более строгой проверки можно использовать Pydantic через PydanticOutputParser, (подробнее об этом на следующем шаге).

 

🔸 JSON-mode

Раз уж как пример работы парсеров мы взяли именно JSON, то стоит отметить что современные версии OpenAI API (и, соответственно, LangChain) позволяют включить нативный режим возврата данных строго в формате JSON.
Для этого при создании модели достаточно «привязать» параметр response_format={"type": "json_object"}:

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI

load_dotenv()

json_llm = ChatOpenAI(model="gpt-5").bind(response_format={"type": "json_object"})

responce = json_llm.invoke("верни JSON объект с ключом 'random_ints' и значением из 10 случайных чисел в диапазоне [0-99]")

print(responce.content) # вывод: '\n{\n  "random_ints": [23, 87, 45, 12, 78, 34, 56, 90, 11, 67]\n}'

Метод .bind() передаёт модели параметр response_format напрямую в API-вызов. Модель должна вернуть валидный JSON-объект, а не просто текст, имитирующий JSON. В отличие от подхода с парсером, здесь не требуется дополнительный парсер — результат уже гарантированно корректен.

Этот способ полезен, если требуется 100 % структурированный и валидный результат без пост-обработки

⚠️ Работает не всегда, необходима модель семейства GPT-4/5 с поддержкой JSON-режима. Если вы работаете с другими провайдерами или моделями, не поддерживающими response_format, используйте OutputParser он универсален и не зависит от конкретного API

 

🔸 Рекомендации по работе с OutputParsers

  • Используйте парсеры, когда задача требует фиксированной структуры данных и интеграции с цепочками.

  • Всегда ловите исключения парсера (try/except), если работаете вне цепочки (RunnableSequence).

  • Если доступен режим JSON-mode, лучше его использовать — он повышает надёжность.

  • В промпте добавляйте уточнение вроде: «Никаких комментариев — только валидный JSON».

  • Имейте план на случай ошибки формата: логируйте её, попробуйте исправить или перепросите модель с более жёсткой инструкцией.

  • При правильной настройке доля удачных JSON-ответов у современных моделей может быть очень высокой, но всегда готовьте обработку редких ошибок.

  • Если вам нужен вывод в определённом формате, имейте ввиду что langchain имеет обширный арсенал парсеров от XML до Markdawn. Описывать их все в рамках курса не имеет смысла, но вы всегда можете найти детальную и актуальную информацию в документации

🔹 Pydantic-схемы и валидация

На этом этапе мы научимся описывать ожидаемый формат ответа с помощью Pydantic-моделей, использовать их для автоматической проверки и преобразования вывода модели, а также разберём два способа строгой структуризации:
через PydanticOutputParser и через метод with_structured_output() у ChatOpenAI.

 

🔸 Что такое Pydantic?

Pydantic — библиотека Python, которая позволяет определять структуру данных через классы, описывать типы и ограничения полей, и автоматически проверять входящие данные на соответствие этой схеме.
В контексте LangChain Pydantic используется для строгого контроля структуры вывода LLM.

 

🔸 PydanticOutputParser

LangChain предоставляет PydanticOutputParser, который объединяет мощь Pydantic и удобство генерации форматирующих инструкций для модели. Этот парсер:

  1. генерирует инструкцию format_instructions на основе модели,

  2. после получения ответа парсит его:
    → JSON-строка → dict → YourModel(**dict)
    (при ошибке — выбрасывает OutputParserException)

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

load_dotenv()
MODEL = os.getenv("OPENAI_API_MODEL", "gpt-5")

llm = ChatOpenAI(model=MODEL)

# Определяем схему данных
class BookInfo(BaseModel):
    title: str = Field(..., description="Название книги")
    author: str = Field(..., description="Имя автора")
    tags: list[str] = Field(..., description="Список тегов или жанров")

# Создаём парсер
output_parser = PydanticOutputParser(pydantic_object=BookInfo)
format_instructions = output_parser.get_format_instructions()

# Шаблон промпта
prompt = PromptTemplate(
    template=(
        "Ответь на вопрос в требуемом формате.\n"
        "{format_instructions}\n"
        "Вопрос: {user_question}\nОтвет:"
    ),
    input_variables=["user_question"],
    partial_variables={"format_instructions": format_instructions},
)

# цепочка
chain = prompt | llm | output_parser

# Запуск
result = chain.invoke({"user_question": "Расскажи о книге '1984' Джорджа Оруэлла."})
print(type(result)) # <class '__main__.BookInfo'>
print(result) # title='1984', author='Джордж Оруэлл', tags=['антиутопия', 'классика']

 

🔸 Валидация и типизация

Модель BookInfo проверяет типы и структуру данных. Если LLM выдаёт "tags": "антиутопия", Pydantic пытается сконвертировать строку в список.
Если это невозможно — выбрасывается ValidationError, который LangChain перехватывает как OutputParserException.

Можно задавать строгие правила, например мы хотим добавить некий пункт со строгими рамками. Пускай это будет рейтинг книги по 10-ти бальной шкале и мы хотим быть точно уверены что тип данных будет int и  лежать в диапазоне от 0 до 10:

from pydantic import BaseModel, conint

class BookInfo(BaseModel):
    rating: conint(ge=0, le=10) = Field(description="Оценка книги по десятибалльной шкале")

Если модель реётинг rating=11, произойдёт ошибка — это удобно для отлова “галлюцинаций”.

 

🔸 Метод .with_structured_output()

Современные версии LangChain позволяют не использовать PydanticOutputParser явно — вместо этого можно создать “структурированную” LLM, которая сама возвращает Pydantic-объект:

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field

load_dotenv()
MODEL = os.getenv("OPENAI_API_MODEL", "gpt-5")

llm = ChatOpenAI(model=MODEL)

class Profile(BaseModel):
    name: str = Field(description="Имя пользователя")
    age: int = Field(description="Возраст в годах")
    premium: bool = Field(description="Есть премиум-подписка")

structured_llm = llm.with_structured_output(Profile)

result = structured_llm.invoke("Создай профиль русского пользователя средних лет")
print(type(result))   # <class '__main__.Profile'>
print(result.name)    # Алексей Петрович
print(result.age)     # 37
print(result.premium) # False

Ответ модели будет в формате:

{
  "name": "Алексей Петрович",
  "age": 37,
  "premium": false
}

LangChain автоматически преобразует этот JSON в экземпляр Pydantic-модели Profile

То есть метод with_structured_output(BaseModel) позволяет создать новый вариант модели, который автоматически формирует схему и передаёт её в модель через API. Результат десериализуется прямо в объект Profile

Чем это полезно:

  • Упрощает логику: не нужно использовать парсер, модель уже возвращает структурированный ответ.

  • Повышает надёжность: если схема задана, можно ожидать, что поля присутствуют и типы соответствуют.

  • Хорошо интегрируется с цепочками в LangChain: можно сразу передать результат дальше как объект, а не текст.

⚠️ Метод работает только если модель и API поддерживают режим, когда модель может гарантировать ответ в требуемой структуре. Если LLM не поддерживает эти возможности, метод вернёт текст вместо объекта

⚠️ Это не заменяет полностью ручное парсирование для сценариев, где нужна специализированная логика преобразования или проверки

 

🔸 Рекомендации

  • Пишите чёткие описания полей — они попадают в инструкцию модели.

  • Современные модели GPT-4o и GPT-5 поддерживают метод .with_structured_output(), что упрощает код

  • Тестируйте схему заранее — неверный тип в одном поле может испортить весь JSON.

Задача 1

Какой риск всегда нужно учитывать при парсинге ответа модели?
 

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Что делает Pydantic в нашем контексте?
 

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Что верно про гибкость Pydantic при «мелких несоответствиях»?
 

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

Что произойдёт при серьёзном расхождении со схемой (нет поля/тип неконвертируемый)?
 

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 5

Упорядочьте шаги получения структурированного результата через LangChain (перетащите):
 

Расположите элементы в правильном порядке

  1. 1.Вызвать
  2. 2.Собрать цепочку
  3. 3.Вставить format_instructions в PromptTemplate
  4. 4.Создать PydanticOutputParser
  5. 5.Описать Pydantic-модель
Без регистрации · результат не сохраняется

Задача 6

🔹 Задание — скрипт API, отдающий валидный JSON

Вам предстоит создадть мини‑API (или консольный скрипт), который будет вызывать модель и гарантировать возврат результата в виде валидного JSON по заданной схеме.

 

🔸 Что предстоит сделать

  1. Скрипт принимает входные данные (параметр командной строки, input(), чтение из файла или фиксированный в коде). Для примера спросим название города.
  2. Формируем запрос к модели через LangChain: PromptTemplate и PydanticOutputParser
  3. Модель должна вернуть JSON, соответствующий схеме. Например, погода:
class WeatherInfo(BaseModel):
    city: str
    temperature: float
    condition: str
  1. Парсим и валидируем через Pydantic. Если валидация прошла – получаем объект.
  2. Выводим результат в виде JSON. Для этого в Pydantic V2 нужно использовать метод .model_dump_json().
  3. Добавляем обработку ошибок try/except. В случае ошибки выводим JSON с полем error, например: {"error": "Invalid response format"}. ​​​​​Опционально можно повторить вызов или вывести пояснение для отладки.

 

🔸 Кросс‑рецензирование

Это задание предполагает кросс‑рецензирование: другие студенты будут проверять вашу работу, а вам предстоит анализировать примеры одногруппников.

После подготовки файла добавьте его в репозиторий проекта (на GitHub) и пришлите ссылку на этот фаил.

Открыть задание в уроке →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку