LangGraph у практыцы: станы, вузлы, рэгістры і пяць шаблонаў агентаў
Выучыце, як задаць стан LangGraph за дапамою анотацый і редуктараў, з’яднаць вузлы лініямі, а таксама як рэалізаваць усе пяць основных шаблонав рабочага процесу агентаў на JavaScript.
Крыніцы: Agentic AI and LangGraph — Part 2: Building Nodes, Edges, and All 5 Workflow Patterns
Падзеўка: Дзе мы застаўлі
У паканальнай частцы мы расказалі пра канцэптуальную базу: агент AI спаўнае мозг (LLM) з оркестрабірам, який координуе інструменты і памяць, пры чым усё гэта належыць пад які-небудзь вид нагляду. Мы таксама пабачылі, што LangGraph ператварае гэта на граф — вузлы выконваюць задачі, рэшткі керуюць ўзаемным прыемам дадзенняў, а спяльны об’ект стану пераводзіцца праз увесь система, пры чым на кожным кроці ён з’яўляецца ў результатах дзейства редактораў.
Ёсць час перейść ад тэорыі да практычнага застосавання. Калі вы дачытайце гэты паст, вы зможазеце:
- Заявіць стан за дапамогою
Annotation, укладаючы ў тое ж самае спецыяльныя редакторы - З’ўязаць вузлы за дапамогою як звычных, так і умовных рэштак
Усі коды, прыведзены нижэй, выкарыстоўваюць @langchain/langgraph і звычнае JavaScript-кодаванне (тэтыя ж шаблоны праіснуюць у TypeScript, трэба толькі дадаць параметры типу для Annotation<T>).
npm install @langchain/langgraph @langchain/core @langchain/openai
1. Стан і анотацыі — Аб’еднаны зошык
Перад тым, як можа запрацаваць хоць адзін вузол, LangGraph павінен разумець структуру дадзейнасі, якая праходзіць через вашу схему. Annotation — гэта спосаб описаў такой структуры.
import { Annotation, StateGraph, START, END } from "@langchain/langgraph";
const WorkflowState = Annotation.Root({
query: Annotation(),
searchResults: Annotation(),
draft: Annotation(),
finalAnswer: Annotation(),
});
Кожны ключ, які вы задаеце ўнутрь Annotation.Root({...}), стае атрыбутам у вашым об’екте спільнага стану. Кожны вузол можа чытаць значэння гэтых атрыбутаў, а калі вяртаецца, яму трэба ўключыць толькі тыя атрыбуты, якія ён фактычна моць выкарыстаць — LangGraph сам з’едначае гэты частковы результат з полным станам.
Reducer’ы: Выбір спосабу з’едначэння апдэйтав
Якщо вы не задаеце іншае, значэнне, якое вяртаецца вузлом, заменяе адпаведны атрыбут цэлком. Гэта падходзіць добра для чаго-небудзь уродзе draft, дзе вам патрабуецца толькі найновейшая версія. Але гэта не працюе для чаго-небудзь уродзе спіса паведамленняў, дзе перазапісвы могу стерць усё, што было раней. Самэлькі reducer дапамагае — це функцыя, якая визначае, як з’еднаць існуюча значэнне з новым апдэйтом.
const AgentState = Annotation.Root({
// Default behavior: last write wins
draft: Annotation(),
// Custom reducer: append instead of overwrite
messages: Annotation({
reducer: (current, update) => [...current, ...update],
default: () => [],
}),
});
Калі такое налажанае, якшо nodeA вяртае { messages: ["hi"] }, а nodeB — { messages: ["hello"] }, редюсер з’яеднвае іх у ["hi", "hello"], а не дазвае аднаму з’явіцца над іншым. Гэты механізм ўскладнілы для безпечнага запуску паралельных галузей (дывіцеся шаблон 3 нижэй) — два вузлы можаюць пісаць у той самы ключ стану адночасна без суперскульніцтва.
2. Вузлы — елементы роботы
Вузел — гэта проста функцыя, сінхронная або асінхронная, якая прыме текущы стан і вяртае частковае апдэйтаванне яго.
async function draftNode(state) {
const response = await llm.invoke(`Write a short draft about: ${state.query}`);
return { draft: response.content };
}
Вы рэгіструеце яго ў графе за дапамою addNode:
const graph = new StateGraph(WorkflowState)
.addNode("draft", draftNode);
Заўважыце, што сам вузел не мае жаданага усведамлення пра структуру графа навакола яго. Ён проста спрыяе змěне стану і стварае апдэйт. З’ѐеднанне вузелаў адбываецца выключна за дапамогою ребер.
3. Ребра — падключэнне потоку
Ребра вялікай меры вплываюць на порядак выканання. LangGraph падтрымляе тры відэны:
Нормальныя ребра — фіксаваны, безусловны шлях ад однаго вузела да іншага.
graph.addEdge(START, "draft"); // entry point: where the graph starts
graph.addEdge("draft", "polish");
graph.addEdge("polish", END); // exit point: where the graph stops
Умовныя ребра — функцыя маршрутызаціі пераглядае текущы стан і вяршыцца, який вузел будзе выкананы далей.
graph.addConditionalEdges("classify", (state) => {
if (state.category === "billing") return "handleBilling";
if (state.category === "technical") return "handleTechnical";
return "handleGeneral";
});
Паралельныя (фан-аут) ребра — калі адзін вузел мае калькольвыя нормальныя ребра, усі ўсіх іх цілі выкананыя адночасна.
graph.addEdge("orchestrator", "workerA");
graph.addEdge("orchestrator", "workerB");
graph.addEdge("orchestrator", "workerC");
Кожны граф должен мяць прынеймна адную грань, яка пачынаецца з START, і маршрут, які ў канцы выходзіць на END; інакша LangGraph не будзе ведаць, дзе трэба пачаць і заканчыць выконанне.
Так как стан, вузлы і гранны ўжо розглянуты, у вас є всі неабходны элементы для стварэння пяці шаблонаў рабочага прыему, якія былі представлены раней.
4. Шаблон 1 — Аўтоматызаваная абработка запитаў
Канцэпцыя: разбіць большую задачу на лінейную ланцюговую серію вызоў LLM, пры чым кожны крок будуе на тым, што было раней.
START → outline → draft → polish → END
const ChainState = Annotation.Root({
topic: Annotation(),
outline: Annotation(),
draft: Annotation(),
final: Annotation(),
});
async function outlineNode(state) {
const res = await llm.invoke(`Create a short outline for: ${state.topic}`);
return { outline: res.content };
}async function draftNode(state) {
const res = await llm.invoke(`Write a draft based on this outline:\n${state.outline}`);
return { draft: res.content };
}async function polishNode(state) {
const res = await llm.invoke(`Polish and tighten this draft:\n${state.draft}`);
return { final: res.content };
}const chainGraph = new StateGraph(ChainState)
.addNode("outline", outlineNode)
.addNode("draft", draftNode)
.addNode("polish", polishNode)
.addEdge(START, "outline")
.addEdge("outline", "draft")
.addEdge("draft", "polish")
.addEdge("polish", END)
.compile();const result = await chainGraph.invoke({ topic: "the history of tea" });
console.log(result.final);
Кожны вузел тут чытае толькі поле, яке запісаў вузел да ўжо — няма розгалужэнняў чы цыклаў, толькі простая лінія ад пачатку да канца.
5. Шаблон 2 — Маршрутаванне
Канцэпцыя: спачатку класіфікаваць прыйняты вхідны данны, а пасля перадаць іх аднойчы з кількох спецыяльна прызначаных шляхоў обробкі.
┌──> handleBilling ──┐
START → classify ───┼──> handleTechnical ─┼──> END
└──> handleGeneral ──┘
const RouteState = Annotation.Root({
message: Annotation(),
category: Annotation(),
response: Annotation(),
});
async function classifyNode(state) {
const res = await llm.invoke(
`Classify this message as "billing", "technical", or "general". Only reply with one word.\n${state.message}`
);
return { category: res.content.trim().toLowerCase() };
}async function handleBilling(state) {
return { response: `[Billing team] ${await llm.invoke(state.message).then(r => r.content)}` };
}
async function handleTechnical(state) {
return { response: `[Tech support] ${await llm.invoke(state.message).then(r => r.content)}` };
}
async function handleGeneral(state) {
return { response: `[General desk] ${await llm.invoke(state.message).then(r => r.content)}` };
}const routeGraph = new StateGraph(RouteState)
.addNode("classify", classifyNode)
.addNode("handleBilling", handleBilling)
.addNode("handleTechnical", handleTechnical)
.addNode("handleGeneral", handleGeneral)
.addEdge(START, "classify")
.addConditionalEdges("classify", (state) => {
if (state.category === "billing") return "handleBilling";
if (state.category === "technical") return "handleTechnical";
return "handleGeneral";
})
.addEdge("handleBilling", END)
.addEdge("handleTechnical", END)
.addEdge("handleGeneral", END)
.compile();
Функцыя, яку вы пасылайте да addConditionalEdges, должна вяртаць назву наступнага вузла, які трэба адвіцарыць — гэта механізм, які дазволяе графу дынамічна выбіраць шлях у замест на тое, каб завжды следаваць однаму і тому ж маршруту.
6. Шаблон 3 — Паралелізацыя
Канцэпцыя: адразу запускаць калькі LLM проты разных частак задання, а пасля сумаваць рэзультаты.
┌──> summarizeDocA ──┐
START → split ──┼──> summarizeDocB ──┼──> merge → END
└──> summarizeDocC ──┘
const ParallelState = Annotation.Root({
documents: Annotation(),
summaries: Annotation({
reducer: (current, update) => [...current, ...update],
default: () => [],
}),
finalSummary: Annotation(),
});
function splitNode(state) {
return {}; // no-op entry point, just fans out to the summarizers below
}async function summarizeDocA(state) {
const res = await llm.invoke(`Summarize:\n${state.documents[0]}`);
return { summaries: [res.content] };
}
async function summarizeDocB(state) {
const res = await llm.invoke(`Summarize:\n${state.documents[1]}`);
return { summaries: [res.content] };
}
async function summarizeDocC(state) {
const res = await llm.invoke(`Summarize:\n${state.documents[2]}`);
return { summaries: [res.content] };
}async function mergeNode(state) {
const res = await llm.invoke(`Combine these summaries into one:\n${state.summaries.join("\n---\n")}`);
return { finalSummary: res.content };
}const parallelGraph = new StateGraph(ParallelState)
.addNode("split", splitNode)
.addNode("summarizeDocA", summarizeDocA)
.addNode("summarizeDocB", summarizeDocB)
.addNode("summarizeDocC", summarizeDocC)
.addNode("merge", mergeNode)
.addEdge(START, "split")
.addEdge("split", "summarizeDocA")
.addEdge("split", "summarizeDocB")
.addEdge("split", "summarizeDocC")
.addEdge("summarizeDocA", "merge")
.addEdge("summarizeDocB", "merge")
.addEdge("summarizeDocC", "merge")
.addEdge("merge", END)
.compile();
Зверніце увагу на редуктар, прыўязаны да summaries: без яго пасляпэўны паралельны відгалужэння проста перазапісае пярэднія рэзультаты, у замест на тое, каб усе трое выходных даных безпечна накапліваліся ў масе.
7. Шаблон 4 — Оркестрайтар-Рабочы
Канцэпцыя: адзін вызов LLM дзельняе загальную задачу на меншыя падзядныя заведаменні і перадае кожна з іх рабочаму вузлу, пасля чым фінальны крок складае ўсё в адны рэзультат. Гэта дужа супаняеся з паралелізацыёй, але тут оркестрайтар у момент выконання вялічыну колькасці рабочых узлаў і тое, што павінен рабіць кожны з іх, адначынае сам, у працоўніку не ёсць фіксаванага плану заздалегідь.
START → orchestrator ──> worker(s), one per subtask ──> compile → END
const OrchestratorState = Annotation.Root({
task: Annotation(),
subtasks: Annotation(),
sections: Annotation({
reducer: (current, update) => [...current, ...update],
default: () => [],
}),
report: Annotation(),
});
async function orchestratorNode(state) {
const res = await llm.invoke(
`Break this task into 3-5 short subtasks, one per line:\n${state.task}`
);
const subtasks = res.content.split("\n").filter(Boolean);
return { subtasks };
}async function workerNode(state) {
// In a real app you'd invoke one worker per subtask (e.g. via Send / map-reduce);
// here's the simple per-subtask logic each worker runs:
const results = await Promise.all(
state.subtasks.map((subtask) => llm.invoke(`Write a section about: ${subtask}`))
);
return { sections: results.map((r) => r.content) };
}async function compileNode(state) {
const res = await llm.invoke(`Compile these sections into one report:\n${state.sections.join("\n\n")}`);
return { report: res.content };
}const orchestratorGraph = new StateGraph(OrchestratorState)
.addNode("orchestrator", orchestratorNode)
.addNode("worker", workerNode)
.addNode("compile", compileNode)
.addEdge(START, "orchestrator")
.addEdge("orchestrator", "worker")
.addEdge("worker", "compile")
.addEdge("compile", END)
.compile();
Якщо вам патрэбна сапраўдная дынамічная структура — тое значыць, што колькасць рабочых вузлаў фактычна зменяецца з аднаго запуску да іншага — LangGraph.js прысвячае цэй задачы API Send, які дазволяе ствараць по аднаму вызову рабочага вузла на кожна падзядная заведаменні ў момент выканання. Гэты механізм заслуговае на самастоятельны дэтальны апіс, але прадставленая тут наладка вже падкрывае большую частку ўседзённых патрабаванняў, такіх як стварэнне звярнункоў чы будова агентаў для даследжэння.
8. Шаблон 5 — Адміністратар-оптымаўчык
Канцэпцыя: адна вузел выдае результат, другі вузел яго перакантролюе, а умовны рэшт выконанне направляе знадворы да генератора, пакуль результат не будзе прыняты. Цэй ёсць шаблон, у яком фактычна выкарыстоўваецца падтрымка цыклаў у LangGraph.
START → generate → evaluate ──(fail)──> generate (loop)
│
(pass)
↓
END
const LoopState = Annotation.Root({
prompt: Annotation(),
code: Annotation(),
feedback: Annotation(),
passed: Annotation(),
attempts: Annotation({
reducer: (current, update) => current + update,
default: () => 0,
}),
});
async function generateNode(state) {
const context = state.feedback ? `\nPrevious feedback: ${state.feedback}` : "";
const res = await llm.invoke(`Write code for: ${state.prompt}${context}`);
return { code: res.content, attempts: 1 };
}async function evaluateNode(state) {
const res = await llm.invoke(
`Review this code for bugs. Reply "PASS" if it's correct, otherwise explain what's wrong:\n${state.code}`
);
const passed = res.content.trim().startsWith("PASS");
return { passed, feedback: passed ? null : res.content };
}const loopGraph = new StateGraph(LoopState)
.addNode("generate", generateNode)
.addNode("evaluate", evaluateNode)
.addEdge(START, "generate")
.addEdge("generate", "evaluate")
.addConditionalEdges("evaluate", (state) => {
if (state.passed || state.attempts >= 4) return END; // safety cap on retries
return "generate";
})
.compile();
Заўважыце, што поле attempts выкарыстоўвае редуктар, які дадае да чысла, якае ведацца, а не заменяе яго — гэта таксама служыць захоўнікам. Без якога-небудзь ліміту на праказы, адміністратар, які ніколі не затверджвае результат, могаць прыводзіць цыкл у дзейнасць несканчэнна.
9. Кампіляванне і запуск будзь-каго з гэтых
Незалежна ад таго, які шаблон вы створылі, вы завершаеце адносна тады ж:
const app = graph.compile();
const finalState = await app.invoke({ /* initial state fields */ });
Выклік .compile() пераканальваецца, чы ўсі вузлы дасягнучы, чы START і END належна з’ўязаны, і ператварае опис вашай графікі на выконвальны об’ект. Пасля чаго .invoke() запускае графіку з пачатку да канца і вяртае рэзультуючы стан — гэты ўсё той жа цыкл выконання, які быў концэптуальна описаны раней у гэтай серыі.
Заключэнне
У ходзе гэтага карусельнага адгуквання ідеі з тэорыі ператварыліся ў рабочы код: Annotation.Root для вялічэння стану, звычныя функцыі як вузлы, addEdge і addConditionalEdges для кантролю прайму, а таксама редукцыяры для таго, каб сумаварныя або паўтаральныя апдэйты не перакрылі адзін другога. За дапамогою толькі гэтых чатырох элементаў кожны з пяці расглянутых шаблонаў рабочага прайму — ланцюгаванне, маршрутызацыя, паралелізацыя, оркестрацыя-рабочы і адмаціраванне-оптымізацыя — выказваецца як разныя комбінацыі тых сабе основных інструментаў.
Наступны частак у гэтай серыі практыкуе внешнія інструменты, дадае памяць, каб стан мог утримвацца між сэсіямі, і адкрывае этап, калі чалавек пераглядае і затварджае дзеянні пры ўсуненні іх да адклонення — гэтыя элементы неабходны, каб ператварыць гэтыя графы на справжні агента.