AI Learning Hub Roadmap Resources Blog Projects Paths

AI Agent Fundamentals: How LLM Agents Think, Plan, and Act

· 5 min read · AI Learning Hub

Learning Objectives

  • Understand what an AI agent is and how it differs from a chatbot
  • Implement the ReAct (Reasoning + Acting) loop
  • Give agents tools and handle tool call results
  • Add memory so agents can reference prior context
  • Design multi-step task flows

What Is an AI Agent?

A chatbot generates a single response. An AI agent takes actions in a loop — reasoning about what to do, using tools, observing results, and repeating until a goal is achieved.

Chatbot:    User → LLM → Response
Agent:      User → [LLM → Tool → Observe → LLM → Tool → Observe → ...] → Final Answer

The key difference: an agent can use external tools (web search, code execution, APIs, databases) and decide autonomously which tools to use and in what order.


The ReAct Pattern

ReAct (Reasoning + Acting) interleaves reasoning traces with tool calls:

Thought: I need to find the current price of Bitcoin.
Action: search("Bitcoin price today")
Observation: Bitcoin is trading at $67,432 as of March 10, 2026.
Thought: I have the price. Now I can answer.
Action: finish("The current Bitcoin price is $67,432.")

This pattern dramatically improves reliability on multi-step tasks compared to single-shot prompting.


Agent Loop from Scratch

from openai import OpenAI
import json

client = OpenAI()

# ── Tool definitions ──────────────────────────────────────────────────────────

def calculator(expression: str) -> str:
    """Safely evaluate a math expression."""
    try:
        result = eval(expression, {"__builtins__": {}}, {})
        return str(result)
    except Exception as e:
        return f"Error: {e}"

def get_weather(city: str) -> str:
    """Mock weather tool."""
    weather_data = {
        "london": "15°C, cloudy",
        "tokyo": "22°C, sunny",
        "new york": "8°C, rainy",
    }
    return weather_data.get(city.lower(), f"No data for {city}")

TOOLS = {
    "calculator": calculator,
    "get_weather": get_weather,
}

TOOL_SCHEMAS = [
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "Evaluate a mathematical expression",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "Math expression to evaluate"}
                },
                "required": ["expression"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name"}
                },
                "required": ["city"],
            },
        },
    },
]

# ── Agent loop ────────────────────────────────────────────────────────────────

def run_agent(user_message: str, max_iterations: int = 10) -> str:
    messages = [
        {"role": "system", "content": "You are a helpful assistant with access to tools. Use them when needed."},
        {"role": "user",   "content": user_message},
    ]

    for iteration in range(max_iterations):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=TOOL_SCHEMAS,
            tool_choice="auto",
        )

        msg = response.choices[0].message
        messages.append(msg)

        # No tool call — final answer
        if not msg.tool_calls:
            return msg.content

        # Execute all tool calls
        for tool_call in msg.tool_calls:
            name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)

            if name in TOOLS:
                result = TOOLS[name](**args)
            else:
                result = f"Unknown tool: {name}"

            print(f"  [Tool] {name}({args}) → {result}")

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })

    return "Max iterations reached."


# Test
print(run_agent("What's 15% of 847?"))
print(run_agent("Is it a good day to visit Tokyo? What's the weather like?"))
print(run_agent("If I spend 2 hours commuting and 8 hours working, how many hours are left in my day?"))

Agent Memory

Without memory, agents can't reference earlier conversations. Add memory by maintaining a message history.

Short-Term Memory (In-Context)

class AgentWithMemory:
    def __init__(self, system_prompt: str):
        self.messages = [{"role": "system", "content": system_prompt}]

    def run(self, user_message: str) -> str:
        self.messages.append({"role": "user", "content": user_message})

        while True:
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=self.messages,
                tools=TOOL_SCHEMAS,
                tool_choice="auto",
            )

            msg = response.choices[0].message
            self.messages.append(msg)

            if not msg.tool_calls:
                return msg.content

            for tool_call in msg.tool_calls:
                name = tool_call.function.name
                args = json.loads(tool_call.function.arguments)
                result = TOOLS.get(name, lambda **k: "Unknown tool")(**args)
                self.messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": str(result),
                })

    def clear_memory(self):
        self.messages = [self.messages[0]]  # keep system prompt


agent = AgentWithMemory("You are a helpful research assistant.")
print(agent.run("What's 15% of 850?"))
print(agent.run("What was the number I just asked about?"))  # remembers 850

Long-Term Memory (External Store)

For persistent memory across sessions, store important facts in a vector database:

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

class MemoryStore:
    def __init__(self):
        self.db = Chroma(embedding_function=OpenAIEmbeddings())

    def remember(self, fact: str):
        self.db.add_texts([fact])

    def recall(self, query: str, top_k: int = 3) -> list[str]:
        results = self.db.similarity_search(query, k=top_k)
        return [r.page_content for r in results]

memory = MemoryStore()
memory.remember("User prefers Python over JavaScript.")
memory.remember("User is building a RAG system for legal documents.")

recalled = memory.recall("What is the user's project?")
print(recalled)  # ["User is building a RAG system for legal documents."]

Planning: Breaking Down Complex Tasks

For complex, multi-step tasks, have the agent create a plan before executing:

def plan_and_execute(task: str) -> str:
    # Step 1: Generate a plan
    plan_response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": """Break down the task into a numbered list of concrete steps.
Each step should be independently executable. Be specific."""},
            {"role": "user", "content": f"Task: {task}"},
        ],
    )
    plan = plan_response.choices[0].message.content
    print(f"Plan:\n{plan}\n")

    # Step 2: Execute each step
    results = []
    agent = AgentWithMemory("You are executing a step in a larger task. Be concise and precise.")

    for line in plan.split('\n'):
        line = line.strip()
        if not line or not line[0].isdigit():
            continue
        step = line.split('.', 1)[-1].strip()
        print(f"Executing: {step}")
        result = agent.run(step)
        results.append(f"{step}: {result}")

    # Step 3: Synthesize results
    synthesis = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "Synthesize the step results into a coherent final answer."},
            {"role": "user", "content": f"Original task: {task}\n\nResults:\n" + "\n".join(results)},
        ],
    )
    return synthesis.choices[0].message.content

Common Agent Patterns

Router Agent

Routes queries to specialized sub-agents:

AGENTS = {
    "math":    AgentWithMemory("You are a math expert."),
    "weather": AgentWithMemory("You are a weather assistant."),
    "general": AgentWithMemory("You are a general assistant."),
}

def route(query: str) -> str:
    router_response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": f"Route the query to one of: {list(AGENTS.keys())}. Reply with just the agent name."},
            {"role": "user",   "content": query},
        ],
    )
    agent_name = router_response.choices[0].message.content.strip().lower()
    agent = AGENTS.get(agent_name, AGENTS["general"])
    return agent.run(query)

Reflection Pattern

Agent evaluates its own output and improves it:

def agent_with_reflection(task: str) -> str:
    agent = AgentWithMemory("You are a careful assistant. Think step by step.")

    # First attempt
    response = agent.run(task)

    # Self-critique
    critique = agent.run(f"""
Review your previous response and identify:
1. Any errors or inaccuracies
2. Missing important information
3. Ways to make it clearer

Previous response: {response}
""")

    # Refined answer
    refined = agent.run("Based on your critique, provide an improved final answer.")
    return refined

Troubleshooting

Agent loops forever

  • Set max_iterations and return a graceful message when exceeded
  • Add a "finish" tool that the agent must call when done

Agent calls the wrong tool

  • Improve tool descriptions — be explicit about when to use each tool
  • Add examples in the tool description
  • Reduce the number of tools (fewer choices = clearer decisions)

Agent ignores available tools

  • Set tool_choice="required" to force tool use when appropriate
  • Move tool descriptions earlier in the system prompt

FAQ

What is the difference between an AI agent and a chatbot? A chatbot responds once. An agent acts in a loop, uses tools, and can autonomously complete multi-step tasks.

When should I use agents vs standard RAG? Use RAG when you need to answer questions about documents. Use agents when you need to take actions, use multiple tools, or complete tasks that require multi-step planning.

Are agents reliable enough for production? It depends on task complexity. Well-defined, narrow tasks (e.g., "look up a customer record and draft an email") can be production-ready. Open-ended, complex tasks have higher failure rates and need human oversight.


What to Learn Next

  • LangChain agents → langchain-agents-tutorial
  • Multi-agent systems → multi-agent-systems
  • Tool use and function calling → tool-use-and-function-calling
  • AI agent projectsAI Projects for Developers
← Back to all articles