LangChain

Composable primitives for building LLM applications: models, prompts, retrieval, tools, and orchestration via LCEL.

What LangChain Is (and Is Not)

LangChain is an open-source framework for building applications powered by large language models. It standardizes how you connect models to data sources, tools, memory, and business logic. The core value is composability: small, testable units (runnables) snap together into pipelines without rewriting glue code for every provider swap.

LangChain is not a model host, vector database, or agent runtime by itself. It integrates with OpenAI, Anthropic, local Ollama, Hugging Face, Pinecone, Postgres pgvector, and hundreds of other backends through adapter packages. For durable, stateful agent workflows with checkpoints and human approval, teams typically pair LangChain with LangGraph (same ecosystem, lower-level graph runtime).

Package Architecture

Modern LangChain is split into focused packages so you install only what you need:

Production code should prefer langchain-core + specific partner packages over monolithic imports. Pin versions in lockfiles; LangChain 0.2+ made breaking cleanups to message types and import paths.

The Runnable Interface and LCEL

LangChain Expression Language (LCEL) treats every step as a Runnable with a uniform contract:

Composition uses the pipe operator |: output of the left runnable becomes input of the right. This replaces brittle subclass hierarchies with declarative pipelines.

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a concise technical writer."),
    ("human", "{topic}"),
])
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
parser = StrOutputParser()

chain = prompt | model | parser

# Same chain supports invoke, stream, batch, async
print(chain.invoke({"topic": "KV caching in LLMs"}))

RunnableParallel and RunnableLambda

RunnableParallel runs branches concurrently and merges dict outputs. Useful for retrieving context and classifying intent in one pass:

from langchain_core.runnables import RunnableParallel, RunnablePassthrough

chain = RunnableParallel(
    context=retriever,
    question=RunnablePassthrough(),
) | prompt | model | parser

RunnableLambda wraps arbitrary Python functions into the Runnable contract so custom business logic participates in streaming and callback propagation.

Configurable runnables

.configurable_fields() and .configurable_alternatives() let you swap models or prompts at runtime via config={"configurable": {"model": "gpt-4o"}} without rebuilding the graph. Critical for A/B tests and tenant-specific model routing.

Messages and Chat Models

Chat models consume a list of messages, not raw strings. Core types in langchain_core.messages:

Multimodal messages use content lists: [{"type": "text", "text": "..."}, {"type": "image_url", "image_url": {"url": "..."}}]. Always persist the full message list for tool-calling loops; dropping ToolMessages breaks the next model call.

Binding tools to models

from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Return current weather for a city."""
    return f"Sunny in {city}"

model_with_tools = model.bind_tools([get_weather])
response = model_with_tools.invoke([HumanMessage("Weather in Berlin?")])
# response.tool_calls -> [{name, args, id}]

bind_tools converts Python functions (with type hints and docstrings) or Pydantic models into provider-native tool schemas. The model returns structured call requests; your runtime executes tools and appends ToolMessages before the next invoke.

Structured output

with_structured_output(MyPydanticModel) constrains responses to a schema via tool calling or JSON mode, depending on provider support. Prefer this over free-form JSON parsing for extraction pipelines.

Prompts and Templates

ChatPromptTemplate supports static tuples, MessagesPlaceholder for dynamic history, and few-shot example selectors. Template variables are validated at invoke time.

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "Answer using only the provided context."),
    MessagesPlaceholder("history"),
    ("human", "Context:\n{context}\n\nQuestion: {question}"),
])

FewShotChatMessagePromptTemplate + example selectors (semantic similarity, length-based) inject dynamic demonstrations without hardcoding giant prompts. Store examples in LangSmith or Langfuse for versioned prompt management.

Document Processing and RAG Building Blocks

Document loaders

Loaders in langchain_community.document_loaders normalize sources into Document(page_content=..., metadata={...}) objects. Metadata (source URL, page number, ACL) flows through to retrieval filters and citations.

Text splitters

RecursiveCharacterTextSplitter splits on paragraph, line, sentence boundaries with overlap. TokenTextSplitter respects tokenizer boundaries. For code, use language-aware splitters. Chunk size and overlap are top RAG quality levers; LangChain does not choose them for you.

Embeddings and vector stores

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

vectorstore = Chroma.from_documents(chunks, OpenAIEmbeddings())
retriever = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 8, "fetch_k": 20},
)

Retrievers are runnables: retriever.invoke("query") returns Document lists. Wrap with create_retrieval_chain or compose manually in LCEL with context injection.

RAG chain pattern

from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain

question_answer_chain = create_stuff_documents_chain(model, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)
result = rag_chain.invoke({"input": "How does HNSW work?"})
# result["answer"], result["context"]

For production RAG, add rerankers, hybrid search, query transformation, and citation enforcement outside or alongside these helpers.

Tools and Legacy Agents

LangChain historically shipped high-level agents (AgentExecutor, ReAct loops). These are largely superseded by LangGraph for anything requiring persistence, branching, or human-in-the-loop. You still use LangChain for:

Tool descriptions are part of the API contract. Invest in clear names, parameter docs, and error messages returned from tool handlers. The model reads tool errors on the next turn.

Callbacks, Config, and Observability Hooks

Pass config={"callbacks": [handler]} to propagate tracing through nested runnables. Built-in handlers integrate with LangSmith; Langfuse provides a CallbackHandler for traces, costs, and scores.

from langfuse.callback import CallbackHandler

langfuse_handler = CallbackHandler()
result = chain.invoke(
    {"topic": "RAG"},
    config={"callbacks": [langfuse_handler]},
)

RunnableConfig also carries tags, metadata, run_name, and max_concurrency. Tag runs by environment, tenant, or feature flag for downstream analytics.

Streaming in Production

chain.stream() yields incremental output. For chat UIs, stream AIMessage chunks. For debugging, astream_events(version="v2") emits fine-grained events (start/end of each runnable, token deltas, tool calls). Map events to SSE or WebSocket frames in your API layer.

Streaming does not automatically cancel upstream work on client disconnect; wire cancellation tokens or abort controllers in async paths.

Testing and Evaluation

Unit-test runnables with mocked chat models (FakeListChatModel) for deterministic outputs. Integration tests hit real APIs sparingly with recorded fixtures. LangSmith datasets and Langfuse experiments run chains against labeled examples and compare versions over time.

Common Pitfalls

When to Choose LangChain

Use LangChain when you need fast integration across models and data sources, LCEL pipelines with streaming, and standard RAG/tool primitives. Pair with LangGraph for agent state machines and Langfuse for production observability. Skip LangChain if you only need a thin OpenAI SDK wrapper with no retrieval composition; extra layers add little value there.