LangChain is the right default for most LLM applications — chains, RAG, and simple agents — while LangGraph is the right choice when your control flow…
Trace the core pain point — reliably wiring models, tools, memory, and control flow in production — and see why a single framework split into two with different philosophies. The running scenario is a content-research agent that fetches URLs, summarizes pages, and routes results.
Traces the core pain point that split one framework into two — integration vs. control flow — and shows where each one fits.
Why this matters: Choosing the wrong framework for your project creates architectural debt that's hard to undo; this module gives you the diagnostic question to ask first.
You're building a content-research agent that fetches URLs, summarizes pages, and routes results — and it keeps breaking in production.
Before reading on, predict: is the problem that you lack the right model integrations, or that you can't control when each step runs and what happens when one fails?
That distinction is exactly why one framework became two. solves the integration problem: a unified layer over models, tools, prompts, and retrievers. solves the control-flow problem: an explicit graph of nodes and edges for stateful agents.
They share a lineage but answer different questions — and picking the wrong one creates pain.
Imagine your agent has three jobs: fetch a URL, summarize it, then route the result — store or flag for review.
Week one: wire the model to fetch and summarize tools. The works in a notebook. Week two: add a second model provider, retry logic, and URL memory. Glue code triples — this is the integration problem LangChain solves.
Week three: the agent must pause and wait for human approval before continuing. A linear chain can't express that pause. A plain gives no way to inspect or resume mid-run. This is the control-flow problem LangGraph solves.
# Naive attempt: a plain sequential chain steps = [fetch_url, summarize, route_result] def run_pipeline(url): result = url for step in steps: result = step(result) return result run_pipeline("https://example.com/article")
This is the obvious first attempt — a loop over three callables. Predict what happens when fetch_url returns an error or when route_result needs to branch on the summary content.
fetch_url failure raises an unhandled exception and kills the whole pipeline — there's no retry or partial-state recovery. route_result can't branch because the loop has no conditional logic; every URL follows the same path. You'd need to add try/except, if/else, and state tracking manually — and that ad-hoc wiring is exactly what both frameworks replace.
# LangChain-style: composable runnables with middleware pipeline = ( fetch_url_tool | summarize_runnable | route_runnable ) # Middleware: retry + tracing added without touching core logic pipeline_with_middleware = with_retry(with_tracing(pipeline)) result = pipeline_with_middleware.invoke({"url": "https://example.com/article"})
LangChain's composition (the | pipe) chains steps cleanly, and wraps the whole pipeline for retry and tracing without touching each step.
Predict: does this solve the human-approval pause between summarize and route?
No. The pipe executes each step immediately and sequentially — there's no mechanism to suspend execution, persist state, and resume after an external event. That's the ceiling of a linear chain, and it's exactly the gap LangGraph fills with its interrupt and checkpointer primitives.
# LangGraph-style: explicit graph with branching and pause graph = StateGraph(ResearchState) graph.add_node("fetch", fetch_url) graph.add_node("summarize", summarize) graph.add_node("review", human_approval_interrupt) # pauses here graph.add_node("route", route_result) graph.add_edge("fetch", "summarize") graph.add_conditional_edges("summarize", lambda s: "review" if s.needs_approval else "route") app = graph.compile(checkpointer=memory_checkpointer)
Each reads and writes a shared ; (including conditional ones) decide what runs next. The persists state so the graph can resume after the at the review node.
Notice what changed from Stage 2: the structure of the workflow is now explicit and inspectable — not implicit in the order of pipe calls.
Change the add_edge("fetch", "summarize") line to a conditional edge:
graph.add_conditional_edges("fetch",
lambda s: "log_error" if s.fetch_failed else "summarize")
Lines changed: one — the direct edge becomes conditional. This is the crux: LangGraph lets you express branching at any node by reading state, which a linear chain cannot do.
You add middleware callbacks to simulate branching. The pipeline grows to 400 lines of nested lambdas. When a step fails, you have no saved state — the whole pipeline restarts. Result: duplicate API calls, ballooning costs, and no audit trail.
You define a graph for a simple summarize-and-return task. You now maintain a state schema, checkpointer, and node wiring for a linear workflow. The overhead is real: more boilerplate, steeper onboarding, slower iteration.
LangGraph can use LangChain runnables inside nodes — so teams sometimes build a graph that is secretly just a linear chain. You get all the complexity with none of the benefits. If your graph has no conditional edges and no interrupts, you wanted a chain.
None instead of raising an error.| Option | Branching / cycles needed | Pause / resume / human-in-loop | Integration surface (models, tools, retrievers) | When to choose | Cost | Complexity |
|---|---|---|---|---|---|---|
| LangChain | Limited — conditional runnables exist but aren't first-class | Not natively supported; requires custom middleware | Extensive — hundreds of provider integrations built-in | Your workflow is linear or lightly conditional, you need broad model/tool integrations, and production concerns (retry, tracing, prompt management) are the main challenge. | Minimal overhead; no graph compilation step | Low–medium; pipe composition is quick to learn |
| LangGraph | First-class — conditional edges and cycles are the core primitive | Built-in via interrupt + checkpointer; resumes from exact pause point | Uses LangChain integrations inside nodes; no native equivalents | Your agent needs cycles, conditional routing, human approval steps, or must recover from mid-run failures without restarting. | Higher setup cost; graph compilation and state persistence add overhead | Medium–high; requires defining state schema, nodes, edges, and a checkpointer |
You now know why the split exists. Before you choose for your project, you need to see what LangChain gives you at the code level.
The next module walks through LangChain's core abstractions — model interfaces, chains, tool definitions, and the create_agent harness — using a worked example that wires a search tool to a model.
Walk through LangChain's core abstractions — model interfaces, chains, tool definitions, and the create_agent harness — using a worked example that wires a search tool and a summarizer into the content-research agent. You'll see exactly what middleware buys you and where the loop lives.
LangChain's four core abstractions — model interface, chains, tool definitions, and the create_agent harness — and how they compose into a production agent.
Why this matters: Knowing what the harness handles for you (middleware, loop, tool dispatch) tells you exactly when it's enough for your SEO/GEO page research agent and when you'll need LangGraph's explicit control.
Decision this forces: Is a ready agent harness with middleware enough, or do I need to design the topology myself?
. By the end you'll know exactly what its harness buys you — and where it stops being enough.
LangChain builds on four abstractions that compose into an agent.
layer wraps the loop. It can retry failed calls, summarize long histories, filter tools, and emit trace data — all without touching the core loop logic.
Your content-research agent needs to fetch a URL and summarize it. Without a framework you'd write the loop, the retry logic, the tool dispatch, and the history trimming yourself.
create_agent harness, you declare a model, two tools (a search tool and a summarizer), and a prompt template. The harness owns the loop.
search_tool with a query.summarize_tool on the fetched content.The two production concerns the middleware handles here are observability (tracing) and context-window management (history trimming) — both wired in without custom code.
from langchain.tools import tool from langchain.chat_models import init_chat_model @tool def search_tool(query: str) -> str: """Fetch the top search result for a query.""" return web_search(query) # your search implementation @tool def summarize_tool(text: str) -> str: """Return a 3-sentence summary of the given text.""" return call_summarizer(text) # your summarizer implementation model = init_chat_model("gpt-4o", temperature=0)
@tool decorator turns a plain Python function into a typed tool schema the model can request. init_chat_model wraps any provider behind a uniform interface — swap "gpt-4o" for any supported model ID without changing downstream code.
The harness appends the return value as a tool observation message in the conversation history, then passes the updated history back to the model for its next reasoning step. The model never calls the function directly — the harness dispatches and collects.
from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "You are a content-research assistant."), ("placeholder", "{agent_scratchpad}"), ]) agent = create_tool_calling_agent(model, [search_tool, summarize_tool], prompt) executor = AgentExecutor(agent=agent, tools=[search_tool, summarize_tool], max_iterations=6, verbose=True) result = executor.invoke({"input": "Summarize the LangChain docs homepage"})
create_tool_calling_agent. AgentExecutor: it calls the agent, dispatches tool requests, appends observations, and repeats until the model stops requesting tools or max_iterations is hit.
On the first iteration verbose=True prints the model's reasoning text and the tool call it chose (name + input). If the model hasn't stopped by iteration 6, AgentExecutor raises an AgentFinish error with the message 'Agent stopped due to iteration limit or time limit.' — it does NOT silently return a partial answer by default.
from langchain.callbacks import LangSmithCallbackHandler from langchain.memory import ConversationTokenBufferMemory memory = ConversationTokenBufferMemory( llm=model, max_token_limit=2000, return_messages=True ) executor = AgentExecutor( agent=agent, tools=[search_tool, summarize_tool], memory=memory, callbacks=[LangSmithCallbackHandler()], max_iterations=6, # TODO: add the kwarg that makes the executor return # intermediate tool calls and observations in the output dict )
This stage adds the two middleware concerns from the scenario: a token-budget memory and a tracing callback. One keyword argument is missing — it's the crux of this stage.
Stop — attempt the TODO before revealing. Hint 1: the kwarg is a boolean. Hint 2: its name describes what it returns alongside the final answer.
The missing kwarg is return_intermediate_steps=True. With it set, executor.invoke(...) returns {"output": "<final answer>", "intermediate_steps": [(AgentAction(tool='search_tool', tool_input='...'), '<observation>'), ...]}. Changed lines vs Stage 2: added memory=memory, callbacks=[LangSmithCallbackHandler()], and return_intermediate_steps=True. The first two add middleware; the third exposes the loop's internals for debugging.
Three failure patterns show up most often when running a LangChain agent in production.
AgentError: Agent stopped due to iteration limit after hitting max_iterations. Fix: lower max_iterations, add a handle_parsing_errors=True guard, or tighten the tool's docstring so the model knows when to stop.ConversationTokenBufferMemory, a long research run silently truncates early observations — the model answers as if it never saw them. No error is raised; the answer is just wrong. Fix: always set a max_token_limit so trimming is explicit.return_intermediate_steps=True and a callback, you see only the final error with no trace of which tool call caused it. Fix: enable both before going to production.max_iterations is set (not left at the default 15), that a memory class with a token limit is wired in, that every @tool docstring is specific enough to guide the model, and that at least one callback is attached for tracing. Generated code often omits all four.Build the same content-research agent as a LangGraph graph — defining a state schema, adding nodes, connecting conditional edges, and attaching a checkpointer — so you can feel the difference in control and verbosity firsthand. A completion example leaves the conditional routing edge for you to wire.
Build the content-research agent as a LangGraph graph — defining state, wiring conditional edges, and attaching a checkpointer — to understand explicit state control firsthand.
Why this matters: Knowing when and how to use LangGraph's graph model lets you add checkpointing, cycles, and human-in-the-loop pauses to your SEO/GEO page agent without losing control of the workflow.
Decision this forces: Does my workflow need explicit state transitions, checkpointing, or human-in-the-loop interrupts?
create_agent wire together? Write them down, then check below.Answer: create_agent
That harness is fast to set up, but you hand control to the framework. You can't pause mid-run, inspect state between steps, or route differently based on what a node produced.
graph. You'll feel exactly where control comes back to you.
decide which node runs next.
A conditional edge is a function that inspects state and returns the next node's name. This is how branching and cycles work.
to persist state between invocations.
The tradeoff is verbosity: you write the topology yourself instead of letting a harness infer it.
Imagine rebuilding the content-research agent as a LangGraph graph with three nodes: fetch_url, summarize, and quality_check.
fetch_url downloads the page and writes raw HTML to state.summarize calls the LLM and writes a summary to state.quality_check scores the summary. If the score is too low, it routes back to summarize. Otherwise it routes to END.The conditional edge after quality_check makes this a graph, not a chain. The route is decided at runtime by inspecting state.
Attach a checkpointer and every invocation with the same thread_id resumes from the last saved state. A crash or pause doesn't lose work.
from typing import TypedDict class ResearchState(TypedDict): url: str html: str summary: str quality_score: float def fetch_url(state: ResearchState) -> dict: state["html"] = download(state["url"]) # your HTTP call return state def summarize(state: ResearchState) -> dict: state["summary"] = llm.invoke(state["html"]) return state
TypedDict — every node reads from and writes to this same dict, so the shape of shared data is explicit and checkable.
Each node is a plain function: it receives the current state and returns an updated dict. No framework magic, no hidden context.
LangGraph passes the current state dict (typed as ResearchState here). The function must return a dict — typically the updated state. LangGraph merges the returned keys back into the shared state before calling the next node.
from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver def route_after_check(state: ResearchState) -> str: return "summarize" if state["quality_score"] < 0.7 else END builder = StateGraph(ResearchState) builder.add_node("fetch_url", fetch_url) builder.add_node("summarize", summarize) builder.add_node("quality_check", quality_check) builder.add_edge(START, "fetch_url") builder.add_edge("fetch_url", "summarize") builder.add_edge("summarize", "quality_check") builder.add_conditional_edges("quality_check", route_after_check)
add_conditional_edges wires the routing function to the quality_check node: at runtime, LangGraph calls route_after_check with the current state and follows whichever node name it returns.
MemorySaver checkpointer means every node's output is snapshotted; passing thread_id in the config lets you resume from the last snapshot on the next invocation.
LangGraph routes execution back to the summarize node, which re-runs with the current state. This creates a cycle — the graph loops until quality_score reaches 0.7 or above, at which point route_after_check returns END and the graph terminates.
from langgraph.types import interrupt def human_review(state: ResearchState) -> dict: # TODO: call interrupt() here so the graph pauses # and surfaces state["summary"] to the caller for approval pass builder.add_node("human_review", human_review) builder.add_edge("quality_check", "human_review") # changed: route here first # TODO: add a conditional edge from "human_review" that # routes to END if approved, or back to "summarize" if not graph = builder.compile(checkpointer=MemorySaver())
Stop — attempt this before revealing. Your task: fill in the two TODOs to add a human-approval pause before the graph ends.
Hint 1: interrupt() takes a JSON-serializable payload to surface to the caller — pass the summary string. Hint 2: the conditional edge function should check a key you add to state (e.g. state["approved"]) that the caller sets when resuming.
# CHANGED LINE 1 — inside human_review:
interrupt({"summary": state["summary"]}) # pauses graph, surfaces payload
return state # resumes here when graph is re-invoked
# CHANGED LINE 2 — the new conditional edge:
def route_after_review(state: ResearchState) -> str:
return END if state.get("approved") else "summarize"
builder.add_conditional_edges("human_review", route_after_review)
# Why these lines: interrupt() uses the checkpointer's persistence layer to
# freeze state at this node. On resume, LangGraph re-enters human_review
# after the interrupt() call, so the caller's updated state["approved"] is
# available to route_after_review.
If your routing function returns a nonexistent node name, LangGraph raises KeyError at runtime. If it returns a valid node that skips a safeguard, execution continues silently. Fix: unit-test every routing function with state fixtures covering each branch.
, LangGraph re-enters the node from the top. Any side effect before interrupt() (DB write, email send) fires twice. Move side effects to after the interrupt, or guard them with an idempotency key.
Invoking the graph without thread_id in config means the checkpointer has no cursor to resume from. The next call starts fresh. Always pass {"configurable": {"thread_id": "<id>"}} on every invocation that needs continuity.
Compare LangChain and LangGraph across five criteria — state model, control flow, persistence, streaming, and setup cost — using the content-research agent as the shared reference point, and surface the failure modes each framework hides from you by default.
A side-by-side breakdown of LangChain and LangGraph across five architectural criteria — state model, control flow, persistence, streaming, and setup cost — using the content-research agent as the shared reference.
Why this matters: Knowing exactly where the two frameworks diverge tells you which one adds real value for your SEO/GEO page agent versus which one just adds boilerplate.
Decision this forces: Is the added graph complexity worth the control it buys for my specific workflow?
Answer: the . Every node reads from and writes to it. This schema is the architectural fork between LangChain and LangGraph.
Module 3 showed you how to wire the graph. This module asks: given that wiring cost, what do you gain — and what new failure modes do you inherit?
| Option | State model | Control flow | Persistence & streaming | When to choose | Cost | Complexity |
|---|---|---|---|---|---|---|
| LangChain | Implicit: the harness manages a message list internally; you don't declare a schema | Fixed loop — model calls tools until a stop condition; branching is not first-class | Callback-based streaming; no built-in thread-scoped checkpointing | You need a working agent fast, the loop topology is simple, and you don't need step-level resume or human-in-the-loop approval. | Minimal boilerplate; hidden cost is opacity when the loop misbehaves | Low — one harness call wires model + tools + middleware |
| LangGraph | Explicit typed schema; every node declares what it reads and writes | Conditional edges route execution; cycles and interrupts are first-class | Checkpointer attaches thread_id; interrupts pause and resume mid-graph | You need cycles with branching, step-level resume after failure, human approval gates, or fine-grained streaming of intermediate node outputs. | More upfront code; pays off at scale or when reliability matters | High — schema + nodes + edges + compile step before first run |
"Agent stopped due to iteration limit or time limit.""This model's maximum context length is 128000 tokens" — but no hook to inspect or trim the list first.None instead.graph.compile() has no effect — the compiled graph is frozen. Your new node never executes and no exception is raised.The content-research agent fetches a URL, summarizes the page, and decides whether to fetch another link or stop.
In , you call create_agent(model, tools). The harness owns the loop. If the fifth URL times out, you get one error — you can't tell which fetch failed or replay from step four.
In , each fetch and summarize is a named . A snapshots state after every node. If step four fails, you resume from step four — not from scratch.
The tradeoff is real: LangGraph needs a schema, node functions, edge wiring, and a compile call. For a one-shot summarizer that never needs replay, that overhead buys nothing.
# LangChain: state is implicit — the harness owns the message list agent = create_agent(model, tools=[fetch_url, summarize]) result = agent.invoke({"input": "research https://example.com"}) # You get result["output"]; intermediate steps are opaque # LangGraph: state is explicit — you own the schema class ResearchState(TypedDict): url: str summary: str should_continue: bool
These two snippets show the same agent's state model side by side. LangChain's harness hides the message list; LangGraph forces you to name every field the graph will touch.
It receives None — LangGraph silently drops keys that aren't declared in the state schema. No exception is raised. This is the schema-drift failure mode: the fix is to add fetched_html: str to ResearchState before running.
# Running example variation: the agent now checks a `depth` counter # and loops back to fetch_node if depth < 3, else routes to END. class ResearchState(TypedDict): url: str; summary: str; depth: int def route(state: ResearchState) -> str: # TODO: return "fetch_node" if depth < 3, else return END pass graph.add_conditional_edges("summarize_node", route)
Stop — attempt the TODO before revealing. The route function is the crux: it's the that decides whether the graph loops or terminates.
return "fetch_node" if state['depth'] < 3 else END
--- What changed and why ---
pass is replaced with a conditional return — this is the ONLY changed line.AI-generated LangGraph code passes syntax checks but fails silently in specific ways. Before trusting it, verify these four things:
graph.compile()) after all nodes and edges are added — not before.END sentinel — a typo routes silently to nowhere.You've seen both frameworks break and shine on the same agent. The question isn't which is better — it's which fits your workflow's actual requirements.
The next module, "Head-to-Head: Picking Your Framework," gives you a structured comparison table across all five criteria and a one-line decision rule for your own project.
Use a structured comparison table (rows = LangChain / LangGraph, columns = five key criteria) and a one-line decision rule to route your project to the right framework — then draft the SEO page's answer capsule, comparison table section, and FAQ block as the solo capstone.
A structured head-to-head comparison of LangChain and LangGraph across five criteria, ending in a one-line decision rule and the SEO page sections needed for the capstone.
Why this matters: Gives you the decision framework and the exact page components — answer capsule, comparison table, FAQ block — to ship a complete, schema-ready SEO page on 'langchain vs langgraph'.
Decision this forces: LangChain or LangGraph — which framework ships my specific project faster and more reliably?
The five criteria from Module 4 were: state model, control flow, persistence, streaming, and setup cost. Those five criteria are the columns in the comparison table you're about to build.
This module turns that architectural knowledge into a decision rule you can apply in under a minute — and into the SEO page sections your capstone requires.
The answer capsule is the 40–60 word block at the top of your SEO page — the sentence an LLM or featured snippet lifts verbatim, so it must name both frameworks and state the decision rule explicitly.
Here is a worked example for the query "langchain vs langgraph":
Notice the pattern: entity named in sentence one, decision rule in sentence two, relationship in sentence three. That structure makes the capsule quotable and self-contained for a cold reader.
faq_items = [
{"question": "Can I use LangGraph without LangChain?",
"answer": "LangGraph builds on LangChain's model and tool abstractions, "
"so LangChain is a dependency, not optional."},
{"question": "Does LangChain support human-in-the-loop approval?",
"answer": # TODO: write a 1-2 sentence direct answer.
# Hint 1: does LangChain have a native interrupt primitive?
# Hint 2: which framework's checkpointer enables pause-and-resume?
},
{"question": "When should I migrate from LangChain to LangGraph?",
"answer": "Migrate when your agent needs conditional cycles, durable state "
"across sessions, or mid-run human approval."},
]This fragment is the FAQ block for your SEO page — each item maps directly to a FAQPage schema entry.
Stop — attempt the TODO before revealing. The answer for item 2 must be a direct 1–2 sentence response that names the right framework and states what it provides.
CHANGED LINE — the TODO answer:
"answer": "LangChain has no native interrupt primitive, so human-in-the-loop approval requires custom middleware. LangGraph's built-in checkpointer and interrupt let a node pause, surface a request, and resume when the graph is invoked again with a resume command."
Why: LangChain's agent loop is implicit — there is no first-class pause mechanism. LangGraph's interrupt is a first-class primitive backed by the checkpointer's thread_id cursor.
| Option | State model | Control flow | Persistence | When to choose | Cost | Complexity |
|---|---|---|---|---|---|---|
| LangChain | Implicit — lives inside the agent loop's message history | Linear or shallow branching via middleware; cycles are opaque | No built-in checkpointer; you add your own storage | Your workflow is mostly linear — fetch, transform, summarize — and you want a working agent in an afternoon without designing a graph topology. | Low — one pip install, minimal boilerplate | Low — create_agent wires model + tools in ~10 lines |
| LangGraph | Explicit TypedDict schema; every node reads and writes shared state | Conditional edges, cycles, and interrupts are first-class primitives | Built-in checkpointer; thread_id resumes any prior run | Your workflow needs cycles, conditional branching, human-in-the-loop approval, or the ability to pause and resume mid-run. | Low marginal cost if LangChain is already installed — LangGraph builds on it | Medium — define a state schema, add nodes, wire edges, compile |
Three failure patterns show up repeatedly when teams pick the wrong framework.
When you use an LLM to draft the comparison table, answer capsule, or FAQ block, check these four things before publishing.
The integration point is straightforward: LangGraph consumes LangChain's model wrappers and tool definitions directly as node callables.
You keep every LangChain tool and model binding you already have. You wrap the logic that needs cycles or persistence inside a LangGraph , connect it with , attach a , and compile. The rest of your LangChain code is untouched.
That additive path is the architectural rung this lesson has been building toward.
Before reading the summary: from memory, name the one criterion that should push you from LangChain to LangGraph, sketch the five comparison dimensions, and state the decision rule in one sentence. Then check your answer against the table in Module 5.
Apply what you learned to SEO/GEO page — the page title is the H1 and the exact search query. OPEN with a 40-60 word self-contained, quotable answer capsule that names the entity explicitly in sentence one (no "in this lesson…" preamble) — this is the sentence an LLM lifts. Name the entity by its full name every time (not pronoun-only). Structure each section as ONE sub-question (How it works · X vs Y · When to use it · Example · Common mistakes), each opening with its own 1-2 sentence direct answer then depth. End with a genuine 3-5 question FAQ phrased as real user questions ending in "?" (these become FAQPage schema + the knowledge check). Prefer quotable specifics — concrete numbers, defaults, version names, one short snippet — over vague prose. Self-contained for a reader who arrived cold from a search engine or an LLM. Intent = comparison: include a comparison TABLE (rows=options, columns=criteria) and a one-line decision rule. Target query: "langchain vs langgraph"..
Which statement best describes the distinct problem each framework solves?
LangChain solves ___; LangGraph solves ___.
LangChain's core value is its integration and middleware layer — it gives you a ready agent harness so you don't wire every piece yourself. LangGraph's core value is explicit graph orchestration: you define nodes, edges, and a typed state schema, which unlocks checkpointing and interrupt/resume. Model fine-tuning, vector storage, and streaming are either separate concerns or incidental features, not the defining problem either framework was built to solve.
A teammate shows you this LangChain snippet:
agent = create_agent(llm, tools=[search, calculator])
result = agent.invoke({"input": "What is 42 * the population of France?"})
Which two production concerns does LangChain's middleware layer handle automatically here that you did NOT have to code yourself?
LangChain's middleware layer handles production concerns like retry logic on transient failures and callback hooks for token usage, latency, and logging — you get these without writing them. Explicit state schema validation, graph edge routing, thread-level checkpointing, and interrupt/resume are all LangGraph concepts that LangChain's agent harness does not expose; they require you to build a graph explicitly.
You are building a multi-step document review workflow where a human must approve the AI's draft before it proceeds, and the job must be resumable if the server restarts mid-run. Which framework should you reach for first, and why?
Human-in-the-loop approval and server-restart resumption are exactly the two capabilities LangGraph was designed for: interrupt nodes pause execution at a defined point, and a checkpointer (e.g., SqliteSaver) persists the full state keyed by thread_id so the run can resume exactly where it stopped. LangChain's create_agent loop has no native interrupt mechanism and no persistent state checkpointing. LangGraph is not universally faster to ship — for simple pipelines LangChain ships faster; the tradeoff is control vs. convenience.
On which criterion does LangChain's agent harness introduce a failure mode that LangGraph's explicit graph avoids?
LangChain's agent harness runs the tool-call loop internally; the intermediate state is implicit and not directly inspectable, so when the model takes an unexpected branch or loops unexpectedly you cannot easily pause, inspect, or redirect it. That is the specific failure mode LangGraph's explicit state schema and edge definitions solve. Defining every edge and requiring a TypedDict schema are LangGraph's own verbosity costs, not LangChain's. LangChain does have a callback system for token costs.
State the one-line decision rule for choosing between LangChain and LangGraph. Your answer should name both frameworks and the condition that triggers each choice.
The decision rule captures the core architectural fork: LangChain's value is convenience and speed for standard agent patterns; LangGraph's value is control over topology, state, and execution lifecycle. Any answer that correctly names both frameworks and ties LangChain to the 'ready harness / simple pipeline' case and LangGraph to the 'explicit state / checkpointing / human-in-the-loop' case is acceptable. Answers that describe only one framework or omit the triggering condition are incomplete.