Explain LCEL and the Runnable interface. What do you get for free by composing with | instead of writing plain Python?
Quick Answer
LCEL chains Runnables with the pipe operator; every composed chain automatically supports invoke/ainvoke, batch/abatch, stream/astream, retries/fallbacks, and emits tracing events per step. Plain Python gives you none of that without hand-writing it.
Detailed Answer
Everything in modern LangChain implements Runnable: prompts, models, parsers, retrievers, and arbitrary functions (RunnableLambda). Composition (prompt | model | parser) returns another Runnable whose sync/async/batch/streaming implementations are derived automatically — streaming propagates through the chain so tokens flow to the client even with a parser at the end, and batch gets provider-level concurrency. You also get declarative structure: RunnableParallel fans out, RunnableBranch routes, .with_retry() and .with_fallbacks() wrap any step, and with_structured_output binds schemas. Because chains are data structures rather than opaque code, callbacks/LangSmith can trace every step with inputs/outputs — the observability story is the strongest practical argument. The honest downside: stack traces route through the framework's invoke machinery, and inserting a debugger or log line between piped steps is less natural than in plain Python — the standard workaround is dropping in a RunnableLambda tap or relying on tracing.
Code Example
chain = (
{'context': retriever | format_docs, 'question': RunnablePassthrough()}
| prompt
| model.with_retry(stop_after_attempt=3)
| StrOutputParser()
).with_fallbacks([cheap_model_chain])
async for chunk in chain.astream('How do I rotate TLS certs?'):
yield chunkInterview Tip
List the four freebies concretely: async, batch, streaming propagation, per-step tracing. Then volunteer the debugging tradeoff before the interviewer raises it.