От простого текста к проверенным объектам: выбор парсера вывода LangChain
Сравните StrOutputParser, JsonOutputParser, StructuredOutputParser и PydanticOutputParser в цепочках LangChain и узнайте, насколько строгую структуру действительно гарантирует каждый из них.
Модель чата возвращает текст, который хорошо читается на экране. Однако когда ответ должен стать входными данными для следующего запроса, сохраниться в базе данных или использоваться в компоненте интерфейса, требуется что-то более предсказуемое: чистая строка, объект JSON или типизированная запись, уже пройдшая проверку. LangChain заполняет этот пробел с помощью парсеров вывода, предоставляя несколько вариантов с совершенно разными гарантиями. В этом руководстве создается тот же небольшой проект «Солнечная система» с использованием четырех парсеров, объясняется каждая строка каждой части кода, и в конце приводится практическое правило для выбора между ними.
Если вы хотите узнать более подробно о том, как парсеры взаимодействуют с LCEL, исполняемыми модулями и памятью, ознакомьтесь с статьей «От сырого текста к пайплайнам в LangChain». Здесь акцент сделан на более узкой теме: что именно обещает каждый парсер и где заканчивается это обещание.
Почему сырой текст модели недостаточен
Если спросить модель о Солнечной системе, вы можете получить что-то вроде приведённого ниже текста. Он хорошо читается, но программа не сможет извлечь из него конкретный факт, не догадываясь о месте окончания одного предложения и начала следующего.
The Solar System consists of the Sun and the objects that orbit it.
It contains eight planets along with moons, asteroids, and comets.
Код, обрабатывающий такой ответ, предпочёл бы получать именованные значения, к которым можно будет обратиться напрямую, например объект с одним ключом на каждый факт:
{
"fact_1": "The Solar System contains eight planets.",
"fact_2": "The Sun is at the center of the Solar System.",
"fact_3": "The Solar System also contains moons, asteroids, and comets."
}
Парсер вывода — это компонент, соединяющий эти две составляющие. Он принимает данные, сгенерированные моделью, и возвращает значение, которое может быть использовано приложением без дополнительной обработки строк. Парсеры отличаются по степени своей функциональности: некоторые лишь распаковывают текст, другие парсят JSON, а самые строгие проверяют результат согласно спецификации.
Raw LLM Response
↓
Output Parser
↓
Parsed Output
Помните о этой трехэтапной схеме. Каждый приведённый ниже пример является её вариацией, при этом по мере ужесточения требований в средний блок добавляется дополнительное оборудование.
Четыре парсера, четыре уровня структуры
Четыре парсера, рассмотренные здесь, образуют «лестницу», при этом каждая ступенька добавляет контроль над результатом:
StrOutputParserпреобразует сообщение модели в обычную строку на Python.JsonOutputParserпарсит ответ в значение, совместимое с JSON, такое как словарь или список.
StructuredOutputParser позволяет указать имена полей, которые должен возвращать модель.PydanticOutputParser описывает ожидаемую структуру с помощью модели Pydantic и проверяет вывод на соответствие ей.Независимо от выбранного инструмента, путь обработки данных остается прежним:
LLM Response
↓
Output Parser
↓
Parsed Output
Изменяется только результат в конце и степень его надежности.
Пример работы: отчет, затем краткое резюме
Первый проект представляет собой двухэтапную обработку. Тема «Солнечная система» поступает в модель, которая генерирует длинный отчет. Этот отчет затем передается во второй инструмент, запрашивающий пятистрочное резюме. Ключевым моментом является передача данных: результат первого вызова модели становится входными данными для следующего инструмента.
Solar System
↓
LLM
↓
Detailed Report
↓
LLM
↓
5-Line Summary
Без парсера первый шаг возвращает объект сообщения, а не текст отчета, поэтому перед созданием второго запроса придется его разобрать. Парсер, находящийся между этими шагами, выполняет это разборение в рамках цепочки обработки, что позволяет двум шагам работать вместе без проблем.
StrOutputParser: когда нужен только текст
StrOutputParser — это самый простой парсер, предлагаемый LangChain. Модели чата возвращают объект AIMessage; этот парсер извлекает его содержимое и предоставляет обычную строку. Он подходит тогда, когда следующим потребителем является человек, еще один запрос или что-либо еще, что требует простого текста, и когда нет необходимости в JSON или схеме.
Интеграция в процесс формирования отчета
В проекте report-then-summary шаги выглядят следующим образом:
- Первый запрос просит модель сгенерировать подробный отчет по заданной теме.
После каждого вызова модели работает StrOutputParser, поэтому при каждой передаче данных используется обычная строка. Полный порядок компонентов следующий:
Solar System
↓
Prompt 1
↓
LLM
↓
StrOutputParser
↓
Detailed Report
↓
Prompt 2
↓
LLM
↓
StrOutputParser
↓
5-Line Summary
Версия OpenAI
Здесь представлена полная цепочка обработки с использованием ChatOpenAI. Прочитайте её сверху вниз один раз, затем мы рассмотрим важные части.
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
load_dotenv()
model = ChatOpenAI()
# 1st prompt -> detailed report
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
# 2nd prompt -> summary
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
parser = StrOutputParser()
chain = template1 | model | parser | template2 | model | parser
result = chain.invoke({'topic': 'Solar System'})
print(result)
Модель создается с параметрами по умолчанию. Функция load_dotenv(), расположенная выше, загружает ключ API из локального файла .env, поэтому в скрипте не хранятся учетные данные.
model = ChatOpenAI()
Первый шаблон принимает одну переменную — topic — и требует подробного отчета по этой теме.
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
Второй шаблон ожидает переменную с именем text, которая будет содержать отчет, а также требует пятистрочного краткого описания этого отчета.
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
Если вы копируете этот код, стоит исправить небольшую ошибку: в строке шаблона присутствует /n, что является буквальным слешем с буквой n, а не переводом строки. Используйте \n, если хотите, чтобы отчет начинался на новой строке. Модели обычно справляются в любом случае, но запрос, который вы думаете отправлять, должен совпадать с тем, который вы действительно отправляете.
Далее идет парсер. Один экземпляр его можно повторно использовать в нескольких местах цепочки, поскольку он не сохраняет состояние между вызовами.
parser = StrOutputParser()
Строка, которая связывает все воедино, — это определение цепочки:
chain = template1 | model | parser | template2 | model | parser
Оператор трубки представляет собой комбинацию в языке выражений LangChain (LCEL): вывод каждого компонента становится входными данными для следующего. При вертикальном расположении порядок выполнения следующий:
template1
↓
model
↓
parser
↓
template2
↓
model
↓
parser
Обратите внимание, что даёт вам первый парсер. После первого вызова модели парсер возвращает отчёт в виде строки, и именно эта строка заполняет {text} во втором шаблоне. Второй парсер выполняет аналогичную задачу для итогового ответа, поэтому результат цепочки — это само резюме, а не объект сообщения.
Цепочку запускают, передав словарь, ключи которого совпадают с переменными входных данных первого шаблона:
result = chain.invoke({'topic': 'Solar System'})
Затем выводится результат:
print(result)
Что возвращает цепочка
Значение, отображаемое в конце, — это пятистрочное резюме, полученное на основе сгенерированного отчета. Поскольку последним компонентом является StrOutputParser, результат представляет собой обычную строку в Python str:
print(result)
Типичная работа программы дает результат примерно в таком виде:
1. The Solar System consists of the Sun and all objects that orbit it.
2. It includes eight planets, along with dwarf planets, moons, asteroids, and comets.
3. The four inner planets are rocky, while the outer planets are mostly gas or ice giants.
4. The Sun contains most of the Solar System's mass and provides the energy that drives many processes.
5. The Solar System is located in the Milky Way galaxy.
Рассматривайте это как пример структуры результата, а не как фиксированный ответ. Формулировки будут отличаться в зависимости от запусков и моделей.
Тот же рабочий процесс с моделью Hugging Face
Процесс сначала генерации отчета, затем резюме может также использоваться с открытой моделью. В этом варианте применяется HuggingFaceEndpoint, настроенный на модель google/gemma-2-2b-it, и она оборачивается в класс ChatHuggingFace, чтобы вести себя как чат-модель:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
load_dotenv()
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
# 1st prompt -> detailed report
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
# 2nd prompt -> summary
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
prompt1 = template1.invoke({'topic': 'Solar System'})
result = model.invoke(prompt1)
prompt2 = template2.invoke({'text': result.content})
result1 = model.invoke(prompt2)
print(result1.content)
Здесь существует важное отличие. В этой версии никогда не используется StrOutputParser, и не создается цепочка труб. Каждый запрос форматируется вручную с помощью .invoke(), вызывается модель, а затем из возвращенного сообщения считывается .content перед его дальнейшей передачей. Этот способ работает, но именно он представляет собой ручную обработку, которую и предназначен устранить парсер.
Если сравнивать оба подхода, они выглядят так. С OpenAI и парсером:
OpenAI
Prompt
↓
ChatOpenAI
↓
StrOutputParser
↓
String
С Hugging Face и ручным доступом:
Hugging Face
Prompt
↓
ChatHuggingFace
↓
result.content
↓
String
Оба варианта заканчиваются строкой. В скрипте OpenAI показано, как парсер выполняет эту задачу в рамках цепочки операций, тогда как в скрипте Hugging Face та же логика применяется с другим поставщиком данных без использования парсера. Ничто не мешает использовать конструкцию template1 | model | parser | template2 | model | parser и с моделью Hugging Face; парсеру безразлично, какой поставщик сгенерировал сообщение.
Вывод для данного этапа: используйте StrOutputParser, когда приложению нужен ответ только в виде текста.
JsonOutputParser: JSON без спецификации
JsonOutputParser представляет собой следующий уровень сложности. Он запрашивает у модели данные в формате JSON и преобразует полученный ответ в структуру данных Python, что удобно, когда коду необходимо извлекать значения по ключам вместо чтения простого текста.
Он не обеспечивает соблюдение определенной структуры. Без схемы он просит модель отвечать в формате JSON, но не указывает, какие ключи должны присутствовать и какого типа должны быть их значения. Два запуска с одинаковым запросом могут закономерно возвращать объекты разной структуры, и ваш код должен быть к этому готов.
Как все соединяется
В этом проекте от модели запрашиваются пять фактов о Солнечной системе. Шаги следующие:
- Создать объект
JsonOutputParser. - Запросить у него инструкции по формату с помощью метода
get_format_instructions(). - Включить эти инструкции в запрос.
- Отправить запрос модели.
- Позволить парсеру преобразовать ответ в значение на языке Python.
Solar System
↓
PromptTemplate
↓
Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON Object
Версия от OpenAI
Полная цепочка операций довольно коротка:
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
load_dotenv()
# Define Model
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
parser = JsonOutputParser()
template = PromptTemplate(
template="Give me 5 facts about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
chain = template | model | parser
result = chain.invoke({"topic": "Solar System"})
print(result)
Модель настроена с использованием gpt-4.1-mini и температурой 0, что обеспечивает максимально воспроизводимый результат в рамках возможностей модели. Именно этот компонент будет записывать пять фактов.
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
Парсер создается без аргументов, и именно поэтому у него нет схемы для её применения:
parser = JsonOutputParser()
Инструкции по формату выполняют основную работу
Перед запуском модели парсер может описать формат, который он ожидает. Это описание получается в результате одного вызова метода:
parser.get_format_instructions()
Он возвращает блок текста, указывающий модели ответить в формате JSON. Это не код, выполняемый на модели; это текст-запрос. Он вставляется в шаблон через partial_variables, что позволяет заполнить переменную шаблона один раз при его определении, а не при каждом вызове:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Результатом работы шаблона являются два места замены:
template = PromptTemplate(
template="Give me 5 facts about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic}задаётся во время вызова и указывает, какие факты необходимо получить.{format_instruction}заранее заполняется рекомендациями по форматированию от парсера.
Таким образом, при вызове цепочки с этими данными модель получает как тему, так и инструкцию к ответу в формате JSON:
{"topic": "Solar System"}
Сборка и запуск цепочки
Сама цепочка состоит всего из трёх этапов:
chain = template | model | parser
В порядке выполнения:
PromptTemplate
↓
ChatOpenAI
↓
JsonOutputParser
↓
Parsed JSON
Шаблон генерирует окончательный запрос, ChatOpenAI отвечает на него, а JsonOutputParser преобразует ответ в данные на языке Python. Парсер также толерантен к некоторым распространённым особенностям моделей, таким как помещение JSON в рамки кода Markdown, которые он удаляет перед обработкой.
Вызовите его с указанием темы:
result = chain.invoke({"topic": "Solar System"})
И выведите то, что вернулось:
print(result)
Что возвращается
Результат содержит пять фактов в формате JSON. Скрипт выводит только значение, поэтому нет канонического формата вывода для цитирования; вероятный ответ выглядит так:
{
"facts": [
"The Solar System is centered around the Sun.",
"There are eight recognized planets in the Solar System.",
"The four inner planets are rocky planets.",
"The outer planets include gas giants and ice giants.",
"The Solar System is located in the Milky Way galaxy."
]
}
Такая структура, когда один ключ facts содержит список, — это один из нескольких вариантов, которые может выбрать модель. В другой раз могут вернуться fact_1, fact_2, fact_3, fact_4 и fact_5, или просто список. Если последующий код будет обращаться к result["facts"], это приведет к ошибкам в тот день, когда модель выберет другую структуру.
Отслеживание потока
По сравнению с StrOutputParser новым элементом является то, что парсер участвует дважды: сначала перед вызовом модели, предоставляя инструкции, а затем после этого — выполняя парсинг.
Prompt
↓
JSON Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON
Инструкции генерируются самим парсером:
parser.get_format_instructions()
Они доходят до интерфейса через переменную partial:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Модель отвечает с учетом этих инструкций, а парсер преобразует ответ в значение на языке Python. От начала до конца:
Solar System
↓
PromptTemplate
↓
JSON Format Instructions
↓
ChatOpenAI
↓
JsonOutputParser
↓
JSON Object
Разница по сравнению с предыдущим этапом помещается в две строки. StrOutputParser генерирует:
StrOutputParser
↓
Plain String
в то время как JsonOutputParser генерирует:
JsonOutputParser
↓
JSON-compatible Structured Data
Не забывайте о том, что по-прежнему отсутствует фиксированная схема. Вы получаете JSON, но структура полей и их вложенность определяются моделью. К слову, текущие версии JsonOutputParser также принимают необязательный аргумент pydantic_object, который добавляет схему к инструкциям формата, но в приведенном здесь виде без аргументов он требует лишь корректного JSON.
Вариант Hugging Face
Рабочий процесс в формате JSON напрямую используется с моделью Gemma. Настраивается сама модель; парсер, инструкции по формату и цепочка обработки остаются прежними:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
load_dotenv()
# Define the model
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
parser = JsonOutputParser()
template = PromptTemplate(
template='Give me 5 facts about {topic} \n {format_instruction}',
input_variables=['topic'],
partial_variables={
'format_instruction': parser.get_format_instructions()
}
)
chain = template | model | parser
result = chain.invoke({'topic': 'Solar System'})
print(result)
Пайплайн идентичен, за исключением блока с моделью:
PromptTemplate
↓
Hugging Face Model
↓
JsonOutputParser
↓
JSON Object
Вот практическая выгода размещения логики парсинга в отдельном компоненте: смена поставщиков не влияет на логику парсинга. Однако следует учитывать, что небольшие открытые модели менее надежно соблюдают инструкции по формату, чем крупные хостинговые модели. Если ответ содержит прозу вокруг JSON или завершающую запятую, парсер выдает исключение OutputParserException, поэтому код в производстве должен его перехватить и попытаться обработать данные заново или использовать альтернативный вариант.
StructuredOutputParser: указание названий ожидаемых полей
StructuredOutputParser извлекает JSON на основе списка полей, которые вы определяете заранее. В то время как JsonOutputParser просто указывает «ответ в формате JSON», этот парсер указывает «ответ в формате JSON с этими ключами».
Поля объявляются с помощью ResponseSchema. Каждое из них имеет name и description, причем описание указывает модели, что должно находиться в данном поле. Это позволяет гораздо лучше контролировать структуру ответа.
Проект «три факта»
В этом проекте требуется предоставить три факта о Солнечной системе, причем для каждого факта предусмотрено по одному полю:
fact_1содержит первый факт по теме.fact_2содержит второй факт.fact_3содержит третий факт.
Парсер преобразует эти объявления в инструкции, а затем сравнивает полученный ответ с ними:
Solar System
↓
PromptTemplate
↓
Predefined Field Schema
↓
LLM
↓
StructuredOutputParser
↓
Structured JSON
Версия OpenAI
Вот полный скрипт:
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
load_dotenv()
# Define Model
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
schema = [
ResponseSchema(name="fact_1", description="Fact 1 about the topic"),
ResponseSchema(name="fact_2", description="Fact 2 about the topic"),
ResponseSchema(name="fact_3", description="Fact 3 about the topic"),
]
parser = StructuredOutputParser.from_response_schemas(schema)
template = PromptTemplate(
template="Give 3 fact about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
chain = template | model | parser
result = chain.invoke({"topic": "Solar System"})
print(result)
Модель использует ту же конфигурацию gpt-4.1-mini, что и раньше:
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
Настоящая разница заключается в списке схем:
schema = [
ResponseSchema(name="fact_1", description="Fact 1 about the topic"),
ResponseSchema(name="fact_2", description="Fact 2 about the topic"),
ResponseSchema(name="fact_3", description="Fact 3 about the topic"),
]
Каждый объект ResponseSchema предоставляет два элемента:
nameстановится ключом в получаемом словаре.descriptionуказывает модели, что должно содержаться в этом ключе.
Три схемы обеспечивают наличие трех обязательных ключей: fact_1, fact_2 и fact_3.
Этот парсер не создается напрямую. Его формирует метод класса на основе списка схем:
parser = StructuredOutputParser.from_response_schemas(schema)
Как и в случае с парсером JSON, инструкции по форматированию поступают от самого парсера:
parser.get_format_instructions()
На этот раз инструкции более подробные. Они включают структуру JSON с перечнем всех имен полей вместе с их описаниями, а также требуют от модели поместить ответ в заключённый блок JSON. Они подключаются таким же образом:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Шаблон запроса содержит два знакомых места замены:
template = PromptTemplate(
template="Give 3 fact about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic} заполняется во время вызова, а {format_instruction} содержит список полей, сгенерированный на основе ваших схем. При вызове с этими данными они оба передаются модели:
{"topic": "Solar System"}
Запуск цепочки
Цепочка имеет три этапа, как и раньше:
chain = template | model | parser
Причём парсер находится в последнем положении:
PromptTemplate
↓
ChatOpenAI
↓
StructuredOutputParser
↓
Structured JSON
Шаблон формирует запрос, модель отвечает, а StructuredOutputParser извлекает объявленные поля из ответа.
result = chain.invoke({"topic": "Solar System"})
print(result)
Как выглядит результат
Результатом является словарь, содержащий три значения под ключами, которые вы определили. Скрипт выводит его:
print(result)
Пример результата выглядит так:
{
"fact_1": "The Solar System is centered around the Sun.",
"fact_2": "There are eight recognized planets in the Solar System.",
"fact_3": "The Solar System is located in the Milky Way galaxy."
}
Конкретные значения могут отличаться. Однако набор ключей должен оставаться неизменным:
fact_1
fact_2
fact_3
В этом и заключается преимущество по сравнению с обычным парсингом JSON: имена полей определяет ваше приложение, а не модель. Если модель пропускает один из объявленных ключей, парсер выдает ошибку вместо того, чтобы молча вернуть результат другой структуры, с чем гораздо проще справиться, чем с исключением KeyError через три функции позже.
Отслеживание потока
Полный путь от темы до результата с ключами:
Solar System
↓
PromptTemplate
↓
ResponseSchema
↓
Format Instructions
↓
ChatOpenAI
↓
StructuredOutputParser
↓
{
fact_1: ...,
fact_2: ...,
fact_3: ...
}
Всё начинается с определений полей:
ResponseSchema(
name="fact_1",
description="Fact 1 about the topic"
)
Парсер преобразует их в инструкции форматирования, эти инструкции передаются в запрос, модель отвечает, а затем парсер извлекает указанные поля. По сравнению с предыдущим этапом:
JsonOutputParser
↓
JSON output
↓
Structure can vary
StructuredOutputParser
↓
Predefined fields
↓
More controlled structure
Короче говоря, JsonOutputParser предназначен для получения JSON в любом виде, тогда как StructuredOutputParser предназначен для получения JSON с ключами, которые вы указали.
Существует ограничение, которое стоит четко указать. У свойства type в объекте ResponseSchema значение по умолчанию — string, но оно лишь влияет на формулировку инструкций. Парсер проверяет наличие ключей; он не проверяет типы значений и диапазоны. Если вам нужно, чтобы значение age было целым числом выше определенного порога, этот парсер этого не обеспечит.
Также проверьте путь импорта в соответствии с версией LangChain, которая у вас установлена. В примере используется импорт из langchain.output_parsers, но в более новых версиях этот устаревший парсер был вынесен из основных пакетов, поэтому импорт может потребоваться изменить.
Вариант Hugging Face
Версия Gemma использует те же три схемы и ту же структуру парсера:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
load_dotenv()
# Define the model
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
schema = [
ResponseSchema(name='fact_1', description='Fact 1 about the topic'),
ResponseSchema(name='fact_2', description='Fact 2 about the topic'),
ResponseSchema(name='fact_3', description='Fact 3 about the topic'),
]
parser = StructuredOutputParser.from_response_schemas(schema)
template = PromptTemplate(
template='Give 3 fact about {topic} \n {format_instruction}',
input_variables=['topic'],
partial_variables={
'format_instruction': parser.get_format_instructions()
}
)
chain = template | model | parser
result = chain.invoke({'topic': 'Solar System'})
print(result)
А также ту же цепочку обработки:
PromptTemplate
↓
ChatHuggingFace
↓
StructuredOutputParser
↓
Structured JSON
Различается только поставщик модели. Схемы и парсер остаются прежними.
PydanticOutputParser: структура и валидация
PydanticOutputParser — самый строгий из четырех вариантов. Он описывает ожидаемый ответ с помощью модели Pydantic, поэтому определение формата вывода одновременно является и критерием его корректности.
Это выходит за рамки простого парсинга. Поля содержат настоящие типы Python, и функция Field() позволяет задавать ограничения, например требование к тому, чтобы целое число превышало определённое минимальное значение. Если ответ модели не соответствует этим ограничениям, возникает исключение, а не некорректные данные.
Почему стоит потратить дополнительное время на настройку
- Обеспечение соответствия схеме: ответ должен соответствовать чётко определённой структуре.
- Безопасность типов: поля используют типы Python, такие как
str,intиfloat; значения автоматически преобразуются или отклоняются в соответствии с этими типами. - Проверка корректности: Pydantic проверяет каждое заданное вами ограничение.
- Интеграция в цепочки обработки: он может использоваться с пromptами, моделями и цепочками LCEL точно так же, как и другие парсеры.
Проект о вымышленных персонах
В этом примере от модели требуется придумать человека из указанного места, здесь «Индия», с тремя полями:
name— имя человека.age— возраст человека.city— город, в котором он живет.
Для поля age также установлено ограничение: оно должно быть больше 18.
Input
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
LLM
↓
PydanticOutputParser
↓
Validated Pydantic Object
Версия OpenAI
Полный скрипт:
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
load_dotenv()
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
class Person(BaseModel):
name: str = Field(description="Name of the person")
age: int = Field(gt=18, description="Age of the person")
city: str = Field(description="Name of the city of the person")
parser = PydanticOutputParser(pydantic_object=Person)
template = PromptTemplate(
template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
input_variables=["place"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
chain = template | model | parser
final_result = chain.invoke({"place": "Indian"})
print(final_result)
Он следует той же структуре, что и ранее: определяется модель Person, она передается в PydanticOutputParser, а затем парсер связывается с промптом и моделью.
Конфигурация модели не изменилась:
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
Основной элемент — это класс Pydantic:
class Person(BaseModel):
name: str = Field(description="Name of the person")
age: int = Field(gt=18, description="Age of the person")
city: str = Field(description="Name of the city of the person")
Вот спецификация ответа:
nameдолжен быть строкой.
age должен быть целым числом, строго большим чем 18.city должен быть строкой.Field() добавляет как описание, понятное человеку и отображаемое в запросе, так и ограничения, которые проверяются после парсинга. Само ограниченное поле:
age: int = Field(gt=18, description="Age of the person")
gt=18 означает «больше чем 18», поэтому возраст ровно 18 не проходит проверку. Если вы имели в виду «18 и старше», используйте ge=18 вместо этого.
Парсер создается путем передачи класса, а не экземпляра:
parser = PydanticOutputParser(pydantic_object=Person)
Это указывает, какую модель использовать как для генерации инструкций, так и для проверки ответа.
Инструкции по формату поступают из того же метода, что и раньше:
parser.get_format_instructions()
Для этого парсера используется JSON Schema, сгенерированное на основе модели Pydantic, включающее описания полей и ограничение exclusiveMinimum для поля age. Оно вставляется через переменную partial, как обычно:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Шаблон запроса:
template = PromptTemplate(
template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
input_variables=["place"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
{place} определяет, какого типа человека должна сгенерировать модель. При использовании этого входного значения запрашиваются имя, возраст и город вымышленного индийца:
{"place": "Indian"}
Запуск цепочки
Цепочка сохраняет привычную трехэтапную структуру:
chain = template | model | parser
На этот раз финальный этап возвращает экземпляр модели:
PromptTemplate
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Pydantic Object
Шаблон формирует запрос, модель отвечает на него, а PydanticOutputParser парсит полученный JSON и проверяет его, преобразуя в объект типа Person.
final_result = chain.invoke({"place": "Indian"})
print(final_result)
Как выглядит результат
Результатом является объект Person, а не словарь. При выводе отображается стандартное представление от Pydantic:
name='Rahul Sharma' age=28 city='Mumbai'
Значения будут отличаться при каждой перезапуске. Гарантии же останутся прежними:
name → string
age → integer (> 18)
city → string
Именно здесь различия становятся наиболее очевидными. JsonOutputParser запрашивал только JSON, StructuredOutputParser указывал названия полей, а PydanticOutputParser представляет весь контракт в виде реального класса. Вы можете использовать автодополнение в редакторе для доступа к final_result.age и быть уверены, что это число типа int, большее 18, поскольку любое другое значение вызвало бы ошибку валидации ещё до выполнения вашего кода.
Отслеживание потока
От входных данных до проверенного объекта:
"Indian"
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Person Object
Всё начинается со структуры, показанной здесь без описаний и ограничений для лучшей читаемости:
class Person(BaseModel):
name: str
age: int
city: str
Класс передаётся парсеру:
PydanticOutputParser(pydantic_object=Person)
Парсер генерирует инструкции на основе модели; эти инструкции включаются в запрос, модель отвечает, а затем парсер преобразует ответ в объект типа Person и выполняет проверку согласно правилам Pydantic. Концептуально:
Pydantic Model
↓
Defines Structure + Types + Constraints
↓
LLM Response
↓
PydanticOutputParser
↓
Validated Pydantic Object
В итоге у вас получается объект на Python, данные которого гарантированно соответствуют установленным правилам, что больше, чем может обеспечить любой словарь в формате JSON.
Одним из практических последствий является то, что сбой проверки проявляется в виде исключения OutputParserException в цепочке обработки. Необходимо решить, что делать в такой ситуации. Распространённые варианты — повторная попытка выполнения запроса, передача ошибки обратно в модель с помощью OutputFixingParser из библиотеки LangChain или логирование с возвратом безопасного значения по умолчанию.
Вариант Hugging Face
Версия Gemma определяет ту же модель Person и передаёт её в тот же парсер:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
load_dotenv()
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
class Person(BaseModel):
name: str = Field(description='Name of the person')
age: int = Field(gt=18, description='Age of the person')
city: str = Field(description='Name of the city the person belongs to')
parser = PydanticOutputParser(pydantic_object=Person)
template = PromptTemplate(
template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
input_variables=['place'],
partial_variables={
'format_instruction': parser.get_format_instructions()
}
)
chain = template | model | parser
final_result = chain.invoke({'place': 'Indian'})
print(final_result)
Пайплайн остаётся без изменений:
PromptTemplate
↓
ChatHuggingFace
↓
PydanticOutputParser
↓
Pydantic Object
Изменяется только поставщик данных; модель Pydantic и парсер остаются общими. Менее сложные модели чаще нарушают ограничения или добавляют лишний текст, и именно в таких случаях проверка данных оказывается полезной: некорректный ответ фиксируется на этапе входа, а не попадает в основные данные.
Выбор подходящего парсера
Решение зависит от того, какой уровень структурирования и проверки действительно требуется от получателя ответа. Ниже приведено краткое описание каждого варианта.
StrOutputParser
Используйте его, когда ответ модели представляет собой просто текст.
- Идеально для: отчётов, объяснений, кратких изложений, ответов в чате.
- Возвращает: строку.
- Парсинг JSON: отсутствует.
- Схема: отсутствует.
- Проверка корректности: отсутствует.
JsonOutputParser
Используйте его, когда вам нужен JSON, но вы готовы мириться с переменной структурой данных или можете с ней справиться.
- Лучше всего подходит для: исследовательского форматирования данных, гибких наборов данных.
- Возвращает: словарь или список.
- Разбор JSON: да.
- Схема: отсутствует (в используемой здесь форме без аргументов).
- Проверка корректности: только проверка на то, что это действительно валидный JSON.
StructuredOutputParser
Используйте его, когда ваш код ожидает определенные ключи, такие как fact_1 до fact_3.
- Лучше всего подходит для: простых записей с известными именами полей.
- Возвращает: словарь с указанными ключами.
- Разбор JSON: да.
- Схема: да, с указанием имен полей и описаний.
- Проверка корректности: только наличие ключей, без проверки типов.
PydanticOutputParser
Используйте его тогда, когда результат напрямую поступает в логику приложения и должен быть корректным.
- Лучше всего подходит для: данных, которые вы храните, обрабатываете или передаёте в API.
- Возвращает: экземпляр вашей модели Pydantic.
- Распаковка JSON: да.
- Схема: да, полные типы и ограничения.
- Проверка: да.
Краткая модель понимания
StrOutputParser: достаточно текста.JsonOutputParser: подойдёт любой валидный JSON.StructuredOutputParser: JSON должен содержать эти ключи.PydanticOutputParser: проверяются эти ключи, типы и все ограничения.
Выберите самый простой парсер, который обеспечивает необходимые гарантии. Каждый уровень сложности добавляет дополнительные токены для инструкций и новые способы отклонения ответа, поэтому повышение строгости должно быть осознанным решением.
Существует ещё один вариант, который стоит учесть. Все четыре парсера работают путём описания формата в промпте и последующей обработки текста. Многие чат-модели также поддерживают нативный структурированный вывод или вызов инструментов, что LangChain реализует с помощью метода with_structured_output() для модели. Если ваш провайдер это поддерживает, такой подход обычно более надёжен для данных в формате схемы, тогда как парсеры на основе промптов остаются полезными для провайдеров и моделей, у которых этой функции нет.
Основные выводы
- Парсеры вывода преобразуют сообщение модели в значение, которое может быть использовано вашим кодом, и они интегрируются в цепочки LCEL с помощью оператора «трубка».
StrOutputParserудаляет обёртку сообщения, чтобы текст одной модели мог стать входными данными для следующего промпта.JsonOutputParserпарсит JSON, но не корректирует его структуру, если только вы не предоставите ему схему.
StructuredOutputParser корректирует имена ключей с помощью ResponseSchema, но не проверяет типы значений.PydanticOutputParser сочетает процесс парсинга с проверками типов и ограничениями, возвращая реальный объект.Как только ответы будут поступать в стабильном формате, следующим логичным шагом будет объединение нескольких промптов, моделей и парсеров в более крупные рабочие процессы, включая последовательные, параллельные и условные цепочки, созданные с использованием RunnableParallel и RunnableBranch. Подробнее об этом см. в статье создание пайплайнов LangChain с использованием LCEL.