LangGraph, CrewAI, and Agno: getting started with AI agents in Python
Table of Contents
Everyone talks about AI agents. Few explain what that actually means in practice — with code, no buzzwords.
The idea here is straightforward: take three popular Python agent frameworks — LangGraph, CrewAI, and Agno — and solve the same problem with each one. No hello worlds. Every example uses real tools that the agent decides when and how to call.
💻 Full source code: all examples from this article are available in the posts.codebase repository — with setup instructions to run locally.
If you already know how to call an LLM API and want to take the next step, this post is for you.
What is an AI agent (hype-free version)
An AI agent is a program that uses a language model (LLM) inside a loop. Instead of receiving a question and returning a single answer, it repeats a cycle until it solves the problem. In the literature, this pattern is called ReAct (Reason + Act):
Question
→ [LLM thinks] → [Calls tool] → [Observes result]
→ [LLM thinks] → [Calls tool] → [Observes result]
→ ...
→ Final answer
In concrete terms:
- Thinks — figures out what needs to be done
- Acts — calls a tool (search, calculator, API, database)
- Observes — reads the tool’s output
- Repeats — until it decides it’s done
The difference from a plain API call? The agent decides what to do. The core mechanism that enables this is called tool calling: the LLM receives the list of available tools and chooses which one to call, with which arguments. The LLM is the brain; the tools are the hands.
The frameworks we’ll look at make this loop easier. Each with a different philosophy.
When NOT to use an agent
Not every problem needs an agent. This is important to know before you start building.
If you already know exactly what needs to happen — extract fields from text, classify an email, generate a summary — a simple pipeline is cheaper, faster, and more predictable. A direct API call does the job.
Agents make sense when:
- The path to the answer isn’t fixed — the model needs to decide the next steps
- There are multiple possible tools — and the choice depends on context
- You want to delegate decisions to the model instead of coding every
if/else
And the costs are real:
- Latency: each loop iteration is an LLM call. Three tools = at least three round-trips
- Tokens: context grows with each step. More steps, more cost
- Unpredictability: the agent can loop, call the wrong tools, or misinterpret results
If the path is fixed, use a pipeline. If the path is dynamic, then yes — an agent makes sense.
The problem we’ll solve
To compare the three frameworks fairly, we’ll solve the same problem in all of them:
“I want to buy a laptop. How much does it cost in BRL with a 10% discount?”
The agent needs to:
- Look up the price of the product (in USD)
- Convert from dollars to reais
- Calculate the discount on the BRL amount
Three tools, three dependent steps. Each step’s result feeds the next. This is the kind of problem where an agent shines — because the sequence of calls isn’t obvious without context.
The three tools (identical across all frameworks):
def lookup_price(product: str) -> str:
"""Look up the price of a product in USD. Available products: laptop, monitor, keyboard."""
catalog = {"laptop": 1200.00, "monitor": 450.00, "keyboard": 85.00}
price = catalog.get(product.lower())
if price:
return f"{product}: US$ {price:.2f}"
return f"Product '{product}' not found."
def convert_currency(amount: float, from_cur: str, to_cur: str) -> str:
"""Convert an amount between currencies."""
rates = {"USD_BRL": 5.20, "BRL_USD": 0.19}
key = f"{from_cur}_{to_cur}".upper()
rate = rates.get(key)
if rate:
return f"{amount:.2f} {from_cur} = {amount * rate:.2f} {to_cur}"
return f"Rate {from_cur} → {to_cur} not available."
def apply_discount(amount: float, percentage: float) -> str:
"""Apply a percentage discount to an amount."""
final = amount * (1 - percentage / 100)
return f"Original: {amount:.2f} → With {percentage}% discount: {final:.2f}"
In the examples that follow, the tool logic is the same. What changes is how each framework orchestrates the agent.
1) LangGraph
LangGraph is an orchestration framework from the LangChain ecosystem. The core idea: you model the agent flow as a graph — nodes that process, edges that connect, state that persists between steps.
It’s the lowest level of the three. You assemble each piece of the loop manually.
Installation
pip install langgraph langchain-openai langchain
Example
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
from langgraph.graph import StateGraph, MessagesState, START, END
@tool
def lookup_price(product: str) -> str:
"""Look up the price of a product in USD. Available products: laptop, monitor, keyboard."""
catalog = {"laptop": 1200.00, "monitor": 450.00, "keyboard": 85.00}
price = catalog.get(product.lower())
if price:
return f"{product}: US$ {price:.2f}"
return f"Product '{product}' not found."
@tool
def convert_currency(amount: float, from_cur: str, to_cur: str) -> str:
"""Convert an amount between currencies. Available rates: USD↔BRL."""
rates = {"USD_BRL": 5.20, "BRL_USD": 0.19}
key = f"{from_cur}_{to_cur}".upper()
rate = rates.get(key)
if rate:
return f"{amount:.2f} {from_cur} = {amount * rate:.2f} {to_cur}"
return f"Rate {from_cur} → {to_cur} not available."
@tool
def apply_discount(amount: float, percentage: float) -> str:
"""Apply a percentage discount to an amount."""
final = amount * (1 - percentage / 100)
return f"Original: {amount:.2f} → With {percentage}% discount: {final:.2f}"
tools = [lookup_price, convert_currency, apply_discount]
tools_by_name = {t.name: t for t in tools}
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
model_with_tools = model.bind_tools(tools)
def call_model(state: MessagesState):
messages = [SystemMessage(content="You are a shopping assistant. Always use the available tools to look up prices, convert currencies, and calculate discounts.")] + state["messages"]
return {"messages": [model_with_tools.invoke(messages)]}
def run_tools(state: MessagesState):
results = []
for call in state["messages"][-1].tool_calls:
fn = tools_by_name[call["name"]]
result = fn.invoke(call["args"])
results.append(ToolMessage(content=str(result), tool_call_id=call["id"]))
return {"messages": results}
def decide_next(state: MessagesState):
if state["messages"][-1].tool_calls:
return "tools"
return END
graph = StateGraph(MessagesState)
graph.add_node("model", call_model)
graph.add_node("tools", run_tools)
graph.add_edge(START, "model")
graph.add_conditional_edges("model", decide_next, ["tools", END])
graph.add_edge("tools", "model")
agent = graph.compile()
result = agent.invoke({
"messages": [HumanMessage(content="I want to buy a laptop. How much in BRL with a 10% discount?")]
})
print(result["messages"][-1].content)
What the agent does under the hood
🤔 Thinking: I need to find the laptop price
🔧 Calling: lookup_price("laptop")
📎 Result: laptop: US$ 1200.00
🤔 Thinking: now I need to convert to BRL
🔧 Calling: convert_currency(1200.00, "USD", "BRL")
📎 Result: 1200.00 USD = 6240.00 BRL
🤔 Thinking: now I apply the 10% discount
🔧 Calling: apply_discount(6240.00, 10)
📎 Result: Original: 6240.00 → With 10% discount: 5616.00
✅ Answer: The laptop costs R$ 5,616.00 with a 10% discount.
Each → arrow in the graph is an LLM call. Three tools, three loop iterations. This is what happens “under the hood” in any agent framework.
Real limitations
- Verbose: even a simple agent requires building nodes, edges, routing functions. Compared to the other two, it’s a lot of code
- Learning curve: thinking in graphs is natural for people with a software engineering background, but can be confusing for beginners
- LangChain ecosystem dependency: tools use LangChain’s
@tool, models use LangChain wrappers. Switching later isn’t trivial
When it shines
Total control. You decide every path, every condition, every state. For complex workflows with branching, human-in-the-loop, and execution persistence, nothing is more flexible.
2) CrewAI
CrewAI thinks of agents as team members. Each agent has a role, a goal, and a backstory. You define tasks, assemble a “crew,” and kick it off. The framework handles coordination.
It’s the highest level of the three. Less code, faster to prototype. And the differentiator really shows when there are multiple agents.
Installation
pip install crewai crewai-tools
Example: two agents collaborating
This is where CrewAI’s strength shows: a researcher finds the price and converts the currency, and an analyst applies the discount and delivers the summary.
from crewai import Agent, Task, Crew, Process
from crewai.tools import tool
@tool("PriceLookup")
def lookup_price(product: str) -> str:
"""Look up the price of a product in USD. Available products: laptop, monitor, keyboard."""
catalog = {"laptop": 1200.00, "monitor": 450.00, "keyboard": 85.00}
price = catalog.get(product.lower())
if price:
return f"{product}: US$ {price:.2f}"
return f"Product '{product}' not found."
@tool("CurrencyConverter")
def convert_currency(amount: float, from_cur: str, to_cur: str) -> str:
"""Convert an amount between currencies. Available rates: USD↔BRL. Parameters: numeric amount, source currency (e.g. USD), target currency (e.g. BRL)."""
rates = {"USD_BRL": 5.20, "BRL_USD": 0.19}
key = f"{from_cur}_{to_cur}".upper()
rate = rates.get(key)
if rate:
return f"{amount:.2f} {from_cur} = {amount * rate:.2f} {to_cur}"
return f"Rate {from_cur} → {to_cur} not available."
@tool("Discount")
def apply_discount(amount: float, percentage: float) -> str:
"""Apply a percentage discount. Parameters: numeric amount, discount percentage."""
final = amount * (1 - percentage / 100)
return f"Original: {amount:.2f} → With {percentage}% discount: {final:.2f}"
# Two agents with different roles
researcher = Agent(
role="Price researcher",
goal="Find product prices and convert to the requested currency",
backstory="International market research specialist.",
tools=[lookup_price, convert_currency],
verbose=True,
)
analyst = Agent(
role="Financial analyst",
goal="Calculate final amounts with discounts and present a clear summary",
backstory="Detail-oriented analyst who always shows the numbers.",
tools=[apply_discount],
verbose=True,
)
# Chained tasks: the first one's output feeds the second
research = Task(
description="Find the laptop price in USD and convert to BRL.",
expected_output="The laptop price in BRL.",
agent=researcher,
)
analysis = Task(
description="Apply a 10% discount to the BRL price and present a summary with original price, discount, and final amount.",
expected_output="Summary with original BRL price, discount amount, and final price.",
agent=analyst,
)
crew = Crew(
agents=[researcher, analyst],
tasks=[research, analysis],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
print(result)
What happens under the hood
👤 Researcher enters the scene
🔧 Calling: PriceLookup("laptop")
📎 Result: laptop: US$ 1200.00
🔧 Calling: CurrencyConverter(1200.00, "USD", "BRL")
📎 Result: 1200.00 USD = 6240.00 BRL
📤 Delivers: "The laptop costs R$ 6,240.00"
👤 Analyst receives the researcher's context
🔧 Calling: Discount(6240.00, 10)
📎 Result: Original: 6240.00 → With 10% discount: 5616.00
✅ Delivers: "Laptop: R$ 6,240.00 → with 10% discount: R$ 5,616.00"
The key point: the analyst doesn’t receive the original question — it receives the researcher’s output as context. That’s what makes multi-agent work: one produces, the other consumes.
Real limitations
- Black box: coordination between agents is abstracted away. When something goes wrong, it’s hard to debug what each agent decided and why
- Less control: you don’t choose the order of tool calls or the conditional flow — the framework decides
- LLM overhead: each agent is a separate session. Two agents = more tokens, more latency, more cost. For simple problems, a single agent solves it faster
When it shines
Agent teams that collaborate. Researcher + writer + reviewer. Parallel tasks with clear roles. Rapid prototyping of multi-agent workflows.
3) Agno
Agno (formerly Phidata) is the most pragmatic of the three. The philosophy: an agent is a model + tools + instructions. No unnecessary abstractions. Plain Python functions become tools automatically — no special decorators needed.
It’s the most direct. Few lines, working agent.
Installation
pip install agno
Example
from agno.agent import Agent
from agno.models.openai import OpenAIChat
def lookup_price(product: str) -> str:
"""Look up the price of a product in USD. Available products: laptop, monitor, keyboard.
Args:
product: Product name to look up.
"""
catalog = {"laptop": 1200.00, "monitor": 450.00, "keyboard": 85.00}
price = catalog.get(product.lower())
if price:
return f"{product}: US$ {price:.2f}"
return f"Product '{product}' not found."
def convert_currency(amount: float, from_cur: str, to_cur: str) -> str:
"""Convert an amount between currencies. Available rates: USD↔BRL.
Args:
amount: Numeric amount to convert.
from_cur: Source currency (e.g. USD).
to_cur: Target currency (e.g. BRL).
"""
rates = {"USD_BRL": 5.20, "BRL_USD": 0.19}
key = f"{from_cur}_{to_cur}".upper()
rate = rates.get(key)
if rate:
return f"{amount:.2f} {from_cur} = {amount * rate:.2f} {to_cur}"
return f"Rate {from_cur} → {to_cur} not available."
def apply_discount(amount: float, percentage: float) -> str:
"""Apply a percentage discount to an amount.
Args:
amount: Original numeric amount.
percentage: Discount percentage to apply.
"""
final = amount * (1 - percentage / 100)
return f"Original: {amount:.2f} → With {percentage}% discount: {final:.2f}"
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[lookup_price, convert_currency, apply_discount],
instructions="Be direct. Always use the available tools to look up prices, convert currencies, and calculate discounts.",
markdown=True,
)
agent.print_response(
"I want to buy a laptop. How much in BRL with a 10% discount?",
stream=True,
)
What the agent does under the hood
The same ReAct loop as the other two:
🤔 Thinking: I need to find the price
🔧 Calling: lookup_price("laptop")
📎 Result: laptop: US$ 1200.00
🤔 Thinking: convert to BRL
🔧 Calling: convert_currency(1200.00, "USD", "BRL")
📎 Result: 1200.00 USD = 6240.00 BRL
🤔 Thinking: apply discount
🔧 Calling: apply_discount(6240.00, 10)
📎 Result: Original: 6240.00 → With 10% discount: 5616.00
✅ Answer: The laptop costs R$ 5,616.00 with a 10% discount.
Notice: same tools, same logic, same result. The difference is it took ~15 lines to define the agent, versus ~30 for LangGraph and ~35 for CrewAI.
Real limitations
- Complex workflows: for flows with conditional branching, controlled loops, or human-in-the-loop, Agno doesn’t have native primitives — you’d need to implement them manually
- Less mature: smaller ecosystem, smaller community, fewer production examples compared to LangGraph
- Less visibility: what the agent does “under the hood” is less transparent without extra debug configuration
When it shines
Shortest path from zero to a working agent. Any Python function becomes a tool. Excellent for rapid prototyping and for using local models (Ollama, LlamaCpp).
Comparison
| LangGraph | CrewAI | Agno | |
|---|---|---|---|
| Abstraction | Low (graph) | High (roles) | Medium (pragmatic) |
| Learning curve | Steeper | Gentle | Short |
| Multi-agent | Yes (manual) | Yes (native, with handoff) | Yes (native) |
| Tools | LangChain’s @tool | Own @tool | Plain Python functions |
| Best for | Complex workflows | Agent teams | Rapid prototypes |
| Fine-grained control | Full | Partial | Partial |
| Persistence | Built-in | Via config | Via sessions |
| Debug / visibility | Good (LangSmith) | Medium | Basic |
| Main risk | Unnecessary complexity | Black box | Limited for complex flows |
Which one to pick?
If you’re a beginner and want to understand agents hands-on: Agno. Least friction, least code, immediate feedback.
If you want speed to prototype with multiple agents: CrewAI. Define roles and tasks, the framework handles the rest.
If you’re heading to serious production with complex flows: LangGraph. More upfront work, but total control over every step.
All three are actively maintained and well documented. The most honest advice: pick one and build something. Switch later if you need to. The best way to learn is by experimenting.
In production
If the examples in this post are the starting point, production is a different story. A few things that matter when the agent leaves your notebook and enters the real world:
- Observability: log every tool call, every LLM decision, every loop iteration. Without logs, debugging agents is guesswork
- Retries and timeouts: tools fail, APIs go down, models take too long. Set limits. An agent stuck in an infinite loop burns tokens and money
- Guardrails: restrict which tools the agent can call, validate inputs before executing, limit the maximum number of iterations
- Cost: monitor tokens per execution. Three iterations with GPT-4o are cheaper than ten with the same model. Agent design directly affects the bill
None of these frameworks solve all of this automatically. They provide the skeleton. The rest is engineering.
A framework doesn’t solve the problem — it just organizes the chaos. A bad agent in LangGraph is still a bad agent in CrewAI or Agno. The difference is in the design, not the tool.
If you want to go deeper on agents, check out the post on the state of the art in AI agents.
Previous in series
Fackel: an autonomous pentest framework powered by ReAct agents