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).
Refer: LangChain concepts · GitHub
Package Architecture
Modern LangChain is split into focused packages so you install only what you need:
- langchain-core - base abstractions:
Runnable, messages, prompts, output parsers, tools, callbacks. No vendor SDKs. - langchain - higher-level chains, legacy agent helpers, and convenience re-exports.
- langchain-community - community integrations (loaders, vector stores, tools) with varying maintenance levels.
- langchain-openai, langchain-anthropic, etc. - first-party partner integrations maintained alongside provider API changes.
- langgraph - separate but related: graph-based orchestration built on langchain-core runnables.
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:
invoke(input)- single synchronous callainvoke(input)- async single callbatch(inputs)/abatch- parallel batching where the backend supports itstream(input)/astream- token or chunk streamingastream_log- stream intermediate steps for debugging
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:
SystemMessage- instructions, persona, policyHumanMessage- user input (text or multimodal content blocks)AIMessage- model output; may includetool_callsToolMessage- result of executing a tool, linked bytool_call_id
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:
- Defining tools with
@toolorStructuredTool - Binding tools to chat models
- Tool execution utilities
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
- Import sprawl - mixing deprecated
langchainpaths withlangchain-core; follow current docs. - Message list corruption - omitting ToolMessages or wrong
tool_call_idafter tool execution. - Unbounded context - stuffing retriever output without token budgeting.
- Silent provider differences - JSON mode, tool calling, and streaming vary by model; abstract via partner packages but test per model.
- Using AgentExecutor for complex workflows - migrate to LangGraph when you need checkpoints, interrupts, or parallel branches.
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.