« Phase 29 README · Hitchhiker's · Deep Dive · Principal Deep Dive · Core Contributor · Staff Notes
Phase 29 Warmup — LangChain Core, In Depth
Who this is for: you can write Python, you've done Phases 00–06 (the agent loop, tool calling, retrieval), and ideally Phase 18 (LangGraph) — because this phase and that one are two halves of a single interview answer. You may have used LangChain; this makes you understand it well enough to reimplement its composition engine, migrate its legacy agents, and defend a "LangChain or LangGraph?" call at the principal level. Everything here is grounded in the miniatures you build in the labs — no framework install needed to follow.
Table of Contents
- What LangChain is, and the ecosystem map
- The evolution: from LLMChain to LCEL
- The Runnable protocol: one interface for everything
- LCEL composition: sequence, parallel, passthrough, branch
- Config, fallbacks, retries, and binding
- Streaming through a chain
- Prompt templates: string, chat, few-shot
- Output parsers and structured output
- Documents, loaders, splitters, embeddings, vector stores
- Retrievers and the canonical RAG chain
- Tools and tool-calling
- Memory and message history
- The classic AgentExecutor — and why LangGraph superseded it
- LangSmith and LangServe
- LangChain vs LangGraph: the decision framework
- Common misconceptions
- Lab walkthrough & success criteria
- Interview Q&A
- References
1. What LangChain is, and the ecosystem map
LangChain is a framework for composing LLM-powered applications out of reusable components: prompt templates, chat models, output parsers, retrievers, tools, and the glue that wires them together. It appeared in late 2022 — months before most teams had shipped a single LLM feature — and became the default way to write "prompt in, structured answer out" applications. Today the name refers to a small family of packages, and knowing the map is table stakes:
langchain-core— the foundation: theRunnableinterface, prompt templates, the message types (HumanMessage,AIMessage,ToolMessage, …), output parsers, theDocumentandBaseRetrieverabstractions, and the tool interface. Deliberately dependency-light. Everything else builds on it — including LangGraph.langchain— the chains, retriever helpers (create_retrieval_chain), and the classic agents (AgentExecutor,create_react_agentin its legacy form) that most production 0.x code uses.- Integration packages —
langchain-openai,langchain-anthropic,langchain-chroma, and dozens more: one package per provider, versioned independently so a vendor SDK change doesn't force a core upgrade. (Older code imported everything from a monolithiclangchain_community; the split packages are the maintained path.) - LangGraph — the stateful agent runtime (Phase 18),
a separate library on top of
langchain-core. Not "LangChain 2.0" — a different execution model for a different problem (cycles + state + persistence). - LangSmith — the commercial observability/eval platform: traces every Runnable invocation, hosts datasets and LLM-as-judge evaluations (§14).
- LangServe — deploy any Runnable as a REST API with streaming and a playground (§14).
The mental model to carry: langchain-core defines the component contract; everything else is
either components (integrations), composition (LCEL), an agent runtime (LangGraph), or ops
(LangSmith/LangServe). When a JD says "LangChain," it means: can you build a real application —
usually RAG plus some tool use — out of these parts, and do you know where each part lives.
2. The evolution: from LLMChain to LCEL
You will inherit code from every era, so know the story. Era one (2022–mid-2023) was
class-per-pattern: LLMChain (prompt + model + parser in one object), SequentialChain (run
chains in order), RetrievalQA (RAG in a box), ConversationChain (chat + memory). It worked,
but every pattern was a bespoke class with its own constructor, its own callbacks, its own
streaming story (usually: none), and customizing one meant subclassing framework internals.
Era two (mid-2023 onward) is LCEL — the LangChain Expression Language. The insight: stop
shipping a class per pattern and instead make every component implement one interface (the
Runnable, §3), then let users compose them with the | operator:
# era one # era two (LCEL)
chain = LLMChain(llm=llm, chain = prompt | llm | StrOutputParser()
prompt=prompt)
chain.run({"topic": "bears"}) chain.invoke({"topic": "bears"})
Why the rewrite won, mechanically: because composition is generic, everything you build gets
streaming, batching, async, fallbacks, retries, and tracing for free — the combinators are
written once against the Runnable interface instead of re-implemented per chain class. An
LLMChain had to implement its own streaming; prompt | llm | parser streams because
RunnableSequence.stream knows how to stream any last step (§6). LLMChain,
SequentialChain, and RetrievalQA are formally deprecated with documented LCEL migrations;
reading legacy code and mentally rewriting it as a pipe expression is a real maintenance skill
interviews probe.
The second half of the evolution matters just as much: LCEL deliberately did not try to be an agent runtime. When LCEL chains grew conditional edges and loop hacks, the LangChain team's answer was not "more LCEL" — it was LangGraph, a separate runtime for cyclic, stateful control flow (§15). LCEL settled into what it is best at: the composition layer.
3. The Runnable protocol: one interface for everything
The Runnable is the whole trick, and Lab 01 makes you build it. A Runnable is anything that
implements:
invoke(input, config=None)— one input → one output, synchronously.batch(inputs, config=None)— many inputs → many outputs, in order (the real one runs a threadpool; the contract is order-preserving results).stream(input, config=None)— yield the output in chunks as it is produced.ainvoke/abatch/astream— the async twins (same semantics on an event loop), plusastream_eventsfor fine-grained intermediate events.
Prompt templates, chat models, output parsers, retrievers, tools, vector-store retrievers, and compiled LangGraph graphs are all Runnables. Two properties make this an algebra rather than a convention:
Closure. Composing Runnables yields a Runnable: prompt | model is a RunnableSequence,
which itself has invoke/batch/stream. So pipelines nest, and any tool built for "a Runnable"
(LangServe, LangSmith tracing, the fallback wrapper) works on a whole chain as easily as one
component.
The stream/invoke invariant. For any Runnable, folding the chunks of stream with +
reproduces invoke's result. Chunks are designed to be additive — string chunks concatenate,
and AIMessageChunk objects implement __add__ so streamed message deltas accumulate into the
full message. Lab 01 proves this invariant in both directions: a default Runnable streams as a
single chunk equal to invoke; a RunnableGenerator implements stream natively and derives
invoke by folding.
Coercion is the ergonomic detail that makes LCEL read cleanly: inside a pipe, a plain Python
function is coerced to a RunnableLambda, and a plain dict is coerced to a RunnableParallel.
That is why the canonical RAG chain can open with a bare dict literal (§10) and why
chain | my_formatting_function just works.
Production significance: the injected-model seam. Because a model is just a Runnable, you can
swap ChatOpenAI for a deterministic FakeListLLM in tests and the chain's wiring is verified
without a network call — the exact discipline every lab in this track uses (the model as an
injected pure function).
4. LCEL composition: sequence, parallel, passthrough, branch
Five primitives cover essentially all LCEL code you will read:
RunnableSequence — a | b | c. invoke threads each step's output into the next step's
input. That is all a "chain" is: left-to-right data flow. Nested pipes flatten into one step list.
RunnableParallel — {"x": r1, "y": r2} (a dict in a pipe): run every branch on the same
input and collect {"x": r1(input), "y": r2(input)}. This is fan-out, and it is how a RAG chain
fetches context and keeps the question (§10). The real one runs branches concurrently; the
result contract (a dict, insertion-ordered) is what your code depends on.
RunnablePassthrough — return the input unchanged. Sounds trivial; it is the connective
tissue that lets a value skip a stage of the pipeline. Its power form is
RunnablePassthrough.assign(key=runnable): take a dict flowing through the chain and add
a computed key to it while keeping everything already there. Each assigned mapper runs against the
original input dict. A RAG chain is essentially three assigns in a trench coat.
RunnableLambda — wrap any function as a Runnable (what coercion produces). Use it for
formatting, extraction, cheap logic between model calls.
RunnableBranch — RunnableBranch((cond, runnable), (cond2, runnable2), default): on
invoke, the first branch whose condition returns truthy runs; otherwise the default. This is
declarative routing — "billing questions to the billing chain, everything else to general" —
and its limit is instructive: a branch chooses a path once, forward. It cannot loop back. (The
other routing idiom: a RunnableLambda that returns a Runnable, which LCEL then invokes —
dynamic routing without enumerating branches.)
full_chain = RunnableBranch(
(lambda x: x["topic"] == "billing", billing_chain),
(lambda x: x["topic"] == "docs", rag_chain),
general_chain,
)
The shape to notice: sequence, parallel, and branch give you exactly a DAG — directed, acyclic. There is deliberately no "loop" primitive. The moment your sketch has a back-edge, you have left LCEL's design envelope and entered LangGraph's (§15).
5. Config, fallbacks, retries, and binding
Every Runnable method takes an optional config — a RunnableConfig dict carrying
run_name, tags, metadata (all for tracing, §14), callbacks, max_concurrency, and
crucially configurable (per-call parameters, e.g. {"configurable": {"session_id": "u42"}} for
message history, §12). Config propagates through composition: pass it to the outer chain's
invoke and every inner Runnable receives it. That propagation is what makes one LangSmith trace
cover a whole nested pipeline.
Three wrappers you should reach for before writing custom error handling:
.with_fallbacks([alt1, alt2])— try the primary; on exception, try each alternative in order; re-raise the last error if all fail. The classic use: primary model provider has an outage or rate-limits you → fall back to another model. Because the wrapper returns a Runnable, a whole chain can have a fallback chain..with_retry(...)— retry the wrapped Runnable on exception with exponential backoff (bounded attempts). Retry handles transient failure; fallbacks handle persistent failure — production chains often use retry inside fallbacks..bind(**kwargs)/.with_config(...)— pre-attach arguments or config to a Runnable:model.bind(stop=["\n"]),model.bind_tools(tools)(§11), orchain.with_config(run_name="rag-v2"). Binding is how a generic component gets specialized without subclassing.
Lab 01 builds with_fallbacks and with_retry (deterministic — no clock, no backoff) and proves
the semantics: primary success short-circuits, fallbacks fire in order, exhaustion re-raises.
6. Streaming through a chain
Users perceive time-to-first-token, not total latency (Phase 12/16 territory), so streaming is not
optional in product code — and LCEL's design answer is elegant: stream on a sequence runs
every step up to the last eagerly, then streams the final step. More precisely, real LCEL
streams through any step that can transform a stream incrementally (an output parser that
operates on partial input keeps the stream alive; a step that must see its whole input — like a
prompt template — materializes it). The practical consequence everyone hits: put the model and a
stream-capable parser last; anything after them that needs the full output kills streaming.
The chunk mechanics: a streamed chat model yields AIMessageChunks; chunks support + so the
consumer (or invoke itself) can fold them into the complete message — the invariant from §3.
astream_events goes further and emits structured events (on_chat_model_stream,
on_retriever_end, …) from inside the pipeline, which is what a UI showing "retrieving… →
thinking… → tokens" consumes. Lab 01's RunnableGenerator and the "sequence streams only the
last step" test make the whole mechanism concrete in thirty lines.
7. Prompt templates: string, chat, few-shot
A prompt template is a Runnable that turns a dict of variables into model input. Three forms:
PromptTemplate— a format string:PromptTemplate.from_template("Tell me about {topic}"). Output: a string. For completion-style models and quick pipelines.ChatPromptTemplate— the workhorse: a list of role-tagged messages, each a template:
prompt = ChatPromptTemplate.from_messages([
("system", "You answer only from the context.\n\nContext:\n{context}"),
MessagesPlaceholder("chat_history"), # a whole message list splices in here
("human", "{question}"),
])
MessagesPlaceholder is the important one: it splices a list of messages (chat history §12,
or an agent scratchpad §13) into the prompt at that position — the bridge between "prompt as
template" and "prompt as conversation."
FewShotPromptTemplate(and its chat variant) — render N worked examples before the real input, optionally choosing them dynamically with an example selector (e.g. embed the input and pick the k most similar examples — retrieval applied to prompting). Few-shot selection is the cheapest accuracy lever most teams never pull.
Two production details: templates validate their variables — a missing variable is an error at
invoke time, not a silently blank prompt (your Lab 02 PromptTemplate raises KeyError, the real
one raises at construction/invoke); and a template's output is a PromptValue that can render as
either a string or a message list, which is how the same template serves both model types.
8. Output parsers and structured output
A model emits text; your program needs values. Output parsers are Runnables that close that gap —
and because they are Runnables, they pipe: prompt | model | parser.
StrOutputParser— extractmessage.contentas a plain string. Ubiquitous, boring, correct.JsonOutputParser— parse JSON out of the reply; notably, it can parse streamed partial JSON, yielding progressively more complete objects as tokens arrive (this is the stream-transforming parser from §6).PydanticOutputParser— declare a Pydantic model; the parser contributes format instructions (parser.get_format_instructions()— a schema description you interpolate into the prompt) and then validates/parses the reply into the typed object. Prompt-and-pray with a validator at the end.
The modern replacement for most parser use: model.with_structured_output(Schema). Instead of
asking the model to emit JSON and parsing whatever comes back, it uses the provider's native
structured-output/tool-calling channel (Phase 02's mechanism) to constrain the output, and
returns validated objects directly. The seniority beat: parsers shape text after the fact;
structured output constrains generation itself — prefer the latter when the provider supports
it, keep a parser + repair loop when it doesn't. Lab 02's parse_with_sources is a parser in this
sense: the step that turns raw generation plus carried state into the typed
{question, answer, sources} your caller actually wants.
9. Documents, loaders, splitters, embeddings, vector stores
The retrieval half of LangChain is a set of narrow interfaces around Phase 05's mechanisms — if you built Phase 05's lab, every one of these is "I know how that works inside":
Document—page_content+metadata(+ optionalid). The unit everything below produces and consumes. (Lab 02'sDocument(id, text, metadata)is this, minimized.)- Document loaders —
PyPDFLoader,WebBaseLoader,CSVLoader, hundreds more;.load()yieldsDocuments. Boring by design: the value is the uniform output type. - Text splitters —
RecursiveCharacterTextSplitteris the default: split on paragraph, then sentence, then word boundaries, targetingchunk_sizewithchunk_overlapso facts straddling a boundary survive in one piece. Chunking policy drives retrieval precision (the Phase 05 lesson; Phase 24's Knowledge Bases lab hits the same tradeoff from the managed side). Embeddings—embed_documents(texts)/embed_query(text)→ vectors. Query and document embedding are separate methods because some models embed them asymmetrically.VectorStore—add_documents,similarity_search(query, k), and the adapter that matters most:.as_retriever(), which wraps the store in the retriever interface (§10). FAISS, Chroma, pgvector, Pinecone, OpenSearch — same interface, different operational envelopes.
The pipeline reads: load → split → embed → store, then retrieve at query time. LangChain's contribution is not the algorithms (you built those in Phase 05 — hashing embedder, BM25, RRF); it is the stable interfaces that let you swap any stage without touching the chain around it.
10. Retrievers and the canonical RAG chain
A retriever is any Runnable with the contract invoke(query: str) -> list[Document]. Vector
stores produce one via .as_retriever(search_kwargs={"k": 4}), but the interface is deliberately
broader — BM25 retrievers, MultiQueryRetriever (LLM-generated query variants),
EnsembleRetriever (fusion — Phase 05's RRF as a component), self-query retrievers (LLM-written
metadata filters), and Lab 02's keyword scorer are all just the contract. The chain depends on
the contract, not the scorer — that is why Lab 02 can use deterministic keyword overlap and
remain a faithful miniature.
The canonical LCEL RAG chain — the single most-pasted LangChain snippet in existence:
chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)
chain.invoke("How do I rotate my API key?")
Read it with §4 eyes: the dict coerces to a RunnableParallel receiving the raw query; the
context branch retrieves and formats documents into a string; the question branch is a
passthrough carrying the query itself; the merged dict fills the prompt; the model generates; the
parser extracts. Retrieve → format → prompt → generate → parse. Lab 02 builds exactly this,
with one production upgrade: citations. By carrying the retrieved docs through the chain
(assign instead of consuming them), the final parser can return
{"question", "answer", "sources": [doc ids]} — the "answer with sources" contract every
enterprise deployment requires (create_retrieval_chain returns input/context/answer the
same way). Lab 02's tests pin the properties that matter: the answer provably quotes the retrieved
doc (retrieval fed generation), changing the query changes the sources, the question survives the
parallel fan-out, and empty retrieval degrades to a grounded refusal — never a hallucinated
answer, never a crash.
One-shot RAG like this is LCEL's sweet spot. Agentic RAG — grade the docs, rewrite the query, loop until grounded — needs a cycle, which is a LangGraph graph with a retriever tool (§15).
11. Tools and tool-calling
A tool is a described capability: name + description + typed arguments + a function. In LangChain you usually write the function and decorate it:
from langchain_core.tools import tool
@tool
def get_order_status(order_id: str) -> str:
"""Look up the shipping status of an order by its ID."""
return db.status(order_id)
The decorator harvests the signature, type hints, and docstring into a schema. Everything matters to the model, not just the runtime: the model chooses tools by reading names and descriptions, and fills arguments from the schema — a tool's docstring is a prompt (Phase 02).
Tool-calling wires tools to a model: model.bind_tools([get_order_status, ...]) advertises
the schemas to the provider's native function-calling API; the model's reply then carries
AIMessage.tool_calls — a list of {name, args, id} — instead of (or alongside) text. You
execute the calls, wrap results as ToolMessages, append them to the conversation, and call the
model again. Notice what that is: a loop — decide, act, observe, repeat. LCEL cannot express
the loop; a single prompt | model_with_tools | parser chain gets you exactly one decision. The
loop needs a runtime, which is §13 (the classic executor) and its successor, Phase 18. Lab 03's
Tool dataclass and dispatch-by-name registry are this machinery, minimized to its testable core.
12. Memory and message history
LLMs are stateless; "memory" is the application re-supplying the past. LangChain's classic answer
was memory objects — ConversationBufferMemory (keep everything),
ConversationBufferWindowMemory (keep the last k turns), ConversationSummaryMemory (LLM-
summarize the past) — mutated by each chain run. The modern LCEL-era answer is
RunnableWithMessageHistory: wrap any chain that has a MessagesPlaceholder("chat_history"),
give it a get_session_history(session_id) factory, and it loads history before invoke and
appends the new turns after, keyed by the session_id in config={"configurable": ...} —
per-conversation state without global mutability. (And in LangGraph, memory is subsumed by the
checkpointer + thread_id — Phase 18 §7 — which is where new code should be.)
The distinction Lab 03 makes concrete, because interviewers probe it: an agent has two memories
with different lifetimes. The scratchpad — the (action, observation) trace — lives for
one run and is how the agent chains steps within a task. The chat history lives across
runs and is how the second question can say "the one you mentioned earlier." Lab 03's executor
keeps both: scratchpad rebuilt per invoke, self.memory appended after each invoke, and a test
proves a recall question answers from the prior turn. Unbounded buffers are the naive policy —
eviction, windowing, and summarization are Phase 04's
whole subject.
13. The classic AgentExecutor — and why LangGraph superseded it
Pre-LangGraph, every LangChain agent ran inside AgentExecutor: a while-loop around an
"agent" (prompt + model + parser producing a decision) and a tool list. The vocabulary is exact
and still in the codebase you will inherit:
AgentAction(tool, tool_input, log)— the model wants a tool run.logcarries its reasoning text (the ReAct "Thought:").AgentFinish(output, log)— the model is done;outputis the answer.- The agent scratchpad — the accumulated
(AgentAction, observation)pairs, re-rendered into the prompt each iteration (as Thought/Action/Observation text for ReAct agents, or asAIMessage+ToolMessagepairs for tool-calling agents).
The loop, which Lab 03 has you build in full: call the model with (question, scratchpad, history);
if AgentFinish, stop; else dispatch the tool, append the observation, repeat — guarded by
max_iterations (real default 15), which on exhaustion forces the literal answer "Agent stopped
due to iteration limit." (early_stopping_method="force"). Errors are data: an unknown tool
becomes the observation "X is not a valid tool, try one of [...]", and handle_tool_error /
handle_parsing_errors turn crashes into observations the model can react to — Phase 02's
validate-and-repair loop, running inside the agent. return_intermediate_steps=True surfaces
the trace, which is your audit and debug surface.
Why it was superseded — and this is the answer that separates "used it" from "understands
it": the loop is a black box. You cannot pause it mid-flight for human approval of a risky
tool call; you cannot checkpoint it and resume after a crash; you cannot add a guardrail node
between "decide" and "act"; you cannot fork the state and replay. Every one of those is a
structural limitation of "the control flow is a hidden while-loop inside a class." LangGraph's
fix is to make the loop explicit: an agent node, a tools node, a conditional edge asking
"tool call or finish?", and a back-edge — at which point the checkpointer gives you
durability/memory/time-travel and interrupt() gives you human-in-the-loop, because every
super-step is addressable state (Phase 18 §6–9 — the same ReAct semantics you built here, exploded
into a graph). LangChain's own docs now direct new agent work to LangGraph, and the migration
guide maps AgentExecutor parameters onto create_react_agent one-to-one. Lab 03's extension
asks you to perform that migration onto your own Phase 18 engine — the two traces should match
step for step.
14. LangSmith and LangServe
LangSmith is the observability and evaluation platform, and its integration is the payoff of
the Runnable design: because every component funnels through the same interface and config
propagation (§5), setting a handful of environment variables (LANGCHAIN_TRACING_V2=true,
LANGCHAIN_API_KEY=...) traces every step of every chain — inputs, outputs, latency, token
usage, errors — as a nested tree matching your composition. On top of traces it layers datasets
(collections of input/expected-output examples), evaluators (heuristics or LLM-as-judge —
Phase 11's discipline, hosted), regression comparison between chain versions, and
prompt-management. The trace tree is the 2 a.m. tool: "which retriever call returned garbage" is
one click, not a print-statement archaeology dig. It is framework-agnostic enough to trace
LangGraph too (same substrate), and Phase 14's
cost/latency meters are the build-it-yourself version of exactly this telemetry.
LangServe closes the loop from Runnable to service: add_routes(app, chain, path="/rag")
mounts any Runnable on a FastAPI app with /invoke, /batch, /stream, /stream_events
endpoints, auto-generated schemas, and a playground UI — the Runnable interface, exposed over
HTTP verbatim. (For LangGraph agents the analogous deployment layer is LangGraph Platform,
Phase 18 §12.) You will not build either in a lab — the lesson is architectural: one interface
means one deployment adapter and one tracing hook for everything you will ever compose.
15. LangChain vs LangGraph: the decision framework
The question you will be asked, so here is the deep version. Both sit on langchain-core; both
are maintained by the same company; they are complements, not competitors.
Execution model. LCEL is a DAG evaluator: sequence, parallel, branch — data flows one way, each invoke is stateless, the expression is the control flow. LangGraph is a Pregel/BSP runtime (Phase 18 §2): named state channels with reducers, nodes that write partial updates, super-steps, and — crucially — cycles as a first-class shape, guarded by a recursion limit.
State. An LCEL chain owns no state between invokes; whatever context it needs must be passed
in (or bolted on via RunnableWithMessageHistory). LangGraph state is declared, reduced, and
checkpointed every super-step, keyed by thread_id — which is what makes multi-turn memory,
crash-resume, time travel, and durable human-in-the-loop pauses (interrupt()) native rather
than bolted on.
The decision, feature by feature:
| You need | Use |
|---|---|
| one-pass RAG, extraction, classification, summarization, prompt pipelines | LCEL |
| routing between sub-chains (no loops) | LCEL (RunnableBranch / dynamic routing) |
| streaming/batch/fallbacks over a fixed pipeline | LCEL |
| any reason→act→observe loop (an agent deciding repeatedly) | LangGraph |
| durable state, multi-turn memory at the runtime level, crash-resume | LangGraph |
| human approval gates mid-flow, time travel, forking | LangGraph |
| multi-agent supervision/handoffs | LangGraph (Phase 18 §11) |
They compose. A compiled LangGraph graph is a Runnable — drop it into an LCEL pipeline; a
LangGraph node's body is ordinary code that freely calls LCEL chains (a RAG chain inside a
retrieve node is idiomatic). Real systems are typically LCEL pipelines inside LangGraph nodes.
The migration heuristic for the codebase you inherit: legacy LLMChain/RetrievalQA →
rewrite as an LCEL expression (mechanical). Legacy AgentExecutor → migrate to a LangGraph
create_react_agent (also mechanical — Lab 03 builds the loop, Phase 18 Lab 01 builds its
replacement, and the extension has you diff the traces). The principal-level answer is never
"LangGraph is better"; it is "LCEL for pipelines, LangGraph for stateful loops — and here is
which one your feature is."
16. Common misconceptions
- "LangChain and LangGraph are competitors — pick one." They share
langchain-coreand compose; LCEL is the pipeline layer, LangGraph the agent runtime. Real systems use both. - "
|is just function composition sugar." It is composition with an interface contract: the result is a Runnable, so streaming, batching, fallbacks, config propagation, and tracing apply to the whole pipeline — that is materially more thanf(g(x)). - "Streaming just works anywhere in the chain." A step that must materialize its whole input
(a prompt template, most custom lambdas) ends incremental flow; only stream-capable last steps
(model,
StrOutputParser,JsonOutputParser) keep tokens flowing to the caller. - "RAG needs an agent." One-shot RAG is a stateless LCEL pipeline. Only agentic RAG (grade/rewrite/retry loops) needs LangGraph — don't pay the runtime complexity for a DAG.
- "
AgentExecutoris how you write LangChain agents." It is how you maintain them; it is deprecated for new work in favor of LangGraph, and "why" (opaque loop vs explicit graph) is the interview question. - "Output parsers make the model emit valid JSON." Parsers process text after generation;
.with_structured_outputconstrains generation itself via the provider's native channel. Different mechanisms, different failure modes. - "Memory is something LangChain does for you." Memory is always the application re-supplying
the past; the framework only standardizes where it is stored and spliced in
(
MessagesPlaceholder+ history wrapper — or LangGraph's checkpointer).
17. Lab walkthrough & success criteria
- Lab 01 (LCEL/Runnables) — implement the
Runnablebase (invoke/batch/stream), coercion,RunnableSequence/Parallel/Passthrough.assign/Lambda/Branch/Generator, andwith_fallbacks/with_retry/with_config. Prove the algebra: invoke == folded stream, the sequence threads, parallel fans out, branch routes, fallbacks recover. 29 tests. - Lab 02 (RAG chain) — implement
Document, the deterministicKeywordRetriever,format_docs, a validatingPromptTemplate, the injectedquoting_model, andparse_with_sources; assembleretrieve | augment | generate | parse. Prove retrieval feeds generation, citations surface, the question threads through, and empty retrieval refuses gracefully. 25 tests. - Lab 03 (AgentExecutor) — implement
Tool,AgentAction/AgentFinish, and the executor loop withmax_iterations, structured error observations,return_intermediate_steps, and cross-invoke memory. Prove single- and multi-step tasks, the trace, the guard, error recovery, and memory. 21 tests.
You're done when you can (a) state the Runnable protocol and why LCEL composes, (b) write the
canonical RAG chain from memory and say where citations come from, (c) explain the AgentExecutor
loop and exactly why LangGraph superseded it, (d) defend a LangChain-vs-LangGraph call for any
feature description, and (e) all three labs pass under both lab and solution.
18. Interview Q&A
Q: What is a Runnable and why does LangChain build everything on it? A: The universal
component interface: invoke (one input), batch (many, order-preserving), stream (chunked
output), plus async twins. Prompts, models, parsers, retrievers, tools — all Runnables. Because
composition (|) yields another Runnable, pipelines are closed under the interface, so
streaming, batching, fallbacks, config propagation, tracing, and deployment are written once
against the interface and work on anything you compose. The key invariant: folding stream's
chunks reproduces invoke.
Q: What does prompt | model | parser actually do? A: Builds a RunnableSequence. invoke
threads the input through each step: dict → formatted prompt → model message → parsed value.
stream runs the upstream steps, then streams from the last stream-capable component. Bare
functions and dicts in the pipe are coerced to RunnableLambda/RunnableParallel — which is why
the canonical RAG chain can open with a dict literal.
Q: Why did LCEL replace LLMChain and SequentialChain? A: The legacy classes were a bespoke class per pattern — each re-implementing (or lacking) streaming, async, batching, and callbacks, and customization meant subclassing. LCEL inverts it: one interface, generic combinators, so every composed chain gets stream/batch/async/fallbacks/tracing for free and the pipeline is visible in the expression. The old classes are deprecated with documented LCEL migrations.
Q: Walk me through the canonical LangChain RAG chain and where citations come from. A:
{"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | model | parser. The dict is a RunnableParallel fanning the query to the retriever (docs → formatted
context string) while a passthrough carries the question; the merged dict fills the prompt; the
model generates; the parser extracts. Citations come from carrying the retrieved docs through
the chain (via parallel/assign) instead of consuming them, so the final parser can emit
answer + source doc IDs. Empty retrieval should yield a grounded refusal — the docs-in-context
are the only ground truth.
Q: LangChain vs LangGraph — when do you use which? A: LCEL is a DAG evaluator — declarative
composition for one-way data flow: RAG, extraction, routing, prompt pipelines; stateless per
invoke; streaming/fallbacks free. LangGraph is a Pregel-style stateful runtime — cycles, reduced
state channels, per-step checkpointing, interrupt() for human-in-the-loop. The line is the
control-flow shape: pipeline → LCEL; loop or durable state or approval gate → LangGraph. They
compose — a compiled graph is a Runnable, and graph nodes call LCEL chains inside.
Q: Why was AgentExecutor deprecated, and what replaced it? A: Its ReAct loop is a black box
inside a class: no mid-loop pause for human approval, no checkpoint/resume, no inserting a
guardrail step, no forking state. LangGraph replaces it with the same loop as an explicit graph
(agent ↔ tools with a conditional edge) where every super-step is checkpointed state — so
durability, memory, time travel, and interrupt() become native. create_react_agent is the
drop-in successor and the migration is essentially mechanical.
Q: How does an AgentExecutor handle a model that requests a nonexistent tool? A: Not with a
crash — the executor feeds back a structured observation: "X is not a valid tool, try one of
[...]". The model sees it next iteration and can self-correct; handle_tool_error does the same
for tool exceptions. Errors-as-observations is the agent's repair loop, and max_iterations
bounds it — on exhaustion the executor force-returns "Agent stopped due to iteration limit."
Q: What's the difference between an output parser and .with_structured_output? A: A parser
transforms text after generation (StrOutputParser, JsonOutputParser,
PydanticOutputParser + format instructions) — the model may still emit garbage, so you pair it
with validation/repair. .with_structured_output uses the provider's native structured-output or
tool-calling channel to constrain generation and returns validated objects. Prefer constraining
when supported; parse-and-repair when not.
Q: How does streaming behave in a multi-step chain? A: The sequence streams its final
stream-capable step; upstream steps that need whole inputs run eagerly. Chunks are additive
(AIMessageChunk.__add__), so folding the stream equals invoke — an invariant you can test.
For intermediate visibility (retriever finished, tool started), astream_events emits structured
events from inside the pipeline. Design consequence: model + stream-capable parser last, or you
silently lose streaming.
Q: How do you test a LangChain application without hitting a model API? A: The model is
just a Runnable, so inject a deterministic fake (FakeListLLM/GenericFakeChatModel, or a plain
function wrapped in RunnableLambda) and assert on the chain's wiring: retrieval fed the prompt,
the parser got the expected shape, fallbacks fire on an injected exception. Same seam as the
labs: model as a pure function of its input; the chain's correctness must not depend on the
model being smart.
Q: A RAG chain answers fluently but wrong. Debug it. A: Trace first (LangSmith or logged intermediate state): was the right chunk retrieved? If not — chunking size/overlap, embedding model, k, hybrid fusion (Phase 05 levers). If retrieved but unused — prompt doesn't foreground the context, or context got truncated. If no docs retrieved and it still answered — the grounding discipline failed: enforce refusal-on-empty-context (Lab 02 tests exactly this) and add citations so wrong answers are attributable. The bug is usually retrieval precision, not the model.
Q: Where does memory live in a modern LangChain app? A: Chat history is loaded and spliced
into a MessagesPlaceholder by RunnableWithMessageHistory, keyed by a session_id in config —
per-conversation, no global mutation. Inside an agent run, the scratchpad (action/observation
trace) is separate, per-run state. In LangGraph both collapse into checkpointed graph state keyed
by thread_id, which is the recommended home for new work. And any buffer needs an eviction/
summarization policy eventually — Phase 04.
19. References
- LangChain documentation — Introduction & conceptual guide. https://python.langchain.com/docs/introduction/ and https://python.langchain.com/docs/concepts/
- LangChain Expression Language (LCEL) — concepts. https://python.langchain.com/docs/concepts/lcel/
- Runnable interface — concepts (invoke/batch/stream, config, coercion). https://python.langchain.com/docs/concepts/runnables/
- Streaming — concepts (chunks, astream_events). https://python.langchain.com/docs/concepts/streaming/
- Prompt templates & few-shot prompting — concepts. https://python.langchain.com/docs/concepts/prompt_templates/
- Output parsers & structured output — concepts. https://python.langchain.com/docs/concepts/output_parsers/ and https://python.langchain.com/docs/concepts/structured_outputs/
- Retrievers, vector stores, embeddings, text splitters — concepts. https://python.langchain.com/docs/concepts/retrievers/
- Build a RAG application — the canonical tutorial (the Lab 02 chain shape). https://python.langchain.com/docs/tutorials/rag/
- Tools & tool calling — concepts. https://python.langchain.com/docs/concepts/tool_calling/
- Agents — concepts, and "How to migrate from legacy LangChain agents to LangGraph." https://python.langchain.com/docs/concepts/agents/ and https://python.langchain.com/docs/how_to/migrate_agent/
- ReAct: Synergizing Reasoning and Acting in Language Models (Yao et al., 2022) — the pattern under AgentExecutor and Lab 03. https://arxiv.org/abs/2210.03629
- LangGraph documentation — the successor runtime (Phase 18 of this track). https://langchain-ai.github.io/langgraph/
- LangSmith documentation — tracing and evaluation. https://docs.smith.langchain.com/
- LangServe — deploy Runnables as REST APIs. https://python.langchain.com/docs/langserve/
- This track: Phase 18 (LangGraph) — the other half of this phase; Phase 01 (the ReAct loop); Phase 02 (tool calling & structured output); Phase 04 (memory policies); Phase 05 / Phase 06 (retrieval mechanisms); Phase 11 (evaluation); Phase 14 (observability).