跳到正文
DEV Community· Said Olano·· 16 小時前AI 評分49

如何用 Spring AI 和 Tool Calling 構建 AI 襯衫智能體

Building an AI Shirt Agent with Spring AI and Tool Calling

AI 導讀

shirts-agent 是一個以 Spring Boot 和 Spring AI 建構的示例,示範 AI 智能體如何透過 Tool Calling 呼叫 Java 方法,搜尋記憶體中的襯衫目錄並返回結構化推薦。

正文

Why a Shirt Recommender Is a Good First Spring AI Project

Most tutorials that introduce large language models to Java developers start with a chatbot that answers questions. That is a useful starting point, but it hides the part that makes LLM features valuable inside a real application: the model has to take an action that your code controls. A chatbot that only produces text is a demo. An agent that looks something up in your own system and returns a structured result is a feature.

This article walks through shirts-agent, a small Spring Boot and Spring AI example that makes the idea concrete. A user sends an item and a color to a REST endpoint. An AI agent calls a Java method that searches an in-memory catalog, and the service returns a structured shirt recommendation. The whole project fits in a few classes, which makes it a good way to see how the pieces fit together before you build something larger.

The project uses Java 21, Spring Boot 3.5.6, Spring AI 1.1.8, OpenAI's gpt-4o-mini model, and Maven. The complete source is available at github.com/josesaid/shirts-agent.

The Problem: Language Models Do Not Know Your Data

A language model is trained on public text up to a certain date. It does not know what is in your product catalog, your inventory, your customer records, or your internal APIs. If you ask it "which shirt do you recommend in red?" without any extra help, it can only guess, and a guess is not a recommendation a shop can sell.

There are several ways to connect a model to your data:

  • Stuff the data into the prompt. This works for small datasets, but it wastes tokens, grows with your catalog, and leaks everything to the model on every request.
  • Retrieval-augmented generation (RAG). You search a vector store or index for relevant documents and include them in the prompt. This is the right tool for unstructured text such as manuals or policies.
  • Tool calling (function calling). You describe a set of Java methods to the model. When the model needs information or wants to perform an action, it asks for one of those methods to be executed. Your code runs the method and returns the result, and the model uses that result to finish its answer.

shirts-agent uses tool calling. The data is structured (a catalog of shirts with a color and a name), the question has a clear shape (find a shirt of this color), and the answer must come from your system rather than from the model's imagination. Tool calling is a natural fit.

The Architecture in Four Steps

The request flow is short and easy to follow. According to the project documentation, ShirtController receives the HTTP request, ShirtService validates the input, and ShirtAgent asks the model to find a shirt. The model then calls ShirtCatalogTool.findShirtByColor and returns a structured Shirt object. Invalid input is rejected with HTTP 400 before the model is ever called.

That last point deserves emphasis. Every call to a hosted model costs money and adds latency. Validation belongs in ordinary Java code, in front of the model, so that obviously bad requests never reach it.

The layers look like this:

  1. Controller. Exposes POST /api/shirts/recommend and maps the JSON body to a request object.
  2. Service. Checks that item and color are present and well formed. Rejects bad input with a 400 response.
  3. Agent. Wraps the Spring AI chat client. It builds the prompt, tells the model what it is allowed to do, and converts the model's final answer into a Shirt.
  4. Tool. A plain Java class with a method annotated with @Tool. This method is the only place where the model touches your data.

Keeping these responsibilities separate pays off quickly. You can test the validation logic without a model, test the tool against the catalog without a model, and swap the model provider without rewriting the business logic.

Defining the Tool

In Spring AI, a tool is an ordinary Spring bean with a method that the model is allowed to call. The @Tool annotation provides a description that the model reads to decide when the tool is useful. The parameter names and descriptions matter, because the model relies on them to fill in the arguments correctly.

An illustrative version of such a tool looks like this. It is a simplified sketch to show the shape of the code, not a verbatim copy of the repository:

@Component
public class ShirtCatalogTool {

    private final ShirtCatalog catalog;

    public ShirtCatalogTool(ShirtCatalog catalog) {
        this.catalog = catalog;
    }

    @Tool(description = "Find a shirt in the catalog by its color")
    public Shirt findShirtByColor(
            @ToolParam(description = "The color of the shirt, for example red or blue") String color) {
        return catalog.findByColor(color)
                .orElseThrow(() -> new IllegalArgumentException("No shirt found for color: " + color));
    }
}

Notice what the tool does not do. It does not decide which shirt is best, it does not format prose, and it does not call the model. It takes a color, looks it up in a deterministic source of truth, and returns a typed result. The model is responsible for the conversation and the final wording. Your code is responsible for the facts.

This separation is the most important design habit when working with tools. Keep the tool small, deterministic, and easy to test. Put the intelligence where it belongs: in the orchestration of the model, not inside the data access.

Keeping the Output Structured

A recommendation that comes back as a paragraph of text is hard to use in an application. A frontend needs fields. A test needs assertions. A downstream service needs a predictable shape.

Spring AI can ask the model to return its answer as a Java object. The agent declares the target type, the framework converts the model's response into that type, and your service receives a Shirt rather than a string it has to parse. This is what the project means by "returns a structured Shirt."

Structured output reduces an entire category of bugs. You no longer depend on regular expressions over free-form text, and a change in the model's phrasing cannot silently break your parsing code. If the model returns something that does not fit the expected type, the failure shows up as an error you can handle rather than as wrong data that reaches a user.

Validation Before the Model

The service layer is where you protect both your budget and your users. In shirts-agent, the service checks the request before the agent is invoked. A missing item or color results in an HTTP 400 response, and the model is never called.

This pattern is worth copying into any LLM-backed feature. Ask yourself which inputs can be rejected with plain Java. Length limits, required fields, allowed values, and basic format checks all belong in front of the model. Each request you reject locally is a request you do not pay for, and a failure you can explain to the caller in a clear message.

Validation is also a security measure. Free-form text sent to a model can contain instructions that try to change its behavior. You do not eliminate that risk by validating input, but you reduce the surface area. Constrain what the model is asked to do, and constrain what the tool can return.

Running the Project Locally

The project is intentionally easy to run. You need JDK 21 and Maven 3.9 or later. Clone the repository, export your OpenAI key, and start the application with Maven:

git clone https://github.com/josesaid/shirts-agent.git
cd shirts-agent
export OPENAI_API_KEY=sk-...
mvn spring-boot:run

Then send a request to the endpoint on port 8080:

curl -X POST http://localhost:8080/api/shirts/recommend \
  -H "Content-Type: application/json" \
  -d '{"item":"playera","color":"red"}'

The unit tests run with mvn test, and they do not call the model. That detail matters. Testing an LLM application should not depend on a live API call for every build. Test the controller, the service validation, and the tool logic in isolation, and reserve live model calls for a small number of integration checks.

What the Project Deliberately Leaves Out

A good example is honest about its boundaries. shirts-agent has no database. The catalog is an in-memory list of four shirts, which is enough to demonstrate the pattern and nothing more. There is no messaging layer, no event stream, and no cloud deployment. There is no Dockerfile or compose file. The application runs on port 8080 and depends on the OpenAI API.

Each of these omissions is a useful discussion point. In production you would replace the in-memory catalog with a repository backed by a real database, perhaps with caching for frequently requested colors. You would add authentication to the endpoint, rate limits to protect your model budget, timeouts around the model call, and observability so you can see latency, token usage, and tool invocations. You would also decide how to handle a color that is not in the catalog: return a friendly message, suggest the closest available color, or ask the user to try again.

The point of the example is not to be production-ready. The point is to show the smallest set of moving parts that still demonstrates a real agent calling a real tool.

Lessons for Java Teams Adding LLM Features

Several practical lessons come out of a project this small.

Start from the data, not from the model. Identify the facts your feature needs and where they live. If the answer depends on your catalog, your orders, or your policies, the model should retrieve those facts through a tool rather than generate them.

Make tools boring. A tool should be a deterministic method with clear inputs and outputs. The less cleverness you put inside a tool, the easier it is to test, reason about, and trust.

Validate in code before you call the model. Use the programming language you already know to reject bad input, enforce limits, and protect your budget.

Prefer structured output. Typed results are easier to test and integrate than free-form text. Let the framework handle the conversion, and fail loudly when the shape is wrong.

Test without the model where you can. Most of your application logic should run in unit tests without network calls. Reserve live calls for integration tests that you run deliberately.

Keep the model replaceable. Spring AI abstracts the chat provider behind common interfaces. Keep your domain logic and tools free of provider-specific details so that changing models is a configuration decision rather than a rewrite.

Where Spring AI Fits in the Java Ecosystem

Spring AI brings the patterns Spring developers already know to LLM applications. Dependency injection wires the chat client and the tools. Configuration properties select the model and its settings. Auto-configuration sets up the common parts so that you can focus on your domain. The result feels familiar to any team that already builds Spring Boot services, which is the main reason to consider it for enterprise Java work.

This matters for teams in regulated or long-lived systems. The question is rarely whether a model can answer a question. The question is whether the feature can be built, tested, deployed, monitored, and maintained by the same engineers who maintain the rest of the platform. Using familiar tools reduces the cost of answering yes.

Extending the Example

If you want to take this project further, there are several natural next steps that keep the same architecture.

First, add more tools. A real shop needs to check stock, look up sizes, and calculate prices. Each of these can be another annotated method with a clear description. The model decides which tool to call based on the request, so give each tool a name and description that make its purpose obvious.

Second, add a second data source. Imagine a product search over a vector index for descriptions and reviews, combined with the exact catalog lookup you already have. Retrieval handles the fuzzy part of the question, and tools handle the exact facts. This hybrid approach is one of the most useful patterns in production LLM systems.

Third, add evaluation. Create a set of requests with expected outcomes and run them regularly. When you change a prompt, a model, or a tool description, the evaluation tells you whether the behavior got better or worse. Without evaluation, LLM features drift in ways that are hard to notice until a user complains.

Fourth, add observability. Record which tools were called, how long each step took, and how many tokens were used. This information is essential for controlling cost and diagnosing unexpected answers.

Conclusion

shirts-agent is a small project with a big lesson. A language model becomes useful in an application when it can call your code, and your code remains the source of truth. A controller accepts the request, a service validates it, an agent coordinates the model, and a tool returns the facts. Each layer has a clear job, and each can be tested on its own.

If you are a Java developer curious about AI features, this is a good place to start. Clone the repository, run the example, change the catalog, add a second tool, and watch how the model chooses between them. Then ask the question that matters for your own domain: which facts in my system should the model be allowed to ask for, and which rules should my code enforce before the model is ever involved?

The full source code and documentation are available at github.com/josesaid/shirts-agent.

Key Takeaways

  • Use tool calling when a model must answer with facts from your own system.
  • Keep tools small, deterministic, and easy to test.
  • Validate requests in ordinary Java code before calling the model.
  • Prefer structured output over free-form text for application features.
  • Test most of your logic without live model calls.
  • Keep the model provider replaceable through the framework's abstractions.

來源:DEV Community · dev.to