LangChain is an open-source framework that gives you a common application layer — models, tools, memory, and chains — so you can build LLM-powered apps…
You trace the exact friction of calling an LLM API directly — repeated boilerplate, no memory, no tool wiring — and see why LangChain's common application layer solves it. The running example is a customer-support bot that needs to remember context and call a database tool.
This module shows why calling an LLM API directly creates repeated boilerplate and no memory or tool support — and how LangChain fixes both.
Why this matters: Before writing a single line of LangChain code, you need to see the exact pain it solves so you know when it earns its place.
Decision this forces: Should I use LangChain or call the provider API directly?
Your customer-support bot needs to remember the last message and look up an order. A raw (Large Language Model) API call does neither — you wire every piece by hand, every time.
Two problems appear immediately.
is an open-source framework that adds a common application layer on top of any LLM API. It handles the boilerplate, memory, and tool wiring so you don't have to.
import openai client = openai.OpenAI(api_key="sk-...") response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Where is my order #42?"}] ) print(response.choices[0].message.content)
This is the simplest possible LLM call — one question, one answer. Predict what happens on the second turn before revealing.
The model answers about order #99 but has NO memory of order #42. Each call is stateless — the conversation history is gone. There is also no way to query a real database; the model can only guess the order status from its training data.
Imagine your customer-support bot handles this exchange:
With a raw API call, turn 2 fails — the model never saw turn 1.
LangChain adds three things to fix this:
You write the tool once. LangChain handles when and how the model calls it.
from langchain_openai import ChatOpenAI from langchain.memory import ConversationBufferMemory from langchain.agents import initialize_agent, Tool def lookup_order(order_id: str) -> str: return f"Order {order_id}: shipped, arrives Friday." tools = [Tool(name="OrderLookup", func=lookup_order, description="Look up an order by ID.")] memory = ConversationBufferMemory() llm = ChatOpenAI(model="gpt-4o") agent = initialize_agent(tools, llm, memory=memory)
This wires memory and a tool into the bot in ~10 lines. Predict what the agent replies to the second turn before revealing.
Turn 1: the agent calls lookup_order("42") and returns the real status. Turn 2: memory replays the full conversation, so the model knows about the earlier refund mention — no context is lost. The raw call would have forgotten everything.
from langchain_openai import ChatOpenAI from langchain.memory import ConversationBufferMemory from langchain.agents import initialize_agent, Tool def lookup_refund(order_id: str) -> str: return f"Refund for order {order_id}: approved, 3-5 days." # TODO: create a Tool named "RefundLookup" using lookup_refund # Hint 1: copy the Tool(...) pattern from Stage 2. # Hint 2: the description should tell the model WHEN to call it. tools = [???] memory = ConversationBufferMemory() agent = initialize_agent(tools, ChatOpenAI(model="gpt-4o"), memory=memory)
Stop — attempt the TODO before revealing. The gap is the exact skill this module teaches: declaring a tool so LangChain can wire it.
tools = [Tool(name="RefundLookup", func=lookup_refund, description="Look up refund status for an order by ID.")]
# Changed lines vs Stage 2:
# - name → "RefundLookup" (matches the new function's purpose)
# - func → lookup_refund (the new function)
# - description → tells the model when to call it
# Everything else stays the same — that's the point of LangChain's Tool abstraction.
Three failure patterns to watch for:
context_length_exceeded error. Fix: switch to a summarizing memory type.description is unclear, the model calls the wrong tool or skips it. Fix: write the description as a one-sentence instruction.ImportError or AttributeError at startup. Fix: pin your version in requirements.txt.| Option | Memory needed | Tool / DB wiring | When to choose | Cost | Complexity |
|---|---|---|---|---|---|
| Direct Provider API | You manage it yourself | Manual every time | Single-turn queries with no memory or tools — e.g. a one-shot text classifier or summarizer. | Minimal overhead | Low |
| LangChain | Built-in memory types | Declare once, reuse everywhere | Multi-turn bots, agents that call tools, or any app needing reusable prompt pipelines. | Small abstraction overhead | Medium |
You get a fully worked tour of LangChain's four core abstractions, each shown in the customer-support bot scenario: a chain routes the question, a tool queries the database, memory stores the conversation, and an agent decides which tool to call next.
A tour of LangChain's four core abstractions — Chain, Agent, Memory, and Tool — shown together in a customer-support bot.
Why this matters: These four blocks are the vocabulary you need to build any LangChain application; every module after this builds on them.
Decision this forces: Which abstraction — chain, agent, memory, or tool — does my task need?
Answer: repeated boilerplate, no memory between turns, and no tool wiring. fixes all three.
This module covers four abstractions that solve those pains.
LangChain gives you four core abstractions: , , , and .
Each one solves exactly one of the raw-API pains from Module 1.
Here's how all four abstractions work in a customer-support bot.
Remove any one block and the bot breaks. No chain means no routing. No tool means no live data. No memory means forgotten context. No agent means no tool selection.
# Stage 1 — a chain: fixed steps, always in order prompt = build_prompt(template="Classify this question: {question}") model = get_llm(provider="openai", model="gpt-4o") chain = prompt | model # pipe two steps together result = chain.invoke({"question": "Status of order #4821?"}) print(result) # Output: "order_status"
A wires steps together with | — the output of one step feeds the next.
The chain always runs the same path: build prompt → call model → return label. No decisions, no branching.
It prints "order_status" — the model classified the question using the fixed prompt→model steps. The chain itself never decides; it just executes in order.
# Stage 2 — tool + agent on top of the chain def lookup_order(order_id: str) -> str: return db.query(f"SELECT status FROM orders WHERE id={order_id}") tools = [lookup_order] # register the function as a Tool agent = create_agent(llm=model, tools=tools) response = agent.invoke({"input": "Status of order #4821?"}) print(response) # Output: "Order #4821 shipped on June 3."
The wraps the model and the tool list. At runtime it decides: call lookup_order or answer directly?
The is just a plain Python function — LangChain makes it callable by the model.
It calls lookup_order — the model knows its training data is stale and that a tool exists for live order data. The agent loop: decide → call tool → read result → reply.
# Stage 3 — add Memory so the bot remembers earlier turns from langchain.memory import ConversationBufferMemory memory = ConversationBufferMemory() agent_with_memory = create_agent( llm=model, tools=tools, memory=memory # <-- TODO: what does passing memory here do? ) agent_with_memory.invoke({"input": "Status of order #4821?"}) agent_with_memory.invoke({"input": "When will it arrive?"}) # no order # needed
Stop — attempt this before revealing: what does passing memory=memory into create_agent actually change about the second invoke call?
Hints: (1) What does the second question lack that the first had? (2) Where does the agent find the missing context?
Passing memory= tells the agent to append every turn to ConversationBufferMemory and prepend that history to the next prompt. So when the second invoke runs, the context window already contains 'order #4821' from turn 1 — the agent doesn't need the customer to repeat it. CHANGED LINE: memory=memory is the crux; without it, the second question returns 'which order?' because history is lost.
These are the three most common wiring failures.
context_length_exceeded error. Fix: use a summarizing or windowed memory type.order_id="#4821" but the function expects an integer. Result: TypeError: invalid literal for int(). Add a docstring — the model reads it to know how to call the tool.You trace the full message flow through a LangChain Expression Language (LCEL) pipeline and through one agent loop iteration in the support-bot, filling in the missing step yourself (completion problem) before the worked answer is shown.
This module traces how data moves through an LCEL pipeline and one full agent loop iteration, then has you complete a working agent yourself.
Why this matters: Knowing the exact flow — prompt → model → tool → observe → decide — lets you debug your support bot and choose the right pattern for each task.
Decision this forces: Should I use a fixed LCEL chain or a dynamic agent loop for this task?
Answer: , , , and . This module shows how data moves through them step by step.
Driving question: when your support bot receives a customer message, what happens inside LangChain before it replies?
(LangChain Expression Language) is a way to chain steps together using the | (pipe) operator.
Each step is a — an object with one job: take input, return output.
prompt_template formats the user's message into a full prompt.llm sends that prompt to the model and gets text back.output_parser cleans the raw text into a usable string.The | operator wires them: output of step 1 becomes input of step 2, automatically.
from langchain.prompts import PromptTemplate from langchain.schema import StrOutputParser prompt = PromptTemplate.from_template( "You are a support bot. Answer this: {question}" ) llm = get_llm() # your model object from module 2 parser = StrOutputParser() chain = prompt | llm | parser
Three wired with | form a complete chain.
Calling chain.invoke({"question": "..."}) runs all three steps in order and returns a plain string.
A plain string — e.g. "Your order is being processed." — because StrOutputParser strips the Message wrapper and returns just the text content.
An doesn't run a fixed chain. It decides what to do at each step.
from langchain.agents import create_react_agent, AgentExecutor tools = [order_lookup_tool] # defined in module 2 agent = create_react_agent(llm, tools, prompt) executor = AgentExecutor(agent=agent, tools=tools) result = executor.invoke({"input": "Where is order #42?"}) # Loop: model picks order_lookup_tool → tool runs → result added # → model reads result → replies with final answer print(result["output"]) # "Order #42 shipped on Monday."
AgentExecutor runs the for you: model → tool → observe → decide, until done.
The comments trace the single iteration for "Where is order #42?" — one tool call, one observation, one final reply.
The agent raises a ValueError: "Tool 'order_lookup_tool' not found." The loop halts immediately — it cannot proceed without the tool definition. This is the missing-tool failure mode.
from langchain.agents import create_react_agent, AgentExecutor tools = [policy_lookup_tool] # new tool: looks up refund policy agent = create_react_agent(llm, tools, prompt) # TODO: create the AgentExecutor with a max of 5 iterations executor = ??? result = executor.invoke({"input": "What is the refund policy?"}) print(result["output"])
This is a small variation of Stage 2 — a new tool, same pattern.
Stop — attempt the TODO before revealing. Hint 1: use AgentExecutor(...). Hint 2: the guard against infinite loops is a keyword argument.
executor = AgentExecutor(agent=agent, tools=tools, max_iterations=5)
← Changed lines vs Stage 2:
Two failure modes hit beginners most often:
max_iterations=10 on AgentExecutor.ValueError: Tool 'X' not found at runtime. Fix: register every tool in the tools=[] list.max_iterations is set, (3) tools= list is not empty.| Option | Steps known up front? | Needs tool calls? | When to choose | Cost | Complexity |
|---|---|---|---|---|---|
| Fixed LCEL Chain | Yes — steps are fixed | No tools needed | Use when the steps are always the same — e.g. format prompt → call model → parse reply. | Low — one model call per invoke. | Low — just wire Runnables with |. |
| Agent Loop | No — model decides at runtime | Yes — one or more tools | Use when the task needs dynamic decisions — e.g. the bot must look up an order, then maybe check a policy, depending on the answer. | Higher — multiple model calls per task. | Medium — must register tools and handle loop termination. |
You map LangSmith (tracing and evaluation), LangServe (API deployment), and LangGraph (stateful graph agents) onto the support-bot, then apply a decision rule — revisiting the raw-API trade-off from Module 1 — to decide when LangChain is the right choice versus a lighter alternative.
Maps LangSmith, LangServe, and LangGraph onto a real support-bot and teaches you when to use each versus a lighter alternative.
Why this matters: Knowing which ecosystem tool to reach for stops you from over-engineering a simple app or under-tooling a complex one.
Decision this forces: Which part of the LangChain ecosystem do I actually need for my app?
Answer: every raw API call forces you to re-wire boilerplate — no memory, no tool wiring, no shared middleware. LangChain's common application layer removes that friction.
agent loop. Now the question is: which extra tools does your support-bot actually need to go to production?
You don't need all three. Pick only what your app's stage demands.
Your support-bot is live. Three problems appear on day one.
import os os.environ["LANGCHAIN_TRACING_V2"] = "true" os.environ["LANGCHAIN_API_KEY"] = "<your-key>" # Your existing support-bot chain runs unchanged. # LangSmith captures every step automatically. result = support_bot_chain.invoke({"question": "Where is my order?"}) print(result)
LangSmith tracing — no code changes to your chain. Every prompt, tool call, and reply now appears in your LangSmith dashboard.
The chain runs exactly as before — same output. But LangSmith silently records every intermediate step (prompt sent, tool called, model reply) to your dashboard. The caller sees no difference; the trace is a side-effect.
from langgraph.graph import StateGraph, END def check_order(state): return {"status": "shipped"} def verify_policy(state): return {"approved": state["status"] == "shipped"} def route(state): return END if state["approved"] else "escalate" def escalate(state): return {"reply": "Escalated to human agent."} graph = StateGraph(dict) graph.add_node("check", check_order) graph.add_node("policy", verify_policy) graph.add_node("escalate", escalate) graph.add_edge("check", "policy") graph.add_conditional_edges("policy", route) app = graph.compile()
Each node is a plain function that reads and updates shared state. routes between nodes based on the result — branching a simple chain can't do.
route() returns 'escalate' because approved is False. LangGraph follows that edge to the escalate node, which sets reply to 'Escalated to human agent.' The graph then ends.
from langgraph.graph import StateGraph, END def greet(state): return {"greeted": True} def check_faq(state): return {"found": state.get("question") == "hours"} def reply_faq(state): return {"reply": "We're open 9-5."} def reply_fallback(state): return {"reply": "Let me find a human."} def route_faq(state): # TODO: return "faq" if state["found"] else "fallback" pass graph = StateGraph(dict) graph.add_node("greet", greet) graph.add_node("check", check_faq) graph.add_node("faq", reply_faq) graph.add_node("fallback", reply_fallback) graph.add_edge("greet", "check") graph.add_conditional_edges("check", route_faq)
Stop — attempt this before revealing. Fill in route_faq so the graph routes correctly. Hints: (1) look at how route() worked in Stage 2; (2) both branches must eventually reach END — add those edges too.
# CHANGED LINE — this is the crux:
def route_faq(state):
return "faq" if state["found"] else "fallback"
# Also add terminal edges (required — graph won't compile without them):
graph.add_edge("faq", END)
graph.add_edge("fallback", END)
app = graph.compile()
# Why: conditional_edges needs a function that returns a node name string.
# Both leaf nodes must connect to END or the graph raises a compile error.
| Option | Needs memory / multi-step loops | Needs tracing & eval | When to choose | Cost | Complexity |
|---|---|---|---|---|---|
| Provider SDK directly | No built-in support | Manual only | Single-model call, no memory, no tools — thin and fast. | Lowest | Low |
| LangChain (LCEL chain) | Memory + tool wiring built in | LangSmith plug-in via env vars | Multi-step pipeline with tools and memory; needs middleware like tracing or retries. | Low | Medium |
| LangGraph | Full graph state + persistence | Works with LangSmith | Agent needs branching, cycles, human-in-the-loop, or recovery from failures. | Low | Higher |
LANGCHAIN_TRACING_V2 but forget LANGCHAIN_API_KEY. Every invoke silently skips the trace — your dashboard stays empty with no error thrown.END for some state. The graph spins forever, burning tokens, with no timeout by default.END, that node functions return a dict (not None), and that the compiled graph is called with .invoke() not .run() (which doesn't exist on compiled graphs).Before you scroll down: from memory, name LangChain's four core abstractions and the role each plays. Then name the three ecosystem tools and what each one does. Finally, state the one-sentence rule for when to use LangChain versus a provider SDK directly.
Apply what you learned to SEO/GEO page — the page title is the H1 and the exact search query. OPEN with a 40-60 word self-contained, quotable answer capsule that names the entity explicitly in sentence one (no "in this lesson…" preamble) — this is the sentence an LLM lifts. Name the entity by its full name every time (not pronoun-only). Structure each section as ONE sub-question (How it works · X vs Y · When to use it · Example · Common mistakes), each opening with its own 1-2 sentence direct answer then depth. End with a genuine 3-5 question FAQ phrased as real user questions ending in "?" (these become FAQPage schema + the knowledge check). Prefer quotable specifics — concrete numbers, defaults, version names, one short snippet — over vague prose. Self-contained for a reader who arrived cold from a search engine or an LLM. Intent = definition: define the named entity + how it works + a concrete example. Target query: "what is langchain"..
Which of the following best describes what LangChain is and the core problem it solves?
LangChain is a framework — it does not host or train models. Its job is to handle the repetitive plumbing (chaining calls, managing memory, routing to tools) that every LLM app needs. The prompt-rewriting and fine-tuning options describe things LangChain does not do at all.
You are building a one-shot text summariser: the user pastes an article, your app sends it to GPT-4o, and returns a three-sentence summary. No history, no external data. Should you use LangChain?
LangChain adds value when you need chaining, memory, or tools. A single-turn summariser needs none of those — a direct OpenAI SDK call is cleaner and has less overhead. LangChain supports GPT-4o fine, so that distractor is factually wrong. Memory is for conversation history, not for returning a result.
A customer-support bot must remember what the user said three messages ago and decide on its own whether to search a knowledge base or escalate to a human. Which TWO LangChain abstractions are doing the most work here?
(Pick the answer that names both correctly.)
Memory stores the conversation history so the bot recalls earlier messages. An Agent makes the runtime decision — search the knowledge base or escalate — rather than following a fixed sequence. A Chain follows a predetermined path and cannot make that dynamic routing decision on its own. Tools are what the agent calls (the search function, the escalation function), but the decision-maker is the agent itself.
Read this LCEL snippet:
chain = prompt | llm | output_parser
result = chain.invoke({"topic": "black holes"})
What does the pipe operator | do here?
In LCEL (LangChain Expression Language), the pipe operator chains components sequentially: the prompt formats the input, passes it to the llm, and the llm's output goes to the output_parser. There is no parallelism, no dynamic routing, and no caching implied by the pipe operator itself. Dynamic routing is the job of an Agent, not a pipe chain.
Name the three companion tools in the LangChain ecosystem (beyond the core library) and state what each one does in a single phrase.
LangSmith handles monitoring and debugging so you can see what your chain did and why. LangServe turns a finished chain into a production REST endpoint. LangGraph extends LangChain for complex multi-step or multi-agent flows that need explicit state and branching — something a simple chain or single agent cannot express cleanly.