Chapter 8: LangChain β The Wiring That Holds It Together
TomΓ‘s has been building agents for a few weeks now. He's got the loop, the
tools, the memory, the planning, the RAG. It works. But his code is
getting... hairy. Every new agent he builds, he copies the loop, rewrites
the tool dispatch, re-implements the message trimming, rewrites the
retrieval. He's written the same agent loop four times.
"There's got to be a better way," he tells Priya.
"There is," she says. "It's called LangChain. It's a framework that
packages up the patterns you've been building by hand β the loop, the
tools, the memory, the retrieval β into reusable pieces. You've already
learned what each piece does. Now you'll see what they look like when
someone else has written the boilerplate."
TomΓ‘s squints. "So I didn't need to build it all by hand?"
Priya grins. "You did need to. You wouldn't understand LangChain
if you hadn't. Now you will."
Why This Matters
By the end of this chapter, you'll rebuild the agent you've been building
by hand β the loop, the tools, the memory β using LangChain. You'll see
how the framework turns the patterns you already know into composable
components, and you'll understand why each piece exists because
you built it yourself first. You'll also know when to use LangChain and
when to stick with hand-rolled code β because the framework isn't always
the right answer.
Why We Waited Until Now
You could have started this book with LangChain. Many agent tutorials do.
Here's why we didn't: LangChain hides the loop. It
wraps the perceive-think-act-observe cycle in abstractions that, if you
don't already understand them, feel like magic. And when the magic
breaks β and it will β you won't know why.
You built the engine by hand. Now LangChain gives you a nicer car β but you know how the engine works.
You've spent seven chapters building the engine. Now you get to drive a
car someone else built. But when it makes a weird noise, you'll know
what's happening under the hood.
What LangChain Gives You
LangChain packages the patterns you've built by hand into reusable
components. Here's the mapping:
# What you built by hand β What LangChain calls it
The agent loop (Ch 3) β AgentExecutor / create_agent
Tools (Ch 4) β @tool decorator + Tool objects
Memory / messages list (Ch 5) β Memory classes (ConversationBufferMemory, etc.)
The LLM call (Ch 2) β ChatOpenAI / ChatModel wrapper
RAG retrieval (Ch 7) β VectorStore + Retriever
Planning prompt (Ch 6) β Agent type (ReAct, etc.)
// Each thing you built by hand has a LangChain equivalent. Same ideas, packaged up.
Rebuilding the Calculator Agent in LangChain
Let's rebuild the agent from Chapter 3 β the one with the calculator tool
β using LangChain. You'll see the same pieces, but with less boilerplate.
Step 1: The model
# pip install langchain langchain-openaifrom langchain_openai import ChatOpenAI
# Same LLM, wrapped in a LangChain class
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
// ChatOpenAI wraps the same API call from Chapter 2. Same model, same temperature, same behaviour.
Step 2: The tool
from langchain_core.tools import tool
# The @tool decorator turns a function into a LangChain tool# It reads the docstring as the description β just like we wrote by hand!
@tool
defcalculate(expression: str) -> str:
"""Evaluate a math expression. Use for any arithmetic, e.g. '17 * 24'."""try:
returnstr(eval(expression))
exceptExceptionas e:
returnf"Error: {e}"
tools = [calculate]
// The @tool decorator reads the docstring as the description. Same idea as Chapter 4 β the description tells the LLM when to use it.
Note
See what just happened? The docstring is the tool description.
LangChain reads it automatically. This is the same principle from
Chapter 4 β the description tells the LLM when to use the tool β but
the framework handles the JSON schema generation for you. You write the
function and a good docstring; LangChain builds the tool definition.
Step 3: The agent
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
# The system prompt β same ReAct ideas from Chapter 6
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant. Use the calculate tool for any math. Think step by step."),
("user", "{input}"),
("placeholder", "{agent_scratchpad}"), # where tool calls go
])
# Create the agent β this wraps the loop from Chapter 3
agent = create_tool_calling_agent(model, tools, prompt)
# The executor runs the loop β with max_turns built in!
agent_executor = AgentExecutor(agent=agent, tools=tools, max_iterations=10)
// create_tool_calling_agent wraps the loop. AgentExecutor runs it. max_iterations is our max_turns from Chapter 3.
Step 4: Run it
# That's it. Run the agent.
result = agent_executor.invoke({"input": "What's 17 times 24, then add 100?"})
print(result["output"])
# "17 Γ 24 = 408, plus 100 = 508."
// One line to run. The loop, the tool dispatch, the message handling β all inside the executor.
Compare that to the 40 lines of hand-rolled loop from Chapter 3. Same
behaviour, same agent, same tools β but the framework handles the
boilerplate. The AgentExecutoris the loop from
Chapter 3. The @tool decorator is the tool
definition from Chapter 4. The ChatPromptTemplateis
the messages list from Chapter 2. You already know what each piece does.
Same agent. The framework handles the loop, the dispatch, the message threading.
Adding Memory in LangChain
In Chapter 5, we built memory by managing the messages list ourselves.
LangChain has memory built in. Here's how to add conversation memory:
from langchain.memory import ConversationBufferMemory
# LangChain manages the messages list for you
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
# The prompt now includes a place for history
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
("placeholder", "{chat_history}"), # β memory goes here
("user", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
# Rebuild the agent with the new prompt β it must include the {chat_history} slot
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(
agent=agent, tools=tools, memory=memory, max_iterations=10
)
// Memory is a component you plug in. ConversationBufferMemory is the messages list from Chapter 5, managed for you.
Now the agent remembers the conversation across calls β same behaviour
as our hand-rolled version, but you didn't have to write the message
threading. LangChain also offers
ConversationSummaryMemory (the summarisation strategy from
Chapter 5) and others, so you can swap memory strategies without
rewriting your code.
Adding RAG in LangChain
In Chapter 7, we built RAG with a list and numpy. LangChain has vector
stores and retrievers built in. Here's the same RAG, packaged:
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# The embedding model (same as Chapter 7, wrapped)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# The vector store (Chroma handles storage + search)
vector_store = Chroma.from_texts(
texts=[
"Returns: Items may be refunded within 30 days with receipt.",
"Shipping: Orders ship within 2 business days.",
"Membership: Members get 10% off and early access to sales.",
],
embedding=embeddings,
)
# A retriever β this is the search_documents tool from Chapter 7
retriever = vector_store.as_retriever(search_kwargs={"k": 2})
# Turn it into a tool the agent can callfrom langchain.tools import create_retriever_tool
search_tool = create_retriever_tool(
retriever,
"search_documents",
"Search the shop's policy documents. Use for questions about returns, shipping, or membership.",
)
// The retriever is the search_documents tool from Chapter 7. LangChain packages the whole pipeline.
Same RAG pipeline from Chapter 7 β chunk, embed, store, retrieve β but
the framework handles the storage and similarity search. You provide the
texts and the embedding model; Chroma handles the rest. And
create_retriever_tool wraps it as a tool the agent can call,
just like any other.
Note
Notice the pattern: every LangChain component is a wrapper around
something you already know. ChatOpenAI wraps the LLM
call. @tool wraps the tool definition.
ConversationBufferMemory wraps the messages list.
Chroma wraps the vector store. The framework doesn't add
new concepts β it packages the ones you've already built.
When to Use LangChain (and When Not To)
LangChain is a tool, not a religion. Use it when it helps; skip it when it doesn't.
Watch it!
LangChain's biggest weakness is its abstraction layers. When something
breaks β and it will β you're debugging through several layers of
framework code. If you don't understand what's underneath (the loop, the
tools, the memory), you'll be lost. That's why we built it by hand
first. Use the framework, but know what it's doing under the
hood.
There Are No Dumb Questions
Q: Is LangChain the only framework? What about LangGraph, CrewAI, AutoGen?
A: There are several. LangChain is the most established and the one we focus on here. LangGraph (from the same team) is for more complex, stateful agent workflows β we'll touch it in Chapter 9. CrewAI and AutoGen are popular for multi-agent systems. The concepts you've learned β the loop, tools, memory, RAG β are universal. Frameworks differ in syntax and philosophy, but the fundamentals are the same.
Q: LangChain changes a lot between versions. Will this code be out of date?
A: LangChain's API does evolve, and that's a real pain point. The concepts here β create_tool_calling_agent, AgentExecutor, @tool, ChatPromptTemplate β are stable as of writing, but check the current docs. The good news: because you understand the underlying patterns, you can adapt to any API change. The framework changes; the loop doesn't.
Q: Should I always use LangChain for production agents?
A: Not always. For simple agents, the hand-rolled loop is clearer and has fewer dependencies. For complex agents with many tools, memory, and RAG, LangChain saves you writing boilerplate. The choice depends on your needs. Some teams use LangChain for prototyping and hand-roll the production version for control. There's no wrong answer β just know what the framework does so you can decide.
Where People Come Unstuck
Mistake #1: Using LangChain without understanding the loop
If you start with the framework, AgentExecutor.invoke() feels
like magic. When it loops forever, or calls the wrong tool, or loses
memory, you have no idea why. You're debugging a black box. The fix:
you've already done it β you built the loop by hand. Now you can peek
inside the framework and see what it's doing.
Mistake #2: Fighting the framework
LangChain has opinions about how things fit together. If you try to use
it like raw Python β managing your own messages list inside an
AgentExecutor, for example β you'll fight it. Either use the framework's
patterns (its memory classes, its prompt templates) or don't use it.
Mixing hand-rolled and framework code in the same agent gets messy.
Mistake #3: Assuming the framework fixes everything
LangChain handles the boilerplate. It doesn't fix bad tool descriptions,
vague prompts, or poor chunking. The quality of your agent still depends
on the fundamentals from Chapters 2β7. The framework makes it easier to
assemble the pieces; it doesn't make the pieces good.
Brain Power
Think about the bookshop agent you've been building β the one with
search_books, calculate, memory, and RAG.
If you rebuilt it in LangChain, which parts would be easier? Which
parts would you keep hand-rolled? Would you use LangChain's memory
classes or manage messages yourself? Would you use
create_retriever_tool or your own
search_documents? There's no right answer β the point is
to start thinking in trade-offs: framework convenience vs
hand-rolled control.
Chapter Summary
LangChain packages the patterns you built by hand β the loop, tools, memory, RAG β into reusable components.
We waited until Chapter 8 because the framework hides the loop. Understanding it first means you can debug the framework when it breaks.
ChatOpenAI wraps the LLM call. @tool wraps tool definitions (the docstring is the description). AgentExecutor wraps the loop. ConversationBufferMemory wraps the messages list. Chroma + retrievers wrap RAG.
Each LangChain component is a wrapper around something you already know. The framework doesn't add new concepts β it packages the ones you've built.
Use LangChain for production agents with many components. Skip it for simple agents or when you need full control over the loop.
The framework handles boilerplate, but it doesn't fix bad prompts, vague tool descriptions, or poor chunking. The fundamentals from Chapters 2β7 still determine your agent's quality.
Chapter Challenge
Rebuild the Bookshop Agent in LangChain. Take the
agent you've built across Chapters 3β7 β with
search_books, calculate, memory, and RAG β
and rebuild it using LangChain components.
1. Use ChatOpenAI for the model.
2. Use @tool for search_books and
calculate.
3. Use ConversationBufferMemory for memory.
4. Use Chroma + create_retriever_tool for
the policy RAG.
5. Use create_tool_calling_agent + AgentExecutor
to run it.
Run the same conversations you ran with the hand-rolled version. The
behaviour should be identical β but the code is shorter. That's the
framework doing its job: same agent, less boilerplate. And when
something behaves oddly, you know what's underneath, because you built
it yourself first.