Construisez un chatbot intelligent avec LangChain.js : mémoire conversationnelle, chaînes de traitement, agents LLM et intégration d'outils externes.
Installation et architecture modulaire
LangChain.js est le portage JavaScript/TypeScript du framework Python LangChain. Il est entièrement modulaire: vous n'installez que les packages des providers que vous utilisez.
# Core + provider OpenAI
npm install langchain @langchain/openai @langchain/core
# Provider Claude (Anthropic)
npm install @langchain/anthropic
# Provider Gemini (Google)
npm install @langchain/google-genai
# Vector stores
npm install @langchain/community # Chroma, Pinecone, pgvector, etc.
# Ou Chroma standalone
npm install chromadb @langchain/community
@langchain/core (abstractions), langchain (chaînes de haut niveau), et des packages provider séparés. Cela permet de changer de LLM (OpenAI → Claude → Gemini) sans modifier le reste du code — uniquement l'import du modèle change.
LLM et multi-provider
Tous les modèles LangChain.js partagent la même interface — invoke(), stream(), batch(). Changer de provider se fait en une ligne.
import { ChatOpenAI } from '@langchain/openai';
import { ChatAnthropic } from '@langchain/anthropic';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
// Tous partagent la même interface BaseChatModel
// OpenAI GPT-4o
const openai = new ChatOpenAI({
model: 'gpt-4o',
temperature: 0.3,
maxTokens: 2048,
});
// Claude Sonnet (Anthropic)
const claude = new ChatAnthropic({
model: 'claude-sonnet-4-6',
temperature: 0,
maxTokens: 4096,
});
// Gemini Flash (Google)
const gemini = new ChatGoogleGenerativeAI({
model: 'gemini-2.0-flash',
temperature: 0.5,
});
// La même requête fonctionne avec tous les providers
const question = 'Explique les Signals Angular en 3 points.';
const [r1, r2, r3] = await Promise.all([
openai.invoke(question),
claude.invoke(question),
gemini.invoke(question),
]);
// Comparer les réponses des différents modèles
console.log('OpenAI:', r1.content);
console.log('Claude:', r2.content);
console.log('Gemini:', r3.content);
Prompt Templates LCEL
LCEL (LangChain Expression Language) permet de composer des chaînes avec l'opérateur | (pipe). C'est la base de tout pipeline LangChain.js moderne.
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { ChatOpenAI } from '@langchain/openai';
import { StringOutputParser, JsonOutputParser } from '@langchain/core/output_parsers';
import { RunnablePassthrough } from '@langchain/core/runnables';
// Template avec variables
const codeReviewPrompt = ChatPromptTemplate.fromMessages([
['system', `Tu es un architecte Angular senior.
Analyse le code TypeScript fourni et retourne une revue structurée.
Réponds en JSON avec: { score: number, issues: string[], suggestions: string[] }`],
['human', 'Code à analyser:\n\`\`\`typescript\n{code}\n\`\`\`'],
]);
const model = new ChatOpenAI({ model: 'gpt-4o', temperature: 0 });
// Chain: prompt → model → parser JSON
const reviewChain = codeReviewPrompt
.pipe(model)
.pipe(new JsonOutputParser());
const review = await reviewChain.invoke({
code: `
@Component({ selector: 'app', template: '{{ getUser().name }}' })
class AppComponent {
getUser() { return this.http.get('/api/user'); } // appel HTTP dans le template!
}
`
});
console.log(review);
// { score: 2, issues: ["HTTP call in template causes N requests"], suggestions: [...] }
RunnableParallel: exécuter plusieurs chains en parallèle
import { RunnableParallel } from '@langchain/core/runnables';
// Analyser le code de 3 angles différents en parallèle
const parallelAnalysis = RunnableParallel.from({
security: ChatPromptTemplate.fromTemplate('Analyse la sécurité de: {code}')
.pipe(model).pipe(new StringOutputParser()),
performance: ChatPromptTemplate.fromTemplate('Analyse les performances de: {code}')
.pipe(model).pipe(new StringOutputParser()),
maintainability: ChatPromptTemplate.fromTemplate('Analyse la maintenabilité de: {code}')
.pipe(model).pipe(new StringOutputParser()),
});
const results = await parallelAnalysis.invoke({ code: componentSourceCode });
// { security: "...", performance: "...", maintainability: "..." }
Mémoire conversationnelle
RunnableWithMessageHistory gère automatiquement le chargement et la sauvegarde de l'historique à chaque appel, par identifiant de session.
import { ChatOpenAI } from '@langchain/openai';
import { ChatMessageHistory } from 'langchain/stores/message/in_memory';
import { RunnableWithMessageHistory } from '@langchain/core/runnables';
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
const model = new ChatOpenAI({ model: 'gpt-4o', temperature: 0.7 });
const prompt = ChatPromptTemplate.fromMessages([
['system', 'Tu es un assistant développeur Angular expert. Session: {sessionId}'],
new MessagesPlaceholder('history'), // l'historique est injecté ici
['human', '{input}'],
]);
const chain = prompt.pipe(model).pipe(new StringOutputParser());
// Sessions stockées en mémoire (Map)
const sessions = new Map<string, ChatMessageHistory>();
const chatbot = new RunnableWithMessageHistory({
runnable: chain,
getMessageHistory: (sessionId: string) => {
if (!sessions.has(sessionId)) {
sessions.set(sessionId, new ChatMessageHistory());
}
return sessions.get(sessionId)!;
},
inputMessagesKey: 'input',
historyMessagesKey: 'history',
});
// Conversation multi-tours avec mémoire
const config = { configurable: { sessionId: 'user-alice' } };
const r1 = await chatbot.invoke(
{ input: 'Je travaille sur une app Angular avec NgRx', sessionId: 'user-alice' },
config
);
const r2 = await chatbot.invoke(
{ input: 'Quelle version de NgRx recommandes-tu pour Angular 20?', sessionId: 'user-alice' },
config
);
// Le modèle connaît le contexte de la question précédente
Gestion de la longueur d'historique
// Tronquer l'historique pour éviter de dépasser la fenêtre de contexte
import { trimMessages, HumanMessage, AIMessage } from '@langchain/core/messages';
// Garder uniquement les N derniers messages (fenêtre glissante)
const getHistory = (sessionId: string) => {
const history = sessions.get(sessionId) ?? new ChatMessageHistory();
// Wrapper qui tronque automatiquement à 20 messages
return {
getMessages: async () => {
const messages = await history.getMessages();
// Garder le premier message système + les 20 derniers
return trimMessages(messages, {
maxTokens: 4000,
strategy: 'last',
tokenCounter: (msgs) => msgs.length * 100, // estimation simple
allowPartial: false,
});
},
addMessages: history.addMessages.bind(history),
clear: history.clear.bind(history),
};
};
Chains avancées et structured output
Structured output avec Zod
import { z } from 'zod';
import { ChatOpenAI } from '@langchain/openai';
// Définir le schéma de sortie avec Zod
const CodeReviewSchema = z.object({
score: z.number().min(1).max(10).describe('Score de qualité de 1 à 10'),
issues: z.array(z.object({
severity: z.enum(['critical', 'high', 'medium', 'low']),
description: z.string(),
line: z.number().optional(),
})).describe('Liste des problèmes détectés'),
suggestions: z.array(z.string()).describe('Améliorations recommandées'),
verdict: z.enum(['approve', 'request_changes', 'comment']),
});
type CodeReview = z.infer<typeof CodeReviewSchema>;
const model = new ChatOpenAI({ model: 'gpt-4o', temperature: 0 });
// withStructuredOutput: force la sortie à correspondre au schéma Zod
const structuredModel = model.withStructuredOutput(CodeReviewSchema);
const review: CodeReview = await structuredModel.invoke([
{
role: 'system',
content: 'Tu es un expert en code Angular. Revue le code fourni.',
},
{
role: 'user',
content: `Code: \`\`\`typescript\n${sourceCode}\n\`\`\``,
},
]);
// Résultat garanti typé TypeScript
console.log(review.score); // number
console.log(review.issues); // Array<{severity, description}>
console.log(review.verdict); // 'approve' | 'request_changes' | 'comment'
RAG complet avec chunking et vector store
Le RAG (Retrieval-Augmented Generation) permet au chatbot de répondre en s'appuyant sur vos propres documents. Voici un pipeline complet: chargement → découpage → embedding → stockage → recherche → génération.
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
import { OpenAIEmbeddings } from '@langchain/openai';
import { Document } from '@langchain/core/documents';
import { createRetrievalChain } from 'langchain/chains/retrieval';
import { createStuffDocumentsChain } from 'langchain/chains/combine_documents';
import { ChatPromptTemplate } from '@langchain/core/prompts';
// Étape 1: Charger et découper les documents
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000, // 1000 caractères par chunk
chunkOverlap: 200, // 200 caractères de chevauchement (préserve le contexte)
separators: ['\n\n', '\n', '. ', ' ', ''],
});
const rawDocs = [
new Document({
pageContent: longDocumentationText,
metadata: { source: 'angular-docs.md', type: 'documentation' },
}),
];
const splitDocs = await splitter.splitDocuments(rawDocs);
console.log(`${splitDocs.length} chunks créés`);
// Étape 2: Générer les embeddings et stocker dans le vector store
const embeddings = new OpenAIEmbeddings({
model: 'text-embedding-3-small', // 1536 dimensions, rapide et économique
});
// En mémoire pour dev/test
const vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, embeddings);
// En production: Chroma (local) ou Pinecone (cloud)
// import { Chroma } from '@langchain/community/vectorstores/chroma';
// const vectorStore = await Chroma.fromDocuments(splitDocs, embeddings, {
// collectionName: 'angular-docs',
// url: 'http://localhost:8000',
// });
// Étape 3: Créer le retriever (recherche sémantique)
const retriever = vectorStore.asRetriever({
k: 4, // retourner les 4 documents les plus pertinents
searchType: 'similarity', // 'similarity' | 'mmr' (diversité)
});
// Étape 4: Construire la chain RAG
const ragPrompt = ChatPromptTemplate.fromMessages([
['system', `Tu es un assistant expert en développement Angular.
Réponds UNIQUEMENT en te basant sur les documents fournis.
Si la réponse ne se trouve pas dans les documents, dis-le clairement.
Documents pertinents:
{context}`],
['human', '{input}'],
]);
const model = new ChatOpenAI({ model: 'gpt-4o', temperature: 0 });
const documentChain = await createStuffDocumentsChain({
llm: model,
prompt: ragPrompt,
});
const ragChain = await createRetrievalChain({
retriever,
combineDocsChain: documentChain,
});
// Étape 5: Utilisation
const response = await ragChain.invoke({
input: 'Comment utiliser linkedSignal dans Angular 19?',
});
console.log('Réponse:', response.answer);
console.log('Sources:', response.context.map(d => d.metadata.source));
Streaming SSE avec Express
Le streaming permet d'afficher la réponse token par token, améliorant considérablement l'UX pour les réponses longues.
// Backend Express: endpoint SSE pour le streaming
import express from 'express';
import { ChatOpenAI } from '@langchain/openai';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
const router = express.Router();
const model = new ChatOpenAI({ model: 'gpt-4o', temperature: 0.7 });
router.post('/chat/stream', async (req, res) => {
const { message, sessionId } = req.body;
// Configurer les headers SSE
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
});
const chain = ChatPromptTemplate.fromMessages([
['system', 'Tu es un assistant développeur Angular.'],
['human', '{input}'],
]).pipe(model).pipe(new StringOutputParser());
try {
// Stream les tokens au fur et à mesure
const stream = await chain.stream({ input: message });
for await (const chunk of stream) {
// Format SSE: "data: content\n\n"
res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
} catch (error) {
res.write(`data: ${JSON.stringify({ error: String(error) })}\n\n`);
res.end();
}
});
Tool calling et agents
LangChain.js permet de donner au LLM des outils (fonctions) qu'il peut appeler pour enrichir ses réponses avec des données réelles.
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { ChatOpenAI } from '@langchain/openai';
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';
// Définir des outils avec Zod schema
const searchCodeTool = tool(
async ({ query, language }) => {
// Appel à votre API de recherche de code (ex: GitHub Code Search)
const results = await codeSearchAPI.search({ query, language });
return JSON.stringify(results.slice(0, 3));
},
{
name: 'search_code',
description: 'Recherche des exemples de code dans la base de données',
schema: z.object({
query: z.string().describe('La requête de recherche de code'),
language: z.enum(['typescript', 'angular', 'javascript']).describe('Le langage'),
}),
}
);
const getDocumentationTool = tool(
async ({ topic, version }) => {
const doc = await fetchAngularDoc(topic, version);
return doc.summary;
},
{
name: 'get_angular_documentation',
description: 'Récupère la documentation officielle Angular pour un sujet',
schema: z.object({
topic: z.string().describe('Le sujet Angular (ex: signals, defer, httpResource)'),
version: z.string().default('20').describe('La version Angular'),
}),
}
);
// Créer l'agent avec les outils
const model = new ChatOpenAI({ model: 'gpt-4o', temperature: 0 });
const tools = [searchCodeTool, getDocumentationTool];
const prompt = ChatPromptTemplate.fromMessages([
['system', 'Tu es un assistant Angular expert. Utilise les outils disponibles pour répondre précisément.'],
['human', '{input}'],
new MessagesPlaceholder('agent_scratchpad'),
]);
const agent = await createToolCallingAgent({ llm: model, tools, prompt });
const executor = new AgentExecutor({ agent, tools, verbose: true });
const result = await executor.invoke({
input: 'Montre-moi un exemple de httpResource() avec gestion d\'erreur en Angular 20',
});
console.log(result.output);
// L'agent a cherché dans la doc + les exemples de code pour répondre