Antler by Autoflux

Architecture

The Runnable contract, LCEL composition, the message/chat-model layer, tools, retrievers, and how LangGraph turns pipelines into agents.

Path: architecture

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 — a RunnableConfig carrying 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

User
Frontend
Lending
Vaults
Trading
Liquidation
Core
Oracle
Data / Storage

Interface

Interface
python
# 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: ...
 
STATUSinterface