Building AI Agent from Scratch: Minimal Implementation Without Frameworks
Hand-write a simple AI Agent framework to understand core principles - no LangChain or other heavyweight frameworks required.
Agent frameworks are powerful, but building from scratch is the fastest way to understand the core principles. This guide shows you how in about 130 lines of code.
Core Components
The simplest Agent needs just 4 parts:
- LLM Engine - model API calls
- Tool System - dynamic tool registration
- Memory - context management
- Loop - control flow
Implementation
1. LLM Engine: Unified Interface
from openai import AsyncOpenAI
from typing import List, Dict, Any
class LLMEngine:
def __init__(self, model: str = "gpt-4o", api_key: str = None):
self.client = AsyncOpenAI(api_key=api_key)
self.model = model
async def chat(
self,
messages: List[Dict],
tools: List[Dict] = None,
temperature: float = 0.7
) -> Dict[str, Any]:
response = await self.client.chat.completions.create(
model=self.model,
messages=messages,
tools=tools,
temperature=temperature
)
return {
"content": response.choices[0].message.content,
"tool_calls": response.choices[0].message.tool_calls,
"usage": response.usage.model_dump()
}
2. Tool System: Dynamic Registration
from typing import Callable, Dict
import inspect
class ToolRegistry:
def __init__(self):
self.tools: Dict[str, Callable] = {}
def register(self, name: str = None):
def decorator(func: Callable):
tool_name = name or func.__name__
self.tools[tool_name] = func
return func
return decorator
def get_tools_schema(self) -> List[Dict]:
tools = []
for name, func in self.tools.items():
sig = inspect.signature(func)
params = {
"type": "object",
"properties": {},
"required": []
}
for pname in sig.parameters:
params["properties"][pname] = {"type": "string"}
params["required"].append(pname)
tools.append({
"type": "function",
"function": {
"name": name,
"description": func.__doc__ or "",
"parameters": params
}
})
return tools
async def call(self, name: str, arguments: Dict) -> Any:
if name not in self.tools:
raise ValueError(f"Tool {name} not found")
return await self.tools[name](**arguments)
3. Memory: Context Management
from collections import deque
from dataclasses import dataclass, field
from typing import List
@dataclass
class Message:
role: str
content: str
tool_calls: List = field(default_factory=list)
class Memory:
def __init__(self, max_turns: int = 20):
self.messages: deque = deque(maxlen=max_turns)
def add(self, role: str, content: str, tool_calls: List = None):
self.messages.append(Message(role, content, tool_calls or []))
def to_openai_format(self) -> List[Dict]:
return [
{"role": m.role, "content": m.content}
for m in self.messages
]
4. Loop Control
class Agent:
def __init__(self, llm: LLMEngine, tools: ToolRegistry, max_iterations: int = 10):
self.llm = llm
self.tools = tools
self.max_iterations = max_iterations
self.memory = Memory()
async def run(self, prompt: str) -> str:
self.memory.add("user", prompt)
for i in range(self.max_iterations):
tools_schema = self.tools.get_tools_schema()
response = await self.llm.chat(
self.memory.to_openai_format(),
tools=tools_schema
)
if response["tool_calls"]:
tool_call = response["tool_calls"][0]
tool_name = tool_call.function.name
arguments = eval(f"dict({tool_call.function.arguments})")
self.memory.add("assistant", f"Using tool: {tool_name}", [tool_call])
result = await self.tools.call(tool_name, arguments)
self.memory.add("tool", str(result))
else:
self.memory.add("assistant", response["content"])
return response["content"]
return "Max iterations reached"
Usage Example
# Create Agent
llm = LLMEngine(model="gpt-4o")
tools = ToolRegistry()
@tools.register("calculate")
def calculate(expression: str) -> str:
"""Execute math calculation"""
return str(eval(expression))
@tools.register("weather")
def weather(city: str) -> str:
"""Query weather"""
return f"{city} is sunny, 25 degrees"
agent = Agent(llm, tools)
# Run
result = asyncio.run(agent.run("What's the weather like in Beijing?"))
print(result)
Summary
| Component | Purpose | Code Lines |
|---|---|---|
| LLM Engine | Unified API call | ~30 |
| Tool Registry | Dynamic tools | ~40 |
| Memory | Context management | ~25 |
| Loop | Control flow | ~35 |
Total approximately 130 lines. You can use frameworks like LangChain later, but understanding the principles lets you customize better.
Learning path: Run this minimal version first, then add streaming, reflection mechanisms, and multimodal capabilities step by step.
Related Articles
AI Coding Agent Ecosystem: A Practical Comparison of OpenClaw, Claude Code, Cursor, and Alternatives
A practical comparison of the leading AI coding agents in 2026, covering architecture, SWE-bench scores, pricing, and ideal use cases for each platform.
Claude vs GPT in 2026 - The AI Coding Assistant Battle Intensifies
As Anthropic's Claude Opus 4.6 challenges OpenAI's GPT-5.4 in coding benchmarks, the AI coding assistant market has become the most competitive segment in the AI industry. We analyze the technical and strategic differences.
NanoClaw vs OpenClaw: A Comprehensive Comparison Guide for AI Agent Selection
An in-depth comparison between NanoClaw and OpenClaw across architectural design, security isolation, ease of use, and ecosystem integration to help developers make informed decisions.
