Галоўная > LangGraph: Дапаможнік для пачаткоўцаў

LangGraph: Дапаможнік для пачаткоўцаў

LangChain
LangGraph
AI
tools

head

Што вы даведаецеся: Як будаваць AI-агентныя сістэмы з LangGraph - ад базавых канцэпцый да працуючага кода. Мы створым пайплайн для напісання артыкулаў з некалькімі AI-агентамі, якія супрацоўнічаюць, правяраюць працу адзін аднаго і ітэруюць, пакуль вынік не будзе ідэальным.


ЧАСТКА 1: Базавыя канцэпцыі

Што такое граф?

Перш чым разбірацца з LangGraph, трэба зразумець, што такое граф у праграмаванні.

Граф - гэта структура даных

Уявіце карту метро:

  • Станцыі - гэта вузлы (nodes)
  • Лініі паміж станцыямі - гэта пераходы (edges)
text
1    Звычайны граф:              Карта метро (аналогія):
2
3        [A]                         [Плошча Перамогі]
4         │                                 │
5         ▼                                 ▼
6        [B]───────►[C]              [Кастрычніцкая]───►[Купалаўская]
7         │                                 │
8         ▼                                 ▼
9        [D]                         [Інстытут культуры]

У LangGraph:

  • Вузлы - гэта функцыі (агенты), якія нешта робяць.
  • Пераходы - гэта правілы, хто пасля каго працуе.

Навошта патрэбны LangGraph?

Праблема: Звычайныя AI-праграмы лінейныя

text
1Звычайны ланцужок (Chain):
2
3    Пытанне → LLM → Адказ
4
5    або
6
7    Пытанне → Інструмент 1 → LLM → Інструмент 2 → Адказ

Гэта працуе для простых задач, але што, калі:

  • Трэба вярнуцца назад і перарабіць?
  • Трэба праверыць вынік і, магчыма, паўтарыць?
  • Трэба некалькі агентаў з рознымі ролямі?

Рашэнне: LangGraph дазваляе ствараць цыклы

text
1LangGraph (Граф):
2
3                    ┌──────────────────────┐
4                    │                      │
5                    ▼                      │
6    Пытанне → [Даследчык] → [Пісьменнік] → [Рэцэнзент]
789                                          Добра? ───НЕ───► назад да Пісьменніка
1011                                              ТАК
121314                                           [Вынік]

Што такое "стан" (State)?

Стан - гэта памяць праграмы

Уявіце, што вы пішаце артыкул з камандай:

  • Даследчык сабраў матэрыялы → трэба некуды запісаць.
  • Пісьменнік напісаў чарнавік → трэба перадаць рэцэнзенту.
  • Рэцэнзент напісаў заўвагі → трэба вярнуць пісьменніку.

Стан - гэта "кошык", куды ўсе кладуць свае вынікі і адкуль бяруць даныя.

text
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 - гэта спосаб апісання структуры стану. Гэта як стварэнне формы з палямі.

Аналогія: Анкета

Уявіце, што вы ствараеце анкету:

text
1┌─────────────────────────────────────────┐
2│            АНКЕТА РАБОТНІКА             │
3├─────────────────────────────────────────┤
4│ Імя: ___________________                │
5│ Узрост: _____                           │
6│ Вопыт працы (гады): _____               │
7│ Спіс навыкаў: ___, ___, ___             │
8└─────────────────────────────────────────┘

У кодзе гэта выглядае так:

typescript
1// АНАЛОГІЯ: Стварэнне формы анкеты
2const Anketa = {
3    imya: '', // Тэкставае поле
4    uzrost: 0, // Лік
5    vopyt: 0, // Лік
6    navyki: [], // Спіс
7};

У LangGraph: Annotation.Root

У LangGraph стан ствараецца з дапамогай Annotation.Root(). Гэта спецыяльная функцыя, якая апісвае структуру вашых даных - якія палі існуюць і як яны павінны абнаўляцца.

typescript
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?

Праблема: Як аб'ядноўваць даныя?

Калі некалькі агентаў пішуць у адно поле, што рабіць?

text
1Агент 1 піша: topic = "Тэма А"
2Агент 2 піша: topic = "Тэма Б"
3
4Што павінна быць у topic? "Тэма А"? "Тэма Б"? "Тэма А + Тэма Б"?

Рашэнне: Reducer - функцыя, якая вырашае

Reducer - гэта функцыя, якая атрымлівае:

  1. Бягучае значэнне (што ўжо ёсць)
  2. Новае значэнне (што прыйшло)

І вяртае: вынік (што захаваць)

typescript
1reducer: (current, update) => результат;
2//         ▲         ▲           ▲
3//         │         │           └── Што захаваецца
4//         │         └── Новае значэнне
5//         └── Бягучае значэнне

Прыклады Reducer-аў:

1. Reducer "ЗАМЯНІЦЬ" (replace)

Новае значэнне поўнасцю замяняе старое.

typescript
1// Код:
2reducer: (current, update) => update;
3
4// Прыклад:
5// Бягучае: "Тэма А"
6// Новае:   "Тэма Б"
7// Вынік:   "Тэма Б"  ← новае замяніла старое

Аналогія: Вы перазапісваеце файл - новы змест замяняе стары.

text
1файл.txt
2──────────────
3Было:   "Стары тэкст"
4        ▼ (перазапіс)
5Стала:  "Новы тэкст"
2. Reducer "ДАДАЦЬ" (append)

Новыя элементы дадаюцца да існуючых.

typescript
1// Код:
2reducer: (current, update) => [...current, ...update];
3
4// Прыклад:
5// Бягучае: ["Паведамленне 1", "Паведамленне 2"]
6// Новае:   ["Паведамленне 3"]
7// Вынік:   ["Паведамленне 1", "Паведамленне 2", "Паведамленне 3"]

Аналогія: Вы дадаеце новыя запісы ў дзённік - старыя застаюцца.

text
1дзённік.txt
2──────────────
3Было:   "Панядзелак: зрабіў А"
4        "Аўторак: зрабіў Б"
5        ▼ (дадаём)
6Стала:  "Панядзелак: зрабіў А"
7        "Аўторак: зрабіў Б"
8        "Серада: зрабіў В"  ← дадалося

Поўны прыклад стану:

typescript
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-аў:

text
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 - гэта функцыя, якая вяртае пачатковае значэнне поля.

typescript
1default: () => значэнне

Чаму функцыя, а не проста значэнне?

Гэта распаўсюджаная пастка JavaScript. Калі вы выкарыстоўваеце просты аб'ект або масіў як default, усе экзэмпляры будуць спасылацца на адну і тую ж спасылку - змены ў адным месцы паўплываюць на ўсе астатнія!

typescript
1// ДРЭННА: Калі гэта аб'ект або масіў
2default: []  // Усе экзэмпляры будуць спасылацца на адзін масіў!
3
4// ДОБРА: Функцыя стварае новы масіў кожны раз
5default: () => []  // Кожны экзэмпляр атрымае свой масіў

Прыклады:

typescript
1// Для радка
2default: () => ''        // Пусты радок
3
4// Для ліку
5default: () => 0         // Нуль
6
7// Для масіва
8default: () => []        // Пусты масіў
9
10// Для аб'екта
11default: () => ({})      // Пусты аб'ект
12
13// Для boolean
14default: () => false     // false

ЧАСТКА 3: Вузлы (Nodes) - Агенты

Што такое вузел?

Вузел (Node) - гэта функцыя, якая:

  1. Атрымлівае поўны стан
  2. Робіць нейкую працу (напрыклад, выклікае LLM)
  3. Вяртае частковае абнаўленне стану

Аналогія: Работнік на канвееры

text
1┌─────────────────────────────────────────────────────────────┐
2│                         КАНВЕЕР                             │
3├─────────────────────────────────────────────────────────────┤
4│                                                             │
5│   [Каробка] ──► [Работнік 1] ───────► [Работнік 2] ──────►  │
6│                    │                      │                 │
7│                    ▼                      ▼                 │
8│              Дадае дэталь А         Дадае дэталь Б          │
9│                                                             │
10└─────────────────────────────────────────────────────────────┘
11
12Кожны работнік:
131. Бачыць, што ўжо зроблена (стан)
142. Робіць сваю частку работы
153. Перадае далей з дапаўненнямі

Структура вузла ў кодзе:

typescript
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-аў, якія вы вызначылі.

typescript
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)

Даследчык - першы агент у нашым пайплайне. Ён бярэ тэму і генеруе даследчыя матэрыялы, якія будуць выкарыстоўвацца пісьменнікам.

typescript
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}

Што адбываецца:

text
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 дапамагае адсочваць, колькі разоў артыкул быў перапісаны.

typescript
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}

Што адбываецца пры паўторным выкліку:

text
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)

Рэцэнзент ацэньвае чарнавік і вырашае, ці гатовы ён да публікацыі. Ключавы момант тут - вывад: калі рэцэнзія змяшчае "ЗАЦВЕРДЖАНА", артыкул ідзе на фіналізацыю. У адваротным выпадку ён вяртаецца да пісьменніка для паляпшэнняў. Менавіта гэта дазваляе ствараць цыкл у нашым графе.

typescript
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}

Два магчымыя вынікі:

text
1ВАРЫЯНТ А: Артыкул добры           ВАРЫЯНТ Б: Трэба дапрацаваць
2┌─────────────────────────┐        ┌─────────────────────────┐
3│ review: "ЗАЦВЕРДЖАНА.   │        │ review: "Дапрацаваць:   │
4│ Артыкул выдатны!"       │        │ 1. Дадай прыклады       │
5│                         │        │ 2. Удакладні тэрміны"   │
6└─────────────────────────┘        └─────────────────────────┘
7         │                                   │
8         ▼                                   ▼
9    Ідзём да                           Вяртаемся да
10    finalizer                          writer

Вузел 4: Фіналізатар (finalizerNode)

Фіналізатар - найпрасцейшы вузел - ён проста капіюе зацверджаны чарнавік у поле finalArticle, пазначаючы канец нашага працоўнага працэсу. Ён выклікаецца, калі рэцэнзент зацвярджае артыкул.

typescript
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}

Што адбываецца:

text
1УВАХОД:                            ВЫХАД:
2┌─────────────────────┐            ┌─────────────────────┐
3│ draft: "Гатовы..."  │ ──────►    │ finalArticle:       │
4│ finalArticle: ""    │            │   "Гатовы..."       │
5└─────────────────────┘            └─────────────────────┘

ЧАСТКА 4: Пераходы (Edges)

Што такое пераход?

Пераход (Edge) - гэта правіла, якое кажа: "Пасля гэтага вузла выконваецца той вузел".

Аналогія: Стрэлкі на схеме

text
1Рэцэпт пічы:
2
3    [Зрабіць цеста] ─────► [Дадаць соус] ─────► [Дадаць сыр] ─────► [Выпекчы]
4           │                     │                    │                 │
5           ▼                     ▼                    ▼                 ▼
6       ПЕРАХОД              ПЕРАХОД              ПЕРАХОД           КАНЕЦ

Два тыпы пераходаў у LangGraph:

1. Простыя пераходы (addEdge)

Заўсёды ідзём у адзін і той жа вузел.

typescript
1workflow.addEdge('researcher', 'writer');
2//                    ▲            ▲
3//                    │            └── Куды ідзём
4//                    └── Адкуль ідзём
5
6// Значыць: Пасля researcher ЗАЎСЁДЫ ідзём да writer

Візуалізацыя:

text
1    [researcher] ───────────────► [writer]
2                   (заўсёды)
2. Умоўныя пераходы (addConditionalEdges)

Выбар наступнага вузла залежыць ад стану.

typescript
1workflow.addConditionalEdges(
2    'reviewer', // Адкуль ідзём
3    shouldContinue, // Функцыя, якая вырашае куды
4    {
5        writer: 'writer', // Калі функцыя верне 'writer' → ідзём у writer
6        finalizer: 'finalizer', // Калі функцыя верне 'finalizer' → ідзём у finalizer
7    }
8);

Візуалізацыя:

text
1                            ┌──────────► [writer]
23    [reviewer] ─────► [?] ──┤
45                            └──────────► [finalizer]
6
7    Функцыя shouldContinue вырашае, якую стрэлку абраць

Функцыя shouldContinue падрабязна

Гэта "мозг" нашага ўмоўнага пераходу. Ён аналізуе бягучы стан і вырашае, куды ісці далей. Функцыя павінна вярнуць радок, які адпавядае аднаму з ключоў у аб'екце супастаўлення, вызначаным у addConditionalEdges.

typescript
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}

Блок-схема рашэння:

text
1                         ┌───────────────────┐
2                         │  shouldContinue   │
3                         │    (функцыя)      │
4                         └─────────┬─────────┘
567                    ┌──────────────────────────────┐
8                    │ Ці ёсць "зацверджана"        │
9                    │ у рэцэнзіі?                  │
10                    └──────────────┬───────────────┘
1112                      ┌────────────┴────────────┐
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 ідзе па простым шаблоне: стварыць граф, дадаць вузлы, злучыць іх пераходамі і скампіляваць.

Паслядоўнасць стварэння:

typescript
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__'); // Фіналізатар → Канец

Спецыяльныя вузлы:

text
1┌─────────────────────────────────────────────────────────────┐
2│  '__start__'  │  Віртуальны пачатковы вузел                │
3│               │  LangGraph аўтаматычна пачынае з яго        │
4├───────────────┼─────────────────────────────────────────────┤
5│  '__end__'    │  Віртуальны канчатковы вузел               │
6│               │  Калі дайшлі да яго - граф завяршыўся       │
7└─────────────────────────────────────────────────────────────┘

ЧАСТКА 5: Кампіляцыя і запуск

Мы вызначылі наш стан, вузлы і пераходы. Цяпер час ператварыць гэты чарцёж у працуючае прыкладанне!

Што такое compile()?

compile() - гэта працэс пераўтварэння апісання графа ў выканальную праграму. Пакуль вы не выклічаце compile(), у вас ёсць толькі апісанне таго, што вы хочаце пабудаваць - а не рэальна працуючая сістэма.

typescript
1// Апісанне графа (blueprint)
2const workflow = new StateGraph(ResearchState)
3    .addNode(...)
4    .addEdge(...);
5
6// Кампіляцыя ў выканальную праграму
7const app = workflow.compile();
8//    ▲
9//    └── Цяпер гэта можна запускаць!

Аналогія: Рэцэпт vs. Гатовая страва

text
1workflow (апісанне)              app (скампіляваная праграма)
2────────────────────            ──────────────────────────────
3Рэцэпт на паперы                 Гатовая піца
4   - Як рабіць цеста                (можна есці)
5   - Што дадаваць
6   - Як выпякаць
7
8Нельга есці!                     Можна есці!

Checkpointer (MemorySaver)

Што гэта такое?

Checkpointer - гэта механізм для захавання стану пасля кожнага кроку.

typescript
1import { MemorySaver } from '@langchain/langgraph';
2
3const checkpointer = new MemorySaver();
4
5const app = workflow.compile({
6    checkpointer, // ← Дадаем checkpointer
7});

Навошта гэта патрэбна?

text
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 → [ЗАХАВАНА]
1617                                   thread_id: "chat-123"
18
19    Запуск 2:   [АДНАЎЛЕННЕ] → writer → reviewer → ...
2021                thread_id: "chat-123"
22
23    Можна працягнуць з месца спынення
24    Можна праглядзець гісторыю
25    Можна мець некалькі незалежных "размоў"

Thread ID - ідэнтыфікатар "трэда"

typescript
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()

typescript
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():

text
1┌─────────────────────────────────────────────────────────────────┐
2│                         invoke()                                 │
3└───────────────────────────────┬─────────────────────────────────┘
456┌─────────────────────────────────────────────────────────────────┐
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└───────────────────────────────┬─────────────────────────────────┘
181920┌─────────────────────────────────────────────────────────────────┐
21│ 2. ПАЧАТАК: __start__ → researcher                              │
22│    Выконваем researcherNode(state)                              │
23│    → Атрымліваем абнаўленне {research: "...", messages: [...]}  │
24│    → Аб'ядноўваем са станам праз reducers                       │
25│    Захоўваем checkpoint                                          │
26└───────────────────────────────┬─────────────────────────────────┘
272829┌─────────────────────────────────────────────────────────────────┐
30│ 3. ПЕРАХОД: researcher → writer                                 │
31│    Выконваем writerNode(state)                                  │
32│    → Атрымліваем абнаўленне {draft: "...", iterationCount: 1}   │
33│    → Аб'ядноўваем са станам                                     │
34│    Захоўваем checkpoint                                         │
35└───────────────────────────────┬─────────────────────────────────┘
363738┌─────────────────────────────────────────────────────────────────┐
39│ 4. ПЕРАХОД: writer → reviewer                                   │
40│    Выконваем reviewerNode(state)                                │
41│    → Атрымліваем абнаўленне {review: "..."}                     │
42│    → Аб'ядноўваем са станам                                     │
43│    Захоўваем checkpoint                                         │
44└───────────────────────────────┬─────────────────────────────────┘
454647┌─────────────────────────────────────────────────────────────────┐
48│ 5. УМОЎНЫ ПЕРАХОД: shouldContinue(state)                        │
49│    → Вяртае 'writer' або 'finalizer'                            │
50│    → Калі 'writer' - вяртаемся да кроку 3                       │
51│    → Калі 'finalizer' - ідзём далей                             │
52└───────────────────────────────┬─────────────────────────────────┘
53                                │ (калі 'finalizer')
5455┌─────────────────────────────────────────────────────────────────┐
56│ 6. ПЕРАХОД: reviewer → finalizer                                │
57│    Выконваем finalizerNode(state)                               │
58│    → Атрымліваем абнаўленне {finalArticle: "..."}               │
59│    → Аб'ядноўваем са станам                                     │
60│    Захоўваем checkpoint                                         │
61└───────────────────────────────┬─────────────────────────────────┘
626364┌─────────────────────────────────────────────────────────────────┐
65│ 7. КАНЕЦ: finalizer → __end__                                   │
66│    Граф завяршыўся                                              │
67│    Вяртаем фінальны стан                                        │
68└───────────────────────────────┬─────────────────────────────────┘
697071                        return state (поўны)

ЧАСТКА 6: FAQ - Частыя пытанні

Чаму reducer для messages дадае, а для topic замяняе?

Паведамленні - гэта гісторыя. Мы хочам захаваць усе паведамленні за ўвесь працэс.

Topic - гэта бягучая тэма. Калі тэма змянілася - старая не патрэбна.

Што будзе, калі я не ўкажу reducer?

LangGraph выкарыстае паводзіны па змаўчанні - замена (як для topic).

Нашто патрэбен iterationCount?

Каб абараніцца ад бясконцага цыкла. Калі рэцэнзент ніколі не скажа "зацверджана", праграма будзе бясконца круціцца паміж writer і reviewer.

Ці можа адзін вузел выклікаць іншы напрамую?

Не. Вузлы не ведаюць адзін пра аднаго. Яны толькі чытаюць/пішуць стан. LangGraph сам вырашае, хто выконваецца наступным, на аснове пераходаў.

Што такое start і end?

Гэта спецыяльныя віртуальныя вузлы:

  • __start__ - адкуль пачынаецца граф
  • __end__ - дзе граф завяршаецца

Яны не выконваюць код - толькі пазначаюць межы.

Ці можна мець некалькі канчатковых вузлоў?

Так! Напрыклад:

typescript
1.addEdge('success', '__end__')
2.addEdge('error', '__end__')

Выснова

Віншуем! Вы вывучылі асноўныя канцэпцыі LangGraph. Давайце падвядзём вынікі таго, што мы разгледзелі:

КанцэпцыяШто робіць
State + AnnotationВызначае структуру даных, якія перадаюцца паміж агентамі
ReducerВызначае, як аб'ядноўваць новыя даныя з існуючымі
Nodes (Вузлы)Функцыі, якія апрацоўваюць стан
Edges (Пераходы)Правілы, якія вызначаюць парадак выканання
Conditional EdgesДынамічны выбар наступнага вузла
CheckpointerЗахаванне стану для працягу пазней

Што далей?

Цяпер, калі вы разумееце асновы, вы можаце:

  1. Пабудаваць уласную агентную сістэму - Пачніце з простага графа з двух вузлоў і паступова дадавайце складанасць
  2. Эксперыментаваць з рознымі патокамі - Паспрабуйце ствараць графы з некалькімі ўмоўнымі галінамі
  3. Дадаць персістэнтнасць - Выкарыстоўвайце MemorySaver або checkpointer-ы на базе дадзеных для прадакшна
  4. Даследаваць прасунутыя магчымасці - Вывучыце падграфы, паралельнае выкананне і патэрны з удзелам чалавека

Ключавыя высновы

  • Думайце графамі: Разбівайце ваш AI-працоўны працэс на дыскрэтныя крокі (вузлы), злучаныя правіламі (пераходамі)
  • Стан - гэта ўсё: Уся камунікацыя паміж вузламі адбываецца праз агульны стан
  • Reducer-ы важныя: Выбірайце правільны reducer для кожнага поля - дадаванне для гісторыі, замена для бягучых значэнняў
  • Цыклы дазваляюць ітэрацыі: У адрозненне ад простых ланцужкоў, графы могуць вяртацца назад для дапрацоўкі

Прыклад з напісаннем артыкула, які мы пабудавалі, дэманструе рэальны патэрн: даследаванне → напісанне → рэцэнзаванне → (паўтарыць пры неабходнасці) → фіналізацыя. Гэты ж патэрн прымяняецца да многіх AI-прыкладанняў: рэцэнзаванне кода, мадэрацыя кантэнту, шматкрокавае разважанне і многае іншае.

Паспяховага кодзінгу! 🚀

Admin, 2025-11-26
Каментары

    (Каб даслаць каментар залагуйцеся ў свой уліковы запіс)