Les bases de LangChain : à quoi il sert et quand il est préférable de l’éviter complètement.
Découvrez ce que LangChain offre réellement, pourquoi les outils utilisant plusieurs fournisseurs incitent les équipes à l’adopter, quels paquets installer, et quand un wrapper simplifié est la meilleure solution.
Sources: [Day 11/100] LangChain 101: Why It Exists, and When You Should (and Should Not) Use It
L’agent à cinquante lignes cesse d’être à cinquante lignes
Ajouter un nouvel outil à un petit agent développé manuellement est peu coûteux. On écrit une fonction, on la registre dans un dictionnaire, on la décrit dans un schéma, et la boucle elle-même ne change jamais. Cette partie du travail sur les agents semble agréable, ce qui amène les gens à croire que l’ensemble du projet restera de cette taille réduite.
Alors, les véritables exigences apparaissent. L’endpoint d’OpenAI renvoie parfois un code 503, il faut donc effectuer des tentatives répétées avec un mécanisme de backoff. Parfois, le fournisseur est indisponible pendant vingt minutes, il est donc nécessaire d’avoir un autre fournisseur en réserve. Un utilisateur qui attend huit secondes que quatre appels enchaînés se terminent pensera que l’application est bloquée, c’est pourquoi on ajoute le streaming de tokens. Un service intermédiaire rejette une réponse parce qu’un champ est revenu sous forme de chaîne de caractères plutôt que de nombre, il faut donc valider la sortie structurée. Et une fois que dix personnes l’utilisent en même temps, les appels bloquants deviennent inacceptables, il faut donc que tout soit asynchrone.
Tout cela est construit à partir des bibliothèques brutes openai et requests, et l’agent atteint progressivement environ cinq cents lignes de code. Très peu de ce code concerne réellement ce que fait l’agent : il s’agit principalement du parsing JSON, de mécanismes d’attente exponentielle, ainsi que de la conversion entre les formats de messages inventés par chaque fournisseur. Chaque équipe finit par devoir écrire ce genre de code de base, et personne n’aime devoir le réécrire.
C’est précisément ce genre de code de base que propose LangChain. Ci-dessous, on explique en détail de quoi se compose réellement cette bibliothèque, les problèmes qu’elle vise à résoudre, ainsi que, tout aussi utilement, les situations où l’ajouter constitue une erreur.
De quoi se compose la bibliothèque (et de quoi elle ne se compose pas)
LangChain ne lance pas votre agent à votre place, il ne héberge rien, et il ne permettra pas à un modèle de raisonner de manière plus efficace. Considérez-le comme un ensemble d’interfaces partagées ainsi que des implémentations prêtes à l’emploi pour les composants d’une application LLM que vous devriez normalement coder vous-même.
Si vous avez déjà créé un agent de base, vous avez déjà développé la plupart de ces éléments sous d’autres noms. La liste des messages échangés correspond à ChatMessageHistory. Un schéma d’outil devient un Tool, ou une fonction ordinaire décorée avec @tool. La boucle while qui continue d’appeler le modèle jusqu’à ce qu’il cesse de demander des outils est précisément ce que faisait AgentExecutor. De plus, l’appel direct client.chat.completions.create se transforme en un ChatModel. Rien ici n’est conceptuellement nouveau. Ce qui change, c’est que ces composants possèdent désormais une structure commune déjà utilisée par des milliers d’autres projets, et que les combinaisons possibles sont prédéfinies.
Les problèmes pour lesquels il a été conçu
Commencez par l’appel à l’outil, car il montre clairement le désordre. OpenAI associe les tool_calls au message de l’assistant et souhaite recevoir le résultat sous forme de message avec role: "tool". Anthropic envoie des blocs de contenu tool_use et attend des blocs tool_result lors du prochain tour de l’utilisateur. Gemini utilise les éléments function_call et function_response. Même idée, trois API, trois formats qui ne s’adaptent pas les uns aux autres.
Avec un seul fournisseur, cela ne pose jamais de problème. Le jour où vous avez besoin d’un deuxième, peut-être parce que le premier est en panne ou est simplement devenu plus cher, vous devez soit écrire et gérer trois adaptateurs, soit vous appuyer sur du code qui s’en occupe déjà. C’était la raison initiale de LangChain : une interface ChatModel où .invoke() et .bind_tools() fonctionnent de la même manière, quel que soit le fournisseur des tokens. Passer de ChatOpenAI à ChatAnthropic consiste simplement à changer l’import, sans avoir à réécrire la gestion des messages.
La composition est le deuxième problème. Fournir une instruction à un modèle, puis ce modèle à un analyseur, et cet analyseur à un autre modèle implique l’ajout de code de liaison à chaque étape, et ce code doit être réécrit chaque fois que le flux de travail change. LangChain uniformise ces étapes. Le mécanisme à l’origine de cela est LCEL, le langage d’expression de LangChain, qui mérite une explication détaillée à part.
Le troisième problème concerne simplement la réutilisation. Un agent qui fait plus que simplement appeler un modèle a besoin d’éléments déjà développés par d’autres : des chargeurs pour une quarantaine de formats de documents, des clients pour une douzaine de bases de données vectorielles, des politiques de tentative répétée, des fonctionnalités de streaming, de regroupement de données, ainsi que des outils de suivi. Une intégration déjà maintenue est presque toujours meilleure qu’un analyseur PDF que l’on a écrit un vendredi et qu’il faut maintenant déboguer.
Ce que vous obtenez sans devoir le coder vous-même
Une seule interface de modèle. ChatOpenAI, ChatAnthropic, ChatGoogleGenerativeAI et ChatOllama pour les modèles locaux exposent tous .invoke(), .stream() et .bind_tools(). Votre code n’a plus besoin de savoir qui a entraîné le modèle derrière l’appel.
Blocs de construction composables. Grâce à l’opérateur pipe de LCEL, vous écrivez prompt | model | parser et obtenez un objet qui se comporte comme chacun de ses composants. Il peut diffuser des données, traiter des lots et exécuter des tâches asynchrones sans effort supplémentaire.
Résilience via des appels de méthode. Tout objet de type Runnable dispose de .with_retry() et .with_fallbacks(), ce qui vous évite d’écrire manuellement une boucle de tentative ou un mécanisme de basculement.
Streaming et asynchrone partout. Chaque objet Runnable dispose de .stream(), .ainvoke(), .astream() et .batch(), même lorsque l’objet initial ne prenait jamais en charge ces fonctions nativement.
Un vaste catalogue d’intégrations. Chargeurs de documents, bases de données vectorielles, outils de recherche, exécution de code, outils de base de données. Beaucoup sont maintenus par les fournisseurs eux-mêmes, et tous utilisent la même interface Runnable.
Un chemin direct vers LangGraph. Lorsque votre agent a besoin de boucles, d’un état sauvegardé ou d’une étape d’approbation humaine que une chaîne linéaire ne peut pas gérer, la boucle de contrôle est transférée vers LangGraph. Comme il s’appuie sur les mêmes abstractions de base, ce que vous apprenez avec LangChain est directement transférable.
Quand l’omettre
La plupart des introductions omettent cette partie, or c’est elle qui évite le plus de problèmes par la suite.
Un fournisseur, un appel, à jamais. Si le produit consiste simplement à « envoyer cette requête à GPT-4o et récupérer le texte », un petit wrapper personnalisé est plus facile à lire et à déboguer qu’une dépendance qui implique des centaines de paquets intermédiaires.
Vous devez pouvoir voir chaque requête. Les abstractions sont utiles justement parce qu’elles cachent les détails, mais ce camouflage vous coûte du temps lorsque quelque chose ne fonctionne pas. Suivre une trace d’erreur à travers six niveaux de wrappers Runnable avant d’atteindre le véritable bug représente une perte de temps réel. Les équipes qui souhaitent examiner chaque ligne entre leur code et l’appel HTTP restent souvent proches du SDK brut.
Cheminements critiques en termes de latence. Chaque couche ajoute une petite charge supplémentaire. À côté d’un appel au modèle qui prend 500 ms ou plus, cette charge est généralement négligeable. Mais sur un cheminement où chaque milliseconde compte, ce n’est pas toujours le cas.
Mises à jour non verrouillées. LangChain introduit régulièrement des changements perturbateurs. Un projet qui repose sur la dernière version finira par devoir faire face à des importations renommées ou à des paramètres par défaut modifiés. Verrouillez la version comme n’importe quelle autre dépendance et mettez à jour lorsque vous l’avez décidé, et non lorsque pip le décide à votre place.
Tout cela ne signifie pas que vous ne devriez jamais utiliser LangChain. Cela signifie que la décision doit être prise au cas par cas, de manière intentionnelle, et non par habitude.
Quel paquet fait quoi
LangChain est divisé en plusieurs paquets, et il est utile de savoir ce que fait chacun avant d’installer quoi que ce soit.
langchain-corecontient les abstractions de base :Runnable,BaseChatModel,BaseTool, des modèles de prompts et des analyseurs de résultats. Il a peu de dépendances, et presque tout le reste s’appuie sur lui.langchainajoute des chaînes, des agents et une logique de récupération par-dessus le noyau, sans vous lier à un fournisseur spécifique.langchain-communityrassemble un grand nombre d’intégrations de tiers. Certaines sont excellentes, tandis que d’autres ont été abandonnées, car n’importe qui peut y contribuer. Vérifiez la date de la dernière mise à jour d’une intégration avant de l’utiliser en production.- Les paquets partenaires tels que
langchain-openai,langchain-anthropicetlangchain-google-genaisont maintenus en même temps que le SDK propre à chaque fournisseur. Installez uniquement ceux que vous utilisez réellement. langchain-classicest l’endroit où se trouvaient les constructeurs d’agents avant la version 1.0 :AgentExecutor,create_tool_calling_agentet l’anciencreate_react_agent. Le code existant continue de fonctionner, mais de nouveaux agents ne devraient pas être créés ici.langgraphest un package distinct par rapport àlangchain-corequi gère la boucle de contrôle pour tout ce qui n’est pas une trajectoire linéaire. Depuis la version v1 delangchain, le constructeur d’agents recommandé,langchain.agents.create_agent, est implémenté directement dessus.
Pour commencer, vous n’avez besoin que de trois packages :
uv add langchain langchain-openai python-dotenv
Remplacer le client brut par ChatOpenAI
Ne vous attendez à rien de spectaculaire à ce stade. L’objectif est de remplacer le client OpenAI simple par l’encapsuleur de modèle de chat de LangChain et de vérifier que vous obtenez la même réponse.
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model="gpt-4o", temperature=0)
response = llm.invoke("What is the weather in Paris?")
print(response.content)
llm.invoke(...) vous renvoie un AIMessage plutôt qu’un dictionnaire brut, mais .content vous fournit le texte exactement comme le faisait .choices[0].message.content. Au fond, ChatOpenAI utilise toujours le même endpoint d’OpenAI que celui que vous auriez appelé manuellement, et le modèle n’a pas du tout changé. La différence réside dans le fait que llm est désormais une Runnable ; ainsi, .stream(), .batch() et .ainvoke() sont déjà disponibles sans que vous ayez eu à les écrire.
for chunk in llm.stream("Tell me one fact about Paris."):
print(chunk.content, end="", flush=True)
Exécutez-le et les tokens arriveront par lots. Vous n’avez jamais écrit de gestionnaire de flux, et cette petite boucle constitue pratiquement l’argument principal en faveur de cette bibliothèque.
Un mot rapide sur les noms de modèles : les exemples utilisent "gpt-4o" à titre indicatif. La liste des fournisseurs évolue plus rapidement que tout tutoriel ne peut le suivre, donc considérez chaque chaîne model="..." comme un placeholder et remplacez-la par le modèle phare proposé par votre fournisseur lors de l’exécution du code. La logique reste identique.
Essayez-le avec votre propre agent
- Ajoutez
langchainetlangchain-openaià votre projet à l’aide deuv add. - Dans l’étape de modèle de votre agent, remplacez l’appel brut
OpenAI()parChatOpenAIet laissez le reste de la boucle inchangé. Vérifiez que l’agent se comporte exactement comme avant. - Appelez
llm.stream(...)pour une question simple sans outils et affichez chaque morceau au fur et à mesure qu’il arrive. Vous disposez désormais d’une fonctionnalité de streaming pour une méthode que vous n’avez jamais implémentée.
Votre agent n’est pas plus intelligent après ce changement. Ce que vous avez gagné, c’est une base permettant aux tentatives répétées, aux solutions de secours, au streaming et au changement de fournisseur de coûter un appel de méthode plutôt que des centaines de lignes de code supplémentaires.