LangChain in depth
← All modules Module 3 · LangChain

Module 3 · LangChain in depth · about 25 minutes

LangChain, one building block at a time

Module 2 showed where LangChain fits next to LangGraph and Langfuse. This module opens the box. We keep the same research assistant: it takes a question, searches the web, reads the results and writes a short report with citations.

modelspromptstoolsretrievalchainsmemoryagents

Words with a dotted underline, like , are technical terms. Tap one for a plain-English definition.

Interactive demo

Chain workbench

Two small pieces of the research assistant, taken apart. In Write chain, notes pass through a of three blocks: a , a model and an that expects . In Tool calling, the model makes a and your code runs a web search. Press Next step, click any block to look inside, then switch on a failure to see a or a retry.

Simulation.No real model or API is called; data, timings and prices are scripted and illustrative.
Question:
Model provider:

        

Keys: N next · P play/pause

The write-report chain


      
What's happening one step at a time
Block inspector click any block above
0messages
0tokens
$0.0000cost
0.0 stime

1 · What LangChain is

A family of packages, not one thing

LangChain is an framework for building apps on top of an . It is made by LangChain Inc., uses the permissive , and comes in two languages: Python and JavaScript/TypeScript. When engineers install it, they install several , each with its own job.

Your research assistant app LANGCHAIN Agents and helpers create_agent · middleware · init_chat_model LANGGRAPH The runtime underneath state · checkpoints · loops LANGCHAIN-CORE Shared building blocks every other package uses messages · chat models · prompts · tools · retrievers · the | pipe langchain-openai langchain-anthropic …one perprovider langchain-community langchain-classic legacy, since 1.0
Most code imports from langchain and one or two provider packages. Everything rests on langchain-core. Agents made with langchain run on LangGraph, which Module 4 covers.
langchain-core

The shared base: the standard shapes for messages, models, prompts and tools. It's small and changes slowly, so other packages can depend on it.

langchain

The main package. Since version 1.0 it is deliberately slim and centres on agents: create_agent, middleware and helpers like init_chat_model.

Provider packages

One per , such as langchain-openai or langchain-anthropic. Each is released separately, so a provider's new feature doesn't wait for a LangChain release.

langchain-community

Hundreds of written and maintained by the community: document loaders, databases, search tools. Quality and upkeep vary from one to the next.

langchain-classic

Where older features moved in 1.0, such as the old chain classes and some retrievers. It keeps old code running, but new code shouldn't start there.

Same company

LangChain Inc. also makes LangGraph (open source, the agent runtime) and LangSmith (a paid service for tracing and evals, an alternative to Langfuse).

Version note. LangChain 1.0 came out in October 2025 with a promise of no breaking changes until 2.0. Many tutorials still show older code (versions 0.1 to 0.3), with names that now live in langchain-classic or no longer exist. The names on this page match the LangChain docs as of October 2026. Check which version your team uses.
PM takeaway

When engineers say "we use LangChain", ask which packages and which version. The answer shows how modern the code is and how hard upgrades will be.

2 · Models

One way to talk to every provider

Each model provider has its own , with different names for the same ideas. LangChain wraps them all in one interface. Your code talks to the wrapper, and the provider package translates.

One interface

init_chat_model("provider:model") creates a chat model from a name. Every model then has the same methods: invoke for one full reply, stream for a reply that arrives bit by bit, and batch for many questions at once.

In the demo, switching providers changes only this one line.

Chat messages

Models read and write a list of . Each message has a role. LangChain uses the same four types for every provider.

Streaming

shows the answer word by word as the model writes it. The total time is the same, but the user sees progress after about a second instead of a blank screen for several.

Structured output

with_structured_output(Report) asks the model to reply in your exact and returns a ready-to-use object. This is : data your app can check and display, not a paragraph to pick apart.

The four message types

SystemMessage The rules
Instructions from your team: "You are a research assistant. Cite every claim."
HumanMessage The user's turn
"Is remote work better for productivity?"
AIMessage The model's turn
Either an answer, or a request to use a tool (Section 4).
ToolMessage A tool's result
What came back when your code ran the tool, such as a list of search results.

What it looks like in code

# Simplified. LangChain 1.x; check names against your version.
from langchain.chat_models import init_chat_model

model = init_chat_model("provider-a:large")   # real names look like "openai:…" or "anthropic:…"

reply = model.invoke("What is RAG in AI?")        # one full AIMessage
for chunk in model.stream("What is RAG in AI?"):  # the same reply, piece by piece
    print(chunk.text, end="")

class Report(BaseModel):                         # the shape we want back
    title: str
    paragraphs: list[str]
    citations: list[str]

writer = model.with_structured_output(Report)
report = writer.invoke(messages)                 # a Report object, not free text

Each call costs , and providers price them differently. One interface makes it cheap to try a smaller model on a step and measure the difference.

PM takeaway

One interface makes switching providers cheap to try. Whenever the app needs data rather than prose, ask for structured output.

3 · Prompts

Templates with blanks to fill in

A is a prompt written once, with named blanks like {question}. Each run fills the blanks with that run's data. The result is a list of chat messages ready for the model.

Templates

Keep the wording in one place. Changing the rules for every report means editing one template, not hunting through code.

Messages

ChatPromptTemplate builds a system message and a human message. A placeholder can slot in earlier conversation, when there is one.

Few-shot examples

are sample questions with ideal answers, placed in the prompt. Showing one good report often works better than describing one.

The research assistant's write prompt

# Simplified.
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a research assistant. Use only the notes. Cite each claim as [n]."),
    ("human",  "Question: {question}\nNotes:\n{notes}"),
])
messages = prompt.invoke({"question": q, "notes": notes})   # blanks filled, no AI involved yet
Without an example
System: Write a short report with citations.

The model guesses what "short" and "citations" mean. Length and format drift between runs.

With one example (few-shot)
Example question: How do index funds work?
Example report: An index fund copies a market
index, such as the Nifty 50 [1]. …
Now answer: {question}

The model copies the length, tone and citation style it was shown. It costs a few hundred extra tokens per run.

Where should prompts live? In the code, a prompt change needs a code release. Many teams store prompts in a prompt manager, such as Langfuse (Module 5) or LangSmith, so they can version them, compare them and roll back without a release.
PM takeaway

Prompts are product copy that the model reads. Version them, review them and test them like any other copy.

4 · Tools and tool calling

How the model asks your code to act

A model can only write text. A is a function in your code that the model is allowed to request, such as web_search. The model never runs it. It writes a , your code runs the tool, and the result goes back to the model.

Model decides what it needs Your code LangChain runs the loop web_search the actual tool 1 · tool call 2 · run it 3 · results 4 · ToolMessage 5 · Final answer no more tool calls, so the loop ends Steps 1 to 4 can repeat: search, read a page, search again. That repeating loop is an agent.
This is what Tab 2 of the demo steps through. The model sees each tool's name, description and inputs, and nothing else about it.

Defining a tool

# Simplified.
from langchain.tools import tool

@tool
def web_search(query: str) -> str:
    """Search the web for credible, recent sources.
    Use a specific query of 3 to 8 words. Returns titles and URLs."""
    ...

model_with_tools = model.bind_tools([web_search])
reply = model_with_tools.invoke("Is remote work better for productivity?")
reply.tool_calls   # [{"name": "web_search", "args": {"query": "remote work productivity research"}, "id": "call_01"}]

@tool turns a normal function into a tool. The function name becomes the tool's name, the type hints become its inputs, and the text in quotes, called a docstring, becomes the description the model reads.

Writing good tool descriptions

Vague
name: search
description: Searches.

The model can't tell when to use it, what to type, or what comes back. It may skip the tool or send a whole paragraph as the query.

Clear
name: web_search
description: Search the web for credible,
recent sources. Use a specific query of
3 to 8 words. Returns titles and URLs.

It says when to use it, how to phrase the input and what to expect. The model makes better calls.

When a tool fails

Tell the model

Catch the error and send it back as a ToolMessage, such as "timed out, try a narrower query". The model can then adjust instead of the run crashing.

Retry, with a limit

Retry briefly for short outages. Cap the number of tool calls, or one bad day for the search provider can burn through budget.

Fail honestly

If the tool keeps failing, the assistant should say it couldn't search, not invent an answer from memory.

With agents, ready-made middleware (Section 8) can catch tool errors, retry and enforce limits. Exact class names vary by version.

Tools from MCP servers

You don't have to write every tool yourself. Many services publish their tools as an server: search engines, Google Drive, Slack, GitHub, databases. An adapter package turns those tools into ordinary LangChain tools, so the agent uses them like any other.

# Simplified. The adapter lives in the langchain-mcp-adapters package.
from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({
    "search": {"transport": "streamable_http", "url": "https://mcp.search.example/mcp"},
    "library": {"transport": "stdio", "command": "python", "args": ["library_server.py"]},
})
tools = await client.get_tools()          # every tool the servers offer, as LangChain tools
agent = create_agent(model, tools=tools)
Why teams use it

A tool written once works in any MCP-compatible app or framework. Integrations the service owner maintains can be better kept up than community ones.

What to check

An MCP server is code you're trusting, and its tool descriptions go straight to the model. Use servers you've reviewed, and pass the agent only the tools it needs, not everything a server offers.

PM takeaway

The model knows a tool only by its name and description. Treat those words as a spec, and decide up front what happens when the tool fails.

Check your understanding · Tools

Engineers want to connect a CRM's MCP server to the research assistant. The server offers 12 tools, including delete_client. What should you ask for?

5 · Retrieval

Letting the model read your documents

Web search finds public pages. To answer from your own documents, such as the team's library of past research reports, you need retrieval. Retrieval plus generation is : find the most relevant passages first, then have the model answer using only those.

AHEAD OF TIME · INDEXING LoadPDFs, pages, wikis Splitinto ~1,000-character chunks Embedtext → list of numbers Storevector store AT QUESTION TIME Questionembedded the same way Retrieveclosest 4 chunks Promptquestion + chunks Answerwith citations search by meaning If the retrieve step picks the wrong chunks, even a perfect model gives a wrong answer.
Indexing happens once, and again whenever documents change. Retrieval happens on every question.
Loaders

A reads a source, such as a PDF, a web page or a wiki, into plain text plus details like the title and page number.

Splitters and chunks

A text splitter cuts long documents into . Too big, and the prompt fills with noise. Too small, and a chunk loses its context.

Embeddings

An turns text into numbers that capture its meaning, so "staff turnover" can match "resignations".

Vector stores

A is a database that saves embeddings and finds the closest ones fast. LangChain connects to many of them.

Retrievers

A is the standard "give me passages for this question" block. You can swap the store behind it without changing the rest.

Why bother

RAG lets the model answer from up-to-date, private sources and cite them. It reduces , but doesn't remove them.

What it looks like in code

# Simplified. Loaders and stores come from different packages, depending on the source.
docs      = PyPDFLoader("library/hybrid-work-trial.pdf").load()
chunks    = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150).split_documents(docs)
store     = InMemoryVectorStore.from_documents(chunks, embeddings)
retriever = store.as_retriever(search_kwargs={"k": 4})

passages  = retriever.invoke("Does hybrid work affect resignations?")   # the 4 closest chunks

Try it: tune the retriever

The question is "Does hybrid work affect resignations?", asked of a small company library. The right passage is in the hybrid-work trial report. Change the settings and watch what reaches the model.

Simulation.The library, scores and token counts are scripted to show typical behaviour. No search runs.
–right passage found
–useful chunks
–tokens sent to the model

    Making retrieval better

    Hybrid search

    combines keyword search, which is good at exact names and codes, with search by meaning, which finds synonyms. Each covers the other's blind spots.

    Rerankers

    A rereads the top 20 or so candidates and puts the truly relevant ones first. It adds a fraction of a second and a small cost, and often fixes the order.

    Metadata filters

    Filter before searching: only approved documents, only the last two years, only this client's files. It stops the wrong document winning on wording alone, and is often a permissions rule too.

    Chunk size and overlap

    Small chunks can cut a fact in half. Large ones bury it in noise and cost more tokens. Start around 500 to 1,000 characters with some overlap, then test on your own questions.

    Measure it

    Label 50 to 100 real questions with the passage that answers each. Then track the (right passage in the top k) separately from answer quality.

    Keep it fresh

    Re-index when documents change, and remove old versions. A perfect retriever over last year's policy still gives a wrong answer.

    Check your understanding · Retrieval

    Users say the policy assistant gives wrong answers about leave. What should you check first?

    PM takeaway

    When a RAG answer is wrong, first ask what was retrieved. Most problems start there, not in the model. Ask for the retrieval hit rate as its own number.

    6 · Chains

    Snapping blocks together with |

    Every block in langchain-core (prompts, models, parsers, retrievers) is a : it takes an input and returns an output. The | symbol, read as "then", joins runnables into a . This style is called .

    Sequence a | b | c prompt model parser Parallel RunnableParallel(a=…, b=…) notes summary key quotes combined Fallback a.with_fallbacks([b]) model A: error model B: works only if A fails The user never sees the error.
    Sequence is the demo's write chain. Parallel saves time when two steps don't depend on each other. A fallback is a backup plan for when a block fails.

    What it looks like in code

    # Simplified.
    write_report = prompt | model | parser                        # one after another
    
    extract = RunnableParallel(summary=summarise, quotes=find_quotes)  # both at the same time
    
    safe_model = model_a.with_fallbacks([model_b])                # if A fails, try B
    safe_chain = write_report.with_fallbacks([fix_and_retry])     # the demo's "bad output format" fix

    Running two steps in cuts : two 2-second steps take about 2 seconds instead of 4.

    Chains in LangChain 1.0. The | pipe still lives in langchain-core and suits fixed, predictable pipelines like our write step. The old ready-made chain classes, such as LLMChain, moved to langchain-classic. For anything that loops or decides its own next step, the docs now point to agents (Section 8) or LangGraph (Module 4).
    PM takeaway

    Chains suit fixed, predictable steps. Fallbacks and parallel steps are cheap wins for reliability and speed.

    7 · Memory today

    Memory now means saved state

    A model remembers nothing between calls. "Memory" means your code saves the conversation and sends the relevant parts back each time. How LangChain does this has changed, so older tutorials can mislead.

    Before 1.0: legacy

    Memory classes

    Classes like ConversationBufferMemory kept chat history inside a chain. They were deprecated before 1.0, didn't work well with tool calling, and now live only in langchain-classic.

    Now

    State plus a checkpointer

    An agent's , mainly its message list, is saved by a LangGraph after every step. Each conversation has a ID. Same ID, same memory.

    Short-term

    Memory within one conversation. "Make it shorter" works because the earlier report is still in the thread.

    Long-term

    Facts kept across conversations, such as "this user prefers UK sources". It is stored separately and deliberately, not by saving every chat.

    Keeping it small

    Long threads overflow the model's and cost more on every turn. Summarisation middleware condenses older messages.

    What it looks like in code

    # Simplified. In development, memory is saved in RAM; in production, in a database such as Postgres.
    agent = create_agent(model, tools=[web_search], checkpointer=InMemorySaver())
    
    config = {"configurable": {"thread_id": "user-42-chat-7"}}
    agent.invoke({"messages": [{"role": "user", "content": "What is RAG in AI?"}]}, config)
    agent.invoke({"messages": [{"role": "user", "content": "Shorter, please."}]}, config)  # remembers the first question

    Deciding what it remembers

    Long-term memory isn't saved in the checkpointer. In LangGraph it lives in a separate , organised by user, and your code decides what goes in. Make these decisions before engineers build it.

    DecisionOptionsResearch assistant v2
    What is remembered?Nothing · preferences · facts about the user · full chat historyPreferences only: report length, favourite sources, regions to focus on
    Who decides to save it?The model, automatically · the user, explicitly · both, with confirmationThe model suggests ("Remember that you prefer UK sources?"), the user confirms
    How long is it kept?The session · a fixed period · until deletedUntil the user deletes it, reviewed yearly
    Who can see it?Only the user · their team · adminsOnly the user, plus admins for support
    Can users see and change it?No · view only · view, edit and deleteA "What I remember" page with edit and delete
    What is never stored?Rules you set up frontNo personal or client data, no passwords, nothing from confidential documents

    Privacy laws, such as India's DPDP Act, expect a clear purpose, consent and deletion on request. A memory feature that users can't see or erase is hard to defend.

    PM takeaway

    Memory is a product decision. Decide what the assistant remembers, for how long, and who can see it.

    8 · Agents

    create_agent and middleware

    An is the tool-calling loop from Section 4, repeated until the model stops asking for tools. In LangChain 1.0, create_agent builds that loop for you. It runs on LangGraph, so it gets saved state, streaming and human approval without extra work.

    start before_model Model after_model Tool calls?did the model ask for any yes wrap_tool_call Toolsweb_search, … results go back into the loop no end · answer middleware hook: your code runs here
    Middleware hooks are fixed points in the loop where your code can look at, change or block what happens. There are also hooks at the very start and end of a run, and one that wraps each model call.

    Middleware, explained simply

    works like checkpoints at an airport. Passengers (messages and tool calls) move through the same route every time, and at each checkpoint a rule can inspect them, change them or stop them. You add a rule without rebuilding the airport.

    Limits

    Call-limit middleware caps model calls and tool calls per run, so a confused agent can't loop forever.

    Human approval

    Human-in-the-loop middleware pauses before chosen tools run, such as "send email", until a person approves, edits or rejects.

    Privacy

    PII middleware finds , like emails or card numbers, and masks or blocks it before it reaches the model.

    Long conversations

    Summarisation middleware condenses older messages when the history gets long.

    Resilience

    Retry and fallback middleware re-runs failed model or tool calls, or switches to a backup model.

    Your own rules

    Engineers can write custom middleware for the same hooks, for example to log every tool call or block certain topics.

    The research assistant as an agent

    # Simplified. Middleware names and options vary by version; check the docs.
    from langchain.agents import create_agent
    
    agent = create_agent(
        model="provider-a:large",
        tools=[web_search, fetch_page],
        system_prompt="You are a research assistant. Cite every claim as [n].",
        response_format=Report,                       # structured final answer (Section 2)
        middleware=[
            ModelCallLimitMiddleware(run_limit=12),   # stop runaway loops (module 1's cap: about 12 model calls)
            SummarizationMiddleware(...),             # shrink long histories
            HumanInTheLoopMiddleware(...),            # approve chosen tool calls
        ],
    )
    result = agent.invoke({"messages": [{"role": "user", "content": question}]})

    create_agent replaced an older LangGraph helper, create_react_agent. If your flow needs fixed steps, branches or custom checks, such as module 2's grade-and-rewrite loop, engineers build a custom graph in LangGraph instead (Module 4).

    PM takeaway

    create_agent gives you a working agent loop in a few lines. Middleware is where the guardrails go: limits, approvals and privacy.

    Check your understanding · Agents

    You want every research run stopped after 12 model calls, and a person to approve before anything is emailed. Where do these rules go?

    9 · Strengths, weak spots, alternatives

    When LangChain helps, and when it gets in the way

    LangChain is popular, but it isn't the only way to build. Use this section to ask better questions, not to pick a winner.

    Strengths

    • One interface for many providers, so switching or mixing models is cheap
    • A huge catalogue of ready-made integrations: loaders, vector stores, tools
    • Standard building blocks that many engineers already know
    • Agents run on LangGraph, so you can grow into custom flows without a rewrite
    • Since 1.0, a promise of no breaking changes until 2.0

    Weak spots

    • Layers of : when something breaks, the error can be hard to trace
    • A history of big changes, so many tutorials and answers online are out of date
    • Community integrations vary in quality and upkeep
    • Overkill for one or two model calls
    • More to keep updated and secure

    Alternatives your engineers may mention

    OptionBest when…Trade-off
    The provider's own You use one provider, the flow is simple, and you want full control and the newest features on day one.You write your own glue code. Switching providers later means rewriting the calls.
    LlamaIndexThe product is mostly about your documents: parsing, indexing and RAG over many sources.Different building blocks to learn. It overlaps with LangChain, and some teams use both.
    Other agent SDKsYou want a lighter agent toolkit, often from a model provider, such as the OpenAI Agents SDK, the Claude Agent SDK or Google's ADK.Often tuned for one provider's models and features. Fewer ready-made integrations.
    LangGraph on its ownYou need full control of the flow: branches, loops, approvals, long-running jobs.More to design up front. Many teams still use LangChain models and tools inside it.

    This space changes quickly. Treat these as examples, not a ranking, and check each project's current docs.

    Questions to ask your engineering team

    Do we need it?

    • How many model calls and providers does this feature really need?
    • Which LangChain parts are we using, and could we do without them?

    Will it last?

    • Which version are we on? Is any code still on langchain-classic?
    • Which community integrations do we rely on, and who maintains them?
    PM takeaway

    LangChain is a good default when you need many providers or integrations. For one provider and a simple flow, the provider's own SDK may be simpler.

    Keep this

    One-page cheat sheet

    What it isAn open-source family of packages with standard building blocks for LLM apps. Python and JavaScript, MIT licence.
    What runs underneathAgents built with create_agent run on LangGraph, which saves state and handles loops.
    What it doesn't doShow you what happened in production. For that you need tracing, such as Langfuse or LangSmith.

    The building blocks

    • Models: one interface, chat messages, streaming, structured output
    • Prompts: templates with blanks, few-shot examples
    • Tools: functions the model can request, written by you or loaded from MCP servers
    • Retrieval: load, split, embed, store, retrieve; improve with hybrid search, rerankers and filters
    • Chains: fixed steps joined with |
    • Memory: state saved by a checkpointer, per thread
    • Agents: the tool loop, plus middleware

    Names you'll hear in code

    • init_chat_model: make a model from a name
    • with_structured_output: reply in my schema
    • @tool, bind_tools: define and attach tools
    • ChatPromptTemplate: a prompt with blanks
    • with_fallbacks: a backup if a block fails
    • MultiServerMCPClient: load tools from MCP servers
    • create_agent, middleware: the agent loop and its guardrails
    • langchain-classic: legacy code from before 1.0

    PM takeaways

    1. Ask which packages and which version.
    2. Ask for structured output when the app needs data.
    3. Prompts are product copy: version and test them.
    4. Tool descriptions are specs. Plan for tool failure.
    5. For a wrong RAG answer, check retrieval first.
    6. Fallbacks and parallel steps are cheap wins.
    7. Memory is a product decision.
    8. Put guardrails in middleware.
    9. For one provider and a simple flow, the SDK may be enough.

    Numbers to ask for

    • Cost per run, and per step, for each model
    • How often parsing fails and a fallback runs
    • Tool error rate, and retries per run
    • Model calls per agent run, and the cap
    • Retrieval quality: how often the right passage is in the top 4

    Glossary

    Learn from others

    3 common mistakes with LangChain

    1

    Building from outdated tutorials

    What it looks like
    New code uses LLMChain, ConversationBufferMemory or other pre-1.0 names copied from old blog posts. It works for now, but sits on legacy code that new features will skip.
    Fix
    Start from the current docs. Agree on a version, pin it, and plan a time to move any langchain-classic code to create_agent and checkpointers.
    2

    Vague tools with no limits

    What it looks like
    Tools have one-word descriptions, errors crash the run, and nothing caps the number of calls. The agent picks the wrong tool, or loops on a failing one.
    Fix
    Write each description as a spec. Send errors back to the model as ToolMessages, and set call limits with middleware. Test the failure path, as in Tab 2 of the demo.
    3

    Trusting free-text output

    What it looks like
    The app asks for "JSON, please" and reads the reply with hand-written parsing. One chatty reply or missing comma breaks the screen for a user.
    Fix
    Use with_structured_output with a schema, add a fallback or retry for the rare failure, and track how often it happens, as in Tab 1 of the demo.
    Next · Module 4 LangGraph in depth →