A chatbot answers one question and stops. Python AI agents keep going: they read a goal, pick a tool, run it, check the result, and repeat until the job is done. That loop, driven by an LLM (Large Language Model), is the entire idea. This post builds it bare in plain code, then shows the same loop inside LangGraph, CrewAI, and the OpenAI and Claude SDKs.
You already met the first piece of this in the LLM API and function calling tutorial, where the model could call one function. An agent takes that and wraps a loop around it, adds memory, and lets the model decide which tool to reach for next. This post walks through the patterns first, then shows real, runnable code in the main frameworks: LangGraph, CrewAI, the OpenAI Agents SDK (Software Development Kit), the Claude Agent SDK, and Pydantic AI. The aim is to understand how agents work, not to memorise one library.
“The best agent is the simplest one that solves the problem. Start with a single LLM call. Add tools only when needed. Add memory only when required.”
Harrison Chase, LangChain
Last Updated: July 2026 | Tested on: Python 3.14.6, langgraph 1.2.6, langchain 1.3.10, openai 2.43.0, anthropic 0.111.0 | Difficulty: Advanced | Reading Time: 24 minutes
- LLM API and function calling tutorial
- prompt engineering tutorial
- pip install langgraph langchain-openai crewai openai anthropic
So here is the plain definition. An AI agent is a program that hands an LLM a goal, a set of tools, and a loop. The model reasons about the task, decides which tool to use, runs it, looks at what came back, and repeats until the job is done. A plain chatbot replies to one message and waits. An agent keeps working: it can search the web, query a database, write a file, or call an API (Application Programming Interface), all on its own, until it has an answer.
The framework scene has grown up fast. LangGraph draws the agent as a graph. CrewAI runs a team of agents that pass work to each other. Both OpenAI and Anthropic ship their own agent SDKs, and Pydantic AI adds type-safe, validated output. They all sit on top of the same loop, so once you see the loop, every framework starts to look familiar. By the end of this post you will understand the observe-think-act loop (often called ReAct), how tools plug in, how memory works, and what makes an agent reliable enough to trust in production.
Table of Contents
The Agent Loop: Observe, Think, Act
Every AI agent, no matter which framework wraps it, runs the same three-beat loop. It observes (reads the input or the result of the last tool), thinks (the LLM reasons about what to do next), and acts (calls a tool, or just answers). Then it loops back and observes again. It keeps spinning until the task is finished or it hits a maximum number of rounds.
It is the same loop a cook follows. Taste the sauce (observe), decide it needs salt (think), add a pinch and stir (act), then taste again. Round and round until it is right, then plate up. The cook does not follow a fixed recipe blindly; they react to what is in the pan. An agent reacts to what comes back from each tool the same way.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram shows the AI agent loop in action. The agent takes a task, the LLM reasons about what to do, it picks and runs a tool (web search, code execution, an API call), reads the result, and then decides: keep going, or hand back the final answer. Memory feeds into the thinking step, both the short-term chat history and any long-term facts pulled from a vector database. Because the cycle repeats, an agent can solve multi-step problems that a single LLM call never could. That tool use is the real difference from a basic chatbot: an agent can act in the world, not just produce text.
Quick Win: An Agent Loop with No API Key
Frameworks and API keys can wait. Think of it like learning to drive in an empty parking lot before you take on the highway: no traffic, no risk, just you and the controls. The fastest way to feel how an agent works is to build one where the “brain” is just a few plain rules instead of an LLM. No keys, no installs, runs on the Python standard library. Watch the loop pick a tool, run it, store the result, and stop. This is the skeleton every real agent is built on.
📄 local_agent.py: the observe-think-act loop with no LLM and no API key
# Niranjan builds a tiny agent with NO LLM, just to see the loop run
import re
def search_web(query: str) -> str:
facts = {
"python": "Python 3.14.6 added an official free-threaded build (PEP 779).",
"langgraph": "LangGraph models an agent as a graph of nodes and edges.",
}
for key, val in facts.items():
if key in query.lower():
return val
return "No results."
def calculate(expression: str) -> str:
return str(eval(expression, {"__builtins__": {}}, {}))
tools = {"search_web": search_web, "calculate": calculate}
# A fake "brain": instead of an LLM, plain rules pick the next tool.
def think(task, scratchpad):
if "calculate" not in scratchpad and re.search(r"[\d+\-*/]", task):
expr = re.search(r"([\d\s+\-*/()]+\d)", task).group(1).strip()
return ("calculate", {"expression": expr})
if "search_web" not in scratchpad:
return ("search_web", {"query": task})
return ("done", {})
def run_agent(task, max_steps=5):
scratchpad = {}
for step in range(1, max_steps + 1):
action, args = think(task, scratchpad)
if action == "done":
print(f"[Agent finished in {step} step(s)]")
return scratchpad
print(f" Step {step}: {action}({args})")
result = tools[action](**args)
print(f" -> {result}")
scratchpad[action] = result
return scratchpad
answer = run_agent("Tell me about python and what is 2**10 * 3")
print("\nFinal scratchpad:")
for k, v in answer.items():
print(f" {k}: {v}")
▶ Output
Step 1: calculate({'expression': '2**10 * 3'})
-> 3072
Step 2: search_web({'query': 'Tell me about python and what is 2**10 * 3'})
-> Python 3.14.6 added an official free-threaded build (PEP 779).
[Agent finished in 3 step(s)]
Final scratchpad:
calculate: 3072
search_web: Python 3.14.6 added an official free-threaded build (PEP 779).
What happened here: The think function is standing in for the LLM. On each pass it looks at the scratchpad (the agent’s short-term memory), decides the next tool, and the loop runs it. First it spots a math expression and calls calculate, then it searches, then it sees both jobs are done and stops on step 3. Swap that rule-based think for a real LLM call and you have a genuine AI agent. Everything else, the loop, the tool dispatch, the memory, stays exactly the same. That is the trick frameworks hide from you, and now you have seen it bare. Every one of the Python AI agents later in this post is this same loop wearing a framework.
Building a Real Agent with the OpenAI API
Now swap the rule-based brain for a real LLM. Picture the difference between a vending machine that always runs the same fixed steps and a smart assistant who reads your request and reaches for the right tool on their own: that is what handing the decision to the model buys you. This version uses the OpenAI API so the model itself decides which tool to call. The structure is the same loop you just saw, with a bit more bookkeeping for message history and tool results.
This snippet needs an OpenAI API key and makes paid calls, so the code below is for reading and adapting rather than running as-is; the output shown is one real run captured for the article.
📄 simple_agent.py: an AI agent in about 50 lines, no framework
from openai import OpenAI
import json
client = OpenAI()
# Rahul builds a research agent with two tools
def search_web(query: str) -> str:
"""Pretend web search over a tiny offline knowledge base."""
results = {
"Python 3.14.6 features": "Python 3.14.6 ships an officially supported free-threaded build (PEP 779) and keeps the experimental JIT (PEP 744).",
"LangGraph tutorial": "LangGraph uses directed graphs to define agent workflows with shared state.",
"CrewAI agents": "CrewAI runs role-based agents that collaborate through a task pipeline.",
}
for key, val in results.items():
if any(word.lower() in query.lower() for word in key.split()):
return val
return f"No results found for: {query}"
def calculate(expression: str) -> str:
"""Safe math evaluation."""
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"Error: {e}"
tools = [
{"type": "function", "function": {
"name": "search_web", "description": "Search the web for information",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
}},
{"type": "function", "function": {
"name": "calculate", "description": "Evaluate a math expression",
"parameters": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]},
}},
]
tool_map = {"search_web": search_web, "calculate": calculate}
def run_agent(user_message, max_iterations=5):
"""Run the agent loop until task is complete or max iterations reached."""
messages = [
{"role": "system", "content": "You are a helpful research assistant. Use tools when needed. Think step by step."},
{"role": "user", "content": user_message},
]
for i in range(max_iterations):
response = client.chat.completions.create(
# gpt-5.4-mini is OpenAI's low-cost tier (current at the time of writing;
# models change fast, check the provider docs)
model="gpt-5.4-mini", messages=messages, tools=tools, tool_choice="auto",
)
msg = response.choices[0].message
if not msg.tool_calls:
# No tool needed, the agent has its final answer
print(f"\n[Agent completed in {i + 1} iteration(s)]")
return msg.content
# Process tool calls
messages.append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
print(f" [Tool] {tc.function.name}({args})")
result = tool_map[tc.function.name](**args)
print(f" [Result] {result[:100]}")
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
return "Agent reached maximum iterations."
# Run the agent
answer = run_agent("What are the new features in Python 3.14.6? Also, what is 2**10 * 3?")
print(f"\nFinal Answer:\n{answer}")
▶ Output
[Tool] search_web({'query': 'Python 3.14.6 features'})
[Result] Python 3.14.6 ships an officially supported free-threaded build (PEP 779) and keeps the experimental J
[Tool] calculate({'expression': '2**10 * 3'})
[Result] 3072
[Agent completed in 2 iteration(s)]
Final Answer:
Here is what I found:
Python 3.14.6 new features:
- Free-threaded build (PEP 779), now officially supported, so you can run Python without the GIL
- JIT compiler (PEP 744), still experimental, for a speed boost on hot code
Math: 2**10 * 3 = 3072
What happened here: The agent got a two-part question, worked out that it needed two tools (search and calculator), called both, then wrote a final answer from the results. The whole agent is a for loop around the OpenAI API with tool dispatch bolted on. The exact wording of the final answer is the model’s, so it varies run to run; the tool calls and the math are deterministic. Frameworks like LangGraph and CrewAI add state handling, error recovery, conditional branching, and multi-agent coordination on top of this same loop.
(The output above is one captured run. The call needs an OpenAI API key and costs money, so it is shown for reading, not as something the article ran on every build.)
gpt-5.4-mini is OpenAI’s low-cost tier at the time of writing. Model names change often, so always check the provider docs for the current id before you ship. The same code works with any OpenAI chat model that supports tool calling, just by changing the model string.LangGraph: Graph-Based Agent Orchestration
LangGraph draws the agent as a graph. Each node is a function (an LLM call, a tool run, a yes-or-no decision), and the edges say what flows to what. Picture a subway map: each station is a step, the lines are the routes between them, and you can trace exactly where a journey goes and where it branches. That makes a complicated workflow easy to see, debug, and test, instead of being buried inside one for loop like our first agent.
📄 langgraph_agent.py: a stateful agent with LangGraph
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from typing import TypedDict, Annotated
import operator
# Anvay builds a LangGraph agent
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
iteration: int
llm = ChatOpenAI(model="gpt-5.4-mini", temperature=0)
def should_continue(state: AgentState) -> str:
"""Decide whether to loop back to the agent or stop."""
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return "end"
def call_model(state: AgentState) -> AgentState:
"""Call the LLM."""
response = llm.invoke(state["messages"])
return {"messages": [response], "iteration": state["iteration"] + 1}
def call_tools(state: AgentState) -> AgentState:
"""Execute tool calls."""
last_message = state["messages"][-1]
results = []
for tc in last_message.tool_calls:
# Execute tools here (simplified)
result = f"Result for {tc['name']}: simulated tool output"
results.append({"role": "tool", "content": result, "tool_call_id": tc["id"]})
return {"messages": results, "iteration": state["iteration"]}
# Build the graph
graph = StateGraph(AgentState)
graph.add_node("agent", call_model)
graph.add_node("tools", call_tools)
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", "end": END})
graph.add_edge("tools", "agent") # After tools, go back to agent
app = graph.compile()
# Run it
result = app.invoke({
"messages": [HumanMessage(content="What is the capital of France?")],
"iteration": 0,
})
print(f"Agent completed in {result['iteration']} iteration(s)")
print(f"Final: {result['messages'][-1].content}")
▶ Output
Agent completed in 1 iteration(s) Final: The capital of France is Paris.
What happened here: The StateGraph lays out the agent as two nodes, agent and tools, joined by edges. The conditional edge after the agent node asks a simple question: did the model ask for a tool? If yes, go to the tools node; if no, stop. After a tool runs, control flows back to the agent. Because the routing lives in the graph, things that get messy inside a plain loop (running tools in parallel, pausing for a human to approve a step, branching on a condition) stay clear and easy to follow.
One honest caveat: in this minimal demo the tools branch never actually fires, because the llm has no tools bound (there is no .bind_tools([...]) call), so the model can never return tool_calls and the graph always exits after one model call. The tools node is wiring shown for structure. To watch that branch run for real, bind at least one tool, for example llm = ChatOpenAI(...).bind_tools([your_tool]), and ask a question that needs it.
The graph wiring shown here is verified on langgraph 1.2.6; the live LLM call needs an OpenAI key, so the answer text above is one captured run.
CrewAI: Multi-Agent Teams
CrewAI goes a different way. Instead of one agent juggling many tools, you build a small team of specialists who hand work to each other. Think of a newsroom: a reporter digs up the facts, a writer turns them into an article, and an editor checks it before it goes out. Each CrewAI agent has a role, a goal, and its own tools, and a manager can route each job to the right person. In practice, plenty of production Python AI agents end up as small teams like this once a single agent has too many tools to juggle.
📄 crewai_team.py: multi-agent collaboration with CrewAI
from crewai import Agent, Task, Crew, Process
# Viraj sets up a content creation crew
researcher = Agent(
role="Technical Researcher",
goal="Find accurate, up-to-date information about Python and AI",
backstory="You are a senior developer who reads research papers and documentation.",
llm="gpt-5.4-mini",
verbose=True,
)
writer = Agent(
role="Technical Writer",
goal="Write clear, engaging tutorial content for Python developers",
backstory="You write for TechnoScripts.com. Be concise, use code examples, avoid filler.",
llm="gpt-5.4-mini",
verbose=True,
)
reviewer = Agent(
role="Code Reviewer",
goal="Verify all code examples are correct and follow Python best practices",
backstory="You are a senior Python developer. You catch bugs, security issues, and anti-patterns.",
llm="gpt-5.4-mini",
verbose=True,
)
# Define tasks
research_task = Task(
description="Research the top 3 Python AI agent frameworks in 2026. Compare their architectures.",
agent=researcher,
expected_output="A comparison of LangGraph, CrewAI, and Pydantic AI with pros/cons.",
)
writing_task = Task(
description="Write a 500-word tutorial section based on the research.",
agent=writer,
expected_output="A tutorial section with code examples.",
context=[research_task], # Depends on research
)
review_task = Task(
description="Review the tutorial for technical accuracy and code correctness.",
agent=reviewer,
expected_output="A list of corrections or 'Approved' if everything is correct.",
context=[writing_task],
)
# Assemble the crew
crew = Crew(
agents=[researcher, writer, reviewer],
tasks=[research_task, writing_task, review_task],
process=Process.sequential, # Or Process.hierarchical for manager delegation
verbose=True,
)
# result = crew.kickoff() # Uncomment to run (needs an OpenAI API key)
print("Crew configured with 3 agents and 3 sequential tasks.")
print("Agent roles: Researcher -> Writer -> Reviewer")
print("Process: Sequential (each task feeds into the next)")
▶ Output
Crew configured with 3 agents and 3 sequential tasks. Agent roles: Researcher -> Writer -> Reviewer Process: Sequential (each task feeds into the next)
What happened here: CrewAI captures teamwork. Each agent has a clear role, and they pass work down a pipeline: the researcher gathers facts, the writer turns them into a draft, the reviewer checks it. The context parameter is the handoff. It feeds one agent’s output into the next, so the writer sees the research and the reviewer sees the draft, just like passing a document down a row of desks.
We print the setup rather than call crew.kickoff(), because the real run needs an OpenAI key and makes paid calls; the printed lines above are the actual output of the configuration code, verified on crewai 1.15.1. One version note: crewai currently supports Python 3.10 to 3.13, so run this snippet on Python 3.13 or earlier (pip will refuse to install it on 3.14). This style fits content creation, data analysis, and multi-step software workflows well.
The Claude Agent SDK
If you want the agent loop wired to Claude instead of OpenAI, it is the same recipe cooked in a different kitchen: the steps do not change, only the brand of the appliances. The official anthropic SDK gives you the same observe-think-act shape with a clean tool API. You define your tools, call client.messages.create(...) with tools=[...], and check stop_reason. When Claude wants a tool, you run it and send a tool_result back; when it is done, you read the text. The code below is the manual loop, so you keep full control (approve a step, log it, retry a failure).
📄 claude_agent.py: a Claude-native agent loop with the anthropic SDK
from anthropic import Anthropic
import json
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
def get_weather(city: str) -> str:
"""A stand-in tool. A real one would call a weather API."""
return json.dumps({"city": city, "temp_c": 31, "sky": "clear"})
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a city. Call this when the user asks about weather.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string", "description": "City name"}},
"required": ["city"],
},
}
]
def run_agent(question, max_turns=5):
"""Manual observe-think-act loop on the Anthropic Messages API."""
messages = [{"role": "user", "content": question}]
for turn in range(1, max_turns + 1):
response = client.messages.create(
model="claude-opus-4-8", # current at the time of writing; check the docs
max_tokens=1024,
thinking={"type": "adaptive"}, # let Claude decide how much to think
tools=tools,
messages=messages,
)
if response.stop_reason != "tool_use":
# No tool requested, Claude has its final answer
text = "".join(b.text for b in response.content if b.type == "text")
print(f"[Agent finished in {turn} turn(s)]")
return text
# Claude asked for a tool. Run it and send the result back.
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type == "tool_use":
print(f" [Tool] {block.name}({block.input})")
out = get_weather(**block.input)
results.append({"type": "tool_result", "tool_use_id": block.id, "content": out})
messages.append({"role": "user", "content": results})
return "Agent reached the maximum number of turns."
print(run_agent("What is the weather in Pune right now?"))
▶ Output (illustrative, requires an API key)
[Tool] get_weather({'city': 'Pune'})
[Agent finished in 2 turn(s)]
Right now in Pune it is about 31C with clear skies.
What happened here: The loop is the same as before, just spoken in Anthropic’s API. Claude reads the question, decides it needs the weather tool, and returns a tool_use block. You run get_weather, append the answer as a tool_result, and call the API again. This time Claude has what it needs and replies with plain text, so stop_reason is no longer "tool_use" and the loop stops. The thinking={"type": "adaptive"} setting lets Claude decide how hard to think per request. This code is syntax-checked and matches the current SDK, but a real call needs an API key and costs money, so the output above is illustrative.
Use the exact model id claude-opus-4-8 from the provider docs; model names change fast, so check the docs before you ship.
Framework Quick Comparison
📄 framework_comparison.py: choosing the right agent framework
# Pravin compares the agent framework landscape
frameworks = {
"LangGraph": {
"approach": "Graph-based state machine",
"best_for": "Complex conditional workflows",
"multi_agent": "Yes, via sub-graphs",
},
"CrewAI": {
"approach": "Role-based multi-agent teams",
"best_for": "Research, writing, analysis",
"multi_agent": "Native, built for it",
},
"OpenAI Agents SDK": {
"approach": "Tool-use with agent handoffs",
"best_for": "OpenAI-native apps",
"multi_agent": "Yes, via handoffs",
},
"Claude Agent SDK": {
"approach": "Message-based with tool use",
"best_for": "Claude apps, long context",
"multi_agent": "Manual orchestration",
},
"Pydantic AI": {
"approach": "Type-safe agents (Pydantic)",
"best_for": "Structured, validated output",
"multi_agent": "Manual orchestration",
},
}
print(f"{'Framework':<20} {'Approach':<32} {'Best For':<30}")
print("=" * 82)
for name, info in frameworks.items():
print(f"{name:<20} {info['approach']:<32} {info['best_for']:<30}")
print("\nRecommendation:")
print(" Start simple: raw OpenAI/Anthropic + function calling")
print(" Single agent: LangGraph or Pydantic AI")
print(" Multi-agent: CrewAI or LangGraph sub-graphs")
print(" OpenAI ecosystem: OpenAI Agents SDK")
print(" Claude ecosystem: Claude Agent SDK")
▶ Output
Framework Approach Best For ================================================================================== LangGraph Graph-based state machine Complex conditional workflows CrewAI Role-based multi-agent teams Research, writing, analysis OpenAI Agents SDK Tool-use with agent handoffs OpenAI-native apps Claude Agent SDK Message-based with tool use Claude apps, long context Pydantic AI Type-safe agents (Pydantic) Structured, validated output Recommendation: Start simple: raw OpenAI/Anthropic + function calling Single agent: LangGraph or Pydantic AI Multi-agent: CrewAI or LangGraph sub-graphs OpenAI ecosystem: OpenAI Agents SDK Claude ecosystem: Claude Agent SDK
What happened here: No single framework wins everything, so the table is a starting map, not a ranking. Start as simple as the job allows: if one API call with function calling does it, you do not need a framework at all. Reach for LangGraph or Pydantic AI when a single agent needs real structure, and for CrewAI or LangGraph sub-graphs when a team of agents must divide the work. That gradual escalation, plain loop first, framework only when it earns its keep, is how reliable Python AI agents usually get built. (This snippet is plain standard-library Python, so the table above is its exact, tested output.)
Common Mistakes
- No iteration limit: Without a cap, an agent can spin forever, calling tool after tool and never settling on an answer (which also burns money on every call). Always set a max, around 5 to 10 rounds for most tasks. Every example in this post has one.
- Reaching for a framework too soon: If one API call with function calling solves the problem, you do not need LangGraph or CrewAI. Start with the plain loop, add a framework only when the workflow really needs structure.
- No plan for when a tool fails: Tools break and APIs time out. Catch the error, hand it back to the model as the tool result, and let the agent try another path instead of crashing the whole run.
Interview Corner
Q: What is the difference between a chatbot and an AI agent?
A chatbot responds to messages in a single turn: user asks, model answers. An AI agent operates in a loop: it reasons about the task, decides which tools to use, executes them, observes results, and iterates until the task is complete. The key difference is autonomy. Agents take actions in the world (calling APIs, querying databases, writing files) rather than just generating text.
Practice Exercises
- Exercise 1: Start from the no-API
local_agent.pyin this post. Add a third tool (say, aword_countfunction) and a rule inthinkthat calls it, then run it to confirm the loop picks the new tool. - Exercise 2: Add error handling to the OpenAI agent: wrap each tool call in a
try/except, and when a tool raises, send the error back as the tool result so the model can recover instead of the script crashing. - Exercise 3: Give an agent long-term memory. After each run, save the final answer to a small JSON file, and on the next run load it so the agent can refer back to what it said before. (Then read the vector databases tutorial to do the same with semantic search.)
More in this series:
- GenAI: QLoRA Fine-Tuning with Unsloth on a Free Colab GPU
- Build an MCP Server in Python (FastMCP, Step by Step)
- Python: LangChain vs LlamaIndex vs CrewAI, AI Agent Frameworks Compared
Frequently Asked Questions
What is an AI agent in Python?
An AI agent in Python is a program that hands an LLM a goal, a set of tools, and a loop. The model reasons about the task, picks a tool, runs it, reads the result, and repeats until the job is done. Unlike a chatbot that answers once and stops, an agent keeps working and can search the web, query a database, or call an API on its own. That working loop is what separates Python AI agents from plain chat scripts.
Which agent framework should I start with?
Start with raw function calling (LLM API and function calling tutorial) so you understand the loop. When you outgrow a plain loop, move to LangGraph for single-agent workflows or CrewAI for multi-agent teams. The framework matters less than the pattern, because all of them run the same observe-think-act loop underneath.
How much do AI agents cost to run?
An agent makes several LLM calls per task, so a simple agent with 3 to 5 tool calls costs roughly 3 to 5 times a single API call. A multi-agent crew with 10 or more turns costs more. Keep costs down by using a cheap model (for example OpenAI's mini tier or Claude Haiku) for the tool-calling steps and a stronger model only for the final answer. Always check the provider's current pricing, since rates change often.
How do agents remember across conversations?
Short-term memory is just the message history inside one session. Long-term memory needs outside storage: a vector database (vector databases tutorial) for semantic search over past chats, or a simple key-value store for user preferences. Most frameworks give you a memory helper, but you can also build it yourself with a database and Retrieval-Augmented Generation, or RAG (RAG tutorial).
How do I stop an agent from looping forever?
Always set a maximum number of iterations, around 5 to 10 for most tasks, and stop the loop the moment the model returns an answer with no tool call. Every code example in this post does both. A token or cost budget on top of that gives you a second safety net for long agentic runs.
Should I use the OpenAI API or the Claude Agent SDK?
Both run the same observe-think-act loop, so pick by ecosystem. Use the OpenAI API if your stack is already OpenAI. Use the official anthropic SDK and the Claude Agent SDK for Claude-native apps, long-context tasks, and adaptive thinking. Model names on both sides change fast, so always confirm the current model id in the provider docs before shipping.
Interview Questions on Python AI Agents
Interviewers rarely ask for definitions. They ask what happens in situations like these.
Q: Your agent keeps calling tools and never returns a final answer, hitting the max iteration cap on every run. What do you check first?
Start with the loop's early-exit condition. The loop should stop the moment the model returns a message with no tool calls (OpenAI's msg.tool_calls is empty) or a stop_reason that is not "tool_use" (Anthropic). If that check is missing or wrong, the agent never exits before the cap. Next, confirm each tool result is actually appended back to the message history: if the model never sees a tool's output, it keeps re-requesting the same tool forever. The iteration cap is the safety net, not the real exit.
Q: Your agent's token bill spikes sharply after you add one new tool. Where do you look?
Every agent step re-sends the full message history, so cost grows with both the number of iterations and the size of what you feed back each turn. Check whether the new tool returns a large payload that gets appended verbatim on every pass, and trim or summarize it before it goes back to the model. Then confirm your max_iterations cap is sane (5 to 10 for most tasks), and consider using a cheaper model for the tool-calling steps with a stronger one only for the final answer.
Q: What is the ReAct pattern, and how does it map to the code in this post?
ReAct stands for Reason plus Act. The model reasons about the task, takes an action (a tool call), observes the result, and reasons again. In our code that is exactly the observe-think-act loop: read the input or the last tool result, let the LLM decide the next step, run the tool, and repeat until it answers with no tool call. Every framework in this post wraps that same loop.
Q: Why model an agent as a graph in LangGraph instead of a plain for loop?
A graph makes the control flow explicit. Nodes are steps (an LLM call, a tool run) and edges say what flows where, including conditional edges that branch on whether the model asked for a tool. That keeps otherwise-messy patterns clear: running tools in parallel, pausing for a human to approve a step, or branching on a condition. In a plain for loop those all get buried inside bookkeeping and are harder to test.
Q: When would you reach for CrewAI over a single LangGraph agent?
Use CrewAI when the work splits cleanly into roles that hand off to each other, like a researcher, a writer, and a reviewer working in sequence. Each agent has its own role, goal, and tools, and the context parameter passes one agent's output to the next. For a single agent juggling many tools with complex branching, LangGraph's graph model fits better. One practical note: crewai currently supports Python 3.10 to 3.13, so run it on Python 3.13 or earlier.
Q: A tool raises an exception mid-run and the whole script crashes. How should the agent handle it instead?
Wrap each tool call in try/except, catch the error, and send it back to the model as the tool result (for Anthropic, set is_error to true). The agent then sees the failure as an observation and can try a different tool or path, rather than the process dying. Tools break and APIs time out, so treating an error as data, not a crash, is a big part of what makes an agent reliable in production.
Q: Why does the OpenAI example call json.loads(tc.function.arguments) instead of using the arguments directly?
The model returns tool-call arguments as a JSON string, not a ready-made Python dictionary, so you must parse it with json.loads before calling the function. On Python 3.14.6 with current models, the escaping of Unicode or slashes inside that string can vary, so always parse it rather than pattern-matching the raw text. Passing the parsed dictionary with **args then maps its keys straight onto the tool's keyword arguments.
What’s Next?
You can now build Python AI agents at every level, from a bare loop all the way up to a multi-agent crew. You have seen the observe-think-act loop that every framework runs, how tools plug in, how short-term and long-term memory feed the thinking step, and the guardrails (an iteration cap, tool error handling) that keep an agent reliable enough to trust in production. Next, in the LangChain vs LlamaIndex comparison, we put these frameworks side by side on the same task so you can see the real differences and pick the right one for your project. For the full roadmap and every tutorial in order, head back to the Python + AI/ML tutorial series home.
Related Posts
Next: Model Context Protocol (MCP): Connect Your AI to Anything
Series Home: Python + AI/ML Tutorial Series

No comment