9 Practical Tips for Efficient Spring AI 2.0 Agent Development
The article shares nine hands‑on tips for building Spring AI 2.0 agents—including using ChatClient as the entry point, delegating tool calls to ToolCallingAdvisor, defining tools with @Tool, crafting effective system prompts, leveraging Advisor chains, streaming responses early, managing conversation memory, limiting tool count, and adding observability—each illustrated with concrete code snippets.
In Spring AI 2.0 an Agent follows the loop: user question → model decides whether to call a tool → tool runs and returns a result → model produces the final answer. The tool‑calling loop is extracted from each ChatModel and managed centrally by ChatClient together with ToolCallingAdvisor, which clarifies the architecture and simplifies debugging.
Tip 1 – Use ChatClient as the entry point
Start every Agent from ChatClient instead of invoking ChatModel directly. ChatClient bundles an advisor chain, tool registration and streaming support, eliminating most glue code.
@RestController
@RequestMapping("/agent")
public class AgentController {
private final ChatClient chatClient;
public AgentController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个 helpful 的助手,回答要简洁。")
.build();
}
@PostMapping("/chat")
public String chat(@RequestBody ChatRequest request) {
return chatClient.prompt()
.user(request.message())
.call()
.content();
}
}Configure the model in application.yml; Spring Boot injects ChatClient.Builder automatically.
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
chat:
options:
model: qwen3.6-plus
temperature: 0.3Tip 2 – Let ToolCallingAdvisor manage tool calls
Before 2.0 each model implementation contained its own tool‑calling logic, causing compatibility issues when swapping models. From 2.0 onward ToolCallingAdvisor uniformly handles the tool loop, and ChatClient registers it automatically.
@Service
public class WeatherAgent {
private final ChatClient chatClient;
private final WeatherTools weatherTools;
public WeatherAgent(ChatClient.Builder builder, WeatherTools weatherTools) {
this.weatherTools = weatherTools;
this.chatClient = builder.build();
}
public String ask(String question) {
return chatClient.prompt()
.user(question)
.tools(weatherTools) // tool registration, loop handled by ToolCallingAdvisor
.call()
.content();
}
}Tip 3 – Define tools with @Tool to reduce boilerplate
Create a plain Java class and annotate methods with @Tool. The framework extracts the description, parameters and return types automatically.
@Component
public class WeatherTools {
@Tool(description = "根据城市名查询当前天气,例如:北京、上海")
public String getWeather(@ToolParam(description = "城市名称,不含'市'字") String city) {
return switch (city) {
case "北京" -> "晴,28°C,东北风 2 级";
case "上海" -> "多云,26°C,东南风 3 级";
default -> "暂无 " + city + " 的天气数据";
};
}
@Tool(description = "查询指定城市未来三天的天气预报")
public List<String> getForecast(String city) {
return List.of(
city + " 明天:多云,25~31°C",
city + " 后天:小雨,22~27°C",
city + " 大后天:晴,24~30°C"
);
}
}Write clear description strings, annotate each argument with @ToolParam, and keep return types simple (e.g., String or a plain POJO).
Tip 4 – System prompt beats model switching
Instead of changing the model when an Agent misbehaves, craft a detailed system prompt that defines role, boundaries, tool‑use rules and output format. This often yields better results and saves cost.
this.chatClient = builder
.defaultSystem("""
你是「小助手」,一个面向企业内部员工的问答 Agent。
规则:
1. 涉及公司制度、流程的问题必须先调用 searchKnowledge 工具检索,不能凭记忆回答。
2. 不确定的信息,明确说「我不确定」,不要编造。
3. 回答用中文,条理清晰,必要时用编号列表。
4. 单次回答控制在 300 字以内,除非用户要求详细说明。
""")
.build();Store the prompt in a configuration file or database for easy updates without redeploying.
Tip 5 – Split responsibilities with an Advisor chain
Spring AI 2.0’s Advisor mechanism decouples concerns such as memory, retrieval‑augmented generation (RAG), tool calling and custom logic (e.g., logging, permission checks). MessageChatMemoryAdvisor – automatically reads and writes conversation history. QuestionAnswerAdvisor – adds RAG retrieval. ToolCallingAdvisor – handles the tool‑calling loop (registered automatically).
Custom advisors – can implement audit logging, permission validation, sensitive‑word filtering, etc.
@Configuration
public class AgentConfig {
@Bean
ChatClient agentChatClient(ChatClient.Builder builder, ChatMemory chatMemory, VectorStore vectorStore) {
var memoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory).build();
var ragAdvisor = QuestionAnswerAdvisor.builder(vectorStore).build();
return builder
.defaultAdvisors(memoryAdvisor, ragAdvisor)
.defaultSystem("你是企业知识库助手,优先依据检索结果回答。")
.build();
}
}Set a custom advisor’s order higher than ToolCallingAdvisor (larger numeric value) so it runs inside the tool loop.
Tip 6 – Hook streaming output early
Tool calls can cause several‑second pauses. Use Server‑Sent Events (SSE) to send partial output as soon as it is available, preventing a blank UI.
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.tools(weatherTools)
.stream()
.content();
}Front‑end should display a loading indicator during tool‑call pauses.
Tip 7 – Let ChatMemory manage conversation state
Use ChatMemory together with MessageChatMemoryAdvisor instead of manually assembling a List<Message>.
@Bean
ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20) // keep recent 20 messages to avoid token explosion
.build();
}
public String chat(String conversationId, String message) {
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
}Keep the memory window small; store important information elsewhere and retrieve it when needed.
Tip 8 – Keep the tool set small
Registering dozens of tools confuses the model. Limit each Agent to 5‑8 tools, merge similar functions, and split complex workflows into multiple specialised agents.
// Bad: too many tools, model gets confused
.tools(tool1, tool2, tool3, tool4, tool5, tool6, tool7, tool8, tool9, tool10)
// Good: group tools by scenario
.tools(orderTools) // 订单 Agent
.tools(inventoryTools) // 库存 AgentTip 9 – Add observability from the start
Implement a custom CallAdvisor that logs request details, measures latency and records token usage. Spring AI also integrates Micrometer for Prometheus/Grafana metrics, which is crucial because agents are billed per token.
@Slf4j
@Component
public class AgentAuditAdvisor implements CallAdvisor {
@Override
public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
log.info("Agent 请求 | user={}", request.prompt().getUserMessage());
long start = System.currentTimeMillis();
ChatClientResponse response = chain.nextCall(request);
log.info("Agent 响应 | 耗时={}ms | tokens={}",
System.currentTimeMillis() - start,
response.chatResponse().getMetadata().getUsage());
return response;
}
@Override
public String getName() { return "AgentAuditAdvisor"; }
@Override
public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 100; }
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
java1234
Former senior programmer at a Fortune Global 500 company, dedicated to sharing Java expertise. Visit Feng's site: Java Knowledge Sharing, www.java1234.com
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
