Skip to main content
RapidDev - Software Development Agency
build-ai-agent

How to Build an AI Agent with LangChain

LangChain v1's create_agent gives you a working tool-calling agent in a few lines, and middleware is how you customize it without touching the core loop. This guide builds a personal-finance agent that categorizes transactions, summarizes long histories automatically, and pauses for human approval before flagging any transaction over $1,000 as reviewed. Stack: Python, langchain, langgraph (used internally). Budget 40-55 minutes.

What you'll learn

  • Get a working tool-calling agent running with create_agent in under 10 lines
  • Add SummarizationMiddleware so long transaction histories don't blow past the context window
  • Add HumanInTheLoopMiddleware to pause before a specific tool call instead of writing your own approval plumbing
  • Resume a paused agent with Command(resume=...) using the same thread_id
Book a free consultation
4.9Clutch rating ⭐
1,000+Happy partners
20+Countries served
200+Team members
Beginner8 min read40-55 minPython 3.10+, langchain 1.x, langgraph (installed as a dependency), Anthropic API keyLast updated November 2026RapidDev Engineering Team
TL;DR

LangChain v1's create_agent gives you a working tool-calling agent in a few lines, and middleware is how you customize it without touching the core loop. This guide builds a personal-finance agent that categorizes transactions, summarizes long histories automatically, and pauses for human approval before flagging any transaction over $1,000 as reviewed. Stack: Python, langchain, langgraph (used internally). Budget 40-55 minutes.

Quick facts about this guide
FactValue
ToolLangChain
DifficultyBeginner
Time required40-55 min
CompatibilityPython 3.10+, langchain 1.x, langgraph (installed as a dependency), Anthropic API key
Last updatedNovember 2026

What you'll build

You'll build a personal-finance agent that reads a list of transactions, categorizes each one, and flags anything unusual. Categorizing is routine and runs on its own; flagging a transaction over $1,000 as reviewed always pauses for a human to approve first, because that flag feeds into a monthly report someone's name goes on.

create_agent is LangChain v1's high-level entry point: hand it a model string, a list of tools, and it returns a runnable agent built on LangGraph underneath, with no graph code of your own. Middleware is where you customize behavior around that loop without forking it. SummarizationMiddleware condenses older messages once the conversation gets long, so a month of transactions doesn't quietly exceed the model's context. HumanInTheLoopMiddleware intercepts specific tool calls based on an interrupt_on mapping and pauses the whole agent (via the checkpointer LangGraph uses underneath) until you resume it with a decision.

Choose create_agent when you want a standard tool-calling agent fast and are fine working at the middleware layer instead of a custom graph. If you need an explicit multi-step pipeline with branching logic beyond what middleware covers, the LangGraph guide builds the same kind of approval pause directly into a graph you design yourself.

Prerequisites

  • Python 3.10+ and a terminal
  • An Anthropic API key (ANTHROPIC_API_KEY)
  • pip install langchain langgraph
  • Basic comfort with Python functions as tools

Step-by-step guide

1

Install LangChain and run a bare create_agent

Install langchain (langgraph comes along as a dependency since create_agent is built on it). Before adding tools or middleware, confirm the basics work: a model string and an empty tool list should still let the agent respond to a plain question.

typescript
1pip install langchain langgraph
2export ANTHROPIC_API_KEY="sk-ant-..."
3python -c "from langchain.agents import create_agent; a=create_agent(model='anthropic:claude-sonnet-5-5', tools=[]); print(a.invoke({'messages':[{'role':'user','content':'say hi'}]})['messages'][-1].content)"

Expected result: A short greeting printed with no errors, confirming create_agent and your API key both work.

2

Define the finance tools

categorize_transaction is routine: it labels a transaction by matching its description against a small set of categories. flag_for_review is the sensitive one: it's what gets gated by middleware in the next step, so its job is only to record that a transaction was marked reviewed, nothing destructive on its own.

typescript
1CATEGORIES = {"grocery": "Groceries", "uber": "Transport", "rent": "Housing"}
2
3def categorize_transaction(description: str, amount: float) -> str:
4 """Categorize a transaction by its description."""
5 for key, label in CATEGORIES.items():
6 if key in description.lower():
7 return f"{label}: ${amount:.2f}"
8 return f"Uncategorized: ${amount:.2f}"
9
10def flag_for_review(description: str, amount: float, reason: str) -> str:
11 """Flag a transaction as reviewed for the monthly report. Requires human approval for amounts over $1,000."""
12 return f"Flagged '{description}' (${amount:.2f}) for review: {reason}"

Expected result: Two plain Python functions with docstrings create_agent will read as each tool's description.

3

Build the agent with SummarizationMiddleware

Pass both functions as tools=[...] directly; create_agent wraps plain functions into tools automatically from their type hints and docstring. SummarizationMiddleware watches the running conversation and condenses older turns once they pass a token threshold, so a long list of transactions processed in one session doesn't silently exceed the model's context window.

typescript
1from langchain.agents import create_agent
2from langchain.agents.middleware import SummarizationMiddleware
3
4agent = create_agent(
5 model="anthropic:claude-sonnet-5-5",
6 tools=[categorize_transaction, flag_for_review],
7 middleware=[
8 SummarizationMiddleware(model="anthropic:claude-haiku-5-5", max_tokens_before_summary=4000),
9 ],
10)

Expected result: An agent object that categorizes transactions normally; nothing visibly changes yet since no conversation has gotten long enough to summarize.

4

Add HumanInTheLoopMiddleware and a checkpointer

interrupt_on maps each tool name to its approval rule: flag_for_review always pauses (True means every decision type is allowed), categorize_transaction never does (False). Human-in-the-loop middleware requires a checkpointer, since pausing and resuming relies on the same persistence layer LangGraph uses for interrupt().

typescript
1from langchain.agents.middleware import HumanInTheLoopMiddleware
2from langgraph.checkpoint.memory import InMemorySaver
3
4agent = create_agent(
5 model="anthropic:claude-sonnet-5-5",
6 tools=[categorize_transaction, flag_for_review],
7 middleware=[
8 SummarizationMiddleware(model="anthropic:claude-haiku-5-5", max_tokens_before_summary=4000),
9 HumanInTheLoopMiddleware(
10 interrupt_on={
11 "flag_for_review": True,
12 "categorize_transaction": False,
13 },
14 description_prefix="Review needed before flagging",
15 ),
16 ],
17 checkpointer=InMemorySaver(),
18)

Expected result: Calling the agent with a transaction over $1,000 now pauses instead of completing; a small one still runs straight through.

5

Invoke the agent and read the interrupt

Every call needs a thread_id in config so the paused run can be found again. Call agent.invoke with version='v2' to get a GraphOutput with an .interrupts attribute; when it's non-empty, each interrupt's value includes the pending action_requests (the tool name and arguments Claude wants to call).

typescript
1config = {"configurable": {"thread_id": "march-close-1"}}
2
3result = agent.invoke(
4 {"messages": [{"role": "user", "content": "Categorize this $1,450 wire transfer labeled 'consulting fee' and flag it if it looks unusual."}]},
5 config=config, version="v2",
6)
7
8if result.interrupts:
9 for action in result.interrupts[0].value["action_requests"]:
10 print(f"Pending: {action['name']}({action['args']})")

Expected result: The agent categorizes the transfer immediately, then pauses with one pending action_request for flag_for_review.

6

Resume with a decision and harden for production

Resume the same thread_id with Command(resume={'decisions': [{'type': 'approve'}]}) (or 'reject') to continue. In production, swap InMemorySaver for a persistent checkpointer (Postgres, SQLite, Redis) since a human review can take hours, and keep the thread_id stable per transaction batch so a restart doesn't lose the pending approval.

typescript
1from langgraph.types import Command
2
3result = agent.invoke(
4 Command(resume={"decisions": [{"type": "approve"}]}),
5 config=config, version="v2",
6)
7print(result.value["messages"][-1].content)

Expected result: After approval, the agent completes the flag_for_review call and returns a final message confirming the transaction was flagged.

Complete working example

finance_review_agent.py
1"""Personal-finance agent: categorizes transactions, pauses for approval before flagging large ones.
2Run: python finance_review_agent.py (pip install langchain langgraph; set ANTHROPIC_API_KEY)
3"""
4from langchain.agents import create_agent
5from langchain.agents.middleware import HumanInTheLoopMiddleware, SummarizationMiddleware
6from langgraph.checkpoint.memory import InMemorySaver
7from langgraph.types import Command
8
9CATEGORIES = {"grocery": "Groceries", "uber": "Transport", "rent": "Housing"}
10
11
12def categorize_transaction(description: str, amount: float) -> str:
13 """Categorize a transaction by its description."""
14 for key, label in CATEGORIES.items():
15 if key in description.lower():
16 return f"{label}: ${amount:.2f}"
17 return f"Uncategorized: ${amount:.2f}"
18
19
20def flag_for_review(description: str, amount: float, reason: str) -> str:
21 """Flag a transaction as reviewed for the monthly report. Requires human approval for amounts over $1,000."""
22 return f"Flagged '{description}' (${amount:.2f}) for review: {reason}"
23
24
25agent = create_agent(
26 model="anthropic:claude-sonnet-5-5",
27 tools=[categorize_transaction, flag_for_review],
28 middleware=[
29 SummarizationMiddleware(model="anthropic:claude-haiku-5-5", max_tokens_before_summary=4000),
30 HumanInTheLoopMiddleware(
31 interrupt_on={
32 "flag_for_review": True,
33 "categorize_transaction": False,
34 },
35 description_prefix="Review needed before flagging",
36 ),
37 ],
38 checkpointer=InMemorySaver(),
39)
40
41
42def review_transaction(description: str, thread_id: str) -> str:
43 config = {"configurable": {"thread_id": thread_id}}
44 result = agent.invoke(
45 {"messages": [{"role": "user", "content": description}]},
46 config=config, version="v2",
47 )
48
49 while result.interrupts:
50 for action in result.interrupts[0].value["action_requests"]:
51 print(f"Pending approval: {action['name']}({action['args']})")
52 decision = input("Approve this flag? (approve/reject): ").strip().lower()
53 result = agent.invoke(
54 Command(resume={"decisions": [{"type": decision}]}),
55 config=config, version="v2",
56 )
57
58 return result.value["messages"][-1].content
59
60
61if __name__ == "__main__":
62 print(review_transaction(
63 "Categorize this $1,450 wire transfer labeled 'consulting fee' and flag it if it looks unusual.",
64 thread_id="march-close-1",
65 ))
66

Common mistakes

Why it's a problem: Adding HumanInTheLoopMiddleware without a checkpointer

How to avoid: Human-in-the-loop middleware relies on LangGraph's persistence layer to pause and resume. Always pass checkpointer=InMemorySaver() (or a persistent one) alongside it.

Why it's a problem: Reusing the same thread_id for unrelated conversations

How to avoid: Each thread_id is one resumable run. Give each independent transaction batch or user session its own, or a resume will land in the wrong context.

Why it's a problem: Setting interrupt_on values to True for every tool 'to be safe'

How to avoid: That pauses routine tool calls like categorize_transaction too, turning every run into a manual-approval slog. Reserve True for tools whose output actually matters if it's wrong.

Why it's a problem: Forgetting version='v2' on invoke when using interrupts

How to avoid: Without it you get the older result shape that doesn't expose .interrupts the way this guide's resume loop expects.

Why it's a problem: Running SummarizationMiddleware's summarizer model at the same size as the main agent model

How to avoid: Summarization doesn't need your most capable model. A cheaper model (Haiku-class) for the summarizer keeps costs down with no real quality loss for that specific job.

Best practices

  • Start with create_agent and middleware before reaching for a custom LangGraph graph; most agents don't need the extra control.
  • Set interrupt_on precisely per tool, not globally, so only genuinely consequential calls pause.
  • Use a cheaper model for SummarizationMiddleware's own summarization calls than for the main agent loop.
  • Keep thread_id stable and meaningful (per batch, per user, per session) so resumes always land in the right place.
  • Use a persistent checkpointer in production; InMemorySaver loses all pending interrupts on restart.
  • Read tool docstrings as what the model actually sees. Write them as clearly as you'd write a prompt, not as internal code comments.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a LangChain create_agent with categorize_transaction and flag_for_review tools, SummarizationMiddleware, and HumanInTheLoopMiddleware gating flag_for_review (code below). Add a third tool, export_monthly_report, that's also gated by HumanInTheLoopMiddleware, and only allow it to run once every flagged transaction for the month has been approved. [paste complete_code]

Build AI Agents Prompt

In Claude Code, open finance_review_agent.py and ask: 'Add a PIIRedactionMiddleware-style custom middleware that strips account numbers from tool arguments before they're logged, following the same middleware list pattern as the existing ones.'

Frequently asked questions

Do I need to know LangGraph to use create_agent?

No. create_agent is the high-level entry point specifically so you don't have to build a graph yourself. You only need LangGraph concepts like checkpointers and Command(resume=...) once you add human-in-the-loop middleware, as this guide does.

What's the difference between HumanInTheLoopMiddleware's approve, edit, and reject decisions?

approve runs the tool call as proposed, reject skips it and tells the model it was declined, and edit lets you change the tool's arguments before it runs (useful for correcting a wrong amount or category rather than rejecting outright).

How does SummarizationMiddleware decide what to summarize?

It watches the running token count and, once older messages push past max_tokens_before_summary, condenses the oldest portion of the conversation into a summary message, keeping recent turns intact. You set the threshold; there's no fixed default that fits every use case.

Can I stack more than two middleware together?

Yes, middleware is a list and order matters since each one wraps the next. A common stack adds a PII-redaction or logging middleware alongside summarization and human-in-the-loop, each handling one concern.

Does this work with OpenAI or Gemini models instead of Claude?

Yes. create_agent accepts any supported provider:model string, like openai:gpt-6.1-sol or google:gemini-3.8-flash, and the middleware stack works the same regardless of which model string you use.

Can someone build this kind of finance or ops agent for us?

Yes. RapidDev builds LangChain and LangGraph agents with the approval and audit layers finance and ops teams actually need. A free 30-minute consultation is the fastest way to scope yours.

RapidDev

Talk to an Expert

Our team has built 1,000+ apps. Get personalized help with your project.

Book a free consultation
Matt Graham

Written by

Matt Graham · CEO & Founder, RapidDev

1,000+ client projects delivered. Columbia University & Harvard Business School alumnus, U.S. Navy veteran. About the author →

Learning is great. Shipping is faster with help.

Our engineers have built 1,000+ apps on your platform and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.