内容概览
- Tool Calling 原理:大模型如何决定调用哪个工具、传递哪些参数
- Spring AI 注解:@Tool 和 @ToolParam 的用法
- ToolCallingAdvisor 配置:最大调用次数限制、回退策略
一、@Tool 注解与工具调用
1. 核心概念理解
什么是 Tool Calling?
- 大模型本身只能生成文本,无法直接查数据库、调 API 或执行计算。
- Tool Calling 机制让模型在对话中"决定"调用你定义的 Java 方法,拿到执行结果后继续生成回答。
- 类比:模型是"大脑",Tool 是"手脚"。大脑决定要做什么,手脚去执行,执行结果再反馈给大脑继续思考。
Tool Calling 的完整循环
用户提问:"北京今天天气怎么样?"
↓
模型推理:需要调用天气查询工具
↓
生成 Tool Call 请求:getWeather(city="北京")
↓
ToolCallingAdvisor 拦截并执行 Java 方法 getWeather("北京")
↓
Java 方法返回结果:"北京,28°C,晴"
↓
结果回传给模型
↓
模型生成最终回答:"北京今天气温28°C,天气晴朗,适合外出。"
Spring AI 2.0 的关键变化
- 1.x 版本用 @Bean Function<...> 注册工具,代码冗长。
- 2.0 版本只需用 @Tool 注解标注普通方法,一个注解即可完成注册。
- 工具调用循环由 ChatModel 内部黑盒提升为 ToolCallingAdvisor,可组合、可观测。
2. 定义第一个工具类
新增一个计算器工具类:
java
@Component
public class CalculatorTools {
@Tool(description = "计算两个数的加法。当用户需要进行数学加法运算时使用此工具。")
public double add(
@ToolParam(description = "第一个加数") double a,
@ToolParam(description = "第二个加数") double b) {
return a + b;
}
@Tool(description = "计算两个数的乘法。当用户需要进行数学乘法运算时使用此工具。")
public double multiply(
@ToolParam(description = "第一个因数") double a,
@ToolParam(description = "第二个因数") double b) {
return a * b;
}
}
关键点:
- @Tool 的 description 是给模型看的,要清晰描述"什么场景下该用"。
- @ToolParam 的 description 也是给模型看的,说明每个参数的含义。
- Spring AI 会自动根据方法签名生成 JSON Schema,无需手写。
3. 注册工具到 ChatClient
java
@RestController
@RequestMapping("/agent")
public class AgentController {
private final ChatClient agentChatClient;
public AgentController(ChatClient.Builder builder, CalculatorTools calculatorTools) {
this.agentChatClient = builder
.defaultSystem("你是一个智能助手,可以帮用户做数学计算。")
.defaultTools(calculatorTools) // 注册工具
.build();
}
@GetMapping("/chat")
public String chat(@RequestParam String message) {
return agentChatClient.prompt()
.user(message)
.call()
.content();
}
}
4. 测试工具调用
bash
# 触发工具调用
curl "http://localhost:8080/agent/chat?message=帮我算一下123乘以456等于多少"
# 不触发工具调用(模型直接回答)
curl "http://localhost:8080/agent/chat?message=什么是微服务架构"
- 第一个请求会调用 multiply 工具,返回计算结果。
- 第二个请求,模型直接回答,不调用工具。
5. 观察工具调用过程
在 application.yml 中开启日志,便于追踪工具调用的完整流程:
logging: level: org.springframework.ai: DEBUG
二、ToolCallingAdvisor 深入与多步工具调用
1. ToolCallingAdvisor 执行流程
用户消息 → ChatClient
↓
Advisor 链执行(按 order 排序)
↓
ToolCallingAdvisor(DEFAULT_ORDER=300)
↓
① 将工具定义(JSON Schema)注入请求
↓
② 发送给模型
↓
③ 模型返回 tool_call → 执行工具 → 结果回传模型 → 循环
↓
④ 模型返回纯文本(不再调工具)→ 退出循环 → 返回最终回答
2. 添加更多工具,支持多步调用
java
@Component
public class UnitConvertTools {
@Tool(description = "将美元转换为人民币。当用户需要进行美元到人民币的汇率换算时使用此工具。")
public String convertUsdToCny(
@ToolParam(description = "美元金额") double usd) {
// 模拟实时汇率(生产环境应调API)
double rate = 7.24;
double cny = usd * rate;
return String.format("%.2f USD = %.2f CNY(汇率:%.4f)", usd, cny, rate);
}
@Tool(description = "将摄氏度转换为华氏度。当用户需要进行温度单位换算时使用此工具。")
public String celsiusToFahrenheit(
@ToolParam(description = "摄氏度温度值") double celsius) {
double fahrenheit = celsius * 9.0 / 5.0 + 32;
return String.format("%.1f°C = %.1f°F", celsius, fahrenheit);
}
}
注册多个工具到 ChatClient:
java
public AgentController(ChatClient.Builder builder,
CalculatorTools calculatorTools,
UnitConvertTools unitConvertTools) {
this.agentChatClient = builder
.defaultSystem("你是一个智能助手,可以帮用户做数学计算和单位换算。")
.defaultTools(calculatorTools, unitConvertTools) // 注册多个工具
.build();
}
3. 测试多步工具调用
bash
# 单步调用:只需要一个工具
curl "http://localhost:8080/agent/chat?message=100美元等于多少人民币"
# 多步调用:需要先用一个工具,再用另一个
curl "http://localhost:8080/agent/chat?message=我有500美元,换成人民币后能买多少件单价为199元的商品"
多步调用流程示例:
- 第一步:调用 convertUsdToCny(500) 得到 3620 元
- 第二步:用 multiply 或模型自行除法 → 3620/199 ≈ 18 件
- 最终回答:"500美元约合3620元人民币,可以购买18件单价199元的商品。"
4. 配置 maxToolExecuteCount
- 工具调用循环默认最多执行 5 次(防止死循环)。
- 如需更多轮次,可自定义最大调用次数:
java
this.agentChatClient = builder
.defaultSystem("你是一个智能助手,可以帮用户做数学计算和单位换算。")
.defaultTools(calculatorTools, unitConvertTools)
.defaultAdvisors(
ToolCallingAdvisor.builder()
.toolCallingManager(ToolCallingManager.builder()
.maxTotalToolCalls(10) // 最多调用10次工具
.build())
.build()
)
.build();
一般设置 5-10 次即可,过小模型来不及完成多步推理,过大会有死循环风险。
三、工具与 RAG 组合(Advisor 链编排)
1. Advisor 链的组合能力
- RAG(检索增强生成)用的是 RetrievalAugmentationAdvisor
- 现在可与 ToolCallingAdvisor 组合,形成多阶段处理链:
用户提问
↓
Memory Advisor(加载历史对话)
↓
ToolCallingAdvisor(如需工具调用)
↓
DocumentRetrievalAdvisor(如需查知识库)
↓
模型生成最终回答
2. 编写"智能运维助手"工具集
java
/**
* 智能运维助手工具
*/
@Component
public class OpsTools {
@Tool(description = "查询指定服务的当前健康状态。当用户询问某个服务是否正常、是否宕机时使用此工具。")
public String checkServiceHealth(@ToolParam(description = "服务名称,如order-service、user-service") String serviceName) {
// 模拟服务健康检查(生产环境调Actuator或监控系统API)
Map<String, String> statusMap = Map.of(
"order-service", "UP - CPU: 45%, Memory: 62%, QPS: 850",
"user-service", "UP - CPU: 23%, Memory: 41%, QPS: 320",
"payment-service", "DOWN - CPU: 98%, Memory: 89%, 连接池耗尽"
);
return statusMap.getOrDefault(serviceName, "未知服务: " + serviceName + ",请确认服务名称是否正确");
}
@Tool(description = "查询指定服务最近1小时的错误日志摘要。当用户询问某个服务有什么报错、异常时使用此工具。")
public String getRecentErrors(@ToolParam(description = "服务名称") String serviceName,
@ToolParam(description = "查询最近多少分钟的日志,默认60") Integer minutes) {
// 模拟日志查询(生产环境调ELK或Loki)
if ("payment-service".equals(serviceName)) {
return String.format("""
最近%d分钟错误日志摘要(共127条):
1. [ERROR] HikariPool-1 - Connection is not available, request timed out after 30000ms (出现89次)
2. [ERROR] OrderController - java.lang.NullPointerException at line 42 (出现23次)
3. [WARN] Redis connection lost, retrying... (出现15次)
""", minutes != null ? minutes : 60);
}
return "最近" + (minutes != null ? minutes : 60) + "分钟内无错误日志";
}
@Tool(description = "查询数据库连接池的当前状态。当用户询问数据库连接、连接池、慢查询等问题时使用此工具。")
public String getDbPoolStatus(@ToolParam(description = "数据源名称,如master、slave") String datasourceName) {
// 模拟数据库连接池状态
return String.format("""
数据源 [%s] 连接池状态:
- 最大连接数:50
- 活跃连接数:48
- 空闲连接数:2
- 等待队列:37个请求
- 最近慢查询:SELECT * FROM orders WHERE status='PENDING'(耗时12.3s)
""", datasourceName);
}
}
3. 组合 Advisor 链
java
/**
* Agent 配置类
*/
@Configuration
public class AgentConfig {
@Bean
public ChatClient opsAgentChatClient(ChatClient.Builder builder,
OpsTools opsTools,
VectorStore vectorStore,
ChatMemory chatMemory) {
return builder
.defaultSystem("""
你是一个智能运维助手,负责帮助开发和运维团队排查线上问题。
你可以通过工具查询服务状态、错误日志和数据库连接池状态。
也可以通过知识库查询历史故障处理方案。
排查问题时,请先收集信息(调工具),再给出分析结论和处理建议。
""")
.defaultAdvisors(
// 对话记忆(保留最近10轮)
MessageChatMemoryAdvisor.builder(chatMemory)
.order(0)
.build(),
// RAG 知识库检索(允许空上下文,让 LLM 能触发工具调用)
RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.7)
.topK(6)
.build())
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(true)
.build())
.build()
)
.defaultTools(opsTools) // 注册运维工具
.build();
}
}
4. 测试组合效果
bash
# 只触发工具调用
curl "http://localhost:8080/agent/ops/chat?message=payment-service好像挂了,帮我看看"
# 同时触发工具+知识库(如果知识库有历史处理方案)
curl "http://localhost:8080/agent/ops/chat?message=payment-service连接池耗尽了,之前有没有类似的处理方案"
第二个请求流程:
- 调用 checkServiceHealth("payment-service") 确认服务状态
- 调用 getRecentErrors("payment-service", 60) 查看错误日志
- 调用 getDbPoolStatus("master") 查看连接池
- 检索知识库历史处理方案
- 综合所有信息,给出分析和建议
以上为 Spring AI 工具调用与 RAG 组合的核心实践流程,欢迎参考与采纳。