LangGraph, CrewAI, and Agno: getting started with AI agents in Python

🇧🇷 Ler em Português
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:

  1. Thinks — figures out what needs to be done
  2. Acts — calls a tool (search, calculator, API, database)
  3. Observes — reads the tool’s output
  4. 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:

  1. Look up the price of the product (in USD)
  2. Convert from dollars to reais
  3. 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

LangGraphCrewAIAgno
AbstractionLow (graph)High (roles)Medium (pragmatic)
Learning curveSteeperGentleShort
Multi-agentYes (manual)Yes (native, with handoff)Yes (native)
ToolsLangChain’s @toolOwn @toolPlain Python functions
Best forComplex workflowsAgent teamsRapid prototypes
Fine-grained controlFullPartialPartial
PersistenceBuilt-inVia configVia sessions
Debug / visibilityGood (LangSmith)MediumBasic
Main riskUnnecessary complexityBlack boxLimited 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.