LangGraph: Дапаможнік для пачаткоўцаў
.jpg?alt=media&token=fa69b8b3-19be-4147-9583-843a5bdcffcb)
Што вы даведаецеся: Як будаваць AI-агентныя сістэмы з LangGraph - ад базавых канцэпцый да працуючага кода. Мы створым пайплайн для напісання артыкулаў з некалькімі AI-агентамі, якія супрацоўнічаюць, правяраюць працу адзін аднаго і ітэруюць, пакуль вынік не будзе ідэальным.
ЧАСТКА 1: Базавыя канцэпцыі
Што такое граф?
Перш чым разбірацца з LangGraph, трэба зразумець, што такое граф у праграмаванні.
Граф - гэта структура даных
Уявіце карту метро:
- Станцыі - гэта вузлы (nodes)
- Лініі паміж станцыямі - гэта пераходы (edges)
text1 Звычайны граф: Карта метро (аналогія): 2 3 [A] [Плошча Перамогі] 4 │ │ 5 ▼ ▼ 6 [B]───────►[C] [Кастрычніцкая]───►[Купалаўская] 7 │ │ 8 ▼ ▼ 9 [D] [Інстытут культуры]
У LangGraph:
- Вузлы - гэта функцыі (агенты), якія нешта робяць.
- Пераходы - гэта правілы, хто пасля каго працуе.
Навошта патрэбны LangGraph?
Праблема: Звычайныя AI-праграмы лінейныя
text1Звычайны ланцужок (Chain): 2 3 Пытанне → LLM → Адказ 4 5 або 6 7 Пытанне → Інструмент 1 → LLM → Інструмент 2 → Адказ
Гэта працуе для простых задач, але што, калі:
- Трэба вярнуцца назад і перарабіць?
- Трэба праверыць вынік і, магчыма, паўтарыць?
- Трэба некалькі агентаў з рознымі ролямі?
Рашэнне: LangGraph дазваляе ствараць цыклы
text1LangGraph (Граф): 2 3 ┌──────────────────────┐ 4 │ │ 5 ▼ │ 6 Пытанне → [Даследчык] → [Пісьменнік] → [Рэцэнзент] 7 │ 8 ▼ 9 Добра? ───НЕ───► назад да Пісьменніка 10 │ 11 ТАК 12 │ 13 ▼ 14 [Вынік]
Што такое "стан" (State)?
Стан - гэта памяць праграмы
Уявіце, што вы пішаце артыкул з камандай:
- Даследчык сабраў матэрыялы → трэба некуды запісаць.
- Пісьменнік напісаў чарнавік → трэба перадаць рэцэнзенту.
- Рэцэнзент напісаў заўвагі → трэба вярнуць пісьменніку.
Стан - гэта "кошык", куды ўсе кладуць свае вынікі і адкуль бяруць даныя.
text1┌─────────────────────────────────────────────────────────────┐ 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 - гэта спосаб апісання структуры стану. Гэта як стварэнне формы з палямі.
Аналогія: Анкета
Уявіце, што вы ствараеце анкету:
text1┌─────────────────────────────────────────┐ 2│ АНКЕТА РАБОТНІКА │ 3├─────────────────────────────────────────┤ 4│ Імя: ___________________ │ 5│ Узрост: _____ │ 6│ Вопыт працы (гады): _____ │ 7│ Спіс навыкаў: ___, ___, ___ │ 8└─────────────────────────────────────────┘
У кодзе гэта выглядае так:
typescript1// АНАЛОГІЯ: Стварэнне формы анкеты 2const Anketa = { 3 imya: '', // Тэкставае поле 4 uzrost: 0, // Лік 5 vopyt: 0, // Лік 6 navyki: [], // Спіс 7};
У LangGraph: Annotation.Root
У LangGraph стан ствараецца з дапамогай Annotation.Root(). Гэта спецыяльная функцыя, якая апісвае структуру вашых даных - якія палі існуюць і як яны павінны абнаўляцца.
typescript1import { 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?
Праблема: Як аб'ядноўваць даныя?
Калі некалькі агентаў пішуць у адно поле, што рабіць?
text1Агент 1 піша: topic = "Тэма А" 2Агент 2 піша: topic = "Тэма Б" 3 4Што павінна быць у topic? "Тэма А"? "Тэма Б"? "Тэма А + Тэма Б"?
Рашэнне: Reducer - функцыя, якая вырашае
Reducer - гэта функцыя, якая атрымлівае:
- Бягучае значэнне (што ўжо ёсць)
- Новае значэнне (што прыйшло)
І вяртае: вынік (што захаваць)
typescript1reducer: (current, update) => результат; 2// ▲ ▲ ▲ 3// │ │ └── Што захаваецца 4// │ └── Новае значэнне 5// └── Бягучае значэнне
Прыклады Reducer-аў:
1. Reducer "ЗАМЯНІЦЬ" (replace)
Новае значэнне поўнасцю замяняе старое.
typescript1// Код: 2reducer: (current, update) => update; 3 4// Прыклад: 5// Бягучае: "Тэма А" 6// Новае: "Тэма Б" 7// Вынік: "Тэма Б" ← новае замяніла старое
Аналогія: Вы перазапісваеце файл - новы змест замяняе стары.
text1файл.txt 2────────────── 3Было: "Стары тэкст" 4 ▼ (перазапіс) 5Стала: "Новы тэкст"
2. Reducer "ДАДАЦЬ" (append)
Новыя элементы дадаюцца да існуючых.
typescript1// Код: 2reducer: (current, update) => [...current, ...update]; 3 4// Прыклад: 5// Бягучае: ["Паведамленне 1", "Паведамленне 2"] 6// Новае: ["Паведамленне 3"] 7// Вынік: ["Паведамленне 1", "Паведамленне 2", "Паведамленне 3"]
Аналогія: Вы дадаеце новыя запісы ў дзённік - старыя застаюцца.
text1дзённік.txt 2────────────── 3Было: "Панядзелак: зрабіў А" 4 "Аўторак: зрабіў Б" 5 ▼ (дадаём) 6Стала: "Панядзелак: зрабіў А" 7 "Аўторак: зрабіў Б" 8 "Серада: зрабіў В" ← дадалося
Поўны прыклад стану:
typescript1const 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-аў:
text1┌─────────────────────────────────────────────────────────────────┐ 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 - гэта функцыя, якая вяртае пачатковае значэнне поля.
typescript1default: () => значэнне
Чаму функцыя, а не проста значэнне?
Гэта распаўсюджаная пастка JavaScript. Калі вы выкарыстоўваеце просты аб'ект або масіў як default, усе экзэмпляры будуць спасылацца на адну і тую ж спасылку - змены ў адным месцы паўплываюць на ўсе астатнія!
typescript1// ДРЭННА: Калі гэта аб'ект або масіў 2default: [] // Усе экзэмпляры будуць спасылацца на адзін масіў! 3 4// ДОБРА: Функцыя стварае новы масіў кожны раз 5default: () => [] // Кожны экзэмпляр атрымае свой масіў
Прыклады:
typescript1// Для радка 2default: () => '' // Пусты радок 3 4// Для ліку 5default: () => 0 // Нуль 6 7// Для масіва 8default: () => [] // Пусты масіў 9 10// Для аб'екта 11default: () => ({}) // Пусты аб'ект 12 13// Для boolean 14default: () => false // false
ЧАСТКА 3: Вузлы (Nodes) - Агенты
Што такое вузел?
Вузел (Node) - гэта функцыя, якая:
- Атрымлівае поўны стан
- Робіць нейкую працу (напрыклад, выклікае LLM)
- Вяртае частковае абнаўленне стану
Аналогія: Работнік на канвееры
text1┌─────────────────────────────────────────────────────────────┐ 2│ КАНВЕЕР │ 3├─────────────────────────────────────────────────────────────┤ 4│ │ 5│ [Каробка] ──► [Работнік 1] ───────► [Работнік 2] ──────► │ 6│ │ │ │ 7│ ▼ ▼ │ 8│ Дадае дэталь А Дадае дэталь Б │ 9│ │ 10└─────────────────────────────────────────────────────────────┘ 11 12Кожны работнік: 131. Бачыць, што ўжо зроблена (стан) 142. Робіць сваю частку работы 153. Перадае далей з дапаўненнямі
Структура вузла ў кодзе:
typescript1async 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-аў, якія вы вызначылі.
typescript1// Поўны стан мае 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)
Даследчык - першы агент у нашым пайплайне. Ён бярэ тэму і генеруе даследчыя матэрыялы, якія будуць выкарыстоўвацца пісьменнікам.
typescript1async 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}
Што адбываецца:
text1УВАХОД (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 дапамагае адсочваць, колькі разоў артыкул быў перапісаны.
typescript1async 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}
Што адбываецца пры паўторным выкліку:
text1ПЕРШЫ ВЫКЛІК: ДРУГІ ВЫКЛІК (пасля рэцэнзіі): 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)
Рэцэнзент ацэньвае чарнавік і вырашае, ці гатовы ён да публікацыі. Ключавы момант тут - вывад: калі рэцэнзія змяшчае "ЗАЦВЕРДЖАНА", артыкул ідзе на фіналізацыю. У адваротным выпадку ён вяртаецца да пісьменніка для паляпшэнняў. Менавіта гэта дазваляе ствараць цыкл у нашым графе.
typescript1async 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}
Два магчымыя вынікі:
text1ВАРЫЯНТ А: Артыкул добры ВАРЫЯНТ Б: Трэба дапрацаваць 2┌─────────────────────────┐ ┌─────────────────────────┐ 3│ review: "ЗАЦВЕРДЖАНА. │ │ review: "Дапрацаваць: │ 4│ Артыкул выдатны!" │ │ 1. Дадай прыклады │ 5│ │ │ 2. Удакладні тэрміны" │ 6└─────────────────────────┘ └─────────────────────────┘ 7 │ │ 8 ▼ ▼ 9 Ідзём да Вяртаемся да 10 finalizer writer
Вузел 4: Фіналізатар (finalizerNode)
Фіналізатар - найпрасцейшы вузел - ён проста капіюе зацверджаны чарнавік у поле finalArticle, пазначаючы канец нашага працоўнага працэсу. Ён выклікаецца, калі рэцэнзент зацвярджае артыкул.
typescript1async 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}
Што адбываецца:
text1УВАХОД: ВЫХАД: 2┌─────────────────────┐ ┌─────────────────────┐ 3│ draft: "Гатовы..." │ ──────► │ finalArticle: │ 4│ finalArticle: "" │ │ "Гатовы..." │ 5└─────────────────────┘ └─────────────────────┘
ЧАСТКА 4: Пераходы (Edges)
Што такое пераход?
Пераход (Edge) - гэта правіла, якое кажа: "Пасля гэтага вузла выконваецца той вузел".
Аналогія: Стрэлкі на схеме
text1Рэцэпт пічы: 2 3 [Зрабіць цеста] ─────► [Дадаць соус] ─────► [Дадаць сыр] ─────► [Выпекчы] 4 │ │ │ │ 5 ▼ ▼ ▼ ▼ 6 ПЕРАХОД ПЕРАХОД ПЕРАХОД КАНЕЦ
Два тыпы пераходаў у LangGraph:
1. Простыя пераходы (addEdge)
Заўсёды ідзём у адзін і той жа вузел.
typescript1workflow.addEdge('researcher', 'writer'); 2// ▲ ▲ 3// │ └── Куды ідзём 4// └── Адкуль ідзём 5 6// Значыць: Пасля researcher ЗАЎСЁДЫ ідзём да writer
Візуалізацыя:
text1 [researcher] ───────────────► [writer] 2 (заўсёды)
2. Умоўныя пераходы (addConditionalEdges)
Выбар наступнага вузла залежыць ад стану.
typescript1workflow.addConditionalEdges( 2 'reviewer', // Адкуль ідзём 3 shouldContinue, // Функцыя, якая вырашае куды 4 { 5 writer: 'writer', // Калі функцыя верне 'writer' → ідзём у writer 6 finalizer: 'finalizer', // Калі функцыя верне 'finalizer' → ідзём у finalizer 7 } 8);
Візуалізацыя:
text1 ┌──────────► [writer] 2 │ 3 [reviewer] ─────► [?] ──┤ 4 │ 5 └──────────► [finalizer] 6 7 Функцыя shouldContinue вырашае, якую стрэлку абраць
Функцыя shouldContinue падрабязна
Гэта "мозг" нашага ўмоўнага пераходу. Ён аналізуе бягучы стан і вырашае, куды ісці далей. Функцыя павінна вярнуць радок, які адпавядае аднаму з ключоў у аб'екце супастаўлення, вызначаным у addConditionalEdges.
typescript1function 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}
Блок-схема рашэння:
text1 ┌───────────────────┐ 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 ідзе па простым шаблоне: стварыць граф, дадаць вузлы, злучыць іх пераходамі і скампіляваць.
Паслядоўнасць стварэння:
typescript1// ═══════════════════════════════════════════════════════════════ 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__'); // Фіналізатар → Канец
Спецыяльныя вузлы:
text1┌─────────────────────────────────────────────────────────────┐ 2│ '__start__' │ Віртуальны пачатковы вузел │ 3│ │ LangGraph аўтаматычна пачынае з яго │ 4├───────────────┼─────────────────────────────────────────────┤ 5│ '__end__' │ Віртуальны канчатковы вузел │ 6│ │ Калі дайшлі да яго - граф завяршыўся │ 7└─────────────────────────────────────────────────────────────┘
ЧАСТКА 5: Кампіляцыя і запуск
Мы вызначылі наш стан, вузлы і пераходы. Цяпер час ператварыць гэты чарцёж у працуючае прыкладанне!
Што такое compile()?
compile() - гэта працэс пераўтварэння апісання графа ў выканальную праграму. Пакуль вы не выклічаце compile(), у вас ёсць толькі апісанне таго, што вы хочаце пабудаваць - а не рэальна працуючая сістэма.
typescript1// Апісанне графа (blueprint) 2const workflow = new StateGraph(ResearchState) 3 .addNode(...) 4 .addEdge(...); 5 6// Кампіляцыя ў выканальную праграму 7const app = workflow.compile(); 8// ▲ 9// └── Цяпер гэта можна запускаць!
Аналогія: Рэцэпт vs. Гатовая страва
text1workflow (апісанне) app (скампіляваная праграма) 2──────────────────── ────────────────────────────── 3Рэцэпт на паперы Гатовая піца 4 - Як рабіць цеста (можна есці) 5 - Што дадаваць 6 - Як выпякаць 7 8Нельга есці! Можна есці!
Checkpointer (MemorySaver)
Што гэта такое?
Checkpointer - гэта механізм для захавання стану пасля кожнага кроку.
typescript1import { MemorySaver } from '@langchain/langgraph'; 2 3const checkpointer = new MemorySaver(); 4 5const app = workflow.compile({ 6 checkpointer, // ← Дадаем checkpointer 7});
Навошта гэта патрэбна?
text1БЕЗ 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 - ідэнтыфікатар "трэда"
typescript1const config = { 2 configurable: { 3 thread_id: 'article-1', // Унікальны ID для гэтай "размовы" 4 }, 5}; 6 7// Кожны thread_id - гэта асобная гісторыя 8// 'article-1' - пра адзін артыкул 9// 'article-2' - пра іншы артыкул 10// Яны не перасякаюцца!
Запуск графа
Нарэшце! Давайце запусцім наш граф. Функцыя invoke() прымае пачатковыя значэнні стану і канфігурацыю, а затым выконвае ўвесь граф ад пачатку да канца.
Функцыя invoke()
typescript1const 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():
text1┌─────────────────────────────────────────────────────────────────┐ 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__- дзе граф завяршаецца
Яны не выконваюць код - толькі пазначаюць межы.
Ці можна мець некалькі канчатковых вузлоў?
Так! Напрыклад:
typescript1.addEdge('success', '__end__') 2.addEdge('error', '__end__')
Выснова
Віншуем! Вы вывучылі асноўныя канцэпцыі LangGraph. Давайце падвядзём вынікі таго, што мы разгледзелі:
| Канцэпцыя | Што робіць |
|---|---|
| State + Annotation | Вызначае структуру даных, якія перадаюцца паміж агентамі |
| Reducer | Вызначае, як аб'ядноўваць новыя даныя з існуючымі |
| Nodes (Вузлы) | Функцыі, якія апрацоўваюць стан |
| Edges (Пераходы) | Правілы, якія вызначаюць парадак выканання |
| Conditional Edges | Дынамічны выбар наступнага вузла |
| Checkpointer | Захаванне стану для працягу пазней |
Што далей?
Цяпер, калі вы разумееце асновы, вы можаце:
- Пабудаваць уласную агентную сістэму - Пачніце з простага графа з двух вузлоў і паступова дадавайце складанасць
- Эксперыментаваць з рознымі патокамі - Паспрабуйце ствараць графы з некалькімі ўмоўнымі галінамі
- Дадаць персістэнтнасць - Выкарыстоўвайце
MemorySaverабо checkpointer-ы на базе дадзеных для прадакшна - Даследаваць прасунутыя магчымасці - Вывучыце падграфы, паралельнае выкананне і патэрны з удзелам чалавека
Ключавыя высновы
- Думайце графамі: Разбівайце ваш AI-працоўны працэс на дыскрэтныя крокі (вузлы), злучаныя правіламі (пераходамі)
- Стан - гэта ўсё: Уся камунікацыя паміж вузламі адбываецца праз агульны стан
- Reducer-ы важныя: Выбірайце правільны reducer для кожнага поля - дадаванне для гісторыі, замена для бягучых значэнняў
- Цыклы дазваляюць ітэрацыі: У адрозненне ад простых ланцужкоў, графы могуць вяртацца назад для дапрацоўкі
Прыклад з напісаннем артыкула, які мы пабудавалі, дэманструе рэальны патэрн: даследаванне → напісанне → рэцэнзаванне → (паўтарыць пры неабходнасці) → фіналізацыя. Гэты ж патэрн прымяняецца да многіх AI-прыкладанняў: рэцэнзаванне кода, мадэрацыя кантэнту, шматкрокавае разважанне і многае іншае.
Паспяховага кодзінгу! 🚀
Каментары
(Каб даслаць каментар залагуйцеся ў свой уліковы запіс)