AI · Automation · Engineering

Skip the Udemy LangChain Courses, Build This Instead

By Lazar MilicevicOctober 15, 20268 min read
Lines of code glowing on a dark monitor, representing hands-on LangChain project building over online courses

Most engineers I talk to who "learned LangChain" can build a chatbot demo and cannot tell me what happens when the process restarts mid-conversation. That gap is where production work lives, and a video course rarely gets you there because nothing in it breaks. So here is the project I'd hand to any engineer who wants real LangChain skills fast: a small agent that touches a real system, pauses for human approval, survives a crash, and returns typed output.

I should be upfront about the limits of this opinion. I haven't audited any Udemy catalog, so I won't claim specific courses are outdated. My claim is narrower and comes from my own experience: passive video builds recognition, not debugging instincts, and the second is what clients pay for.

The project: a refund-approval agent that survives restarts

Build an agent that triages inbound support requests, looks up account data through tools, drafts a structured decision, and requires a human to approve any money-moving action. It sounds small, but it forces you through every layer that matters in production: tool design, structured output, middleware, persistence, and failure handling.

I like support triage because it mirrors the shape of real work I've shipped, including a serverless integration between AWS and Zendesk. The domain is boring on purpose. You spend your attention on the agent mechanics, not on a clever idea.

Here is the scope I'd give you:

  1. One agent with three tools: lookup account, fetch order history, issue refund (stubbed).
  2. Typed decision output (approve, deny, escalate, plus a reason).
  3. Human approval gate on the refund tool.
  4. A persistent checkpointer so a paused run survives a process restart.
  5. PII redaction before anything reaches the model.
  6. A small test harness that replays failure cases.

Step 1: Start on the 1.0 surface, not the old tutorials

Use create_agent from the v1 line. LangChain and LangGraph 1.0 launched in an announcement dated October 22, 2025, and create_agent is now the standard way to build agents, replacing langgraph.prebuilt.create_react_agent (docs). Under the hood it runs the agent loop on LangGraph (LangChain blog).

This matters for learning because a lot of tutorial material predates 1.0. If a course has you wiring LLMChain or ConversationChain, know that those legacy chains, retrievers, the indexing API and the hub module moved to the langchain-classic package (migration guide). Learning them first is learning a layer you will mostly not ship on.

Two practical notes before you write a line:

  • All LangChain packages now require Python 3.10 or higher; 3.9 support was dropped (migration guide).
  • Custom state in create_agent supports only TypedDict. Pydantic models and dataclasses are no longer supported for state (migration guide). I hit this the hard way porting an older pattern, and the error does not point you at the cause quickly.

Check PyPI for the current version rather than trusting any number I'd quote. Pin it in your lockfile either way.

Step 2: Make the output typed, and understand where it comes from

Use response_format so the agent returns a validated decision object instead of prose you parse with regex. It accepts ToolStrategy (structured output via tool calling) or ProviderStrategy (the provider's native structured output), and the result lands in the structured_response key of the agent state (structured output docs).

from pydantic import BaseModel
from typing import Literal
from langchain.agents import create_agent

class Decision(BaseModel):
    action: Literal["approve", "deny", "escalate"]
    reason: str
    refund_amount: float | None = None

agent = create_agent(
    model="<your-model-string>",
    tools=[lookup_account, fetch_orders, issue_refund],
    response_format=Decision,
    middleware=[...],
)

The detail worth knowing: in v1, structured output is generated inside the main model and tools loop, which removes the extra LLM call older versions made. That lowers latency and cost (LangChain blog). When you read your traces, you should see one fewer model call than a pre-1.0 tutorial would lead you to expect. If you see the extra call, you are probably on an old pattern.

Note that Decision here is a Pydantic model used for the output schema, which is fine. The Pydantic restriction applies to the agent's state schema, which is a separate thing. Engineers conflate these constantly.

Step 3: Put the human gate in middleware, not in your prompt

Require approval on issue_refund with HumanInTheLoopMiddleware, and never rely on a prompt that says "ask before refunding." Prompts are suggestions. Middleware is a control. LangChain ships three prebuilt middlewares: PIIMiddleware (redacts sensitive data), SummarizationMiddleware (condenses long histories) and HumanInTheLoopMiddleware (requires approval for sensitive tool calls) (docs).

Custom middleware subclasses AgentMiddleware and can implement six hooks: before_agent, before_model, wrap_model_call, wrap_tool_call, after_model and after_agent (docs). Here is how I'd map the project's concerns onto them:

Concern Hook / middleware Why there
Redact card numbers, emails PIIMiddleware Runs before data reaches the model
Block refunds above a limit custom wrap_tool_call Enforced in code, not in the prompt
Approval for money movement HumanInTheLoopMiddleware Pauses the run durably
Long ticket threads SummarizationMiddleware Keeps context bounded
Audit log of every tool call custom wrap_tool_call One place, no tool-by-tool copy-paste

The exercise I'd push hardest: write that refund-limit guard yourself as a wrap_tool_call hook. It teaches you the real extension point, and the same pattern covers rate limiting, retries and logging. It is the part of v1 I'd argue most courses will teach least, simply because it is newer.

Step 4: Make it survive a restart (where most demos die)

The approval gate only works if the paused run is persisted, so add a real checkpointer and test it by killing the process. The human-in-the-loop middleware requires a checkpointer to persist state across interrupts. The docs recommend a persistent one such as AsyncPostgresSaver or MongoDBSaver in production and InMemorySaver for testing or prototyping (docs).

The trap is that InMemorySaver (and MemorySaver) keep checkpoints in RAM, so every checkpoint is lost when the process restarts (persistence docs). Your demo works flawlessly because you never restart it. Production restarts constantly: deploys, crashes, scale-to-zero cold starts.

When an interrupt fires, LangGraph saves graph state through the persistence layer and waits indefinitely until you resume (interrupts docs). "Indefinitely" is the word to respect. A reviewer might approve in ten minutes or three days, so your state store has to hold up for that long.

Also, you must pass a thread_id in the configurable part of the config whenever you invoke with a checkpointer (checkpointers docs):

config = {"configurable": {"thread_id": f"ticket-{ticket_id}"}}
result = agent.invoke({"messages": [...]}, config)

My thread-ID rule: derive it from the business entity (the ticket), not a random UUID per call. That makes resuming idempotent and makes debugging a support ticket a matter of looking up one key.

The verification step that makes this a real exercise: run the agent until it pauses for approval, kill the process, start a fresh one pointing at Postgres, and resume with the same thread_id. If it continues where it stopped, you built it correctly. If you used InMemorySaver, you will watch it fail, and you will remember that failure better than any lecture.

Step 5: Break it on purpose

Spend a third of your time on failure cases, because that is the difference between a demo and a system. The harness I'd build replays these:

  • A tool that times out halfway through a refund.
  • A model returning an action your guard rejects.
  • A reviewer who rejects, then a second resume on the same thread.
  • A ticket thread long enough to trigger summarization, then a question about a detail from early in the thread.
  • A prompt-injection string inside the customer's message trying to talk the agent into a refund.

That last one deserves a pause. The guard against it is not a smarter prompt. It is that the refund tool cannot execute without passing your wrap_tool_call check and the human gate, regardless of what the model wants. I apply the same principle in the autonomous systems I run unattended: assume the model will eventually be talked into something, and make the dangerous action structurally hard rather than politely discouraged.

I would also have you capture traces for each failure and read them. Tracing tooling exists for this, though I won't quote pricing since I haven't verified it, so check the vendor's pricing page directly if cost matters to your choice.

What I'd do

If I were learning LangChain from scratch today, I'd skip the paid video route and do three things in order.

  1. Read the v1 release notes and migration guide first. They are short, free, and tell you exactly which patterns are current.
  2. Build the refund agent above in a weekend, ending with the kill-and-resume test. Don't move on until that passes.
  3. Use the free official material for gaps. LangChain Academy content is free; the only paid item is the certification exam (LangChain Academy). I haven't re-checked whether each course has been updated for v1, so verify that before you invest hours. LangGraph itself is MIT-licensed and free (LangGraph).

One more point on timing. LangChain 1.x follows semantic versioning, so breaking changes come only in major releases, and 1.0 is an LTS release that stays active until 2.0 ships and then enters maintenance for at least a year (release policy). Meanwhile 0.3 and LangGraph 0.4 are in maintenance until December 2026 (versioning). Skills you build on 1.x have a stable runway, and anything you learn on 0.x has a visible expiry date.

None of this means video is worthless. If a course gets you unstuck, use it. But the person who has resumed a paused agent from Postgres after a crash will out-debug the person who has watched ten hours of lectures, every time.

If you build this and get stuck on the persistence or middleware parts, or you're scoping something similar for a real team, you can reach me at lazar-milicevic.com/#contact. I also write more production-agent breakdowns on the blog.

Frequently asked questions

What is the best project to learn LangChain for production work?

I'd build a refund-approval agent for support triage. It uses three tools (account lookup, order history, a stubbed refund), returns a typed approve/deny/escalate decision, requires human approval before any money moves, and persists state so a paused run survives a restart. That single project forces you through tool design, structured output, middleware, persistence and failure handling, which is where production LangChain work actually lives.

Should I use create_agent or create_react_agent in LangChain?

Use `create_agent` from the LangChain v1 line. With the 1.0 release announced October 22, 2025, it became the standard way to build agents and replaced `langgraph.prebuilt.create_react_agent`. It runs the agent loop on LangGraph under the hood, so you still get LangGraph's runtime benefits. Tutorials that predate 1.0 often teach the older pattern.

How do I add human approval to a LangChain agent?

Use `HumanInTheLoopMiddleware` on the sensitive tool, such as a refund tool, instead of relying on a prompt that says to ask first. Prompts are suggestions, while middleware is an enforced control that pauses the run until a human approves. Pair it with a persistent checkpointer so a paused run can survive a process restart. LangChain also ships `PIIMiddleware` and `SummarizationMiddleware` as prebuilt options.

How do I get structured output from a LangChain agent?

Pass a schema through `response_format` when you call `create_agent`. It accepts `ToolStrategy`, which uses tool calling, or `ProviderStrategy`, which uses the provider's native structured output, and the validated result appears under `structured_response` in the agent state. In v1 this happens inside the main model and tools loop, so it avoids the extra LLM call older versions made. That lowers latency and cost.

What changed in LangChain 1.0 that affects older tutorials?

Legacy pieces like `LLMChain`, `ConversationChain`, retrievers, the indexing API and the hub module moved to the `langchain-classic` package. All LangChain packages now require Python 3.10 or higher. Custom state in `create_agent` supports only `TypedDict`, not Pydantic models or dataclasses, though Pydantic is still fine for the output schema. If a course is built around the old chains, you're learning a layer you'll mostly not ship on.

Lazar Milicevic

Lazar Milićević

Senior Technical Engineer. I build AI automation, GenAI/LLM systems and cloud architecture — autonomous systems that run while you sleep. Founder of BizFlowAI.

Building something hard with AI or automation? I am open to talk.

Get in touch

← All posts