Aller au contenu principal

Module 2 — Modèles, consignes et analyseurs de sortie

Trois briques suffisent pour un premier appel utile : un modèle chat, un gabarit de consigne, un analyseur qui transforme la réponse en objet Python typé. Ce module fixe la mécanique de chacune en gardant le fil rouge : extraire d'un ticket de restaurant scanné les champs qu'attend notre système de notes de frais.

L'interface chat unifiée

langchain-core définit l'interface BaseChatModel. Un modèle instancié expose toujours les mêmes verbes, que l'implémentation soit ChatOpenAI, ChatAnthropic ou ChatOllama :

from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage

modele = ChatOpenAI(model="gpt-4o-mini", temperature=0)

reponse = modele.invoke([
SystemMessage(content="Vous êtes un assistant de notes de frais."),
HumanMessage(content="Le taxi du soir est-il remboursable ?"),
])
print(reponse.content)

.invoke(...) renvoie un AIMessage avec un content (le texte) et des response_metadata (jetons utilisés, latence, finish_reason). .stream(...) retourne un itérable de fragments pour un rendu progressif. .batch([...]) traite plusieurs entrées en parallèle avec un contrôle explicite de la concurrence — utile pour ré-annoter mille tickets sans écrire de boucle asynchrone.

Trois paramètres à connaître par cœur. temperature=0 pour toute tâche déterministe (extraction, classification, appel d'outil) ; max_tokens pour couper une réponse trop longue ; timeout pour éviter qu'un incident réseau ne fige le processus. Les autres paramètres du fournisseur (top_p, pénalités) passent en model_kwargs.

Les gabarits de consigne

Concaténer des f-string à la main est la source du 80 % des bogues d'un premier prototype. Un ChatPromptTemplate sépare la structure des messages des variables à injecter :

from langchain_core.prompts import ChatPromptTemplate

consigne = ChatPromptTemplate.from_messages([
("system", "Vous extrayez les champs d'un ticket de restaurant. Répondez uniquement par du JSON valide."),
("human", "Voici le texte OCR d'un ticket :\n\n{texte}\n\nDate d'aujourd'hui : {date}."),
])

messages = consigne.format_messages(texte="RESTAURANT LA POSTE 14/03/2026 27,50 EUR", date="2026-03-15")

Trois avantages tangibles. La consigne système est déclarée une fois, jamais retapée. Les variables sont explicitées : si on oublie date, format_messages lève une KeyError avant l'appel API, pas au milieu de la production. Enfin, le gabarit est composable avec | : consigne | modele devient une chaîne réutilisable.

Un point qui piège : les accolades {} dans le texte du gabarit sont interprétées. Un exemple JSON inséré littéralement doit doubler ses accolades ({{ et }}) ou passer par une variable.

Les analyseurs de sortie

Un LLM répond en texte. Une application a besoin d'un objet. L'analyseur (OutputParser) fait le pont.

Le plus simple, StrOutputParser, extrait .content du message et retourne une chaîne. Utile pour clore un pipeline dont on veut le texte brut.

Pour de l'extraction structurée, la voie moderne s'appuie sur pydantic :

from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser

class LigneTicket(BaseModel):
marchand: str = Field(description="Nom du marchand")
montant: float = Field(description="Montant total en euros, séparateur décimal '.'")
date: str = Field(description="Date au format ISO AAAA-MM-JJ")
categorie: str = Field(description="restauration, transport, hebergement, autre")

analyseur = PydanticOutputParser(pydantic_object=LigneTicket)

L'analyseur fournit deux services. À la construction, analyseur.get_format_instructions() génère une consigne prête à injecter qui décrit le schéma attendu. À la sortie, analyseur.invoke(reponse) valide le JSON et retourne une instance LigneTicket. Un champ manquant ou un type incorrect lève une ValidationError avant que l'objet ne pollue le reste du pipeline.

Sorties structurées natives

Depuis 2024, la plupart des fournisseurs proposent un mode « sortie structurée » garanti côté serveur, plus fiable qu'un simple Répondez en JSON :

modele_structure = ChatOpenAI(model="gpt-4o-mini", temperature=0).with_structured_output(LigneTicket)
ticket = modele_structure.invoke("Extrayez : RESTAURANT LA POSTE 14/03/2026 27,50 EUR")

with_structured_output(...) accepte une classe pydantic ou un schéma JSON et renvoie directement un objet valide. En interne, LangChain choisit entre trois mécanismes selon le fournisseur : JSON mode, appels de fonction, ou grammaire contrainte. Cette méthode est aujourd'hui la voie par défaut pour extraire du structuré ; réservez PydanticOutputParser aux modèles qui ne supportent pas encore la sortie stricte.

Gérer les échecs d'analyse

Un modèle échoue parfois : il renvoie du texte hors JSON, un champ mal typé, une clé oubliée. Trois lignes de défense empilées :

  • Contraindre à la source. with_structured_output réduit le taux d'échec d'un ordre de grandeur par rapport à un simple Répondez en JSON.
  • Envelopper d'un OutputFixingParser. À la première ValidationError, ce parseur redemande au modèle de corriger sa sortie en lui montrant l'erreur. Coûte un appel supplémentaire, sauve la journée.
  • Journaliser et rejouer. Un try/except ValidationError qui écrit la sortie brute dans un fichier de quarantaine évite de perdre les cas problématiques ; ils alimentent le jeu d'évaluation du module 9.
Le piège de la température

Un temperature=0.7 en extraction structurée est une erreur silencieuse : la sortie devient inconstante d'un appel à l'autre alors qu'on cherche exactement le contraire. Toute tâche d'extraction, de classification et d'appel d'outil demande temperature=0.

En résumé

  • L'interface BaseChatModel uniformise .invoke, .stream, .batch entre fournisseurs ; temperature, max_tokens et timeout sont les trois paramètres à toujours fixer.
  • Un ChatPromptTemplate sépare la structure des variables et détecte les variables oubliées avant l'appel API.
  • with_structured_output avec un modèle pydantic est la voie par défaut de l'extraction structurée en 2026 ; PydanticOutputParser reste utile pour les modèles sans sortie stricte.
  • OutputFixingParser récupère la plupart des erreurs d'analyse en redemandant une correction au modèle ; toujours associer un temperature=0 en extraction.

Module suivant : composer plusieurs Runnable en LCEL pour enchaîner et paralléliser ces briques.