Construire des Agents IA Stateful avec LangGraph.js et TypeScript

La plupart des chatbots IA sont sans état — ils traitent un message et oublient. Les applications réelles ont besoin d'agents capables de mémoriser, raisonner et décider quoi faire en fonction du contexte.
LangGraph.js résout ce problème. Il vous offre un framework pour construire des agents IA sous forme de graphes orientés — chaque nœud représente une étape (appeler un LLM, utiliser un outil, prendre une décision) et les arêtes définissent le flux d'exécution. L'état traverse le graphe en accumulant du contexte à chaque étape.
Dans ce tutoriel, vous allez construire un agent IA complet capable de :
- Rechercher des informations sur le web
- Effectuer des calculs mathématiques
- Choisir les outils appropriés selon la question de l'utilisateur
- Conserver la mémoire de conversation entre les tours
- Gérer les erreurs de manière élégante
À la fin, vous disposerez d'un pattern d'agent prêt pour la production que vous pourrez étendre à n'importe quel cas d'usage.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (vérifiez avec
node --version) - Des bases en TypeScript (types, interfaces, async/await)
- Une clé API OpenAI (ou tout autre fournisseur — nous montrerons des alternatives)
- Une compréhension basique des LLMs et des prompts
- Un éditeur de code (VS Code recommandé)
Qu'est-ce que LangGraph.js ?
LangGraph.js est le portage JavaScript/TypeScript de LangGraph — une bibliothèque créée par l'équipe LangChain pour construire des workflows d'agents IA multi-étapes avec gestion d'état.
Pensez-y comme une machine à états pour l'IA :
| Concept | Signification |
|---|---|
| État (State) | Les données qui circulent à travers votre agent (messages, résultats, décisions) |
| Nœud (Node) | Une fonction qui reçoit l'état, exécute une action (appel LLM, outil), et retourne l'état mis à jour |
| Arête (Edge) | La connexion entre les nœuds — peut être fixe ou conditionnelle |
| Graphe (Graph) | Le workflow complet : nœuds + arêtes + schéma d'état |
Pourquoi ne pas simplement chaîner des appels de fonctions ? Parce que les vrais agents ont besoin de logique de branchement. Un agent peut avoir besoin d'appeler un outil, vérifier le résultat, appeler un autre outil, puis formuler une réponse. LangGraph rend cela explicite et facile à déboguer.
Étape 1 : Configuration du projet
Créez un nouveau projet et installez les dépendances :
mkdir ai-agent-langgraph && cd ai-agent-langgraph
npm init -y
npm install @langchain/langgraph @langchain/openai @langchain/core zod
npm install -D typescript tsx @types/nodeInitialisez TypeScript :
npx tsc --initMettez à jour votre tsconfig.json :
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true
},
"include": ["src/**/*"]
}Créez un fichier .env pour votre clé API :
OPENAI_API_KEY=sk-your-key-hereStructure du projet :
ai-agent-langgraph/
├── src/
│ ├── agent.ts # Graphe principal de l'agent
│ ├── tools.ts # Définitions des outils
│ ├── state.ts # Schéma d'état
│ └── index.ts # Point d'entrée
├── .env
├── tsconfig.json
└── package.json
Étape 2 : Définir l'état de l'agent
L'état est la colonne vertébrale de votre agent. Chaque nœud lit et écrit dedans.
Créez src/state.ts :
import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
// Définir le schéma d'état pour notre agent
export const AgentState = Annotation.Root({
// Les messages s'accumulent au fil de la conversation
...MessagesAnnotation.spec,
// Suivre les appels d'outils (pour le débogage)
toolCallCount: Annotation<number>({
reducer: (current, update) => (update ?? current ?? 0),
default: () => 0,
}),
// Réponse finale de l'agent
finalAnswer: Annotation<string>({
reducer: (current, update) => update ?? current ?? "",
default: () => "",
}),
});
export type AgentStateType = typeof AgentState.State;Concepts clés :
MessagesAnnotation— Une annotation intégrée qui gère l'accumulation des messages. Les nouveaux messages sont automatiquement ajoutés à la liste.Annotation— Définit un champ d'état typé avec unreducer(comment fusionner les mises à jour) et une valeur par défaut.- Réducteurs (Reducers) — Fonctions qui déterminent comment fusionner les nouvelles valeurs avec l'état existant.
💡 Le pattern de réducteur est ce qui rend LangGraph puissant. Chaque nœud peut retourner une mise à jour partielle de l'état, et le framework sait comment la fusionner correctement.
Étape 3 : Créer les outils
Les outils sont des fonctions que l'IA peut appeler. LangGraph utilise le format d'outils de LangChain — vous définissez le nom, la description, le schéma d'entrée et l'implémentation.
Créez src/tools.ts :
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// Outil 1 : Calculatrice
export const calculatorTool = tool(
async ({ expression }) => {
try {
const sanitized = expression.replace(/[^0-9+\-*/().%\s]/g, "");
if (!sanitized || sanitized !== expression.trim()) {
return "Erreur : Expression invalide. Seuls les nombres et opérateurs de base (+, -, *, /, %) sont autorisés.";
}
const result = new Function(`return (${sanitized})`)();
return `Résultat : ${result}`;
} catch (error) {
return `Erreur de calcul : ${(error as Error).message}`;
}
},
{
name: "calculator",
description:
"Effectue des calculs mathématiques. L'entrée doit être une expression comme '2 + 2' ou '(10 * 5) / 3'.",
schema: z.object({
expression: z
.string()
.describe("L'expression mathématique à évaluer"),
}),
}
);
// Outil 2 : Recherche web (simulée pour ce tutoriel)
export const webSearchTool = tool(
async ({ query }) => {
console.log(`[Outil] Recherche web pour : "${query}"`);
const results = [
{
title: `Meilleur résultat pour : ${query}`,
snippet: `Informations complètes sur ${query}. Couvre les derniers développements et faits clés en 2026.`,
url: `https://example.com/search?q=${encodeURIComponent(query)}`,
},
];
return JSON.stringify(results, null, 2);
},
{
name: "web_search",
description:
"Recherche des informations actuelles sur le web. Utilisez-le quand vous avez besoin de faits, actualités ou données à jour.",
schema: z.object({
query: z.string().describe("La requête de recherche"),
}),
}
);
// Outil 3 : Date et heure
export const dateTimeTool = tool(
async ({ timezone }) => {
const now = new Date();
const formatter = new Intl.DateTimeFormat("fr-FR", {
timeZone: timezone || "UTC",
dateStyle: "full",
timeStyle: "long",
});
return formatter.format(now);
},
{
name: "get_current_datetime",
description:
"Obtient la date et l'heure actuelles. Vous pouvez spécifier un fuseau horaire comme 'Europe/Paris' ou 'Africa/Tunis'.",
schema: z.object({
timezone: z
.string()
.optional()
.describe("Nom de fuseau horaire IANA (par défaut : UTC)"),
}),
}
);
export const allTools = [calculatorTool, webSearchTool, dateTimeTool];⚠️ Note de sécurité : La calculatrice utilise un nettoyage basique. En production, utilisez un parseur mathématique sécurisé comme
mathjsau lieu denew Function().
Étape 4 : Construire le graphe de l'agent
C'est le cœur du tutoriel. Nous allons créer un graphe où :
- Le nœud LLM reçoit les messages et décide quoi faire
- S'il veut utiliser des outils → routage vers le nœud outils
- Le nœud outils exécute les outils et retourne les résultats
- Retour au nœud LLM pour traiter les résultats
- Quand le LLM a une réponse finale → fin
Créez src/agent.ts :
import { StateGraph, END, START } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import {
AIMessage,
HumanMessage,
SystemMessage,
} from "@langchain/core/messages";
import { AgentState } from "./state.js";
import { allTools } from "./tools.js";
// Initialiser le LLM avec liaison d'outils
const llm = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
}).bindTools(allTools);
// Prompt système définissant le comportement de l'agent
const SYSTEM_PROMPT = `Tu es un assistant IA utile avec accès à des outils.
Tes capacités :
- calculator : Pour tout calcul mathématique
- web_search : Pour trouver des informations actuelles en ligne
- get_current_datetime : Pour obtenir la date/heure actuelle
Directives :
- Utilise les outils quand tu as besoin de données factuelles ou de calculs
- Réfléchis étape par étape pour les questions complexes
- Si un outil retourne une erreur, explique-la à l'utilisateur
- Sois concis mais complet dans tes réponses finales`;
// Nœud 1 : Appeler le LLM
async function callModel(
state: typeof AgentState.State
): Promise<Partial<typeof AgentState.State>> {
const messages = [new SystemMessage(SYSTEM_PROMPT), ...state.messages];
const response = await llm.invoke(messages);
return { messages: [response] };
}
// Nœud 2 : Exécuter les outils
const toolNode = new ToolNode(allTools);
// Arête conditionnelle : continuer vers les outils ou terminer ?
function shouldContinue(
state: typeof AgentState.State
): "tools" | typeof END {
const lastMessage = state.messages[state.messages.length - 1];
if (
lastMessage instanceof AIMessage &&
lastMessage.tool_calls &&
lastMessage.tool_calls.length > 0
) {
return "tools";
}
return END;
}
// Construire le graphe
export function createAgentGraph() {
const graph = new StateGraph(AgentState)
.addNode("agent", callModel)
.addNode("tools", toolNode)
.addEdge(START, "agent")
.addConditionalEdges("agent", shouldContinue, {
tools: "tools",
[END]: END,
})
.addEdge("tools", "agent");
return graph.compile();
}Schéma visuel du graphe :
┌─────────┐
│ DÉBUT │
└────┬─────┘
│
▼
┌─────────┐ appels d'outils requis ┌─────────┐
│ Agent │ ──────────────────────► │ Outils │
│ (LLM) │ ◄────────────────────── │ (Exécut.)│
└────┬─────┘ retour des résultats └──────────┘
│
│ pas d'appels d'outils
▼
┌─────────┐
│ FIN │
└──────────┘
🚀 Besoin d'aide pour implémenter des agents IA dans votre produit ? Noqta conçoit des solutions IA pour les équipes qui veulent des résultats concrets, pas des prototypes.
Étape 5 : Exécuter l'agent
Créez src/index.ts :
import "dotenv/config";
import { HumanMessage } from "@langchain/core/messages";
import { createAgentGraph } from "./agent.js";
async function main() {
const agent = createAgentGraph();
console.log("🤖 Agent IA prêt. Testons quelques requêtes.\n");
// Test 1 : Calcul simple
console.log("--- Test 1 : Maths ---");
const result1 = await agent.invoke({
messages: [
new HumanMessage(
"Combien fait 15% de 2 340 ? Et ajoute 99 au résultat."
),
],
});
const lastMsg1 = result1.messages[result1.messages.length - 1];
console.log("Réponse :", lastMsg1.content, "\n");
// Test 2 : Recherche web
console.log("--- Test 2 : Recherche ---");
const result2 = await agent.invoke({
messages: [
new HumanMessage(
"Cherche les dernières tendances du développement TypeScript en 2026"
),
],
});
const lastMsg2 = result2.messages[result2.messages.length - 1];
console.log("Réponse :", lastMsg2.content, "\n");
// Test 3 : Utilisation multi-outils
console.log("--- Test 3 : Multi-outils ---");
const result3 = await agent.invoke({
messages: [
new HumanMessage(
"Quelle heure est-il à Tokyo ? Et calcule combien d'heures restent avant minuit là-bas."
),
],
});
const lastMsg3 = result3.messages[result3.messages.length - 1];
console.log("Réponse :", lastMsg3.content, "\n");
}
main().catch(console.error);Lancez-le :
npx tsx src/index.tsVous verrez l'agent raisonner sur chaque question, appeler les outils selon les besoins et retourner des réponses cohérentes.
Étape 6 : Ajouter la mémoire conversationnelle
Un agent sans état oublie tout après chaque invocation. Ajoutons une mémoire persistante pour que l'agent se souvienne des tours précédents.
Créez src/memory-agent.ts :
import "dotenv/config";
import { StateGraph, END, START, MemorySaver } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import {
AIMessage,
HumanMessage,
SystemMessage,
} from "@langchain/core/messages";
import { AgentState } from "./state.js";
import { allTools } from "./tools.js";
const memory = new MemorySaver();
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 }).bindTools(
allTools
);
const SYSTEM_PROMPT = `Tu es un assistant IA avec mémoire.
Tu te souviens des messages précédents dans la conversation.
Utilise les outils si nécessaire : calculator, web_search, get_current_datetime.`;
async function callModel(state: typeof AgentState.State) {
const messages = [new SystemMessage(SYSTEM_PROMPT), ...state.messages];
const response = await llm.invoke(messages);
return { messages: [response] };
}
function shouldContinue(state: typeof AgentState.State) {
const last = state.messages[state.messages.length - 1];
if (
last instanceof AIMessage &&
last.tool_calls &&
last.tool_calls.length > 0
) {
return "tools";
}
return END;
}
const agentWithMemory = new StateGraph(AgentState)
.addNode("agent", callModel)
.addNode("tools", new ToolNode(allTools))
.addEdge(START, "agent")
.addConditionalEdges("agent", shouldContinue, {
tools: "tools",
[END]: END,
})
.addEdge("tools", "agent")
.compile({ checkpointer: memory }); // ← Mémoire activée !
async function main() {
// Configuration avec un identifiant de conversation
const config = { configurable: { thread_id: "user-123" } };
// Tour 1
console.log("👤 Utilisateur : Je m'appelle Marie et je travaille dans une startup.");
const res1 = await agentWithMemory.invoke(
{
messages: [
new HumanMessage(
"Je m'appelle Marie et je travaille dans une startup."
),
],
},
config
);
console.log(
"🤖 Agent :",
res1.messages[res1.messages.length - 1].content
);
console.log();
// Tour 2 — l'agent devrait se souvenir du nom
console.log("👤 Utilisateur : Quel est mon nom ?");
const res2 = await agentWithMemory.invoke(
{ messages: [new HumanMessage("Quel est mon nom ?")] },
config
);
console.log(
"🤖 Agent :",
res2.messages[res2.messages.length - 1].content
);
console.log();
// Tour 3 — multi-étapes avec contexte mémorisé
console.log(
"👤 Utilisateur : Calcule le nombre de lettres dans mon prénom multiplié par 100."
);
const res3 = await agentWithMemory.invoke(
{
messages: [
new HumanMessage(
"Calcule le nombre de lettres dans mon prénom multiplié par 100."
),
],
},
config
);
console.log(
"🤖 Agent :",
res3.messages[res3.messages.length - 1].content
);
}
main().catch(console.error);Lancez-le :
npx tsx src/memory-agent.tsL'agent se souvient maintenant que l'utilisateur s'appelle Marie entre les tours. Le thread_id dans la config agit comme identifiant de session — des identifiants différents vous donnent des conversations isolées.
Conseil :
MemorySaverstocke l'état en mémoire (perdu au redémarrage). En production, utilisez un checkpointer persistant commePostgresSaverouSqliteSaverdes packages@langchain/langgraph-checkpoint-*.
Étape 7 : Gestion des erreurs et relances
Les agents en production doivent gérer les erreurs élégamment. Ajoutons un wrapper de relance et des limites de sécurité.
Créez src/utils.ts :
import { AIMessage } from "@langchain/core/messages";
// Wrapper de relance avec backoff exponentiel
export function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
return new Promise(async (resolve, reject) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await fn();
return resolve(result);
} catch (error) {
console.warn(
`Tentative ${attempt}/${maxRetries} échouée :`,
(error as Error).message
);
if (attempt === maxRetries) {
return reject(error);
}
await new Promise((r) =>
setTimeout(r, Math.pow(2, attempt) * 1000)
);
}
}
});
}
// Wrapper de timeout
export async function invokeWithTimeout(
agent: any,
input: any,
config: any,
timeoutMs = 30000
) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const result = await agent.invoke(input, {
...config,
signal: controller.signal,
});
return result;
} catch (error) {
if ((error as Error).name === "AbortError") {
throw new Error(
`L'agent a expiré après ${timeoutMs}ms. La requête est peut-être trop complexe.`
);
}
throw error;
} finally {
clearTimeout(timeout);
}
}
// Suivi de consommation de tokens
export function extractTokenUsage(result: any) {
const messages = result.messages || [];
let totalTokens = 0;
for (const msg of messages) {
if (msg instanceof AIMessage && msg.usage_metadata) {
totalTokens +=
(msg.usage_metadata.input_tokens || 0) +
(msg.usage_metadata.output_tokens || 0);
}
}
return totalTokens;
}Ces utilitaires vous donnent :
- Relance avec backoff exponentiel — gère les pannes temporaires d'API
- Protection par timeout — empêche les boucles infinies
- Suivi des tokens — surveille les coûts par invocation
Étape 8 : Streaming des réponses
Pour une meilleure expérience utilisateur, streamez la sortie de l'agent token par token au lieu d'attendre la réponse complète.
Créez src/stream.ts :
import "dotenv/config";
import { HumanMessage } from "@langchain/core/messages";
import { createAgentGraph } from "./agent.js";
async function streamAgent() {
const agent = createAgentGraph();
const input = {
messages: [
new HumanMessage(
"Explique l'informatique quantique simplement, puis calcule 2^64."
),
],
};
console.log("🤖 Streaming de la réponse :\n");
for await (const event of await agent.streamEvents(input, {
version: "v2",
})) {
if (event.event === "on_chat_model_stream") {
const chunk = event.data.chunk;
if (chunk.content) {
process.stdout.write(chunk.content);
}
}
if (event.event === "on_tool_start") {
console.log(`\n\n🔧 Appel d'outil : ${event.name}`);
console.log(` Entrée : ${JSON.stringify(event.data.input)}`);
}
if (event.event === "on_tool_end") {
console.log(` Résultat : ${event.data.output.content}\n`);
}
}
console.log("\n\n✅ Streaming terminé.");
}
streamAgent().catch(console.error);Étape 9 : Utiliser des fournisseurs LLM alternatifs
Pas lié à OpenAI ? LangGraph.js fonctionne avec tout modèle compatible LangChain :
Anthropic (Claude)
npm install @langchain/anthropicimport { ChatAnthropic } from "@langchain/anthropic";
const llm = new ChatAnthropic({
model: "claude-sonnet-4-20250514",
temperature: 0,
}).bindTools(allTools);Google Gemini
npm install @langchain/google-genaiimport { ChatGoogleGenerativeAI } from "@langchain/google-genai";
const llm = new ChatGoogleGenerativeAI({
model: "gemini-2.0-flash",
temperature: 0,
}).bindTools(allTools);Modèles locaux via Ollama
npm install @langchain/ollamaimport { ChatOllama } from "@langchain/ollama";
const llm = new ChatOllama({
model: "llama3.3",
temperature: 0,
}).bindTools(allTools);Conseil : En production, envisagez d'utiliser plusieurs fournisseurs avec une chaîne de repli. Si OpenAI tombe, basculez automatiquement vers Anthropic.
Étape 10 : Considérations pour la production
Avant de déployer votre agent, adressez ces points :
1. Limitation de débit
import { RateLimiter } from "limiter";
const limiter = new RateLimiter({
tokensPerInterval: 10,
interval: "minute",
});
async function rateLimitedInvoke(agent: any, input: any) {
await limiter.removeTokens(1);
return agent.invoke(input);
}2. Observabilité avec LangSmith
npm install langsmithLANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=your-langsmith-key
LANGCHAIN_PROJECT=my-ai-agentChaque invocation de l'agent est maintenant tracée — vous pouvez voir chaque exécution de nœud, appel d'outil et réponse LLM dans le tableau de bord LangSmith.
3. L'humain dans la boucle
Pour les actions sensibles (envoi d'emails, achats), ajoutez une étape d'approbation :
import { interrupt } from "@langchain/langgraph";
async function sensitiveToolNode(state: typeof AgentState.State) {
const lastMessage = state.messages[state.messages.length - 1];
const approval = interrupt({
action: "tool_call",
description: "L'agent veut effectuer une action sensible",
toolCalls: (lastMessage as any).tool_calls,
});
if (!approval.approved) {
return {
messages: [
new HumanMessage("L'action a été rejetée par l'utilisateur."),
],
};
}
const toolNode = new ToolNode(allTools);
return toolNode.invoke(state);
}4. Contrôle des coûts
Définissez un nombre maximal d'appels LLM par invocation :
const MAX_ITERATIONS = 10;
function shouldContinue(state: typeof AgentState.State) {
const aiMessages = state.messages.filter(
(m) => m instanceof AIMessage
);
if (aiMessages.length >= MAX_ITERATIONS) {
console.warn("Nombre maximal d'itérations atteint");
return END;
}
const last = state.messages[state.messages.length - 1];
if (last instanceof AIMessage && last.tool_calls?.length > 0) {
return "tools";
}
return END;
}Résumé
Vous avez construit un système complet d'agent IA avec LangGraph.js :
| Ce que vous avez construit | Pourquoi c'est important |
|---|---|
| Schéma d'état avec annotations | Flux de données typé à travers l'agent |
| Définitions d'outils avec schémas Zod | L'IA peut appeler des fonctions externes en sécurité |
| Workflow basé sur un graphe | Logique d'agent explicite et débogable |
| Routage conditionnel | L'agent choisit son propre chemin |
| Mémoire conversationnelle | Interactions multi-tours |
| Gestion d'erreurs et relances | Résilience en production |
| Streaming des réponses | Meilleure expérience utilisateur |
| Support multi-fournisseurs | Pas de verrouillage vendeur |
Points clés à retenir :
- Graphes > Chaînes — Quand votre agent a besoin de logique de branchement, LangGraph le rend explicite
- L'état est roi — Concevez votre schéma d'état soigneusement ; tout passe par lui
- Les outils ont besoin de schémas — Des outils bien décrits avec Zod aident le LLM à prendre de meilleures décisions
- La mémoire a besoin de persistance — Utilisez
PostgresSaverouSqliteSaveren production - Ajoutez toujours des limites de sécurité — Itérations max, timeouts et limitation de débit préviennent les catastrophes
Prochaines étapes
- Ajoutez plus d'outils (requêtes BDD, envoi d'emails, opérations fichiers)
- Implémentez des sous-graphes pour l'orchestration multi-agents
- Déployez comme API avec Express ou Hono
- Ajoutez le traçage LangSmith pour la surveillance en production
- Explorez LangGraph Studio pour le débogage visuel
💡 Prêt à passer de la lecture à l'action ? Contactez notre équipe pour concevoir et déployer des systèmes d'agents IA qui s'intègrent à votre stack existant.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Créer un Web Scraper Intelligent avec Playwright et l'API Claude en TypeScript
Apprenez à construire un scraper web intelligent qui utilise Playwright pour l'automatisation du navigateur et l'IA Claude pour extraire, nettoyer et structurer les données de n'importe quel site — sans sélecteurs CSS fragiles.

Introduction au Model Context Protocol (MCP)
Découvrez le Model Context Protocol (MCP), ses cas d'usage, ses avantages et comment construire et utiliser un serveur MCP avec TypeScript.

AI SDK 4.0 : Nouvelles Fonctionnalites et Cas d'Utilisation
Decouvrez les nouvelles fonctionnalites et cas d'utilisation d'AI SDK 4.0, incluant le support PDF, l'utilisation de l'ordinateur et plus encore.