Від звичайного тексту до перевірених об’єктів: вибір парсера результату 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
Оператор трубки є композицією у мові виразів LCEL (LangChain Expression Language): результат кожного компонента стає вхідними даними для наступного. Якщо представити це вертикально, порядок виконання буде таким:
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_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 із ключами, які ви зазначили.
Існує обмеження, яке варто чітко зазначити. У ResponseSchema є атрибут type, який за замовчуванням дорівнює 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 перевіряє кожне заявлене вами обмеження.
- Інтеграція в ланцюги: він інтегрується з запитами, моделями та ланцюгами 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.