Урок курса
Structured output: JSON/Pydantic, строгие схемы и валидация
LangChain 1.0.x: c нуля до продакшн LLM-приложений🔹 Зачем нужен структурированный вывод
Иногда важно, чтобы модель возвращала не просто текст, а строго структурированный результат — например, в формате JSON. Такой подход облегчает интеграцию с другими системами и делает взаимодействие с моделью более надёжным.
🔸 Почему это важно?
- Удобная обработка ответов. Если модель возвращает JSON-объект с заранее определёнными полями, ваш код может сразу его разобрать (
json.loads) и использовать данные без парсинга текста на естественном языке. - Интеграция с фронтендом и API. Структурированные ответы проще передавать в базу данных, веб-сервис или другую функцию. Например, чатбот может вернуть ответ с полем
score(уверенность), а интерфейс решит — показывать результат пользователю или нет. - Валидация и предсказуемость. При фиксированной схеме можно проверять соответствие ответа требованиям. Это помогает быстро выявлять ошибки, если модель отклоняется от заданного формата.
- Передача данных между компонентами. Когда результат одной модели используется как вход для другой, структурированный формат упрощает настройку всей цепочки обработки.
🔸 Пример использования
Представьте, что вы создаёте систему анализа отзывов. Модель должна вернуть оценку тональности и выделить ключевые темы. Вместо свободного текста можно уточнить формат:
Верни 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 и удобство генерации форматирующих инструкций для модели. Этот парсер:
-
генерирует инструкцию
format_instructionsна основе модели, -
после получения ответа парсит его:
→ 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.Вызвать
- 2.Собрать цепочку
- 3.Вставить format_instructions в PromptTemplate
- 4.Создать PydanticOutputParser
- 5.Описать Pydantic-модель
Задача 6
🔹 Задание — скрипт API, отдающий валидный JSON
Вам предстоит создадть мини‑API (или консольный скрипт), который будет вызывать модель и гарантировать возврат результата в виде валидного JSON по заданной схеме.
🔸 Что предстоит сделать
- Скрипт принимает входные данные (параметр командной строки,
input(), чтение из файла или фиксированный в коде). Для примера спросим название города. - Формируем запрос к модели через LangChain:
PromptTemplateиPydanticOutputParser - Модель должна вернуть JSON, соответствующий схеме. Например, погода:
class WeatherInfo(BaseModel):
city: str
temperature: float
condition: str
- Парсим и валидируем через Pydantic. Если валидация прошла – получаем объект.
- Выводим результат в виде JSON. Для этого в Pydantic V2 нужно использовать метод
.model_dump_json(). - Добавляем обработку ошибок
try/except. В случае ошибки выводим JSON с полемerror, например:{"error": "Invalid response format"}. Опционально можно повторить вызов или вывести пояснение для отладки.
🔸 Кросс‑рецензирование
Это задание предполагает кросс‑рецензирование: другие студенты будут проверять вашу работу, а вам предстоит анализировать примеры одногруппников.
После подготовки файла добавьте его в репозиторий проекта (на GitHub) и пришлите ссылку на этот фаил.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
