.jpg?alt=media&token=fa69b8b3-19be-4147-9583-843a5bdcffcb)
Што вы даведаецеся: Як будаваць AI-агентныя сістэмы з LangGraph - ад базавых канцэпцый да працуючага кода. Мы створым пайплайн для напісання артыкулаў з некалькімі AI-агентамі, якія супрацоўнічаюць, правяраюць працу адзін аднаго і ітэруюць, пакуль вынік не будзе ідэальным.
ЧАСТКА 1: Базавыя канцэпцыі
Што такое граф?
Перш чым разбірацца з LangGraph, трэба зразумець, што такое граф у праграмаванні.
Граф - гэта структура даных
Уявіце карту метро:
- Станцыі - гэта вузлы (nodes)
- Лініі паміж станцыямі - гэта пераходы (edges)
1 Звычайны граф: Карта метро (аналогія):
2
3 [A] [Плошча Перамогі]
4 │ │
5 ▼ ▼
6 [B]───────►[C] [Кастрычніцкая]───►[Купалаўская]
7 │ │
8 ▼ ▼
9 [D] [Інстытут культуры]
У LangGraph:
- Вузлы - гэта функцыі (агенты), якія нешта робяць.
- Пераходы - гэта правілы, хто пасля каго працуе.
Навошта патрэбны LangGraph?
Праблема: Звычайныя AI-праграмы лінейныя
1Звычайны ланцужок (Chain):
2
3 Пытанне → LLM → Адказ
4
5 або
6
7 Пытанне → Інструмент 1 → LLM → Інструмент 2 → Адказ
Гэта працуе для простых задач, але што, калі:
- Трэба вярнуцца назад і перарабіць?
- Трэба праверыць вынік і, магчыма, паўтарыць?
- Трэба некалькі агентаў з рознымі ролямі?
Рашэнне: LangGraph дазваляе ствараць цыклы
1LangGraph (Граф):
2
3 ┌──────────────────────┐
4 │ │
5 ▼ │
6 Пытанне → [Даследчык] → [Пісьменнік] → [Рэцэнзент]
7 │
8 ▼
9 Добра? ───НЕ───► назад да Пісьменніка
10 │
11 ТАК
12 │
13 ▼
14 [Вынік]
Што такое "стан" (State)?
Стан - гэта памяць праграмы
Уявіце, што вы пішаце артыкул з камандай:
- Даследчык сабраў матэрыялы → трэба некуды запісаць.
- Пісьменнік напісаў чарнавік → трэба перадаць рэцэнзенту.
- Рэцэнзент напісаў заўвагі → трэба вярнуць пісьменніку.
Стан - гэта "кошык", куды ўсе кладуць свае вынікі і адкуль бяруць даныя.
1┌─────────────────────────────────────────────────────────────┐
2│ СТАН (State) │
3├─────────────────────────────────────────────────────────────┤
4│ │
5│ topic: "Пра што пішам" │
6│ research: "Матэрыялы ад даследчыка" │
7│ draft: "Чарнавік ад пісьменніка" │
8│ review: "Заўвагі ад рэцэнзента" │
9│ finalArticle: "Гатовы артыкул" │
10│ messages: [гісторыя ўсіх паведамленняў] │
11│ │
12└─────────────────────────────────────────────────────────────┘
13 ▲ ▲ ▲
14 │ │ │
15 Даследчык Пісьменнік Рэцэнзент
16 (чытае і піша) (чытае і піша) (чытае і піша)
ЧАСТКА 2: Annotation - Як ствараецца стан
Што такое Annotation?
Annotation - гэта спосаб апісання структуры стану. Гэта як стварэнне формы з палямі.
Аналогія: Анкета
Уявіце, што вы ствараеце анкету:
1┌─────────────────────────────────────────┐
2│ АНКЕТА РАБОТНІКА │
3├─────────────────────────────────────────┤
4│ Імя: ___________________ │
5│ Узрост: _____ │
6│ Вопыт працы (гады): _____ │
7│ Спіс навыкаў: ___, ___, ___ │
8└─────────────────────────────────────────┘
У кодзе гэта выглядае так:
1// АНАЛОГІЯ: Стварэнне формы анкеты
2const Anketa = {
3 imya: '', // Тэкставае поле
4 uzrost: 0, // Лік
5 vopyt: 0, // Лік
6 navyki: [], // Спіс
7};
У LangGraph: Annotation.Root
У LangGraph стан ствараецца з дапамогай Annotation.Root(). Гэта спецыяльная функцыя, якая апісвае структуру вашых даных - якія палі існуюць і як яны павінны абнаўляцца.
1import { Annotation } from '@langchain/langgraph';
2
3const ResearchState = Annotation.Root({
4 // Кожнае поле апісваецца праз Annotation<Тып>
5 topic: Annotation<string>({...}),
6 research: Annotation<string>({...}),
7 draft: Annotation<string>({...}),
8 // і г.д.
9});
Кожнае поле ўнутры Annotation.Root мае дзве важныя ўласцівасці: reducer (як абнаўляць значэнне) і default (пачатковае значэнне). Мы разбяром іх далей.
Што такое Reducer?
Праблема: Як аб'ядноўваць даныя?
Калі некалькі агентаў пішуць у адно поле, што рабіць?
1Агент 1 піша: topic = "Тэма А"
2Агент 2 піша: topic = "Тэма Б"
3
4Што павінна быць у topic? "Тэма А"? "Тэма Б"? "Тэма А + Тэма Б"?
Рашэнне: Reducer - функцыя, якая вырашае
Reducer - гэта функцыя, якая атрымлівае:
- Бягучае значэнне (што ўжо ёсць)
- Новае значэнне (што прыйшло)
І вяртае: вынік (што захаваць)
1reducer: (current, update) => результат;
2// ▲ ▲ ▲
3// │ │ └── Што захаваецца
4// │ └── Новае значэнне
5// └── Бягучае значэнне
Прыклады Reducer-аў:
1. Reducer "ЗАМЯНІЦЬ" (replace)
Новае значэнне поўнасцю замяняе старое.
1// Код:
2reducer: (current, update) => update;
3
4// Прыклад:
5// Бягучае: "Тэма А"
6// Новае: "Тэма Б"
7// Вынік: "Тэма Б" ← новае замяніла старое
Аналогія: Вы перазапісваеце файл - новы змест замяняе стары.
1файл.txt
2──────────────
3Было: "Стары тэкст"
4 ▼ (перазапіс)
5Стала: "Новы тэкст"
2. Reducer "ДАДАЦЬ" (append)
Новыя элементы дадаюцца да існуючых.
1// Код:
2reducer: (current, update) => [...current, ...update];
3
4// Прыклад:
5// Бягучае: ["Паведамленне 1", "Паведамленне 2"]
6// Новае: ["Паведамленне 3"]
7// Вынік: ["Паведамленне 1", "Паведамленне 2", "Паведамленне 3"]
Аналогія: Вы дадаеце новыя запісы ў дзённік - старыя застаюцца.
1дзённік.txt
2──────────────
3Было: "Панядзелак: зрабіў А"
4 "Аўторак: зрабіў Б"
5 ▼ (дадаём)
6Стала: "Панядзелак: зрабіў А"
7 "Аўторак: зрабіў Б"
8 "Серада: зрабіў В" ← дадалося
Поўны прыклад стану:
1const ResearchState = Annotation.Root({
2 // ═══════════════════════════════════════════════════════
3 // ПОЛЕ: messages (паведамленні)
4 // ═══════════════════════════════════════════════════════
5 messages: Annotation<BaseMessage[]>({
6 // REDUCER: Дадаваць новыя паведамленні да існуючых
7 reducer: (current, update) => [...current, ...update],
8 // DEFAULT: Пачатковае значэнне - пусты масіў
9 default: () => [],
10 }),
11 //
12 // Як гэта працуе:
13 // 1. Пачатак: messages = []
14 // 2. Даследчык: messages = [] + [AIMessage] = [AIMessage]
15 // 3. Пісьменнік: messages = [AIMessage] + [AIMessage] = [AIMessage, AIMessage]
16 // 4. І г.д. - усе паведамленні захоўваюцца
17
18 // ═══════════════════════════════════════════════════════
19 // ПОЛЕ: topic (тэма)
20 // ═══════════════════════════════════════════════════════
21 topic: Annotation<string>({
22 // REDUCER: Замяняць старое значэнне новым
23 reducer: (_, update) => update, // "_" значыць "ігнаруем"
24 // DEFAULT: Пачатковае значэнне - пусты радок
25 default: () => '',
26 }),
27 //
28 // Як гэта працуе:
29 // 1. Пачатак: topic = ""
30 // 2. Карыстач: topic = "" → "LangChain"
31 // Калі хтось яшчэ напіша ў topic - старое значэнне знікне
32
33 // ═══════════════════════════════════════════════════════
34 // ПОЛЕ: iterationCount (лічыльнік ітэрацый)
35 // ═══════════════════════════════════════════════════════
36 iterationCount: Annotation<number>({
37 reducer: (_, update) => update, // Проста замяняем
38 default: () => 0, // Пачынаем з 0
39 }),
40 //
41 // Пісьменнік кожны раз піша: iterationCount: state.iterationCount + 1
42 // 1. Пачатак: iterationCount = 0
43 // 2. Пісьменнік (1): iterationCount = 0 + 1 = 1
44 // 3. Пісьменнік (2): iterationCount = 1 + 1 = 2
45 // 4. Пісьменнік (3): iterationCount = 2 + 1 = 3
46});
Візуалізацыя Reducer-аў:
1┌─────────────────────────────────────────────────────────────────┐
2│ REDUCER: ЗАМЯНІЦЬ │
3├─────────────────────────────────────────────────────────────────┤
4│ │
5│ Бягучае: █████████████ "Стары тэкст" │
6│ ▼ │
7│ Новае: ░░░░░░░░░░░░░ "Новы тэкст" │
8│ ▼ │
9│ Вынік: ░░░░░░░░░░░░░ "Новы тэкст" ← толькі новае │
10│ │
11└─────────────────────────────────────────────────────────────────┘
12
13┌─────────────────────────────────────────────────────────────────┐
14│ REDUCER: ДАДАЦЬ │
15├─────────────────────────────────────────────────────────────────┤
16│ │
17│ Бягучае: [█] [█] [█] ← тры элементы │
18│ + │
19│ Новае: [░] [░] ← два новыя │
20│ = │
21│ Вынік: [█] [█] [█] [░] [░] ← усе разам │
22│ │
23└─────────────────────────────────────────────────────────────────┘
Што такое default?
default - гэта функцыя, якая вяртае пачатковае значэнне поля.
Чаму функцыя, а не проста значэнне?
Гэта распаўсюджаная пастка JavaScript. Калі вы выкарыстоўваеце просты аб'ект або масіў як default, усе экзэмпляры будуць спасылацца на адну і тую ж спасылку - змены ў адным месцы паўплываюць на ўсе астатнія!
1// ДРЭННА: Калі гэта аб'ект або масіў
2default: [] // Усе экзэмпляры будуць спасылацца на адзін масіў!
3
4// ДОБРА: Функцыя стварае новы масіў кожны раз
5default: () => [] // Кожны экзэмпляр атрымае свой масіў
Прыклады:
1// Для радка
2default: () => '' // Пусты радок
3
4// Для ліку
5default: () => 0 // Нуль
6
7// Для масіва
8default: () => [] // Пусты масіў
9
10// Для аб'екта
11default: () => ({}) // Пусты аб'ект
12
13// Для boolean
14default: () => false // false
ЧАСТКА 3: Вузлы (Nodes) - Агенты
Што такое вузел?
Вузел (Node) - гэта функцыя, якая:
- Атрымлівае поўны стан
- Робіць нейкую працу (напрыклад, выклікае LLM)
- Вяртае частковае абнаўленне стану
Аналогія: Работнік на канвееры
1┌─────────────────────────────────────────────────────────────┐
2│ КАНВЕЕР │
3├─────────────────────────────────────────────────────────────┤
4│ │
5│ [Каробка] ──► [Работнік 1] ───────► [Работнік 2] ──────► │
6│ │ │ │
7│ ▼ ▼ │
8│ Дадае дэталь А Дадае дэталь Б │
9│ │
10└─────────────────────────────────────────────────────────────┘
11
12Кожны работнік:
131. Бачыць, што ўжо зроблена (стан)
142. Робіць сваю частку работы
153. Перадае далей з дапаўненнямі
Структура вузла ў кодзе:
1async function myNode(
2 state: ResearchStateType // ← УВАХОД: Поўны стан
3): Promise<Partial<ResearchStateType>> {
4 // ← ВЫХАД: Частковае абнаўленне
5 // 1. Чытаем даныя са стану
6 const data = state.someField;
7
8 // 2. Робім працу
9 const result = await doSomething(data);
10
11 // 3. Вяртаем абнаўленне (толькі тое, што змянілі)
12 return {
13 someField: result,
14 };
15}
Важна: Partial<State>
Вузел не абавязаны вяртаць увесь стан - толькі палі, якія змяніліся. LangGraph аб'яднае вашае частковае абнаўленне з існуючым станам з дапамогай reducer-аў, якія вы вызначылі.
1// Поўны стан мае 6 палёў:
2state = {
3 topic: "...",
4 research: "...",
5 draft: "...",
6 review: "...",
7 finalArticle: "...",
8 messages: [...],
9 iterationCount: 0
10}
11
12// Але вузел можа вярнуць толькі тое, што змяніў:
13return {
14 draft: "Новы чарнавік", // Змянілі
15 messages: [new AIMessage("...")], // Дадалі
16 // Астатнія палі не згадваем - яны застануцца як былі
17}
Нашы вузлы падрабязна
Цяпер разгледзім чатыры вузлы ў нашай сістэме напісання артыкулаў. Кожны вузел мае канкрэтную ролю і перадае свае вынікі наступнаму праз агульны стан.
Вузел 1: Даследчык (researcherNode)
Даследчык - першы агент у нашым пайплайне. Ён бярэ тэму і генеруе даследчыя матэрыялы, якія будуць выкарыстоўвацца пісьменнікам.
1async function researcherNode(
2 state: ResearchStateType
3): Promise<Partial<ResearchStateType>> {
4 // ┌─────────────────────────────────────────────────────────────┐
5 // │ КРОК 1: Атрымліваем тэму для даследавання │
6 // └─────────────────────────────────────────────────────────────┘
7 const topic =
8 state.topic ||
9 String(state.messages[state.messages.length - 1]?.content) ||
10 '';
11 // ▲ ▲
12 // │ └── Альбо апошняе паведамленне
13 // └── Спачатку спрабуем узяць topic
14
15 // ┌─────────────────────────────────────────────────────────────┐
16 // │ КРОК 2: Фармуем prompt для LLM │
17 // └─────────────────────────────────────────────────────────────┘
18 const prompt = `Ты эксперт-даследчык. Твая задача - сабраць ключавую інфармацыю.
19 Тэма: ${topic}
20 Правядзі кароткае даследаванне...`;
21
22 // ┌─────────────────────────────────────────────────────────────┐
23 // │ КРОК 3: Выклікаем LLM │
24 // └─────────────────────────────────────────────────────────────┘
25 const response = await model.invoke([
26 { role: 'system', content: prompt }, // Інструкцыі для AI
27 { role: 'user', content: `Даследуй тэму: ${topic}` }, // Запыт
28 ]);
29
30 const research = String(response.content); // Вынік ад LLM
31
32 // ┌─────────────────────────────────────────────────────────────┐
33 // │ КРОК 4: Вяртаем абнаўленне стану │
34 // └─────────────────────────────────────────────────────────────┘
35 return {
36 research, // Запісваем вынік даследавання
37 messages: [
38 new AIMessage({ content: `[Даследаванне завершана]\n${research}` }),
39 ],
40 // ▲ Дадаем паведамленне ў гісторыю
41 };
42}
Што адбываецца:
1УВАХОД (state): ВЫХАД (абнаўленне):
2┌─────────────────────┐ ┌─────────────────────┐
3│ topic: "LangChain" │ │ research: "LangCh.. │
4│ research: "" │ ──────► │ messages: [+1 msg] │
5│ draft: "" │ └─────────────────────┘
6│ messages: [1 msg] │
7└─────────────────────┘
8
9 ▼ Пасля аб'яднання:
10 ┌─────────────────────┐
11 │ topic: "LangChain" │
12 │ research: "LangCh...│ ← абноўлена
13 │ draft: "" │
14 │ messages: [2 msgs] │ ← дадалося
15 └─────────────────────┘
Вузел 2: Пісьменнік (writerNode)
Пісьменнік бярэ даследаванне і стварае чарнавік артыкула. Калі гэта перапрацоўка (пасля водгукаў рэцэнзента), ён таксама ўлічвае каментары да рэцэнзіі. Звярніце ўвагу, як iterationCount дапамагае адсочваць, колькі разоў артыкул быў перапісаны.
1async function writerNode(
2 state: ResearchStateType
3): Promise<Partial<ResearchStateType>> {
4 // ┌─────────────────────────────────────────────────────────────┐
5 // │ КРОК 1: Чытаем даследаванне і магчымую рэцэнзію │
6 // └─────────────────────────────────────────────────────────────┘
7 const prompt = `Ты тэхнічны пісьменнік. На аснове даследавання напішы артыкул.
8
9Даследаванне:
10${state.research}
11
12${
13 state.review
14 ? `Папярэдняя рэцэнзія (улічы заўвагі):
15${state.review}`
16 : ''
17}
18`;
19
20 // ┌─────────────────────────────────────────────────────────────┐
21 // │ КРОК 2: Выклікаем LLM │
22 // └─────────────────────────────────────────────────────────────┘
23 const response = await model.invoke([
24 { role: 'system', content: prompt },
25 { role: 'user', content: 'Напішы артыкул на аснове даследавання' },
26 ]);
27
28 const draft = String(response.content);
29
30 // ┌─────────────────────────────────────────────────────────────┐
31 // │ КРОК 3: Вяртаем абнаўленне │
32 // └─────────────────────────────────────────────────────────────┘
33 return {
34 draft, // Чарнавік артыкула
35 iterationCount: state.iterationCount + 1, // Павялічваем лічыльнік
36 messages: [
37 new AIMessage({
38 content: `[Чарнавік ${state.iterationCount + 1}]`,
39 }),
40 ],
41 };
42}
Што адбываецца пры паўторным выкліку:
1ПЕРШЫ ВЫКЛІК: ДРУГІ ВЫКЛІК (пасля рэцэнзіі):
2┌─────────────────────┐ ┌─────────────────────┐
3│ research: "..." │ │ research: "..." │
4│ review: "" │ │ review: "Дапрацуй...│ ← ёсць заўвагі!
5│ iterationCount: 0 │ │ iterationCount: 1 │
6└─────────────────────┘ └─────────────────────┘
7 │ │
8 ▼ ▼
9 Піша без заўваг Улічвае заўвагі
10 │ │
11 ▼ ▼
12┌─────────────────────┐ ┌─────────────────────┐
13│ draft: "Версія 1" │ │ draft: "Версія 2" │
14│ iterationCount: 1 │ │ iterationCount: 2 │
15└─────────────────────┘ └─────────────────────┘
Вузел 3: Рэцэнзент (reviewerNode)
Рэцэнзент ацэньвае чарнавік і вырашае, ці гатовы ён да публікацыі. Ключавы момант тут - вывад: калі рэцэнзія змяшчае "ЗАЦВЕРДЖАНА", артыкул ідзе на фіналізацыю. У адваротным выпадку ён вяртаецца да пісьменніка для паляпшэнняў. Менавіта гэта дазваляе ствараць цыкл у нашым графе.
1async function reviewerNode(state: ResearchStateType): Promise<Partial<ResearchStateType>> {
2
3 // ┌─────────────────────────────────────────────────────────────┐
4 // │ КРОК 1: Фармуем запыт на рэцэнзію │
5 // └─────────────────────────────────────────────────────────────┘
6 const prompt = `Ты строгі рэдактар. Ацані артыкул.
7
8Артыкул:
9${state.draft}
10
11Ацані па крытэрыях:
121. Дакладнасць інфармацыі
132. Структура і лагічнасць
143. Якасць мовы
154. Паўната раскрыцця тэмы
16
17Калі артыкул добры - скажы "ЗАЦВЕРДЖАНА".
18Калі трэба дапрацаваць - дай канкрэтныя рэкамендацыі.`;
19
20 // ┌─────────────────────────────────────────────────────────────┐
21 // │ КРОК 2: Атрымліваем рэцэнзію │
22 // └─────────────────────────────────────────────────────────────┘
23 const response = await model.invoke([...]);
24 const review = String(response.content);
25
26 // ┌─────────────────────────────────────────────────────────────┐
27 // │ КРОК 3: Вяртаем рэцэнзію │
28 // └─────────────────────────────────────────────────────────────┘
29 return {
30 review, // Рэцэнзія (або "ЗАЦВЕРДЖАНА" або заўвагі)
31 messages: [new AIMessage({ content: `[Рэцэнзія]\n${review}` })],
32 };
33}
Два магчымыя вынікі:
1ВАРЫЯНТ А: Артыкул добры ВАРЫЯНТ Б: Трэба дапрацаваць
2┌─────────────────────────┐ ┌─────────────────────────┐
3│ review: "ЗАЦВЕРДЖАНА. │ │ review: "Дапрацаваць: │
4│ Артыкул выдатны!" │ │ 1. Дадай прыклады │
5│ │ │ 2. Удакладні тэрміны" │
6└─────────────────────────┘ └─────────────────────────┘
7 │ │
8 ▼ ▼
9 Ідзём да Вяртаемся да
10 finalizer writer
Вузел 4: Фіналізатар (finalizerNode)
Фіналізатар - найпрасцейшы вузел - ён проста капіюе зацверджаны чарнавік у поле finalArticle, пазначаючы канец нашага працоўнага працэсу. Ён выклікаецца, калі рэцэнзент зацвярджае артыкул.
1async function finalizerNode(
2 state: ResearchStateType
3): Promise<Partial<ResearchStateType>> {
4 // Просты вузел - капіюе чарнавік у фінальны артыкул
5 return {
6 finalArticle: state.draft, // Гатовы артыкул
7 messages: [new AIMessage({ content: `[ГАТОВА]\n\n${state.draft}` })],
8 };
9}
Што адбываецца:
1УВАХОД: ВЫХАД:
2┌─────────────────────┐ ┌─────────────────────┐
3│ draft: "Гатовы..." │ ──────► │ finalArticle: │
4│ finalArticle: "" │ │ "Гатовы..." │
5└─────────────────────┘ └─────────────────────┘
ЧАСТКА 4: Пераходы (Edges)
Што такое пераход?
Пераход (Edge) - гэта правіла, якое кажа: "Пасля гэтага вузла выконваецца той вузел".
Аналогія: Стрэлкі на схеме
1Рэцэпт пічы:
2
3 [Зрабіць цеста] ─────► [Дадаць соус] ─────► [Дадаць сыр] ─────► [Выпекчы]
4 │ │ │ │
5 ▼ ▼ ▼ ▼
6 ПЕРАХОД ПЕРАХОД ПЕРАХОД КАНЕЦ
Два тыпы пераходаў у LangGraph:
1. Простыя пераходы (addEdge)
Заўсёды ідзём у адзін і той жа вузел.
1workflow.addEdge('researcher', 'writer');
2// ▲ ▲
3// │ └── Куды ідзём
4// └── Адкуль ідзём
5
6// Значыць: Пасля researcher ЗАЎСЁДЫ ідзём да writer
Візуалізацыя:
1 [researcher] ───────────────► [writer]
2 (заўсёды)
2. Умоўныя пераходы (addConditionalEdges)
Выбар наступнага вузла залежыць ад стану.
1workflow.addConditionalEdges(
2 'reviewer', // Адкуль ідзём
3 shouldContinue, // Функцыя, якая вырашае куды
4 {
5 writer: 'writer', // Калі функцыя верне 'writer' → ідзём у writer
6 finalizer: 'finalizer', // Калі функцыя верне 'finalizer' → ідзём у finalizer
7 }
8);
Візуалізацыя:
1 ┌──────────► [writer]
2 │
3 [reviewer] ─────► [?] ──┤
4 │
5 └──────────► [finalizer]
6
7 Функцыя shouldContinue вырашае, якую стрэлку абраць
Функцыя shouldContinue падрабязна
Гэта "мозг" нашага ўмоўнага пераходу. Ён аналізуе бягучы стан і вырашае, куды ісці далей. Функцыя павінна вярнуць радок, які адпавядае аднаму з ключоў у аб'екце супастаўлення, вызначаным у addConditionalEdges.
1function shouldContinue(state: ResearchStateType): 'writer' | 'finalizer' {
2 // ▲
3 // └── Вяртае адзін з гэтых радкоў
4
5 const review = state.review.toLowerCase();
6 const maxIterations = 3;
7
8 // ═══════════════════════════════════════════════════════════════
9 // УМОВА 1: Рэцэнзент сказаў "ЗАЦВЕРДЖАНА"
10 // ═══════════════════════════════════════════════════════════════
11 if (review.includes('зацверджана') || review.includes('approved')) {
12 console.log('Артыкул зацверджаны');
13 return 'finalizer'; // ← Ідзём завяршаць
14 }
15
16 // ═══════════════════════════════════════════════════════════════
17 // УМОВА 2: Занадта шмат спробаў (абарона ад бясконцага цыкла)
18 // ═══════════════════════════════════════════════════════════════
19 if (state.iterationCount >= maxIterations) {
20 console.log('Дасягнуты максімум ітэрацый');
21 return 'finalizer'; // ← Прымусова завяршаем
22 }
23
24 // ═══════════════════════════════════════════════════════════════
25 // ІНАКШ: Патрэбна дапрацоўка
26 // ═══════════════════════════════════════════════════════════════
27 console.log('Патрабуецца дапрацоўка');
28 return 'writer'; // ← Вяртаемся да пісьменніка
29}
Блок-схема рашэння:
1 ┌───────────────────┐
2 │ shouldContinue │
3 │ (функцыя) │
4 └─────────┬─────────┘
5 │
6 ▼
7 ┌──────────────────────────────┐
8 │ Ці ёсць "зацверджана" │
9 │ у рэцэнзіі? │
10 └──────────────┬───────────────┘
11 │
12 ┌────────────┴────────────┐
13 │ │
14 ТАК НЕ
15 │ │
16 ▼ ▼
17 ┌────────────┐ ┌──────────────────────────┐
18 │ return │ │ Ці iterationCount >= 3? │
19 │'finalizer' │ └──────────────┬───────────┘
20 │ │ │
21 └────────────┘ ┌────────────┴────────────┐
22 │ │
23 ТАК НЕ
24 │ │
25 ▼ ▼
26 ┌────────────┐ ┌────────────┐
27 │ return │ │ return │
28 │'finalizer' │ │ 'writer' │
29 │ │ │ │
30 └────────────┘ └────────────┘
Як будуецца граф
Цяпер давайце злучым усё разам! Пабудова графа ў LangGraph ідзе па простым шаблоне: стварыць граф, дадаць вузлы, злучыць іх пераходамі і скампіляваць.
Паслядоўнасць стварэння:
1// ═══════════════════════════════════════════════════════════════
2// КРОК 1: Ствараем StateGraph з нашым станам
3// ═══════════════════════════════════════════════════════════════
4const workflow = new StateGraph(ResearchState)
5
6 // ═══════════════════════════════════════════════════════════
7 // КРОК 2: Дадаем вузлы (рэгіструем функцыі)
8 // ═══════════════════════════════════════════════════════════
9 .addNode('researcher', researcherNode) // Імя 'researcher' → функцыя
10 .addNode('writer', writerNode) // Імя 'writer' → функцыя
11 .addNode('reviewer', reviewerNode) // Імя 'reviewer' → функцыя
12 .addNode('finalizer', finalizerNode) // Імя 'finalizer' → функцыя
13
14 // ═══════════════════════════════════════════════════════════
15 // КРОК 3: Дадаем простыя пераходы
16 // ═══════════════════════════════════════════════════════════
17 .addEdge('__start__', 'researcher') // Пачатак → Даследчык
18 .addEdge('researcher', 'writer') // Даследчык → Пісьменнік
19 .addEdge('writer', 'reviewer') // Пісьменнік → Рэцэнзент
20
21 // ═══════════════════════════════════════════════════════════
22 // КРОК 4: Дадаем умоўны пераход
23 // ═══════════════════════════════════════════════════════════
24 .addConditionalEdges('reviewer', shouldContinue, {
25 writer: 'writer', // Калі 'writer' → назад да пісьменніка
26 finalizer: 'finalizer', // Калі 'finalizer' → да фіналізатара
27 })
28
29 // ═══════════════════════════════════════════════════════════
30 // КРОК 5: Дадаем канчатковы пераход
31 // ═══════════════════════════════════════════════════════════
32 .addEdge('finalizer', '__end__'); // Фіналізатар → Канец
Спецыяльныя вузлы:
1┌─────────────────────────────────────────────────────────────┐
2│ '__start__' │ Віртуальны пачатковы вузел │
3│ │ LangGraph аўтаматычна пачынае з яго │
4├───────────────┼─────────────────────────────────────────────┤
5│ '__end__' │ Віртуальны канчатковы вузел │
6│ │ Калі дайшлі да яго - граф завяршыўся │
7└─────────────────────────────────────────────────────────────┘
ЧАСТКА 5: Кампіляцыя і запуск
Мы вызначылі наш стан, вузлы і пераходы. Цяпер час ператварыць гэты чарцёж у працуючае прыкладанне!
Што такое compile()?
compile() - гэта працэс пераўтварэння апісання графа ў выканальную праграму. Пакуль вы не выклічаце compile(), у вас ёсць толькі апісанне таго, што вы хочаце пабудаваць - а не рэальна працуючая сістэма.
1// Апісанне графа (blueprint)
2const workflow = new StateGraph(ResearchState)
3 .addNode(...)
4 .addEdge(...);
5
6// Кампіляцыя ў выканальную праграму
7const app = workflow.compile();
8// ▲
9// └── Цяпер гэта можна запускаць!
Аналогія: Рэцэпт vs. Гатовая страва
1workflow (апісанне) app (скампіляваная праграма)
2──────────────────── ──────────────────────────────
3Рэцэпт на паперы Гатовая піца
4 - Як рабіць цеста (можна есці)
5 - Што дадаваць
6 - Як выпякаць
7
8Нельга есці! Можна есці!
Checkpointer (MemorySaver)
Што гэта такое?
Checkpointer - гэта механізм для захавання стану пасля кожнага кроку.
1import { MemorySaver } from '@langchain/langgraph';
2
3const checkpointer = new MemorySaver();
4
5const app = workflow.compile({
6 checkpointer, // ← Дадаем checkpointer
7});
Навошта гэта патрэбна?
1БЕЗ CHECKPOINTER:
2═══════════════════════════════════════════════════════════════
3
4 Запуск 1: START → researcher → writer → ... → END
5
6 Запуск 2: START → researcher → ... (усё з пачатку!)
7
8 Немагчыма працягнуць з месца спынення
9 Немагчыма праглядзець гісторыю
10
11
12З CHECKPOINTER:
13═══════════════════════════════════════════════════════════════
14
15 Запуск 1: START → researcher → [ЗАХАВАНА]
16 │
17 thread_id: "chat-123"
18
19 Запуск 2: [АДНАЎЛЕННЕ] → writer → reviewer → ...
20 │
21 thread_id: "chat-123"
22
23 Можна працягнуць з месца спынення
24 Можна праглядзець гісторыю
25 Можна мець некалькі незалежных "размоў"
Thread ID - ідэнтыфікатар "трэда"
1const config = {
2 configurable: {
3 thread_id: 'article-1', // Унікальны ID для гэтай "размовы"
4 },
5};
6
7// Кожны thread_id - гэта асобная гісторыя
8// 'article-1' - пра адзін артыкул
9// 'article-2' - пра іншы артыкул
10// Яны не перасякаюцца!
Запуск графа
Нарэшце! Давайце запусцім наш граф. Функцыя invoke() прымае пачатковыя значэнні стану і канфігурацыю, а затым выконвае ўвесь граф ад пачатку да канца.
Функцыя invoke()
1const result = await app.invoke(
2 // Пачатковыя даныя для стану
3 {
4 topic: 'Перавагі LangChain',
5 messages: [new HumanMessage('Перавагі LangChain')],
6 },
7 // Канфігурацыя
8 {
9 configurable: {
10 thread_id: 'article-1',
11 },
12 }
13);
Што адбываецца пры invoke():
1┌─────────────────────────────────────────────────────────────────┐
2│ invoke() │
3└───────────────────────────────┬─────────────────────────────────┘
4 │
5 ▼
6┌─────────────────────────────────────────────────────────────────┐
7│ 1. ІНІЦЫЯЛІЗАЦЫЯ СТАНУ │
8│ state = { │
9│ topic: "Перавагі LangChain", │
10│ messages: [HumanMessage], │
11│ research: "", ← default │
12│ draft: "", ← default │
13│ review: "", ← default │
14│ finalArticle: "", ← default │
15│ iterationCount: 0, ← default │
16│ } │
17└───────────────────────────────┬─────────────────────────────────┘
18 │
19 ▼
20┌─────────────────────────────────────────────────────────────────┐
21│ 2. ПАЧАТАК: __start__ → researcher │
22│ Выконваем researcherNode(state) │
23│ → Атрымліваем абнаўленне {research: "...", messages: [...]} │
24│ → Аб'ядноўваем са станам праз reducers │
25│ Захоўваем checkpoint │
26└───────────────────────────────┬─────────────────────────────────┘
27 │
28 ▼
29┌─────────────────────────────────────────────────────────────────┐
30│ 3. ПЕРАХОД: researcher → writer │
31│ Выконваем writerNode(state) │
32│ → Атрымліваем абнаўленне {draft: "...", iterationCount: 1} │
33│ → Аб'ядноўваем са станам │
34│ Захоўваем checkpoint │
35└───────────────────────────────┬─────────────────────────────────┘
36 │
37 ▼
38┌─────────────────────────────────────────────────────────────────┐
39│ 4. ПЕРАХОД: writer → reviewer │
40│ Выконваем reviewerNode(state) │
41│ → Атрымліваем абнаўленне {review: "..."} │
42│ → Аб'ядноўваем са станам │
43│ Захоўваем checkpoint │
44└───────────────────────────────┬─────────────────────────────────┘
45 │
46 ▼
47┌─────────────────────────────────────────────────────────────────┐
48│ 5. УМОЎНЫ ПЕРАХОД: shouldContinue(state) │
49│ → Вяртае 'writer' або 'finalizer' │
50│ → Калі 'writer' - вяртаемся да кроку 3 │
51│ → Калі 'finalizer' - ідзём далей │
52└───────────────────────────────┬─────────────────────────────────┘
53 │ (калі 'finalizer')
54 ▼
55┌─────────────────────────────────────────────────────────────────┐
56│ 6. ПЕРАХОД: reviewer → finalizer │
57│ Выконваем finalizerNode(state) │
58│ → Атрымліваем абнаўленне {finalArticle: "..."} │
59│ → Аб'ядноўваем са станам │
60│ Захоўваем checkpoint │
61└───────────────────────────────┬─────────────────────────────────┘
62 │
63 ▼
64┌─────────────────────────────────────────────────────────────────┐
65│ 7. КАНЕЦ: finalizer → __end__ │
66│ Граф завяршыўся │
67│ Вяртаем фінальны стан │
68└───────────────────────────────┬─────────────────────────────────┘
69 │
70 ▼
71 return state (поўны)
ЧАСТКА 6: FAQ - Частыя пытанні
Чаму reducer для messages дадае, а для topic замяняе?
Паведамленні - гэта гісторыя. Мы хочам захаваць усе паведамленні за ўвесь працэс.
Topic - гэта бягучая тэма. Калі тэма змянілася - старая не патрэбна.
Што будзе, калі я не ўкажу reducer?
LangGraph выкарыстае паводзіны па змаўчанні - замена (як для topic).
Нашто патрэбен iterationCount?
Каб абараніцца ад бясконцага цыкла. Калі рэцэнзент ніколі не скажа "зацверджана", праграма будзе бясконца круціцца паміж writer і reviewer.
Ці можа адзін вузел выклікаць іншы напрамую?
Не. Вузлы не ведаюць адзін пра аднаго. Яны толькі чытаюць/пішуць стан. LangGraph сам вырашае, хто выконваецца наступным, на аснове пераходаў.
Што такое start і end?
Гэта спецыяльныя віртуальныя вузлы:
__start__ - адкуль пачынаецца граф
__end__ - дзе граф завяршаецца
Яны не выконваюць код - толькі пазначаюць межы.
Ці можна мець некалькі канчатковых вузлоў?
Так! Напрыклад:
1.addEdge('success', '__end__')
2.addEdge('error', '__end__')
Выснова
Віншуем! Вы вывучылі асноўныя канцэпцыі LangGraph. Давайце падвядзём вынікі таго, што мы разгледзелі:
| Канцэпцыя | Што робіць |
|---|
| State + Annotation | Вызначае структуру даных, якія перадаюцца паміж агентамі |
| Reducer | Вызначае, як аб'ядноўваць новыя даныя з існуючымі |
| Nodes (Вузлы) | Функцыі, якія апрацоўваюць стан |
| Edges (Пераходы) | Правілы, якія вызначаюць парадак выканання |
| Conditional Edges | Дынамічны выбар наступнага вузла |
| Checkpointer | Захаванне стану для працягу пазней |
Што далей?
Цяпер, калі вы разумееце асновы, вы можаце:
- Пабудаваць уласную агентную сістэму - Пачніце з простага графа з двух вузлоў і паступова дадавайце складанасць
- Эксперыментаваць з рознымі патокамі - Паспрабуйце ствараць графы з некалькімі ўмоўнымі галінамі
- Дадаць персістэнтнасць - Выкарыстоўвайце
MemorySaver або checkpointer-ы на базе дадзеных для прадакшна
- Даследаваць прасунутыя магчымасці - Вывучыце падграфы, паралельнае выкананне і патэрны з удзелам чалавека
Ключавыя высновы
- Думайце графамі: Разбівайце ваш AI-працоўны працэс на дыскрэтныя крокі (вузлы), злучаныя правіламі (пераходамі)
- Стан - гэта ўсё: Уся камунікацыя паміж вузламі адбываецца праз агульны стан
- Reducer-ы важныя: Выбірайце правільны reducer для кожнага поля - дадаванне для гісторыі, замена для бягучых значэнняў
- Цыклы дазваляюць ітэрацыі: У адрозненне ад простых ланцужкоў, графы могуць вяртацца назад для дапрацоўкі
Прыклад з напісаннем артыкула, які мы пабудавалі, дэманструе рэальны патэрн: даследаванне → напісанне → рэцэнзаванне → (паўтарыць пры неабходнасці) → фіналізацыя. Гэты ж патэрн прымяняецца да многіх AI-прыкладанняў: рэцэнзаванне кода, мадэрацыя кантэнту, шматкрокавае разважанне і многае іншае.
Паспяховага кодзінгу! 🚀