Module 1: Anatomy of an AI Agent
7. When to Use (and When NOT to Use) Agents
Capsule overview
You already know what an agent is, how it thinks (perceive-reason-act), what types exist, how it differs from chains and workflows, and which frameworks are available. What's missing is the most important question: when should you actually build one? This capsule gives you the answer — and it's more nuanced than it looks.
Agents have real costs. Every iteration of the reason-act-observe loop means an LLM call, with its latency and its tokens. An agent that runs 4 iterations consumes 4-8x more tokens than an equivalent chain, takes 3-5x longer, and produces less predictable outputs. That isn't bad — it's the price of flexibility. The problem is paying that price when you don't need it. The most common anti-pattern in AI Engineering today is "an agent for everything": teams turning every pipeline into an agent because it sounds more sophisticated, without asking whether the task justifies the complexity.
This is one of the most valuable capsules in the whole guide. Not because it's technically hard — but because it saves you months of over-engineering and thousands of dollars in unnecessary costs. You leave here with a decision framework, a cost-benefit analysis with real numbers, and a practical checklist you can use tomorrow at work to answer: "do I need an agent here, or am I complicating something that should be simple?"
Signs That You DO Need an Agent
These are the clear signals that your system benefits from an agent. The more signals present, the more justified the extra cost.
1. The task is open-ended
The user doesn't specify exactly what to do. "Research the impact of AI in healthcare" might require 2 searches or 10, depending on what it finds. You can't write a fixed pipeline because you don't know how many steps you'll need.
# Open-ended task → you can't predict the flow
"Research the AI agents market and give me a summary with data"
# How many searches? Does it need to compute a growth rate? Compare with other markets?
# It depends on what it finds at each step → AGENT
2. Multiple tools available and the model must choose
If you have 3+ tools and the choice depends on the user's input, you need the LLM to decide. A chain can't choose — it always runs the same sequence.
tools = [search_web, query_database, calculator, read_document, send_email]
# The user says: "How much did we spend on AWS last month?"
# → query_database + calculator
# The user says: "What does OpenAI's latest paper say about agents?"
# → search_web + read_document
# Same interface, different tools every time → AGENT
3. Iteration is required
The answer requires multiple steps where each step depends on the previous result: search → read → refine the search → synthesize. If the first search doesn't return good results, the agent searches again with a different query. A chain can't do that.
4. Stopping conditions aren't fixed
You don't know up front how many tool calls will be needed. Sometimes 1, sometimes 5. The agent decides when it has enough information to answer.
5. The user's intent varies
The same system receives radically different inputs. A research assistant might get "What is MCP?" (1 search) or "Compare 5 agent frameworks with pros, cons and benchmarks" (multiple searches, calculations, synthesis).
Complete example: YES, agent
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
@tool
def search_web(query: str) -> str:
"""Search the web for current information."""
return f"AI agents market: $5.2B in 2024, projected $47B by 2030. CAGR: 44.8%."
@tool
def calculator(expression: str) -> str:
"""Evaluate math expressions."""
try:
return str(round(eval(expression), 2))
except Exception as e:
return f"Error: {e}"
model = init_chat_model("openai:gpt-4.1-mini")
tools = [search_web, calculator]
tools_by_name = {t.name: t for t in tools}
model_with_tools = model.bind_tools(tools)
messages = [
SystemMessage(content="You are a research assistant. Search for data, compute, and synthesize."),
HumanMessage(content="Research the AI agents market: size, growth rate, and main use cases.")
]
for i in range(6):
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
print(f"Final answer (after {i} iterations):")
print(response.content)
break
for tc in response.tool_calls:
result = tools_by_name[tc["name"]].invoke(tc["args"])
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
print(f" [{i+1}] {tc['name']}({tc['args']})")
# The model decides: how many searches, whether to compute CAGR, when to stop.
# A chain couldn't adapt — it doesn't know how many searches it needs.
Signs That You DON'T Need an Agent
These signals indicate that a chain or workflow solves your problem with less cost, more speed and greater predictability.
1. Deterministic flow
Always the same steps, in the same order, for every input. Translating, summarizing, classifying, extracting data — those are fixed pipelines.
# Always: input → translate → format → output
# No decision. No iteration. No tool selection.
# → CHAIN
2. A single tool (or none)
If your system only uses one tool and always uses it, there's no decision to make. The overhead of having the LLM "decide" to use the only available tool is pure waste.
3. Clear, finite rules
The conditional branches can be written as if/elif in code. You don't need an LLM to infer the routing — you already know the rules.
4. Latency is critical
Each agent iteration adds 300-800ms (one LLM call). If you need responses in <500ms, an agent with 3 iterations (~1.5-3s) isn't viable.
5. Tight token budget
At scale, the difference between a chain (1 LLM call) and an agent (3-5 LLM calls) multiplies. 10,000 requests/day × 4x more tokens = significant costs with no proportional benefit.
6. Full auditability required
In regulated industries (finance, healthcare), you need to explain exactly what the system did. A chain produces deterministic logs. An agent produces variable traces that are harder to audit.
Complete example: NO agent
from dotenv import load_dotenv
load_dotenv()
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini")
translate = ChatPromptTemplate.from_messages([
("system", "Translate the following text from Spanish to English. Return only the translation."),
("human", "{text}")
]) | model | StrOutputParser()
format_professional = ChatPromptTemplate.from_messages([
("system", "Rewrite in a professional tone for a corporate email."),
("human", "{text}")
]) | model | StrOutputParser()
def translate_and_format(text: str) -> str:
translated = translate.invoke({"text": text})
return format_professional.invoke({"text": translated})
result = translate_and_format("Hola, quiero saber sobre el puesto de trabajo")
print(result)
# Always: translate → format. 2 LLM calls, ~1s, fixed cost.
# An agent would do the same thing with ~4-5 LLM calls and ~3s. No benefit.
Anti-Patterns: The 7 Most Expensive Mistakes
Anti-Pattern 1: "An Agent for Everything"
The most common one. Every feature is implemented as an agent because "agents are more flexible." Flexibility has a cost — and if you don't need it, you're just paying more.
# ❌ WRONG: an agent to summarize text
from langgraph.prebuilt import create_react_agent
@tool
def summarize_text(text: str) -> str:
"""Summarize a text."""
m = init_chat_model("openai:gpt-4.1-mini")
return m.invoke(f"Summarize: {text}").content
agent = create_react_agent(model, [summarize_text])
result = agent.invoke({"messages": [("user", "Summarize this article: [long text]")]})
# 3-4 LLM calls: decide what to do → call tool → tool runs an LLM → answer
# Latency: ~3-4s. Tokens: ~2000-3000.
# ✅ RIGHT: a direct chain
chain = ChatPromptTemplate.from_messages([
("system", "Summarize in 3-4 sentences."), ("human", "{text}")
]) | model | StrOutputParser()
result = chain.invoke({"text": "[long text]"})
# 1 LLM call. Latency: ~1s. Tokens: ~500-800.
Real cost: If you process 5,000 articles/day, the unnecessary agent costs ~$45/day extra. That's ~$1,350/month that contributes exactly zero.
Anti-Pattern 2: "An Agent with One Tool"
If there's only one tool and it's always called, the model doesn't need to "decide." The decision has already been made.
# ❌ WRONG: an agent with 1 tool that always gets called
agent = create_react_agent(model, [get_weather])
result = agent.invoke({"messages": [("user", "Weather in Madrid?")]})
# The model "decides" to use get_weather... because it's the only option.
# Overhead: 1-2 extra LLM calls with no real decision.
# ✅ RIGHT: a direct call
weather = get_weather.invoke({"city": "Madrid"})
response = model.invoke(f"The weather in Madrid is: {weather}. Format it for the user.")
# 1 LLM call + 1 tool call. No "decision" overhead.
Anti-Pattern 3: "An Agent Without Limits"
Without max_iterations, an agent can fall into infinite loops. The model searches → doesn't find what it wants → searches again → still doesn't find it... Tokens pile up exponentially.
# ❌ WRONG: no iteration limit
def agent_loop(messages):
while True: # potential infinite loop
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
return response.content
for tc in response.tool_calls:
result = tools_by_name[tc["name"]].invoke(tc["args"])
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
# ✅ RIGHT: with a limit and a fallback
def agent_loop(messages, max_iterations: int = 5):
for i in range(max_iterations):
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
return response.content
for tc in response.tool_calls:
result = tools_by_name[tc["name"]].invoke(tc["args"])
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
return "I couldn't complete the task within the iteration limit."
Anti-Pattern 4: "Preemptive Over-engineering"
"I'll build it as an agent just in case we need more flexibility later." This is YAGNI (You Ain't Gonna Need It) applied to agents. Start with a chain. Refactor to an agent when a real requirement justifies it. Every tool schema you add "just in case" is ~50 extra tokens on every call with no value added.
Anti-Pattern 5: "An Agent for CRUD"
Create-Read-Update-Delete operations are deterministic by nature. You always know which operation to run. The model doesn't need to "decide."
# ❌ WRONG: an agent for database operations
@tool
def create_user(name: str, email: str) -> str:
"""Create a user."""
return f"User {name} created"
@tool
def get_user(user_id: str) -> str:
"""Get a user."""
return f"User {user_id}: data..."
agent = create_react_agent(model, [create_user, get_user])
result = agent.invoke({"messages": [("user", "Create user: Ana, ana@email.com")]})
# ✅ RIGHT: a direct endpoint
def handle_create_user(name: str, email: str):
return create_user(name, email)
# You don't need an LLM to decide that "Create user" → create_user.
# An if/elif or a router in code is faster, cheaper and more predictable.
Exception: If the user interacts in natural language and the operations are ambiguous ("I want to change my profile, I'm not sure what exactly"), then an agent can be justified.
Anti-Pattern 6: "An Agent Without Observability"
Putting an agent in production without tracing is like running a server without logs. When something fails — and it will — you have no idea why. Always turn on tracing (LangSmith, Langfuse, or similar) before going to production.
Anti-Pattern 7: "An Agent That Ignores Cumulative Costs"
Every agent iteration includes the entire history of previous messages. Iteration 1: ~500 tokens. Iteration 2: ~900 tokens. Iteration 3: ~1400 tokens. The cost grows quadratically, not linearly.
Iteration 1: system + user + response → ~500 tokens input
Iteration 2: system + user + response + tool → ~900 tokens input
Iteration 3: everything above + response + tool → ~1400 tokens input
Iteration 4: everything above + response + tool → ~2000 tokens input
Iteration 5: everything above + response → ~2500 tokens input
Total: ~7,300 tokens input (not 5 × 500 = 2,500)
Mitigation: Message trimming, history summarization, or simply using a chain if the task doesn't require that many iterations.
Cost-Benefit Analysis: The Real Numbers
Cost by architecture (gpt-4.1-mini, typical task)
| Factor | Chain | Workflow | Agent (3 iter.) | Agent (5 iter.) |
|---|---|---|---|---|
| LLM calls | 1-3 (fixed) | 2-3 (predictable) | 4-7 (variable) | 6-11 (variable) |
| Input tokens | ~500-1,500 | ~700-2,000 | ~3,000-5,000 | ~5,000-10,000 |
| Output tokens | ~200-500 | ~300-600 | ~500-1,500 | ~800-2,500 |
| Cost per request | ~$0.001-$0.003 | ~$0.002-$0.004 | ~$0.005-$0.015 | ~$0.010-$0.030 |
| Latency | 0.5-1.5s | 1-2s | 2-5s | 4-10s |
| Predictability | High | High | Medium | Low |
Impact at scale
| Volume | Chain ($0.002/req) | Agent ($0.010/req) | Monthly difference |
|---|---|---|---|
| 1,000/day | $60/month | $300/month | $240/month |
| 10,000/day | $600/month | $3,000/month | $2,400/month |
| 100,000/day | $6,000/month | $30,000/month | $24,000/month |
Same task: chain vs agent
To illustrate the cost of the wrong decision, here's the same task implemented as a chain and as an agent.
Task: "Summarize this article and translate it into English."
from dotenv import load_dotenv
load_dotenv()
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini")
article = "AI agents are transforming the industry..."
# --- CHAIN VERSION ---
summarize = ChatPromptTemplate.from_messages([
("system", "Summarize in 2 sentences."), ("human", "{text}")
]) | model | StrOutputParser()
translate = ChatPromptTemplate.from_messages([
("system", "Translate into English."), ("human", "{text}")
]) | model | StrOutputParser()
def chain_version(text: str) -> str:
return translate.invoke({"text": summarize.invoke({"text": text})})
result_chain = chain_version(article)
# 2 LLM calls | ~800 tokens total | ~1s | ~$0.001
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
model = init_chat_model("openai:gpt-4.1-mini")
article = "AI agents are transforming the industry..."
@tool
def summarize_tool(text: str) -> str:
"""Summarize a text in 2 sentences."""
m = init_chat_model("openai:gpt-4.1-mini")
return m.invoke(f"Summarize in 2 sentences: {text}").content
@tool
def translate_tool(text: str) -> str:
"""Translate text into English."""
m = init_chat_model("openai:gpt-4.1-mini")
return m.invoke(f"Translate into English: {text}").content
tools = [summarize_tool, translate_tool]
tools_by_name = {t.name: t for t in tools}
model_with_tools = model.bind_tools(tools)
messages = [
SystemMessage(content="Summarize and translate into English."),
HumanMessage(content=f"Process this article: {article}")
]
for i in range(5):
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
print(f"Agent ({i} iterations): {response.content}")
break
for tc in response.tool_calls:
result = tools_by_name[tc["name"]].invoke(tc["args"])
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
# ~5 LLM calls (decide + summarize + its LLM + translate + its LLM + answer)
# ~3,000-4,000 tokens total | ~3-4s | ~$0.005-$0.008
Result: Same output, but the agent uses 4-5x more tokens, takes 3-4x longer, and costs 5-8x more. For a task with a fixed flow (always summarize → translate), the agent adds no value — only cost.
When the Extra Cost IS Worth It
The analysis above may look anti-agent. It isn't. Agents justify their cost when:
| Scenario | Why it's worth it |
|---|---|
| Open-ended research | You don't know how many searches you need. The agent adapts its depth to the topic |
| Multi-tool assistant | The user can ask for varied things (search, compute, create). A chain can't cover every case |
| Tasks where quality matters more than speed | 3 extra seconds for a more complete, more accurate answer can be a good trade-off |
| Unpredictable inputs | The long tail of requests you can't anticipate |
| Exploration | The user doesn't know exactly what they want. The agent explores alongside them |
Real-world examples:
- 📌 Notion AI writing assistant → Chain. Always: input → process → output. Needs no tools.
- 📌 GitHub Copilot Chat → Agent. It can search the codebase, read files, run commands — it depends on the question.
- 📌 Stripe billing categorizer → Workflow. Clear rules: charge type → category. No LLM deciding needed.
- 📌 Perplexity AI → Agent. Iterative search: search → evaluate → refine → synthesize.
- 📌 Grammarly corrections → Chain. Input → analyze → suggest. Fixed flow.
- 📌 Cursor Composer → Agent. Reads files, searches, edits, runs — the flow depends on what the user asks.
Decision Checklist
Before you write a single line of code, answer these 7 questions. If you answer "Yes" to 3+ questions in group A, you need an agent. If you answer "Yes" to 3+ in group B, you don't.
Group A: Pro-agent signals
- Is the task open-ended? The user doesn't specify exactly which steps to follow
- Are there 2+ tools and the model must pick one? The choice depends on the input
- Does the number of steps vary? Sometimes 1 step, sometimes 5, depending on what it finds
- Do intermediate results affect the next steps? The agent adapts its behavior to what it observes
Group B: Anti-agent signals
- Is the flow always the same? Same steps, same order, for every input
- Can the branches be coded as if/elif? The conditions are rules, not inference
- Is latency critical (<1s)? Each agent iteration adds 300-800ms
- Do you need predictable costs? A fixed budget per request
The final tiebreaker question
Does the LLM need to make a decision you can't encode as a rule?
If yes → Agent (or at least LLM-based routing). If no → Chain or Workflow.
Comparison: Task by Task
| Task | Architecture | Rationale |
|---|---|---|
| Translate text es→en | Chain | Fixed flow: input → translate → output |
| FAQ chatbot with a knowledge base | Chain (RAG) | embed → retrieve → generate. Always the same |
| Classify support tickets | Workflow | classify → route by rule → handle. Finite branches |
| "Research X and give me a report" | Agent | Iterative search, variable number of steps |
| Generate contracts by type | Workflow | Type → matching template. Deterministic routing |
| Assistant that searches, computes and queries a DB | Agent | The model picks tools based on the user's question |
| ETL pipeline: CSV → transform → DB | Chain | Always the same steps. No decision |
| "Plan my trip to Japan" | Agent | Open-ended task: flights, hotels, activities. The user explores |
| Validate a signup form | Code | Neither chain nor agent. Deterministic validation rules |
| Summarize meeting transcripts | Chain | Input → summarize → format. Fixed flow |
Gray area: RAG with refinement
Basic RAG is a chain: embed → retrieve → generate. But if the model can "refine the query" when the initial results aren't good, it becomes an agent. The key question: does the model decide whether to search again?
# RAG as a chain (fixed flow)
def rag_chain(query: str) -> str:
docs = retriever.invoke(query)
return generate_chain.invoke({"context": docs, "query": query})
# RAG as an agent (the model decides whether to refine)
# If the docs aren't relevant → the model rephrases and searches again
# If the first search is enough → it answers directly
# → The model DECIDES the flow → it's an agent
Connection to the Project
In this module's project (capsule 08), you'll implement a ReAct agent from scratch. Now you have the criteria to understand why it's an agent and not a chain:
- The research task is open-ended — you don't know how many searches you'll need
- There are multiple tools (search, calculator) and the model picks which one to use
- The number of iterations depends on what the agent finds
- A chain couldn't adapt to varied queries
In the evolving project (Research Agent, Modules 4-10), this capsule's decision framework applies to every design:
- 📌 Module 4 (State Machine): The research loop is an agent, but the routing between phases (plan → research → analyze → synthesize) can be a workflow
- 📌 Module 5 (Planning): The planning phase could be a chain (fixed plan), but the reflection phase needs an agent (it decides whether to re-plan)
- 📌 Module 8 (Multi-Agent): The supervisor is an agent (it decides who to delegate to), but each specialized sub-agent can be a chain or an agent depending on its task
The question "do I need an agent here?" is one you'll answer in every module.
Troubleshooting
Problem 1: "I build everything as an agent"
Symptom: Every feature uses create_react_agent even though the task is predictable and the flow is fixed.
Cause: Tool bias. "If all you have is a hammer, everything looks like a nail." You just learned about agents and you want to apply them to everything.
Fix: Before writing code, answer: "Does the LLM need to decide something I can't encode as a rule?" If not → chain or workflow. Use this capsule's decision checklist.
Problem 2: "My agent is too slow for the UX I need"
Symptom: The agent takes 4-8 seconds to respond. Users abandon.
Cause: An agent with 3+ iterations for a task that could be a 1-iteration chain.
Fix: (1) Evaluate whether it really needs to be an agent. (2) If it does, implement streaming so the user sees progress. (3) Consider the hybrid pattern: a router that classifies "simple vs complex" → chain for simple, agent for complex. Typically 70-80% of requests are "simple".
Problem 3: "My agent's costs exploded in production"
Symptom: The API bill went from $500/month to $3,000/month with no proportional traffic increase.
Cause: Agents without max_iterations, message history growing without trimming, or agents for tasks that should be chains.
Fix: (1) Audit every agent: does it really need to be one? (2) Implement max_iterations on all of them. (3) Message trimming to bound the context. (4) Token budgets per request. (5) Monitor tokens/request with LangSmith.
Problem 4: "I don't know if my system is a workflow or an agent"
Symptom: Your code has if/else for routing, but inside some paths the LLM decides which tools to use.
Cause: It's a hybrid system, and that's fine. It's the most common pattern in production.
Fix: Call it a "workflow with sub-agents". The routing = workflow (rules in code). The complex subnodes = agents (the LLM decides). Don't force the classification — design each part with the architecture it deserves.
Problem 5: "The team wants agents because it 'sounds better' in the presentation"
Symptom: Architecture decisions driven by internal marketing, not by technical requirements.
Cause: Hype cycle. "AI Agent" sounds more impressive than "a 3-step chain".
Fix: Present the cost-benefit analysis with real numbers. "$2,400/month extra with no quality improvement" is a hard argument to ignore. Propose: "We use an agent for the features that justify it, a chain for everything else."
Exercises
Exercise 1: Classify 8 tasks (Easy)
For each task, indicate Chain, Workflow or Agent. Justify in one sentence.
a) Generate thumbnails for YouTube videos b) A shopping assistant that searches for products, compares prices and recommends c) Send a welcome email when a user signs up d) "Analyze my codebase and suggest improvements" e) Classify product reviews as positive/negative/neutral f) A dashboard that generates reports based on user filters g) A chatbot that can search the docs, create tickets and check order status h) Convert CSV to JSON with a fixed schema
See solution
a) Chain — Input (video) → generate thumbnail → output. Always the same steps.
b) Agent — Open-ended task: the model decides what to search for, how many products to compare, when it has enough to recommend.
c) Plain code — Neither chain nor agent. Trigger → send email. Deterministic, no LLM needed.
d) Agent — Open exploration: read files, analyze patterns, compare against best practices. The flow depends on the codebase.
e) Chain — Input → classify → output. 1 LLM call, fixed flow.
f) Workflow — Filters → route by report type → generate with a template. Finite, predefined branches.
g) Agent — 3+ tools, the model picks based on the user's question. Variable intent.
h) Plain code — Deterministic parsing. Doesn't need an LLM at all.
Pattern: If it doesn't need an LLM → code. If LLM but fixed flow → chain. If branches with rules → workflow. If the LLM decides the flow → agent.
Exercise 2: Compute the cost of the wrong decision (Easy)
A "news summarizer" system processes 8,000 articles/day. It currently uses an agent with 1 tool (summarize). Each agent run makes ~3 LLM calls. Each LLM call costs $0.0015.
a) How much does it cost per day as an agent? b) How much would it cost as a chain (1 LLM call)? c) How much do you save per month and per year? d) Is the agent justified?
See solution
a) Agent: 8,000 × 3 × $0.0015 = $36/day
b) Chain: 8,000 × 1 × $0.0015 = $12/day
c) Savings: ($36 - $12) × 30 = $720/month = $8,640/year
d) Not justified. It always calls the same tool — there's no dynamic decision. It's a chain in agent's clothing. The 2 extra LLM calls (decide + confirm) add no value. Refactoring to a chain keeps the same output quality.
Exercise 3: Design a hybrid system (Medium)
Design the architecture for a customer support system with these requirements:
- Frequently asked questions → automatic answer
- Complaints → escalate to a human with context
- Technical inquiries → search the docs and create a ticket if unresolved
- Sales requests → generate a personalized proposal
Indicate which architecture (chain, workflow, agent) you'd use for each part and why.
See solution
# ARCHITECTURE: Workflow with specialized sub-systems
# Step 1: Classify (chain - 1 LLM call)
def classify(message: str) -> str:
return llm_classify(message) # → "faq", "complaint", "technical", "sales"
# Step 2: Routing (workflow - rules in code)
def route(category: str) -> str:
routes = {
"faq": "faq_chain",
"complaint": "escalate_workflow",
"technical": "tech_agent",
"sales": "sales_chain",
}
return routes[category]
# Sub-systems:
# FAQ → Chain (embed → retrieve → generate). Fixed flow.
# Complaint → Workflow (extract context → notify team → confirm). Clear rules.
# Technical → AGENT (search_docs, create_ticket - the model decides whether to search more or open a ticket).
# Sales → Chain (extract needs → match products → generate proposal). Fixed flow.
Only the technical part justifies an agent — because the model must decide whether the documentation's answer is enough or whether it needs to create a ticket. The other parts are chains or workflows with predictable flows.
Result: ~75% of requests (FAQ + sales) use cheap chains. ~15% (complaints) use a workflow. Only ~10% (technical) use the expensive agent. Significant cost optimization vs "an agent for everything".
Exercise 4: Refactor an agent into a chain (Medium)
This agent processes invoices. Refactor it to reduce cost while keeping the functionality.
from langgraph.prebuilt import create_react_agent
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
@tool
def extract_total(invoice_text: str) -> str:
"""Extract the total from an invoice."""
m = init_chat_model("openai:gpt-4.1-mini")
return m.invoke(f"Extract only the total amount: {invoice_text}").content
@tool
def categorize_expense(description: str) -> str:
"""Categorize an expense."""
m = init_chat_model("openai:gpt-4.1-mini")
return m.invoke(f"Categorize: {description}. Options: office, travel, software, other").content
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_react_agent(model, [extract_total, categorize_expense])
result = agent.invoke({"messages": [("user", f"Process this invoice: {invoice_text}")]})
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = init_chat_model("openai:gpt-4.1-mini")
extract = ChatPromptTemplate.from_messages([
("system", "Extract the total amount from the invoice. Return only the number."),
("human", "{invoice}")
]) | model | StrOutputParser()
categorize = ChatPromptTemplate.from_messages([
("system", "Categorize the expense. Answer ONLY: office, travel, software, other."),
("human", "{description}")
]) | model | StrOutputParser()
def process_invoice(invoice_text: str) -> dict:
total = extract.invoke({"invoice": invoice_text})
category = categorize.invoke({"description": invoice_text})
return {"total": total, "category": category}
invoice_text = "Invoice #1234. Service: GitHub Enterprise. Amount: $450 USD."
print(process_invoice(invoice_text))
What changed: From ~6 LLM calls (agent decides + extract_tool + its LLM + categorize_tool + its LLM + final answer) down to 2 LLM calls (extract + categorize). The flow is always extract → categorize. There's no dynamic decision. Pure chain.
Estimated savings: 3x fewer tokens, 3x faster, same result.
Exercise 5: Document an architecture decision (Medium)
Write a paragraph documenting the architecture decision for a system at your company (real or fictional). It must include: which architecture you chose, why, which alternative you discarded and why, and which trade-offs you accept.
See solution
Example:
"The research assistant system uses an agent because users ask open-ended questions ('research the market for X') and the system must dynamically decide how many searches to run, whether to compute metrics, and when it has enough information to synthesize. We discarded a chain because we can't predetermine the number of steps — it depends on the quality of the intermediate results. We accept the trade-offs: 3-5s latency (vs <1s for a chain),
5x higher cost per request, and non-deterministic output. We mitigate with$120/month) is justified by an output quality a chain can't reach."max_iterations=6, a 10K token budget per request, and LangSmith to monitor quality. At our volume (500 queries/day), the extra cost (
Key elements: decision + rationale + discarded alternative + trade-offs + mitigation + numbers.
Exercise 6: When to scale from chain to agent (Hard)
Your customer support system started as a RAG chain (embed → retrieve → generate). Now the team wants to add: (1) create tickets automatically, (2) check order status, (3) process returns. Do you keep the chain, migrate to an agent, or design a hybrid? Justify with a cost analysis.
See solution
Analysis:
- The original RAG chain works for informational questions (FAQ)
- The 3 new features require different tools (create_ticket, check_order, process_return)
- The user can ask for any of the 4 things → the system must choose
Design: Hybrid
# Router (chain - 1 LLM call) classifies the intent:
# → "info" (70% of requests) → original RAG chain (cheap, fast)
# → "action" (30% of requests) → agent with 3 tools (more expensive, but necessary)
def route(state):
if state["intent"] == "info":
return "rag_chain" # Chain: embed → retrieve → generate. ~$0.002
return "action_agent" # Agent: picks among create_ticket, check_order,
# process_return. ~$0.010
Cost analysis (10,000 requests/day):
| Option | Cost/day | Cost/month |
|---|---|---|
| Everything as a chain (doesn't work for actions) | $20 | $600 |
| Everything as an agent (unnecessary for info) | $100 | $3,000 |
| Hybrid (70% chain + 30% agent) | $44 | $1,320 |
The hybrid saves $1,680/month vs "everything as an agent" while supporting all the features.
Rule: Don't migrate everything to an agent. Add the agent only where the dynamic decision is needed.
Summary
In this capsule you learned:
- YES, agent when: the task is open-ended, multiple tools with a real choice, iteration is required, stopping conditions vary, the user's intent is unpredictable
- NO agent when: deterministic flow, a single tool, codifiable rules, critical latency, fixed budget
- 7 anti-patterns to avoid: an agent for everything, an agent with 1 tool, an agent without limits, preemptive over-engineering, an agent for CRUD, an agent without observability, ignoring cumulative costs
- Real cost-benefit: An agent costs 3-8x more than a chain for the same task. At 10,000 requests/day that can be a $2,400/month difference
- The hybrid pattern (workflow + agents in subnodes) is the most efficient architecture in production: chain for the predictable (~70%), agent for the open-ended (~30%)
- Decision checklist: 7 questions that settle 90% of architecture decisions
- The definitive question: "Does the LLM need to make a decision I can't encode as a rule?" If yes → agent. If no → chain or workflow
Agents are powerful tools — but that's exactly what they are: tools. Use them when the problem justifies it, not because they're trendy.
Next capsule: Project — a basic ReAct agent from scratch. You'll implement the ReAct loop manually (no framework) and then with create_react_agent, with the criteria you now have to understand why this task does justify an agent.
Additional Resources
- Building Effective Agents (Anthropic) — Anthropic's decision framework for when to use agents. Required reading
- How to Think About Agent Frameworks (LangChain Blog) — A perspective on agents vs chains vs workflows
- When to Use AI Agents (Google DeepMind) — Google's analysis of when agents add real value
- OpenAI: Orchestrating Agents — OpenAI's guide to designing agent systems and when to pick simpler alternatives
- LangGraph Documentation: When to Use Agents — The official section on architecture decisions with LangGraph
- Cognitive Architectures for Language Agents (CoALA) — The paper that formalizes the taxonomy and helps decide the level of autonomy needed
- The Cost of AI Agents in Production (a16z) — Analysis of the real costs of agents in companies
- LangSmith — Tracing platform to monitor agent cost and performance in production