« 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

  1. What LangChain is, and the ecosystem map
  2. The evolution: from LLMChain to LCEL
  3. The Runnable protocol: one interface for everything
  4. LCEL composition: sequence, parallel, passthrough, branch
  5. Config, fallbacks, retries, and binding
  6. Streaming through a chain
  7. Prompt templates: string, chat, few-shot
  8. Output parsers and structured output
  9. Documents, loaders, splitters, embeddings, vector stores
  10. Retrievers and the canonical RAG chain
  11. Tools and tool-calling
  12. Memory and message history
  13. The classic AgentExecutor — and why LangGraph superseded it
  14. LangSmith and LangServe
  15. LangChain vs LangGraph: the decision framework
  16. Common misconceptions
  17. Lab walkthrough & success criteria
  18. Interview Q&A
  19. 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: the Runnable interface, prompt templates, the message types (HumanMessage, AIMessage, ToolMessage, …), output parsers, the Document and BaseRetriever abstractions, 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_agent in its legacy form) that most production 0.x code uses.
  • Integration packageslangchain-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 monolithic langchain_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), plus astream_events for 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:

RunnableSequencea | 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.

RunnableBranchRunnableBranch((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), or chain.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 — extract message.content as 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":

  • Documentpage_content + metadata (+ optional id). The unit everything below produces and consumes. (Lab 02's Document(id, text, metadata) is this, minimized.)
  • Document loadersPyPDFLoader, WebBaseLoader, CSVLoader, hundreds more; .load() yields Documents. Boring by design: the value is the uniform output type.
  • Text splittersRecursiveCharacterTextSplitter is the default: split on paragraph, then sentence, then word boundaries, targeting chunk_size with chunk_overlap so 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).
  • Embeddingsembed_documents(texts) / embed_query(text) → vectors. Query and document embedding are separate methods because some models embed them asymmetrically.
  • VectorStoreadd_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. log carries its reasoning text (the ReAct "Thought:").
  • AgentFinish(output, log) — the model is done; output is 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 as AIMessage+ToolMessage pairs 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 needUse
one-pass RAG, extraction, classification, summarization, prompt pipelinesLCEL
routing between sub-chains (no loops)LCEL (RunnableBranch / dynamic routing)
streaming/batch/fallbacks over a fixed pipelineLCEL
any reason→act→observe loop (an agent deciding repeatedly)LangGraph
durable state, multi-turn memory at the runtime level, crash-resumeLangGraph
human approval gates mid-flow, time travel, forkingLangGraph
multi-agent supervision/handoffsLangGraph (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-core and 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 than f(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.
  • "AgentExecutor is 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_output constrains 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 Runnable base (invoke/batch/stream), coercion, RunnableSequence/Parallel/Passthrough.assign/Lambda/Branch/Generator, and with_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 deterministic KeywordRetriever, format_docs, a validating PromptTemplate, the injected quoting_model, and parse_with_sources; assemble retrieve | 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 with max_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 (agenttools 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).