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.
Where Are They On The Stack?
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.
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.
Source-complete notesbriefly
briefly
- Quickly assemble the agent LangChain
- Custom runtime by LangGraph
- Both are often used in the same project.
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.
Source-complete notesWhat it does it mean practically?
textI 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 examplesLangChain 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.
Source-complete notestable
| Question | Who 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 |
LangChain: High level
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.
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.
Source-complete notesminimum
sqlfrom 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?"]
})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’.
Source-complete notestypical
sqlfrom langchain.tools import tool
@tool
def search_docs(query: str, limit: int = 3) -> str:
"""Search internal docs."""
...
Docstring and signature become part of tool schemaTools 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.
Source-complete notessignature
sqlfrom 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 toolThrough ‘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.
LangGraph: Low-level Runtime
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.
Source-complete notesframe
sqlfrom 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()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.
Source-complete notesstate-key
state-key
- messages
- user_info
- plan
- approval
- Don't turn the state into a garbage dump.
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.
Source-complete notestypical
pythonclass State(TypedDict):
messages: Annotated[list, add_messages]
def node(state: State):
return {"messages": [("assistant", "next step")]}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.
Source-complete notesexample
sqlfrom 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")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.
What Really Happens During Launch
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.
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.
Source-complete notesgranularly
typescript1. 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 nextThe 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.
Source-complete notesstop / continue
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
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.
Source-complete notestable
| Strategy | What's happening? |
|---|---|
| ProviderStrategy | Provider returns structured response |
| ToolStrategy | structure is implemented through tool-calling mechanics |
| Outcome | Validated object in 'structured response' |
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.
Source-complete notesfrequent use cases
pythonif messages_too_long:
switch to stronger model
if tool failed:
return friendly ToolMessage
if user has no permission:
hide dangerous toolsLangChain/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.
Source-complete notesmajor regimes
textupdates -> State deltas by step
messages -> tokens / message chunks + metadata
Traditional Signals of Progress
debug -> maximum detailed flow (LangGraph)Persistence, Threads and Human-In-The-Loop
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.
Source-complete notesMinimal logic
textgraph = 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 stateThread 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.
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.
Source-complete notesgranularly
textapproved = 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.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.
Source-complete notesresponsibility
responsibility
- state/messages → current conversation
- store/preferences → long-lived data
- Do not try to solve both problems by one entity.
How they're usually combined
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.
Source-complete notessignal
signal
- tool-calling assistant
- RAG + tools
- structured output agent
- We need a quick start.
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.
Source-complete notessignal
signal
- human-in-the-loop
- branching
- custom state machine
- long-lived
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.
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.
Source-complete notesevolution
sql1. 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 componentsGrabbles, Minimum Code and Output
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.
Source-complete notesred flags
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
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.
Source-complete notesLangChain · LangGraph
textagent = create_agent(
model="provider:model",
tools=[search_docs],
response_format=AnswerSchema,
)textbuilder = 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)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.
Source-complete notesformula
formula
- LangChain = components + high-level API
- LangGraph = orchestration runtime
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.
Source-complete notestakeaway
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.