Создание автономных мультиагентных рабочих процессов с LangGraph и Next.js
Архитектура мультиагентных LLM-графов с сохранением состояния, пользовательскими каналами состояния, условной маршрутизацией, проверкой человеком и потоковым ответом в Next.js.

При разработке продакшен-решений ИИ, выходящих за рамки простых интерфейсов чата, линейные цепочки LLM быстро упираются в архитектурные ограничения. Сложные инженерные задачи — такие как автоматическое проведение ревью кода, синтез исследований из нескольких источников или автономный рефакторинг систем — требуют делегирования задач между специализированными агентами, итеративных циклов проверки и строгого сохранения состояния.
Линейные цепочки (A -> B -> C) дают сбой, когда агент B выдает неточный результат или когда шаг C требует подтверждения человеком перед изменением базовых данных в продакшене. Чтобы создавать отказоустойчивые агентные сети, системы должны поддерживать циклические графы состояния, условную маршрутизацию и детерминированные контрольные точки (checkpointing).
В этом руководстве мы реализуем архитектуру графа мультиагентов с сохранением состояния с использованием LangGraph (TypeScript) и интегрируем ее в приложение Next.js App Router с потоковой передачей SSE в реальном времени.
Архитектура: Мультиагентные графы с сохранением состояния
Вместо того чтобы заставлять одну модель LLM выполнять все роли, мультиагентная система разделяет обязанности между отдельными узлами, связанными общим контекстом состояния.
+-----------------------+
| 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 |
+-------------------+ +-------------------+Ключевые примитивы в LangGraph:
- Каналы состояния (State Channels): Строго типизированная общая память, передаваемая между узлами.
- Узлы (Nodes): Изолированные функции TypeScript, представляющие отдельных агентов или исполнителей инструментов.
- Ребра (Edges): Направленные связи, передающие выполнение между узлами.
- Условные ребра (Conditional Edges): Динамическая логика маршрутизации на основе критериев оценки или пороговых значений ошибок.
- Контрольные точки (Checkpointers): Персистентное хранилище состояния, позволяющее приостанавливать, возобновлять и запрашивать подтверждение человека.
1. Определение строго типизированных каналов состояния
В LangGraph состояние определяется с использованием аннотированных каналов. Каждый узел получает текущее состояние графа и возвращает частичное обновление состояния. Редукторы определяют, как обновления объединяются с существующим состоянием.
Создайте 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. Реализация специализированных узлов агентов
Узлы — это асинхронные функции, выполняющие предметно-ориентированную логику. Здесь мы определяем три узла: Researcher, Writer и Reviewer.
Создайте 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. Построение StateGraph с условными ребрами
Мы объединяем узлы в циклический граф с помощью функции условного ребра, чтобы решить, перенаправлять ли запрос обратно Writer для доработки или завершить выполнение.
Создайте 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. Потоковая передача API маршрута Next.js с помощью Server-Sent Events
Чтобы обеспечивать мгновенное обновление интерфейса во время работы агентов, мы передаем события состояния в потоковом режиме из Next.js App Router с помощью ReadableStream.
Создайте 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. Компромиссы и архитектурные соображения
Хотя мультиагентные топологии графов обеспечивают четкую структуру и высокую надежность, инженерным командам необходимо учитывать важные эксплуатационные компромиссы:
| Аспект | Одиночная цепочка LLM | Мультиагентный граф |
|---|---|---|
| Задержка (Latency) | Низкая (Одиночный запрос) | Выше (Множественные последовательные / параллельные вызовы) |
| Стоимость (Cost) | Минимальный расход токенов | Выше из-за дублирования контекста между узлами |
| Детерминированность | Низкая (Высокая вариативность в сложных инструкциях) | Высокая (Строгие переходы состояний и контроль ограничений) |
| Проверка человеком | Трудно корректно приостановить | Встроенные контрольные точки (interrupt()) |
Чек-лист для продакшена:
- Повторный вход и сериализация: Убедитесь, что объект состояния канала может сериализоваться в JSON без потери ссылок на символы при сохранении в Redis или PostgreSQL.
- Ограничители циклов: Всегда задавайте максимальные лимиты итераций (
state.iterationCount >= N) в условных ребрах, чтобы предотвратить бесконечный расход токенов в ошибочных циклах повтора. - Наблюдаемость (Observability): Отслеживайте переходы графа с помощью таких инструментов, как LangSmith или OpenTelemetry, для выявления узких мест задержки на узлах.
Заключение
Мультиагентные архитектуры переводят инженерию LLM от неконтролируемой генерации текста к предсказуемым распределенным системам. Используя LangGraph вместе с Next.js, команды могут создавать самокорректирующиеся графы агентов с сохранением состояния, которые передают прогресс в реальном времени под строгим контролем человека.