Why LangGraph? From a While-Loop Agent to a Graph (LangGraph in Production, Module 01)
Why LangGraph? From a While-Loop Agent to a Graph (LangGraph in Production, Module 01)
This is Module 01 of LangGraph in Production, a free 16-module course that takes you from your first StateGraph to a deployed, durable autonomous coding agent on LangGraph v1.x. Start at Module 1 or browse the full syllabus.
The While-Loop Agent You’ve Already Written
Here is the agent almost everyone builds first:
# The naive while-loop agent — works great in a demo, falls apart in production
def run_agent(llm, tools, initial_messages):
messages = list(initial_messages)
while True:
response = llm.invoke(messages)
messages.append(response)
if not response.tool_calls:
return response # done
for tc in response.tool_calls:
result = tools[tc["name"]](**tc["args"])
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
It calls the model, checks for tool calls, executes them, loops. Clean enough. You demo it, it works, you feel good.
Then your teammate asks: “Can it pick up where it left off if the process dies?” You add a flag and a pickle file. Then: “Can it wait for my approval before it edits a file?” You add a bool and a time.sleep loop. Then: “Can it process those four files in parallel?” You add threading.Thread. Then: “Can I replay the run from 14
Each new requirement glues another patch onto the while loop. After three patches the control flow is invisible. After six it is a maintenance liability.
Those are not bugs in your code. They are capabilities that a bare loop cannot provide: resumability after a crash, human-in-the-loop gates, deterministic parallel branches, and inspectable execution history. These are platform concerns. LangGraph makes them first-class primitives — not afterthoughts bolted on with try/except and global state.
That is the module’s premise. By the end you will understand why, you will have the stack installed, and you will have run your first compiled StateGraph — the seed of Forge, the autonomous coding agent you will build across 16 modules.
In This Module
You’ll learn:
- Why a
while-loop agent breaks on four specific axes: cycles, branching, persistence, and human-in-the-loop (HITL) - The three core primitives of any LangGraph graph: node, edge, and state
- The Pregel / superstep execution model that makes LangGraph deterministic
- Where LangGraph ends and LangChain begins after the 1.0 split of October 22, 2025
- When NOT to reach for LangGraph (honest answer: not always)
- How
init_chat_modelmakes Forge model-agnostic with a one-line swap
You’ll build: your development environment for the whole series (uv, LangGraph v1.x, .env), the model layer Forge runs on (get_model via init_chat_model), and your first compiled StateGraph.
Concepts covered: CA1 — Foundations & mental model (core) — 7 concepts, this is the primary module for CA1.
Prerequisites: solid Python 3.10+ (the course uses 3.12), any LLM API basics, git/CLI comfort, and either an ANTHROPIC_API_KEY or Ollama installed (free path). No cloud account needed.
T4 — The Series, Forge, and Tally
A Free 16-Module Course Without a Certification
There is no LangGraph certification. Instead of a badge, you leave with something more useful: a public repo containing Forge, a production-grade autonomous coding agent you built and deployed yourself. That is the credential — the repo, not a certificate. The series has no exam, no domains, no percentage weights. What it has is an artefact-portfolio: something you can point at in a job interview and walk through in detail.
Sixteen modules, all free. Cost for all 15 labs: approximately $2.50–$3 at June 2026 Claude prices, or $0 with the Ollama fallback documented in every lab. You pick.
Forge
Forge is a production-grade autonomous coding agent: hand it an issue, it plans the change, you approve the plan, it edits the target files in parallel, runs the test suite in a fix-it loop until green, opens a pull request, and waits for your approval to ship — fully persisted, durable, streamed, and evaluated with LangGraph v1.x.
Forge exercises every major LangGraph capability: stateful graphs, conditional routing, cycles, HITL gates via interrupt(), parallel fan-out with the Send API, persistence with checkpointers, long-term memory with the Store, streaming with get_stream_writer, subgraphs, the Functional API (@entrypoint/@task), and deployment on LangGraph Platform. You build it incrementally. Each module adds one capability to a real, running system.
If you have already taken the NCP-AAI course and built Scout (a research assistant in LangGraph), Forge is a different animal: Forge is a coding agent, it covers the complete v1.x surface that Scout only touched the edges of, and it is the case study most searched by developers in 2026.
Tally
Tally is the target repo Forge will work on. It is a small Python expense-tracking CLI: cli.py, storage.py, models.py, report.py, and a real pytest suite (~6 files). Forge always works on a sandboxed copy of Tally (a tempfile directory) — never the original. The sandbox is hardened in Module 9; for now, the key principle is: Tally stays untouched.
The 16-Module Map
M1 Setup & mental model → this module
M2 StateGraph fundamentals → ForgeState, reducers, intake
M3 Control flow → router, test-fix cycle, recursion_limit
M4 Tools & the agent loop → Planner, create_agent, structured output
M5 Parallelism & map-reduce → Send API, Editor×N, fan-in
M6 Persistence & threads → SqliteSaver, survive a kill
M7 Human-in-the-loop → interrupt(), plan approval, PR approval
M8 Time-travel & debugging → replay, fork, get_state_history
M9 Durable execution → durability=, RetryPolicy, sandbox hardening
M10 Long-term memory → the Store, semantic recall
M11 Streaming → stream modes, get_stream_writer
M12 Multi-agent → subgraphs, Supervisor, handoffs
M13 Functional API → @entrypoint/@task variant
M14 Deployment → LangGraph Platform, Studio, SDK
M15 Evaluation → LangSmith, golden set, trajectory eval
M16 Review (no lab) → production checklist, mastery review
The While-Loop Agent and Where It Breaks
Let’s be precise about the four failure modes. These are not edge cases — they are the exact walls that Forge hits.
1. Persistence: What Happens When the Process Dies
A while-loop agent lives entirely in the call stack. When the process exits — OS kill, OOM, deployment restart, laptop lid — the entire execution state vanishes. You restart from scratch.
For a demo that runs in 10 seconds, that is fine. For an agent that plans and edits code over minutes, it is not. Forge needs to survive a kill mid-run and resume exactly where it stopped. LangGraph checkpoints the graph state at every superstep (more on that shortly) into a durable store. After a restart, it reads the latest checkpoint and continues. This is covered in Module 6 with SqliteSaver.
2. Cycles: The Test-Fix Loop
A coding agent that runs tests and fixes failures is inherently cyclic: run_tests → fail → fix → run_tests → fail → fix → .... In a while loop, you implement this as a nested loop or a flag. There is no natural recursion guard. LangGraph graphs have explicit cycles with a configurable recursion_limit that raises GraphRecursionError if the loop spins past the bound. The test-fix cycle of Forge is built in Module 3.
3. Branching: Routing by Issue Type
Forge needs to handle bugs, features, and chores differently. In a while loop, routing is if/elif/else scattered through the function — invisible from the outside, hard to test in isolation. In LangGraph, routing is a conditional edge: a function that returns a node name, attached explicitly to the graph. You can read the graph structure, visualize it, and test the router in isolation. This arrives in Module 3.
4. Human-in-the-Loop: Waiting for Approval
A coding agent should pause and wait for human approval before doing destructive things (editing files, opening a PR). In a while loop, this is a polling loop with a flag that must be checked across process boundaries — or you serialize to a queue and rehydrate state. In LangGraph, it is interrupt(): one call that halts execution, persists the full state, and resumes when you call Command(resume=...). This is Module 7.
The Three Primitives: Node, Edge, State
LangGraph builds on three ideas. You need them clearly defined before any code makes sense.
Node
A node is a Python function with the signature (state) -> partial_state. It receives the full current state, does some work, and returns a dictionary of keys to update. It does not return the entire state — only the fields it touched. The graph applies those updates to the shared state.
def respond(state: HelloState) -> dict:
return {"messages": [model.invoke(state["messages"])]}
This node reads state["messages"], calls the model, and returns a dict with one key. That is the complete contract.
Edge
An edge is a directed connection between nodes. It tells the graph what to execute after a node finishes. You connect nodes with builder.add_edge(source, target). The special nodes START and END (from langgraph.graph) mark the entry and exit points of the graph.
from langgraph.graph import START, END, StateGraph
builder.add_edge(START, "respond")
builder.add_edge("respond", END)
A conditional edge uses a function instead of a fixed target — that is Module 3.
State
The state is a typed dictionary that flows through the graph. Every node reads from it and writes partial updates back. LangGraph calls the state fields channels. Each channel has a reducer that controls how updates are merged.
MessagesState is LangGraph’s built-in state schema for message-passing agents:
from typing import Annotated
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
class HelloState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
The Annotated[list[AnyMessage], add_messages] means: the messages channel accumulates (appends) rather than overwrites. The add_messages reducer is how multiple nodes can append messages without clobbering each other. The full state schema for Forge — ForgeState — is born in Module 2.
The Hello-Graph
Here is the smallest possible graph: one node, two edges, live in the lab as hello_graph.py:
from typing import Annotated, TypedDict
from langchain_core.messages import AnyMessage, HumanMessage
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from forge.config import get_model
class HelloState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
def build_hello_graph(model=None):
model = model or get_model("fast")
def call(state: HelloState) -> dict:
return {"messages": [model.invoke(state["messages"])]}
builder = StateGraph(HelloState)
builder.add_node("call", call)
builder.add_edge(START, "call")
builder.add_edge("call", END)
return builder.compile()
builder.compile() transforms the builder into a compiled graph — the object you call .invoke() or .stream() on. The graph is now a reusable, inspectable artifact. You can visualize its structure, pass it to a checkpointer, or run it inside a subgraph.
The control flow looks like this:
flowchart LR
direction LR
S([START]) --> callNode["call<br/>(invoke the model)"]
callNode --> E([END])
subgraph state ["HelloState (channels)"]
direction TB
M["messages: Annotated[list[AnyMessage], add_messages]"]
end
S -.->|"{ messages: [HumanMessage] }"| state
callNode -.->|"partial: { messages: [AIMessage] }"| state
state -.->|"merged by add_messages"| callNode
The state flows into the node, the node returns a partial update, the update is merged by the reducer, and the result propagates to the next node. This is the whole execution model in miniature.
Under the Hood: the Pregel / Superstep Model
LangGraph executes graphs using the Pregel model, named after the Google paper on bulk-synchronous parallel computation (BSP). Understanding this is what separates you from someone who just copies examples.
Here is how it works:
- All nodes in the current step execute. If multiple nodes are scheduled for the same step (parallel branches), they all run concurrently.
- Writes are applied at the barrier. No node sees another node’s writes until the entire step completes. This makes parallel branches deterministic.
- The next step begins. The graph checks which nodes have new input and schedules them.
Each superstep is a complete round of this cycle. The boundary between supersteps is where the graph can be safely checkpointed — which is why LangGraph’s persistence story works so cleanly. The checkpoint is always consistent: it captures the state after a full barrier, not mid-computation.
For the hello-graph there is only one superstep: START → call → END. The parallelism model becomes important in Module 5, where Forge uses the Send API to fan out to multiple Editor nodes simultaneously. The checkpoint model becomes important in Module 6.
LangGraph vs LangChain After the 1.0 Split
On October 22, 2025, LangChain Inc. released both frameworks at v1.0 GA and clarified the boundary between them. This is the most important context to have right.
| LangGraph | LangChain | Hand-rolled while-loop | |
|---|---|---|---|
| Role | Low-level runtime | High-level API / middleware | DIY control flow |
| Abstraction level | Graphs, Pregel, persistence, durability, streaming | Agent patterns, tools, create_agent, prompt middleware | None |
| What you write | Nodes, edges, state schemas, checkpointers | create_agent(model, tools), chains, prompts | while True:, if/else, try/except |
| When to use | Stateful, cyclical, durable, HITL, or parallel agents | Building on top of an existing graph runtime | Simple, stateless, one-shot calls |
LangGraph is the runtime: it handles the graph execution engine (Pregel), persistence, streaming, durability, and the low-level HITL primitives. LangChain is the high-level API that lives on top of the LangGraph runtime. When you call create_agent in Module 4, you will import it from langchain.agents — it builds a LangGraph graph for you using higher-level conventions, while still running on the same Pregel runtime.
The practical takeaway: these are two libraries with a clear layering, not two alternatives. LangGraph is the engine. LangChain is the toolkit that uses the engine.
GA 1.0 commitment: both projects promise no breaking changes before v2.0. The ~=1.2 / ~=1.3 pins in the labs will not surprise you with unexpected breakage.
When NOT to Use LangGraph
Honesty requires this section. LangGraph has overhead — you define a graph builder, compile it, and manage state schemas. If your agent:
- Makes a single LLM call to summarize text
- Runs a static prompt chain with no branching or cycles
- Does a stateless transform on an input
…you do not need LangGraph. The overhead is not worth it. A plain model.invoke(messages) or a LangChain expression chain is the right tool. Module 16 goes deep on this distinction with a full “when NOT to use” analysis.
init_chat_model and Model Agnosticism
init_chat_model is LangChain’s unified entry point for any supported model provider. Instead of instantiating ChatAnthropic(model="...", temperature=0) directly — which locks you to Anthropic in every node — you call:
from langchain.chat_models import init_chat_model
model = init_chat_model("anthropic:claude-haiku-4-5", temperature=0)
The string "provider:model" is all LangChain needs. Change "anthropic:claude-haiku-4-5" to "ollama:qwen2.5-coder:7b" and the same call runs locally for $0, with identical interface. That is the pedagogical point of CA1.6.
In Forge, this flexibility is isolated in one place: forge/config.py. Every other file calls get_model(tier) and never touches a model ID.
Reading the Response: .content_blocks
In LangGraph v1.x, the standardized way to read the content of an AI message is .content_blocks (not the bare .content string). .content_blocks returns a list of typed content items — text blocks, tool-use blocks, thinking blocks — that works consistently across providers and across Claude model families. Reading .content as a string works for simple text but breaks when the model returns structured content. Get into the habit of .content_blocks now; it is the CA1.7 contract for this entire course.
⚠️ Common misconception — two freshness traps that will burn you
Trap 1:
create_react_agentfromlanggraph.prebuilt. If you copy a tutorial from before October 2025, it likely does this:# v0 idiom — deprecated at 1.0, removed in 2.0 from langgraph.prebuilt import create_react_agent agent = create_react_agent(model, tools)In LangGraph v1.x this is deprecated (1.0) and removed in v2.0. The v1 path is:
# v1 canonical path — covered in Module 4 from langchain.agents import create_agentRoughly 60–80 % of the LangGraph tutorials that rank on Google in mid-2026 still use
create_react_agent. This course teaches only the v1 idioms; the old symbol appears here — in this callout — and never again in live code.Trap 2: “LangChain and LangGraph are the same thing.” They are not, and the distinction matters. LangGraph is the low-level runtime (graphs, Pregel, persistence). LangChain is the high-level API that hosts
create_agentand the prompt/chain middleware. They split into separate release tracks at 1.0 on October 22, 2025.Bonus trap:
config["configurable"]for app context. The v0 pattern was to pass application context viaconfig={"configurable": {...}}. In v1, app context goes throughcontext_schema+Runtime(fromlanggraph.runtime). Theconfigurabledict is still used for thread-scoped things likethread_id(Module 6) — but not for injecting app-level dependencies. Details in Module 6.
Hands-On Lab: Setup and Your First StateGraph
This lab cost me $0.03 at June 2026 prices. Forge always works on a sandbox copy of Tally — never the original.
Goal: bootstrap the environment for the entire series, seed the model layer, compile and run a first StateGraph.
Observable result:
uv run python -m pytest tally/tests/ -q → 6 passed in 0.02s (no API key needed)
uv run python -m pytest tests/ -q → 2 passed in 0.07s (offline, model stubbed)
uv run python hello_graph.py "What does a StateGraph give me?" → model reply
Step 1 — Initialize the Repo with uv
Create the repo root and the module directory:
mkdir langgraph-prod-labs && cd langgraph-prod-labs
uv init --python 3.12
mkdir module-01 && cd module-01
uv init creates a pyproject.toml at the root and sets up the virtual environment with Python 3.12. All subsequent uv run commands use this environment.
Step 2 — Pin the v1.x Dependencies
Add to your module-01/pyproject.toml (or the root pyproject.toml):
[project]
name = "langgraph-prod-labs-module-01"
version = "0.1.0"
requires-python = ">=3.12"
[project.dependencies]
langgraph = "~=1.2" # 1.2.5
langchain = "~=1.3" # 1.3.9
langchain-core = "~=1.4" # 1.4.7
langchain-anthropic = "~=1.4" # 1.4.6
python-dotenv = ">=1.0"
pytest = "~=8"
Run uv sync and commit uv.lock. The lock file is what makes the labs reproducible — anyone who clones the repo gets byte-identical versions.
uv sync
git add pyproject.toml uv.lock
git commit -m "module-01: pin langgraph v1.x stack"
Step 3 — Set Up Credentials
Create .env in module-01/:
# .env — never commit this file
ANTHROPIC_API_KEY=sk-ant-...
Add .env to .gitignore. Load it at the top of scripts with:
from dotenv import load_dotenv
load_dotenv()
Unlike course 2 (AWS), where credentials went through IAM profiles, here you store a third-party API key directly in .env. This is the normal pattern for LLM API keys.
Zero-cost path: If you have Ollama installed and do not want to use an Anthropic key, skip this step entirely and set FORGE_MODEL_PROVIDER=ollama in your environment. The forge/config.py you create in the next step handles both paths with no other code changes.
Step 4 — Seed forge/config.py (the Model Layer)
Create module-01/forge/__init__.py (empty) and module-01/forge/config.py:
"""The model layer — the SOLE home of model IDs (frozen at Module 1).
Forge is model-agnostic: every node asks for a *tier* ("fast" or "smart")
and `get_model` resolves it through `init_chat_model`. Swapping providers
is a one-line change here — that is the whole point of `init_chat_model`.
fast -> cheap, deterministic work (triage, routing, simple fixes, tests)
smart -> planning, editing, review
To run every lab for $0, set FORGE_MODEL_PROVIDER=ollama (and `ollama pull
qwen2.5-coder`). No other file changes.
"""
from __future__ import annotations
import os
from typing import Literal
from langchain.chat_models import init_chat_model
from langchain_core.language_models import BaseChatModel
Tier = Literal["fast", "smart"]
# Default (Anthropic). IDs verified June 2026.
_ANTHROPIC = {
"fast": "anthropic:claude-haiku-4-5", # triage, routing, simple fixes
"smart": "anthropic:claude-sonnet-4-5", # planning, editing, review
}
# Free, local fallback — one-line swap, $0. Ollama exposes an OpenAI-compat API.
_OLLAMA = {
"fast": "ollama:qwen2.5-coder:7b",
"smart": "ollama:qwen2.5-coder:7b",
}
_PROVIDERS = {"anthropic": _ANTHROPIC, "ollama": _OLLAMA}
def model_spec(tier: Tier) -> str:
provider = os.environ.get("FORGE_MODEL_PROVIDER", "anthropic")
return _PROVIDERS[provider][tier]
def get_model(tier: Tier = "fast", **kwargs) -> BaseChatModel:
"""Resolve a tier to a concrete chat model. The only place model IDs live."""
return init_chat_model(model_spec(tier), **kwargs)
This file is frozen from this point forward. Modules 2–16 inherit it byte-identical. If you need a new model in a later module, you add a constant here — you never edit _ANTHROPIC, never add a new if to get_model, never put a model ID string anywhere else in the codebase. That discipline is the whole point.
Note the FORGE_MODEL_PROVIDER environment variable. Setting it to "ollama" switches every get_model call in the course to local inference with no other changes. That is the one-line swap that CA1.6 is about.
Step 5 — Write hello_graph.py
Now create the first graph. The full file lives in the lab repo; here are the essential parts:
from typing import Annotated, TypedDict
from langchain_core.messages import AnyMessage, HumanMessage
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from forge.config import get_model
class HelloState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
def build_hello_graph(model=None):
model = model or get_model("fast")
def call(state: HelloState) -> dict:
return {"messages": [model.invoke(state["messages"])]}
builder = StateGraph(HelloState)
builder.add_node("call", call)
builder.add_edge(START, "call")
builder.add_edge("call", END)
return builder.compile()
The build_hello_graph(model=None) signature accepts an optional model argument. When called without one, it uses get_model("fast"). When called from tests, you pass in a fake model — no API key required.
To read the response, use .content_blocks:
result = graph.invoke({"messages": [HumanMessage(content=question)]})
last = result["messages"][-1]
# Read the standardized content blocks (v1 contract):
for block in last.content_blocks:
if block.type == "text":
print(block.text)
Run it:
uv run python hello_graph.py "What does a StateGraph give me?"
Expected: the model answers in one sentence, printed to stdout.
With the Ollama fallback:
FORGE_MODEL_PROVIDER=ollama uv run python hello_graph.py "What does a StateGraph give me?"
Same command, same output shape, $0 spent.
Step 6 — Tally (the Fixture Repo)
Tally lives in module-01/tally/. It is a real Python project with real tests. Copy it from the lab repo or create it:
tally/models.py— PydanticExpensemodel with validatorstally/storage.py— JSON file storage, CRUD for expensestally/report.py— per-category summariestally/cli.py— CLI entry pointstally/tests/— 6 pytest tests (no mocking, no API, deterministic)
The Tally tests are the lab’s observable signal. They run without any API key and tell you the environment is healthy:
uv run python -m pytest tally/tests/ -q
# 6 passed in 0.02s
If that is green, your Python environment, uv, and the package layout are all correct.
Step 7 — The Offline Smoke Test
Create module-01/tests/conftest.py:
"""Offline test harness — graphs run without an API key."""
import pytest
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
def fake_model(responses):
msgs = [AIMessage(content=r) if isinstance(r, str) else r for r in responses]
return GenericFakeChatModel(messages=iter(msgs))
@pytest.fixture
def make_fake_model():
return fake_model
And module-01/tests/test_module_01.py:
"""Module 1 smoke test: the first StateGraph compiles and runs offline."""
from langchain_core.messages import AIMessage, HumanMessage
from hello_graph import build_hello_graph
def test_hello_graph_runs_offline(make_fake_model):
model = make_fake_model([AIMessage(content="A StateGraph wires nodes over typed state.")])
graph = build_hello_graph(model=model)
out = graph.invoke({"messages": [HumanMessage(content="what is a StateGraph?")]})
assert out["messages"][-1].content == "A StateGraph wires nodes over typed state."
assert len(out["messages"]) == 2 # human turn + AI turn
def test_get_model_is_the_only_model_id_home():
from forge.config import model_spec
assert model_spec("fast").startswith("anthropic:")
assert model_spec("smart").startswith("anthropic:")
Run:
uv run python -m pytest tests/ -q
# 2 passed in 0.07s
The GenericFakeChatModel from langchain_core.language_models.fake_chat_models replays canned AIMessage responses in sequence, so the graph runs, the state is updated, and the assertions fire — all without touching any API.
Live tests (optional) use a skip_unless_live marker gated on FORGE_LIVE_TESTS=1. One real get_model("fast") call.
Step 8 — Try It Yourself
Two extensions to explore on your own (no solutions provided — the comments in the code are enough):
a. Add a second node. Add a shout node that takes the AI message and uppercases it, then wire call → shout → END. Run the graph and observe that the messages channel now has three entries: the human turn, the AI turn, and the shouted version. Notice that shout executes in its own superstep after call finishes.
b. Verify the fallback. Set FORGE_MODEL_PROVIDER=ollama, make sure Ollama is running with qwen2.5-coder:7b pulled, and run hello_graph.py again. Same output format, $0 spent. Then look at forge/config.py and confirm that is the only file you would need to touch to switch any lab in the course to a different provider.
In Production
Three things change when you move from a laptop demo to production, and all three are baked into the choices made in this module.
Reproducibility is not optional. The uv.lock file you committed in Step 2 is not bureaucracy — it is the guarantee that your teammate, your CI runner, and your production container all run the exact same versions. The LangGraph ecosystem moves fast; a minor version bump can break an idiom. The lock file is your snapshot. Commit it every time uv sync changes it.
Model agnosticism is a cost lever. get_model("fast") currently resolves to Claude Haiku, which costs roughly $1/$5 per million input/output tokens (as of June 2026). If a cheaper model becomes available, or if you want to run a lab batch without spending anything, you change one line in forge/config.py. No other file changes. That is not an academic point — in Module 15, where Forge runs 10 full issues against the golden set, the tier selection drives the cost of the eval run. The discipline of a single model-ID file pays off there. The full cost/quality tradeoff is analyzed in Module 15.
Security starts now. Forge will eventually execute code — real pytest runs in a subprocess, file writes applied to a sandboxed repo. The invariant that Tally’s original files are never touched is not just a learning scaffold; it is the first ring of the security model. When an agent executes arbitrary code, “the sandbox is a copy” is the most important sentence in the design. The sandbox is hardened in Module 9 against path traversal, resource exhaustion, and secrets leakage. But the principle — Forge never touches the original — is established here and never relaxed.
Mastery Corner
What to Really Understand Here
CA1.1 — The four break points. A while-loop agent cannot natively support persistence across process restarts (no checkpoint), deterministic parallel branches (shared mutable state), explicit routing graphs (buried in if/else), or HITL gates (polling + flag). Each is a platform capability, not a code smell.
CA1.2 — Node / edge / state. A node is (state) -> partial_state (a delta, not a full replacement). An edge is a directed connection; START and END are LangGraph sentinel nodes. The state is a typed dict of channels, each with a reducer. A channel with add_messages accumulates; a plain TypedDict field (no annotation) is last-write-wins.
CA1.3 — Pregel / superstep. All nodes in a step execute; their writes are collected; the barrier fires; the merged state is the input to the next step. This is what makes parallel branches deterministic and what makes every superstep boundary a natural checkpoint site.
CA1.4 — The 1.0 split. LangGraph = the runtime (Pregel engine, graphs, persistence, streaming). LangChain = the high-level API (agent patterns, prompt middleware, create_agent). They share the same underlying runtime but have separate versioning and different abstraction levels.
CA1.5 — When NOT to use LangGraph. One-shot stateless calls, static prompt chains, and simple transforms do not need a graph. The orchestration overhead is real. Do not add LangGraph just because it is there.
CA1.6 — Model agnosticism. init_chat_model("provider:model") is the single switch. The one-line swap from Claude to Ollama is a design constraint enforced by keeping all model IDs in forge/config.py.
CA1.7 — .content_blocks. Reading .content as a bare string works for simple text but breaks on structured content. Use .content_blocks to iterate over typed content items (text, tool-use, thinking). This is the v1 standardized content contract.
5 Mastery Questions
Q1. Your manager asks you to add a “summarize this document” feature to an existing pipeline. The feature takes one document, calls the model once, and returns the summary. A colleague suggests wrapping it in a StateGraph. What do you tell them?
- A. Use
StateGraph— it handles token management automatically. - B. Use
StateGraph— it is required for any LLM call in LangGraph v1. - C. Skip
StateGraph— a single stateless LLM call does not justify the overhead. - D. Skip
StateGraph— it only works with multi-turn conversations.
Q2. A node in your graph returns {"messages": [ai_message]}. The current state has messages: [human_message]. After the superstep barrier, what is the value of state["messages"]?
- A.
[ai_message]— the node’s return value overwrites the channel. - B.
[human_message, ai_message]—add_messagesappends the returned message. - C.
[human_message]— the state is immutable until the graph exits. - D. An error — you cannot return a list from a node.
Q3. You clone a LangGraph tutorial from GitHub. It imports from langgraph.prebuilt import create_react_agent and the import raises a deprecation warning in LangGraph 1.2.5. What is the correct v1.x replacement, and in which module of this course do you build it?
- A.
from langgraph.graph import create_agent— Module 2. - B.
from langchain.agents import create_agent— Module 4. - C.
from langgraph.prebuilt import ReActAgent— Module 3. - D. There is no v1 replacement; you must build the ReAct loop by hand.
Q4. Forge’s model layer uses get_model("fast") in every node. Your team wants to switch from Anthropic to an Ollama local model for a cost-reduction experiment. How many files need to change?
- A. Every file that calls
get_model. - B. Every file that contains an LLM call.
- C. One file:
forge/config.py, specifically the_PROVIDERS/FORGE_MODEL_PROVIDERconstant. - D. None — LangGraph automatically discovers local models.
Q5. In an interview, you are asked: “What is the architectural difference between LangGraph and LangChain after the October 2025 v1.0 release?” Which answer is most accurate?
- A. LangGraph and LangChain are the same project; the names are interchangeable.
- B. LangGraph is the low-level runtime (Pregel engine, graphs, persistence, streaming); LangChain is the high-level API (agent patterns, middleware,
create_agent) that runs on the LangGraph runtime. - C. LangChain is deprecated in v1.0; all functionality moved to LangGraph.
- D. LangGraph is for multi-agent systems; LangChain is for single-agent systems.
Answers
Q1 → C. A single stateless LLM call does not need a StateGraph. The graph builder, schema, and compilation have real overhead. Use LangGraph when you need cycles, branching, persistence, HITL, or parallel execution — not for a one-off call (CA1.5).
Q2 → B. The messages channel is annotated with add_messages, which is an accumulating reducer. When a node returns {"messages": [ai_message]}, the add_messages function appends it to the existing list. The write is applied at the superstep barrier, so state["messages"] becomes [human_message, ai_message] (CA1.2 + CA1.3).
Q3 → B. The v1 canonical import is from langchain.agents import create_agent. langgraph.prebuilt.create_react_agent is deprecated at LangGraph 1.0 and removed at 2.0. In this course, create_agent is covered in Module 4 (CA1.4).
Q4 → C. Only forge/config.py changes — specifically setting FORGE_MODEL_PROVIDER=ollama (or editing _PROVIDERS). Every other file calls get_model(tier) and is unaware of the underlying model ID. That is the whole design point of the model layer (CA1.6).
Q5 → B. LangGraph is the low-level Pregel-based runtime. LangChain is the high-level API that provides create_agent, prompt templates, chains, and other patterns, and it runs on top of the LangGraph runtime. They are separate but layered, with separate version tracks since the October 22, 2025 1.0 GA release (CA1.4).
Traps to Avoid
Trap 1 — Using create_react_agent by reflex.
This is the most common mistake when learning LangGraph in 2026. from langgraph.prebuilt import create_react_agent works in LangGraph 0.x, is deprecated at 1.0, and is removed at 2.0. The v1 path is from langchain.agents import create_agent. An estimated 60–80 % of the tutorials currently ranking for “LangGraph tutorial” still use the old idiom. If you copy one, check the imports first.
Trap 2 — Conflating LangGraph and LangChain. They are not the same, and they are not interchangeable. LangGraph is the engine; LangChain is the toolkit that uses the engine. Saying “I use LangChain” when you mean “I use LangGraph” is like saying “I use the compiler” when you mean “I use the language.” The split at 1.0 formalized a boundary that was already there in the architecture.
Trap 3 — Reaching for a graph when you do not need one.
A stateless summarization step, a static two-prompt chain, a one-shot classifier — none of these justify a StateGraph. The graph has overhead: schema definition, compilation, state management. Use it when you need what it provides: stateful execution, cycles, persistence, branching, HITL, or parallelism. If you are writing builder.add_edge(START, "step1"); builder.add_edge("step1", END) with no cycles, no branches, and no need to resume — you have added ceremony without value.
Key Takeaways
- A
while-loop agent breaks on four specific platform capabilities: persistence after a crash, cycles with bounded recursion, conditional branching that is inspectable, and HITL gates that survive process restarts. AStateGraphprovides all four as primitives. - A graph has three core concepts: a node (
(state) -> partial_state), an edge (a wired connection between nodes, usingSTARTandEND), and a state (a typed dict of channels with reducers). - LangGraph executes using the Pregel / superstep model: all nodes in a step execute, their writes are collected, the barrier fires, and the merged state flows to the next step. This is why parallel branches are deterministic and why every superstep boundary is a checkpoint site.
- After the 1.0 split on October 22, 2025: LangGraph is the low-level runtime (Pregel engine, persistence, streaming); LangChain is the high-level API (agent patterns,
create_agent, middleware). Usefrom langchain.agents import create_agent— neverfrom langgraph.prebuilt import create_react_agent(deprecated 1.0, removed 2.0). init_chat_model("provider:model")is the single entry point for any supported LLM. All model IDs live only inforge/config.py(get_model(tier)). Swapping from Claude to Ollama is one environment variable. The course costs $2.50–$3 total with Claude or $0 with Ollama.- Read AI message content via
.content_blocks(not bare.content). This is the v1 standardized content contract that works across providers and model families. - Do not reach for LangGraph when you do not need it. A single LLM call, a static prompt chain, or a stateless transform has no business inside a graph. Use
StateGraphwhen you need statefulness, cycles, persistence, HITL, or parallel execution.
What’s Next and How to Stay Connected
Your hello-graph is a single node with a single channel. In Module 2, it grows a real state schema — ForgeState is born (issue, messages, repo_path) with reducers, and Forge starts reading issues from Tally. The while-loop skeleton becomes a real directed graph with an intake node.
Want the full LangGraph mastery question bank (interview-style) and the next module in your inbox? Subscribe here — it’s free, like everything in this series.
Links:
References
- LangChain / LangGraph 1.0 GA announcement (October 22, 2025): blog.langchain.com/langgraph-v1
- LangGraph conceptual documentation — “Why LangGraph”: langchain-ai.github.io/langgraph/concepts/why-langgraph/
- LangGraph low-level API —
StateGraph: langchain-ai.github.io/langgraph/reference/graphs/ init_chat_model(LangChain): python.langchain.com/docs/how_to/chat_models_universal_init/uv— Python package and project manager: docs.astral.sh/uv- LangGraph Pregel execution model: langchain-ai.github.io/langgraph/concepts/pregel/