In this post you’ll learn how to build AI agents that do more than just a chain of if‑statements. I’ll show why a simple rule‑based wrapper falls apart, and walk you through the core pieces – memory, planning, and tool orchestration – that make an agent robust and reusable. You’ll walk away with a small code template you can adapt for your own projects.

Why a Pure If‑Statement Agent Breaks Quickly

When you first experiment with a language model, it’s tempting to write something like:

if user_input.contains('weather'):
    call_weather_api()
elif user_input.contains('joke'):
    tell_joke()
else:
    ask_for_clarification()

That looks fine for a demo, but it has three hidden problems:

  • Scalability – Adding a new capability means adding another elif. The file grows messy fast.
  • Context loss – The model never remembers previous turns, so it can’t handle multi‑step tasks.
  • Error handling – If the user asks something ambiguous, the hard‑coded branches may send the wrong request, wasting GPU time.

A reliable agent needs a bit more structure. Think of it as a small robot: it has a brain (the LLM), a notebook (memory), a planner (decides what to do next), and hands (tools) to act.

Core Components of a Well‑Behaved Agent

1. Memory (short‑term context)

Memory is a place to store facts from the conversation that the model can refer back to. It can be as simple as a list of recent messages or a vector store for semantic retrieval.

memory = []  # list of (role, content) tuples

def add_to_memory(role, content):
    memory.append((role, content))
    if len(memory) > 10:  # keep only last 10 turns
        memory.pop(0)
  • memory holds recent exchanges.
  • add_to_memory trims the list to keep it short, saving tokens.

2. Planner (decides the next action)

The planner asks the LLM to output a structured plan. A common pattern is to request JSON with a type field like "action" or "response".

import json

plan_prompt = """
You are an AI assistant. Based on the user request and the conversation history, decide what to do.
Return a JSON with two keys: \"type\" (either \"action\" or \"response\") and \"value\" (the tool name or the text to say).
"""
  • The prompt tells the model to produce machine‑readable output.
  • Using JSON makes downstream parsing safe.

3. Tool Wrapper (the "hands")

Each tool is a small function that the planner can call. Keep the interface uniform: tool_name(args) -> result.

def get_weather(city):
    # placeholder for real API call
    return f"Sunny in {city} today."

def tell_joke():
    return "Why did the programmer quit his job? Because he didn't get arrays."

tools = {
    "weather": get_weather,
    "joke": tell_joke,
}
  • tools is a dictionary mapping names to callables.
  • Adding a new tool is just a new entry, no extra if.

Putting It All Together: A Minimal Agent Loop

import json

while True:
    user = input('You: ')
    add_to_memory('user', user)

    # Build the prompt with memory
    context = "\n".join([f"{role}: {msg}" for role, msg in memory])
    prompt = f"{plan_prompt}\nConversation:{context}\nUser: {user}\n"

    # Ask the model for a plan (replace with your LLM call)
    plan_json = fake_llm_call(prompt)  # returns a JSON string
    plan = json.loads(plan_json)

    if plan["type"] == "action":
        tool_name = plan["value"]
        result = tools[tool_name]()
        add_to_memory('assistant', result)
        print('AI:', result)
    else:
        response = plan["value"]
        add_to_memory('assistant', response)
        print('AI:', response)
  • Collects user input and stores it.
  • Builds a prompt that includes recent memory.
  • Calls the LLM to get a JSON plan.
  • Executes the requested tool or sends a plain response.

Why This Works Better

  • Extensible – New tools are added without touching the loop.
  • Stateful – Memory lets the agent handle follow‑up questions like "What about tomorrow?".
  • Cost‑aware – The planner can decide to skip a tool call if the answer is already known, saving GPU cycles.

Common Pitfalls and How to Avoid Them

  1. Forgetting to sanitize tool output – Always treat tool results as untrusted data. Escape or validate before feeding back to the model.
  2. Unlimited memory growth – Tokens cost money. Trim or summarize older turns.
  3. Over‑relying on the model for parsing – If the model returns malformed JSON, fall back to a safe default or ask it to retry.
  4. Hard‑coding tool names in the prompt – Keep the prompt generic and let the tools dict be the single source of truth.

Quick Checklist Before Deploying

  • [ ] Memory window limited to a reasonable number of turns.
  • [ ] All tool functions return plain strings (no objects).
  • [ ] LLM prompt asks for JSON and includes clear error‑recovery instructions.
  • [ ] Logging of tool calls for audit and debugging.
  • [ ] Rate‑limit user inputs to avoid runaway loops.

FAQ

How do I store memory across server restarts?

You can serialize the memory list to a file or a lightweight database like SQLite. On startup, load it back into the list. Keep the size small to avoid large payloads.

What if the LLM returns an unknown tool name?

Treat it as a normal response: tell the user you don’t understand the request. You can also add a fallback that logs the unknown name for later analysis.

Can I use this pattern with non‑OpenAI models?

Absolutely. The only requirement is that the model can follow the instruction to output JSON. Adjust the API call accordingly.

Wrapping up

Building AI agents that survive beyond a demo means giving them memory, a planner, and a clean tool interface. Replace fragile if‑statements with a structured loop, and you’ll see fewer crashes, lower GPU bills, and happier users. Give this template a spin, add your own tools, and watch the agent grow without turning into a tangled mess of conditions.