DEV Community

Why do we import 100MB of frameworks to run a 50-line LLM reasoning loop?

The Three Pillars of an AI Agent

Any basic agent can be broken down into three simple components:

  • The State (Memory): A list of message dictionaries (role and content) passed to and from the LLM.
  • The Schema (Tools): A dictionary mapping tool names to standard Python functions.
  • The Loop (Reasoning): A standard while loop that calls the LLM, checks if it wants to use a tool, runs the tool if requested, appends the result to the State, and repeats until the LLM returns a final answer.

Coding the Agent (under 60 lines of Python)

This example uses the official openai SDK, but the same logic applies to Anthropic, Gemini, or local models running via Ollama.

import os
import json
from openai import OpenAI

# Initialize client
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

# 1. Define the tools our agent can use
def get_weather(location):
    if "tokyo" in location.lower():
        return "Tokyo is sunny and 25°C."
    return "Cool and rainy, 15°C."

# Map the function name to the actual function object
tools_map = {
    "get_weather": get_weather
}

# Define the JSON schema so the LLM knows how to call it
tool_definition = {
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current weather for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string"}
            },
            "required": ["location"]
        }
    }
}

# 2. The Agent reasoning loop
def run_agent(user_prompt):
    # Initialize the State (Memory)
    messages = [
        {"role": "system", "content": "You are a helpful assistant. Call tools when necessary."},
        {"role": "user", "content": user_prompt}
    ]

    # Run the loop (max 5 turns to prevent infinite runs)
    for _ in range(5):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=[tool_definition]
        )
        message = response.choices[0].message
        messages.append(message)

        # Check if the model wants to call a tool
        if message.tool_calls:
            for tool_call in message.tool_calls:
                name = tool_call.function.name
                args = json.loads(tool_call.function.arguments)
                print(f"[*] Calling tool: {name} with args: {args}")
                tool_output = tools_map[name](**args)

                # Append tool response back to state
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "name": name,
                    "content": tool_output
                })
        else:
            # If no tool was called, this is the final answer
            return message.content

# Run it
if __name__ == "__main__":
    result = run_agent("What is the weather like in Tokyo right now?")
    print(f"\n[Agent Response]: {result}")

Why You Should Start From Scratch

Building agents this way offers three massive advantages:

  • Zero-Abstraction Debugging: If your tool calls fail, you can simply print the raw JSON arguments payload or inspect lists. There is no custom library logic hidden from you.
  • Control Over Token Cost: Since you manage the messages array manually, you choose when to truncate history, summarize old messages, or prune system instructions.
  • Extreme Performance: You avoid the import overhead and instantiation lag of massive framework packages.

Before you reach for a heavy wrapper to orchestrate your next AI feature, try writing the loop yourself. You might find you only needed 50 lines of Python all along.

Are you building agents with raw loops or orchestration frameworks? Let's discuss in the comments!

Comments

No comments yet. Start the discussion.