LangGraph без лишней теории: от проблем одиночного агента к практической оркестрации нескольких агентов
Статья начинается с проблем одиночного агента — больших затрат контекста, информационных помех и невозможности параллельной работы. Автор показывает, что разделение на несколько агентов снижает расход токенов и повышает точность, и приводит формулу: агент = LLM + Harness.
Отталкиваясь от проблем контекста и параллелизма у одиночного агента, автор на пяти рабочих примерах разбирает состояния, маршрутизацию, циклы, персистентность и ручное прерывание в LangGraph.
I. Зачем нам нужны мультиагентные системы?
По мере углубления применения больших моделей сложные агентные продукты почти всегда строятся на мультиагентной архитектуре. В разработке мы постоянно сталкиваемся с болями, которые создаёт одиночный агент:
- Огромные накладные расходы на контекст: в архитектуре с одним агентом описания всех инструментов и промпты каждой функции свалены в системный промпт.
- Сильные информационные помехи: для выполнения конкретной функции на самом деле нужна лишь часть промпта, но одиночный агент каждый раз тащит весь контекст. Это не только приводит к более высокому расходу токенов, но что ещё хуже — большое количество нерелевантной информации снижает эффективность рассуждений модели и повышает вероятность ошибок.
- Сложности с параллелизмом: у одной LLM только один «мозг», она может думать лишь шаг за шагом и не способна по-настоящему работать параллельно.
Как разбить на несколько агентов?
Каждый подагент хранит только нужный ему промпт. При выполнении конкретной функции расходуется меньше токенов, нет помех от нерелевантной информации — и точность, естественно, выше.
Три ключевых преимущества мультиагентной системы
- Высокая точность решений и меньший расход токенов: хотя число вызовов LLM может вырасти, каждый раз передаётся только необходимый минимальный промпт без лишней информации — и совокупная стоимость, наоборот, снижается.
- Параллельное мышление и обработка задач: главный агент раздаёт задачи, подагенты обрабатывают их параллельно (например, агент-программист пишет код, а агент-тестировщик синхронно пишет тесты в духе TDD) — общая эффективность выше.
- Обсуждение между несколькими ролями и более сильная самокоррекция: заимствуя «судебный» режим AutoGen, за счёт дебатов между ролями можно значительно повысить надёжность результата.
Формула агента: Agent = LLM + Harness (tool + mcp + rag + skill + ...).
II. LangChain vs LangGraph: от линейности к сетевой структуре
В области оркестрации мультиагентных систем наши инструменты тоже развиваются:
- LangChain: силён в оркестрации линейных рабочих процессов (базовые модули: llm api, document loaders, splitter, embedding, vector store, output parser, memory).
- LangGraph: создан специально для оркестрации сетевых рабочих процессов. Он вводит
State(граф состояний), состоящий из рабочих узлов + способа организации (API), и идеально поддерживает взаимодействие сложных агентов.
Ключевые концепции LangGraph
В LangGraph всё есть граф (Graph):
- Начальный узел (START): инициализирует состояние.
- Рабочий узел (Node): его задача — принять State и вернуть новый State.
- Соединение рабочих узлов (Edge): задаёт направление переходов между узлами.
- Ветвления и циклы (Conditional Edges): динамически определяют следующий шаг на основе State.
- Конечное состояние (END): завершение рабочего процесса.
Кроме того, критически важна персистентность (Persistence). Чтобы справляться с прерыванием, сбоем, паузой или необходимостью авторизации при выполнении агента, нужно сохранять State в память (MemoryServer / MemorySaver) или в базу данных (SQLite, Redis), чтобы продолжить выполнение в следующий раз и не запускать всё заново.
III. Практика LangGraph
Ниже мы шаг за шагом, от основ к продвинутому, реализуем четыре ключевые возможности LangGraph.
1. Построение базового графа и визуализация (basic-graph.mjs)
Всё начинается с самого базового Graph. Нам нужно определить состояние (State) и узлы (Node).
javascript
import {
Annotation, // 注释 工作流的状态值的描述 数据 state
END, // 结束节点
START, // 开始节点
StateGraph, // 状态图 流程编排器 节点的组织
} from '@langchain/langgraph';
// Объявление состояния: с помощью Annotation объявляем поле
const StateAnnotation = Annotation.Root({
text: Annotation({ // 只有 text 字段
// _prev — состояние перед текущим узлом, next — состояние, полученное текущим узлом
// Как reducer обрабатывает изменения состояния
reducer: (_prev, next) => next, // js 数组reduce 消消乐 text状态如何变
default: () => "", // 默认值
})
});
// Определяем узел
// Функция — это узел
// Возвращаемое значение — состояние следующего узла
// reducer обрабатывает изменения состояния
const step1 = (state) => ({text: `${state.text} -> step1`});
const step2 = (state) => ({text: `${state.text} -> step2`});
// Создаём оркестратор графа рабочего процесса
const graph = new StateGraph(StateAnnotation)
// Объявляем все узлы
.addNode("step1", step1)
.addNode("step2", step2)
.addEdge(START, "step1")
.addEdge("step1", "step2")
.addEdge("step2", END)
.compile(); // 编译工作流 执行
// Визуализируем граф
// mermaid — инструмент для рисования диаграмм из текста: простой markdown, автоматически генерирует блок-схему
// Визуализируем всю цепочку переходов между узлами.
const drawable = await graph.getGraphAsync();
const mermaid = drawable.drawMermaid({ withState: true });
console.log(mermaid);
2. Условные ветвления и маршрутизация (conditional-routing.mjs)
В реальных задачах нам нужно распределять входящие данные пользователя по разным узлам для обработки (например: математические вычисления vs светская беседа).
javascript
import {
Annotation, END, START, StateGraph
} from '@langchain/langgraph';
// Объявление состояния
const StateAnnotation = Annotation.Root({
query: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
route: Annotation({ reducer: (_prev, next) => next, default: () => "chat" }),
answer: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
});
// Объявление узлов: направление можно выбирать
const router = (state) => {
const isMath = /[+-*/]/.test(state.query); // 匹配加减乘除任意一个运算符
// Куда идти дальше?
// Если это математическая задача — идём в узел math, иначе — в узел chat
return {
route: isMath ? "math" : "chat",
}
}
const mathNode = (state) => {
try {
return {
answer: String(eval(state.query)) // 匹配
}
} catch {
return {
answer: "表达式无法计算"
}
}
}
const chatNode = (state) => ({
answer: `你说的是:${state.query}`,
})
const graph = new StateGraph(StateAnnotation)
// Объявляем узел маршрутизации, name — имя узла
.addNode("router", router)
.addNode("math", mathNode)
.addNode("chat", chatNode)
.addEdge(START, "router") // 走向固定
// Условный переход: проверяем значение state.route — math или chat
// Условие ветвления
.addConditionalEdges(
"router",
(state) => state.route,
{
math: "math",
chat: "chat",
},
)
.addEdge("math", END)
.addEdge("chat", END)
.compile(); // 编译状态图 执行
// Тестируем вызов
console.log("result:", await graph.invoke({query: "你好"}));
console.log("result:", await graph.invoke({query: "1+2"}));
3. Циклы и механизм повторных попыток (loop-retry.mjs)
Агенты часто сталкиваются со сценариями повторных попыток после сбоя. С помощью условных рёбер мы легко реализуем цикл.
javascript
import {
Annotation, END, START, StateGraph
} from '@langchain/langgraph';
const StateAnnotation = Annotation.Root({
tries: Annotation({ reducer: (_prev, next) => next, defaultValue: 0 }),
ok: Annotation({ reducer: (_prev, next) => next, default: () => false }),
message: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
})
const attempt = (state) => {
const tries = state.tries + 1;
const ok = tries >= 3;
return {
tries,
ok,
message: ok ? `第${tries}次成功尝试` : `第${tries}次失败`,
}
}
const graph = new StateGraph(StateAnnotation)
.addNode("attempt", attempt)
.addEdge(START, "attempt")
.addConditionalEdges("attempt", (state) => state.ok ? "done" : "retry", {
retry: "attempt", // 循环回到 attempt 节点
done: END,
})
.compile();
console.log("result:", await graph.invoke({tries: 0}));
4. Персистентность состояния и изоляция между пользователями (checkpointer-memory.mjs)
Как заставить агента помнить контекст? Как различать сессии разных пользователей? LangGraph предоставляет MemorySaver.
javascript
import {
Annotation, END, START, MemorySaver, StateGraph
} from '@langchain/langgraph';
const StateAnnotation = Annotation.Root({
// Сессия — связанные сеансы: кто-то, количество посещений
visitCount: Annotation({ reducer: (_prev, next) => next, default: () => 0 }),
message: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
})
// Узел подсчёта
function recordVist(state) {
const visitCount = state.visitCount + 1;
const message = visitCount === 1
? "这是你在本会话里第1次进入。"
: `这是你在本会话里第${visitCount}次进入。`;
return {
visitCount,
message,
}
}
const graph = new StateGraph(StateAnnotation)
.addNode("recordVist", recordVist)
.addEdge(START, "recordVist")
.addEdge("recordVist", END)
const checkpointer = new MemorySaver(); // 实例化一个内存保存器
const app = graph.compile({
checkpointer // 挂载持久化
})
// Изоляция нескольких пользователей (различаются по thread_id)
const user1 = { configurable: { thread_id: "用户-小张" } };
const user2 = { configurable: { thread_id: "用户-小李" } };
const res1 = await app.invoke({}, user1); // 小张第一次
console.log(res1);
const res2 = await app.invoke({}, user1); // 小张第二次
console.log(res2);
const res3 = await app.invoke({}, user1); // 小张第三次
console.log(res3);
const res4 = await app.invoke({}, user2); // 小李第一次,独立计数
console.log(res4);
5. Прерывание и возобновление: Human-in-the-loop (graph-interrupt.mjs)
Для чувствительных операций (например, перевода средств) нужно подтверждение человеком. LangGraph позволяет прервать выполнение графа и возобновить его после получения ввода пользователя.
javascript
import {
Annotation, END, START, StateGraph, Command, interrupt, MemorySaver
} from '@langchain/langgraph';
import { createInterface } from 'node:readline/promises';
// Схема графа
const StateAnnotation = Annotation.Root({
actionSummary: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
userInput: Annotation({ reducer: (_prev, next) => next, default: () => "" }),
})
const showTransfer = () => ({
actionSummary: "向张三转账 $100"
})
const waitConfirm = (state) => {
const text = interrupt({ // 中断
hint: "终端里输入[确认]或者备注后回车,图才会继续",
actionSummary: state.actionSummary
});
return { userInput: String(text) }
}
const graph = new StateGraph(StateAnnotation)
.addNode("showTransfer", showTransfer)
.addNode("waitConfirm", waitConfirm)
.addEdge(START, "showTransfer")
.addEdge("showTransfer", "waitConfirm")
.addEdge("waitConfirm", END)
.compile({ checkpointer: new MemorySaver() })
const config = { configurable: { thread_id: "interrupt-demo" } };
const paused = await graph.invoke({}, config);
console.log("待你确认", paused.__interrupt__?.[0]?.value);
// Узел — возобновление выполнения бизнес-процесса
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
const line = (await rl.question("> ")).trim();
console.log("你输入了", line);
await rl.close();
// Возобновляем выполнение с помощью Command и передаём ввод пользователя
const done = await graph.invoke(new Command({resume: line}), config);
console.log("done:", done);
IV. Заключение
От линейной компоновки LangChain к сетевой структуре LangGraph — неизбежный путь ИИ-агентов от «одиночной игры» к «командной работе».
С помощью LangGraph мы легко можем реализовать:
- Общее состояние: с помощью
Annotation.Rootиreducerмы точно управляем переходами состояния. - Гибкое управление потоком: через условные рёбра (
addConditionalEdges) реализуются сложная маршрутизация (Router) и циклы (Loop/Retry). - Персистентность продакшен-уровня: вместе с
MemorySaver/SqliteSaverиthread_idобеспечивает изоляцию сессий разных пользователей и возобновление с точки останова. - Human-in-the-loop: через
interrupt()иCommand(resume)на ключевых этапах бизнес-процесса вводится ручная проверка.
Двери для совместной работы нескольких агентов уже открыты — попробуйте LangGraph в своём бизнесе прямо сейчас!
Источник: Juejin · ИИ-программирование · juejin.cn