Building AI Agents: From Chatbots to Autonomous Systems
on Ai, Agents, Llm, Autonomous systems, Openai, Anthropic, Langchain
Building AI Agents: From Chatbots to Autonomous Systems
The AI landscape has shifted dramatically. We’ve moved beyond simple chatbots to sophisticated AI agents that can reason, plan, and execute complex tasks autonomously. This guide explores the architecture, patterns, and best practices for building production-ready AI agents.
Photo by Alex Knight on Unsplash
What Are AI Agents?
An AI agent is a system that can:
- Perceive: Understand context and user intent
- Reason: Break down complex problems into steps
- Plan: Create actionable sequences
- Act: Execute tasks using tools and APIs
- Learn: Improve from feedback and outcomes
Unlike traditional chatbots that simply respond to queries, agents take initiative, make decisions, and accomplish goals.
Agent Architecture
┌─────────────────────────────────────────────────────────────┐
│ AI Agent │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Memory │ │ Reasoning │ │ Tool Interface │ │
│ │ │ │ Engine │ │ │ │
│ │ • Short-term│ │ • Planning │ │ • API Calls │ │
│ │ • Long-term │ │ • Reflection│ │ • Code Execution │ │
│ │ • Working │ │ • Decision │ │ • Database Access │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ └────────────────┼─────────────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ LLM Core │ │
│ │ (GPT/Claude) │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
The ReAct Pattern
The most effective agent pattern combines Reasoning and Acting:
from anthropic import Anthropic
class ReActAgent:
def __init__(self, tools: list[Tool]):
self.client = Anthropic()
self.tools = {tool.name: tool for tool in tools}
self.max_iterations = 10
def run(self, task: str) -> str:
messages = [{"role": "user", "content": task}]
for i in range(self.max_iterations):
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system=self._build_system_prompt(),
messages=messages,
tools=self._format_tools()
)
# Check for final answer
if response.stop_reason == "end_turn":
return self._extract_answer(response)
# Process tool calls
if response.stop_reason == "tool_use":
tool_results = self._execute_tools(response)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
return "Max iterations reached"
def _execute_tools(self, response) -> list:
results = []
for block in response.content:
if block.type == "tool_use":
tool = self.tools[block.name]
result = tool.execute(**block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
})
return results
Memory Systems
Effective agents need sophisticated memory:
Short-term Memory (Conversation Context)
class ConversationMemory:
def __init__(self, max_tokens: int = 8000):
self.messages = []
self.max_tokens = max_tokens
def add(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
self._trim_if_needed()
def _trim_if_needed(self):
while self._count_tokens() > self.max_tokens:
# Keep system message, remove oldest user/assistant pairs
if len(self.messages) > 2:
self.messages.pop(1)
Long-term Memory (Vector Store)
from chromadb import Client
import hashlib
class LongTermMemory:
def __init__(self):
self.client = Client()
self.collection = self.client.create_collection("agent_memory")
def store(self, content: str, metadata: dict = None):
doc_id = hashlib.md5(content.encode()).hexdigest()
self.collection.add(
documents=[content],
ids=[doc_id],
metadatas=[metadata or {}]
)
def recall(self, query: str, n_results: int = 5) -> list[str]:
results = self.collection.query(
query_texts=[query],
n_results=n_results
)
return results["documents"][0]
Photo by Growtika on Unsplash
Tool Design Patterns
Structured Tool Definitions
from pydantic import BaseModel, Field
from typing import Callable
class Tool(BaseModel):
name: str
description: str
parameters: dict
function: Callable
class Config:
arbitrary_types_allowed = True
def execute(self, **kwargs):
return self.function(**kwargs)
# Example: Web Search Tool
def web_search(query: str, num_results: int = 5) -> list[dict]:
"""Search the web and return results."""
# Implementation with search API
pass
search_tool = Tool(
name="web_search",
description="Search the web for current information. Use for recent events, facts, or research.",
parameters={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"num_results": {"type": "integer", "default": 5}
},
"required": ["query"]
},
function=web_search
)
Tool Categories
| Category | Examples | Use Case |
|---|---|---|
| Information | Web search, Wikipedia, News API | Research, fact-checking |
| Computation | Calculator, Code interpreter | Math, data analysis |
| Integration | Email, Calendar, CRM | Workflow automation |
| File System | Read, write, list files | Document processing |
| Database | SQL queries, vector search | Data retrieval |
Planning Strategies
Hierarchical Task Decomposition
class PlanningAgent:
def create_plan(self, goal: str) -> list[dict]:
prompt = f"""
Break down this goal into actionable steps:
Goal: {goal}
Return a JSON array of steps, each with:
- step_number: int
- action: string (what to do)
- tool: string (which tool to use, or "none")
- dependencies: list[int] (which steps must complete first)
- expected_output: string
"""
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}]
)
return json.loads(response.content[0].text)
def execute_plan(self, plan: list[dict]) -> dict:
results = {}
completed = set()
while len(completed) < len(plan):
for step in plan:
if step["step_number"] in completed:
continue
# Check dependencies
deps = set(step["dependencies"])
if not deps.issubset(completed):
continue
# Execute step
result = self._execute_step(step, results)
results[step["step_number"]] = result
completed.add(step["step_number"])
return results
Error Handling and Recovery
Robust agents need graceful failure handling:
class ResilientAgent:
def execute_with_retry(self, tool_call: dict, max_retries: int = 3):
for attempt in range(max_retries):
try:
result = self.tools[tool_call["name"]].execute(**tool_call["input"])
return {"success": True, "result": result}
except Exception as e:
if attempt == max_retries - 1:
return self._handle_failure(tool_call, e)
# Ask LLM to fix the call
fixed_call = self._request_fix(tool_call, str(e))
tool_call = fixed_call
def _request_fix(self, original_call: dict, error: str) -> dict:
prompt = f"""
This tool call failed:
{json.dumps(original_call)}
Error: {error}
Please provide a corrected tool call that might work.
"""
# Get LLM to suggest fix
...
Multi-Agent Systems
For complex tasks, orchestrate multiple specialized agents:
class AgentOrchestrator:
def __init__(self):
self.agents = {
"researcher": ResearchAgent(),
"writer": WritingAgent(),
"coder": CodingAgent(),
"reviewer": ReviewAgent()
}
async def delegate(self, task: str) -> str:
# Determine which agents needed
plan = await self._create_delegation_plan(task)
results = {}
for step in plan:
agent = self.agents[step["agent"]]
context = self._gather_context(step, results)
result = await agent.run(step["task"], context)
results[step["id"]] = result
return self._synthesize_results(results)
Observability and Debugging
Production agents need comprehensive logging:
import structlog
from opentelemetry import trace
class ObservableAgent:
def __init__(self):
self.logger = structlog.get_logger()
self.tracer = trace.get_tracer(__name__)
def run(self, task: str):
with self.tracer.start_as_current_span("agent_run") as span:
span.set_attribute("task", task[:100])
self.logger.info("agent_started", task=task)
try:
result = self._execute(task)
span.set_attribute("success", True)
self.logger.info("agent_completed", result_length=len(result))
return result
except Exception as e:
span.set_attribute("success", False)
span.record_exception(e)
self.logger.error("agent_failed", error=str(e))
raise
Security Considerations
Agents with tool access require careful security:
- Sandboxing: Execute code in isolated environments
- Permission Scoping: Limit tool access based on context
- Rate Limiting: Prevent runaway API calls
- Input Validation: Sanitize all tool inputs
- Audit Logging: Track all agent actions
class SecureAgent:
def __init__(self, permissions: set[str]):
self.permissions = permissions
def can_use_tool(self, tool_name: str) -> bool:
required = self.tools[tool_name].required_permissions
return required.issubset(self.permissions)
def execute_tool(self, tool_name: str, **kwargs):
if not self.can_use_tool(tool_name):
raise PermissionError(f"Agent lacks permission for {tool_name}")
# Validate inputs
self._validate_inputs(tool_name, kwargs)
# Execute in sandbox
return self._sandboxed_execute(tool_name, kwargs)
Conclusion
Building AI agents is both an art and a science. The key principles:
- Start Simple: Begin with ReAct, add complexity as needed
- Design Good Tools: Clear descriptions, validated inputs
- Implement Memory: Both short-term and long-term
- Plan for Failure: Retry logic, graceful degradation
- Monitor Everything: Logs, traces, metrics
The future of software is agentic. These systems will increasingly handle complex workflows, make decisions, and act on our behalf. Understanding how to build them well is essential for every developer.
What will you build with AI agents?
이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)
