LangChain Agent Loop — Legacy ReAct & Modern LangGraph
LangChain Agent Loop — Legacy ReAct & Modern LangGraph
Source: two code snapshots from commit 50febb79 of langchain-ai/langchain
- Legacy —
libs/langchain/langchain_classic/agents/react/base.pyL31–36 - Modern —
libs/langchain_v1/langchain/agents/factory.pyL1771
Legacy: ReAct Prompt Templates (wiki_prompt.py, textworld_prompt.py)
Both ReAct prompts share the same two input variables:
| Variable | Role |
|---|---|
{input} |
Task/question for this run — filled once at invocation |
{agent_scratchpad} |
Accumulated Thought:/Action:/Observation: text — grows each iteration |
SUFFIX = """\nQuestion: {input}
{agent_scratchpad}"""
WIKI_PROMPT = PromptTemplate.from_examples(EXAMPLES, SUFFIX, ["input", "agent_scratchpad"])
PromptTemplate.from_examples() concatenates static few-shot demonstrations + the dynamic SUFFIX. The scratchpad starts empty; the framework appends each round to it.
Action format — text the LLM writes, parser reads
The LLM outputs plain text; ReActOutputParser extracts it:
| Signal | Text form | Who produces it |
|---|---|---|
| Tool call | Action: ToolName[argument] |
LLM |
| Exit | Action: Finish[answer] |
LLM |
| Generation stop | stop token "\nObservation:" |
Framework (injected) |
| LLM turn prefix | "Thought:" |
Framework (llm_prefix) |
| Result injection | "Observation: <result>" |
Framework (appended to scratchpad) |
Tool names appear directly in text: Search[query], Lookup[term] (wiki); Play[command] (TextWorld). The entire control protocol is a text convention shared between the prompt template and the output parser.
Contrast with modern tool-call loop
In LangGraph there are no {input}/{agent_scratchpad} template slots and no text parsing. The LLM emits a structured tool_calls array in its JSON response; the framework reads AIMessage.tool_calls directly and routes via typed conditional edges. Control moved from text contract (LLM writes Action: X, parser reads it) to structured JSON (LLM emits object, framework routes natively).
Legacy: ReActDocstoreAgent (base.py L31–36)
Lines 31–36 are the deprecation decorator + class declaration:
@deprecated(
"0.1.0",
message=AGENT_DEPRECATION_WARNING,
removal="1.0",
)
class ReActDocstoreAgent(Agent):
What it implements
The ReAct paper (Reasoning + Acting) applied to a document store.
Class hierarchy: ReActDocstoreAgent → Agent → AgentExecutor (the outermost ReActChain is a thin AgentExecutor subclass that wires things up).
The loop (lives in AgentExecutor):
- Build prompt with accumulated
Thought/Action/Observationhistory - Call LLM with stop sequence
["\nObservation:"]; LLM prefix is"Thought:" - Parse output via
ReActOutputParser→ extractsaction+action_input - Execute tool; append
"Observation: <result>"to context - Repeat until LLM outputs
"Final Answer: <answer>"
Tool constraints: Exactly {"Lookup", "Search"} — validated at construction time. DocstoreExplorer bridges them to a Docstore, maintaining cursor state across lookup pagination.
Variants:
ReActTextWorldAgent— same pattern, requires exactly{"Play"}toolReActChain— convenienceAgentExecutorsubclass that auto-wiresDocstoreExplorer + Search + Lookup
All classes deprecated at 0.1.0, removed at 1.0.
Modern: create_agent / _make_tools_to_model_edge (factory.py L1771)
factory.py builds a LangGraph StateGraph and compiles it into a CompiledStateGraph. L1771 is the start of _make_tools_to_model_edge, one of four routing functions that control the loop.
Graph topology (minimal, no middleware)
START → model ──(tool calls?)──► tools ──► model (loop)
└──(no tool calls)──────────────────► END
Graph topology (full, with middleware)
START
→ before_agent* (once, runs before loop)
→ before_model* (once per iteration)
→ model
→ after_model* (once per iteration)
→ tools (parallel fan-out via Send)
→ [loop back to before_model / model]
→ after_agent* (once, at end)
→ END
Four routing functions
_make_model_to_tools_edge (post-model / post-after_model):
| Priority | Condition | Destination |
|---|---|---|
| 1 | state["jump_to"] set |
_resolve_jump() |
| 2 | No AIMessage in messages |
END |
| 3 | AIMessage.tool_calls == [] |
END (classic exit) |
| 4 | Pending tool calls exist | Send("tools", ...) per call (parallel) |
| 5 | structured_response in state |
END |
| 6 | All calls already have ToolMessages (artificial injection) | model |
_make_tools_to_model_edge (post-tools, L1771):
| Priority | Condition | Destination |
|---|---|---|
| 1 | state["jump_to"] set |
_resolve_jump() |
| 2 | No AIMessage |
model |
| 3 | All client-side tools have return_direct=True |
END |
| 4 | A structured output tool was executed | END |
| 5 | Default | model (continue loop) |
_make_model_to_model_edge (structured output retry loop):
| Priority | Condition | Destination |
|---|---|---|
| 1 | state["jump_to"] set |
_resolve_jump() |
| 2 | structured_response in state |
END |
| 3 | Default | model (retry) |
_add_middleware_edge: wraps any middleware node — either a fixed edge to default_destination or a conditional edge that resolves jump_to before falling back to default_destination.
Key state fields
| Field | Purpose |
|---|---|
messages |
Accumulates all AnyMessage (AI, Tool, System) |
jump_to |
"model" | "end" | "tools" — middleware directive to skip ahead |
structured_response |
Set when a structured output tool completes |
Middleware lifecycle hooks
Each AgentMiddleware may implement up to four hook nodes:
| Hook | Runs | Frequency |
|---|---|---|
before_agent |
Before loop starts | Once |
before_model |
Before each model call | Per iteration |
after_model |
After each model call | Per iteration |
after_agent |
After loop exits | Once |
Hooks can short-circuit by setting jump_to in state.
Compilation options
graph.compile() accepts: checkpointer, store, interrupt_before, interrupt_after, debug, name, cache — enabling durable state, HITL interruption, and tracing.
Key differences: Legacy vs Modern
| Aspect | Legacy ReAct | Modern LangGraph |
|---|---|---|
| Pattern | Class inheritance (Agent subclass) |
Graph compilation (StateGraph) |
| Loop representation | Implicit (AgentExecutor.run while-loop) |
Explicit DAG with conditional edges |
| Exit trigger | LLM outputs "Final Answer:" |
No tool calls in AIMessage; or return_direct; or jump_to="end" |
| Parallel tools | No | Yes (fan-out via Send) |
| Tool constraints | Hard-coded | Any BaseTool |
| Middleware/hooks | None | 4-phase lifecycle (before/after agent/model) |
| HITL | None | interrupt_before / interrupt_after |
| State | Implicit (prompt string) | Typed AgentState dict |
| Structured output | No | OutputToolBinding with retry loop |
| Status | Deprecated 0.1.0 / removed 1.0 | Current (langchain_v1) |