↑ Contents Chapter 8 of 12

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.

Two paths to learning agents Start with LangChain agent = create_agent(...) agent.invoke("...") It works! But why? When it breaks, you're stuck. Build by hand first Write the loop yourself Then meet the framework You know what each piece does. When it breaks, you can fix it.
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-openai from 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 def calculate(expression: str) -> str: """Evaluate a math expression. Use for any arithmetic, e.g. '17 * 24'.""" try: return str(eval(expression)) except Exception as e: return f"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 AgentExecutor is the loop from Chapter 3. The @tool decorator is the tool definition from Chapter 4. The ChatPromptTemplate is the messages list from Chapter 2. You already know what each piece does.

Hand-rolled vs LangChain β€” same pieces, less boilerplate Hand-rolled (Ch 3) - messages = [...] - for turn in range(max_turns): - response = client.create(...) - if not msg.tool_calls: return - messages.append(msg) - for tool_call in ...: run - messages.append(result) ~40 lines, all visible LangChain (Ch 8) model = ChatOpenAI(...) @tool def calculate(...) prompt = ChatPromptTemplate(...) agent = create_tool_calling_agent(...) executor = AgentExecutor(...) result = executor.invoke(...) ~10 lines, same behaviour
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 call from 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)

Use LangChain when... You're building a production agent You need multiple tools, memory, RAG You want to swap components easily You're prototyping and want speed You need integrations (many LLMs, vector DBs) Skip it when... The agent is simple (one tool, no memory) You need full control over the loop The framework's abstractions get in the way You're learning (build by hand first!) Minimal dependencies matter
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.

← Previous Next β†’