AI Agents

An AI agent is an LLM that can reason about a task, decide which tools to call, observe the results, and iterate until the task is complete. Unlike a simple prompt-response interaction, agents operate in a loop - they think, act, observe, and repeat.

Most production agent failures between 2024 and 2026 were architectural, not model quality failures. The model is capable; the surrounding system (tool design, error handling, memory, guardrails) determines whether it works reliably.

How Agents Work

The ReAct Loop

Every agent follows the same fundamental cycle: Reason about what to do next, Act by calling a tool, Observe the result, then decide whether to continue or return a final answer.

Agent Architecture

The Agentic Reasoning Loop

The ReAct loop above compresses the cycle into three words: Reason, Act, Observe. In practice, "Reason" hides several distinct decisions the agent has to make before it ever touches a tool, and the loop has explicit entry and exit conditions. The walkthrough below breaks the cycle into the granularity needed for implementation planning - what an agentic system actually does, step by step, between receiving a task and returning an answer.

  1. User provides a task. The user gives the agent a task, not necessarily a question. Unlike a plain LLM call, the agent can decide for itself whether it needs external information before it responds.
  2. Agent understands the request. The agent converts the natural-language request into something it can reason about, analysing the user's goal, what information is required, and which tools are available to it.
  3. Planning. Before taking any action, the agent produces a plan, breaking a complex task into smaller sub-tasks it can execute one at a time.
  4. Tool selection. The agent decides whether a tool is required at all. If the task can be answered from reasoning alone, no tool call happens. If it needs external information or an action, the agent picks the most appropriate tool - a "search the internet" request needs a web search tool, not a database lookup.
  5. Tool calling. The agent invokes the selected tool, passing input in the format the tool expects. Example: tool web_search, input "latest stock price change".
  6. Tool execution. The tool performs the requested operation - a query, an API call, a lookup - and returns its output to the agent.
  7. Observation. The agent examines what the tool returned: the data itself, and whether the call succeeded or failed. This is the "observation" phase of the loop.
  8. Iterative reasoning loop. If the task isn't complete, the agent starts another cycle: Think -> Act -> Observe -> Evaluate -> Think Again. This repeats until the objective is met, the agent reaches a stopping condition, or it hits the maximum iteration limit (see Guardrails under Production Considerations below).
  9. Response construction and output. Once the agent has gathered enough information, it assembles the final response - combining tool outputs, dropping anything irrelevant, and organising the rest into a text answer, a report, or a dashboard, depending on what was asked.

This is the same Reason-Act-Observe cycle shown above, laid out at the granularity you need to design prompts, logging, and guardrails around it:

⚠️
Warning

Two failure modes show up repeatedly at Tool Selection and Tool Calling (steps 4 and 5): giving the agent too many tools to choose between, and leaving the rules for when to use a tool ambiguous. The first is expanded on below under Tool design principles ("fewer tools, better results"); the second means the agent's instructions should state explicitly when reasoning alone is sufficient and when a tool call is required.

Tool Use and Function Calling

Tools are functions the agent can invoke. The LLM decides which tool to call, with what arguments, based on the user's request and the tool descriptions you provide.

Defining tools

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_order_status",
            "description": "Look up the current status of a customer order by order ID",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "The order ID (e.g. ORD-12345)"
                    }
                },
                "required": ["order_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_knowledge_base",
            "description": "Search internal documentation for answers to policy or product questions",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search query"}
                },
                "required": ["query"]
            }
        }
    }
]

Tool calling flow

from openai import AzureOpenAI
import json

client = AzureOpenAI(...)

messages = [
    {"role": "system", "content": "You are a support agent. Use tools to answer questions."},
    {"role": "user", "content": "Where is my order ORD-98765?"}
]

# Step 1: LLM decides to call a tool
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools
)

tool_call = response.choices[0].message.tool_calls[0]
# tool_call.function.name = "get_order_status"
# tool_call.function.arguments = '{"order_id": "ORD-98765"}'

# Step 2: Execute the tool
result = get_order_status(order_id="ORD-98765")

# Step 3: Feed result back to LLM
messages.append(response.choices[0].message)
messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": json.dumps(result)
})

# Step 4: LLM generates final answer using tool result
final = client.chat.completions.create(model="gpt-4o", messages=messages)
print(final.choices[0].message.content)

Tool design principles

  • Descriptions matter more than names. The LLM reads the description to decide when to call a tool. Vague descriptions cause wrong tool selection.
  • Fewer tools, better results. 5-8 well-designed tools outperform 30 poorly-scoped ones. The model gets confused with too many options.
  • Return structured data. Tools should return JSON, not prose. Let the LLM format the response for the user.
  • Handle errors explicitly. Return error messages the LLM can reason about, not stack traces.
  • Idempotent where possible. Agents retry. If a tool creates a record, it should check if it already exists.

Agent Design Patterns

PatternHow it worksBest for
ReActReason-Act-Observe loop with toolsGeneral-purpose agents, most use cases
Plan-then-ExecuteGenerate full plan first, then execute stepsComplex multi-step tasks with dependencies
ReflectionAgent critiques its own output, then revisesWriting, code generation, quality-sensitive tasks
Tool-onlyNo reasoning loop, just route to correct toolSimple classification + action (chatbots, routing)

ReAct with LangGraph

from langgraph.prebuilt import create_react_agent
from langchain_openai import AzureChatOpenAI
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Get current weather for a city."""
    # Implementation here
    return f"22C, sunny in {city}"

@tool
def search_flights(origin: str, destination: str, date: str) -> str:
    """Search available flights between two cities on a date."""
    # Implementation here
    return f"3 flights found from {origin} to {destination} on {date}"

llm = AzureChatOpenAI(model="gpt-4o", temperature=0)
agent = create_react_agent(llm, tools=[get_weather, search_flights])

result = agent.invoke({
    "messages": [{"role": "user", "content": "I want to fly from London to Barcelona next Friday. What's the weather like there?"}]
})

Memory Systems

Agents need memory to maintain context across interactions and learn from past actions.

Memory typeWhat it storesPersistenceUse case
ConversationCurrent chat historySessionMulti-turn dialogue
WorkingCurrent task state, intermediate resultsTaskComplex multi-step tasks
Short-termRecent interactions summaryHours/daysPersonalisation within a session
Long-termUser preferences, facts, past decisionsPermanentCross-session personalisation

Production Considerations

Failure modes

FailureCauseMitigation
Infinite loopsAgent keeps calling tools without convergingSet max iterations (typically 5-10)
Cascading tool errorsFirst tool returns bad data, subsequent calls failValidate tool outputs, implement retry with backoff
Wrong tool selectionAmbiguous tool descriptionsImprove descriptions, reduce tool count, add examples
Hallucinated tool callsModel invents tool names or argumentsStrict schema validation, reject unknown tools
Cost runawayAgent loops burn tokensToken budget per task, circuit breaker on spend

Guardrails

MAX_ITERATIONS = 8
MAX_TOKENS_PER_TASK = 50000
ALLOWED_TOOLS = {"get_order_status", "search_knowledge_base", "create_ticket"}

def run_agent_with_guardrails(agent, user_input):
    iterations = 0
    total_tokens = 0
    
    while iterations < MAX_ITERATIONS:
        response = agent.step(user_input)
        total_tokens += response.usage.total_tokens
        
        if total_tokens > MAX_TOKENS_PER_TASK:
            return "Task exceeded token budget. Escalating to human."
        
        if response.tool_calls:
            for call in response.tool_calls:
                if call.function.name not in ALLOWED_TOOLS:
                    return f"Blocked: agent tried to call unauthorized tool {call.function.name}"
        
        if response.is_final:
            return response.content
        
        iterations += 1
    
    return "Agent did not converge. Escalating to human."

Observability

Every production agent needs tracing. You must be able to see:

  • What the agent reasoned at each step
  • Which tools it called and with what arguments
  • What each tool returned
  • How many iterations it took
  • Total token consumption and latency

Tools: LangSmith, Langfuse, Phoenix (Arize), Azure AI Foundry Tracing, OpenTelemetry.

Framework Comparison

FrameworkStrengthsBest for
LangGraphGraph-based control flow, checkpointing, human-in-the-loopComplex agents with branching logic
Azure Foundry AgentsManaged runtime, built-in tracing, Microsoft integrationEnterprise on Azure
OpenAI AssistantsSimple API, built-in file search and code interpreterQuick prototypes, OpenAI-only
AWS Bedrock AgentsAWS integration, knowledge bases, guardrailsEnterprise on AWS
Semantic Kernel.NET/Python, planner, plugin systemMicrosoft ecosystem developers

Key Takeaways

  • Agents are LLMs in a loop. The loop (ReAct) is simple; the hard part is tool design, error handling, and guardrails.
  • Tool descriptions determine agent quality more than model choice. Write them like API documentation, not casual comments.
  • Set hard limits: max iterations, token budgets, allowed tool lists. Agents without guardrails will surprise you in production.
  • Trace everything. You cannot debug an agent from its final output alone.
  • Start with a single ReAct agent and 3-5 tools. Add complexity only when the simple version demonstrably fails.