Startseite / Artikel / Vom reinen Text zu validierten Objekten: Auswahl eines LangChain-Output-Parsers

Vom reinen Text zu validierten Objekten: Auswahl eines LangChain-Output-Parsers

Vergleichen Sie StrOutputParser, JsonOutputParser, StructuredOutputParser und PydanticOutputParser in LangChain-Ketten und erfahren Sie, wie viel Struktur jede dieser Parser tatsächlich gewährleistet.

5418 Wörter

Ein Chat-Modell gibt Text zurück, und dieser Text eignet sich gut für jemanden, der auf einem Bildschirm liest. Sobald eine Antwort dazu dient, einen weiteren Prompt zu versorgen, in einer Datenbank abgelegt zu werden oder eine UI-Komponente anzusteuern, benötigt man etwas Vorhersehbareres: einen sauberen String, ein JSON-Objekt oder ein typisiertes Datensatz, der bereits überprüft wurde. LangChain schließt diese Lücke mit Ausgabeparseuren, und es werden mehrere davon mit sehr unterschiedlichen Garantien mitgeliefert. In dieser Anleitung wird dasselbe kleine Sonnensystem-Projekt mit vier Parseuren erstellt, jede Zeile jeder Kette erläutert und am Ende eine praktische Regel zur Auswahl zwischen ihnen gegeben.

Für eine umfassendere Übersicht darüber, wie Parser neben LCEL, Ausführbaren und Speicher funktionieren, lesen Sie „Von Rohtext zu Pipelines in LangChain“. Hier liegt der Fokus enger: Was jeder Parser tatsächlich verspricht und wo diese Versprechen enden.

Warum Rohtext des Modells nicht ausreicht

Fragen Sie ein Modell nach dem Sonnensystem – Sie erhalten möglicherweise etwas in der Art. Der Text liest sich gut, doch ein Programm kann ohne Rückschluss darauf, wo ein Satz endet und der nächste beginnt, keine spezifischen Fakten daraus extrahieren.

The Solar System consists of the Sun and the objects that orbit it.
It contains eight planets along with moons, asteroids, and comets.

Code, der diese Antwort verarbeitet, würde viel lieber benannte Werte erhalten, auf die er direkt zugreifen kann – beispielsweise ein Objekt mit einem Schlüssel pro Fakt:

{
    "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."
}

Ein Ausgabeparser ist der Komponente, die diese beiden Formen miteinander verbindet. Er nimmt alles entgegen, was das Modell erzeugt hat, und gibt einen Wert zurück, den Ihre Anwendung ohne zusätzliche Bearbeitung der Zeichenkette verwenden kann. Parser unterscheiden sich darin, wie weit sie gehen: Einige entpacken lediglich den Text, andere parsen JSON, und die strengsten validieren das Ergebnis anhand eines Schemas.

Raw LLM Response
       ↓
Output Parser
       ↓
Parsed Output

Halten Sie dieses dreistufige Konzept im Hinterkopf. Jedes der untenstehenden Beispiele ist eine Variation davon, wobei im mittleren Bereich je nach steigenden Anforderungen weitere Komponenten hinzugefügt werden.

Vier Parser, vier Ebenen der Struktur

Die hier vorgestellten vier Parser bilden eine Leiter, wobei jede Stufe mehr Kontrolle über das Ergebnis bietet:

  • StrOutputParser wandelt die Nachricht des Modells in einen einfachen Python-String um.
  • JsonOutputParser parsiert die Antwort in einen JSON-kompatiblen Wert wie ein Dictionary oder eine Liste.
  • StructuredOutputParser ermöglicht es Ihnen, die benannten Felder anzugeben, die das Modell zurückgeben soll.
  • PydanticOutputParser beschreibt die erwartete Struktur mit einem Pydantic-Modell und prüft den Ausgangswert dagegen.
  • Egal, welchen Sie wählen, der Datenpfad sieht gleich aus:

    LLM Response
         ↓
    Output Parser
         ↓
    Parsed Output
    

    Was sich ändert, ist das Ergebnis am Ende und inwieweit man diesem vertrauen kann.

    Das laufende Beispiel: Bericht, dann Zusammenfassung

    Das erste Projekt ist ein zweistufiger Prozess. Ein Thema, „Sonnensystem“, wird an ein Modell weitergeleitet, das einen langen Bericht erstellt. Dieser Bericht wird anschließend an eine zweite Anfrage übergeben, die nach einer fünfzeiligen Zusammenfassung verlangt. Der entscheidende Punkt ist die Übertragung: Das Ergebnis der ersten Modellaufruf wird zum Eingang für die nächste Anfrage.

    Solar System
         ↓
    LLM
         ↓
    Detailed Report
         ↓
    LLM
         ↓
    5-Line Summary
    

    Ohne Parser gibt der erste Schritt ein Message-Objekt zurück, nicht den Berichtstext, sodass man dieses Objekt vor dem Erstellen des zweiten Prompts entpacken müsste. Ein Parser übernimmt diese Entpackung als Teil der Verarbeitungskette, wodurch die beiden Schritte sauber miteinander kombiniert werden können.

    StrOutputParser: Wenn nur Text benötigt wird

    StrOutputParser ist der einfachste Parser, den LangChain anbietet. Chat-Modelle liefern ein AIMessage; dieser Parser extrahiert dessen Inhalt und gibt einen gewöhnlichen String zurück. Er eignet sich, wenn der nächste Empfänger ein Mensch, ein weiterer Prompt oder etwas anderes ist, das lediglich Prosa benötigt, und man weder JSON noch ein Schema braucht.

    Einbinden in den Berichtsprozess

    Im Projekt „Report-then-Summary“ sind die Schritte wie folgt:

    1. Der erste Prompt bittet das Modell um einen detaillierten Bericht zum Thema.
  • Der Bericht wird an die zweite Anweisung weitergeleitet.
  • Die zweite Anweisung bittet das Modell, den Bericht auf fünf Zeilen zu kürzen.
  • Ein StrOutputParser befindet sich nach jedem Modellaufruf, sodass jede Weitergabe eine einfache Zeichenkette enthält. Die vollständige Abfolge der Komponenten ist:

    Solar System
         ↓
    Prompt 1
         ↓
    LLM
         ↓
    StrOutputParser
         ↓
    Detailed Report
         ↓
    Prompt 2
         ↓
    LLM
         ↓
    StrOutputParser
         ↓
    5-Line Summary
    

    Die OpenAI-Version

    Hier ist die vollständige Kette unter Verwendung von ChatOpenAI. Lesen Sie sie einmal von oben nach unten, danach gehen wir auf die wichtigen Teile ein.

    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)
    

    Das Modell wird mit Standardeinstellungen erstellt. load_dotenv() lädt darüber hinaus den API-Schlüssel aus einer lokalen .env-Datei, sodass keine Anmeldeinformationen im Skript gespeichert sind.

    model = ChatOpenAI()
    

    Das erste Template nimmt eine einzige Variable, topic, entgegen und bittet um einen detaillierten Bericht dazu.

    template1 = PromptTemplate(
        template='Write a detailed report on {topic}',
        input_variables=['topic']
    )
    

    Das zweite Template erwartet eine Variable mit dem Namen text, die den Bericht enthält, und bittet um eine fünfzeilige Zusammenfassung desselben.

    template2 = PromptTemplate(
        template='Write a 5 line summary on the following text. /n {text}',
        input_variables=['text']
    )
    

    Ein kleiner Fehler sollte behoben werden, wenn man dies kopiert: Der Template-String enthält /n, was ein literales Schrägstrich gefolgt von dem Buchstaben n ist und kein Zeilenumbruch darstellt. Verwenden Sie \n, wenn der Bericht auf einer neuen Zeile beginnen soll. Modelle funktionieren in der Regel in beiden Fällen, aber die Anweisung, die Sie senden zu wollen, sollte auch die tatsächlich gesendete Anweisung sein.

    Danach kommt der Parser. Eine einzige Instanz kann an mehreren Stellen in der Kette wiederverwendet werden, da sie zwischen den Aufrufen keinen Zustand speichert.

    parser = StrOutputParser()
    

    Die Zeile, die alles miteinander verbindet, ist die Definition der Kette:

    chain = template1 | model | parser | template2 | model | parser
    

    Der Pipe-Operator ist eine Komposition in der LCEL (LangChain Expression Language): Die Ausgabe jedes Komponenten wird zur Eingabe des nächsten Komponenten. In vertikaler Anordnung ist die Ausführungsreihenfolge wie folgt:

    template1
        ↓
    model
        ↓
    parser
        ↓
    template2
        ↓
    model
        ↓
    parser
    

    Achten Sie darauf, was der erste Parser für Sie leistet. Nach dem ersten Modellaufruf gibt der Parser den Bericht als String zurück, und dieser String füllt {text} im zweiten Template. Der zweite Parser erledigt dieselbe Aufgabe für die endgültige Antwort, sodass das Ergebnis der Kette selbst die Zusammenfassung ist und kein Nachrichtenobjekt.

    Man startet die Kette, indem man ein Dictionary übergeben wird, dessen Schlüssel den Eingabevariablen des ersten Templates entsprechen:

    result = chain.invoke({'topic': 'Solar System'})
    

    Dann wird das Ergebnis angezeigt:

    print(result)
    

    Was die Kette zurückgibt

    Der am Ende ausgegebene Wert ist die fünfzeilige Zusammenfassung, die aus dem erzeugten Bericht abgeleitet wird. Da das letzte Komponente ein StrOutputParser ist, ergibt sich das Ergebnis als normale Python-str-Variable:

    print(result)
    

    Eine typische Ausführung liefert etwas in dieser Art:

    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.
    

    Betrachten Sie dies als Veranschaulichung der Struktur, nicht als festes Ergebnis. Die Formulierungen unterscheiden sich je nach Ausführung und Modell.

    Derselbe Workflow mit einem Hugging Face-Modell

    Auch gegenüber einem öffentlichen Modell kann der Bericht-dann-Zusammenfassung-Workflow verwendet werden. Diese Variante nutzt HuggingFaceEndpoint, der auf google/gemma-2-2b-it verweist, und umhüllt ihn mit ChatHuggingFace, sodass er sich wie ein Chat-Modell verhält:

    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)
    

    Es gibt hier einen wichtigen Unterschied. Diese Version verwendet niemals StrOutputParser und erstellt auch keine Piping-Kette. Jede Anfrage wird manuell mit .invoke() formatiert, das Modell aufgerufen und anschließend .content aus der zurückgegebenen Nachricht ausgelesen, bevor diese weitergeleitet wird. Das funktioniert zwar, ist aber genau das manuelle Entpacken, wofür der Parser vorhanden ist.

    Nebeneinander sehen die beiden Ansätze so aus: Mit OpenAI und einem Parser:

    OpenAI
    
    Prompt
      ↓
    ChatOpenAI
      ↓
    StrOutputParser
      ↓
    String
    

    Mit Hugging Face und manuellem Zugriff:

    Hugging Face
    
    Prompt
      ↓
    ChatHuggingFace
      ↓
    result.content
      ↓
    String
    

    Sowohl Fälle enden mit einem String. Das OpenAI-Skript zeigt, wie der Parser diese Aufgabe innerhalb einer Kette ausführt, während das Hugging Face-Skript dieselbe Anwendungslogik mit einem anderen Anbieter und ohne Parser zeigt. Es gibt nichts, was Sie daran hindert, auch mit dem Hugging Face-Modell template1 | model | parser | template2 | model | parser zu schreiben; dem Parser ist es egal, welcher Anbieter die Nachricht erzeugt hat.

    Die Erkenntnis für diesen Schritt: Wählen Sie StrOutputParser, sobald die Anwendung nur die Antwort als Text benötigt.

    JsonOutputParser: JSON ohne Vertragsstruktur

    JsonOutputParser ist der nächste Schritt. Er bittet das Modell um JSON und wandelt die Antwort in Python-Daten um, was nützlich ist, wenn Ihr Code Werte anhand von Schlüsseln auswählen muss anstelle von Prosa zu lesen.

    Es zwingt nicht zu einer bestimmten Struktur. Ohne Schema weist es das Modell an, in JSON zu antworten, sagt aber nichts darüber, welche Schlüssel vorhanden sein müssen oder welche Datentypen ihre Werte haben sollten. Zwei Ausführungen mit derselben Anfrage können legitim Objekte unterschiedlicher Struktur zurückgeben, und Ihr Code muss darauf vorbereitet sein.

    Wie die Komponenten zusammenpassen

    In diesem Projekt wird das Modell um fünf Fakten über das Sonnensystem gebeten. Die Schritte sind:

    1. Einen JsonOutputParser erstellen.
    2. Mit get_format_instructions() Anweisungen zur Formatierung anfordern.
    3. Diese Anweisungen in die Anfrage einfügen.
    4. Die Anfrage an das Modell senden.
    5. Den Parser verwenden, um die Antwort in einen Python-Wert umzuwandeln.
    Solar System
         ↓
    PromptTemplate
         ↓
    Format Instructions
         ↓
    LLM
         ↓
    JsonOutputParser
         ↓
    JSON Object
    

    Die OpenAI-Version

    Die gesamte Kette ist kurz:

    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)
    

    Das Modell ist mit gpt-4.1-mini und einer Temperatur von 0 konfiguriert, wodurch die Ausgabe so reproduzierbar wie möglich ist. Es handelt sich dabei um den Komponenten, der die fünf Fakten schreibt.

    model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    

    Der Parser wird ohne Argumente erstellt, und genau deshalb verfügt er über kein Schema zur Durchsetzung von Regeln:

    parser = JsonOutputParser()
    

    Formatanweisungen erledigen die eigentliche Arbeit

    Vor dem Ausführen des Modells kann der Parser das von ihm erwartete Format beschreiben. Diese Beschreibung stammt aus einem Methodenaufruf:

    parser.get_format_instructions()
    

    Er gibt einen Textblock zurück, der dem Modell mitteilt, dass es in JSON antworten soll. Es handelt sich dabei nicht um Code, der gegen das Modell ausgeführt wird; es ist Prompt-Text. Diesen injiziert man in das Template über partial_variables, wodurch eine Template-Variable nur einmal, beim Definieren des Templates und nicht bei jedem Aufruf, gefüllt wird:

    partial_variables={
        "format_instruction": parser.get_format_instructions()
    }
    

    Das resultierende Template enthält zwei Platzhalter:

    template = PromptTemplate(
        template="Give me 5 facts about {topic} \n {format_instruction}",
        input_variables=["topic"],
        partial_variables={
            "format_instruction": parser.get_format_instructions()
        },
    )
    
    • {topic} wird zum Aufrufzeitpunkt bereitgestellt und gibt an, welche Informationen gewünscht sind.
    • {format_instruction} wird bereits mit den Formatierungshinweisen des Parsers vorgefüllt.

    Sobald die Kette mit diesen Eingaben aufgerufen wird, erhält das Modell sowohl das Thema als auch die Anweisung zur Beantwortung im JSON-Format:

    {"topic": "Solar System"}
    

    Zusammenstellen und Ausführen der Kette

    Die Kette selbst besteht aus nur drei Schritten:

    chain = template | model | parser
    

    In der Ausführungsreihenfolge:

    PromptTemplate
          ↓
    ChatOpenAI
          ↓
    JsonOutputParser
          ↓
    Parsed JSON
    

    Das Template generiert die endgültige Anfrage, ChatOpenAI beantwortet sie, und JsonOutputParser wandelt die Antwort in Python-Daten um. Der Parser ist außerdem tolerant gegenüber einigen gängigen Verhaltensweisen von Modellen, wie zum Beispiel dem Einbetten des JSON in einen Markdown-Codeblock, welches er vor der Analyse entfernt.

    Rufen Sie es mit dem Thema auf:

    result = chain.invoke({"topic": "Solar System"})
    

    Und geben Sie das Zurückgegebene aus:

    print(result)
    

    Was wird zurückgegeben?

    Das Ergebnis enthält fünf Fakten in JSON-Format. Die Skript gibt nur den Wert aus, sodass es kein standardisiertes Ausgabeformat gibt; eine plausible Antwort sieht so aus:

    {
      "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."
      ]
    }
    

    Diese Struktur – ein einziger Schlüssel facts, der eine Liste enthält – ist eine von mehreren möglichen Formaten, die das Modell wählen kann. Ein weiterer Ausführungsvorgang könnte fact_1 bis fact_5 oder eine reine Liste zurückgeben. Wenn späterer Code auf result[„facts“] zugreift, funktioniert das nicht mehr, sobald das Modell ein anderes Format wählt.

    Ablaufverfolgung

    Verglichen mit StrOutputParser besteht der neue Unterschied darin, dass der Parser zweimal beteiligt ist: einmal vor dem Aufruf des Modells, indem er Anweisungen liefert, und einmal danach, indem er die Ausgabe analysiert.

    Prompt
      ↓
    JSON Format Instructions
      ↓
    LLM
      ↓
    JsonOutputParser
      ↓
    JSON
    

    Die Anweisungen werden vom Parser selbst erzeugt:

    parser.get_format_instructions()
    

    Sie gelangen über die Teilvariable zum Prompt:

    partial_variables={
        "format_instruction": parser.get_format_instructions()
    }
    

    Das Modell antwortet unter Berücksichtigung dieser Anweisungen, und der Parser wandelt die Antwort in einen Python-Wert um. Von Anfang bis Ende:

    Solar System
         ↓
    PromptTemplate
         ↓
    JSON Format Instructions
         ↓
    ChatOpenAI
         ↓
    JsonOutputParser
         ↓
    JSON Object
    

    Der Kontrast zum vorherigen Schritt passt in zwei Zeilen. StrOutputParser erzeugt:

    StrOutputParser
        ↓
    Plain String
    

    während JsonOutputParser erzeugt:

    JsonOutputParser
        ↓
    JSON-compatible Structured Data
    

    Bedenken Sie: Es gibt immer noch kein festes Schema. Man erhält JSON, doch die Felder und die Verzweigung hängen vom Modell ab. Nebenbei bemerkt akzeptieren aktuelle Versionen von JsonOutputParser auch ein optionales Argument pydantic_object, das ein Schema zu den Formatanweisungen hinzufügt, doch in der hier gezeigten Form ohne Argumente wird lediglich gültiges JSON gefordert.

    Hugging Face-Variante

    Der JSON-Ablauf wird direkt auf das Gemma-Modell übertragen. Die Modellkonfiguration ändert sich; der Parser, die Formatanweisungen sowie die Verarbeitungskette bleiben unverändert:

    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)
    

    Der Pipeline ist abgesehen von dem Modulfeld identisch:

    PromptTemplate
          ↓
    Hugging Face Model
          ↓
    JsonOutputParser
          ↓
    JSON Object
    

    Der praktische Vorteil daran, das Parsen in eine eigene Komponente zu legen, besteht darin, dass der Austausch der Anbieter die Parselogik nicht beeinträchtigt. Beachten Sie jedoch, dass kleine, offene Modelle die Formatanweisungen weniger zuverlässig befolgen als größere, gehostete Modelle. Wenn die Antwort Prosa um das JSON enthält oder ein zusätzliches Komma am Ende steht, wirft der Parser eine OutputParserException aus – daher sollte Code in der Produktion diese Ausnahme fangen und entweder erneut versuchen oder auf eine Alternative zurückgreifen.

    StructuredOutputParser: Benennung der erwarteten Felder

    StructuredOutputParser extrahiert JSON auf der Grundlage einer von Ihnen im Voraus definierten Liste von Feldern. Während JsonOutputParser lediglich „Antwort in JSON“ vorschreibt, besagt dieser Parser „Antwort in JSON mit diesen Schlüsseln“.

    Felder werden mithilfe von ResponseSchema deklariert. Jedes Feld verfügt über einen name und eine description; die Beschreibung gibt dem Modell an, was in diesem Feld enthalten sein soll. Dadurch lässt sich die Struktur der Antwort deutlich besser steuern.

    The three-fact project

    In diesem Projekt werden drei Fakten über das Sonnensystem angefordert, wobei pro Fakt ein Feld vorhanden ist:

    • fact_1 enthält den ersten Fakt über das Thema.
    • fact_2 enthält den zweiten.
    • fact_3 enthält den dritten.

    Der Parser wandelt diese Deklarationen in Anweisungen um und prüft anschließend die erhaltene Antwort daran.

    Solar System
         ↓
    PromptTemplate
         ↓
    Predefined Field Schema
         ↓
        LLM
         ↓
    StructuredOutputParser
         ↓
    Structured JSON
    

    Die OpenAI-Version

    Hier ist der vollständige Script:

    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)
    

    Das Modell verwendet weiterhin die gleiche gpt-4.1-mini-Konfiguration wie zuvor:

    model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    

    Der eigentliche Unterschied beginnt mit der Liste der Schemata:

    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"),
    ]
    

    Jedes ResponseSchema liefert zwei Elemente:

    • name wird zum Schlüssel im resultierenden Dictionary.
    • description gibt dem Modell an, was in diesem Schlüssel enthalten sein soll.

    Drei Schemata ergeben drei erforderliche Schlüssel: fact_1, fact_2 und fact_3.

    Man instanziert diesen Parser nicht direkt. Eine Klassenmethode erstellt ihn aus der Liste der Schemata:

    parser = StructuredOutputParser.from_response_schemas(schema)
    

    Ebenso wie beim JSON-Parser stammen die Formatanweisungen vom Parser selbst:

    parser.get_format_instructions()
    

    Dieses Mal sind die Anweisungen umfangreicher. Sie enthalten ein JSON-Skelett, das jeden Feldnamen zusammen mit seiner Beschreibung auflistet, und fordern das Modell auf, die Antwort in einen eingerahmten JSON-Block zu packen. Sie sind auf dieselbe Weise verbunden:

    partial_variables={
        "format_instruction": parser.get_format_instructions()
    }
    

    Das Prompt-Muster weist die vertrauten beiden Platzhalter auf:

    template = PromptTemplate(
        template="Give 3 fact about {topic} \n {format_instruction}",
        input_variables=["topic"],
        partial_variables={
            "format_instruction": parser.get_format_instructions()
        },
    )
    

    {topic} wird zum Zeitpunkt der Aufrufung ausgefüllt, und {format_instruction} enthält die aus Ihren Schemata generierte Feldliste. Durch Ausführung mit diesen Eingaben werden beide an das Modell gesendet:

    {"topic": "Solar System"}
    

    Ausführung der Kette

    Die Kette weist wie zuvor die gleichen drei Phasen auf:

    chain = template | model | parser
    

    Mit dem Parser in der letzten Position:

    PromptTemplate
          ↓
    ChatOpenAI
          ↓
    StructuredOutputParser
          ↓
    Structured JSON
    

    Das Muster erstellt den Prompt, das Modell gibt eine Antwort, und StructuredOutputParser extrahiert die deklarierten Felder aus der Antwort.

    result = chain.invoke({"topic": "Solar System"})
    
    print(result)
    

    Wie das Ausgabeergebnis aussieht

    Das Ergebnis ist ein Dictionary, das drei Werte unter den von Ihnen definierten Schlüsseln enthält. Das Skript gibt es aus:

    print(result)
    

    und ein repräsentatives Ergebnis ist:

    {
        "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."
    }
    

    Die konkreten Werte können variieren. Was jedoch nicht variieren darf, ist die Menge der Schlüssel:

    fact_1
    fact_2
    fact_3
    

    Das ist der Vorteil gegenüber dem einfachen JSON-Parsing: Ihre Anwendung bestimmt die Feldnamen, nicht das Modell. Wenn das Modell einen der deklarierten Schlüssel weglässt, wirft der Parser einen Fehler aus, anstatt stillschweigend eine andere Struktur zurückzugeben – was weitaus einfacher zu handhaben ist als ein KeyError drei Funktionen später.

    Ablaufverfolgung

    Der vollständige Pfad, vom Thema bis zum resultierenden Dictionary:

    Solar System
         ↓
    PromptTemplate
         ↓
    ResponseSchema
         ↓
    Format Instructions
         ↓
    ChatOpenAI
         ↓
    StructuredOutputParser
         ↓
    {
        fact_1: ...,
        fact_2: ...,
        fact_3: ...
    }
    

    Es beginnt mit den Felddefinitionen:

    ResponseSchema(
        name="fact_1",
        description="Fact 1 about the topic"
    )
    

    Der Parser wandelt diese in Formatanweisungen um, die Anweisungen werden in den Prompt eingefügt, das Modell antwortet und der Parser extrahiert die deklarierten Felder. Im Vergleich zum vorherigen Schritt:

    JsonOutputParser
           ↓
    JSON output
           ↓
    Structure can vary
    
    StructuredOutputParser
           ↓
    Predefined fields
           ↓
    More controlled structure
    

    Kurz gesagt: JsonOutputParser dient dazu, überhaupt JSON zu erhalten, während StructuredOutputParser darauf abzielt, JSON mit den von Ihnen angeforderten Schlüsseln zu erhalten.

    Es gibt eine Grenze, die klar benannt werden sollte. ResponseSchema verfügt über ein type-Attribut, das standardmäßig auf string gesetzt ist, doch es ändert lediglich die Formulierung der Anweisungen. Der Parser überprüft, ob die Schlüssel vorhanden sind; er validiert jedoch weder Werttypen noch -bereiche. Wenn age beispielsweise ein Integer über einem bestimmten Schwellenwert sein muss, wird dies von diesem Parser nicht durchgesetzt.

    Prüfen Sie daher den Importpfad im Einklang mit der von Ihnen genutzten LangChain-Version. Das Beispiel importiert aus langchain.output_parsers, und in neueren Versionen wurde dieser veraltete Parser aus den Kernpaketen entfernt, sodass der Import möglicherweise geändert werden muss.

    Hugging Face-Variante

    Die Gemma-Version verwendet weiterhin dieselben drei Schemata sowie dieselbe Parser-Konstruktion:

    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)
    

    Und denselben Pipeline:

    PromptTemplate
          ↓
    ChatHuggingFace
          ↓
    StructuredOutputParser
          ↓
    Structured JSON
    

    Nur der Modellanbieter unterscheidet sich. Das Schema und der Parser bleiben unverändert.

    PydanticOutputParser: Struktur plus Validierung

    PydanticOutputParser ist die strengste der vier Varianten. Er beschreibt die erwartete Antwort mit einem Pydantic-Modell, wodurch die Definition der Ausgabe gleichzeitig auch die Definition dessen ist, was als gültig gilt.

    Das geht über das einfache Parsen hinaus. Die Felder tragen echte Python-Typen, und Field() kann Einschränkungen hinzufügen – beispielsweise, dass eine Ganzzahl einen Mindestwert überschreiten muss. Wenn die Antwort des Modells diesen Anforderungen nicht entspricht, erhält man eine Ausnahme anstelle von fehlerhaften Daten.

    Warum sich die zusätzliche Einrichtung lohnt

    • Schema-Überwachung: Die Antwort muss einer gut definierten Struktur entsprechen.
    • Typsicherheit: Die Felder verwenden Python-Typen wie str, int und float, wobei die Werte entsprechend umgewandelt oder abgelehnt werden.
    • Validierung: Pydantic überprüft jede von Ihnen deklarierte Einschränkung.
    • Integration in Ketten: Es kann genauso wie die anderen Parser in Prompts, Modelle und LCEL-Ketten eingebunden werden.

    Das Projekt zu fiktiven Personen

    In diesem Beispiel wird das Modell aufgefordert, eine Person aus einem bestimmten Ort – hier „Indien“ – mit drei Feldern zu erfinden:

    • name, der Name der Person.
    • age, das Alter der Person.
    • city, die Stadt, in der sie lebt.

    Auch das Feld age unterliegt einer Einschränkung: Es muss größer als 18 sein.

    Input
      ↓
    PromptTemplate
      ↓
    Pydantic Model
      ↓
    Format Instructions
      ↓
    LLM
      ↓
    PydanticOutputParser
      ↓
    Validated Pydantic Object
    

    Die OpenAI-Version

    Der vollständige Script:

    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)
    

    Es folgt dem gleichen Aufbau wie zuvor: Es wird ein Person-Modell definiert, dieses an PydanticOutputParser übergeben und der Parser mit einer Anfrage sowie einem Modell verbunden.

    Die Konfiguration des Modells bleibt unverändert:

    model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    

    Der Kernbestandteil ist die Pydantic-Klasse:

    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")
    

    Dies ist der Vertrag für die Antwort:

    • name muss eine Zeichenkette sein.
  • age muss eine ganze Zahl sein, die streng größer als 18 ist.
  • city muss ein String sein.
  • Field() fügt sowohl eine für Menschen lesbare Beschreibung hinzu, die im Prompt erscheint, als auch Einschränkungen, die nach der Parsing-Operation überprüft werden. Das eingeschränkte Feld an sich:

    age: int = Field(gt=18, description="Age of the person")
    

    gt=18 bedeutet „größer als 18“, wodurch ein Alter von genau 18 die Validierung nicht bestehen kann. Wenn Sie „18 oder älter“ meinen, verwenden Sie stattdessen ge=18.

    Der Parser wird durch Übergeben der Klasse und nicht einer Instanz erstellt:

    parser = PydanticOutputParser(pydantic_object=Person)
    

    Dadurch wird angegeben, welches Modell sowohl zur Erstellung von Anweisungen als auch zur Überprüfung der Antwort verwendet werden soll.

    Die Formatierungsanweisungen stammen von derselben Methode wie zuvor:

    parser.get_format_instructions()
    

    Für diesen Parser enthalten sie ein aus dem Pydantic-Modell generiertes JSON Schema, das Feldbeschreibungen sowie die exclusiveMinimum-Beschränkung für das Feld age enthält. Sie werden wie üblich über die partial-Variable eingefügt:

    partial_variables={
        "format_instruction": parser.get_format_instructions()
    }
    

    Das Prompt-Muster:

    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} bestimmt, welche Art von Person das Modell erfinden soll. Bei Verwendung dieses Eingabewerts werden Name, Alter und Stadt einer fiktiven indischen Person abgefragt:

    {"place": "Indian"}
    

    Ausführung der Kette

    Die Kette behält ihre bekannte dreistufige Struktur bei:

    chain = template | model | parser
    

    Dieses Mal gibt die letzte Stufe eine Instanz des Modells zurück:

    PromptTemplate
          ↓
    ChatOpenAI
          ↓
    PydanticOutputParser
          ↓
    Pydantic Object
    

    Das Muster erstellt den Prompt, das Modell gibt eine Antwort, und PydanticOutputParser parsiert das JSON und validiert es zu einer Person.

    final_result = chain.invoke({"place": "Indian"})
    
    print(final_result)
    

    Wie das Ausgabeergebnis aussieht

    Das Ergebnis ist ein Person-Objekt, kein Dictionary. Bei der Ausgabe wird die Standarddarstellung von Pydantic angezeigt:

    name='Rahul Sharma' age=28 city='Mumbai'
    

    Die Werte unterscheiden sich von Ausführung zu Ausführung. Die Garantien hingegen nicht:

    name → string
    age  → integer (> 18)
    city → string
    

    Genau hier unterscheiden sich die Ansätze am deutlichsten. JsonOutputParser verlangte lediglich JSON, StructuredOutputParser gab den Feldnamen an, und PydanticOutputParser repräsentiert den gesamten Vertrag als echte Klasse. Mit der Autocompletion des Editors können Sie auf final_result.age zugreifen und sicher sein, dass es sich um eine int-Wert größer als 18 handelt, denn alles Andere hätte bereits vor Erreichen Ihres Codes einen Validierungsfehler ausgelöst.

    Ablaufverfolgung

    Von der Eingabe zum validierten Objekt:

    "Indian"
        ↓
    PromptTemplate
        ↓
    Pydantic Model
        ↓
    Format Instructions
        ↓
    ChatOpenAI
        ↓
    PydanticOutputParser
        ↓
    Person Object
    

    Es beginnt mit der Struktur, die hier ohne Beschreibungen und Einschränkungen zur besseren Lesbarkeit dargestellt wird:

    class Person(BaseModel):
        name: str
        age: int
        city: str
    

    Die Klasse wird an den Parser übergeben:

    PydanticOutputParser(pydantic_object=Person)
    

    Der Parser erzeugt Anweisungen aus dem Modell; diese Anweisungen werden in die Aufforderung aufgenommen, das Modell antwortet und der Parser analysiert die Antwort in ein Person-Objekt um und führt anschließend eine Pydantic-Validierung durch. Konzeptionell gesehen:

    Pydantic Model
          ↓
    Defines Structure + Types + Constraints
          ↓
    LLM Response
          ↓
    PydanticOutputParser
          ↓
    Validated Pydantic Object
    

    Am Ende erhält man ein Python-Objekt, dessen Daten gemäß Ihren Regeln strukturiert sind – was mehr ist, als jedes JSON-Objekt bieten kann.

    Eine praktische Folge: Ein Validierungsfehler tritt als OutputParserException in der Kette auf. Man muss entscheiden, was dann geschehen soll. Häufige Optionen sind das Erneuten der Aufruf, das Zurückgeben des Fehlers an das Modell mithilfe von LangChain’s OutputFixingParser oder das Protokollieren und Zurückgeben eines sicheren Standardwerts.

    Hugging Face-Variante

    Die Gemma-Version definiert dasselbe Person-Modell und gibt es an denselben Parser weiter:

    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)
    

    Der Pipeline bleibt unverändert:

    PromptTemplate
          ↓
    ChatHuggingFace
          ↓
    PydanticOutputParser
          ↓
    Pydantic Object
    

    Nur der Anbieter ändert sich; das Pydantic-Modell sowie der Parser werden gemeinsam genutzt. Kleinere Modelle neigen eher dazu, Einschränkungen zu verletzen oder unerwünschten Text hinzuzufügen – genau in solchen Fällen ist Validierung von Nutzen: Die fehlerhafte Antwort wird bereits an der Grenze erkannt, anstatt in Ihre Daten einzudringen.

    Die richtige Wahl des Parsers

    Die Entscheidung hängt davon ab, wie viel Struktur und wie viel Validierung der Empfänger der Antwort tatsächlich benötigt. Hier ist eine Zusammenfassung jeder Option.

    StrOutputParser

    Verwenden Sie ihn, wenn die Antwort des Modells einfach nur Text ist.

    • Am besten geeignet für: Berichte, Erklärungen, Zusammenfassungen, Chat-Antworten.
    • Gibt zurück: einen String.
    • JSON-Parsing: nicht möglich.
    • Schema: nein.
    • Validierung: nein.

    JsonOutputParser

    Verwenden Sie ihn, wenn Sie JSON benötigen, aber eine variable Struktur in Ordnung finden oder bewältigen können.

    • Am besten geeignet für: explorative, strukturierte Ausgaben sowie flexible Datenmengen.
    • Gibt zurück: ein Dictionary oder eine Liste.
    • JSON-Parsing: ja.
    • Schema: nein (in der hier verwendeten Form ohne Argumente).
    • Validierung: nur „ist gültiges JSON“.

    StructuredOutputParser

    Verwenden Sie ihn, wenn Ihr Code bestimmte Schlüssel erwartet, wie z. B. fact_1 bis fact_3.

    • Am besten geeignet für: einfache Datensätze mit bekannten Feldnamen.
    • Gibt zurück: ein Dictionary mit den deklarierten Schlüsseln.
    • JSON-Parsing: ja.
    • Schema: ja, mit Feldnamen und Beschreibungen.
    • Validierung: nur Prüfung auf Vorhandensein der Schlüssel, keine Typüberprüfungen.

    PydanticOutputParser

    Verwenden Sie es, wenn die Ausgabe direkt in die Anwendungslogik eingespeist wird und korrekt sein muss.

    • Am besten geeignet für: Daten, die Sie speichern, verarbeiten oder an APIs übergeben.
    • Gibt zurück: eine Instanz Ihres Pydantic-Modells.
    • JSON-Parsing: ja.
    • Schema: ja, vollständige Typen und Einschränkungen.
    • Validierung: ja.

    Ein schnelles mentales Modell

    • StrOutputParser: Text reicht aus.
    • JsonOutputParser: Jedes gültige JSON ist geeignet.
    • StructuredOutputParser: Das JSON muss diese Schlüssel enthalten.
    • PydanticOutputParser: Diese Schlüssel, diese Typen sowie alle Einschränkungen werden überprüft.

    Wählen Sie den einfachsten Parser, der die von Ihnen benötigten Garantien bietet. Jeder zusätzliche Schritt fügt weitere Token für Anweisungen hinzu sowie neue Möglichkeiten, eine Antwort abzulehnen – daher sollte eine höhere Strenge bewusst gewählt werden.

    Noch eine Option sollte berücksichtigt werden. Alle vier Parser arbeiten, indem sie im Prompt ein Format beschreiben und anschließend den Text analysieren. Viele Chatmodelle unterstützen außerdem eine native strukturierte Ausgabe oder den Aufruf von Tools, was LangChain über with_structured_output() am Modell bereitstellt. Wenn Ihr Anbieter dies unterstützt, ist dieser Ansatz in der Regel zuverlässiger für datenstrukturierte Informationen, während promptbasierte Parser weiterhin nützlich sind für Anbieter und Modelle, die diese Funktion nicht bieten.

    Kernpunkte

    • Ausgabeparser wandeln die Nachricht eines Modells in einen Wert um, den Ihr Code verwenden kann, und werden mithilfe des Pipe-Operators in LCEL-Ketten eingefügt.
    • StrOutputParser entfernt die Nachrichtenverpackung, sodass der Text eines Modells als Eingabe für den nächsten Prompt dienen kann.
    • JsonOutputParser analysiert JSON, passt dessen Struktur jedoch nicht an, es sei denn, Sie geben ihm ein Schema vor.
  • StructuredOutputParser korrigiert die Schlüsselnamen über ResponseSchema, lässt jedoch die Werttypen unüberprüft.
  • PydanticOutputParser kombiniert die Parsing-Aufgabe mit Typüberprüfungen und -beschränkungen und gibt ein echtes Objekt zurück.
  • Formatanweisungen sind lediglich Prompt-Texte: Das Modell kann sie dennoch ignorieren, weshalb man mit Parsing- und Validierungsfehlern rechnen sollte, insbesondere bei kleineren offenen Modellen.
  • Durch den Wechsel des Anbieters bleibt der Parser unverändert, was es erleichtert, gehostete und offene Modelle für dieselbe Aufgabe miteinander zu vergleichen.
  • Sobald die Antworten in einer zuverlässigen Form vorliegen, ist der natürliche nächste Schritt die Zusammenstellung mehrerer Prompts, Modelle und Parser zu größeren Workflows – einschließlich sequenzieller, paralleler sowie bedingter Ketten, die mit RunnableParallel und RunnableBranch erstellt werden. Zu diesem Aspekt siehe die Erstellung von LangChain-Pipelines mit LCEL.