« Phase 29 README · Warmup · Hitchhiker's · Deep Dive · Principal Deep Dive · Core Contributor

Phase 29 — Staff Engineer Notes: LangChain Core

Anyone can paste chain = prompt | model | parser. What a team pays a staff engineer for is judgment: which runtime a feature belongs in, when the framework earns its dependency and when it doesn't, and what to block in review before it reaches production. This is the seniority signal, and it's what the harder interviews are actually probing.

The decisions a staff engineer owns here

Runtime selection: LCEL or LangGraph. This is the one that separates levels, and the answer is a shape, not a brand. Trace the control flow. Does data move one way — retrieve, format, prompt, generate, parse? That's a DAG; LCEL is simpler and correct, and pulling in a graph runtime is overengineering. Does anything loop with state — an agent re-deciding after a tool result, a grader bouncing a bad retrieval back for a rewrite, a human approving before the flow proceeds? That's a cycle with durable state; LCEL structurally cannot express it and LangGraph is the answer. The one-sentence version you say out loud: "LCEL for pipelines, LangGraph for stateful loops, and here's which one this feature is." Fanboying either is how you fail the question.

Whether to use LangChain at all. A staff engineer is allowed to say no. If the task is a single templated call to one provider with no composition, no retrieval, no fan-out, and no need for provider portability, the raw SDK is less indirection and less dependency risk. LangChain earns its place when you want the closure benefits — uniform streaming/batching/fallbacks/tracing across a multi-step pipeline, the ability to swap providers behind a stable interface, and first-class observability. Reaching for the framework for a one-liner is a smell; refusing it for a real pipeline is reinventing the Runnable badly.

Structured output strategy. .with_structured_output(Schema) constrains generation through the provider's native tool/JSON channel; a PydanticOutputParser post-processes free text. The native channel fails less but only where the model supports it; the parser works everywhere but fails messier. Choosing — and knowing the model's tool-calling ceiling before routing traffic to a cheaper, tool-blind model — is a design decision, not a default.

Migration posture on legacy code. Half the real job is "we have 40k lines of 2023-era RetrievalQA and initialize_agent, make it maintainable." The staff move is triage, not a rewrite: "this still runs; the chains migrate to LCEL on next touch; the agent part moves to LangGraph because it needs a pause we can't get from the executor." Deprecated is not dead — maintain it, migrate it, don't start on it.

Code-review red flags

  • A new agent built on AgentExecutor. Almost always means the author copied a pre-LangGraph tutorial. Ask what happens when they need a human approval mid-loop.
  • A whole-input step after the model (a trailing lambda, a json.loads, a strip). It silently kills streaming. If the product streams, this is a defect the tests won't catch.
  • Tests that hit a live model API. The model is a Runnable — inject a fake and assert the wiring. Live-model tests are slow, flaky, non-deterministic, and cost tokens on every CI run.
  • Consuming retrieved docs instead of carrying them. Then there's nowhere for citations to come from. Sources must thread through via assign.
  • No .with_fallbacks/timeout on a hot-path model call, or a fallback with no instrumentation (a quiet quality cliff), or max_iterations left at the default on an agent a cron re-invokes (a budget bomb).
  • Imports from the monolith (from langchain import ...) instead of langchain-core plus per-provider packages — a version-conflict generator.
  • Treating tool docstrings as comments. The model reads them to choose tools; they are prompts, and a vague one causes wrong tool selection.

War stories

The chain that stopped streaming after a one-line change. Someone appended a tidy post-processing lambda after the parser. It needed the whole string, so the default buffering transform kicked in and the UI went from token-by-token to a nine-second freeze. No test failed — the final output was identical. Fix: keep the model plus a stream-capable parser last. Lesson: streaming is a property of pipeline shape, not a flag.

The RAG bot that confidently described a product that didn't exist. Retrieval returned empty, the prompt didn't force grounding, and the model freestyled. The customer saw a fabricated feature. The fix was Lab 02's pattern verbatim: refuse on empty context and return citations so every claim is attributable. Retrieval bugs masquerade as model bugs.

The agent that burned $400 overnight. A legacy AgentExecutor with a flaky tool: the error observation ("not a valid tool") made the model retry the same hallucinated tool forever, max_iterations was still 15 for a two-step task, and a cron kept re-invoking it. Diagnosed from return_intermediate_steps. Lesson: budgets are a feature; the trace is how you find out.

The signal an interviewer or architecture review listens for

They are not checking whether you can write prompt | model | parser — everyone can. They are listening for four things: (1) you can explain the Runnable closure and why it gives streaming/batching/fallbacks for free, including the invoke == stream folded with + invariant; (2) you can make the LCEL-vs-LangGraph call from the control-flow shape without hesitating; (3) you know the unglamorous plumbing — citations are carried not generated, streaming dies to a trailing step, tool docstrings are prompts, the model is a testable seam; and (4) you treat deprecation as an architecture lesson — you can explain why the hidden executor loop was the wrong abstraction and what LangGraph fixed. Candidates who defend LangChain like fans or sneer at it like commenters both miss; the one who says "that's an era-one RetrievalQA, here's the LCEL rewrite, and the agent part should move to LangGraph" is the one handed the migration.

Closing takeaways

  • The runtime call is the seniority call. One-way data flow is LCEL; a back-edge with state is LangGraph. Match the runtime to the control-flow shape and say why.
  • Know when not to use the framework. A single provider call doesn't need Runnables; a real multi-step pipeline does. Both mistakes read as inexperience.
  • Protect streaming and budgets as invariants. A trailing whole-input step and an unbounded max_iterations are the two silent production failures reviewers should reflexively catch.
  • Test the wiring, not the model. Inject a fake Runnable; assert composition. The seam this track has drilled since Phase 00 is exactly how the real framework wants to be tested.
  • Deprecated is a migration budget, not a verdict. Reading, fixing, and migrating era-one LangChain is the actual job the JD line is buying — and the story that gets you hired.