On a migré trois projets clients de LangChain vers le SDK Anthropic natif en 2025-2026. Aucun regret. Bundle plus léger, latence plus basse, code lisible, debugging redevenu trivial. Cet article documente le playbook qu'on suit, les chiffres qu'on a mesurés, et les rares cas où on garde LangChain.
LangChain a eu son moment. En 2023, c'était la bonne abstraction au bon moment — un orchestrateur multi-LLM dans un monde où chaque provider avait son SDK incompatible. En 2026, le paysage a changé, et la couche d'abstraction est devenue plus coûteuse que la cohésion qu'elle apportait.
Pourquoi LangChain pose problème en 2026
Quatre problèmes structurels, observés sur des audits internes et des projets clients :
Over-engineering. LangChain expose 200+ classes (chains, agents, parsers, memory, vectorstores, retrievers, callbacks). Sur les bases de code qu'on a auditées, moins de 20 % de cette surface est utilisée. Le reste est du poids mort dans le bundle et dans la charge cognitive.
Abstractions qui cachent. Quand un agent LangChain plante, le stack
trace traverse 6-8 couches : AgentExecutor → Chain → LLMChain →
Runnable → Callback → OutputParser → ton code. Le debug devient
exotique. Avec le SDK natif, tu vois le payload qui part chez Anthropic et
la réponse qui revient. Point.
Lock-in implicite. "On peut toujours migrer plus tard" est rarement
vrai. Les chaines LangChain s'interconnectent — un ConversationChain
appelle un RetrievalQAChain qui hérite d'un BaseLanguageModel. Sortir
de cet écheveau demande de réécrire l'ensemble, pas une partie.
Versioning chaotique. Entre langchain v0.0.x, v0.1.x, v0.2.x, v0.3.x,
les breaking changes ont été fréquents en 2024-2025. Le split en
langchain-core, langchain-community, langchain-anthropic aide, mais
ajoute son propre lot de dépendances et de conflits de versions.
Performance. Chaque appel passe par plusieurs couches de pre/post processing. Sur des benchmarks internes, on mesure 150 à 400 ms de latence ajoutée par requête en p95, juste due à la chaîne d'abstractions.
Ce qui a changé en 2025-2026
Le SDK Anthropic ne couvrait pas tous les besoins en 2023. Aujourd'hui, oui.
- SDK TypeScript et Python matures, avec types complets, retry, streaming, rate limiting intégrés.
- Tool-use natif stabilisé : déclaratif, typé, plus simple que les
ToolsLangChain à wrapper manuellement. - Structured output via JSON Schema couvert nativement par
tool_use, validation Zod côté code. - MCP (Model Context Protocol) émerge comme standard d'intégration — beaucoup d'intégrations LangChain sont remplacées par un serveur MCP plus simple.
- Mastra (TypeScript) propose une alternative légère pour les workflows
multi-étapes qui demandaient
LangGraphauparavant. ~30 KB vs ~400 KB pourlangchain+langgraph.
Quand garder LangChain — vraiment
Migration n'est pas religieuse. On garde LangChain dans deux cas :
| Cas | Garder LangChain ? |
|---|---|
| Agent simple (1-3 tools, 1 LLM) | Migrer |
| Structured output unique | Migrer |
| RAG single-source (Postgres pgvector) | Migrer |
| Classification, extraction, résumé | Migrer |
| RAG multi-sources (Pinecone + Weaviate + Postgres) | Garder |
| Framework agentique très expérimental (R&D) | Garder |
| LangGraph + cycles complexes + checkpointing | Garder (ou Mastra) |
Sur nos projets clients, ~85 % des cas tombent dans la colonne "Migrer".
Le playbook de migration
Six étapes. On ne saute aucune. Le plus dangereux n'est pas la migration — c'est de croire qu'on a migré et de découvrir trois semaines plus tard que les outputs ne sont plus les mêmes.
Étape 1 — Inventaire
Lister toutes les chaines LangChain en prod, par criticité métier :
chain-classify-support | critique | 8 000 req/jour
chain-rag-faq | critique | 2 400 req/jour
chain-extract-invoice | critique | 350 req/jour
chain-summarize-emails | non-bloquant | 1 200 req/jour
chain-generate-meta | non-bloquant | 80 req/jour
On migre du moins critique vers le plus critique. Pas l'inverse. La première chaîne migrée sert d'apprentissage — on accepte qu'elle prenne 3× plus de temps que prévu.
Étape 2 — Eval golden set
Avant de toucher au code, on capture 30 à 50 cas de test sur la chaîne existante. Input + output produit par LangChain. C'est la baseline.
// evals/classify-support.golden.ts
export const goldenSet = [
{
id: 'C001',
input: { text: 'Mon paiement a échoué hier soir, comment faire ?' },
expected: { category: 'billing', priority: 'high', confidence: '>0.8' },
},
{
id: 'C002',
input: { text: 'Comment je change mon mot de passe ?' },
expected: { category: 'account', priority: 'low', confidence: '>0.7' },
},
// ...30 à 50 cas représentatifs
]
L'accuracy cible sur la version migrée : ≥ 90 % du golden set. En dessous, on n'avance pas.
Étape 3 — Réécrire la chaîne la plus simple
Toujours commencer par une classification ou un structured output simple. C'est là où l'écart de complexité est le plus criant.
Étape 4 — Valider contre le golden set
On fait tourner les 30-50 cas, on compare output ancien (LangChain) vs output nouveau (SDK natif), on calcule l'accuracy. Si < 90 %, on itère sur le prompt nouveau — pas sur le code.
Étape 5 — Shadow mode en prod
Pendant 1 à 2 semaines, on tourne les deux versions en parallèle. LangChain sert toujours le trafic, le SDK natif tourne en miroir, on log les différences dans une table dédiée.
const [legacyResult, nativeResult] = await Promise.all([
langchainChain.invoke({ text }),
nativeClassify(text),
])
if (!deepEqual(legacyResult, nativeResult)) {
await db.divergences.insert({ text, legacy: legacyResult, native: nativeResult })
}
return legacyResult // legacy continue à servir
Au bout d'une semaine, on a 1 000+ comparaisons réelles, pas 50 cas forgés. C'est là qu'on découvre les edge cases.
Étape 6 — Switch progressif
Feature flag : 10 % du trafic sur le natif, puis 50 %, puis 100 %. Si une métrique business dérape (taux de complétion, NPS, escalation rate), on revient en arrière instantanément.
Une fois à 100 % stable pendant deux semaines, on supprime le code LangChain. Pas avant. La tentation de "nettoyer tout de suite" a coûté deux incidents en 2024 — on ne le refait pas.
Avant / après — code comparatif
Voilà la même classification de ticket support, version LangChain et version SDK natif.
Avant — LangChain (~30 lignes)
import { ChatAnthropic } from '@langchain/anthropic'
import { ChatPromptTemplate } from '@langchain/core/prompts'
import { StructuredOutputParser } from 'langchain/output_parsers'
import { RunnableSequence } from '@langchain/core/runnables'
import { z } from 'zod'
const schema = z.object({
category: z.enum(['billing', 'account', 'technical', 'other']),
priority: z.enum(['low', 'medium', 'high']),
confidence: z.number(),
})
const parser = StructuredOutputParser.fromZodSchema(schema)
const prompt = ChatPromptTemplate.fromMessages([
['system', 'Classify the support ticket. {format_instructions}'],
['human', '{text}'],
])
const model = new ChatAnthropic({
modelName: 'claude-haiku-4-5',
temperature: 0,
})
const chain = RunnableSequence.from([
prompt.partial({ format_instructions: parser.getFormatInstructions() }),
model,
parser,
])
export async function classify(text: string) {
return await chain.invoke({ text })
}
Après — Anthropic SDK natif (~12 lignes)
import Anthropic from '@anthropic-ai/sdk'
import { z } from 'zod'
const client = new Anthropic()
const schema = z.object({
category: z.enum(['billing', 'account', 'technical', 'other']),
priority: z.enum(['low', 'medium', 'high']),
confidence: z.number(),
})
export async function classify(text: string) {
const response = await client.messages.create({
model: 'claude-haiku-4-5',
max_tokens: 256,
system: 'Classify the support ticket. Return strict JSON only.',
messages: [{ role: 'user', content: text }],
})
const block = response.content[0]
if (block.type !== 'text') throw new Error('Unexpected response type')
return schema.parse(JSON.parse(block.text))
}
Moins de classes à apprendre. Le payload qui part chez Anthropic est
visible dans le code. Si quelque chose plante, le stack trace pointe la
ligne qui plante — pas un RunnableSequence anonyme dans node_modules.
Bénéfices mesurés
Sur les trois projets migrés en 2025-2026, on a tracé chaque métrique avant/après. Synthèse :
| Métrique | Projet A | Projet B | Projet C |
|---|---|---|---|
| Bundle JS (gzipped) | −180 KB | −240 KB | −340 KB |
| Latency p95 | −150 ms | −280 ms | −380 ms |
| Lignes de code | −42 % | −51 % | −60 % |
| Coût API mensuel | −8 % | −12 % | −15 % |
| Time-to-debug incident | −60 % | −70 % | −55 % |
La baisse de coût API vient surtout de la réduction du system prompt —
LangChain injecte des format_instructions longues pour le parsing, qu'on
peut remplacer par une consigne courte + JSON.parse côté code.
Les risques de la migration
Tout n'est pas gratuit. Trois risques à anticiper :
Pas de tracing centralisé "out of the box". LangSmith était plug-and-play
avec LangChain. En natif, il faut câbler Langfuse, Helicone ou écrire un
trace minimal (50 lignes de OTEL autour des appels client). On
recommande Langfuse en open-source self-hosted — c'est le compromis qu'on
fait sur nos projets.
Multi-modèle. Si tu switches souvent entre Claude, GPT et Mistral (évals comparatives, fallback inter-providers), garder une couche d'abstraction minimale reste utile. Mais 50 lignes de code custom qu'on maîtrise valent mieux que 400 KB de LangChain qu'on subit.
Memory et conversation state. LangChain offre ConversationBufferMemory,
ConversationSummaryMemory, etc. En natif, c'est à toi de gérer le state
côté DB. Pas plus difficile, juste explicite. Pour la plupart des projets,
un champ messages: Json[] en Postgres suffit.
Quand on ne migre pas
On a un projet où on garde LangChain : un agent R&D qui orchestre 4 LLMs différents (Claude, GPT-4, Mistral Large, Llama 3.1 70B) avec routing dynamique selon la tâche. Réécrire ça en natif aurait demandé 2 semaines de travail pour un agent qui ne va pas en prod — pas rentable.
Le critère qu'on applique : est-ce que la couche d'abstraction sauve plus de complexité qu'elle n'en ajoute sur ce projet précis ? Si oui, on garde. Si non, on migre. Pas de dogme.
Ce qu'on fait sur les projets
Sur nos 3 projets clients migrés en 2025-2026, on n'a pas eu de regret. Sur les nouveaux projets IA depuis janvier 2026, on part directement en SDK Anthropic natif — LangChain n'arrive qu'en discussion si on identifie un des cas "Garder" plus haut.
Le repo opcodia/ai-evals montre le pattern complet :
classification + structured output + Zod parse + retry exponentiel + trace
Langfuse + Spend Limits côté serveur. Mehdi le met à jour à chaque release
majeure du SDK.
La doc Anthropic est devenue suffisamment riche en 2026 pour qu'on n'ait plus besoin de LangChain dans 90 % des cas. C'est cette bascule qu'on documente ici. À chaque équipe de juger sur ses propres 10 % restants.
