Introduction to AI Agents: Building Your First Action-Oriented Agent
This chapter explores the transition from raw Large Language Models (LLMs) to AI Agents. While raw LLMs excel at passive text generation, reasoning, and code writing, they are fundamentally limited because they cannot execute actions or access real-time data. By integrating non-deterministic LLMs with deterministic external tools, we can build autonomous agents capable of performing complex, multi-step tasks—such as calculating precise mathematical equations, fetching live weather data, converting currencies, and even building and saving complete static websites to a local directory.
1. The Limitations of Raw LLMs
A raw LLM operates on a simple, autoregressive token-prediction loop. It takes input tokens, processes them through a transformer architecture, calculates a probability distribution over its vocabulary, and predicts the next token. It repeats this process until it generates a complete response.
While highly sophisticated, this architecture has three fundamental limitations:
- Non-Deterministic Nature: LLMs predict words based on probabilities. They do not perform actual logical or mathematical calculations. When asked to multiply two large numbers (e.g., ), they often hallucinate a plausible-looking but incorrect number because they are guessing the next token based on patterns in their training data rather than executing an arithmetic algorithm.
- No Real-Time Data Access: A raw LLM is frozen in time, limited to the data available up to its training cutoff date. It cannot browse the live internet to find today's weather in Delhi or the current USD-to-INR currency exchange rate.
- Inability to Take Actions: An LLM can write the code for a website, but it cannot create a directory on your computer, write that code into files, or run a local server to host it. It relies entirely on a human copy-pasting its output.
2. The "Smart Person Without Hands" Analogy
To understand the core mental model of AI Agents, consider the following analogy:
Real-Life Analogy: The Smart Person Without Hands
Think of a raw LLM as an incredibly smart, highly educated person who has no hands.
If you ask them to turn on the air conditioner, they cannot physically do it. However, they can explain the exact thermodynamics of cooling, tell you where the remote control is located, and instruct you step-by-step on which button to press.
To make this person truly autonomous, you must give them "hands." In the software world, these hands are External Tools (APIs, compilers, database connectors, and file system writers). When you give the smart person access to these tools, they transition from a passive advisor to an active AI Agent.
3. Non-Deterministic LLMs vs. Deterministic Tools
To build reliable applications, we must combine the creative reasoning of LLMs with the predictable execution of traditional software.
4. The Mechanics of Tool Calling
When an LLM uses a tool, it does not execute the tool's code directly. Instead, it generates a structured request (usually in JSON format) indicating which tool it wants to use and what arguments it wants to pass. The hosting application (written in Java, Python, or Node.js) intercepts this request, runs the local deterministic code, and sends the result back to the LLM.
User
|
| "Convert 5000 Euros to GBP"
v
Spring Boot / Host Application
|
| Passes prompt + Tool definitions
v
LLM (Decision Maker)
|
| Decides to call Currency Tool: {"from": "EUR", "to": "GBP"}
v
Spring Boot / Host Application
|
| Intercepts JSON, executes local Currency Tool API call
v
External API (frankfurter.dev)
|
| Returns raw exchange rate: 0.858
v
Spring Boot / Host Application
|
| Sends raw tool result back to LLM
v
LLM (Reasoning Engine)
|
| Calculates final value & formats response
v
Spring Boot / Host Application
|
| "At the current rate of 0.858, 5000 EUR is 4290 GBP."
v
User
5. Step-by-Step Evolution: Building Basic Informative Tools
Let's implement a deterministic Calculator Tool in Java using Spring AI. To make the LLM understand how to use this tool, we use annotations to describe the tool's purpose and its expected parameters.
Naive vs. Structured Tool Design
If we do not explicitly define the parameters, the LLM might pass arbitrary strings like "addition", "sum", or "+" when our code strictly expects "add". We must use strict descriptions to constrain the LLM's outputs.
The Calculator Tool Implementation
package com.coderarmy.aitools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class CalculatorTool {
@Tool(description = "Performs basic arithmetic calculations. Supported operations are: add, subtract, multiply, divide, mod, power.")
public double calculate(
@ToolParam(description = "The arithmetic operation to perform: 'add', 'subtract', 'multiply', 'divide', 'mod', 'power'") String operation,
@ToolParam(description = "The first operand (number)") double a,
@ToolParam(description = "The second operand (number)") double b) {
System.out.println("--> [SYSTEM LOG] CalculatorTool executed: " + a + " " + operation + " " + b);
switch (operation.toLowerCase()) {
case "add":
return a + b;
case "subtract":
return a - b;
case "multiply":
return a * b;
case "divide":
if (b == 0) {
throw new IllegalArgumentException("Cannot divide by zero.");
}
return a / b;
case "mod":
if (b == 0) {
throw new IllegalArgumentException("Cannot perform modulo by zero.");
}
return a % b;
case "power":
return Math.pow(a, b);
default:
throw new IllegalArgumentException("Unsupported operation: " + operation);
}
}
}
6. Integrating Real-Time Data: Weather & Currency Tools
To give our agent access to the real world, we can build tools that make HTTP requests to live external APIs.
1. Weather Tool (Using WeatherAPI.com)
package com.coderarmy.aitools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class WeatherTool {
private final RestClient restClient;
private final String apiKey = "YOUR_WEATHER_API_KEY"; // Loaded from application.properties
public WeatherTool() {
this.restClient = RestClient.builder()
.baseUrl("https://api.weatherapi.com/v1")
.build();
}
@Tool(description = "Gets the current weather information for a given city.")
public String getCurrentWeather(
@ToolParam(description = "The name of the city (e.g., 'Delhi', 'New York')") String city) {
System.out.println("--> [SYSTEM LOG] WeatherTool executed for city: " + city);
try {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/current.json")
.queryParam("key", apiKey)
.queryParam("q", city)
.build())
.retrieve()
.body(String.class);
} catch (Exception e) {
return "Failed to fetch weather data: " + e.getMessage();
}
}
}
2. Currency Exchange Tool (Using Frankfurter API)
package com.coderarmy.aitools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class CurrencyExchangeTool {
private final RestClient restClient;
public CurrencyExchangeTool() {
this.restClient = RestClient.builder()
.baseUrl("https://api.frankfurter.dev/v1")
.build();
}
@Tool(description = "Gets the latest currency exchange rate between two currencies.")
public String getExchangeRate(
@ToolParam(description = "The source currency code (e.g., 'USD', 'EUR', 'GBP')") String from,
@ToolParam(description = "The target currency code (e.g., 'INR', 'EUR', 'GBP')") String to) {
System.out.println("--> [SYSTEM LOG] CurrencyExchangeTool executed: " + from + " to " + to);
try {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/latest")
.queryParam("base", from.toUpperCase())
.queryParam("symbols", to.toUpperCase())
.build())
.retrieve()
.body(String.class);
} catch (Exception e) {
return "Failed to fetch exchange rate: " + e.getMessage();
}
}
}
7. Informative Tools vs. Action Tools
Tools generally fall into two categories based on how they interact with external systems:
- Informative Tools (Read-Only): These tools fetch data or perform calculations without changing the state of any system. Examples include querying weather APIs, fetching currency exchange rates, or running local math calculations.
- Action Tools (Write/Execute): These tools modify the state of the external environment. Examples include sending an email, creating a database record, processing a refund, or writing files to a local hard drive.
8. The Security Nightmare: Why Raw Terminal Access is Dangerous
When building action tools, developers are often tempted to create a generic "Terminal Command Runner" tool that allows the LLM to execute arbitrary shell commands.
LLM (Hallucinating or Malicious Prompt)
|
| Sends command: "rm -rf /" or "rmdir /s /q C:\"
v
Terminal Command Runner Tool
|
| Executes command directly on host OS
v
System Crashes / Data Loss
The Danger of Arbitrary Execution
If an LLM hallucinates, misinterprets a user prompt, or falls victim to a Prompt Injection Attack (where a user tricks the LLM into running malicious instructions), it could execute destructive commands like rm -rf / or expose sensitive environment variables.
To prevent this, we must apply the Principle of Least Privilege using a design pattern called Sandboxing.
9. The Sandboxing Solution & Path Validation
Instead of giving the LLM direct access to the operating system, we create a restricted environment (a sandbox) and expose only highly specific, safe, and validated tools:
Create DirectoryWrite FileRead FileList Files
Path Traversal Prevention
Even with restricted tools, a malicious prompt could try to write files outside the designated workspace using path traversal sequences (e.g., ../../etc/passwd). To prevent this, we must implement strict path validation.
Secure File System Tools Implementation
package com.coderarmy.aitools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component
public class WebsiteTools {
// Define the absolute root path of our sandbox workspace
private final Path workspaceRoot;
public WebsiteTools() {
// Resolve a safe directory named "generated-sites" in the project root
this.workspaceRoot = Paths.get("generated-sites").toAbsolutePath().normalize();
try {
Files.createDirectories(workspaceRoot);
} catch (IOException e) {
throw new RuntimeException("Could not initialize workspace directory", e);
}
}
/**
* Security Check: Validates that the requested path is strictly inside the sandbox workspace.
*/
private Path validateAndResolvePath(String relativePathStr) {
Path resolvedPath = workspaceRoot.resolve(relativePathStr).normalize();
if (!resolvedPath.startsWith(workspaceRoot)) {
throw new SecurityException("Access Denied: Path traversal attempt detected outside the sandbox workspace!");
}
return resolvedPath;
}
@Tool(description = "Creates a new directory inside the website workspace.")
public String createDirectory(
@ToolParam(description = "Relative path of the directory to create (e.g., 'my-portfolio')") String dirPath) {
try {
Path targetPath = validateAndResolvePath(dirPath);
Files.createDirectories(targetPath);
return "Directory created successfully at: " + dirPath;
} catch (Exception e) {
return "Error creating directory: " + e.getMessage();
}
}
@Tool(description = "Writes content to a file. Creates the file if it does not exist, or overwrites it if it does.")
public String writeFile(
@ToolParam(description = "Relative path of the file (e.g., 'my-portfolio/index.html')") String filePath,
@ToolParam(description = "The complete text content to write into the file") String content) {
try {
Path targetPath = validateAndResolvePath(filePath);
// Ensure parent directories exist
Files.createDirectories(targetPath.getParent());
Files.writeString(targetPath, content, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
return "File written successfully: " + filePath;
} catch (Exception e) {
return "Error writing file: " + e.getMessage();
}
}
@Tool(description = "Reads and returns the content of an existing file.")
public String readFile(
@ToolParam(description = "Relative path of the file to read (e.g., 'my-portfolio/style.css')") String filePath) {
try {
Path targetPath = validateAndResolvePath(filePath);
if (!Files.exists(targetPath)) {
return "Error: File does not exist at " + filePath;
}
return Files.readString(targetPath);
} catch (Exception e) {
return "Error reading file: " + e.getMessage();
}
}
@Tool(description = "Lists all files and directories inside the workspace.")
public String listFiles() {
try (Stream<Path> stream = Files.walk(workspaceRoot, 3)) {
List<String> filesList = stream
.map(path -> workspaceRoot.relativize(path).toString())
.filter(str -> !str.isEmpty())
.collect(Collectors.toList());
return "Workspace Files:\n" + String.join("\n", filesList);
} catch (Exception e) {
return "Error listing files: " + e.getMessage();
}
}
}
10. Building a Website Generator Agent
Now we can combine our secure file system tools with a specialized system prompt to build an autonomous Website Generator Agent.
The System Prompt
The system prompt defines the agent's persona, boundaries, and instructions. It tells the LLM that it is an expert front-end developer and instructs it to use the provided tools to write files rather than simply outputting code blocks in its chat response.
You are an expert front-end website developer.
Your job is to create complete, modern, beautiful, and responsive static websites based on user requirements.
You must follow these strict rules:
1. ALWAYS create a separate directory for every new website project.
2. Create modular files: 'index.html', 'style.css', and 'script.js' (if dynamic behavior is needed).
3. Use modern, responsive CSS layouts (Flexbox, Grid) with clean color palettes.
4. DO NOT just print the code blocks in your chat response. You must actually write the files to the workspace using the 'writeFile' tool.
5. Read and verify files if you need to debug or refine the code.
6. Declare the task complete only when all files are successfully written to the workspace.
Service Layer Implementation
package com.coderarmy.services;
import com.coderarmy.aitools.WebsiteTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class WebsiteBuilderService {
private final ChatClient chatClient;
private final WebsiteTools websiteTools;
public WebsiteBuilderService(ChatClient.Builder chatClientBuilder, WebsiteTools websiteTools) {
this.websiteTools = websiteTools;
this.chatClient = chatClientBuilder
.defaultSystem("""
You are an expert front-end website developer.
Your job is to create complete, modern, beautiful, and responsive static websites based on user requirements.
You must follow these strict rules:
1. ALWAYS create a separate directory for every new website project.
2. Create modular files: 'index.html', 'style.css', and 'script.js' (if dynamic behavior is needed).
3. Use modern, responsive CSS layouts (Flexbox, Grid) with clean color palettes.
4. DO NOT just print the code blocks in your chat response. You must actually write the files to the workspace using the 'writeFile' tool.
5. Read and verify files if you need to debug or refine the code.
6. Declare the task complete only when all files are successfully written to the workspace.
""")
.build();
}
public String generateWebsite(String userPrompt) {
System.out.println("--> [AGENT START] Processing prompt: " + userPrompt);
// Execute the chat client, registering our secure website tools
return chatClient.prompt()
.user(userPrompt)
.tools(websiteTools) // Exposes createDirectory, writeFile, readFile, listFiles
.call()
.content();
}
}
Controller Layer Implementation
package com.coderarmy.controllers;
import com.coderarmy.services.WebsiteBuilderService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class WebsiteBuilderController {
private final WebsiteBuilderService websiteBuilderService;
public WebsiteBuilderController(WebsiteBuilderService websiteBuilderService) {
this.websiteBuilderService = websiteBuilderService;
}
@PostMapping("/website")
public String buildWebsite(@RequestBody String prompt) {
return websiteBuilderService.generateWebsite(prompt);
}
}
11. Complete Flow Walkthrough
Here is exactly what happens behind the scenes when a user sends a request to our Website Generator Agent:
12. Summary and Key Takeaways
- Raw LLMs are Passive: Raw language models are non-deterministic token predictors that cannot perform calculations, access real-time data, or execute system actions on their own.
- AI Agents Have "Hands": An AI Agent is created by combining a non-deterministic LLM (which handles reasoning and decision-making) with deterministic external tools (which handle execution).
- Structured Tool Calling: LLMs do not run code directly. Instead, they generate structured JSON payloads containing tool names and arguments. The host application intercepts these payloads, runs the corresponding local function, and returns the result to the LLM.
- Informative vs. Action Tools: Informative tools are read-only (e.g., fetching weather or exchange rates), while action tools modify the state of the environment (e.g., writing files or sending emails).
- The Danger of Raw Shell Access: Giving an LLM unrestricted command-line access is a severe security risk. Hallucinations or prompt injection attacks can lead to system-wide data loss or unauthorized access.
- Sandboxing is Essential: To secure action-oriented agents, restrict them to a safe, isolated environment (a sandbox) with specific, validated tools.
- Path Traversal Prevention: Always validate and normalize file paths in your tools to ensure the LLM cannot read or write files outside its designated workspace directory.
- Multi-Step Agent Loops: True AI agents can run in a loop, analyzing the output of one tool to decide which tool to call next until they achieve their overall goal.