TRENDING
A mountain gorilla looks at the camera from green forest undergrowth
October 28, 2026
How to Compress Time-Series Metrics in Python With Gorilla Delta-of-Delta and XOR Encoding
A bronze-framed hourglass with white sand beside a small Ferris wheel ornament on a wooden table against a teal wall
October 28, 2026
NIST’s Final Post-Quantum Transition Report Puts Every Quantum-Vulnerable Public-Key Scheme on One 2030 and 2035 Clock, and Three Federal Documents Word the Deadline Three Ways
Six weathered stone columns of the Temple of Apollo at Delphi, Greece, standing on a stone platform above a valley of olive groves
October 28, 2026
Honeypots Recorded Probes Against Oracle E-Business Suite CVE-2026-101767 Seven Days After Its Patch, and the Finder’s Write-Up Is Dated Patch Day
Vintage silver stopwatch with a chain on a black background, its dial showing seconds from 5 to 60
October 28, 2026
How to Find a Slow Query in PostgreSQL 18 With pg_stat_statements, Read Its EXPLAIN ANALYZE Plan, and Fix It With the Right Index
A wooden Tower of Hanoi puzzle on a market table, with three pegs and stacked discs of different sizes
October 28, 2026
Two Labs’ Claimed Refutations of the Exponential Time Hypothesis Turn Verification Into a Statement-Reading Problem, and Their Lean Theorems Promise Only That a Constant Exists
28 Oct 2026
SXZ.io SXZ.io
  • Home
Search the Site
Popular Searches:
Technology Amazon AI
Recent Posts
A flock of snow geese in flight, seen as dark silhouettes against an orange and red sunset sky with layered clouds
How to Run Zero-Downtime Schema Migrations on PostgreSQL 18 and Block Unsafe ALTER TABLE Statements With a Python Lint Gate
October 28, 2026
An old green steel truss bridge resting on temporary supports beside a new arch bridge under completion on a river in Portland, Oregon
The Angular 2 Rewrite Turns a Migration Lesson Into a Security Bill: AngularJS Still Draws 533,480 Downloads a Week, and 10 Advisories Have No Open-Source Fix
October 28, 2026
Hundreds of yellow rubber ducks floating in a river enclosure during a duck race
WordPress 7.1.4 Closes an Unauthenticated Race in Its Own Update Routine, but 19 Older Branches Had No Backport Hours After Release
October 28, 2026
SXZ.io SXZ.io
  • Home

Categories

Articles 270 Posts
News 273 Posts
Learning Hub 243 Posts
Home/Learning Hub/How to Build a Self-Correcting AI Agent With LangGraph and Ollama
Learning Hub

How to Build a Self-Correcting AI Agent With LangGraph and Ollama

A step-by-step tutorial that builds a LangGraph agent in Python which drafts an answer, critiques its own work, and retries automatically using a conditional edge, then adds cross-call memory with a...

July 13, 2026 13 Min Read
88

Most introductions to LangGraph jump straight into a wall of code. Before writing any, it helps to understand the one idea that makes LangGraph different from a plain LangChain chain: a chain runs in a straight line from start to finish, but a LangGraph agent is built as a graph that can loop back on itself. That loop is what lets an agent check its own work and try again, something a straight-line chain cannot do without extra scaffolding bolted on top.

Table Of Content

  • What you need before you start
  • Step 1: Install Ollama and pull a local model
  • Step 2: Set up your Python project
  • Step 3: Install LangGraph and its dependencies
  • Step 4: See the shape of the agent before you build it
  • Step 5: Define the agent’s state
  • Step 6: Write the node that drafts an answer
  • Step 7: Write the node that grades the answer
  • Step 8: Write the node that records the result
  • Step 9: Wire the nodes into a graph, including the conditional edge
  • Step 10: Compile the graph and run it
  • Step 11: Add persistent memory with a checkpointer
  • Common mistakes and how to catch them
  • Verifying everything works end to end
  • Next steps

In this tutorial you will build a small, complete LangGraph agent that answers a question, grades its own draft answer against a rule, and automatically retries if the draft fails that rule, up to a limit you control. Along the way you will learn the building blocks every LangGraph application is made of: state, nodes, edges, conditional edges (the mechanism behind the loop), and the checkpointer that gives an agent memory across separate runs. Everything here runs entirely on your own machine through Ollama, a free tool for running open-weight language models locally, so you do not need an API key, a credit card, or a cloud account to follow along. Every command, package version, and API call in this tutorial was actually run on a fresh Ubuntu 26.04 LTS host while writing it, including the full working example near the end.

Before the first command, here is what each term means in plain language, since LangGraph’s own documentation assumes you already know them:

  • State is a shared, dictionary-like object that flows through the graph, carrying whatever data your agent needs to remember from one step to the next.
  • A node is a plain Python function that reads the current state and returns updates to it. Nodes are where the actual work happens, including any call out to a language model.
  • An edge tells LangGraph which node to run next. A normal edge always leads to the same next node. A conditional edge picks the next node at runtime by running a small routing function against the current state, and it is what makes loops possible: the routing function can send execution back to a node that already ran.
  • A checkpointer saves the state after every step so a run can be resumed, and so two separate calls to the graph can share memory of an ongoing conversation.

What you need before you start

  • A Linux machine. This tutorial was written and tested on Ubuntu 26.04 LTS; other recent Ubuntu or Debian releases work the same way, and macOS works too if you swap in the macOS Ollama installer.
  • About 4 GB of free disk space and 8 GB of RAM. The model used here downloads roughly 3.4 GB, and runs fine on CPU alone, just slower per answer than it would on a GPU.
  • Python 3.10 or newer. LangGraph 1.2 requires at least Python 3.10.
  • Comfort with basic Python: functions, dictionaries, and type hints. No prior LangChain or LangGraph experience is assumed.
  • A regular terminal. Nothing in this tutorial needs root or sudo access.

Step 1: Install Ollama and pull a local model

Ollama is a small background service that downloads open-weight language models and runs them on your own hardware, exposing them through a local API on port 11434. Using it here means the tutorial costs nothing to run and produces the same kind of answers for everyone following along, instead of depending on a paid API key.

# Context: any Linux machine (Ubuntu 26.04 LTS used in this tutorial), regular user account.
# Purpose: install the Ollama runtime and register it as a background service.
curl -fsSL https://ollama.com/install.sh | sh

Expected output: a progress bar while the installer downloads, followed by lines ending in The Ollama API is now available at 127.0.0.1:11434. and Install complete. Run "ollama" from the command line. If your machine has no NVIDIA or AMD GPU, you will also see a warning that Ollama will run in CPU-only mode. That is expected on most laptops and virtual machines, and this tutorial’s model is small enough to work fine that way.

Next, download the model this tutorial uses:

# Context: Ollama installed and running from the previous step.
# Purpose: download the 4-billion-parameter tag of Qwen 3.5 for local inference.
ollama pull qwen3.5:4b

Expected output: a series of progress bars, one per model layer, ending in verifying sha256 digest, writing manifest, and finally success. Qwen 3.5 ships in tags from 0.8B up to 122B parameters; the 4B tag used here is about 3.4 GB and is a reasonable balance of quality and speed for a CPU-only machine. Confirm the download landed correctly with ollama list, which should print qwen3.5:4b alongside its size and a “modified” timestamp.

Step 2: Set up your Python project

Keep this tutorial’s dependencies isolated from anything else on your system using a virtual environment:

# Context: a terminal, in whatever folder you keep coding projects.
# Purpose: create a project folder and an isolated Python virtual environment inside it.
mkdir langgraph-self-correcting-agent
cd langgraph-self-correcting-agent
python3 -m venv venv
source venv/bin/activate

Expected output: your shell prompt gains a (venv) prefix, which confirms the virtual environment is active. Every pip install and every python command for the rest of this tutorial assumes that prefix is showing.

Step 3: Install LangGraph and its dependencies

# Context: venv activated, inside the langgraph-self-correcting-agent directory.
# Purpose: install LangGraph, LangChain's core package, and the Ollama chat integration.
pip install langgraph langchain-core langchain-ollama

Expected output: pip resolves and installs each package, ending with a line starting Successfully installed that lists langgraph, langchain-core, langchain-ollama, and their dependencies, including langgraph-checkpoint, langgraph-prebuilt, langgraph-sdk, ollama, and pydantic. At the time of writing, this installs LangGraph 1.2.9, langchain-core 1.4.9, and langchain-ollama 1.1.0, and it requires Python 3.10 or newer, matching the prerequisite above.

Step 4: See the shape of the agent before you build it

It is much easier to write graph code once you can picture the graph. This tutorial builds exactly this shape:

  • START leads to a generate node, which drafts an answer to the question.
  • generate always leads to a critique node, which checks the draft against a rule.
  • critique leads to a conditional edge: if the draft passed, or the agent has already tried three times, it moves on to finalize; otherwise it loops back to generate, this time with feedback about what to fix.
  • finalize records the result and leads to END.

The loop between critique and generate is the cycle a plain LangChain chain cannot express on its own, since a chain’s steps run once each, in a fixed order. LangGraph’s own documentation describes nodes and edges as nothing more than functions: nodes do the work, and edges decide what runs next. Internally, LangGraph passes messages between nodes in discrete rounds it calls super-steps, an approach borrowed from Google’s Pregel graph-processing system, and it keeps running until every node has nothing left to do.

Step 5: Define the agent’s state

Create a file named agent.py in your project folder and start with the state schema, written as a TypedDict, which is the schema style LangGraph’s own documentation recommends by default:

from typing import TypedDict, Annotated
import operator

class AgentState(TypedDict):
    question: str
    draft_answer: str
    feedback: str
    attempts: int
    is_valid: bool
    history: Annotated[list, operator.add]

Five of these fields (question, draft_answer, feedback, attempts, is_valid) behave the way you would expect: whenever a node returns a new value for one of them, LangGraph simply overwrites the old value. That default behavior is called a reducer, and plain overwrite is the default reducer. The sixth field, history, is different on purpose. It is annotated with operator.add, which tells LangGraph to use Python’s list concatenation as its reducer instead, so every value a node returns for history gets appended to the existing list rather than replacing it. That one line is what lets a running transcript survive across separate questions later in this tutorial.

Step 6: Write the node that drafts an answer

Add the following below the state definition:

from langchain_ollama import ChatOllama

MODEL_NAME = "qwen3.5:4b"
MAX_ATTEMPTS = 3

llm = ChatOllama(model=MODEL_NAME, temperature=0)


def generate(state: AgentState) -> dict:
    feedback = state.get("feedback", "")
    prompt = (
        "Answer the question in exactly one sentence, under 20 words. "
        f"QUESTION: {state['question']}"
    )
    if feedback:
        prompt += f"\nYour previous attempt was rejected for this reason: {feedback}. Fix it."
    response = llm.invoke(prompt)
    return {
        "draft_answer": response.content.strip(),
        "attempts": state.get("attempts", 0) + 1,
    }

A node is only ever a function that takes the state and returns a dictionary of the fields it wants to change, which is why generate returns just draft_answer and attempts instead of the entire state. temperature=0 makes the model’s output as deterministic as a language model can be, which is useful while you are testing a new agent. The if feedback branch is what actually closes the loop: the very first time this node runs there is no feedback yet, but on a retry it folds the critique’s complaint straight into the next prompt, so the model gets a concrete reason its last attempt failed instead of just being asked to try again blindly.

Step 7: Write the node that grades the answer

def critique(state: AgentState) -> dict:
    prompt = (
        "You are a strict editor checking one rule: the ANSWER must be a single "
        "sentence, under 20 words, and must directly address the QUESTION.\n"
        f"QUESTION: {state['question']}\nANSWER: {state['draft_answer']}\n"
        "Reply with exactly the word PASS if the rule is met. Otherwise reply "
        "with FAIL: followed by a short reason."
    )
    verdict = llm.invoke(prompt).content.strip()
    if verdict.upper().startswith("PASS"):
        return {"is_valid": True, "feedback": ""}
    return {"is_valid": False, "feedback": verdict}

This is a small version of a pattern usually called LLM-as-judge, or self-reflection: instead of trusting the first draft, a second, separately-prompted call checks it against a specific, narrow rule. Asking the same call to both answer the question and grade its own answer tends to be far less reliable, since the model has every incentive to agree with whatever it just wrote. A fresh prompt whose only job is grading tends to catch problems the original generation missed.

Step 8: Write the node that records the result

def finalize(state: AgentState) -> dict:
    entry = (
        f"Q: {state['question']} -> A: {state['draft_answer']} "
        f"(attempts: {state['attempts']}, passed_review: {state['is_valid']})"
    )
    return {"history": [entry]}

Because history uses the operator.add reducer from Step 5, returning {"history": [entry]} here appends a single-item list to whatever history already existed, rather than replacing it. This node is also the natural place to add anything else you would want to do once an answer is final, like writing it to a database.

Step 9: Wire the nodes into a graph, including the conditional edge

from langgraph.graph import StateGraph, START, END

def route_after_critique(state: AgentState) -> str:
    if state["is_valid"] or state["attempts"] >= MAX_ATTEMPTS:
        return "finalize"
    return "retry"


builder = StateGraph(AgentState)
builder.add_node("generate", generate)
builder.add_node("critique", critique)
builder.add_node("finalize", finalize)
builder.add_edge(START, "generate")
builder.add_edge("generate", "critique")
builder.add_conditional_edges(
    "critique",
    route_after_critique,
    {"retry": "generate", "finalize": "finalize"},
)
builder.add_edge("finalize", END)

add_edge creates the two fixed, normal edges in this graph: START always goes to generate, and generate always goes to critique. add_conditional_edges is where the cycle lives. Its three arguments are the node the condition runs after (critique), a plain Python function that inspects the state and returns a short label, and a dictionary that maps each possible label to the real node it should lead to. Here, route_after_critique returns "finalize" once the answer passes or three attempts have been used, and "retry" otherwise, and the mapping sends "retry" straight back to the generate node you already wrote. That is the entire loop: nothing about generate or critique needed to change to make retries possible, because the looping logic lives entirely in the edge.

The attempts >= MAX_ATTEMPTS check is doing real work here, not just decoration. Without some way out of the loop, a rule the model can never satisfy would send the graph back to generate forever. LangGraph also enforces its own recursion limit as a hard backstop, configurable through a recursion_limit key in a run’s config, so a bug in your own routing logic fails loudly with a clear recursion error instead of hanging. Writing your own exit condition, as route_after_critique does, is still the right first line of defense, since it lets your agent give up gracefully with a real answer instead of erroring out.

Step 10: Compile the graph and run it

A graph builder cannot run until you compile it. Add this and run the file:

graph = builder.compile()

if __name__ == "__main__":
    result = graph.invoke(
        {
            "question": "What is the capital of France?",
            "attempts": 0,
            "is_valid": False,
            "feedback": "",
        }
    )
    print("Attempts:", result["attempts"])
    print("Answer:", result["draft_answer"])
# Context: venv activated, agent.py saved in the current directory, Ollama running in the background.
# Purpose: run the compiled graph end to end for a single question.
python agent.py

Expected output (from an actual run while writing this tutorial):

Attempts: 1
Answer: The capital of France is Paris.

Passing on the very first attempt is the common case for a plain factual question like this one; the loop from Step 9 only actually fires when critique returns is_valid: False. You can watch it fire yourself by tightening the rule, for example changing both prompts in Steps 6 and 7 from “under 20 words” to “under 8 words” and asking a question that genuinely needs more room to answer well, such as “Why do leaves change color in autumn?” With a tighter rule in place, expect critique to reject at least one draft and attempts to land above 1 in the final result, since the state now carries the rejection reason back into the next generate call exactly as described in Step 6.

Step 11: Add persistent memory with a checkpointer

The agent above forgets everything the moment invoke returns. To let it remember previous questions, compile it with a checkpointer instead, and give every call a thread_id so LangGraph knows which conversation’s memory to load:

from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

thread_1 = {"configurable": {"thread_id": "demo-thread-1"}}

result1 = graph.invoke(
    {"question": "What is the capital of France?", "attempts": 0, "is_valid": False, "feedback": ""},
    thread_1,
)
print("Turn 1 history:", result1["history"])

result2 = graph.invoke(
    {"question": "What ocean lies between Europe and North America?", "attempts": 0, "is_valid": False, "feedback": ""},
    thread_1,
)
print("Turn 2 history:", result2["history"])

InMemorySaver is LangGraph’s simplest checkpointer: it keeps every thread’s state in your Python process’s memory, which is perfect for learning and local development but is explicitly documented as not surviving a process restart. For anything you deploy, LangGraph’s own persistence guidance recommends a PostgresSaver for production, since it writes checkpoints to a real database with async support, or a SqliteSaver for local, file-based persistence during development that is sturdier than plain in-memory storage. Notice that thread_1 is passed as the second argument to invoke, not folded into the input dictionary; that config argument, keyed by thread_id, is how the checkpointer knows which saved state to load before running the graph.

Expected output (captured from an actual run):

Turn 1 history: ['Q: What is the capital of France? -> A: The capital of France is Paris. (attempts: 1, passed_review: True)']
Turn 2 history: ['Q: What is the capital of France? -> A: The capital of France is Paris. (attempts: 1, passed_review: True)', 'Q: What ocean lies between Europe and North America? -> A: The Atlantic Ocean lies between Europe and North America. (attempts: 1, passed_review: True)']

Turn 2’s history contains both questions, proving the checkpointer restored turn 1’s state before turn 2 ran, and that the operator.add reducer from Step 5 appended rather than overwrote. If you invoke the same graph again with a different thread ID, for example {"configurable": {"thread_id": "demo-thread-2"}}, its history starts empty again, since threads are isolated from each other by design. Notice, too, that question, attempts, is_valid, and feedback are still passed fresh on every call. Those fields use the default overwrite reducer from Step 5, so they do not need resetting by hand; only history carries forward, because it is the only field you told LangGraph to accumulate.

Common mistakes and how to catch them

  • Calling invoke() before compile(). A StateGraph builder object has no invoke method; only the object returned by builder.compile() does. If you see an AttributeError mentioning invoke, check that you saved compile’s return value and are calling it on that, not on builder directly.
  • Compiling with a checkpointer but forgetting the config. Once you pass checkpointer=... to compile(), every invoke() call needs a config dictionary with a thread_id, or LangGraph will raise an error telling you one is required. It is easy to copy Step 10’s graph.invoke(state_dict) call forward and forget to add the second argument from Step 11.
  • Returning the whole state from a node instead of just the changed keys. A node should return only the fields it actually updated, as every node in this tutorial does. Returning the full state back verbatim still works most of the time, but it silently reintroduces stale values for fields another branch may have changed in the same step, which gets confusing fast in a graph with more than two or three nodes.
  • A conditional edge with no real way out. If route_after_critique only ever returned "retry", the graph would loop until LangGraph’s own recursion limit stops it with an error. Always give a conditional edge a condition that is guaranteed to become true eventually, the way attempts >= MAX_ATTEMPTS does here.
  • A typo in the path map passed to add_conditional_edges. The dictionary’s values must exactly match node names you already registered with add_node. A mismatched string surfaces immediately, when you call compile(), rather than later at run time, which makes it one of the easier mistakes to catch.
  • A mistyped model tag. Asking for a model Ollama has not downloaded, for example qwen3.5 without the :4b tag if only the tagged version was pulled, fails with a model-not-found error from Ollama. Run ollama list any time to confirm the exact tag string you pulled in Step 1.

Verifying everything works end to end

Run python agent.py after finishing Step 11 and confirm all of the following before moving on:

  • The script exits with no traceback.
  • Turn 1’s printed history has exactly one entry, and it names Paris.
  • Turn 2’s printed history has exactly two entries, with turn 1’s entry still present unchanged, confirming the checkpointer actually restored state instead of starting fresh.
  • If you add a third call using a brand new thread_id, its history prints with only one entry, confirming threads do not leak into each other.

If all four hold, you have a working LangGraph agent with a genuine self-correction loop and cross-call memory, built entirely from the state, node, edge, and checkpointer primitives introduced above.

Next steps

From here, a few directions are worth exploring once the basics feel solid. Swap InMemorySaver for a PostgresSaver or SqliteSaver before you rely on this pattern for anything long-running, since, as Step 11 mentioned, in-memory checkpoints vanish on restart. Look at LangChain’s bind_tools to let generate call real tools, like a search function or a calculator, instead of only drafting text, which turns this same retry-loop shape into a genuine tool-using agent. For a graph with many more nodes than this tutorial’s three, look into LangGraph’s subgraphs, which let you package a cluster of nodes as a single reusable unit inside a larger graph. And once you are comfortable with StateGraph, LangGraph’s Functional API offers a more code-first way to express the same durable, resumable execution model for people who would rather write ordinary Python control flow than assemble nodes and edges by hand. Real Python’s LangGraph tutorial walks through a different worked example, an email-processing agent, and is a good next read once this one clicks.

Tags:

AI AgentsLangChainLangGraphOllamaPython

Share

Rust source code with syntax highlighting displayed on a laptop screen
Previous Post

Microsoft’s SymCrypt Proofs Turn Verified Cryptography Into Routine Engineering

A dimly lit data center aisle with tall server racks showing blue and green status lights on both sides
Next Post

HCLTech Bets $370 Million on Owning AI Datacenters, Not Renting Them

No Comment! Be the first one.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Latest
28 Oct
How to Run Zero-Downtime Schema Migrations on PostgreSQL 18 and Block Unsafe ALTER TABLE Statements With a Python Lint Gate
28 Oct
The Angular 2 Rewrite Turns a Migration Lesson Into a Security Bill: AngularJS Still Draws 533,480 Downloads a Week, and 10 Advisories Have No Open-Source Fix
Trending
October 28, 2026
How to Run Zero-Downtime Schema Migrations on PostgreSQL 18 and Block Unsafe ALTER TABLE Statements With a Python Lint Gate
October 28, 2026
The Angular 2 Rewrite Turns a Migration Lesson Into a Security Bill: AngularJS Still Draws 533,480 Downloads a Week, and 10 Advisories Have No Open-Source Fix
October 28, 2026
WordPress 7.1.4 Closes an Unauthenticated Race in Its Own Update Routine, but 19 Older Branches Had No Backport Hours After Release
October 28, 2026
How to Compress Time-Series Metrics in Python With Gorilla Delta-of-Delta and XOR Encoding
October 28, 2026
NIST’s Final Post-Quantum Transition Report Puts Every Quantum-Vulnerable Public-Key Scheme on One 2030 and 2035 Clock, and Three Federal Documents Word the Deadline Three Ways
October 28, 2026
Honeypots Recorded Probes Against Oracle E-Business Suite CVE-2026-101767 Seven Days After Its Patch, and the Finder’s Write-Up Is Dated Patch Day

Related Posts

A laptop wrapped in a chain and padlock, illustrating least-privilege controls for AI agents.
Learning Hub

How to Secure Tool-Using AI Agents Before They Touch Production

June 8, 2026
Colorful sticky notes arranged on an office wall, symbolizing governance checklists and planning.
Learning Hub

AI Governance for Agentic Apps: A Practical Checklist for Builders

June 8, 2026
A technician connects green fiber optic cables at a data center, representing a private production inference endpoint.
Learning Hub

How to Deploy a Fine-Tuned LLM Behind a Private Production Inference Endpoint

June 8, 2026
Narrow aisle behind black supercomputer racks in a data center
Learning Hub

Kubernetes SELinux Volume Labeling: What Cluster Operators Should Audit Before v1.37

June 8, 2026
SXZ.io SXZ.io
  • [email protected]

Categories

Articles
Learning Hub
News

All Rights Reserved by SXZ.io ©2026