Skip to content

Introduction to AI Agents: Tools, Tool Calling and the Agent Loop

An LLM writes text but cannot act. Tools give it hands. Tool definitions, the tool calling loop, information vs action tools, and a sandboxed website builder agent in Spring AI, Node.js and Python.

Builds on Talking to an LLM from Your Application

Stage 6core27 min read

An LLM is very good with words. It can explain a bug, write a cover letter or plan a trip. But on its own it cannot do anything. It cannot check the weather, add two numbers with certainty, or save a file. It only writes text. This note is about how an application gives the model a way to act, and what changes once it can.

Note 4 ended with a picture of a real AI app: the model is one box, and the application around it does most of the work. Tools were one of the boxes in that picture. Here we open that box. We start with one calculator tool, add weather and currency, look at the loop that runs them, and finish with a small agent that builds a website inside a locked folder. The code is in Spring Boot, Node.js and Python under code/05-ai-agent-tools/.

Here are the words used in this note. Each one gets a plain meaning the first time it shows up, but this table is the short version:

WordWhat it means here
LLMThe trained model. Text goes in, text comes out
ToolA normal function in your code that the model is allowed to ask for
Tool definitionThe name, description and input shape of a tool. This is all the model sees
Tool callA message from the model that says "please run this tool with these inputs"
Tool resultWhat the function returned. It goes back to the model as text
ObservationAnother name for a tool result, used when we talk about agents
Tool calling loopAsk the model, run the tool it wants, send the result back, ask again
AgentA model plus tools plus a loop, working toward a goal over several steps
WorkflowA fixed list of steps that a programmer wrote in advance
SandboxA closed area. The agent's tools can only touch what is inside it
Path traversalUsing ../ in a file path to climb out of the folder you were given
Step limitThe most loop rounds we allow before we stop the agent
Human in the loopA person must approve a risky action before it runs

1. Writing an Action Is Not Doing It

From note 2 you know what an LLM does at its core. It looks at the tokens so far (small pieces of text) and predicts the next one. Then it does that again, and again.

Ask it:

Create a folder called portfolio.

It may reply:

mkdir portfolio

That is the right command. But no folder was made. The model wrote a line of text that describes an action. Nothing ran them.

Loading diagram…

The dotted line is the gap this note is about. The model's output stops at text. Getting from text to a real change needs something else.

Two everyday versions of the same gap:

  • A recipe book tells you exactly how to make dal. It has never cooked anything.
  • A cricket commentator can describe the perfect cover drive. He is not the one holding the bat.

Knowing how is not the same as doing.

2. Brain and Hands

Picture a very clever person locked in a room. They know a lot, they reason well and they speak clearly. But they have no hands.

You call them and say: "Please turn on the AC." They understand you. They might say: "Press the power button on the remote." They still cannot press it.

A raw LLM is in that room. It has no direct way to reach:

  • a database
  • the file system
  • live data on the internet
  • a calculator
  • a terminal
  • email, calendars or payment systems

Every one of those has to be handed to it by the application that runs it.

Loading diagram…

The model is the brain. Tools are the hands. Notice the arrows say "asks". The brain never touches the world directly, even with hands attached.

LLM = brain. Tools = hands. The application connects the two.

3. The Application Around the Model

ChatGPT and similar apps are much more than a model. The model sits inside an application, and the application decides what the model can reach.

Loading diagram…

Look at where the tool sits. It is attached to the application, not to the model. The model can only ask the application to use it.

A food delivery app works the same way with people. The support agent on chat does not drive to your house. They ask the delivery system, and the system sends a rider.

4. Splitting the Work: the Calculator

Take this question:

What is 938475 x 736294?

A modern model might get it right. It might also get one digit wrong and say it with total confidence. Plain code gets it right every single time. So we split the job.

WhoJob
The modelRead the question. Work out that it means "multiply 938475 by 736294"
The calculatorDo the multiplication
The model againTurn the number into a friendly sentence

Here is the same split as a conversation between the three of them:

Loading diagram…

Each side does what it is good at. The model handles language. The calculator handles numbers.

A shop till is a good picture of this. The shopkeeper talks to you, finds out what you want and bags it. The till adds up the bill. Nobody asks the shopkeeper to add 47 items in their head.

5. Why Software Wants Structure

Here is a normal Java method:

public double add(double a, double b) {
    return a + b;
}

A computer understands add(10, 20) perfectly. A person might say any of these:

  • "Add 10 and 20."
  • "What's ten plus twenty?"
  • "Sum of 10 and 20 please."
  • "I had 10 apples and got 20 more. How many now?"

For a person, these all mean the same thing. For old style code, they are four different strings. You could try:

if (message.contains("add") || message.contains("sum") || message.contains("plus")) {
    // ...
}

Then someone types "I had 300 rupees and my friend gave me 200." There is no "add" in it at all. You can never write enough if checks to cover how people talk.

What code really wants is something fixed and boring:

{ "operation": "ADD", "a": 10, "b": 20 }

The model sits in the middle and turns every version into that one shape:

Loading diagram…

Many messy sentences go in. One clean shape comes out. This is the model's most useful job in an application.

Train tickets show the same thing. "Two seats to Pune tomorrow morning", "Book me and my wife on the 7 am to Pune" and "Pune, tomorrow, 2 people" should all turn into the same booking request.

6. The Model as a Translator

So an LLM is more than a text writer. It can translate messy human intent into structured data that code can use.

Think about a food delivery support chat.

A customer writes:

My burger never came and I want my money back.

The backend needs:

{ "intent": "REFUND_REQUEST", "reason": "ITEM_NOT_DELIVERED", "item": "burger" }

Another customer writes:

I ordered pizza 90 minutes ago. Where is it?

The backend needs:

{ "intent": "ORDER_STATUS" }

Put the two messages side by side and you can see where each one ends up:

Loading diagram…

The model reads the sentence and picks the route. The services behind it stay normal code that expects normal inputs.

Two more examples of the same idea:

  • A library app. "Can I keep this book one more week?" becomes {"intent": "RENEW", "days": 7}.
  • A phone contacts app. "Save Ravi's new number, 98450 12345" becomes {"action": "UPDATE_CONTACT", "name": "Ravi", "phone": "9845012345"}.

7. Why "Reply in JSON" Is Not Enough

You could just tell the model: "Always answer in JSON." Most of the time it will. Sometimes you get this:

Sure! Here is the JSON:
{ "operation": "multiplication" }

Two problems. There is chatty text before the JSON, so a JSON parser fails. And it says multiplication when your code expects MULTIPLY. It is close, but code does not do close.

What we want is a contract: a fixed list of fields, their types and the allowed values, agreed before the model says anything.

Loading diagram…

On the left, the shape is only a polite request, and the model may ignore it. On the right, the shape is part of the API call. An unpredictable model meets strict code here, so the code needs the contract.

The model understands what the user means. The contract decides the shape it must use to say it.

8. What a Tool Is

Take away the AI words, and a tool is a normal function. This multiply is a simplified example. The repo has one calculate(operation, a, b) tool for all maths, shown below.

public double multiply(double a, double b) {
    return a * b;
}

There is nothing AI about it. What turns it into a tool is a description the model can read:

Name:        multiply
Description: Multiplies two numbers.
Inputs:      a (number), b (number)

That description is the tool definition. It has three parts.

Loading diagram…

The name lets the model point at the tool. The description tells it when the tool fits. The parameters tell it what to fill in. The body stays in your code.

In Spring AI, you write the definition as annotations on the method. Here is the calculator from this note's code:

CalculatorTools.java
@Tool(description = """
        Does exact arithmetic on two numbers. Use it for every calculation,
        even easy ones, instead of working the answer out yourself.
        Supported operations: add, subtract, multiply, divide, mod, power.
        """)
public double calculate(
        @ToolParam(description = "One of: add, subtract, multiply, divide, mod, power") String operation,
        @ToolParam(description = "The first number") double a,
        @ToolParam(description = "The second number") double b) {

    String op = operation == null ? "" : operation.trim().toLowerCase(Locale.ROOT);
    return switch (op) {
        case "add" -> a + b;
        case "multiply" -> a * b;
        // ... subtract, divide, mod, power
        default -> throw new IllegalArgumentException("Unsupported operation '" + operation + "'.");
    };
}

Spring AI reads the method name, the descriptions and the parameter types, and builds the definition for you. The switch is the function body. It stays in your code.

The descriptions matter more than they look. "Use it for every calculation, even easy ones" is there because models like to do small sums in their head. If the description is vague, the model may not pick the tool at all.

9. A Tool Definition Is an API Contract

You already know this idea from REST. A POST /users endpoint says: send me a name and an email, both strings. The client follows that or gets an error.

A tool definition is the same deal, but the client is the model.

Loading diagram…

In both, one side publishes what it accepts and the other side picks an action and fills in the fields. The LLM acts like a smart client that decides which function to call.

In the Node and Python versions we write the definition by hand. This is the exact shape the OpenAI Responses API takes:

{
  "type": "function",
  "name": "calculate",
  "description": "Does exact arithmetic on two numbers. Use it for every calculation.",
  "parameters": {
    "type": "object",
    "properties": {
      "operation": { "type": "string", "enum": ["add", "subtract", "multiply", "divide", "mod", "power"] },
      "a": { "type": "number", "description": "The first number" },
      "b": { "type": "number", "description": "The second number" }
    },
    "required": ["operation", "a", "b"],
    "additionalProperties": false
  },
  "strict": true
}

Three things to notice:

  • enum lists the only allowed operations. The model cannot send multiplication. This fixes the problem from section 7.
  • strict: true asks the API to make the arguments match the schema exactly.
  • Strict mode wants every field in required and additionalProperties: false (no extra fields allowed). The code has a small functionTool() helper that adds both, so no tool forgets them.

10. What the Model Actually Receives

The model does not get your Java class. It never sees the switch. It only gets the definition, plus the user's message.

Loading diagram…

Everything in the right box stays on your server. This is good for safety. The model cannot read your keys or your database just because a tool uses them inside.

For "What is 450 divided by 9?" the model sends back something like:

{ "name": "calculate", "arguments": "{\"operation\":\"divide\",\"a\":450,\"b\":9}" }

That message is a tool call (also called a function call). Note that arguments is a string of JSON, not a ready object. Your code has to parse it into real values first.

11. The Five Steps of One Tool Call

Let us follow one question all the way through: "What is 53 x 29?"

  1. The app sends the user's message and the list of tool definitions.
  2. The model does not answer yet. It replies with a tool call: calculate("multiply", 53, 29).
  3. The app runs the real function. It gets 1537.
  4. The app sends a second request: the same message, the tool call, and the result 1537.
  5. The model sees the result and writes the final answer.
Loading diagram…

Count the arrows to the LLM. One question from the user made two model calls. And look at who runs calculate(): the app, not the model.

The model asks for the tool. Your application runs it.

This point is easy to get wrong. The model never runs inside your Java or Node program. It cannot. It sends text that says what it wants. Your code reads that text and decides to run the function.

12. The Tool Calling Loop

After step 5 the model might not be done. It might need another tool. So the steps are really a loop.

Loading diagram…

The arrow from "Add the result" back to the LLM is the whole idea. Each round, the model sees everything so far and decides again.

You will see the same loop described in four words: think, act, observe, think again. A person fixing a leaking tap does the same. Look, tighten the nut, check if it still drips, decide what next.

13. Writing the Loop by Hand

This section is about the Node and Python loops. In Spring AI you write no loop at all: Spring runs it for you. You pass the tool objects with .tools(...) and get the final text back:

ChatService.java
String reply = chatClient.prompt()
        .system(SYSTEM_PROMPT)
        .user(message)
        .tools(calculatorTools, weatherTools, currencyTools) // definitions + the loop
        .call()
        .content(); // the final text, after every tool round

Inside, Spring AI sends the definitions, runs the method the model asks for, sends the result back and repeats. You never see the rounds.

The Node and Python versions write the loop by hand so you can see each round. This is the core of createAgent().run():

agent.js
const input = [{ role: "user", content: message }];

for (let step = 1; step <= maxSteps; step += 1) {
  const response = await client.responses.create({ model, instructions, input, tools: definitions });
  const calls = response.output.filter((item) => item.type === "function_call");

  // No tool requested: this is the final answer.
  if (calls.length === 0) return response.output_text;

  // Send back what the model produced, then one result per call.
  input.push(...response.output);
  for (const call of calls) {
    const output = await runTool(call); // parse arguments, run the function, catch errors
    input.push({ type: "function_call_output", call_id: call.call_id, output });
  }
}
throw new StepLimitError(maxSteps); // section 27

Each round, the input list grows. A function_call item is the model asking for one tool. Here is what the list holds after one tool call:

Loading diagram…

The call_id ties each result to the call that asked for it. That matters when the model asks for two tools at once (section 16).

Two details that are easy to miss:

  • We push all of response.output, not only the calls. Reasoning models such as gpt-5-mini add a reasoning item (a note of their own thinking) before a call. They expect to get it back on the next round.
  • If a tool throws, runTool catches it and sends "Error: Cannot divide by zero." as the output. The model can then explain the problem to the user. The user gets a sentence, not an HTTP 500 error.

14. Many Tools Behind One Endpoint

Now give the model three tools: calculate, getCurrentWeather and convertCurrency.

Why weather? The calculator was about being exact. Weather is about being current. The model's training data is months old. It has no idea if it is raining in Delhi right now. So getCurrentWeather(city) calls a live weather service.

Why currency? Rates change every day. convertCurrency(amount, from, to) gets today's rate and does the multiplication in code.

All three sit behind one endpoint, POST /api/chat. There is no /calculator or /weather route. The model reads the message and picks.

Loading diagram…

This is tool selection. The model chooses based on the descriptions you wrote. Clear, different descriptions make the choice easy.

A hospital front desk works like this. You say what is wrong. The person at the desk sends you to the right department. You do not need to know which floor cardiology is on.

15. Chained Calls

Some questions need one tool's answer before the next tool can start:

Convert 120 US dollars to Indian rupees, then split it equally between 6 people.

Say today's rate is 83 rupees to the dollar. The model works it out step by step:

Loading diagram…

Nobody wrote "call currency, then call calculator" in the code. The model read the goal and chose the order. The second call uses a number that did not exist until the first call finished.

That is the first real taste of agent behaviour: several decisions in a row, each one based on what the last one returned.

16. Independent Calls

Other questions need two tools that do not depend on each other:

I am travelling to Delhi. What is the weather there, and how much is 500 USD in rupees?

Neither answer needs the other, so both tools can run in the same round:

Loading diagram…

The model can ask for both in the same round. The response then holds two function_call items with different call_ids. Our loop runs each one and adds two function_call_output items. The call_id is how the model knows which result is the weather and which is the money.

Our loop runs them one after the other, which is simple and fine for two quick calls. A busy app could run them in parallel, at the same time.

17. Information Tools and Action Tools

Tools come in two families, and the difference matters a lot.

Information tools fetch something the model does not have. They read.

  • current weather
  • exchange rates
  • a customer's order status
  • stock left in a warehouse

Action tools change something in the world. They write.

  • sendEmail()
  • refundOrder()
  • bookTicket()
  • writeFile()
Loading diagram…

Look at the bottom row. A bad information call gives a wrong sentence. A bad action call sends an email you cannot unsend.

Your bank app shows both. Checking your balance is information. Sending money is action. That is why the bank asks for a PIN only for the second one.

A library is the same. Searching the catalogue is information. Borrowing a book changes who has it.

18. Levels of Agency

When does "a model with a tool" become "an agent"? It is easier to think in levels than to argue about one definition.

Loading diagram…

Read it from the bottom. Each level adds one new decision for the model.

LevelExampleWhat the model decides
1"Explain polymorphism."Only what words to write
2"What is 53 x 29?" with a calculatorWhether to use the tool
3Calculator, weather and currencyWhich tool to use
4"Convert 500 USD to INR and split it 7 ways"Which tool, in what order, and when it is finished

Level 4 is where we start calling it an agent. The system is no longer doing one isolated call. It is making a chain of decisions toward a goal.

19. The Six Parts of an Agent

Skip the long definitions. An agent has six parts:

PartWhat it isIn our website builder
GoalWhat needs to be achieved"Build a landing page for BrewLab"
Decision makerWhat picks the next stepThe LLM
ActionsWhat it can docreateDirectory, writeFile, readFile, listFiles
EnvironmentWhat the actions changeThe generated-sites/ folder
ObservationsWhat it learns after each action"File written: brewlab/index.html"
LoopKeep going until the goal is metThe tool calling loop

Joined up, the six parts make a circle:

Loading diagram…

Goal, decide, act, observe, decide again. It is the loop from section 12 with a goal at the top and a real environment at the bottom.

Think of someone cleaning a messy kitchen. The goal is a clean kitchen. Their brain picks what to do next. Their hands wash, wipe and put away. The kitchen is the environment. Looking around after each job is the observation. They stop when it is clean, not after a fixed number of moves.

20. The Agent as a State Machine

The same loop, drawn as states (the stages the agent moves through). This is how the Node and Python code actually behaves, including the stop we added for safety.

Loading diagram…

There are two ways out. Done is the happy one. StepLimitHit is the emergency exit for a model that keeps calling tools and never finishes. Section 27 explains why that exit must exist.

21. Agent vs Workflow

A workflow and an agent can build the same thing. The difference is who decides the steps.

In a workflow, we write the what and the how:

Step 1: create the directory
Step 2: write index.html
Step 3: write style.css
Step 4: write script.js

In an agent, we write the what and hand over the tools. The model decides the how.

Loading diagram…

The workflow always does the same four things, in the same order. The agent might skip script.js, or read a file back and fix it, depending on the goal.

Cooking gives a clear picture. A recipe card is a workflow: step 1, step 2, step 3. Telling a good cook "make dinner for four, there is rice and paneer in the fridge" is an agent. You gave the goal and the kitchen. They choose the steps.

Real systems mix both. A refund might be a fixed workflow with one agent step in the middle that reads the customer's message.

22. The Website Builder Agent

Now the practical part. We want to send one request:

{ "prompt": "Build a landing page for a premium coffee shop called BrewLab." }

and end up with real files:

generated-sites/
└── brewlab/
    ├── index.html
    ├── style.css
    └── script.js

Opening index.html in a browser should show a working page.

The model already knows HTML, CSS and JavaScript very well. So what is missing? Knowing how to write code is not the same as being able to save a file.

Loading diagram…

On the left, you get code you have to copy and paste yourself. On the right, the files exist when the request ends. Same model, different hands.

23. Four Tools, and No runCommand

Four tools are enough for a static website:

createDirectory(path)
writeFile(path, content)
readFile(path)
listFiles(path)

Notice what is not on the list:

@Tool
public String runCommand(String command) { ... }

It is tempting. One tool, and the model could mkdir, touch, anything. That is the problem. Your server process can reach the code, the environment variables (where secrets like API keys usually live), the network and every file the user account can read. A shell tool hands all of that to whatever the model decides to type. That includes text a user slipped into the prompt to trick it.

Loading diagram…

Both sides can build a website. Only one of them can also read your API key. The narrow tools give the model what the job needs and nothing more.

Tool = capability + permission. Giving the model a tool means letting it touch that part of your system.

A hotel key card is a good comparison. Your card opens your room and the gym. It does not open every room on the floor, even though the hotel could make one that does.

24. Sandboxing and Path Traversal

So the agent works in one folder, generated-sites/. That folder is the sandbox. The agent must not touch anything outside it:

project/
├── src/              off limits
├── .env              off limits
└── generated-sites/  the only place the agent can write

But what if the model asks for this path?

brewlab/../../.env

.. means "go up one folder". Two of them climb out of the workspace and land on your .env file with all its secrets. This trick is called path traversal. A user could also put it in the prompt on purpose: "write the file to ../../src/main/java/...".

So every tool runs every path through one check, safePath, before it touches the disk:

WorkspaceTools.java
Path safePath(String path) {
    if (path == null || path.isBlank()) {
        throw new IllegalArgumentException("A path is required.");
    }
    // Join onto the workspace, then remove every "." and "..".
    Path resolved = workspace.resolve(path.trim()).normalize();
    // Path.startsWith compares whole folder names, not characters.
    if (!resolved.startsWith(workspace)) {
        throw new IllegalArgumentException("Path '" + path + "' is outside the workspace and is not allowed.");
    }
    return resolved;
}

Here is what that check does with a few paths a model might send:

Loading diagram…

Four sample paths, one check. The last one is sneaky. As a string, generated-sites-old starts with generated-sites. That is why the Java version uses Path.startsWith (whole folder names) and the Node version uses path.relative, instead of a plain string startsWith.

When the check fails, the tool does not crash. It returns "Failed to read file: Path '../../.env' is outside the workspace". The model reads that and usually picks a proper path.

The model decides what it wants to do. Your application decides what it is allowed to do.

Never treat the model as the security boundary. A prompt that says "only write inside generated-sites" is a hint. safePath is the lock.

25. The System Prompt as a Job Description

The model needs to know its role. Without one, a model asked to "build a website" will often just paste the HTML into its reply. It has seen far more code shown in text than code saved to disk.

So the website builder gets a job description as its system prompt. A short version of the one in the code:

You are a frontend developer who builds small static websites.
Your only way to work is the workspace tools.

For every website:
1. Create one new directory named after the site, in lower case.
2. Write index.html with semantic HTML.
3. Write style.css. Make the page responsive.
4. Write script.js only if the page needs behaviour.
5. Use plain HTML, CSS and JavaScript. No frameworks.
6. When the files are written, call listFiles on the directory.
7. Read back any file you are unsure about and fix obvious mistakes.

Do not describe the code or paste it in your reply.
Create the files with the tools.

The last two lines carry the most weight. "Do not describe it. Do it." That one rule is what turns a code explainer into a code writer.

Steps 6 and 7 are also worth a look. They ask the model to observe its own work. listFiles and readFile are information tools inside an action job. They let the model check, then fix.

26. Watching the Agent Build a Site

Here is what one POST /api/sites call can look like from the inside. The exact steps depend on the model. This is a typical run.

Loading diagram…

Some return arrows are left out to keep the picture short. Every tool result does go back to the model before its next step.

The API answers with the reply and the list of files written:

{
  "reply": "Built BrewLab: a dark landing page with a hero, a menu and a contact form.",
  "files": ["brewlab/index.html", "brewlab/script.js", "brewlab/style.css"]
}

Now send a different prompt without changing any code:

{ "prompt": "A futuristic page for NeuralForge, a company that builds AI robots." }

You get neuralforge/ with its own three files. We did not build a coffee shop generator. We built a website building agent. The goal changes. The tools stay the same. The model works out how to use them each time.

27. The Step Limit

What stops an agent that never says "done"? Without a limit, nothing. A confused model can call listFiles again and again. Each round is a paid model call, and the user's request just hangs.

So the loop has a counter. MAX_STEPS (default 10) is the most rounds one request may take.

Loading diagram…

The limit turns "hang forever and burn money" into "fail fast and say why". The 502 body says "The agent stopped after 10 steps without finishing", so the caller knows to simplify the prompt or raise the limit.

Spring AI has its own cap. In application.yml we set spring.ai.tools.limits.max-total-tool-calls from the same MAX_STEPS variable. It counts tool calls instead of model rounds, but the purpose is the same. The flowchart above shows the Node and Python behaviour.

A website with three files takes several rounds. If big sites get cut off, raise the limit. Do not remove it.

28. Human in the Loop

Some actions should not run just because the model asked.

Sending a normal email might be fine. Now imagine a support agent with this tool:

refundCustomer(orderId, amount)

and the model asks for a refund of 50,000 rupees. Running that on the spot would be risky. The model might have misread the order. The customer might have tricked it.

So we pause and ask a person.

Loading diagram…

Either way, the model gets an observation and can tell the customer what happened. The decision about money stayed with a person.

This is called human in the loop. Use it for:

  • money: refunds, payments, transfers
  • things you cannot undo: deleting data, cancelling a booking
  • messages that leave the company: emails to customers, social posts
  • security changes: new users, new permissions

A simple rule: the bigger the result of a mistake, the stronger the check around the tool. An online shop already works like this for people. A new support agent can issue small refunds alone, but a large one needs a manager's click.

The code for this note has no approval step. Its tools only write files in a sandbox. A refund tool would need one.

29. Building It: the Agent Tools Service

Time to turn the ideas into a service. It has two endpoints:

EndpointBodyWhat it does
POST /api/chat{"message": "..."}Assistant with calculate, getCurrentWeather, convertCurrency. Returns {"reply": "..."}
POST /api/sites{"prompt": "..."}Website builder agent with the four workspace tools. Returns {"reply": "...", "files": [...]}

Both return 400 for an empty message or prompt, and 502 when the model provider fails or the step limit is hit.

One /api/chat request moves through the layers like this:

Loading diagram…

The HTTP layer knows nothing about tools. The agent loop knows nothing about HTTP. The tools know nothing about the model. Each part can be tested on its own, and the tests use a fake model client, so they need no key.

A few decisions worth copying:

DecisionWhy
Tools return short text, not raw JSONThe weather API returns about a kilobyte. One line has the same facts and far fewer input tokens, so it is cheaper and faster
Tool errors go back to the model as textThe model can explain "you cannot divide by zero" instead of the request failing
The weather key is optionalWithout it the tool says "weather is not configured". The model reports that instead of inventing a temperature
The weather error message never includes the URLThe URL holds the API key, and tool output goes to the model
Currency codes are checked against three capital lettersThe model can send any string. It should not end up in a URL unchecked
A new workspace object per site requestSo files lists only what this request wrote
The client is passed in, not created insideTests pass a fake that scripts a function_call, then a final text

30. The Code, Side by Side

The full projects are on the code page:

Browse the code for this note, or open src/FDE/code/05-ai-agent-tools/ on GitHub. The README there has the run and test commands for each version.

30.1 Spring AI vs Node.js and Python

The same ideas, mapped:

ConcernSpring AINode.js and Python
Define a tool@Tool and @ToolParam on a methodA definition object (type, name, description, parameters, strict) next to a run function
Give tools to the model.tools(calculatorTools, ...)tools: definitions in responses.create
Read a tool callDone for youItems in response.output with type: "function_call"
Run the toolDone for youLook up by name, JSON.parse(arguments), call run
Send the result backDone for youPush {type: "function_call_output", call_id, output}
Loop until doneDone for youA for loop up to MAX_STEPS
Tool errorA RuntimeException message goes back to the modelrunTool catches and returns "Error: ..."
Step limitspring.ai.tools.limits.max-total-tool-callsStepLimitError after MAX_STEPS rounds
Sandbox checkPath.resolve().normalize().startsWith()path.resolve + path.relative (Node), Path.resolve().is_relative_to() (Python)
Test double (a stand-in used in tests)@MockitoBean services, plain unit tests for toolsA fake client whose responses.create returns scripted outputs

Spring hides the loop, which is what you want in a real project. Node and Python show it, which is what you want while learning. Read the Node or Python loop once, and you will know what .tools(...) is doing for you.

30.2 Where to look in the code

PartSpring BootNode.jsPython
Calculator toolCalculatorTools.javatools.jstools.py
Weather and currency toolsWeatherTools.java, CurrencyTools.javatools.jstools.py
Workspace tools and safePathWorkspaceTools.javaworkspace.jsworkspace.py
The loop and the system promptsChatService.java, SiteBuilderService.javaagent.jsagent.py
HTTP layerChatController.java, SiteBuilderController.javaapp.jsmain.py
Keys, model, limitsapplication.yml.env.example.env.example
Loop testsAgentControllersTest.javaagent.test.jstest_agent.py
Sandbox testsWorkspaceToolsTest.javaworkspace.test.jstest_workspace.py

The tests check what this note cares about. The tool result goes back with the same call_id. A chained task takes one round per tool. A tool error reaches the model as text. The step limit stops a loop that never ends. ../.env, absolute paths and look-alike folders are all rejected.

30.3 What this demo leaves out on purpose

ShortcutWhat a real app does
No login on either endpointPuts both behind authentication. Anyone who can reach the port can spend your credit and write files
No approval stepAdds human in the loop for risky tools (section 28)
Paths are checked, sizes are notLimits file size, file count and total disk use per request
Symlinks are not refusedRefuses symlinks inside the workspace, or checks the real path of every file
Each request starts from nothingKeeps history per user, like note 4, if follow up messages need it
Tools run one after anotherRuns independent calls at the same time and adds timeouts per tool
The generated HTML is not checkedScans or serves generated pages from a separate origin so a bad script cannot reach your main site

31. The Full Mental Model

We started with a model that only writes text. Then we gave it one tool, then three, then a goal, an environment and a loop.

Loading diagram…

Look at the box in the middle. The model proposes. The application checks and runs. That box is where your engineering lives: which tools exist, what they may touch, how many steps are allowed, and what needs a person.

In one line:

LLM + tools + environment + observations + loop + goal = AI agent

A chatbot answers. An agent decides, acts, looks at the result and keeps going until the goal is met.

Key Takeaways

  • An LLM writes tokens. Writing mkdir portfolio does not make a folder.
  • LLM = brain, tools = hands. The application connects them and owns the hands.
  • The model's most useful job in an app is turning messy human sentences into structured inputs for normal code.
  • "Reply in JSON" is a request. A tool definition with a schema, an enum and strict: true is a contract.
  • A tool is a normal function plus a name, a description and a parameter schema. The model sees only the definition, never the body or your keys.
  • The model asks for a tool. Your application runs it and sends the result back. One user question can mean several model calls.
  • The tool calling loop: send, run the requested tool, add the result with the same call_id, send again, until the model answers with text.
  • Chained calls use one result in the next call. Independent calls can come in the same round.
  • Information tools read the world. Action tools change it, so they need more care.
  • An agent has six parts: goal, decision maker, actions, environment, observations and a loop.
  • A workflow fixes the what and the how. An agent gets the what and picks the how.
  • Tool = capability + permission. Give narrow tools like writeFile, never a general runCommand.
  • Sandbox every action tool. Check every path in code, because ../../.env will come.
  • The system prompt is the agent's job description. "Do not describe it, do it" is the line that makes it act.
  • Always cap the loop with a step limit, and put a human in front of actions that move money or cannot be undone.

In this topic

Done with this topic?

How well do you know it?

PreviousBuilding a Chatbot: Context, Memory, Streaming and Hallucinations
Practise 9 questions