De texte brut à objets validés : choisir un analyseur de sortie LangChain
Comparez StrOutputParser, JsonOutputParser, StructuredOutputParser et PydanticOutputParser dans les chaînes LangChain, et découvrez quel niveau de structure chacun garantit réellement.
Si vous souhaitez une vue d’ensemble plus large sur la manière dont les analyseurs coexistent avec LCEL, les exécutables et la mémoire, consultez de texte brut en pipelines dans LangChain. Ici, l’attention est plus ciblée : ce que chaque analyseur promet réellement, et où s’arrête cette promesse.
Pourquoi le texte brut du modèle ne suffit pas
Si vous posez une question à un modèle sur le système solaire, vous pourriez obtenir quelque chose comme ci-dessous. Cela se lit bien, mais un programme ne peut en extraire aucun fait spécifique sans deviner où s’achève une phrase et où commence la suivante.
The Solar System consists of the Sun and the objects that orbit it.
It contains eight planets along with moons, asteroids, and comets.
Un code qui consomme cette réponse préférerait de loin recevoir des valeurs nommées auxquelles il peut se référer directement, par exemple un objet contenant une clé pour chaque fait :
{
"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."
}
Raw LLM Response
↓
Output Parser
↓
Parsed Output
Quatre analyseurs, quatre niveaux de structure
StrOutputParsertransforme le message du modèle en une chaîne de caractères Python standard.JsonOutputParseranalyse la réponse pour en obtenir une valeur compatible JSON, telle qu’un dictionnaire ou une liste.
StructuredOutputParser vous permet de déclarer les champs nommés que le modèle doit retourner.PydanticOutputParser décrit la structure attendue à l’aide d’un modèle Pydantic et valide la sortie par rapport à celle-ci.Quel que soit le choix, le chemin des données reste identique :
LLM Response
↓
Output Parser
↓
Parsed Output
Ce qui change, c’est ce qui est produit à la fin et dans quelle mesure on peut lui faire confiance.
Exemple en cours : rapport, puis résumé
Le premier projet est un pipeline en deux étapes. Un sujet, « Système solaire », est transmis à un modèle qui rédige un long rapport. Ce rapport est ensuite utilisé comme entrée pour une deuxième demande visant à obtenir un résumé de cinq lignes. Le point clé réside dans le transfert : la sortie de la première appel du modèle devient l’entrée de la demande suivante.
Solar System
↓
LLM
↓
Detailed Report
↓
LLM
↓
5-Line Summary
En l’absence de parseur, la première étape renvoie un objet message plutôt que le texte du rapport, il faudrait donc le déballer avant de construire la deuxième demande. Un parseur intermédiaire effectue ce déballage dans le cadre de la chaîne, ce qui permet aux deux étapes de s’assembler proprement.
StrOutputParser : lorsque seul le texte est nécessaire
StrOutputParser est le parseur le plus simple proposé par LangChain. Les modèles de chat renvoient un AIMessage ; ce parseur en extrait le contenu pour vous fournir une chaîne de caractères ordinaire. C’est l’outil idéal lorsque le destinataire suivant est un humain, une autre demande ou tout autre élément qui ne nécessite que du texte narratif, sans besoin de JSON ni de schéma.
L’intégration dans le pipeline de rapport
Dans le projet report-then-summary, les étapes sont les suivantes :
- La première demande demande au modèle de produire un rapport détaillé sur le sujet.
Un StrOutputParser est placé après chaque appel au modèle, de sorte que chaque transmission contient une chaîne de caractères simple. La séquence complète des composants est :
Solar System
↓
Prompt 1
↓
LLM
↓
StrOutputParser
↓
Detailed Report
↓
Prompt 2
↓
LLM
↓
StrOutputParser
↓
5-Line Summary
La version OpenAI
Voici la chaîne complète utilisant ChatOpenAI. Lisez-la de haut en bas une fois, puis nous examinerons les parties importantes.
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)
Le modèle est créé avec des paramètres par défaut. La fonction load_dotenv() située au-dessus récupère la clé API depuis un fichier local .env, de sorte que aucune identifiante n’est stockée dans le script.
model = ChatOpenAI()
Le premier modèle de template prend une seule variable, topic, et demande un rapport détaillé à son sujet.
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
Le deuxième modèle attend une variable nommée text, qui recevra le rapport, et demande un résumé de cinq lignes à son sujet.
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
Il y a une petite erreur à corriger si vous copiez ce code : la chaîne de modèle contient /n, qui représente une barre oblique littérale suivie de la lettre n, et non un saut de ligne. Utilisez \n si vous souhaitez que le rapport commence sur une nouvelle ligne. Les modèles fonctionnent généralement dans les deux cas, mais le message que vous pensez envoyer doit être exactement celui que vous envoyez réellement.
Vient ensuite l’analyseur. Une seule instance peut être réutilisée à plusieurs endroits de la chaîne, car elle ne conserve aucune information entre les appels.
parser = StrOutputParser()
La ligne qui relie tout est la définition de la chaîne :
chain = template1 | model | parser | template2 | model | parser
L’opérateur pipe correspond à une composition en LCEL (LangChain Expression Language) : la sortie de chaque composant devient l’entrée du suivant. Disposés verticalement, l’ordre d’exécution est le suivant :
template1
↓
model
↓
parser
↓
template2
↓
model
↓
parser
Remarquez ce que vous obtenez avec le premier analyseur. Après la première appel au modèle, l’analyseur renvoie le rapport sous forme de chaîne de caractères, et c’est cette chaîne qui remplit {text} dans le deuxième modèle de template. Le deuxième analyseur effectue la même tâche pour la réponse finale, de sorte que le résultat de la chaîne est le résumé lui-même et non un objet message.
Vous commencez la chaîne en passant un dictionnaire dont les clés correspondent aux variables d’entrée du premier modèle de template :
result = chain.invoke({'topic': 'Solar System'})
Puis vous affichez le résultat :
print(result)
Ce que la chaîne renvoie
La valeur affichée en fin de texte est le résumé en cinq lignes issu du rapport généré. Comme la dernière composante est un StrOutputParser, le résultat est une chaîne de caractères Python standard str:
print(result)
Un exécution typique produit quelque chose de similaire à ceci :
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.
Considérez cela comme une illustration de la structure, et non comme une réponse fixe. La formulation variera d’une exécution à l’autre ainsi que d’un modèle à un autre.
Même flux de travail avec un modèle Hugging Face
Le processus rapport-suivi de résumé peut également être appliqué à un modèle ouvert. Cette variante utilise HuggingFaceEndpoint orienté vers google/gemma-2-2b-it et le met dans un conteneur ChatHuggingFace afin qu’il se comporte comme un modèle de chat :
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)
Il existe une différence importante ici. Cette version n’utilise jamais StrOutputParser et ne crée jamais de chaîne de tubes. Elle formate chaque prompt manuellement à l’aide de .invoke(), appelle le modèle, puis lit .content dans le message retourné avant de le transmettre. Cela fonctionne, mais c’est précisément ce déballage manuel que le parseur est conçu pour éliminer.
Ces deux approches se présentent ainsi côte à côte. Avec OpenAI et un parseur :
OpenAI
Prompt
↓
ChatOpenAI
↓
StrOutputParser
↓
String
Avec Hugging Face et un accès manuel :
Hugging Face
Prompt
↓
ChatHuggingFace
↓
result.content
↓
String
Les deux se terminent par une chaîne de caractères. Le script d’OpenAI montre le parseur effectuant cette tâche au sein d’une chaîne, tandis que le script de Hugging Face présente la même logique d’application avec un fournisseur différent et sans parseur. Rien ne vous empêche d’écrire template1 | model | parser | template2 | model | parser en utilisant également le modèle de Hugging Face ; le parseur ne se soucie pas du fournisseur qui a généré le message.
Le point clé à retenir pour ce niveau : utilisez StrOutputParser chaque fois que l’application a uniquement besoin de la réponse sous forme de texte.
JsonOutputParser : JSON sans contrat
JsonOutputParser représente l’étape suivante. Il demande au modèle de produire du JSON et transforme la réponse en données Python, ce qui est pratique lorsque votre code doit extraire des valeurs par clé plutôt que de lire du texte narratif.
Ce qu’il ne fait pas, c’est imposer une forme particulière. Sans schéma, il indique au modèle de répondre en JSON, mais ne précise pas quels champs doivent apparaître ni quel type leurs valeurs doivent avoir. Deux exécutions avec la même demande peuvent légitimement retourner des objets de formes différentes, et votre code doit être prêt pour cela.
Comment les éléments s’assemblent
Ce projet demande au modèle cinq faits sur le Système solaire. Les étapes sont :
- Créer un
JsonOutputParser. - Demander des instructions de formatage avec
get_format_instructions(). - Insérer ces instructions dans la demande.
- Envoyer la demande au modèle.
- Laisser le parseur transformer la réponse en une valeur Python.
Solar System
↓
PromptTemplate
↓
Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON Object
La version OpenAI
La chaîne complète est courte :
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)
Le modèle est configuré avec gpt-4.1-mini et une température de 0, ce qui permet d’obtenir des résultats aussi reproductibles que le modèle le permet. C’est ce composant qui écrira les cinq faits.
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
Le parseur est créé sans arguments, c’est précisément pourquoi il n’a aucun schéma à appliquer :
parser = JsonOutputParser()
Les instructions de format font le travail réel
Au préalable du lancement du modèle, le parseur peut décrire le format qu’il attend. Cette description provient d’une seule appel de méthode :
parser.get_format_instructions()
Il renvoie un bloc de texte indiquant au modèle de répondre en JSON. Il ne s’agit pas de code exécuté sur le modèle, mais de texte d’instruction. On l’injecte dans le modèle via partial_variables, ce qui remplit une variable de template une seule fois, lors de sa définition, et non à chaque appel :
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Le modèle de template résultant contient deux placeholders :
template = PromptTemplate(
template="Give me 5 facts about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic}est fourni au moment de l’appel et indique les informations souhaitées.{format_instruction}est prérempli avec les directives de formatage du parseur.
Ainsi, lorsque vous lancez la chaîne avec ces données d’entrée, le modèle reçoit à la fois le sujet et l’instruction pour répondre sous forme JSON :
{"topic": "Solar System"}
Assemblage et exécution de la chaîne
La chaîne elle-même ne comporte que trois étapes :
chain = template | model | parser
Dans l’ordre d’exécution :
PromptTemplate
↓
ChatOpenAI
↓
JsonOutputParser
↓
Parsed JSON
Le template génère la demande finale, ChatOpenAI y répond, et JsonOutputParser analyse la réponse pour en extraire des données Python. Le parseur est également tolérant envers certaines habitudes courantes des modèles, comme l’encadrement du JSON dans des balises de code Markdown, qu’il supprime avant l’analyse.
Appelons-le avec le sujet :
result = chain.invoke({"topic": "Solar System"})
Et affichons ce qui est retourné :
print(result)
Ce qui est retourné
Le résultat contient cinq faits sous forme JSON. Le script n’affiche que la valeur, il n’y a donc pas de sortie canonique à citer ; une réponse plausible ressemble à ceci :
{
"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."
]
}
Cette structure, une seule clé facts contenant une liste, est l’une des plusieurs que le modèle peut choisir. Une autre exécution pourrait retourner fact_1 jusqu’à fact_5, ou simplement une liste. Si le code ultérieur accède à result["facts"], il fonctionnera mal dès que le modèle choisira un autre format.
Suivi du flux
Par rapport à StrOutputParser, la nouveauté réside dans le fait que le parseur participe à deux reprises : une fois avant l’appel au modèle, en fournissant des instructions, et une fois après, en effectuant le parsing.
Prompt
↓
JSON Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON
Les instructions sont générées par le parseur lui-même :
parser.get_format_instructions()
Elles parviennent à l’invite de commande via la variable partielle :
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Le modèle répond en tenant compte de ces instructions, et le parseur convertit cette réponse en une valeur Python. De bout en bout :
Solar System
↓
PromptTemplate
↓
JSON Format Instructions
↓
ChatOpenAI
↓
JsonOutputParser
↓
JSON Object
La différence par rapport à l’étape précédente tient en deux lignes. StrOutputParser produit :
StrOutputParser
↓
Plain String
alors que JsonOutputParser produit :
JsonOutputParser
↓
JSON-compatible Structured Data
N’oubliez pas que l’on ne dispose toujours d’aucun schéma fixe. Vous obtenez du JSON, mais les champs et la structure en nesting dépendent du modèle. À noter que les versions actuelles de JsonOutputParser acceptent également un argument optionnel pydantic_object qui ajoute un schéma aux instructions de formatage, mais dans la forme présentée ici, sans arguments, il ne demande que du JSON valide.
Variant Hugging Face
Le flux de travail JSON s’adapte directement au modèle Gemma. La configuration du modèle change ; le parseur, les instructions de format et la chaîne ne changent pas :
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)
Le pipeline est identique, à l’exception de la case du modèle :
PromptTemplate
↓
Hugging Face Model
↓
JsonOutputParser
↓
JSON Object
C’est l’avantage pratique de placer le parsing dans une composante distincte : changer de fournisseur n’affecte pas la logique de parsing. Cependant, gardez à l’esprit que les petits modèles ouverts suivent moins fidèlement les instructions de format que les grands modèles hébergés. Si la réponse contient du texte autour du JSON ou une virgule en fin de chaîne, le parseur lance une OutputParserException ; par conséquent, le code de production doit la capturer et tenter à nouveau ou recourir à une solution de secours.
StructuredOutputParser : nommer les champs attendus
StructuredOutputParser extrait le JSON en se basant sur une liste de champs que vous définissez à l’avance. Alors que JsonOutputParser se contente d’indiquer « réponse en JSON », ce parseur précise « réponse en JSON avec ces clés ».
Les champs sont déclarés à l’aide de ResponseSchema. Chacun possède un name et une description, la description indiquant au modèle ce qui doit figurer dans ce champ. Cela permet un contrôle nettement plus grand sur la structure de la réponse.
Le projet des trois faits
Ce projet demande trois faits sur le Système solaire, un champ par fait :
fact_1contient le premier fait sur le sujet.fact_2contient le deuxième.fact_3contient le troisième.
Le parseur transforme ces déclarations en instructions, puis vérifie que la réponse correspond bien à celles-ci :
Solar System
↓
PromptTemplate
↓
Predefined Field Schema
↓
LLM
↓
StructuredOutputParser
↓
Structured JSON
La version OpenAI
Voici le script complet :
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 que précédemment :
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 contribue à deux éléments :
namedevient la clé dans le dictionnaire résultant.descriptionindique au modèle ce que cette clé doit contenir.
Trois schémas fournissent trois clés obligatoires : fact_1, fact_2 et fact_3.
Vous ne créez pas directement cet analyseur. Une méthode de classe le construit à partir de la liste des schémas :
parser = StructuredOutputParser.from_response_schemas(schema)
parser.get_format_instructions()
Cette fois, les instructions sont plus détaillées. Elles incluent un squelette JSON listant chaque nom de champ avec sa description, et demandent au modèle d’encadrer la réponse dans un bloc JSON. Leur configuration est identique :
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Le modèle de prompt contient les deux placeholders habituels :
template = PromptTemplate(
template="Give 3 fact about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic} est rempli au moment de l’appel, tandis que {format_instruction} contient la liste des champs générée à partir de vos schémas. L’utilisation de ces valeurs en entrée envoie les deux éléments au modèle :
{"topic": "Solar System"}
Exécution de la chaîne
La chaîne comporte les mêmes trois étapes que précédemment :
chain = template | model | parser
Avec le analyseur en dernière position :
PromptTemplate
↓
ChatOpenAI
↓
StructuredOutputParser
↓
Structured JSON
Le modèle de prompt construit la requête, le modèle répond, et StructuredOutputParser extrait les champs déclarés de la réponse.
result = chain.invoke({"topic": "Solar System"})
print(result)
À quoi ressemble la sortie
Le résultat est un dictionnaire contenant trois informations sous les clés que vous avez définies. Le script les affiche :
print(result)
Et un exemple de résultat est :
{
"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."
}
Les informations spécifiques varieront. Ce qui ne doit pas changer, c’est l’ensemble des clés :
fact_1
fact_2
fact_3
C’est là l’avantage par rapport à une simple analyse JSON : c’est votre application qui décide des noms de champ, et non le modèle. Si le modèle omet l’une des clés déclarées, l’analyseur génère une erreur au lieu de retourner silencieusement une structure différente, ce qui est bien plus facile à gérer qu’un KeyError trois fonctions plus tard.
Suivi du flux
Le chemin complet, du sujet au résultat structuré :
Solar System
↓
PromptTemplate
↓
ResponseSchema
↓
Format Instructions
↓
ChatOpenAI
↓
StructuredOutputParser
↓
{
fact_1: ...,
fact_2: ...,
fact_3: ...
}
Tout commence par les définitions des champs :
ResponseSchema(
name="fact_1",
description="Fact 1 about the topic"
)
Le parseur transforme ces éléments en instructions de format, ces instructions sont intégrées dans la requête, le modèle répond, et le parseur extrait les champs déclarés. Par rapport à l’étape précédente :
JsonOutputParser
↓
JSON output
↓
Structure can vary
StructuredOutputParser
↓
Predefined fields
↓
More controlled structure
En bref, JsonOutputParser vise à obtenir du JSON sous n’importe quelle forme, tandis que StructuredOutputParser vise à obtenir du JSON avec les clés que vous avez spécifiées.
Il existe une limite qu’il convient de mentionner clairement. ResponseSchema possède un attribut type dont la valeur par défaut est string, mais celui-ci ne modifie que la formulation des instructions. Le parseur vérifie simplement la présence des clés ; il ne valide pas les types de valeurs ni leurs plages. Si vous souhaitez que la valeur de age soit un entier supérieur à un certain seuil, ce parseur ne le garantira pas.
Vérifiez également le chemin d’importation en fonction de la version de LangChain que vous utilisez. L’exemple importe depuis langchain.output_parsers, mais dans les versions plus récentes, ce parseur obsolète a été retiré des packages principaux, il faudra donc éventuellement modifier l’importation.
Variante Hugging Face
La version Gemma réutilise les mêmes trois schémas ainsi que la même structure de parseur :
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)
Et le même pipeline :
PromptTemplate
↓
ChatHuggingFace
↓
StructuredOutputParser
↓
Structured JSON
Seul le fournisseur du modèle diffère. Les schémas et les parseurs restent identiques.
PydanticOutputParser : structure et validation
PydanticOutputParser est le plus strict des quatre. Il décrit la réponse attendue à l’aide d’un modèle Pydantic, de sorte que la définition de la sortie correspond également à celle de ce qui est considéré comme valide.
Cela dépasse le simple parsing. Les champs possèdent de véritables types Python, et Field() permet d’appliquer des contraintes, par exemple exiger qu’un entier dépasse une valeur minimale. Lorsque la réponse du modèle ne les respecte pas, on obtient une exception plutôt que des données incorrectes.
Pourquoi cela vaut la peine de faire cet effort supplémentaire
- Application du schéma : la réponse doit correspondre à une structure bien définie.
- Sécurité des types : les champs utilisent des types Python tels que
str,intetfloat, et les valeurs sont converties ou rejetées en conséquence. - Vérification : Pydantic vérifie chaque contrainte que vous déclarez.
- Intégration dans les chaînes : il s’intègre aux prompts, aux modèles et aux chaînes LCEL exactement comme les autres analyseurs.
Le projet sur les personnes fictives
Cet exemple demande au modèle d’inventer une personne originaire d’un lieu donné, ici « Indien », avec trois champs :
name, le nom de la personne.age, l’âge de la personne.city, la ville où elle vit.
Le champ age est également soumis à une contrainte : il doit être supérieur à 18.
Input
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
LLM
↓
PydanticOutputParser
↓
Validated Pydantic Object
La version OpenAI
Le script complet :
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)
Il suit le même schéma que précédemment : on définit un modèle Person, on le passe à PydanticOutputParser, puis on relie ce parseur à une instruction et au modèle.
La configuration du modèle reste inchangée :
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
L’élément central est la classe 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")
Ceci constitue le contrat de la réponse :
namedoit être une chaîne de caractères.
age doit être un entier strictement supérieur à 18.city doit être une chaîne de caractères.Field() ajoute à la fois une description lisible par l’humain, qui apparaîtra dans la demande, et des contraintes, qui sont vérifiées après le parsing. Le champ soumis à des contraintes seul :
age: int = Field(gt=18, description="Age of the person")
gt=18 signifie « supérieur à 18 », donc un âge exact de 18 échoue à la validation. Si vous vouliez dire « 18 ans ou plus », utilisez plutôt ge=18.
Le parseur est créé en passant la classe, et non une instance :
parser = PydanticOutputParser(pydantic_object=Person)
Cela indique au parseur quel modèle utiliser, tant pour générer les instructions que pour valider la réponse.
Les instructions de format proviennent de la même méthode que précédemment :
parser.get_format_instructions()
Pour ce parseur, ils contiennent un JSON Schema généré à partir du modèle Pydantic, incluant des descriptions de champs ainsi que la contrainte exclusiveMinimum pour age. Ils sont insérés via la variable partielle comme d’habitude :
partial_variables={
"format_instruction": parser.get_format_instructions()
}
Le modèle de prompt :
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} détermine le type de personne que le modèle doit créer. En utilisant cette entrée, on demande le nom, l’âge et la ville d’une personne indienne fictive :
{"place": "Indian"}
Exécution de la chaîne
La chaîne conserve sa structure habituelle en trois étapes :
chain = template | model | parser
Cette fois, l’étape finale renvoie une instance de modèle :
PromptTemplate
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Pydantic Object
Le modèle génère le prompt, le modèle répond, et PydanticOutputParser analyse le JSON pour le valider en un objet Person.
final_result = chain.invoke({"place": "Indian"})
print(final_result)
À quoi ressemble la sortie
Le résultat est un objet Person, et non un dictionnaire. En l’affichant, on voit la représentation par défaut de Pydantic :
name='Rahul Sharma' age=28 city='Mumbai'
Les valeurs différeront d’une exécution à l’autre. Les garanties, cependant, resteront les mêmes :
name → string
age → integer (> 18)
city → string
C’est ici que les différences deviennent les plus évidentes. JsonOutputParser ne demandait que du JSON, StructuredOutputParser donnait des noms aux champs, tandis que PydanticOutputParser représente l’ensemble du contrat sous forme de véritable classe. Vous pouvez accéder à final_result.age grâce à l’autocomplétion de l’éditeur et être certain qu’il s’agit d’un int supérieur à 18, car tout autre type aurait provoqué une erreur de validation avant même d’arriver à votre code.
Suivre le flux
De l’entrée à l’objet validé :
"Indian"
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Person Object
Cela commence par la structure, présentée ici sans les descriptions et contraintes pour une meilleure lisibilité :
class Person(BaseModel):
name: str
age: int
city: str
La classe est transmise au analyseur :
PydanticOutputParser(pydantic_object=Person)
L’analyseur génère des instructions à partir du modèle ; ces instructions sont incluses dans la requête, le modèle répond, puis l’analyseur traite cette réponse en objets Person et effectue une validation avec Pydantic. Conceptuellement :
Pydantic Model
↓
Defines Structure + Types + Constraints
↓
LLM Response
↓
PydanticOutputParser
↓
Validated Pydantic Object
On obtient ainsi un objet Python dont les données respectent systématiquement vos règles, ce qui dépasse ce que tout dictionnaire JSON peut garantir.
Une conséquence pratique : une erreur de validation se manifeste sous forme d’une OutputParserException au sein de la chaîne. Il faut alors décider de la procédure à suivre. Les options courantes sont de réessayer l’appel, de renvoyer l’erreur au modèle à l’aide du OutputFixingParser de LangChain, ou encore d’enregistrer l’erreur et de retourner une valeur par défaut sûre.
Variant Hugging Face
La version Gemma définit le même modèle Person et le transmet au même analyseur :
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)
Le pipeline reste inchangé :
PromptTemplate
↓
ChatHuggingFace
↓
PydanticOutputParser
↓
Pydantic Object
Seul le fournisseur change ; le modèle Pydantic et l’analyseur sont partagés. Les modèles plus petits ont tendance à enfreindre des contraintes ou à ajouter du texte indésirable, ce qui est précisément le cas où la validation est utile : la mauvaise réponse est détectée à la frontière plutôt que de se retrouver dans vos données.
Sélectionner le bon analyseur
La décision dépend du niveau de structure et de validation dont a réellement besoin le destinataire de la réponse. Voici un résumé de chaque option.
StrOutputParser
Utilisez-le lorsque la réponse du modèle n’est que du texte.
- Idéal pour : rapports, explications, résumés, réponses de chat.
- Réponse : une chaîne de caractères.
- Analyse JSON : non.
JsonOutputParser
Utilisez-le lorsque vous avez besoin de JSON mais que vous pouvez accepter ou gérer une structure variable.
- Meilleur usage : sortie structurée exploratoire, charges utiles flexibles.
- Résultat : un dictionnaire ou une liste.
- Analyse JSON : oui.
- Schéma : non (dans la version sans argument utilisée ici).
- Vérification : uniquement pour s’assurer que c’est du JSON valide.
StructuredOutputParser
Utilisez-le lorsque votre code attend des clés spécifiques, comme fact_1 à fact_3.
- Meilleur usage : enregistrements simples avec des noms de champs connus.
- Résultat : un dictionnaire contenant les clés déclarées.
- Analyse JSON : oui.
- Schéma : oui, avec noms de champs et descriptions.
- Vérification : uniquement la présence des clés, sans vérifications de type.
PydanticOutputParser
Utilisez-le lorsque la sortie alimente directement la logique de l’application et doit être exacte.
- Idéal pour : les données que vous stockez, que vous traitez ou que vous transmettez aux API.
- Rétablit : une instance de votre modèle Pydantic.
- Analyse JSON : oui.
- Schéma : oui, avec tous les types et contraintes.
- Vérification : oui.
Un modèle mental rapide
StrOutputParser: le texte suffit.JsonOutputParser: n’importe quel JSON valide convient.StructuredOutputParser: le JSON doit contenir ces clés.PydanticOutputParser: ces clés, ces types et toutes les contraintes sont vérifiées.
Choisissez le parser le plus simple qui offre les garanties dont vous avez besoin. Chaque niveau supplémentaire ajoute des tokens de prompt pour les instructions et de nouvelles façons de rejeter une réponse, donc une rigueur accrue doit être un choix délibéré.
Une autre option mérite d’être prise en compte. Les quatre analyseurs fonctionnent tous en décrivant un format dans l’instruction puis en analysant le texte par la suite. De nombreux modèles de chat prennent également en charge une sortie structurée native ou l’appel d’outils, fonctionnalités que LangChain met à disposition via with_structured_output() sur le modèle. Lorsque votre fournisseur le prend en charge, cette approche est généralement plus fiable pour les données structurées selon un schéma, tandis que les analyseurs basés sur des instructions restent utiles pour les fournisseurs et modèles qui ne le supportent pas.
Points clés
- Les analyseurs de sortie transforment le message d’un modèle en une valeur que votre code peut utiliser, et ils s’intègrent dans les chaînes LCEL à l’aide de l’opérateur pipe.
StrOutputParsersupprime l’enveloppe du message afin que le texte d’un modèle puisse alimenter la prochaine instruction.JsonOutputParseranalyse le JSON mais ne modifie pas sa structure à moins que vous ne lui fournissiez un schéma.
StructuredOutputParser corrige les noms des clés via ResponseSchema, mais ne vérifie pas les types de valeurs.PydanticOutputParser combine le parsing avec des vérifications de type et des contraintes, en retournant un objet réel.Lorsque les réponses reviennent sous une forme fiable, l’étape suivante naturelle consiste à assembler plusieurs prompts, modèles et analyseurs pour créer des workflows plus complexes, incluant des chaînes séquentielles, parallèles et conditionnelles construites à l’aide de RunnableParallel et RunnableBranch. Pour en savoir plus à ce sujet, consultez la création de pipelines LangChain avec LCEL.