Intelligence Artificielle angularforall.com

- LangChain.js : créer un chatbot IA avec mémoire

LangchainJavascriptChatbotLlmLangchain-JsMemoire-ConversationnelleAgents-IaOpenaiRagPromptsNode-JsIa-GenerativeTool-Use
LangChain.js : créer un chatbot IA avec mémoire

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
Architecture LangChain.js 2026 : Le framework est divisé en @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

Partager