Галоўная > LangGraph: A Beginner's Guide

LangGraph: A Beginner's Guide

AI
Beginers

head

What you'll learn: How to build AI agent systems with LangGraph - from basic concepts to working code. We'll create an article-writing pipeline with multiple AI agents that collaborate, review each other's work, and iterate until the result is perfect.


PART 1: Basic Concepts

What is a Graph?

Before diving into LangGraph, it's essential to understand what a graph is in programming.

A Graph is a Data Structure

Imagine a subway map:

  • Stations are the nodes
  • Lines between stations are the edges
text
1    Typical Graph:              Subway Map (Analogy):
2
3        [A]                         [Victory Square]
4         │                                 │
5         ▼                                 ▼
6        [B]───────►[C]              [October]───►[Kupala]
7         │                                 │
8         ▼                                 ▼
9        [D]                         [Cultural Institute]

In LangGraph:

  • Nodes are functions (agents) that perform tasks.
  • Edges are rules indicating the order in which tasks are executed.

Why is LangGraph Needed?

Problem: Traditional AI Programs are Linear

text
1Typical Chain:
2
3    Question → LLM → Answer
4
5    or
6
7    Question → Tool 1 → LLM → Tool 2 → Answer

This works for simple tasks, but what if:

  • You need to go back and redo something?
  • You need to verify the result and possibly repeat?
  • You need multiple agents with different roles?

Solution: LangGraph Allows Creating Cycles

text
1LangGraph (Graph):
2
3                    ┌──────────────────────┐
4                    │                      │
5                    ▼                      │
6    Question → [Researcher] → [Writer] → [Reviewer]
789                                          Good? ───NO───► back to Writer
1011                                              YES
121314                                           [Result]

What is "state"?

State is the memory of the program.

Imagine you are writing an article with a team:

  • The researcher gathered materials → needs to write them down somewhere.
  • The writer created a draft → needs to pass it to the reviewer.
  • The reviewer wrote comments → needs to return them to the writer.

State is like a "basket" where everyone puts their results and from which they take data.

text
1┌─────────────────────────────────────────────────────────────┐
2│                        STATE (Стан)                         │
3├─────────────────────────────────────────────────────────────┤
4│                                                             │
5│   topic: "What I Write"                                     │
6│   research: "Materials from the Researcher"                 │
7│   draft: "Draft from the Writer"                            │
8│   review: "Comments from the Reviewer"                      │
9│   finalArticle: "Final Article"                             │
10│   messages: [history of all messages]                       │
11│                                                             │
12└─────────────────────────────────────────────────────────────┘
13              ▲               ▲               ▲
14              │               │               │
15         Researcher        Writer         Reviewer
16      (reads and writes) (reads and writes) (reads and writes)

PART 2: Annotation - How State is Created

What is Annotation?

Annotation is a way of describing the structure of a state. It is like creating a form with fields.

Analogy: Survey

Imagine you are creating a survey:

text
1┌─────────────────────────────────────────┐
2│            EMPLOYEE SURVEY              │
3├─────────────────────────────────────────┤
4│ Name: ___________________               │
5│ Age: _____                              │
6│ Work Experience (years): _____          │
7│ Skills List: ___, ___, ___              │
8└─────────────────────────────────────────┘

In code, it looks like this:

typescript
1// ANALOGY: Creating a survey form
2const Survey = {
3    name: '', // Text field
4    age: 0, // Number
5    experience: 0, // Number
6    skills: [], // List
7};

In LangGraph: Annotation.Root

In LangGraph, state is created using Annotation.Root(). This is a special function that describes the structure of your data - what fields exist and how they should be updated.

typescript
1import { Annotation } from '@langchain/langgraph';
2
3const ResearchState = Annotation.Root({
4    // Each field is described through Annotation<Type>
5    topic: Annotation<string>({...}),
6    research: Annotation<string>({...}),
7    draft: Annotation<string>({...}),
8    // etc.
9});

Each field inside Annotation.Root has two important properties: reducer (how to update the value) and default (initial value). We'll explore these next.


What is a Reducer?

Problem: How to merge data?

What should you do when multiple agents write to the same field?

text
1Agent 1 writes: topic = "Topic A"
2Agent 2 writes: topic = "Topic B"
3
4What should be in topic? "Topic A"? "Topic B"? "Topic A + Topic B"?

Solution: Reducer - a function that resolves

Reducer is a function that takes:

  1. Current value (what is already there)
  2. New Value (what comes in)

It returns: result (what to keep)

typescript
1reducer: (current, update) => result;
2//         ▲         ▲           ▲
3//         │         │           └── What will be kept
4//         │         └── New value
5//         └── Current value

Examples of Reducers:

1. Reducer "REPLACE"

The new value completely replaces the old one.

typescript
1// Code:
2reducer: (current, update) => update;
3
4// Example:
5// Current: "Theme A"
6// New:     "Theme B"
7// Result:   "Theme B"  ← new replaces old

Analogy: You overwrite a file - the new content replaces the old one.

text
1file.txt
2──────────────
3Was:   "Old text"
4        ▼ (overwrite)
5Is:    "New text"
2. Reducer "APPEND"

New elements are added to the existing ones.

typescript
1// Code:
2reducer: (current, update) => [...current, ...update];
3
4// Example:
5// Current: ["Message 1", "Message 2"]
6// New:     ["Message 3"]
7// Result:   ["Message 1", "Message 2", "Message 3"]

Analogy: You add new entries to a diary - the old ones remain.

text
1diary.txt
2──────────────
3Was:    "Monday: did A"
4        "Tuesday: did B"
5             ▼ (adding)
6Became: "Monday: did A"
7        "Tuesday: did B"
8        "Wednesday: did C" ← added

Full example of the state:

typescript
1const ResearchState = Annotation.Root({
2    // ═══════════════════════════════════════════════════════
3    // FIELD: messages (messages)
4    // ═══════════════════════════════════════════════════════
5    messages: Annotation<BaseMessage[]>({
6        // REDUCER: Add new messages to existing ones
7        reducer: (current, update) => [...current, ...update],
8        // DEFAULT: Initial value - empty array
9        default: () => [],
10    }),
11    //
12    // How it works:
13    // 1. Beginning:    messages = []
14    // 2. Researcher:  messages = [] + [AIMessage] = [AIMessage]
15    // 3. Writer: messages = [AIMessage] + [AIMessage] = [AIMessage, AIMessage]
16    // 4. And so on - all messages are stored
17
18    // ═══════════════════════════════════════════════════════
19    // FIELD: topic (topic)
20    // ═══════════════════════════════════════════════════════
21    topic: Annotation<string>({
22        // REDUCER: Replace old value with new one
23        reducer: (_, update) => update, // "_" means "ignore"
24        default: () => '',
25    }),
26    //
27    // How it works:
28    // 1. Beginning:    topic = ""
29    // 2. User:   topic = "" → "LangChain"
30    // If someone else writes in topic - the old value will disappear
31
32    // ═══════════════════════════════════════════════════════
33    // FIELD: iterationCount (iteration counter)
34    // ═══════════════════════════════════════════════════════
35    iterationCount: Annotation<number>({
36        reducer: (_, update) => update, // Simply replace
37        default: () => 0, // Start from 0
38    }),
39    //
40    // The writer writes each time: iterationCount: state.iterationCount + 1
41    // 1. Start:         iterationCount = 0
42    // 2. Writer (1):   iterationCount = 0 + 1 = 1
43    // 3. Writer (2):   iterationCount = 1 + 1 = 2
44    // 4. Writer (3): iterationCount = 2 + 1 = 3
45});

Visualization of Reducers:

text
1┌─────────────────────────────────────────────────────────────────┐
2│                       REDUCER: REPLACE                          │
3├─────────────────────────────────────────────────────────────────┤
4│                                                                 │
5│   Current: █████████████  "Old text"                            │
6│                  ▼                                              │
7│   New:     ░░░░░░░░░░░░░  "New text"                            │
8│                  ▼                                              │
9│   Result:  ░░░░░░░░░░░░░  "New text" ← only new                 │
10│                                                                 │
11└─────────────────────────────────────────────────────────────────┘
12
13┌─────────────────────────────────────────────────────────────────┐
14│                         REDUCER: ADD                            │
15├─────────────────────────────────────────────────────────────────┤
16│                                                                 │
17│   Current:  [█] [█] [█]      ← three elements                   │
18│                  +                                              │
19│   New:      [░] [░]          ← two new                          │
20│                  =                                              │
21│   Result:   [█] [█] [█] [░] [░]  ← all together                 │
22│                                                                 │
23└─────────────────────────────────────────────────────────────────┘

What is default?

default is a function that returns the initial value of a field.

typescript
1default: () => value

Why a function and not just a value?

This is a common JavaScript pitfall. If you use a plain object or array as default, all instances will share the same reference - changes in one place will affect all others!

typescript
1// POOR: If this is an object or array
2default: []  // All instances will refer to the same array!
3
4// GOOD: The function creates a new array each time
5default: () => []  // Each instance will get its own array

Examples:

typescript
1// For a string
2default: () => ''        // Empty string
3
4// For a number
5default: () => 0         // Zero
6
7// For an array
8default: () => []        // Empty array
9
10// For an object
11default: () => ({})      // Empty object
12
13// For boolean
14default: () => false     // false

PART 3: Nodes - Agents

What is a node?

Node is a function that:

  1. Receives the full state
  2. Performs some work (for example, calls LLM)
  3. Returns a partial state update

Analogy: Worker on the Assembly Line

text
1┌─────────────────────────────────────────────────────────────┐
2│                       ASSEMBLY LINE                         │
3├─────────────────────────────────────────────────────────────┤
4│                                                             │
5│   [Box] ──► [Worker 1] ───────► [Worker 2] ──────►          │
6│                  │                    │                     │
7│                  ▼                    ▼                     │
8│            Adds part A          Adds part B                 │
9│                                                             │
10└─────────────────────────────────────────────────────────────┘
11
12Each worker:
131. Sees what has already been done (state)
142. Does their part of the work
153. Passes it on with additions

Node Structure in Code:

typescript
1async function myNode(
2    state: ResearchStateType // ← INPUT: Full state
3): Promise<Partial<ResearchStateType>> {
4    // ← OUTPUT: Partial update
5    // 1. Read data from state
6    const data = state.someField;
7
8    // 2. Do the work
9    const result = await doSomething(data);
10
11    // 3. Returning the update (only what has changed)
12    return {
13        someField: result,
14    };
15}

Important: Partial<State>

A node doesn't need to return the entire state - only the fields that changed. LangGraph will merge your partial update with the existing state using the reducers you defined.

typescript
1// The full state has 6 fields:
2state = {
3    topic: "...",
4    research: "...",
5    draft: "...",
6    review: "...",
7    finalArticle: "...",
8    messages: [...],
9    iterationCount: 0
10}
11
12// But the node can only return what it has changed:
13return {
14    draft: "New draft",           // Changed
15    messages: [new AIMessage("...")], // Added
16    // The other fields are not mentioned - they will remain as they were
17}

Our nodes in detail

Now let's look at the four nodes in our article-writing system. Each node has a specific role and passes its results to the next one through the shared state.

Node 1: Researcher (researcherNode)

The researcher is the first agent in our pipeline. It takes the topic and generates research materials that will be used by the writer.

typescript
1async function researcherNode(
2    state: ResearchStateType
3): Promise<Partial<ResearchStateType>> {
4    // ┌─────────────────────────────────────────────────────────────┐
5    // │ STEP 1: Get the topic for research                         │
6    // └─────────────────────────────────────────────────────────────┘
7    const topic =
8        state.topic ||
9        String(state.messages[state.messages.length - 1]?.content) ||
10        '';
11    //            ▲               ▲
12    //            │               └── Or the last message
13    //            └── First, try to take topic
14
15    // ┌─────────────────────────────────────────────────────────────┐
16    // │ STEP 2: Form the prompt for LLM                            │
17    // └─────────────────────────────────────────────────────────────┘
18    const prompt = `You are an expert researcher. Your task is to gather key information.
19    Topic: ${topic}
20    Conduct a brief research...`;
21
22    // ┌─────────────────────────────────────────────────────────────┐
23    // │ STEP 3: Call LLM                                           │
24    // └─────────────────────────────────────────────────────────────┘
25    const response = await model.invoke([
26        { role: 'system', content: prompt }, // Instructions for AI
27        { role: 'user', content: `Research the topic: ${topic}` }, // Request
28    ]);
29
30    const research = String(response.content); // Result from LLM
31
32    // ┌─────────────────────────────────────────────────────────────┐
33    // │ STEP 4: Return state update                                │
34    // └─────────────────────────────────────────────────────────────┘
35    return {
36        research, // Store the research result
37        messages: [
38            new AIMessage({
39                content: `[Research completed]
40${research}`,
41            }),
42        ],
43        // ▲ Add a message to the history
44    };
45}

What happens:

text
1INPUT (state):                       OUTPUT (update):
2┌─────────────────────┐              ┌─────────────────────┐
3│ topic: "LangChain"  │              │ research: "LangCh.. │
4│ research: ""        │  ──────►     │ messages: [+1 msg]  │
5│ draft: ""           │              └─────────────────────┘
6│ messages: [1 msg]   │
7└─────────────────────┘
8                                     ▼ After merging:
9
10                                ┌─────────────────────┐
11                                │ topic: "LangChain"  │
12                                │ research: "LangCh...│ ← updated
13                                │ draft: ""           │
14                                │ messages: [2 msgs]  │ ← added
15                                └─────────────────────┘

Node 2: Writer (writerNode)

The writer takes the research and creates an article draft. If this is a revision (after reviewer feedback), it also considers the review comments. Notice how iterationCount helps us track how many times the article has been rewritten.

typescript
1async function writerNode(
2    state: ResearchStateType
3): Promise<Partial<ResearchStateType>> {
4    // ┌─────────────────────────────────────────────────────────────┐
5    // │ STEP 1: Read the research and possible review              │
6    // └─────────────────────────────────────────────────────────────┘
7    const prompt = `You are a technical writer. Based on the research, write an article.
8
9Research:
10${state.research}
11
12${state.review ? `Previous review (consider comments): ${state.review}` : ''}
13`;
14
15    // ┌─────────────────────────────────────────────────────────────┐
16    // │ STEP 2: Call LLM                                           │
17    // └─────────────────────────────────────────────────────────────┘
18    const response = await model.invoke([
19        { role: 'system', content: prompt },
20        { role: 'user', content: 'Write an article based on the research' },
21    ]);
22
23    const draft = String(response.content);
24
25    // ┌─────────────────────────────────────────────────────────────┐
26    // │ STEP 3: Return the update                                  │
27    // └─────────────────────────────────────────────────────────────┘
28    return {
29        draft, // Draft of the article
30        iterationCount: state.iterationCount + 1, // Increment the counter
31        messages: [
32            new AIMessage({ content: `[Draft ${state.iterationCount + 1}]` }),
33        ],
34    };
35}

What happens on repeat call:

text
1FIRST CALL:                          SECOND CALL (after review):
2┌─────────────────────┐              ┌─────────────────────┐
3│ research: "..."     │              │ research: "..."     │
4│ review: ""          │              │ review: "Refine..." │ ← there are comments!
5│ iterationCount: 0   │              │ iterationCount: 1   │
6└─────────────────────┘              └─────────────────────┘
7         │                                    │
8         ▼                                    ▼
9Writing without comments             Considering comments
10         │                                    │
11         ▼                                    ▼
12┌─────────────────────┐              ┌─────────────────────┐
13│ draft: "Version 1"  │              │ draft: "Version 2"  │
14│ iterationCount: 1   │              │ iterationCount: 2   │
15└─────────────────────┘              └─────────────────────┘

Node 3: Reviewer (reviewerNode)

The reviewer evaluates the draft and decides if it's ready for publication. The key here is the output: if the review contains "APPROVED", the article moves to finalization. Otherwise, it goes back to the writer for improvements. This is what enables the cycle in our graph.

typescript
1async function reviewerNode(state: ResearchStateType): Promise<Partial<ResearchStateType>> {
2
3    // ┌─────────────────────────────────────────────────────────────┐
4    // │ STEP 1: Formulating the review request                      │
5    // └─────────────────────────────────────────────────────────────┘
6    const prompt = `You are a strict editor. Evaluate the article.
7
8Article:
9${state.draft}
10
11Evaluate based on the criteria:
121. Accuracy of information
132. Structure and logic
143. Language quality
154. Completeness of the topic coverage
16
17If the article is good - say "APPROVED". When improvements are needed, please provide specific recommendations.
18`;
19
20    // ┌─────────────────────────────────────────────────────────────┐
21    // │ STEP 2: Get the review                                     │
22    // └─────────────────────────────────────────────────────────────┘
23    const response = await model.invoke([...]);
24    const review = String(response.content);
25
26    // ┌─────────────────────────────────────────────────────────────┐
27    // │ STEP 3: Return the review                                  │
28    // └─────────────────────────────────────────────────────────────┘
29    return {
30        review,  // Review (either "APPROVED" or comments)
31        messages: [new AIMessage({ content: `[Review]\n${review}` })],
32    };
33}

Two possible outcomes:

text
1OPTION A: The article is good             OPTION B: Needs improvement
2┌───────────────────────────┐             ┌───────────────────────────┐
3│ review: "APPROVED.        │             │ review: "Improve:         │
4│ The article is excellent!"│             │ 1. Add examples           │
5│                           │             │ 2. Clarify the terms"     │
6└───────────────────────────┘             └───────────────────────────┘
7             │                                        │
8             ▼                                        ▼
9        Moving to                              Returning to
10        finalizer                                 writer

Node 4: Finalizer (finalizerNode)

The finalizer is the simplest node - it just copies the approved draft to the finalArticle field, marking the end of our workflow. This is called when the reviewer approves the article.

typescript
1async function finalizerNode(
2    state: ResearchStateType
3): Promise<Partial<ResearchStateType>> {
4    // Simple node - copies the draft to the final article
5    return {
6        finalArticle: state.draft, // Final article
7        messages: [new AIMessage({ content: `[READY]\n\n${state.draft}` })],
8    };
9}

What happens:

text
1INPUT:                              OUTPUT:
2┌─────────────────────┐             ┌─────────────────────┐
3│ draft: "Ready..."   │  ──────►    │ finalArticle:       │
4│ finalArticle: ""    │             │   "Ready..."        │
5└─────────────────────┘             └─────────────────────┘

PART 4: Edges

What is an edge?

Edge is a rule that states: "After this node, the following node is executed."

Analogy: Arrows on the Diagram

text
1Recipe steps:
2
3    [Make dough] ────► [Add sauce] ────► [Add cheese] ────► [Bake]
4          │                  │                  │               │
5          ▼                  ▼                  ▼               ▼
6      TRANSITION         TRANSITION        TRANSITION         END

Two Types of Transitions in LangGraph:

1. Simple Transitions (addEdge)

Always go to the same node.

typescript
1workflow.addEdge('researcher', 'writer');
2//                    ▲            ▲
3//                    │            └── Where are we going
4//                    └── From where are we coming
5
6// Meaning: After researcher, we ALWAYS go to writer

Visualization:

text
1    [researcher] ───────────────► [writer]
2                   (always)
2. Conditional Transitions (addConditionalEdges)

The choice of the next node depends on the state.

typescript
1workflow.addConditionalEdges(
2    'reviewer', // Source node
3    shouldContinue, // Function that decides where to go
4    {
5        writer: 'writer', // If the function returns 'writer' → go to writer
6        finalizer: 'finalizer', // If the function returns 'finalizer' → go to finalizer
7    }
8);

Visualization:

text
1                            ┌──────────► [writer]
23    [reviewer] ─────► [?] ──┤
45                            └──────────► [finalizer]
6
7    The function shouldContinue decides which arrow to take.

Detailed Function shouldContinue

This is the "brain" of our conditional edge. It examines the current state and decides where to go next. The function must return a string that matches one of the keys in the mapping object we defined in addConditionalEdges.

typescript
1function shouldContinue(state: ResearchStateType): 'writer' | 'finalizer' {
2    //                                              ▲
3    //                                              └── Returns one of these strings
4
5    const review = state.review.toLowerCase();
6    const maxIterations = 3;
7
8    // ═══════════════════════════════════════════════════════════════
9    // CONDITION 1: Reviewer said "APPROVED"
10    // ═══════════════════════════════════════════════════════════════
11    if (review.includes('зацверджана') || review.includes('approved')) {
12        console.log('Article approved');
13        return 'finalizer'; // ← Proceed to finalize
14    }
15
16    // ═══════════════════════════════════════════════════════════════
17    // CONDITION 2: Too many attempts (protection against infinite loop)
18    // ═══════════════════════════════════════════════════════════════
19    if (state.iterationCount >= maxIterations) {
20        console.log('Maximum iterations reached');
21        return 'finalizer'; // ← Force finalize
22    }
23
24    // ═══════════════════════════════════════════════════════════════
25    // OTHERWISE: Needs revision
26    // ═══════════════════════════════════════════════════════════════
27    console.log('Revision needed');
28    return 'writer'; // ← Go back to writer
29}

Decision flowchart:

text
1                         ┌───────────────────┐
2                         │  shouldContinue   │
3                         │    (function)     │
4                         └─────────┬─────────┘
567                    ┌──────────────────────────────┐
8                    │   Is "approved" present      │
9                    │   in the review?             │
10                    └──────────────┬───────────────┘
1112                      ┌────────────┴────────────┐
13                      │                         │
14                     YES                       NO
15                      │                         │
16                      ▼                         ▼
17               ┌────────────┐      ┌──────────────────────────┐
18               │  return    │      │ Is iterationCount >= 3?  │
19               │'finalizer' │      └──────────────┬───────────┘
20               │            │                     │
21               └────────────┘        ┌────────────┴────────────┐
22                                     │                         │
23                                    YES                       NO
24                                     │                         │
25                                     ▼                         ▼
26                              ┌────────────┐            ┌────────────┐
27                              │  return    │            │  return    │
28                              │'finalizer' │            │  'writer'  │
29                              │            │            │            │
30                              └────────────┘            └────────────┘

How the graph is built

Now let's put everything together! Building a graph in LangGraph follows a simple pattern: create the graph, add nodes, connect them with edges, and compile.

Sequence of creation:

typescript
1// ═══════════════════════════════════════════════════════════════
2// STEP 1: Create StateGraph with our state
3// ═══════════════════════════════════════════════════════════════
4const workflow = new StateGraph(ResearchState)
5
6    // ═══════════════════════════════════════════════════════════
7    // STEP 2: Add nodes (register functions)
8    // ═══════════════════════════════════════════════════════════
9    .addNode('researcher', researcherNode) // Name 'researcher' → function
10    .addNode('writer', writerNode) // Name 'writer' → function
11    .addNode('reviewer', reviewerNode) // Name 'reviewer' → function
12    .addNode('finalizer', finalizerNode) // Name 'finalizer' → function
13
14    // ═══════════════════════════════════════════════════════════
15    // STEP 3: Add simple transitions
16    // ═══════════════════════════════════════════════════════════
17    .addEdge('__start__', 'researcher') // Start → Researcher
18    .addEdge('researcher', 'writer') // Researcher → Writer
19    .addEdge('writer', 'reviewer') // Writer → Reviewer
20
21    // ═══════════════════════════════════════════════════════════
22    // STEP 4: Add conditional transition
23    // ═══════════════════════════════════════════════════════════
24    .addConditionalEdges('reviewer', shouldContinue, {
25        writer: 'writer', // If 'writer' → back to writer
26        finalizer: 'finalizer', // If 'finalizer' → to finalizer
27    })
28
29    // ═══════════════════════════════════════════════════════════
30    // STEP 5: Add final transition
31    // ═══════════════════════════════════════════════════════════
32    .addEdge('finalizer', '__end__'); // Finalizer → End

Special nodes:

text
1┌───────────────┬─────────────────────────────────────────────┐
2│  '__start__'  │  Virtual start node                         │
3│               │  LangGraph automatically starts from it     │
4├───────────────┼─────────────────────────────────────────────┤
5│  '__end__'    │  Virtual end node                           │
6│               │  When reached - the graph is completed      │
7└───────────────┴─────────────────────────────────────────────┘

PART 5: Compilation and execution

We've defined our state, nodes, and edges. Now it's time to turn this blueprint into a running application!

What is compile()?

compile() is the process of transforming a graph description into an executable program. Until you call compile(), you just have a description of what you want to build - not an actual runnable system.

typescript
1// Graph description (blueprint)
2const workflow = new StateGraph(ResearchState)
3    .addNode(...)
4    .addEdge(...);
5
6// Compilation into an executable program
7const app = workflow.compile();
8//    ▲
9//    └── Now this can be run!

Analogy: Recipe vs. Finished Dish

text
1workflow (description)                 app (compiled program)
2──────────────────────                 ──────────────────────
3Recipe on paper                        Ready pizza
4   - How to make the dough                (can be eaten)
5   - What to add
6   - How to bake
7
8Cannot be eaten!                       Can be eaten!

Checkpointer (MemorySaver)

What is it?

Checkpointer is a mechanism for saving state after each step.

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

Why is this needed?

text
1WITHOUT CHECKPOINTER:
2═════════════════════════════════════════════════════════════════
3
4    Run 1:   START → researcher → writer → ... → END
5
6    Run 2:   START → researcher → ... (everything from the beginning!)
7
8    ✗ Cannot continue from the stopping point
9    ✗ Cannot view history
10
11
12WITH CHECKPOINTER:
13═════════════════════════════════════════════════════════════════
14
15    Run 1:   START → researcher → [SAVED]
1617                               thread_id: "chat-123"
18
19    Run 2:   [RESUMING] → writer → reviewer → ...
2021              thread_id: "chat-123"
22
23    ✓ Can continue from the stopping point
24    ✓ Can view history
25    ✓ Can have multiple independent "conversations"

Thread ID - identifier of the "thread"

typescript
1const config = {
2    configurable: {
3        thread_id: 'article-1', // Unique ID for this "conversation"
4    },
5};
6
7// Each thread_id is a separate story
8// 'article-1' - about one article
9// 'article-2' - about another article
10// They do not overlap!

Running the Graph

Finally! Let's run our graph. The invoke() function takes initial state values and configuration, then executes the entire graph from start to finish.

The invoke() Function

typescript
1const result = await app.invoke(
2    // Initial data for the state
3    {
4        topic: 'Benefits of LangChain',
5        messages: [new HumanMessage('Benefits of LangChain')],
6    },
7    // Configuration
8    {
9        configurable: {
10            thread_id: 'article-1',
11        },
12    }
13);

What Happens When invoke() is Called:

text
1┌─────────────────────────────────────────────────────────────────┐
2│                         invoke()                                │
3└───────────────────────────────┬─────────────────────────────────┘
456┌─────────────────────────────────────────────────────────────────┐
7│ 1. INITIALIZATION OF STATE                                      │
8│    state = {                                                    │
9│        topic: "Advantages of 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. BEGINNING: __start__ → researcher                            │
22│    Executing researcherNode(state)                              │
23│    → Receiving update {research: "...", messages: [...]}        │
24│    → Merging with state through reducers                        │
25│    Saving checkpoint                                            │
26└───────────────────────────────┬─────────────────────────────────┘
272829┌─────────────────────────────────────────────────────────────────┐
30│ 3. TRANSITION: researcher → writer                              │
31│    Execute writerNode(state)                                    │
32│    → Receive update {draft: "...", iterationCount: 1}           │
33│    → Merge with state                                           │
34│    Save checkpoint                                              │
35└───────────────────────────────┬─────────────────────────────────┘
363738┌─────────────────────────────────────────────────────────────────┐
39│ 4. TRANSITION: writer → reviewer                                │
40│    Execute reviewerNode(state)                                  │
41│    → Receive update {review: "..."}                             │
42│    → Merge with state                                           │
43│    Save checkpoint                                              │
44└───────────────────────────────┬─────────────────────────────────┘
454647┌─────────────────────────────────────────────────────────────────┐
48│ 5. CONDITIONAL TRANSITION: shouldContinue(state)                │
49│    → Returns 'writer' or 'finalizer'                            │
50│    → If 'writer' - return to step 3                             │
51│    → If 'finalizer' - proceed further                           │
52└───────────────────────────────┬─────────────────────────────────┘
53                                │ (if 'finalizer')
5455┌─────────────────────────────────────────────────────────────────┐
56│ 6. TRANSITION: reviewer → finalizer                             │
57│    Execute finalizerNode(state)                                 │
58│    → Receive update {finalArticle: "..."}                       │
59│    → Merge with state                                           │
60│    Save checkpoint                                              │
61└───────────────────────────────┬─────────────────────────────────┘
626364┌─────────────────────────────────────────────────────────────────┐
65│ 7. FINALIZER: finalizer → __end__                               │
66│    The graph has completed                                      │
67│    Returning the final state                                    │
68└───────────────────────────────┬─────────────────────────────────┘
697071                        return state (full)

PART 6: FAQ - Frequently Asked Questions

Why does the reducer add messages but replace the topic?

Messages represent history. We want to keep all messages throughout the process.

Topic is the current theme. When the topic changes, the old one is no longer needed.

What happens if I don’t specify a reducer?

LangGraph will use the default behavior - replacement (like for the topic).

Why is iterationCount needed?

To protect against infinite loops. If the reviewer never says "approved", the program will keep cycling between the writer and the reviewer.

Can one node directly invoke another?

No. Nodes do not know about each other. They only read/write state. LangGraph determines who executes next based on transitions.

What are start and end?

These are special virtual nodes:

  • __start__ - where the graph begins
  • __end__ - where the graph ends

They do not execute code - they only mark the boundaries.

Can there be multiple end nodes?

Yes! For example:

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

Conclusion

Congratulations! You've learned the core concepts of LangGraph. Let's recap what we covered:

ConceptWhat it does
State + AnnotationDefines the data structure passed between agents
ReducerSpecifies how to combine new data with existing data
Nodes (Agents)Functions that process the state
Edges (Transitions)Rules determining the order of operations
Conditional EdgesDynamic selection of the next node
CheckpointerSaves the state for continuation later

What's Next?

Now that you understand the basics, you can:

  1. Build your own agent system - Start with a simple two-node graph and gradually add complexity
  2. Experiment with different flows - Try creating graphs with multiple conditional branches
  3. Add persistence - Use MemorySaver or database-backed checkpointers for production
  4. Explore advanced features - Look into subgraphs, parallel execution, and human-in-the-loop patterns

Key Takeaways

  • Think in graphs: Break down your AI workflow into discrete steps (nodes) connected by rules (edges)
  • State is everything: All communication between nodes happens through the shared state
  • Reducers matter: Choose the right reducer for each field - append for history, replace for current values
  • Cycles enable iteration: Unlike simple chains, graphs can loop back for refinement

The article-writing example we built demonstrates a real-world pattern: research → write → review → (repeat if needed) → finalize. This same pattern applies to many AI applications: code review, content moderation, multi-step reasoning, and more.

Happy building! 🚀

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

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