Vol. 21 · Agent Systems

LangChain & LangGraph Under the Hood

This volume is not about “which framework is more fashionable,” but about the modern architecture of the LangChain Inc. stack. In the current version, `LangChain` is a high level with models, messages, tools, middleware and fast `create_agent`, and `LangGraph` is a low-level runtime for stateful agents: `State`, `Nodes`, `Edges`, `Command`, `thread_id`, `checkpoints`, `interrupts`, replay and long-running execution. Below is how this is really connected and what exactly happens during startup.

01

Where Are They On The Stack?

4 cards
01Framing`LangChain` and `LangGraph` today are not competitors, but two layers

Up-to-date documentation describes them as a linked stack. LangChain gives a high level: models, messages, tools, middleware, ready-made agent and standard interfaces. LangGraph sits below and is responsible for orchestration runtime: state, transitions, persistence, pauses, renewal and long-lived workflow.

Diagramtext
Contemporary Mental Model

app code

LangChain = comfortable high-level primitives

LangGraph = graph runtime and orchestration

model / tools / stores / external APIs

LangChain often uses LangGraph under the hood rather than replacing it.
02FramingCurrent stack recommendation: Start from above, drop lower in pain

The official position is simple: if you want a quick start and a typical agent cycle, it’s wise to start with ‘LangChain.create agent’. If the orchestration becomes non-standard, there are custom branches, hand hops, pauses, replay and a complex state, then they descend on the LangGraph.

briefly

  • Quickly assemble the agent LangChain
  • Custom runtime by LangGraph
  • Both are often used in the same project.
03FramingIt is important to read old materials with revision

There are a lot of old tutorials around LangChain. In them you can find "Chain", "AgentExecutor", old memory classes, LCEL-first style, and LangGraph - the former "create react agent". In the current stack, the emphasis has shifted towards 'create agent' in LangChain and low-level graph runtime in LangGraph.

What it does it mean practically?text
I found an old example on the blog.
Check what version it is on.
Do not confuse old abstractions with the current recommended entry point.
Take a look at agents/memory/graph examples
04FramingThe main boundary between them: “components” versus “execution”

LangChain primarily provides standard components and convenient high-level entry points. ‘LangGraph’ primarily determines how long a state lives, which nodes run further, where checkpoints are placed, how interrupt/resume works, and how the graph goes along the steps.

QuestionWho answers more often?
How do you call the model uniformly?LangChain
How to describe tool and schema?LangChain
How do you keep the state step by step?LangGraph
How to pause, resume and replay an agent?LangGraph
02

LangChain: High level

5 cards
05LangChainIn LangChain, the basic unit of context is ‘messages’.

Modern LangChain relies on a list of messages rather than one giant prompt-string. Key types: SystemMessage, HumanMessage, AIMessage, ToolMessage. This is important because tool-calling, memory, streaming, and structured output in an agent live around message history.

Diagramtext
MESSAGE HISTORY

system: "You're the assistant..."
user: "Find flights"
ai:     tool_call(search_flights, {...})
tool: "I found 3 options"
ai: "These are the best options..."

ToolMessage is not just a log. This is part of a valid story that the model sees again.
06LangChainCreate agent() is the main high-level input

In the current LangChain, the type agent is assembled through ‘create agent(model=..., tools=..., system prompt=..., middleware=...)’. If the list of tools is empty, you get essentially one model node without a tool-calling loop. If tools are available, the agent begins to turn the model/tool cycle to a stop condition.

minimumsql
from langchain.agents import create_agent

agent = create_agent(
    model="provider:model",
    tools=[search_docs, get_weather],
    system_prompt="Be concise",
)

result = agent.invoke({
    "messages": [{"role": "user", "content": "What's going on?"]
})
07LangChainTool in LangChain is callable with clear schema

Tool is most often created through '@tool'. The model doesn’t “see your Python,” it sees the tool name, description, and schema of arguments. When the agent decides to call the tool, LangChain parses the tool call, calls the Python function, and forms the result as ‘ToolMessage’.

typicalsql
from langchain.tools import tool

@tool
def search_docs(query: str, limit: int = 3) -> str:
    """Search internal docs."""
    ...

Docstring and signature become part of tool schema
08LangChain`ToolRuntime` opens access to state, context, store and config

Tools may not be pure functions, but runtime-aware. Through ToolRuntime, the tool receives the short-lived state of the current thread, immutable context of the call, long-term store, stream writer, config and tool call id. This makes the tool not just a utility, but a full member of the agent runtime.

signaturesql
from langchain.tools import tool, ToolRuntime

@tool
def get_last_question(runtime: ToolRuntime) -> str:
    messages = runtime.state["messages"]
    ...

Runtime parameter does not show the model as an argument tool
09LangChainStructured output and middleware are two very strong points of LangChain.

Through ‘response format’, you can ask the agent to return the typed result. LangChain uses either a native provider structured output or a tool-calling strategy, and the result is structured response. Through middleware, you can dynamically change the model, filter tools, intercept tool errors and embed policy logic.

Diagramtext
Two important hooks

response_format=MySchema
  The agent collects the structured response
  validate
  Include structured response

middleware=[...]
  Up to model call
  Around the tool call
  Dynamic routing / error handling / policy
03

LangGraph: Low-level Runtime

5 cards
10LangGraphThe three primitives of LangGraph are “State”, “Nodes”, “Edges”.

LangGraph simulates agent workflow as a graph. 'State' is the current image of the app. Nodes are functions that do something. Edges are the rules that determine which node to run next. Everything else grows out of these three: loops, branches, subgraphs, map-reduce and human-in-the-loop.

framesql
from langgraph.graph import StateGraph, START, END

builder = StateGraph(State)
builder.add_node("model", model_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "model")
builder.add_edge("tools", "model")
graph = builder.compile()
11LangGraph“State” is a shared snapshot, not just a dict for convenience.

The state in LangGraph is the central part of the performance. The nodes read the current state and return the update, which freezes back. Therefore, the state scheme should be designed consciously: what keys are, what reducers combine them, what does “message history” mean, which means “pending action”, where intermediate results lie.

state-key

  • messages
  • user_info
  • plan
  • approval
  • Don't turn the state into a garbage dump.
12LangGraphReducers are needed where updates should not be erased, but accumulated

If the node returns a new piece of state, it must somehow be combined with the old one. Message history often uses a reducer like 'add messages', otherwise you run the risk of rewrite the entire story with one new message. That is, the reducer in LangGraph is a rule of merge, not a secondary technique.

typicalpython
class State(TypedDict):
    messages: Annotated[list, add_messages]

def node(state: State):
    return {"messages": [("assistant", "next step")]}
13LangGraph“Command” means “update the state and jump on.”

When one update is not enough, the node can return Command(update=..., goto=...) It is handoff, agent routing and human-in-the-loop resume. Important detail: Command does not cancel the already added static edges. If a node has a static edge and a “goto” return at the same time, the graph can go both ways.

examplesql
from langgraph.types import Command

def route_node(state: State) -> Command[Literal["search", "answer"]]:
    if needs_search(state):
        return Command(goto="search")
    return Command(update={"done": True}, goto="answer")
14LangGraphUnder the hood, LangGraph thinks in “supersteps” rather than “functional challenges.”

LangGraph is inspired by the Pregel-like execution model. Nodes become active when they receive an incoming message/state update. Parallel branches can be executed within one super-step. When the node has no new inputs, it “votes to stop.” The count stops when all the nodes are inactive and nothing else flies over the ribs.

Diagramtext
SUPER-STEP MENTAL MODEL

node A active
  Provides updates
  Send management further

Node B and C receive entry
  Become active
  They can run in one super-step.

No new messages.
  Inactive nodes
  graph completed
04

What Really Happens During Launch

6 cards
15Runtime'create agent' under the hood is a graph-based loop

When you call ‘agent.invoke(...)’, there is no ‘one magic call’. The agent moves on the graph: model node reads "messages", generates a response; if there is a tool call, management goes to tools node; tools are executed and return "ToolMessage"; then the graph again calls the model. This is done before the final answer or the iteration limit.

Diagramtext
Agent's type cycle

user message

model node

AIMessage
  If you have tool calls, tools node
  │                         ↓
  │                     ToolMessage
  │                         ↓
  └────────────────────→ model node

AIMessage without tool calls

final output
16RuntimeAt the message level, a tool call is a strict handshake.

The model is not “itself calling Python.” It returns "AIMessage" with the tool request. LangChain reads this query, executes the desired function, and then adds "ToolMessage", which refers to the original "tool call id". The next model step receives an already extended message history and continues reasoning on its basis.

granularlytypescript
1. model node -> AIMessage(tool_calls=[...])
2. runtime parsite tool calls
3. calls Python function.
4. wraps the result in ToolMessage(tool call id=...)
5. ToolMessage added to state["messages"]
6. the next model node sees the tool result and decides what to do next
17RuntimeWhat exactly “triggers the next step”

The next step is not cosine magic, but a change in the graph state and the logic of the ribs. If model node issues a tool call, runtime directs control to tools node. If tools node returns "ToolMessage", the graph activates the model node again. If the model returns the final AIMessage without the new tool calls, the agent completes the cycle.

stop / continue

  • There is a tool calls to go to tools
  • ToolMessage is back in model
  • No tool calls can be completed.
  • Iteration limit / runtime guards
18RuntimeStructured output goes through runtime, not just parser at the end.

When you specify ‘response format’, LangChain either uses provider-native structured output or implements a schema through tool calling. The result is then validated and put into structured response. That is, the final contract is already embedded in the execution of the agent, rather than hanging a separate regex after the response of the model.

StrategyWhat's happening?
ProviderStrategyProvider returns structured response
ToolStrategystructure is implemented through tool-calling mechanics
OutcomeValidated object in 'structured response'
19RuntimeMiddleware is a model/tool runtime intervention point.

Through middleware, you can replace the model on the fly according to the dialog state, filter out available tools, wrap the tool call with custom error processing or policy logic. This is important because in a real project, an agent’s behavior is rarely determined by a prompt.

frequent use casespython
if messages_too_long:
    switch to stronger model

if tool failed:
    return friendly ToolMessage

if user has no permission:
    hide dangerous tools
20RuntimeStreaming provides not only tokens, but also state updates

LangChain/LangGraph streaming isn’t just about printing text piece by piece. You can stream state updates by agent steps, token chunks from model and custom progress events from nodes or tools. Therefore, streaming is useful for both UX and debag.

major regimestext
updates -> State deltas by step
messages -> tokens / message chunks + metadata
Traditional Signals of Progress
debug -> maximum detailed flow (LangGraph)
05

Persistence, Threads and Human-In-The-Loop

4 cards
21Persistence`thread id` + checkpointer = memory and restart

When a graph is compiled with a checkpointer, LangGraph stores the checkpoint state on each super-step. All of these checkpoints belong to thread. Therefore, ‘thread id’ is not a decorative parameter, but a key from which runtime understands which story and current state to download.

Minimal logictext
graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "thread-42"}}
graph.invoke(inputs, config=config)

Without thread id there is nothing to address the saved state
22PersistenceThread, checkpoint and state snapshot are three different entities.

Thread is the lifeline of a particular conversation or launch. Checkpoint is a snapshot of the state at a particular point in the line. “StateSnapshot” is the content of this image: values, metadata, next tasks and other runtime information. This distinction is important for replay, fork and debugging.

Diagramtext
A simple picture

thread-42
  ├─ checkpoint A
  ├─ checkpoint B
  ├─ checkpoint C
  └─ current state

You can not only store the “last state”, but also go through the history of execution.
23Persistence'interrupt()' really pauses the graph, and 'Command(resume=...)' really continues it.

Interrupt in LangGraph is a built-in execution pause mechanism for external input. When you call "interrupt()", the graph saves the state and returns the payload to " interrupt ". To continue, you need to call the graph again with the same "thread id" and transmit "Command(resume=..)". In this case, the node starts again from the beginning, and the resume value becomes the result of “interrupt()” inside the node code.

granularlytext
approved = interrupt("Approve?")

Outside:
result = graph.invoke(inputs, config=config)
print(result["__interrupt__"])

graph.invoke(Command(resume=True), config=config)

The code before interrupt can be executed again, so the side effects before it must be idempotent.
24PersistenceShort-lived memory and long-lived memory are not the same thing.

Short-term memory usually lives in the graph state and is tied to thread. Long-term memory lives in a store and can experience conversations, sessions, and threads. This is important: the checkpointer stores the progress of a particular execution, and the store stores more stable user or application data.

responsibility

  • state/messages → current conversation
  • store/preferences → long-lived data
  • Do not try to solve both problems by one entity.
06

How they're usually combined

4 cards
25PatternsWhen one LangChain is enough

If you have a typical agent loop, a comprehensible tool list, a moderate amount of state, and no specific requirements for custom graph management, 'create agent' is usually sufficient. Especially if the goal is to quickly get a production-usable baseline without manual assembly runtime.

signal

  • tool-calling assistant
  • RAG + tools
  • structured output agent
  • We need a quick start.
26PatternsWhen is the time to switch to LangGraph?

LangGraph is usually switched to when the standard "model , tools" cycle no longer describes a system. For example: many states and manual hops, multiple subagents, approval steps, time travel, long-running jobs, cycles with non-standard stop conditions, custom subgraphs and complex routing.

signal

  • human-in-the-loop
  • branching
  • custom state machine
  • long-lived
27PatternsFrequent production pattern: LangChain components within LangGraph nodes

In real systems, you don’t have to choose either-or. Often take 'init chat model', 'messages', tools, structured output and other LangChain amenities, but orchestration is written manually through LangGraph. That is, LangChain supplies building blocks, and LangGraph manages the life cycle and transitions.

Diagramtext
Hybrid assembly

LangGraph node
  ─ calls the LangChain model
  Works with LangChain messages
  Uses LangChain tools/schemas
  Update returns to graph state

This is essentially the most natural way to use the stack together.
28PatternsThe Normal Way of Project Evolution

It’s almost always easier not to “design a perfect graph in advance,” but to go in layers. First, take 'create agent' and check the problem. Then take the policy out to middleware. Then, if this is not enough, build a custom “StateGraph”, leaving the LangChain components inside. This reduces the risk of overengineering.

evolutionsql
1. create_agent baseline
2. middleware / response_format / memory
3. see where high-level behavior is no longer enough
4. take out orchestration in StateGraph
5. LangChain remains a layer of components
07

Grabbles, Minimum Code and Output

4 cards
29OpsFrequent rakes in LangChain/LangGraph

The main problem here is not the AI, but the wrong mental model. People confuse old versions with new ones, think tool call is the magic of the model, don't pass 'thread id', put non-idempotent side effects before 'interrupt', mix static edges and 'Command', or try to debug the graph as a linear function.

red flags

  • Read the old tutorial as the current standard
  • No 'thread id' with persistence/interruptions
  • side effects to 'interrupt' without idempotency
  • Expect Command to eliminate static edge
30BridgeMinimum code: High-level agent vs manual graph

These two pieces of code show the difference. In the first case, you describe “what I need”, and runtime is already assembled. In the second, you describe the execution graph itself.

LangChaintext
agent = create_agent(
    model="provider:model",
    tools=[search_docs],
    response_format=AnswerSchema,
)
LangGraphtext
builder = StateGraph(State)
builder.add_node("plan", plan_node)
builder.add_node("act", act_node)
builder.add_node("review", review_node)
builder.add_edge(START, "plan")
builder.add_conditional_edges("plan", route)
graph = builder.compile(checkpointer=checkpointer)
31BridgeIf you need to explain the stack in one sentence

LangChain helps to quickly assemble agent applications from standard components. LangGraph gives precise control over how these applications are actually executed in time: with state, branching, pauses, memory and renewal.

formula

  • LangChain = components + high-level API
  • LangGraph = orchestration runtime
32BridgeThe main idea of the volume

To speak well of LangChain and LangGraph, it’s helpful to stop thinking of them as “two competing libraries for AI.” High-level agent components on top, low-level stateful runtime on bottom. Then it becomes clear why "create agent" works quickly, and why StateGraph is even needed.

takeaway

  • Understand the message history
  • Understand state + edges
  • Understand thread/checkpoint/interrupt
  • This is the language of conversation with LangChain/LangGraph engineers.

No dead end

Keep moving through the map.

Continue in sequence, switch to a related guide, or return to the seven-track learning map.