Создание надежных оркестрируемых мультиагентных систем: циклические графы StateGraph, разделение ролей на узкоспециализированных агентов (Исследователь, Генератор, Критик), автоматические циклы саморефлексии (Reflection Loops) и точка прерывания для подтверждения человеком (Human-in-the-loop).
Одиночные линейные вызовы LLM (Single Prompt Pipeline) фундаментально ограничены при решении комплексных инженерных задач:
LangGraph переводит разработку AI-агентов на рельсы строгой теории графов и конечных автоматов. Состояние системы строго типизировано, узлы представляют собой изолированные функции, а переходы между агентами регулируются детерминированными условными ребрами (Conditional Edges) с поддержкой контрольных точек (Checkpoints).
Воркфлоу оркестрации основан на разделении обязанностей между тремя агентами с поддержкой точки прерывания (Breakpoint) перед публикацией результата:
+-------------------------------------------------------------------------------+
| LANGGRAPH STATEGRAPH ARCHITECTURE |
| |
| [ START: Задача от пользователя ] |
| | |
| v |
| +---------------------------+ |
| | Node 1: Researcher | |
| | Поиск фактов и сбор API | |
| +-------------+-------------+ |
| | |
| v |
| +---------------------------+ |
| | Node 2: Writer / Coder | |
| | Генерация черновика | |
| +-------------+-------------+ |
| | |
| v |
| +---------------------------+ |
| | Node 3: Critic / Auditor| |
| | Проверка качества и тесты| |
| +-------------+-------------+ |
| | |
| Conditional Edge: Is Approved? |
| / \ |
| (REVISE: score < 8) / \ (APPROVED: score >= 8)|
| v v |
| [ Return with Feedback ] +-------------------+ |
| Цикл рефлексии (max 3 раза) | Human-in-the-loop | |
| ^ | Пауза до аппрува | |
| | +---------+---------+ |
| +--- Node 2 (Writer) | (Operator OK) |
| v |
| [ END: Deploy ] |
+-------------------------------------------------------------------------------+Определение структуры общего контекста графа (messages, draft, critique, iterationCount, approved) с использованием редьюсеров каналов.
Создание специализированных узлов с узкими промптами и системными инструкциями для исследователя, генератора и ревьюера.
Декларация логики роутинга: если оценка критика ниже порога — возврат автору с правками; иначе — переход к шагу оператора.
Подключение MemorySaver для персистенции шагов и указание interruptBefore: ['human_approval'] для остановки выполнения.
Прием подтверждения оператора через UI и вызов graph.invoke(null, { configurable: { thread_id } }) для финализации.
Зачем в аннотации состояния LangGraph для поля messages используется функция-редьюсер (например, messagesStateReducer или Annotation<BaseMessage[]>({ reducer: ... }))?
Ниже представлена промышленная реализация мультиагентного графа ревизии технической статьи с циклами саморефлексии и подтверждением оператора.
import { StateGraph, Annotation, END, START, MemorySaver } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { HumanMessage, SystemMessage, AIMessage, BaseMessage } from '@langchain/core/messages';
// 1. Декларация типобезопасного общего состояния графа
export const EditorialState = Annotation.Root({
topic: Annotation<string>(),
researchNotes: Annotation<string>(),
draft: Annotation<string>(),
critique: Annotation<string>(),
qualityScore: Annotation<number>(),
iterations: Annotation<number>({
reducer: (curr, update) => (update !== undefined ? update : curr + 1),
default: () => 0
}),
isApproved: Annotation<boolean>({
reducer: (_, update) => update,
default: () => false
})
});
// Инициализация LLM-модели (Google Gemini 2.5 Flash / Pro)
const model = new ChatGoogleGenerativeAI({
model: 'gemini-2.5-pro',
apiKey: process.env.GOOGLE_GEMINI_API_KEY!,
temperature: 0.2
});
// 2. Узел 1: Агент-исследователь (Researcher)
async function researchNode(state: typeof EditorialState.State) {
const prompt = [
new SystemMessage('Ты ведущий технический архитектор. Собери 3 ключевых инженерных факта и архитектурные инварианты по теме.'),
new HumanMessage(`Тема: ${state.topic}`)
];
const response = await model.invoke(prompt);
return { researchNotes: String(response.content) };
}
// 3. Узел 2: Агент-автор (Writer)
async function writerNode(state: typeof EditorialState.State) {
const prompt = [
new SystemMessage('Ты технический писатель Senior уровня. Напиши четкое руководство на основе заметок исследователя. Если есть критика от аудитора, исправь все указанные недочеты.'),
new HumanMessage(
`Заметки исследователя:\n${state.researchNotes}\n\n` +
(state.critique ? `Замечания критика с прошлой итерации:\n${state.critique}\nТекущий черновик:\n${state.draft}` : '')
)
];
const response = await model.invoke(prompt);
return {
draft: String(response.content),
iterations: state.iterations + 1
};
}
// 4. Узел 3: Агент-критик и аудитор качества (Critic)
async function criticNode(state: typeof EditorialState.State) {
const prompt = [
new SystemMessage(
'Ты строгий технический рецензент. Оцени черновик по шкале от 1 до 10. ' +
'Верни JSON строгого формата: {"score": number, "feedback": "подробные замечания"}. Никакого лишнего текста!'
),
new HumanMessage(`Черновик статьи:\n${state.draft}`)
];
const response = await model.invoke(prompt);
const text = String(response.content).replace(/```json|```/g, '').trim();
let score = 5;
let feedback = 'Требуется доработка';
try {
const parsed = JSON.parse(text);
score = Number(parsed.score) || 5;
feedback = String(parsed.feedback) || 'Общие замечания';
} catch (err: unknown) {
console.error('Ошибка парсинга ответа критика:', err);
}
return {
qualityScore: score,
critique: feedback
};
}
// 5. Узел подтверждения человеком (Human Approval Node)
async function humanApprovalNode(state: typeof EditorialState.State) {
console.log(`[HUMAN CHECKPOINT] Черновик одобрен AI с оценкой ${state.qualityScore}/10. Ожидание подписи оператора.`);
return { isApproved: true };
}
// 6. Функция условного перехода (Conditional Edge Router)
function shouldContinue(state: typeof EditorialState.State) {
const { qualityScore, iterations } = state;
// Если оценка высокая (>= 8) или исчерпан лимит 3 итераций -> переход к человеку
if (qualityScore >= 8 || iterations >= 3) {
return 'human_approval';
}
// Иначе отправляем черновик обратно автору на доработку
return 'revise';
}
// 7. Сборка графа StateGraph
const workflow = new StateGraph(EditorialState)
.addNode('researcher', researchNode)
.addNode('writer', writerNode)
.addNode('critic', criticNode)
.addNode('human_approval', humanApprovalNode)
// Связи между узлами
.addEdge(START, 'researcher')
.addEdge('researcher', 'writer')
.addEdge('writer', 'critic')
.addConditionalEdges('critic', shouldContinue, {
revise: 'writer',
human_approval: 'human_approval'
})
.addEdge('human_approval', END);
// 8. Компиляция с персистентным хранилищем и точкой прерывания
const checkpointer = new MemorySaver();
export const editorialAgentGraph = workflow.compile({
checkpointer,
interruptBefore: ['human_approval'] // КРИТИЧНО: Пауза перед публикацией для одобрения человеком!
});import { editorialAgentGraph } from './editorial-graph';
async function main() {
const threadConfig = {
configurable: { thread_id: 'editorial-session-2026-001' }
};
console.log('--- Старт выполнения мультиагентного графа ---');
// Фаза 1: Автономная работа агентов до точки прерывания
const initialResult = await editorialAgentGraph.invoke(
{ topic: 'Архитектура React 19 Server Components' },
threadConfig
);
console.log('--- Граф приостановлен на точке human_approval ---');
console.log('Текущая оценка критика:', initialResult.qualityScore);
console.log('Черновик:', initialResult.draft.slice(0, 200) + '...');
// Имитация действия живого оператора в дашборде
console.log('Оператор нажимает "Одобрить к публикации"...');
// Фаза 2: Возобновление выполнения с сохраненной контрольной точки
const finalResult = await editorialAgentGraph.invoke(null, threadConfig);
console.log('--- Пайплайн успешно завершен! ---');
console.log('Финальный статус isApproved:', finalResult.isApproved);
}Если критик выставляет строгие требования, а писатель раз за разом не может набрать нужный балл, граф войдет в бесконечный цикл взаимных правок, моментально исчерпав баланс токенов API. Всегда ограничивайте число циклов (например, iterations >= 3) в условии условного перехода.
Передача всей сырой переписки в системный промпт каждого узла без очистки раздувает контекстное окно до сотен тысяч токенов. Выделяйте в состояние только структурированные результаты работы (researchNotes, draft, critique), а не плоский лог служебных сообщений.
При работе с контрольными точками и механизмом interruptBefore вызов graph.invoke() без передачи уникального { configurable: { thread_id: '...' } } приведет к тому, что граф не сможет восстановить прерванное состояние и начнет выполнение с самого первого узла.
Какая возможность появляется в LangGraph благодаря персистентному сохранению каждого состояния через Checkpointer?