Retour au Journal
8 min de lecture

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.

LangGraphNext.jsTypeScriptMulti-Agent SystemsAI Architecture
Conception de Workflows Multi-Agents Autonomes avec LangGraph et 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 :

  1. Canaux d'État (State Channels) : Mémoire partagée typée transmise entre les nœuds.
  2. Nœuds (Nodes) : Fonctions TypeScript isolées représentant des agents individuels ou des exécuteurs d'outils.
  3. Arêtes (Edges) : Connexions orientées transférant l'exécution entre les nœuds.
  4. Arêtes Conditionnelles (Conditional Edges) : Logique de routage dynamique basée sur des critères d'évaluation ou des seuils d'erreur.
  5. 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 :

  1. 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.
  2. 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.
  3. 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.

Share this article