Od tekstu prostego do zweryfikowanych obiektów: wybór parsera wyników LangChain
Porównaj StrOutputParser, JsonOutputParser, StructuredOutputParser oraz PydanticOutputParser w łańcuchach LangChain i dowiedz się, jak dużą strukturę faktycznie gwarantuje każdy z nich.
Model czatowy zwraca tekst, który jest dobrze czytelny dla osoby patrzącej na ekran. Gdy odpowiedź musi służyć jako podstawa dla kolejnego zapytania, trafić do bazy danych lub sterować komponentem interfejsu użytkownika, potrzebny jest coś bardziej przewidywalnego: czysta ciąg znaków, obiekt JSON lub zapis typowany, który został już sprawdzony. LangChain wypełnia tę lukę za pomocą parserów wyników, dostarczając kilku z nich o zupełnie różnych gwarancjach. W tym przewodniku budujemy ten sam mały projekt Układu Słonecznego przy użyciu czterech parserów, wyjaśniamy każdą linię każdego łańcucha i kończymy praktyczną zasadą wyboru między nimi.
Jeśli chcesz dowiedzieć się więcej o tym, jak parserzy współpracują z LCEL, uruchamialnymi elementami i pamięcią, przeczytaj od surowego tekstu do pipeline’ów w LangChain. Tutaj skupienie jest węższe: na tym, co każdy parser faktycznie obiecuje i gdzie kończy się ta obietnica.
Dlaczego sam surowy tekst modelu nie wystarcza
Gdy zapytasz model o Układ Słoneczny, możesz otrzymać coś w rodzaju poniższego tekstu. Jest dobrze napisany, ale program nie może z niego wydobyć konkretnej informacji bez domyślania się, gdzie kończy się jedno zdanie, a zaczyna następne.
The Solar System consists of the Sun and the objects that orbit it.
It contains eight planets along with moons, asteroids, and comets.
Kod, który przetwarza tę odpowiedź, wolałby otrzymywać wartości nazwane, do których może się bezpośrednio odwołać, na przykład obiekt z jedną kluczem dla każdej informacji:
{
"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."
}
Parser wyjściowy to komponent, który łączy te dwa elementy. Otrzymuje wszystko, co wygenerował model, i zwraca wartość, której aplikacja może używać bez dodatkowej obróbki ciągów znaków. Parserzy różnią się stopniem zaawansowania: niektóre jedynie odsłaniają tekst, inne parsują JSON, a te najbardziej rygorystyczne weryfikują wynik pod kątem schematu.
Raw LLM Response
↓
Output Parser
↓
Parsed Output
Pamiętaj o tym trójetapowym modelu. Każdy przykład poniżej jest jego wariacją – do środkowej części dodaje się coraz więcej elementów w miarę zaostrzania wymagań.
Cztery parserzy, cztery poziomy struktury
Cztery parserzy omówione tutaj tworzą „drabinę”, przy czym każdy kolejny poziom zapewnia większą kontrolę nad wynikiem:
StrOutputParserprzekształca wiadomość modelu w zwykły ciąg znaków w Pythonie.JsonOutputParserparsuje odpowiedź na wartość kompatybilną z JSON, taką jak słownik lub lista.
StructuredOutputParser umożliwia określenie pól o nazwach, które model powinien zwrócić.PydanticOutputParser opisuje oczekiwany kształt danych za pomocą modelu Pydantic i weryfikuje wynik pod względem tego opisu.Niezależnie od wyboru, ścieżka danych wygląda tak samo:
LLM Response
↓
Output Parser
↓
Parsed Output
Zmienia się to, co pojawia się na końcu, oraz stopień, w jakim można mu ufać.
Przykład działania: raport, a następnie streszczenie
Pierwszy projekt to proces składający się z dwóch kroków. Temat „System Słoneczny” trafia do modelu, który tworzy długi raport. Ten raport jest następnie podawany do drugiego modelu, który prosi o pięciolinijowe streszczenie. Kluczowym elementem jest przekazanie danych: wynik pierwszego wywołania modelu staje się wejściem dla następnego modelu.
Solar System
↓
LLM
↓
Detailed Report
↓
LLM
↓
5-Line Summary
Bез parsera pierwszy krok zwraca obiekt wiadomości, a nie tekst raportu, więc trzeba by go rozpakować przed utworzeniem drugiego zapytania. Parser umieszczony pomiędzy nimi wykonuje to rozpakowanie jako część łańcucha, dzięki czemu oba kroki mogą funkcjonować sprawnie.
StrOutputParser: gdy potrzebny jest tylko tekst
StrOutputParser to najprostszy parser dostępny w LangChain. Modele do rozmów zwracają obiekt AIMessage; ten parser wydobywa jego zawartość i dostarcza zwykłą ciąg znaków. Jest to odpowiedni narzędzie, gdy kolejnym odbiorcą jest człowiek, inne zapytanie lub cokolwiek innego, co potrzebuje jedynie tekstu opisowego, a nie JSON ani schematu.
Włączenie go do procesu tworzenia raportów
W projekcie report-then-summary kroki wyglądają następująco:
- Pierwsze zapytanie prosi model o szczegółowy raport na dany temat.
Po każdej wywołaniu modelu znajduje się StrOutputParser, dzięki czemu każda przekazana wartość to zwykła ciąg znaków. Pełna sekwencja komponentów jest następująca:
Solar System
↓
Prompt 1
↓
LLM
↓
StrOutputParser
↓
Detailed Report
↓
Prompt 2
↓
LLM
↓
StrOutputParser
↓
5-Line Summary
Wersja OpenAI
Oto pełny łańcuch używający ChatOpenAI. Przeczytaj go od góry do dołu raz, a potem omówimy te części, które są istotne.
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)
Model jest tworzony przy użyciu domyślnych ustawień. Funkcja load_dotenv() pobiera klucz API z lokalnego pliku .env, więc żadne dane uwierzytelniające nie znajdują się w skrypcie.
model = ChatOpenAI()
Pierwszy szablon przyjmuje jedną zmienną, topic, i żąda szczegółowego raportu na jej temat.
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
Drugi szablon oczekuje zmiennej o nazwie text, która będzie przechowywać raport, oraz prosi o pięciowierszowe podsumowanie tego raportu.
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
Jeden mały błąd warto naprawić, jeśli to kopiujesz: ciąg szablonu zawiera /n, co jest dosłownym ukośnikiem połączonym z literą n, a nie przejściem do nowej linii. Użyj \n, jeśli chcesz, aby raport zaczynał się na nowej linii. Modele zazwyczaj radzą sobie w obu przypadkach, ale komenda, którą myślisz, że wysyłasz, powinna być tą samą, którą faktycznie wysyłasz.
Następnie przychodzi parser. Jedna instancja może być ponownie wykorzystana w kilku miejscach łańcucha, ponieważ nie przechowuje żadnego stanu pomiędzy wywołaniami.
parser = StrOutputParser()
Linia, która łączy wszystko razem, to definicja łańcucha:
chain = template1 | model | parser | template2 | model | parser
Operator rury stanowi kompozycję w języku wyrażeń LCEL (LangChain Expression Language): wynik każdego komponentu staje się wejściem dla następnego. Ułożone wertykalnie, kolejność wykonywania jest następująca:
template1
↓
model
↓
parser
↓
template2
↓
model
↓
parser
Zwróć uwagę, co daje ci pierwszy parser. Po pierwszym wywołaniu modelu parser zwraca raport w postaci ciągu znaków, a właśnie ten ciąg znaków wypełnia {text} w drugim szablonie. Drugi parser wykonuje tę samą czynność dla ostatecznej odpowiedzi, więc wynikiem łańcucha jest sam streszczenie, a nie obiekt wiadomości.
Rozpoczynasz łańcuch, przekazując słownik, którego klucze odpowiadają zmiennym wejściowym pierwszego szablonu:
result = chain.invoke({'topic': 'Solar System'})
Następnie wyświetlasz wynik:
print(result)
Co zwraca łańcuch
Wartość wyświetlona na końcu to pięciowierszowy streszczenie pochodzące z wygenerowanego raportu. Ponieważ ostatnim komponentem jest StrOutputParser, wynikiem jest zwykła wartość typu str w Pythonie:
print(result)
Typowy przeprowadzony test daje wynik w przybliżeniu taki:
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.
Traktuj to jako ilustrację struktury, a nie jako stałą odpowiedź. Sformułowania będą się różnić w zależności od przeprowadzanych testów oraz modeli.
Ta sama procedura z modelem Hugging Face
Proces raportowania, a następnie tworzenia streszczenia może być również stosowany z otwartym modelem. Ta wersja wykorzystuje HuggingFaceEndpoint skierowany na adres google/gemma-2-2b-it i otacza go warstwą ChatHuggingFace, dzięki czemu zachowuje się jak model do rozmów:
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)
Istnieje tu ważna różnica. Ta wersja nigdy nie używa StrOutputParser ani nie buduje łańcucha rurek. Każde polecenie formatuje ręcznie za pomocą .invoke(), wywołuje model i odczytuje .content z zwróconej wiadomości, zanim ją przekaże dalej. To działa, ale jest to dokładnie ten ręczny proces rozpakowywania, który ma na celu usunięcie parsera.
Obok siebie obie metody wyglądają w ten sposób. Z OpenAI i parserem:
OpenAI
Prompt
↓
ChatOpenAI
↓
StrOutputParser
↓
String
Z Hugging Face i ręcznym dostępem:
Hugging Face
Prompt
↓
ChatHuggingFace
↓
result.content
↓
String
Oba kończą się ciągiem znaków. Skrypt OpenAI pokazuje, jak parser wykonywać tę funkcję w ramach łańcucha, natomiast skrypt Hugging Face przedstawia tę samą logikę aplikacji z innym dostawcą i bez parsera. Nic nie stoi na przeszkodzie w użyciu konstrukcji template1 | model | parser | template2 | model | parser również z modelem Hugging Face – parser nie przejmuje się tym, który dostawca wygenerował wiadomość.
Wniosek z tego etapu: używaj StrOutputParser, gdy aplikacja potrzebuje odpowiedzi wyłącznie w formie tekstu.
JsonOutputParser: JSON bez kontraktu
JsonOutputParser to kolejny krok w rozwoju. Prosi model o dane w formacie JSON i przekształca odpowiedź na dane w języku Python, co jest przydatne, gdy kod musi wybierać wartości na podstawie kluczy zamiast czytać tekst opisowy.
To, czego nie robi, to narzucanie określonego kształtu. Bez schematu model jest proszony o odpowiedź w formacie JSON, ale nie podaje się żadnych informacji na temat tego, które klucze muszą występować ani jakiego typu powinny być ich wartości. Dwa wykonywania z tym samym promptem mogą legalnie zwracać obiekty o różnym kształcie, a Twój kod musi być na to przygotowany.
Jak wszystko się łączy
W tym projekcie od modelu wymaga się pięciu faktów o Układzie Słonecznym. Kroki są następujące:
- Stworzyć
JsonOutputParser. - Zapytać go o instrukcje formatowania za pomocą
get_format_instructions(). - Wstawić te instrukcje do promptu.
- Przesłać prompt do modelu.
- Pozwolić parserowi przekształcić odpowiedź na wartość w Pythonie.
Solar System
↓
PromptTemplate
↓
Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON Object
wersja OpenAI
Cały łańcuch jest krótki:
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)
Model jest skonfigurowany z gpt-4.1-mini i temperaturą 0, co zapewnia wynik tak powtarzalny, na jaki pozwala model. To właśnie ten komponent będzie zapisywał pięć faktów.
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
Parser jest tworzony bez żadnych argumentów, i to właśnie dlatego nie ma schematu do egzekwowania:
parser = JsonOutputParser()
Instrukcje formatu wykonują rzeczywistą pracę
Zanim model zostanie uruchomiony, parser może opisać format, którego oczekuje. Ten opis pochodzi z jednej wywołania metody:
parser.get_format_instructions()
Zwraca on blok tekstu, który instruuje model do odpowiedzi w formacie JSON. Nie jest to kod uruchamiany na modelu; to tekst promptu. Wstrzykuje się go do szablonu za pomocą partial_variables, co polega na uzupełnieniu zmiennej szablonu tylko raz, podczas definiowania szablonu, a nie przy każdym wywołaniu:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Wynikowy szablon zawiera dwa miejsca zastępcze:
template = PromptTemplate(
template="Give me 5 facts about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic}jest podawane w momencie wywołania i określa, jakie informacje są potrzebne.{format_instruction}jest z góry wypełniane wytycznymi dotyczącymi formatowania od strony parsera.
Zatem gdy wywołasz łańcuch z tymi danymi, model otrzymuje zarówno temat, jak i instrukcję dotyczącą odpowiedzi w formacie JSON:
{"topic": "Solar System"}
Kompletowanie i uruchamianie łańcucha
Sam łańcuch składa się z zaledwie trzech etapów:
chain = template | model | parser
W kolejności wykonywania:
PromptTemplate
↓
ChatOpenAI
↓
JsonOutputParser
↓
Parsed JSON
Szablon generuje ostateczne zapytanie, ChatOpenAI na nie odpowiada, a JsonOutputParser przetwarza tę odpowiedź na dane w języku Python. Parser jest również wyrozumiały wobec niektórych powszechnych nawyków modeli, takich jak umieszczanie JSON w ramkach kodu Markdown, które są usuwane przed przetwarzaniem.
Wywołaj to z tematem:
result = chain.invoke({"topic": "Solar System"})
I wydrukuj to, co wróciło:
print(result)
Co wraca
Wynik zawiera pięć faktów w formacie JSON. Skrypt wydrukuje tylko wartość, więc nie ma kanonicznego wyniku do ujęcia w cudzysłowy; prawdopodobna odpowiedź wygląda tak:
{
"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."
]
}
Taka struktura, pojedynczy klucz facts zawierający listę, to jedna z kilku możliwych opcji wybranych przez model. Inny przeprowadzony test może zwrócić fact_1 do fact_5, albo prostą listę. Jeśli kod poniżej odwołuje się do result["facts"], będzie działał błędnie w momencie, gdy model wybierze inną strukturę.
Śledzenie przepływu
W porównaniu z StrOutputParser, nowym elementem jest to, że parser bierze udział dwa razy: raz przed wywołaniem modelu, dostarczając instrukcje, i raz po nim, przeprowadzając analizę.
Prompt
↓
JSON Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON
Instrukcje są generowane przez sam parser:
parser.get_format_instructions()
Dostają się do interfejsu użytkownika za pośrednictwem zmiennej częściowej:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Model odpowiada, korzystając z tych instrukcji, a parser przekształca odpowiedź na wartość w języku Python. Od początku do końca:
Solar System
↓
PromptTemplate
↓
JSON Format Instructions
↓
ChatOpenAI
↓
JsonOutputParser
↓
JSON Object
Różnica w porównaniu z poprzednim etapem mieści się w dwóch liniach. StrOutputParser generuje:
StrOutputParser
↓
Plain String
Podczas gdy JsonOutputParser generuje:
JsonOutputParser
↓
JSON-compatible Structured Data
Pamiętaj o ograniczeniu: nadal nie ma ustalonej struktury. Otrzymujesz JSON, ale pola i ich ułożenie zależą od modelu. Należy dodać, że aktualne wersje JsonOutputParser przyjmują również opcjonalny argument pydantic_object, który dodaje strukturę do instrukcji formatowania, ale w przedstawionej tutaj formie, bez argumentów, wymaga jedynie poprawnego JSON.
Wariant Hugging Face
Przepływ pracy w formacie JSON jest bezpośrednio przekazywany do modelu Gemma. Ustawienia modelu ulegają zmianie; parser, instrukcje formatowania oraz łańcuch przetwarzania pozostają bez zmian:
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)
Proces jest identyczny, z wyjątkiem pola z modelem:
PromptTemplate
↓
Hugging Face Model
↓
JsonOutputParser
↓
JSON Object
To jest praktyczna korzyść z umieszczenia procesu parsowania w osobnym komponencie: zmiana dostawców nie wpływa na logikę parsowania. Należy jednak pamiętać, że małe modele otwarte traktują instrukcje formatowania mniej wiarygodnie niż większe modele hostowane. Jeśli odpowiedź zawiera tekst otaczający JSON lub przecinek na końcu, parser wywołuje błąd OutputParserException, dlatego kod produkcyjny powinien go przechwycić i spróbować ponownie lub przejść na alternatywę.
StructuredOutputParser: nadawanie nazw oczekiwanych pól
StructuredOutputParser wydobywa dane w formacie JSON na podstawie listy pól, które zdefiniujesz z góry. Podczas gdy JsonOutputParser wymaga jedynie „odpowiedzi w formacie JSON”, ten parser określa „odpowiedź w formacie JSON z tymi kluczami”.
Pola są deklarowane za pomocą ResponseSchema. Każde z nich ma name oraz description, a opis ten informuje model, co powinno znajdować się w danym polu. Dzięki temu uzyskujemy znacznie większą kontrolę nad strukturą odpowiedzi.
W tym projekcie wymagane są trzy fakty na temat Układu Słonecznego, po jednym polu na każdy fakt:
fact_1zawiera pierwszy fakt na dany temat.fact_2zawiera drugi.fact_3zawiera trzeci.
Parser przekształca te deklaracje w instrukcje, a następnie sprawdza odpowiedź pod kątem ich spełnienia:
Solar System
↓
PromptTemplate
↓
Predefined Field Schema
↓
LLM
↓
StructuredOutputParser
↓
Structured JSON
Wersja OpenAI
Oto pełny skrypt:
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)
Model ma taką samą konfigurację gpt-4.1-mini jak wcześniej:
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
Prawdziwa różnica zaczyna się od listy schematów:
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"),
]
Każdy element ResponseSchema dostarcza dwie informacje:
namestaje się kluczem w otrzymanym słowniku.descriptionokreśla, co powinien zawierać dany klucz.
Trzy schematy dają trzy wymagane klucze: fact_1, fact_2 i fact_3.
Nie tworzy się tego parsera bezpośrednio. Metoda klasowa buduje go na podstawie listy schematów:
parser = StructuredOutputParser.from_response_schemas(schema)
Podobnie jak w przypadku parsera JSON, instrukcje dotyczące formatu pochodzą od samego parsera:
parser.get_format_instructions()
Tym razem instrukcje są bardziej rozbudowane. Zawierają szkielet w formacie JSON z wyliczeniem każdej nazwy pola wraz z jej opisem oraz proszą model o umieszczenie odpowiedzi w zamkniętym bloku JSON. Są one połączone w ten sam sposób:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Ścieżka promptu zawiera dwa znajome miejsca zastępcze:
template = PromptTemplate(
template="Give 3 fact about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic} jest wypełniane podczas wywołania, a {format_instruction} zawiera listę pól wygenerowaną na podstawie schematów. Wywołanie z tymi danymi wysyła oba elementy do modelu:
{"topic": "Solar System"}
Uruchamianie łańcucha
Łańcuch składa się z tych samych trzech etapów co wcześniej:
chain = template | model | parser
Gdzie parser znajduje się na ostatniej pozycji:
PromptTemplate
↓
ChatOpenAI
↓
StructuredOutputParser
↓
Structured JSON
Ścieżka tworzy prompt, model udziela odpowiedzi, a StructuredOutputParser wyodrębnia deklarowane pola z tej odpowiedzi.
result = chain.invoke({"topic": "Solar System"})
print(result)
Jak wygląda wynik
Wynikiem jest słownik zawierający trzy dane pod kluczami, które zdefiniowałeś. Skrypt je wyświetla:
print(result)
A przykładowy wynik wygląda następująco:
{
"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."
}
Konkretne dane mogą się różnić. To, co nie powinno ulegać zmianie, to zbiór kluczy:
fact_1
fact_2
fact_3
To jest zaleta w porównaniu z prostym parsowaniem JSON: to twoja aplikacja decyduje o nazwach pól, a nie model. Jeśli model pominie jeden z deklarowanych kluczy, parser wywoła błąd zamiast cicho zwrócić inny format, co jest o wiele łatwiejsze do obsługi niż błąd KeyError trzy funkcje później.
Śledzenie przepływu
Pełna ścieżka, od tematu do wyniku z kluczami:
Solar System
↓
PromptTemplate
↓
ResponseSchema
↓
Format Instructions
↓
ChatOpenAI
↓
StructuredOutputParser
↓
{
fact_1: ...,
fact_2: ...,
fact_3: ...
}
Zaczyna się od definicji pól:
ResponseSchema(
name="fact_1",
description="Fact 1 about the topic"
)
Parser przekształca je w instrukcje formatowania, które trafiają do promptu, model odpowiada, a następnie parser wyodrębnia deklarowane pola. W porównaniu z poprzednim etapem:
JsonOutputParser
↓
JSON output
↓
Structure can vary
StructuredOutputParser
↓
Predefined fields
↓
More controlled structure
Krótko mówiąc, JsonOutputParser służy do uzyskania JSON w ogóle, natomiast StructuredOutputParser służy do uzyskania JSON z kluczami, o które prosiłeś.
Istnieje ograniczenie, które warto jasno podkreślić. ResponseSchema ma atrybut type, którego domyślna wartość to string, ale zmienia on jedynie sformułowanie instrukcji. Parser sprawdza obecność kluczy; nie weryfikuje typów ani zakresów wartości. Jeśli potrzebujesz, aby age było liczbą całkowitą powyżej określonego progu, ten parser tego nie zagwarantuje.
Należy również sprawdzić ścieżkę importu w odniesieniu do wersji LangChain, którą używasz. Przykład importuje z langchain.output_parsers, a w nowszych wersjach ten starszy parser został usunięty z pakietów podstawowych, więc import może wymagać zmiany.
Wariant Hugging Face
Wersja Gemma ponownie wykorzystuje te same trzy schematy oraz tę samą strukturę parsera:
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)
I ten sam pipeline:
PromptTemplate
↓
ChatHuggingFace
↓
StructuredOutputParser
↓
Structured JSON
Różni się tylko dostawca modelu. Schemat i parser pozostają bez zmian.
PydanticOutputParser: struktura plus walidacja
PydanticOutputParser jest najbardziej rygorystycznym z czterech. Opisuje oczekiwaną odpowiedź za pomocą modelu Pydantic, więc definicja wyjścia jest jednocześnie definicją tego, co uznaje się za ważne.
To wykracza poza zwykłe parsowanie. Pola przechowują rzeczywiste typy w Pythonie, a Field() umożliwia dodawanie ograniczeń, na przykład wymógu, aby liczba całkowita przekraczała określoną wartość minimalną. Gdy odpowiedź modelu ich nie spełnia, otrzymujemy wyjątek zamiast nieprawidłowych danych.
Dlaczego warto poświęcić dodatkowy czas na konfigurację
- Wymuszanie zgodności ze schematem: odpowiedź musi odpowiadać dobrze zdefiniowanemu kształtowi.
- Bезpieczeństwo typów: pola wykorzystują typy z Pythona, takie jak
str,intifloat, a wartości są odpowiednio przekształcane lub odrzucane. - Walidacja: Pydantic sprawdza każde zadeklarowane ograniczenie.
- Integracja z łańcuchami: pozwala na integrację z promptami, modelami oraz łańcuchami LCEL dokładnie tak samo jak inne narzędzia do parsowania.
Projekt dotyczący fikcyjnych postaci
To przykład poleca modelowi stworzenie osoby pochodzącej z określonego miejsca, w tym przypadku „Indii”, z trzema polami:
name– imię osoby.age– wiek osoby.city– miasto, w którym mieszka.
Pole age ma również ograniczenie: musi być większe od 18.
Input
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
LLM
↓
PydanticOutputParser
↓
Validated Pydantic Object
Wersja OpenAI
Pełny skrypt:
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)
Przestrzega tej samej struktury co wcześniej: definiuje się model Person, przekazuje go do PydanticOutputParser, a następnie łączy parser z promptem i modelem.
Konfiguracja modelu pozostała bez zmian:
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
Głównym elementem jest klasa 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")
Oto specyfikacja odpowiedzi:
namemusi być łańcuchem znaków.
age musi być liczbą całkowitą wyraźnie większą od 18.city musi być łańcuchem tekstowym.Field() umożliwia dodanie zarówno opisu czytelnego dla człowieka, który trafia do wiadomości, jak i ograniczeń, które są sprawdzane po parsowaniu. Pole objęte ograniczeniami samo w sobie:
age: int = Field(gt=18, description="Age of the person")
gt=18 oznacza „większe od 18”, więc wiek dokładnie 18 lat nie przechodzi walidacji. Jeśli chodziło o „18 lat lub więcej”, użyj zamiast tego ge=18.
Parsery tworzy się poprzez przekazanie klasy, a nie instancji:
parser = PydanticOutputParser(pydantic_object=Person)
To określa, który model ma być używany zarówno do generowania instrukcji, jak i do walidacji odpowiedzi.
Instrukcje formatowania pochodzą z tej samej metody co wcześniej:
parser.get_format_instructions()
Dla tego parsera zawierają one schemat JSON wygenerowany na podstawie modelu Pydantic, w tym opisy pól oraz ograniczenie exclusiveMinimum dla pola age. Są one wstawiane za pomocą zmiennej partial, jak zwykle:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Ścieżka promptu:
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} określa, jaki rodzaj osoby powinien stworzyć model. Użycie tego wprowadzenia polega na zapytaniu o imię, wiek i miasto fikcyjnej osoby z Indii:
{"place": "Indian"}
Uruchamianie łańcucha
Łańcuch zachowuje znajomy trójetapowy układ:
chain = template | model | parser
Tym razem ostatni etap zwraca instancję modelu:
PromptTemplate
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Pydantic Object
Ścieżka buduje prompt, model udziela odpowiedzi, a PydanticOutputParser parsuje JSON i waliduje je jako obiekt typu Person.
final_result = chain.invoke({"place": "Indian"})
print(final_result)
Jak wygląda wynik
Wynikiem jest obiekt typu Person, a nie słownik. Jego wydrukowanie pokazuje standardową reprezentację dostarczaną przez Pydantic:
name='Rahul Sharma' age=28 city='Mumbai'
Wartości mogą się różnić przy każdym uruchomieniu. Gwarancje natomiast pozostaną niezmienne:
name → string
age → integer (> 18)
city → string
Tutaj różnice są najbardziej wyraźne. JsonOutputParser prosił jedynie o format JSON, StructuredOutputParser określał nazwy pól, natomiast PydanticOutputParser reprezentuje cały kontrakt jako rzeczywistą klasę. Można uzyskać dostęp do final_result.age za pomocą autodopowiadania w edytorze i mieć pewność, że jest to wartość typu int większa niż 18, ponieważ wszystko inne spowodowałoby błąd walidacji jeszcze przed dotarciem do kodu.
Śledzenie przepływu
Od danych wejściowych do zweryfikowanego obiektu:
"Indian"
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Person Object
Zaczyna się od struktury, pokazanej tutaj bez opisów i ograniczeń dla lepszej czytelności:
class Person(BaseModel):
name: str
age: int
city: str
Klasa jest przekazywana parserowi:
PydanticOutputParser(pydantic_object=Person)
Parser generuje instrukcje na podstawie modelu, instrukcje te są zawarte w promptzie, model odpowiada, a parser analizuje tę odpowiedź na obiekt typu Person i przeprowadza weryfikację za pomocą Pydantic. Koncepcyjnie:
Pydantic Model
↓
Defines Structure + Types + Constraints
↓
LLM Response
↓
PydanticOutputParser
↓
Validated Pydantic Object
Otrzymujemy obiekt w Pythonie, którego dane z góry spełniają ustalone reguły, co jest lepsze niż to, co może zagwarantować dowolny słownik JSON.
Jedną z praktycznych konsekwencji jest to, że błąd weryfikacji pojawia się jako OutputParserException w łańcuchu przetwarzania. Należy zdecydować, co w takim przypadku robić. Powszechnymi opcjami są ponowne wykonanie operacji, przekazanie błędu z powrotem do modelu za pomocą OutputFixingParser z LangChain lub zapisanie informacji o błędzie i zwrócenie bezpiecznego wartości domyślnej.
Wariant Hugging Face
Wersja Gemma definiuje ten sam model Person i przekazuje go do tego samego parsera:
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)
Proces przetwarzania pozostaje niezmieniony:
PromptTemplate
↓
ChatHuggingFace
↓
PydanticOutputParser
↓
Pydantic Object
Zmienia się tylko dostawca; model Pydantic oraz parser są wspólne. Mniejsze modele częściej naruszają ograniczenia lub dodają niepotrzebny tekst, co jest dokładnie sytuacją, w której walidacja się sprawdza: błędna odpowiedź jest wykrywana na wstępie, zamiast przedostać się do Twoich danych.
Wybór odpowiedniego parsera
Decyzja zależy od tego, ile struktury i ile walidacji faktycznie potrzebuje odbiorca odpowiedzi. Poniżej znajduje się streszczenie każdej opcji.
StrOutputParser
Użyj go, gdy odpowiedź modelu to po prostu tekst.
- Najlepszy do: raportów, wyjaśnień, streszczeń, odpowiedzi w czacie.
- Zwraca: ciąg znaków.
- Analiza JSON: nie.
- Schemat: nie.
JsonOutputParser
Użyj go, gdy potrzebujesz formatu JSON, ale możesz zaakceptować lub obsłużyć zmienną strukturę.
- Najlepiej nadaje się do: eksploracyjnego wyświetlania danych strukturalnych, elastycznych treści.
- Zwraca: słownik lub listę.
- Analiza JSON: tak.
- Schemat: nie (w formie bez argumentów użytej tutaj).
- Walidacja: tylko sprawdzenie, czy jest to ważny JSON.
StructuredOutputParser
Użyj go, gdy twój kod oczekuje określonych kluczy, takich jak fact_1 do fact_3.
- Najlepiej nadaje się do: prostych rekordów z znanych nazw pól.
- Zwraca: słownik zawierający deklarowane klucze.
- Analiza JSON: tak.
- Schemat: tak, z nazwami pól i opisami.
- Walidacja: tylko obecność kluczy, brak sprawdzeń typów.
PydanticOutputParser
Użyj go wtedy, gdy wynik jest podawany bezpośrednio do logiki aplikacji i musi być poprawny.
- Najlepszy do: danych, które przechowujesz, nad którymi obliczasz lub przekazujesz do API.
- Zwraca: instancję twojego modelu Pydantic.
- Analiza JSON: tak.
- Schemat: tak, pełne typy i ograniczenia.
Szybki model mentalny
StrOutputParser: wystarcza tekst.JsonOutputParser: zadziała każdy ważny plik JSON.StructuredOutputParser: plik JSON musi zawierać te klucze.PydanticOutputParser: sprawdzane są te klucze, te typy oraz wszystkie ograniczenia.
Wybierz najprostszy parser, który zapewnia wymagane gwarancje. Każdy kolejny poziom dodaje tokeny szybkiego działania dla instrukcji oraz nowe sposoby odrzucenia odpowiedzi, więc wyższy stopień ścisłości powinien być celowym wyborem.
Jeszcze jedna opcja powinna być uwzględniona. Wszystkie cztery parserzy działają poprzez opisanie formatu w instrukcji i następne przetworzenie tekstu. Wiele modeli czatowych obsługuje również natywny strukturyzowany wynik lub wywoływanie narzędzi, co LangChain umożliwia za pomocą metody with_structured_output() w modelu. Gdy dostawca tego obsługuje, taki podejście jest zazwyczaj bardziej niezawodne dla danych w formie schematu, natomiast parserzy oparte na instrukcjach pozostają przydatne dla dostawców i modeli, które tego nie mają.
Główne wnioski
- Parserzy wyników przekształcają wiadomość modelu w wartość, którą może wykorzystać kod, i łączą się z łańcuchami LCEL za pomocą operatora pipe.
StrOutputParserusuwa otoczkę wiadomości, dzięki czemu tekst jednego modelu może służyć jako treść następnej instrukcji.JsonOutputParserprzetwarza JSON, ale nie koryguje jego struktury, chyba że podasz mu schemat.
StructuredOutputParser koryguje nazwy kluczy za pomocą ResponseSchema, ale nie sprawdza typów wartości.PydanticOutputParser łączy proces parsowania z kontrolą typów i ograniczeniami, zwracając rzeczywisty obiekt.Gdy odpowiedzi będą dostępne w spójnej formie, naturalnym następnym krokiem jest łączenie różnych promptów, modeli i parserów w większe procesy pracy, w tym sekwencyjne, równoległe i warunkowe łańcuchy budowane za pomocą RunnableParallel oraz RunnableBranch. Aby dowiedzieć się więcej na ten temat, zapoznaj się z tworzeniem pipeline’ów LangChain przy użyciu LCEL.