Architecture
The Runnable contract, LCEL composition, the message/chat-model layer, tools, retrievers, and how LangGraph turns pipelines into agents.
Third-party documentation. This is independently authored analysis of the public LangChain codebase — not the official docs, and not reviewed or endorsed by the LangChain team.
Architecture
Everything in LangChain is built on one idea: a Runnable. Understand the Runnable contract and LangChain Expression Language (LCEL), and you understand how the framework stays coherent across hundreds of integrations.
The Runnable contract
A Runnable is any object with a small, uniform interface:
invoke(input)/ainvoke(input)— one input, one output (sync/async).batch(inputs)/abatch(inputs)— many inputs, parallelized by default.stream(input)/astream(input)— token-by-token output for chat models.config— aRunnableConfigcarrying callbacks, metadata, tags, and runtime params.
Every component — prompt templates, models, output parsers, retrievers, tools, and LangGraph's compiled graphs — implements this interface. That uniformity is what makes the | operator meaningful.
LCEL: composition by piping
a | b returns a RunnableSequence that feeds a's output into b's input. Because the contract is uniform, sequences are themselves Runnables — you can pipe a sequence into another runnable, batch it, stream it, and pass it a config. A chain is just a nested sequence.
Composition primitives beyond piping:
RunnableParallel/RunnableMap— run several Runnables over the same input and merge results (fan-out).RunnableBranch— route based on a condition.RunnableLambda— wrap any plain function as a Runnable.RunnablePassthrough— pass input through unchanged (used to thread context alongside a transform).
The chat-model layer
LLMs are abstracted as chat models: objects that take a list of BaseMessages (system/human/ai/tool) and return an AIMessage, possibly with tool calls attached. This is the modern core of the framework — most "text generation" is really message-turn exchange. Providers are wrapped by integration packages, but all expose invoke/stream/bind_tools.
Tools and function calling
Models can declare tool-calls (structured requests to call your functions). A @tool-decorated Python function becomes a BaseTool with a JSON schema automatically derived from its signature/docstring. The model's tool-call output is parsed back into an actual call by the agent loop — this is the mechanism underneath all modern LangChain agents.
Retrieval architecture
RAG pipelines are just more Runnables: a BaseRetriever (given a query, returns Documents) composed with a prompt+model. Vector stores (FAISS, pgvector, etc.) implement the "store + search" half and expose .as_retriever() to produce the Runnable half, keeping indexing and retrieval decoupled.
LangGraph: stateful graphs for agents
LCEL is ideal for acyclic pipelines; agents need cycles (model → tool → model → …). LangGraph models the app as a state machine: a StateGraph with nodes (each a function/Runnable) and edges, where state is a shared object that nodes read and update. It adds:
- Persistence — checkpoint state between runs (threads), enabling interrupts and resumption.
- Human-in-the-loop — pause execution and resume with human input.
- Streaming of intermediate steps — the agent's tool calls become visible as they happen.
The framework's trajectory is clear: pipelines for the simple 90%, LangGraph for anything that loops or needs memory.
Design principles
- Uniform interface — one contract makes a thousand integrations composable.
- Composition over configuration — you build by piping objects, not by editing YAML.
- Provider-agnostic core — swap OpenAI for Anthropic by changing one constructor, not the whole app.
System Diagram
Interface
# The Runnable contract (langchain_core.runnables.base.Runnable) — the# interface every component implements:class Runnable(Generic[Input, Output]):def invoke(self, input: Input, config: RunnableConfig | None = None) -> Output: ...async def ainvoke(self, input: Input, config: RunnableConfig | None = None) -> Output: ...def batch(self, inputs: list[Input], config=None) -> list[Output]: ...async def abatch(self, inputs, config=None) -> list[Output]: ...def stream(self, input: Input, config=None) -> Iterator[Output]: ...async def astream(self, input, config=None) -> AsyncIterator[Output]: ...# The pipe operator turns two runnables into a RunnableSequence:def __or__(self, other: Runnable) -> RunnableSequence: ...def __ror__(self, other) -> RunnableSequence: ...
