Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›LangChain›LangChain Architecture and LCEL

Core Concepts

LangChain Architecture and LCEL

LangChain provides a structured framework for building complex LLM applications by abstracting interaction patterns into unified components. LCEL acts as a declarative language that allows developers to compose these components into execution chains with native support for streaming, retries, and parallelism. You reach for this architecture when your application logic moves beyond simple prompt-completion loops into multi-step reasoning, memory management, or retrieval-augmented generation pipelines.

The Core Primitive: Runnable Protocol

The foundation of modern LangChain is the Runnable protocol, which defines a unified interface for all components like prompts, models, and output parsers. Every component implements this protocol, meaning they all support 'invoke', 'stream', and 'batch' methods. This design choice is critical because it forces modularity; by treating every building block as a pipeable unit, you avoid creating bespoke interfaces for every piece of your application. When you define a chain, you are essentially defining a directed graph of these runnables. Understanding the protocol is essential because it is the mechanism that enables automatic trace logging and error handling across your entire pipeline. By ensuring every piece shares the same input/output contract, the framework can handle complexity at the abstraction layer rather than forcing the developer to manage state manually between disparate library function calls.

from langchain_core.runnables import RunnableLambda

# A Runnable acts as a function with extra powers.
# Every component here adheres to the same interface.
format_input = RunnableLambda(lambda x: f"Translate to French: {x}")

# Invoke returns the result of the chain operation.
result = format_input.invoke("Hello world")
print(result)  # Output: Translate to French: Hello world

Composition via LCEL

LangChain Expression Language (LCEL) uses the pipe operator (|) to compose runnables into a seamless pipeline. Under the hood, this operator constructs a specialized RunnableSequence object that automatically handles the passing of outputs from one stage as inputs to the next. The power of this declarative style is that the framework can inspect the topology of the sequence at runtime. This allows for features like automatic parallel execution of independent branches and streaming responses that propagate back through the entire chain without manual buffer management. By describing the relationship between components rather than the imperative execution flow, you ensure that your code remains resilient to changes in internal state. You reason about your application as a data transformation pipeline, which is conceptually cleaner and significantly easier to debug when complex orchestration logic is required for production-level systems.

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

# Composition using the pipe operator | 
# The prompt output is fed directly into the model.
prompt = ChatPromptTemplate.from_template("Tell me a joke about {topic}")
model = ChatOpenAI()

# Define the chain via LCEL
chain = prompt | model
print(chain.invoke({"topic": "programming"}))

Managing Dynamic Context with RunnablePassthrough

One of the most common challenges in complex chains is maintaining context across multiple steps. In a retrieval-augmented generation scenario, you might have a question that needs to be passed to both a retriever and the final prompt template simultaneously. RunnablePassthrough solves this by allowing you to inject additional data into the execution flow without interrupting the primary data stream. When used within a dictionary, it allows you to construct complex input objects dynamically. This approach is superior to manual variable passing because it keeps the chain definition flat and readable. The framework detects the need for this parallelism and executes the branches concurrently, which is vital for minimizing latency in real-time applications. By leveraging passthroughs, you ensure that your chains remain pure and stateless, relying on the framework to merge disparate data streams into the correct final input schema for your model.

from langchain_core.runnables import RunnablePassthrough

# Passthrough allows merging of data streams.
# We keep the 'topic' and add 'timestamp'.
chain = {
    "topic": RunnablePassthrough(),
    "timestamp": lambda x: "2023-10-27"
} | (lambda x: f"Topic: {x['topic']} at {x['timestamp']}")

print(chain.invoke("AI"))

Streaming and Async Orchestration

Streaming is the backbone of high-quality user experiences in generative applications, and LCEL makes it a first-class citizen. Because the entire chain is built on the Runnable protocol, a stream initiated at the end of a long chain is automatically propagated back through every component in the sequence. This means you do not have to write manual async loops or handle partial completions at every layer of your architecture. The framework handles the interleaving of tokens and the buffering of intermediary results. When reasoning about your application, you can rely on the fact that asynchronous calls are handled efficiently, preventing blocking operations from stalling the pipeline. This native support for streaming allows developers to implement 'chunking' or 'token-level' feedback loops, which are critical for building sophisticated agents that provide immediate, low-latency interactions to end users, regardless of chain length.

import asyncio

# Streaming is supported across the entire chain structure.
chain = prompt | model

async def stream_output():
    async for chunk in chain.astream({"topic": "coding"}):
        print(chunk.content, end="", flush=True)

asyncio.run(stream_output())

Configurable Parameters and Fallbacks

Finally, the architecture supports runtime configuration, allowing you to swap components or adjust parameters like temperature or model versions without rewriting your logic. This is achieved through the 'configurable' interface, which binds specific settings to the chain at invocation time. Furthermore, LCEL supports 'fallbacks', allowing you to define a secondary or tertiary model to try if the primary one fails due to rate limits or API errors. This architectural resilience is necessary for production reliability. By decoupling the definition of the workflow from the configuration of the components, you can write generic pipelines that are easily adapted for testing or specialized use cases. When you combine this with the framework's ability to inspect and log the state at every stage, you gain a robust diagnostic environment that helps you reason about why specific chains behave as they do under varied conditions.

# Fallbacks ensure production stability.
# If the first model fails, the second one triggers.
chain = (model | (lambda x: x.content)).with_fallbacks([model])

# Configure settings dynamically at runtime.
configured_chain = chain.with_config({"configurable": {"temperature": 0.7}})
print(configured_chain.invoke({"topic": "testing"}))

Key points

  • The Runnable protocol ensures that all components share a consistent interface for interaction.
  • LCEL uses the pipe operator to create declarative and composable execution sequences.
  • Data streams are automatically managed through the chain pipeline without manual buffer handling.
  • RunnablePassthrough enables the injection of additional context into chains during execution.
  • Asynchronous operations are natively supported to maintain low-latency application performance.
  • Streaming is propagated automatically across all components within an LCEL sequence.
  • Runtime configuration allows for swapping components and parameters without code refactoring.
  • Fallbacks provide an essential mechanism for ensuring reliability against intermittent model errors.

Common mistakes

  • Mistake: Manually creating chains using inheritance from BaseChain. Why it's wrong: It is verbose and lacks the composition capabilities of LCEL. Fix: Use the pipe operator (|) to compose components into Runnable sequences.
  • Mistake: Forgetting to include input/output schemas for custom Runnable components. Why it's wrong: Type checking and LangSmith tracing will fail to capture the data flow. Fix: Use the .with_types() method or inherit from RunnablePassthrough/RunnableLambda with explicit signatures.
  • Mistake: Assuming that .stream() works the same way for every component. Why it's wrong: Not all components support streaming; some need to process the entire input before outputting. Fix: Always check the 'supports_streaming' property of the component or use .invoke() for non-streaming components.
  • Mistake: Over-reliance on global state instead of passing context via RunnableConfig. Why it's wrong: It breaks the predictability of chains and hinders parallel execution. Fix: Pass configuration objects through the chain using .with_config() or via the invoke argument.
  • Mistake: Misunderstanding the role of RunnablePassthrough. Why it's wrong: Developers often think it is purely for logging. Fix: Use it to pipe input forward while concurrently fetching extra data from other sources to enrich the prompt context.

Interview questions

What is LangChain and what is its primary purpose in building applications?

LangChain is a robust framework designed to facilitate the development of applications powered by large language models. Its primary purpose is to move beyond simple API calls by providing modular abstractions for chaining together different components. By using LangChain, developers can create complex workflows—like retrieval-augmented generation—where the output of one step becomes the input for the next, allowing for stateful, context-aware applications that interact effectively with external data sources and tools.

What is LCEL and why is it preferred over standard Python code for building chains?

LCEL, or LangChain Expression Language, is a declarative way to compose chains by connecting components using the pipe operator. While you could technically write standard Python code to handle chain logic, LCEL is preferred because it automatically provides essential features like streaming support, asynchronous execution, and optimized batch processing. Furthermore, it generates a standardized interface for tracing and debugging, which makes complex production chains much easier to monitor and maintain compared to custom, manual implementations.

How do you manage state in a LangChain application, and why is this important?

Managing state in LangChain is typically handled through 'Message History' objects, which store the conversation context so the model can remember previous interactions. This is crucial because language models are stateless by default. Without a defined state management strategy, the model would treat every query as an isolated event. By using a RunnableWithMessageHistory wrapper, we inject the historical context into the prompt, ensuring the application maintains continuity and behaves like a truly coherent assistant throughout a session.

Compare the 'Chain of Thought' prompting strategy with the use of LCEL-based function calling.

The 'Chain of Thought' approach encourages the model to generate reasoning steps internally to improve accuracy on complex logic problems, whereas LCEL-based function calling (often called Tool Calling) shifts the logic to external, deterministic code. Chain of Thought relies on the model's probabilistic reasoning, which can sometimes hallucinate logic, while function calling in LangChain forces the model to trigger precise Python functions or APIs, offering much higher reliability and control when interacting with external systems.

Explain the role of the 'Runnable' interface in LangChain architecture.

The 'Runnable' interface is the fundamental building block of modern LangChain architecture. It defines a standard set of methods—such as invoke, batch, and stream—that every component must implement. Because prompt templates, models, and output parsers all implement this interface, they become 'composable'. This standardization is why we can connect disparate components using the pipe operator; the interface guarantees that the output of one component is compatible with the input requirements of the next.

How would you design a robust RAG pipeline using LCEL to handle document retrieval?

To design a robust RAG pipeline in LCEL, I would chain a retriever, a prompt template, and a chat model. The structure would look like: `setup_chain = {'context': retriever, 'question': RunnablePassthrough()} | prompt | model | parser`. I choose this architecture because it treats the retriever as a first-class component, automatically passing document content into the prompt's context variable. By using `RunnablePassthrough`, I ensure the original query remains available, allowing the model to ground its response specifically in the retrieved data provided.

All LangChain interview questions →

Check yourself

1. What is the primary architectural advantage of using the pipe (|) operator in LCEL compared to traditional class-based chaining?

  • A.It automatically compiles the code into binary for faster execution.
  • B.It forces all inputs to be strings, simplifying the data validation layer.
  • C.It creates a unified interface (Runnable) that allows components to be composed, streamed, and traced consistently.
  • D.It disables the need for prompts and memory modules entirely.
Show answer

C. It creates a unified interface (Runnable) that allows components to be composed, streamed, and traced consistently.
The pipe operator creates a Runnable sequence, ensuring all components follow the same lifecycle (invoke, stream, batch). Option 1 is wrong because it's still Python code; 2 is wrong because LCEL supports complex types; 4 is wrong because LCEL integrates with, not replaces, these modules.

2. When building a chain, why would you prefer `RunnableLambda` over a standard Python function?

  • A.To allow the function to be tracked and instrumented as part of a LangChain trace.
  • B.To bypass the memory constraints of the underlying language model.
  • C.To automatically generate unit tests for the logic contained within the function.
  • D.To allow the function to execute on external hardware accelerators.
Show answer

A. To allow the function to be tracked and instrumented as part of a LangChain trace.
RunnableLambda makes logic a first-class citizen of the chain, enabling visibility in LangSmith. Other options describe capabilities outside the scope of the LCEL framework itself.

3. How does `RunnablePassthrough.assign()` differ from `RunnablePassthrough()`?

  • A.It converts all output to a dictionary instead of passing it through.
  • B.It adds new fields to the input dictionary while keeping the original input intact.
  • C.It deletes all metadata associated with the previous chain step.
  • D.It creates a hard stop in the chain, preventing further data flow.
Show answer

B. It adds new fields to the input dictionary while keeping the original input intact.
The assign method specifically allows you to augment the current input state by injecting new key-value pairs without losing the original data. Other options incorrectly describe the function of the method.

4. If you are streaming a chain, what happens if an intermediate component in the sequence does not support streaming?

  • A.The entire chain will throw a compilation error.
  • B.The chain will automatically cache the entire output and yield it in one chunk once it completes.
  • C.The chain will skip that component and proceed to the next.
  • D.The chain will force a hard crash to prevent data corruption.
Show answer

B. The chain will automatically cache the entire output and yield it in one chunk once it completes.
LCEL is designed to be resilient; if a step can't stream, it will compute the result internally and provide it to the next step, essentially 'batching' the output. It does not crash or skip components.

5. Why is it recommended to use `RunnableConfig` when invoking complex chains?

  • A.It is the only way to define the model temperature.
  • B.It allows for dynamic runtime settings like callback handlers, tags, and recursion limits without changing the chain definition.
  • C.It compresses the prompt to reduce token usage.
  • D.It allows the chain to access restricted environment variables.
Show answer

B. It allows for dynamic runtime settings like callback handlers, tags, and recursion limits without changing the chain definition.
RunnableConfig provides a standard way to pass runtime parameters that affect behavior (like metadata or tracing) without modifying the static logic of the chain. It is not used for prompt compression or environment variable access.

Take the full LangChain quiz →

Next →LLMs and Chat Models

LangChain

24 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app