AI Agent Architecture: From Chatbots to Autonomous Systems
Deep dive into building AI agents that can plan, take actions, and complete multi-step tasks autonomously. Covers ReAct, tool use, memory, and multi-agent patterns.

DevForge Team
AI Development Educators

What is an AI Agent?
An AI agent is a system that uses an LLM as its reasoning engine to take actions toward a goal, rather than just answer a single question. The key difference:
- Chatbot: User asks question → AI answers → done
- Agent: User gives goal → AI plans steps → AI takes actions → AI observes results → AI adapts → goal achieved (or explained why not)
Agents can browse the web, run code, call APIs, read/write files, send emails, query databases, and more — all autonomously, with the LLM deciding what to do based on results.
This is genuinely powerful and genuinely different from previous automation approaches. The LLM's reasoning ability means agents can handle situations that rigid rule-based automation cannot.
The Core Loop: ReAct
The most common agent architecture is ReAct (Reason + Act):
- Reason: LLM analyzes the current state and decides what to do
- Act: LLM calls a tool/function
- Observe: LLM receives the tool result
- Repeat: Until the goal is achieved
Goal: "Find the top 3 trending JavaScript repositories on GitHub today,
summarize what each does, and format as a markdown table."
Thought: I need to search GitHub for trending JavaScript repos.
Action: search_github(query="trending javascript", period="today")
Observation: [{name: "shadcn/ui", stars: 1234, description: "..."}, ...]
Thought: I have the data. Now I need to summarize each repo.
Action: summarize_text(text="shadcn/ui is a collection of re-usable...")
Observation: "A customizable component library for React using Tailwind CSS"
Thought: I have summaries for all 3 repos. I'll format as markdown.
Action: format_table(data=[{name, summary, stars}, ...])
Observation: "| Repository | Description | Stars |..."
Final Answer: [markdown table with 3 repos]Building a Simple Agent with Claude
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// Define your tools
const tools: Anthropic.Tool[] = [
{
name: "search_web",
description: "Search the web for current information",
input_schema: {
type: "object" as const,
properties: {
query: { type: "string", description: "Search query" },
},
required: ["query"],
},
},
{
name: "run_code",
description: "Execute Python code and return the output",
input_schema: {
type: "object" as const,
properties: {
code: { type: "string", description: "Python code to execute" },
},
required: ["code"],
},
},
{
name: "read_file",
description: "Read the contents of a file",
input_schema: {
type: "object" as const,
properties: {
path: { type: "string", description: "File path to read" },
},
required: ["path"],
},
},
];
// Tool execution
async function executeTool(
name: string,
input: Record<string, unknown>
): Promise<string> {
switch (name) {
case "search_web":
return await searchWeb(input.query as string);
case "run_code":
return await runPythonCode(input.code as string);
case "read_file":
return await readFile(input.path as string);
default:
return `Unknown tool: ${name}`;
}
}
// The agent loop
async function runAgent(
goal: string,
maxSteps = 10
): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: goal }
];
let steps = 0;
while (steps < maxSteps) {
steps++;
console.log(`\nStep ${steps}:`);
const response = await client.messages.create({
model: "claude-opus-4-5",
max_tokens: 4096,
tools,
messages,
system: `You are an autonomous agent that completes tasks step by step.
Use the available tools to gather information and take actions.
Think carefully before each action. Be efficient — don't use tools
unless necessary. When you have enough information, provide your
final answer directly without using tools.`,
});
console.log(`Stop reason: ${response.stop_reason}`);
// Agent is done
if (response.stop_reason === "end_turn") {
const finalText = response.content
.filter(b => b.type === "text")
.map(b => b.type === "text" ? b.text : "")
.join("");
return finalText;
}
// Agent wants to use tools
if (response.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
console.log(`Tool: ${block.name}(${JSON.stringify(block.input)})`);
const result = await executeTool(
block.name,
block.input as Record<string, unknown>
);
console.log(`Result: ${result.slice(0, 100)}...`);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: result,
});
}
}
messages.push({ role: "user", content: toolResults });
}
}
return "Maximum steps reached without completing the task.";
}Agent Memory
One of the biggest challenges with agents is memory. LLMs have a context window — they can only "remember" what's in the current prompt.
Short-term Memory (In-context)
Simply include relevant history in the messages array. Works for single sessions.
Long-term Memory (External)
For memory that persists across sessions:
interface Memory {
type: 'fact' | 'preference' | 'interaction';
content: string;
embedding: number[];
timestamp: Date;
}
class AgentMemory {
async store(content: string, type: Memory['type']) {
const embedding = await generateEmbedding(content);
await db.memories.insert({ content, type, embedding });
}
async retrieve(query: string, k = 5): Promise<Memory[]> {
const queryEmbedding = await generateEmbedding(query);
return await vectorSearch(queryEmbedding, k);
}
async buildContext(currentTask: string): Promise<string> {
const relevant = await this.retrieve(currentTask);
return relevant.map(m => m.content).join('\n');
}
}
// Inject memory into system prompt
const memory = new AgentMemory();
const context = await memory.buildContext(userGoal);
const systemPrompt = `${baseSystemPrompt}
Relevant context from memory:
${context}`;Multi-Agent Systems
For complex tasks, a single agent often isn't sufficient. Multi-agent architectures use specialized agents that collaborate:
User Goal: "Research quantum computing and write a technical blog post"
Orchestrator Agent
├── Research Agent
│ ├── Tool: search_arxiv
│ ├── Tool: search_web
│ └── Returns: research findings
├── Writing Agent
│ ├── Tool: draft_content
│ ├── Input: research findings
│ └── Returns: draft article
└── Review Agent
├── Tool: fact_check
├── Tool: improve_writing
└── Returns: final articleclass Orchestrator {
async execute(task: string): Promise<string> {
// Break task into subtasks
const plan = await this.planTask(task);
const results: Record<string, string> = {};
for (const subtask of plan.subtasks) {
const agent = this.getAgent(subtask.type);
results[subtask.id] = await agent.run(
subtask.instruction,
subtask.inputs.map(id => results[id]) // pass results from dependencies
);
}
return await this.synthesize(task, results);
}
private getAgent(type: string): Agent {
const agents = {
research: new ResearchAgent(),
writing: new WritingAgent(),
review: new ReviewAgent(),
code: new CodeAgent(),
};
return agents[type as keyof typeof agents] || new GeneralAgent();
}
}Guardrails and Safety
Agents can cause real-world harm if not properly constrained:
const safetyConfig = {
// Actions that require human confirmation
requireConfirmation: [
'send_email',
'delete_file',
'post_to_social_media',
'make_payment',
],
// Actions that are completely blocked
blocked: [
'access_user_passwords',
'read_private_keys',
'modify_auth_config',
],
// Rate limits
rateLimits: {
api_calls_per_minute: 60,
files_written_per_session: 100,
emails_sent_per_session: 10,
}
};
async function safeExecuteTool(
name: string,
input: unknown,
config: typeof safetyConfig
): Promise<string> {
if (config.blocked.includes(name)) {
return `Error: Tool '${name}' is blocked for safety reasons.`;
}
if (config.requireConfirmation.includes(name)) {
const confirmed = await requestHumanApproval(name, input);
if (!confirmed) {
return `User declined to execute '${name}'.`;
}
}
return await executeTool(name, input as Record<string, unknown>);
}When to Use Agents (and When Not To)
Good use cases for agents:
- Multi-step research and synthesis
- Automated testing and bug fixing
- Data pipeline orchestration
- Content generation with research
- Customer support with system access
Where agents struggle:
- Tasks requiring real-time responses
- Very long-running tasks (token costs)
- Tasks requiring perfect accuracy (agents make mistakes)
- Tasks where you can't verify the output
The pragmatic approach: Start with simple tool use (one round of retrieval or code execution) before building full agents. Full agentic loops are expensive and prone to failure. Use them when simpler approaches fail.
The Future of AI Agents
We're still in early days. The most significant near-term improvements:
- Better planning: Models that can break down complex goals more reliably
- Longer context: Bigger context windows mean more history, better coherence
- Faster inference: Speed improvements making real-time agents feasible
- Multimodal: Agents that can see, hear, and interact with UIs visually
Build your agent expertise now. This skill will be extraordinarily valuable over the next 3-5 years as these systems become more capable and move into production at scale.