AI Engineering — Tool Use & Agents
An AI agent is a system that uses a language model to reason about a goal, plan actions, execute tools, observe results, and iterate until completion. Unlike simple chat completions, agents maintain state, use external tools, and make decisions across multiple steps.
Modern agent architectures combine LLMs with function calling, memory systems, planning algorithms, and orchestration frameworks to tackle complex, multi-step tasks autonomously.
Every agent system shares a core architecture: a reasoning loop that repeatedly decides what to do, executes tools, and incorporates results. The loop continues until a termination condition is met.
The Agent Loop
| 1 | // Core agent reasoning loop |
| 2 | class Agent { |
| 3 | private messages: Message[] = []; |
| 4 | private maxSteps = 25; |
| 5 | private step = 0; |
| 6 | |
| 7 | constructor( |
| 8 | private model: string, |
| 9 | private tools: Tool[], |
| 10 | private systemPrompt: string |
| 11 | ) { |
| 12 | this.messages.push({ |
| 13 | role: "system", |
| 14 | content: systemPrompt |
| 15 | }); |
| 16 | } |
| 17 | |
| 18 | async run(userInput: string): Promise<string> { |
| 19 | this.messages.push({ role: "user", content: userInput }); |
| 20 | |
| 21 | while (this.step < this.maxSteps) { |
| 22 | const response = await this.think(); |
| 23 | const action = this.parseAction(response); |
| 24 | |
| 25 | if (action.type === "final") { |
| 26 | return action.content; |
| 27 | } |
| 28 | |
| 29 | const result = await this.act(action); |
| 30 | this.messages.push({ |
| 31 | role: "tool", |
| 32 | tool_call_id: action.id, |
| 33 | content: JSON.stringify(result) |
| 34 | }); |
| 35 | |
| 36 | this.step++; |
| 37 | } |
| 38 | |
| 39 | return "Max steps reached without completion."; |
| 40 | } |
| 41 | |
| 42 | private async think() { |
| 43 | return await openai.chat.completions.create({ |
| 44 | model: this.model, |
| 45 | messages: this.messages, |
| 46 | tools: this.tools, |
| 47 | tool_choice: "auto" |
| 48 | }); |
| 49 | } |
| 50 | |
| 51 | private parseAction(response: any) { |
| 52 | if (!response.choices[0].message.tool_calls) { |
| 53 | return { type: "final", content: response.choices[0].message.content }; |
| 54 | } |
| 55 | return { type: "tool", ...response.choices[0].message.tool_calls[0] }; |
| 56 | } |
| 57 | |
| 58 | private async act(action: any) { |
| 59 | const args = JSON.parse(action.function.arguments); |
| 60 | return await executeTool(action.function.name, args); |
| 61 | } |
| 62 | } |
best practice
Memory in agents operates at multiple timescales. Short-term memory is the conversation context window. Long-term memory uses external storage (vector databases, key-value stores) for information that persists across sessions.
| Memory Type | Storage | Retention | Use Case |
|---|---|---|---|
| Short-term | Context window | Single session | Conversation flow |
| Working | Scratchpad | Current task | Intermediate state |
| Long-term | Vector DB | Cross-session | User preferences, facts |
| Episodic | Log / DB | Audit trail | Past actions, outcomes |
| Procedural | Prompts / Code | Permanent | How-to knowledge |
| 1 | // Long-term memory with vector store |
| 2 | interface MemoryStore { |
| 3 | add(text: string, metadata?: Record<string, any>): Promise<void>; |
| 4 | search(query: string, topK?: number): Promise<MemoryItem[]>; |
| 5 | forget(ids: string[]): Promise<void>; |
| 6 | } |
| 7 | |
| 8 | class VectorMemory implements MemoryStore { |
| 9 | constructor( |
| 10 | private embedder: (text: string) => Promise<number[]>, |
| 11 | private storage: VectorDatabase |
| 12 | ) {} |
| 13 | |
| 14 | async add(text: string, metadata?: Record<string, any>) { |
| 15 | const embedding = await this.embedder(text); |
| 16 | await this.storage.upsert({ |
| 17 | id: crypto.randomUUID(), |
| 18 | values: embedding, |
| 19 | metadata: { text, ...metadata, timestamp: Date.now() } |
| 20 | }); |
| 21 | } |
| 22 | |
| 23 | async search(query: string, topK = 5) { |
| 24 | const embedding = await this.embedder(query); |
| 25 | const results = await this.storage.query({ |
| 26 | topK, |
| 27 | vector: embedding, |
| 28 | includeMetadata: true |
| 29 | }); |
| 30 | return results.map(r => ({ |
| 31 | text: r.metadata!.text, |
| 32 | score: r.score, |
| 33 | id: r.id |
| 34 | })); |
| 35 | } |
| 36 | |
| 37 | async forget(ids: string[]) { |
| 38 | await this.storage.deleteMany(ids); |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | // Memory-augmented agent |
| 43 | class MemoryAgent extends Agent { |
| 44 | constructor( |
| 45 | model: string, |
| 46 | tools: Tool[], |
| 47 | private memory: MemoryStore |
| 48 | ) { |
| 49 | super(model, tools, buildMemoryPrompt()); |
| 50 | } |
| 51 | |
| 52 | async run(input: string): Promise<string> { |
| 53 | const relevant = await this.memory.search(input); |
| 54 | if (relevant.length > 0) { |
| 55 | this.messages.push({ |
| 56 | role: "system", |
| 57 | content: `Relevant memories: ${JSON.stringify(relevant)}` |
| 58 | }); |
| 59 | } |
| 60 | const result = await super.run(input); |
| 61 | await this.memory.add(input, { type: "user_query" }); |
| 62 | await this.memory.add(result, { type: "agent_response" }); |
| 63 | return result; |
| 64 | } |
| 65 | } |
Advanced agents decompose complex tasks into subtasks before execution. Planning strategies include ReAct (Reasoning + Acting), Plan-and-Solve, Tree of Thoughts, and Reflexion — each with different trade-offs between depth, cost, and reliability.
| 1 | // ReAct: Reasoning + Acting loop |
| 2 | // The model outputs "Thought:" then "Action:" then "Observation:" |
| 3 | // This structured thinking improves complex reasoning |
| 4 | |
| 5 | const reactPrompt = `You are a reasoning agent. For each step: |
| 6 | 1. Thought: Reason about what to do next |
| 7 | 2. Action: Select a tool and specify arguments |
| 8 | 3. After receiving the observation, repeat |
| 9 | |
| 10 | When you have enough information, output: |
| 11 | Final Answer: <your answer> |
| 12 | |
| 13 | Available tools: |
| 14 | ${tools.map(t => t.function.name).join(", ")} |
| 15 | |
| 16 | Always show your reasoning in Thought steps.`; |
| 17 | |
| 18 | // Plan-and-Solve decomposition |
| 19 | async function planAndSolve(task: string, agent: Agent) { |
| 20 | // Phase 1: Decompose |
| 21 | const plan = await agent.ask( |
| 22 | `Decompose this task into 3-5 subtasks: "${task}"` |
| 23 | ); |
| 24 | |
| 25 | // Phase 2: Execute each subtask |
| 26 | const results = []; |
| 27 | for (const subtask of plan.subtasks) { |
| 28 | const result = await agent.run(subtask); |
| 29 | results.push(result); |
| 30 | } |
| 31 | |
| 32 | // Phase 3: Synthesize |
| 33 | const final = await agent.ask( |
| 34 | `Synthesize these results into a final answer: ${results}` |
| 35 | ); |
| 36 | |
| 37 | return final; |
| 38 | } |
info
LangChain & LangGraph
LangChain provides abstractions for chains, agents, and tools. LangGraph extends this with graph-based state machines for complex, cyclic agent workflows.
| 1 | from langchain.agents import create_tool_calling_agent |
| 2 | from langchain.tools import tool |
| 3 | from langchain_openai import ChatOpenAI |
| 4 | |
| 5 | @tool |
| 6 | def get_weather(location: str) -> str: |
| 7 | """Get weather for a location.""" |
| 8 | return f"Weather in {location}: 22°C, sunny" |
| 9 | |
| 10 | llm = ChatOpenAI(model="gpt-4o") |
| 11 | tools = [get_weather] |
| 12 | |
| 13 | agent = create_tool_calling_agent(llm, tools) |
| 14 | |
| 15 | result = agent.invoke({ |
| 16 | "input": "What's the weather in Paris?" |
| 17 | }) |
| 18 | print(result["output"]) |
Vercel AI SDK
The Vercel AI SDK provides first-class support for tool use with generateText and streamText, including automatic tool execution via maxSteps.
| 1 | import { generateText, tool } from "ai"; |
| 2 | import { openai } from "@ai-sdk/openai"; |
| 3 | import { z } from "zod"; |
| 4 | |
| 5 | const result = await generateText({ |
| 6 | model: openai("gpt-4o"), |
| 7 | tools: { |
| 8 | get_weather: tool({ |
| 9 | description: "Get weather for a location", |
| 10 | parameters: z.object({ |
| 11 | location: z.string().describe("City name"), |
| 12 | units: z.enum(["c", "f"]).default("c") |
| 13 | }), |
| 14 | execute: async ({ location, units }) => { |
| 15 | return await fetchWeather(location, units); |
| 16 | } |
| 17 | }) |
| 18 | }, |
| 19 | maxSteps: 5, // Auto-executes tool calls |
| 20 | prompt: "What's the weather in Tokyo?" |
| 21 | }); |
CrewAI & AutoGen
CrewAI provides role-based agents with built-in orchestration. AutoGen (Microsoft) emphasizes multi-agent conversations with programmable termination and human-in-the-loop.
| 1 | # CrewAI — role-based agents |
| 2 | from crewai import Agent, Task, Crew |
| 3 | |
| 4 | researcher = Agent( |
| 5 | role="Research Analyst", |
| 6 | goal="Find accurate information", |
| 7 | backstory="Expert researcher with web access", |
| 8 | tools=[search_tool, scrape_tool] |
| 9 | ) |
| 10 | |
| 11 | writer = Agent( |
| 12 | role="Technical Writer", |
| 13 | goal="Create clear documentation", |
| 14 | backstory="Experienced technical writer" |
| 15 | ) |
| 16 | |
| 17 | research_task = Task( |
| 18 | description="Research the topic", |
| 19 | agent=researcher |
| 20 | ) |
| 21 | |
| 22 | write_task = Task( |
| 23 | description="Write documentation", |
| 24 | agent=writer |
| 25 | ) |
| 26 | |
| 27 | crew = Crew( |
| 28 | agents=[researcher, writer], |
| 29 | tasks=[research_task, write_task], |
| 30 | verbose=True |
| 31 | ) |
| 32 | |
| 33 | result = crew.kickoff() |
Different tasks call for different agent architectures. Simple tasks work with a single agent loop, while complex workflows benefit from specialized patterns.
| Type | Architecture | Best For |
|---|---|---|
| Simple Agent | Single LLM + tools loop | Q&A, simple tasks |
| ReAct Agent | Thought → Action → Observation | Complex reasoning |
| Plan-and-Execute | Planner → Executor → Synthesizer | Multi-step projects |
| Reflection Agent | Generate → Critique → Refine | Content creation, code |
| Multi-Agent | Multiple agents with roles | Complex workflows |
| Human-in-the-Loop | Agent + human approval step | Sensitive operations |
pro tip