Conception de Workflows Multi-Agents Autonomes avec LangGraph et Next.js
Architecturer des graphes LLM multi-agents avec état, canaux de state personnalisés, routage conditionnel, validation humaine et réponses en streaming dans Next.js.

Lors du développement d'applications d'IA en production dépassant les simples interfaces de chat, les chaînes LLM à prompt unique atteignent rapidement leurs limites architecturales. Les tâches d'ingénierie complexes — telles que les revues de code automatisées, la synthèse de recherches multi-sources ou le refactoring autonome de systèmes — nécessitent une délégation entre agents spécialisés, des boucles de révision itératives et une persistance stricte de l'état.
Les chaînes linéaires (A -> B -> C) échouent lorsque l'agent B produit des résultats inexacts ou lorsque l'étape C nécessite une vérification humaine avant de modifier des bases de données de production. Pour construire des réseaux d'agents résilients, les systèmes doivent prendre en charge des graphes d'état cycliques, un routage conditionnel et un checkpointing déterministe.
Dans ce guide, nous allons implémenter une architecture de graphe multi-agents avec état en utilisant LangGraph (TypeScript) et l'intégrer dans une application Next.js App Router avec du streaming SSE en temps réel.
L'Architecture : Graphes Multi-Agents avec État
Au lieu de forcer un seul LLM à jouer tous les rôles, un système multi-agents sépare les responsabilités en nœuds distincts reliés par un contexte d'état partagé.
+-----------------------+
| User Prompt |
+-----------------------+
|
v
+-----------------------+
| Supervisor Node |
+-----------------------+
/ | \
(Research) / | \ (Drafting)
v | v
+------------+ | +------------+
| Researcher | | | Writer |
| Agent | | | Agent |
+------------+ | +------------+
\ | /
\ v /
+-----------------------+
| Reviewer Node |
+-----------------------+
|
(Needs Revision?)
/ \
[YES / Retry] [NO / Complete]
/ \
v v
+-------------------+ +-------------------+
| Interrupt / Edit | | Final Output |
+-------------------+ +-------------------+Primitives Clés dans LangGraph :
- Canaux d'État (State Channels) : Mémoire partagée typée transmise entre les nœuds.
- Nœuds (Nodes) : Fonctions TypeScript isolées représentant des agents individuels ou des exécuteurs d'outils.
- Arêtes (Edges) : Connexions orientées transférant l'exécution entre les nœuds.
- Arêtes Conditionnelles (Conditional Edges) : Logique de routage dynamique basée sur des critères d'évaluation ou des seuils d'erreur.
- Checkpointers : Stockage d'état persistant permettant la mise en pause, la reprise et l'interruption pour validation humaine.
1. Définition de Canaux d'État Typés
Dans LangGraph, l'état est défini à l'aide de canaux annotés. Chaque nœud reçoit l'état actuel du graphe et renvoie une mise à jour partielle de l'état. Des fonctions de réduction déterminent comment les mises à jour fusionnent avec l'état existant.
Créez src/lib/agents/state.ts :
import { Annotation, BaseMessage } from "@langchain/core/messages";
export interface AgentTask {
id: string;
description: string;
assignedTo: "researcher" | "writer" | "reviewer";
status: "pending" | "in_progress" | "completed" | "failed";
result?: string;
}
// Define the central state annotation schema
export const AgentGraphAnnotation = Annotation.Root({
// Append new messages to conversation history
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
default: () => [],
}),
// Active task context
tasks: Annotation<AgentTask[]>({
reducer: (x, y) => y, // Overwrite with latest task list
default: () => [],
}),
// Quality rating produced by the Reviewer Agent (0 to 10)
reviewScore: Annotation<number>({
reducer: (_, y) => y,
default: () => 0,
}),
// Re-evaluation loop counter to prevent infinite recursion
iterationCount: Annotation<number>({
reducer: (x, y) => x + y,
default: () => 0,
}),
// Flag indicating human approval requirement
requiresApproval: Annotation<boolean>({
reducer: (_, y) => y,
default: () => false,
}),
});
export type AgentGraphState = typeof AgentGraphAnnotation.State;2. Implémentation des Nœuds d'Agent Spécialisés
Les nœuds sont des fonctions asynchrones qui exécutent une logique métier spécifique. Nous définissons ici trois nœuds : Researcher, Writer et Reviewer.
Créez src/lib/agents/nodes.ts :
import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
import type { AgentGraphState } from "./state";
const model = new ChatOpenAI({
modelName: "gpt-4o-mini",
temperature: 0.2,
});
// Node 1: Researcher Agent
export async function researcherNode(state: AgentGraphState) {
const lastMessage = state.messages[state.messages.length - 1];
const response = await model.invoke([
new SystemMessage(
"You are a Senior Technical Researcher. Gather structural facts, API constraints, and performance benchmarks."
),
new HumanMessage(lastMessage.content as string),
]);
return {
messages: [new AIMessage({ content: `[Researcher]: ${response.content}` })],
iterationCount: 1,
};
}
// Node 2: Writer Agent
export async function writerNode(state: AgentGraphState) {
const researchData = state.messages
.filter((m) => typeof m.content === "string" && m.content.startsWith("[Researcher]"))
.map((m) => m.content)
.join("\n");
const response = await model.invoke([
new SystemMessage(
"You are a Technical Writer. Synthesize research data into structured, concrete TypeScript guides."
),
new HumanMessage(`Synthesize the following research:\n${researchData}`),
]);
return {
messages: [new AIMessage({ content: `[Writer]: ${response.content}` })],
};
}
// Node 3: Reviewer Node (Evaluates output quality)
export async function reviewerNode(state: AgentGraphState) {
const draftMessage = state.messages.find(
(m) => typeof m.content === "string" && m.content.startsWith("[Writer]")
);
const evaluation = await model.invoke([
new SystemMessage(
"You are a Code Reviewer. Score the technical accuracy from 1 to 10. Respond ONLY with JSON: {\"score\": number, \"feedback\": string}"
),
new HumanMessage((draftMessage?.content as string) || ""),
]);
let score = 5;
try {
const parsed = JSON.parse(evaluation.content as string);
score = parsed.score;
} catch {
score = 6;
}
return {
reviewScore: score,
requiresApproval: score >= 8,
};
}3. Construction du StateGraph avec Arêtes Conditionnelles
Nous relions les nœuds dans un graphe cyclique en utilisant une fonction d'arête conditionnelle pour décider de rediriger vers le Writer pour révision ou de terminer l'exécution.
Créez src/lib/agents/graph.ts :
import { StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
import { AgentGraphAnnotation } from "./state";
import { researcherNode, writerNode, reviewerNode } from "./nodes";
// Router function determining graph execution path
function routeReviewOutcome(state: typeof AgentGraphAnnotation.State) {
// Prevent infinite iteration loops
if (state.iterationCount >= 3) {
return "finalize";
}
// If score meets threshold, proceed to human approval or completion
if (state.reviewScore >= 8) {
return "finalize";
}
// Otherwise, route back to Writer for revision
return "revision";
}
export function buildAgentGraph() {
const workflow = new StateGraph(AgentGraphAnnotation)
.addNode("researcher", researcherNode)
.addNode("writer", writerNode)
.addNode("reviewer", reviewerNode)
// Primary execution path
.addEdge(START, "researcher")
.addEdge("researcher", "writer")
.addEdge("writer", "reviewer")
// Conditional edge after evaluation
.addConditionalEdges("reviewer", routeReviewOutcome, {
revision: "writer",
finalize: END,
});
// Attach memory checkpointer for state retention across HTTP requests
const checkpointer = new MemorySaver();
return workflow.compile({
checkpointer,
});
}4. Streaming d'API Route Next.js avec Server-Sent Events
Pour fournir des mises à jour immédiates de l'interface pendant l'exécution des agents, nous diffusons les événements d'état depuis l'App Router Next.js en utilisant ReadableStream.
Créez src/app/api/agent/stream/route.ts :
import { NextRequest, NextResponse } from "next/server";
import { HumanMessage } from "@langchain/core/messages";
import { buildAgentGraph } from "@/lib/agents/graph";
export const runtime = "nodejs";
export async function POST(req: NextRequest) {
const { prompt, threadId } = await req.json();
if (!prompt || !threadId) {
return NextResponse.json({ error: "Missing prompt or threadId" }, { status: 400 });
}
const app = buildAgentGraph();
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
try {
const eventStream = await app.streamEvents(
{
messages: [new HumanMessage(prompt)],
},
{
version: "v2",
configurable: { thread_id: threadId },
}
);
for await (const event of eventStream) {
if (event.event === "on_chain_start") {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: "node_start", node: event.name })}\n\n`)
);
} else if (event.event === "on_chat_model_stream") {
const chunk = event.data?.chunk?.content;
if (chunk) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: "token", text: chunk })}\n\n`)
);
}
} else if (event.event === "on_chain_end" && event.name === "LangGraph") {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: "done", state: event.data?.output })}\n\n`)
);
}
}
} catch (err: any) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: "error", error: err.message })}\n\n`)
);
} finally {
controller.close();
}
},
});
return new NextResponse(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}5. Compromis et Considérations Architecturales
Bien que les topologies de graphes multi-agents apportent structure et résilience, les équipes d'ingénierie doivent prendre en compte des compromis opérationnels essentiels :
| Aspect | Chaîne LLM Unique | Graphe Multi-Agents |
|---|---|---|
| Latence | Faible (Appel unique) | Plus élevée (Appels séquentiels / parallèles multiples) |
| Coût | Consommation minimale de tokens | Plus élevé en raison de la duplication de contexte |
| Déterminisme | Faible (Forte variance sur des instructions complexes) | Élevé (Transitions d'état strictes et contrôle des limites) |
| Validation Humaine | Difficile à mettre en pause proprement | Checkpointers intégrés (interrupt()) |
Checklist pour la Production :
- Réentrance et Sérialisation : Assurez-vous que l'objet d'état peut être sérialisé en JSON sans perte de références lors de la sauvegarde dans Redis ou PostgreSQL.
- Disjoncteurs de Boucle : Imposez toujours des limites d'itération (
state.iterationCount >= N) dans les arêtes conditionnelles pour éviter une consommation illimitée de tokens en cas de boucle de révision défaillante. - Observabilité : Suivez les transitions du graphe avec des outils comme LangSmith ou OpenTelemetry afin de détecter les goulots d'étranglement de latence.
Conclusion
Les architectures multi-agents font évoluer l'ingénierie LLM de la génération de texte non contrainte vers des systèmes distribués prévisibles. En combinant LangGraph et Next.js, les équipes peuvent concevoir des graphes d'agents avec état et capacité d'autocorrection qui diffusent leur progression en temps réel tout en conservant un contrôle humain rigoureux.