Aller au contenu

LangChain Expression Language (LCEL)

Vous construisez une chaîne LLM complexe. Pourquoi celle-ci reste-t-elle lisible ?

Imaginez la ligne de commande UNIX. Vous enchaînez des commandes via le symbole | : cat fichier.txt | grep "erreur" | sort | uniq. Chaque programme reçoit la sortie du précédent, la transforme, la transmet au suivant. Pas d’imbrication verbale, pas d’état partagé—juste un flux de données transparent.

LCEL (LangChain Expression Language) transpose cette élégance UNIX au monde des grands modèles de langage. Au lieu de nester des appels de fonction ou de gérer manuellement le plumbing entre composants, vous écrivez : prompt | llm | parser. La syntaxe reste déclarative, lisible, modularisable.

C’est ce passage du verbeux au fluide qui explique pourquoi LCEL a devenu le standard de facto en 2023-2024 chez les équipes LangChain. Moins de friction cognitive, plus de temps pour peaufiner la logique métier.


Pourquoi LCEL a remplacé LLMChain (et comment)

Avant 2023, les développeurs LangChain enchaînaient les opérations via la classe LLMChain. Voici le paradigme ancien :

# Ancien style (LLMChain) - verbeux et peu composable
chain1 = LLMChain(prompt=prompt_template, llm=llm)
chain2 = LLMChain(prompt=follow_up_prompt, llm=llm)
result1 = chain1.run(input="test")
result2 = chain2.run(input=result1)

Trois problèmes majeurs :

  • Verbosité : chaque composant requiert une instanciation explicite et un appel .run().
  • Manque de composabilité : assembler trois chaînes demandait du code glue ad hoc pour passer les résultats.
  • Absence de streaming natif : afficher les tokens en temps réel nécessitait une refactorisation complète.

LCEL émerge en 2023 comme syntaxe minimaliste exposant cette abstraction sous-jacente :

# Nouveau style (LCEL) - déclaratif et composable
chain = prompt | llm | parser
# Invocation simple
result = chain.invoke({"input": "test"})
# Streaming natif
for token in chain.stream({"input": "test"}):
print(token, end="", flush=True)

Le code se réduit de 50%, et le streaming devient une ligne au lieu d’une refactorisation.


Comment LCEL transforme les données (sous le capot)

Tous les composants LangChain—ChatPromptTemplate, ChatOpenAI, JsonOutputParser—sont des Runnables. L’opérateur | n’est pas magique : c’est la méthode Python __or__ surchargée pour créer une chaîne d’appels.

Quand vous écrivez prompt | llm | parser, vous créez une séquence implicite :

  1. prompt (Runnable) reçoit l’input utilisateur, génère un prompt formaté.
  2. llm (Runnable) reçoit le prompt, appelle l’API OpenAI, retourne la génération brute.
  3. parser (Runnable) reçoit la génération, l’extrait en JSON, retourne un dictionnaire structuré.

Chaque étape expose invoke(), stream(), batch(). LCEL propage automatiquement ces interfaces à travers la chaîne. Appeler chain.stream() décompose l’exécution en tokens partiels traversant tout le pipeline.

Patterns avancés : branchements et parallélisme

Les chaînes linéaires simples ne suffisent pas. LCEL propose des composants spécialisés :

RunnableBranch : conditionnel déclaratif.

from langchain.runnables import RunnableBranch
# Créer une chaîne de classification
is_spam = prompt_spam | llm_spam | bool_parser
# Brancher sur le résultat
chain = RunnableBranch(
[(is_spam, spam_handler_chain), normal_handler_chain]
)
# Si is_spam retourne True, exécute spam_handler_chain.
# Sinon, exécute normal_handler_chain.

RunnableParallel : exécution concurrente de plusieurs branches.

from langchain.runnables import RunnableParallel
parallel_chain = RunnableParallel({
"summary": summarize_chain,
"entities": ner_chain,
"sentiment": sentiment_chain
})
result = parallel_chain.invoke({"text": "..."})
# result = {"summary": "...", "entities": [...], "sentiment": "positif"}

RunnablePassthrough : préserver le contexte à travers la chaîne.

from langchain.runnables import RunnablePassthrough
# Pattern RAG classique
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt_template
| llm
| parser
)
# La sortie du retriever et la question originale sont tous deux
# disponibles pour le prompt template.

Cas d’usage réels : de la théorie à la production

E-commerce : chatbot de recommandation

Une startup doit deployer un chatbot produit qui (1) récupère les fiches articles pertinentes, (2) les synthétise, (3) formule des recommandations personnalisées avec streaming.

Ancien approche : 5 classes LLMChain imbriquées, gestion manuelle du contexte, refactorisation complète pour ajouter le streaming.

Approche LCEL :

from langchain.runnables import RunnableParallel, RunnablePassthrough
rag_chain = (
{"products": retriever, "user_profile": profile_loader}
| RunnablePassthrough.assign(enriched_prompt=enrich_prompt)
| recommendation_prompt
| llm
| json_parser
)
# Deploy via LangServe (3 lignes supplémentaires)
for token in rag_chain.stream({"query": "téléphones sous 500€"}):
print(token, end="", flush=True)

Résultat : code compact, lisible, streaming natif, déploiement trivial.

Audit légal : classification multi-étapes de documents

Une équipe compliance doit classifier des documents (risque faible/moyen/élevé) et déclencher des workflows différents.

classifier_chain = document | risk_classifier | risk_parser
escalation_chain = doc_summary | escalation_prompt | llm | notify_slack
audit_pipeline = RunnableBranch(
[(high_risk_check, escalation_chain), standard_workflow_chain]
)

RunnableBranch encode le décisionnel sans if/else éparpillés. LangSmith trace chaque branche exécutée pour audit trail automatique.


Déploiement en production via LangServe

LCEL brille particulièrement au déploiement. Une chaîne LCEL peut être exposée comme API REST en trois lignes :

from langserve import add_routes
# Votre chaîne LCEL existante
chain = prompt | llm | parser
# Exposition automatique
add_routes(app, chain, path="/chain")
# LangServe génère automatiquement :
# POST /chain/invoke
# POST /chain/stream
# POST /chain/batch

Pas d’adaptation FastAPI manuelle, pas de sérialisation JSON à coder. LangServe infère les schémas via les type hints de votre chaîne et génère swagger/docs automatiquement.


Observabilité intégrée : LangSmith

Chaque appel à chain.invoke() ou chain.stream() génère des événements structurés (début, fin, erreur, token). LangSmith consomme ces événements pour tracer, versionner, évaluer les chaînes en production.

Configuration (3 variables d’environnement) :

Fenêtre de terminal
export LANGCHAIN_API_KEY="your_key"
export LANGCHAIN_PROJECT="production"
export LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"

Bénéfice : dashboard centralisé pour monitoring, A/B testing de prompts, debugging visuel des chaînes complexes.


Les limites critiques

Courbe d’apprentissage : LCEL suppose une familiarité avec la programmation fonctionnelle (composition, pipes, immutabilité). Les juniors venant de paradigmes impératifs trouvent cette transition abrupte.

Debugging cryptique : quand une chaîne nested échoue, la stack trace est opaque. Le recours à LangSmith devient quasi obligatoire pour clarifier les points de rupture.

Performance des petites chaînes : LCEL ajoute un overhead d’abstraction (Runnable protocol, type inference). Pour des chaînes triviales (1-2 composants), ce coût peut être observable.

Pérennité : LCEL est étroitement lié à LangChain. Si l’écosystème diverge, vous êtes bloqué.


Notions liées


Sources & Références

  • LangChain Official Documentation - Expression Language Guide
  • Wild Code School - LCEL: Qu’est-ce que le LCEL
  • Pinecone - LangChain Expression Language Explained
  • AWS - Qu’est-ce que LangChain
  • DataCamp - Développement d’applications LLM avec LangChain
  • Aurelio AI Learn - LangChain Expression Language (LCEL)