
前言
在 AI 应用开发中,"闭门造车" 的 AI 毫无价值 ------ 真正的企业级 AI 助手,必须能连接外部系统:查股票行情、查天气预警、查数据库数据、调用第三方 API。早期的 Function-call 虽然能实现基础的外部调用,但存在注册繁琐、参数校验缺失、调用流程不规范等问题,难以支撑复杂场景。
Spring AI 的 Tools 模块横空出世,彻底解决了这些痛点:通过@Tool注解实现工具一键注册、内置参数校验、标准化调用流程,让开发者能快速搭建 "AI 大脑 + 外部工具" 的强大应用。本文将从底层原理、常用工具开发、调用流程拆解到实战落地,全方位解析 Spring AI 工具调用技术,带你从 Function-call 的 "能用",升级到 Tools 模块的 "好用、稳定、可扩展",真正让 AI 连接外部世界。
一、Spring AI Tools 模块核心原理:注解驱动 + 标准化注册
Spring AI Tools 模块的核心设计思想是 "标准化、可扩展、低侵入",通过@Tool注解简化工具定义,通过ToolRegistry统一管理工具,通过ToolExecutor标准化执行流程。理解这三层核心机制,是掌握工具调用的关键。
1.1 @Tool 注解:工具定义的 "魔法开关"
@Tool是 Spring AI 中定义工具的核心注解,只需一个注解,就能将普通 Java 方法转化为 AI 可调用的工具。其底层是通过 AOP 扫描注解方法,自动生成ToolDefinition(工具定义),包含工具名称、描述、参数信息等元数据,供 LLM 识别和选择。
1.1.1 @Tool 注解核心属性
| 属性名 | 作用 | 必填性 | 实战注意事项 |
|---|---|---|---|
| name | 工具唯一标识 | 可选(默认取方法名) | 建议手动指定,避免方法名修改导致 LLM 调用失败 |
| description | 工具功能描述 | 必选 | 必须详细、直白,包含 "什么场景用""输入什么""返回什么",LLM 靠这个选工具 |
| parameters | 自定义参数描述 | 可选 | 复杂参数需手动指定名称、描述、是否必填,提升 LLM 参数提取准确率 |
1.1.2 基础工具定义示例(无参 / 单参 / 多参)
java
import org.springframework.ai.annotation.Tool;
import org.springframework.stereotype.Component;
// 工具类必须交给Spring管理(@Component/@Service等)
@Component
public class CommonTools {
// 无参工具:获取当前系统时间
@Tool(description = "获取当前系统时间,格式为yyyy-MM-dd HH:mm:ss,无需输入参数")
public String getCurrentTime() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
// 单参工具:根据城市名查询所属省份
@Tool(description = "根据输入的城市名称,查询该城市所属的省份。输入参数:城市名称(如北京、上海)")
public String getProvinceByCity(String city) {
// 实际开发中可对接数据库或地理信息API
Map<String, String> cityProvinceMap = new HashMap<>();
cityProvinceMap.put("北京", "北京市");
cityProvinceMap.put("上海", "上海市");
cityProvinceMap.put("深圳", "广东省");
cityProvinceMap.put("杭州", "浙江省");
return cityProvinceMap.getOrDefault(city, "未查询到该城市的省份信息");
}
// 多参工具:计算两个数的加减乘除
@Tool(
name = "calculate",
description = "实现两个数字的四则运算,支持加法、减法、乘法、除法",
parameters = {
@ToolParameter(name = "num1", description = "第一个数字,支持整数和小数", required = true),
@ToolParameter(name = "num2", description = "第二个数字,支持整数和小数,除法时不能为0", required = true),
@ToolParameter(name = "operator", description = "运算符号,可选值:+、-、*、/", required = true)
}
)
public String calculate(double num1, double num2, String operator) {
switch (operator) {
case "+": return num1 + " + " + num2 + " = " + (num1 + num2);
case "-": return num1 + " - " + num2 + " = " + (num1 - num2);
case "*": return num1 + " * " + num2 + " = " + (num1 * num2);
case "/":
if (num2 == 0) return "错误:除数不能为0";
return num1 + " / " + num2 + " = " + (num1 / num2);
default: return "错误:不支持的运算符号,仅支持+、-、*、/";
}
}
}
1.2 工具注册机制:从扫描到管理的全流程
Spring AI 的工具注册分为 "自动注册" 和 "手动注册" 两种方式,核心是通过ToolRegistry接口管理所有工具,供后续调用时查询和匹配。
1.2.1 自动注册(默认推荐)
Spring AI 启动时,会自动扫描带有@Tool注解的 Spring Bean 方法,将其转化为ToolDefinition并注册到DefaultToolRegistry(默认实现)中。无需额外配置,开箱即用。
自动注册核心流程:

1.2.2 手动注册(复杂场景适用)
当工具未被 Spring 管理(如第三方工具类)、或需要动态注册 / 卸载工具时,可通过ToolRegistry手动操作。
java
import org.springframework.ai.tool.ToolDefinition;
import org.springframework.ai.tool.ToolParameter;
import org.springframework.ai.tool.DefaultToolRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class ToolConfig {
// 手动注册第三方工具
@Bean
public ToolRegistry customToolRegistry() {
DefaultToolRegistry toolRegistry = new DefaultToolRegistry();
// 1. 定义第三方工具(无Spring注解的普通类)
ThirdPartyTool thirdPartyTool = new ThirdPartyTool();
// 2. 构建ToolDefinition
ToolDefinition toolDefinition = ToolDefinition.builder()
.name("thirdPartyWeatherTool")
.description("查询指定城市的实时天气,返回温度、湿度、天气状况")
.parameters(List.of(
ToolParameter.builder()
.name("city")
.description("城市名称,如北京、上海")
.required(true)
.build()
))
// 绑定工具执行逻辑
.function(()-> thirdPartyTool.getWeather(((String) parameters.get("city"))))
.build();
// 3. 注册工具
toolRegistry.register(toolDefinition);
return toolRegistry;
}
// 第三方工具类(无Spring注解)
static class ThirdPartyTool {
public String getWeather(String city) {
// 调用第三方天气API逻辑
return city + " 实时天气:晴,25℃,湿度50%";
}
}
}
1.3 核心组件交互:ToolRegistry + ToolExecutor
Spring AI 工具调用的核心是 "注册 - 查询 - 执行" 的闭环,涉及两个关键组件:
- ToolRegistry :工具注册表,负责存储和查询
ToolDefinition,提供get(String toolName)、findAll()等方法;- ToolExecutor:工具执行器,接收工具名称和参数,从注册表中获取工具,执行并返回结果,内置参数校验、异常处理。
组件交互时序图:

1.4 原理避坑核心要点
@Tool注解的方法必须是 Spring Bean 的方法,否则无法被自动扫描;- 工具描述(description)是 LLM 选择工具的唯一依据,描述模糊会导致工具选错,建议按 "功能 + 输入 + 输出" 格式编写;
- 复杂参数(如自定义对象)需手动指定
@ToolParameter,否则 LLM 无法正确提取参数; - 手动注册工具时,需确保
ToolDefinition的 name 唯一,避免覆盖自动注册的工具。
二、常用工具开发实战:HTTP / 数据库 / 文件操作
企业开发中,HTTP 客户端、数据库查询、文件操作是最常用的三类工具。本节将基于 Spring AI Tools 模块,提供可直接复用的企业级实现方案,包含参数校验、异常处理、LLM 友好描述。
2.1 HTTP 客户端工具:调用第三方 API
HTTP 工具是 AI 连接外部世界的核心,用于调用天气、股票、地图等第三方 API。核心需求:支持 GET/POST 请求、参数传递、响应解析、异常处理。
2.1.1 工具实现(支持 GET/POST)
java
import org.springframework.ai.annotation.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
@Component
public class HttpTool {
@Autowired
private RestTemplate restTemplate;
/**
* HTTP GET请求工具
* 适用场景:调用第三方GET接口,获取数据(如天气查询、股票行情)
* @param url 接口URL(必填,如https://api.example.com/weather)
* @param params 请求参数(可选,格式:key1=value1&key2=value2)
* @param headers 请求头(可选,格式:key1=value1&key2=value2,如Authorization=Bearer token)
* @return 接口返回的JSON字符串
*/
@Tool(
name = "httpGet",
description = "发送HTTP GET请求,用于调用第三方GET接口获取数据。参数说明:url为接口地址(必填),params为请求参数(可选,格式key1=value1&key2=value2),headers为请求头(可选,格式key1=value1&key2=value2)",
parameters = {
@ToolParameter(name = "url", description = "接口URL,如https://api.example.com/weather", required = true),
@ToolParameter(name = "params", description = "请求参数,格式key1=value1&key2=value2,可选"),
@ToolParameter(name = "headers", description = "请求头,格式key1=value1&key2=value2,可选")
}
)
public String httpGet(String url, String params, String headers) {
try {
// 1. 构建请求头
HttpHeaders httpHeaders = buildHeaders(headers);
// 2. 构建带参数的URL
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
if (params != null && !params.isEmpty()) {
for (String param : params.split("&")) {
String[] keyValue = param.split("=");
if (keyValue.length == 2) {
builder.queryParam(keyValue[0], keyValue[1]);
}
}
}
// 3. 发送请求
HttpEntity<?> entity = new HttpEntity<>(httpHeaders);
ResponseEntity<String> response = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
entity,
String.class
);
// 4. 处理响应
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
return "HTTP GET请求失败,状态码:" + response.getStatusCodeValue() + ",响应体:" + response.getBody();
}
} catch (Exception e) {
return "HTTP GET请求异常:" + e.getMessage();
}
}
/**
* HTTP POST请求工具
* 适用场景:调用第三方POST接口,提交数据(如表单提交、数据上报)
* @param url 接口URL(必填)
* @param jsonBody 请求体(JSON格式,必填)
* @param headers 请求头(可选,格式:key1=value1&key2=value2)
* @return 接口返回的JSON字符串
*/
@Tool(
name = "httpPost",
description = "发送HTTP POST请求,用于调用第三方POST接口提交数据。参数说明:url为接口地址(必填),jsonBody为请求体(JSON格式,必填),headers为请求头(可选,格式key1=value1&key2=value2)",
parameters = {
@ToolParameter(name = "url", description = "接口URL,如https://api.example.com/submit", required = true),
@ToolParameter(name = "jsonBody", description = "请求体,JSON格式字符串,如{\"name\":\"test\"}", required = true),
@ToolParameter(name = "headers", description = "请求头,格式key1=value1&key2=value2,可选")
}
)
public String httpPost(String url, String jsonBody, String headers) {
try {
// 1. 构建请求头
HttpHeaders httpHeaders = buildHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
// 2. 发送请求
HttpEntity<String> entity = new HttpEntity<>(jsonBody, httpHeaders);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.POST,
entity,
String.class
);
// 3. 处理响应
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
return "HTTP POST请求失败,状态码:" + response.getStatusCodeValue() + ",响应体:" + response.getBody();
}
} catch (Exception e) {
return "HTTP POST请求异常:" + e.getMessage();
}
}
// 辅助方法:构建请求头
private HttpHeaders buildHeaders(String headersStr) {
HttpHeaders headers = new HttpHeaders();
if (headersStr != null && !headersStr.isEmpty()) {
for (String header : headersStr.split("&")) {
String[] keyValue = header.split("=");
if (keyValue.length == 2) {
headers.add(keyValue[0], keyValue[1]);
}
}
}
return headers;
}
}
// 配置RestTemplate(Spring Boot 3.x)
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
2.1.2 开发避坑点
- 必须明确 HTTP 工具的适用场景(GET/POST),避免 LLM 混淆;
- 请求参数和请求头的格式要明确(如
key1=value1&key2=value2),LLM 无法识别复杂格式; - 异常处理要详细,返回具体的错误信息(如状态码、异常原因),方便 LLM 调整请求参数;
- 敏感信息(如 API 密钥)不要硬编码,通过配置文件注入,工具描述中提示 "headers 参数传入密钥"。
2.2 数据库查询工具:操作结构化数据
数据库工具用于 AI 查询数据库中的结构化数据(如用户信息、订单数据),核心需求:支持简单 SQL 查询、参数绑定、结果格式化,避免 SQL 注入。
2.2.1 工具实现(基于 JPA)
java
import org.springframework.ai.annotation.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
@Component
public class DatabaseTool {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 数据库查询工具(仅支持SELECT语句)
* 适用场景:查询数据库中的结构化数据(如用户信息、订单列表)
* @param sql SELECT语句(必填,仅支持SELECT,不支持INSERT/UPDATE/DELETE)
* @param params SQL参数(可选,格式:value1,value2,value3,顺序与SQL中的?对应)
* @return 查询结果(JSON格式,包含字段名和数据)
*/
@Tool(
name = "dbQuery",
description = "查询数据库中的结构化数据,仅支持SELECT语句,避免SQL注入。参数说明:sql为SELECT语句(必填,如SELECT * FROM user WHERE id=?),params为SQL参数(可选,格式value1,value2,顺序与SQL中的?对应)",
parameters = {
@ToolParameter(name = "sql", description = "SELECT语句,如SELECT name,age FROM user WHERE id=?,仅支持查询操作", required = true),
@ToolParameter(name = "params", description = "SQL参数,多个参数用逗号分隔,顺序与SQL中的?对应,可选")
}
)
public String dbQuery(String sql, String params) {
try {
// 1. 校验SQL是否为SELECT语句
if (!sql.trim().toUpperCase().startsWith("SELECT")) {
return "错误:仅支持SELECT查询语句,不支持增删改操作";
}
// 2. 处理参数
Object[] paramArray = new Object[0];
if (StringUtils.hasText(params)) {
paramArray = params.split(",");
// 去除参数前后空格
for (int i = 0; i < paramArray.length; i++) {
paramArray[i] = paramArray[i].toString().trim();
}
}
// 3. 执行查询
List<Map<String, Object>> resultList = jdbcTemplate.queryForList(sql, paramArray);
// 4. 格式化结果为JSON
return new ObjectMapper().writeValueAsString(resultList);
} catch (Exception e) {
return "数据库查询异常:" + e.getMessage();
}
}
}
// 数据库配置(application.yml)
spring:
datasource:
url: jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
2.2.1 开发避坑点
- 严格限制仅支持 SELECT 语句,禁止增删改操作,避免 AI 误操作数据库;
- 强制使用参数绑定(? 占位符),禁止直接拼接 SQL,防止 SQL 注入;
- 结果格式化为 JSON,方便 LLM 解析和展示;
- 生产环境中,建议给数据库账号分配最小权限(仅查询权限),进一步降低风险。
2.3 文件操作工具:读写本地 / 云存储文件
文件工具用于 AI 读写文件(如读取配置文件、生成报告并保存),核心需求:支持本地文件和云存储文件(如 OSS),包含路径校验、权限控制。
2.3.1 工具实现(本地文件 + OSS)
java
import org.springframework.ai.annotation.Tool;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
@Component
public class FileTool {
// OSS配置(实际开发中使用OSS SDK,此处简化)
@Value("${oss.bucket:test-bucket}")
private String ossBucket;
/**
* 读取文件工具
* 适用场景:读取本地文件或OSS文件内容(如配置文件、报告文档)
* @param filePath 文件路径(必填,本地文件:/data/test.txt;OSS文件:oss://bucket/path/test.txt)
* @return 文件内容(文本格式)
*/
@Tool(
name = "fileRead",
description = "读取本地文件或OSS文件的文本内容。参数说明:filePath为文件路径(必填,本地文件格式:/data/test.txt;OSS文件格式:oss://bucket/path/test.txt)",
parameters = {
@ToolParameter(name = "filePath", description = "文件路径,本地文件:/data/test.txt;OSS文件:oss://bucket/path/test.txt", required = true)
}
)
public String fileRead(String filePath) {
try {
// 1. 区分本地文件和OSS文件
if (filePath.startsWith("oss://")) {
// 读取OSS文件(实际开发中使用OSS SDK,此处简化)
return readOssFile(filePath);
} else {
// 读取本地文件
return readLocalFile(filePath);
}
} catch (Exception e) {
return "文件读取异常:" + e.getMessage();
}
}
/**
* 写入文件工具
* 适用场景:将内容写入本地文件或OSS文件(如生成报告、保存配置)
* @param filePath 文件路径(必填,格式同读取工具)
* @param content 写入内容(必填,文本格式)
* @param append 是否追加(可选,true=追加,false=覆盖,默认false)
* @return 写入结果(成功/失败信息)
*/
@Tool(
name = "fileWrite",
description = "将文本内容写入本地文件或OSS文件。参数说明:filePath为文件路径(必填),content为写入内容(必填),append为是否追加(可选,true=追加,false=覆盖,默认false)",
parameters = {
@ToolParameter(name = "filePath", description = "文件路径,本地文件:/data/test.txt;OSS文件:oss://bucket/path/test.txt", required = true),
@ToolParameter(name = "content", description = "写入的文本内容", required = true),
@ToolParameter(name = "append", description = "是否追加内容,true=追加,false=覆盖,默认false", required = false)
}
)
public String fileWrite(String filePath, String content, String append) {
try {
// 处理append参数(默认false)
boolean appendFlag = StringUtils.hasText(append) && Boolean.parseBoolean(append);
// 区分本地文件和OSS文件
if (filePath.startsWith("oss://")) {
// 写入OSS文件(实际开发中使用OSS SDK,此处简化)
writeOssFile(filePath, content, appendFlag);
return "OSS文件写入成功:" + filePath;
} else {
// 写入本地文件
writeLocalFile(filePath, content, appendFlag);
return "本地文件写入成功:" + filePath;
}
} catch (Exception e) {
return "文件写入异常:" + e.getMessage();
}
}
// 读取本地文件
private String readLocalFile(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
throw new FileNotFoundException("文件不存在:" + filePath);
}
if (!file.isFile()) {
throw new IOException("不是文件:" + filePath);
}
// 读取文件内容
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
return sb.toString().trim();
}
}
// 写入本地文件
private void writeLocalFile(String filePath, String content, boolean append) throws IOException {
File file = new File(filePath);
// 创建父目录
File parentDir = file.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
// 写入文件
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, append), StandardCharsets.UTF_8))) {
writer.write(content);
}
}
// 读取OSS文件(简化实现,实际使用阿里云OSS/华为云OBS SDK)
private String readOssFile(String filePath) {
String ossPath = filePath.replace("oss://" + ossBucket + "/", "");
// 实际逻辑:调用OSS SDK获取文件内容
return "OSS文件[" + ossPath + "]内容:模拟读取成功";
}
// 写入OSS文件(简化实现)
private void writeOssFile(String filePath, String content, boolean append) {
String ossPath = filePath.replace("oss://" + ossBucket + "/", "");
// 实际逻辑:调用OSS SDK写入文件
}
}
2.3.2 开发避坑点
- 明确文件路径格式(本地 / OSS),避免 LLM 传入错误路径;
- 本地文件操作需处理父目录创建、文件不存在等异常;
- 生产环境中,禁止 AI 写入敏感目录(如 /root、/etc),可通过路径白名单限制;
- 大文件读写需用流式处理,避免内存溢出。
三、工具调用全流程拆解:从需求到结果的闭环
Spring AI 的工具调用不是简单的 "调用方法",而是一套标准化的闭环流程:需求解析→工具选择→参数生成→结果处理。每个环节都有其核心逻辑和避坑点,理解流程才能更好地调试和优化 AI 助手。
3.1 需求解析:LLM 理解用户意图
核心目标:让 LLM 从用户的自然语言中,提取 "要做什么"(核心需求)。
3.1.1 实现逻辑
- 依赖 Prompt Engineering:通过 Prompt 告诉 LLM"需要分析用户需求,判断是否需要调用工具";
- 无工具需求:直接返回答案(如 "你好""什么是 AI");
- 有工具需求:明确核心动作(如 "查天气""查股票""查数据库")。
3.1.2 示例 Prompt(工具调用专用)
java
String toolPrompt = """
你是一个智能助手,具备调用外部工具的能力。请按照以下规则处理用户请求:
1. 分析用户需求:判断是否需要调用工具(如查天气、查股票、查数据库、读文件);
2. 不需要调用工具:直接用自然语言回答用户;
3. 需要调用工具:明确核心需求(如"查询北京的天气""查询贵州茅台的股价");
4. 工具选择:根据需求选择合适的工具(参考工具描述);
5. 参数提取:从需求中提取工具所需的必填参数,若参数缺失,询问用户补充。
""";
3.1.3 避坑点
- Prompt 必须明确 "工具调用的触发条件",避免 LLM 无工具需求时强行调用;
- 复杂需求(如 "查北京明天的天气并保存到文件")需让 LLM 拆分步骤(先查天气,再写文件)。
3.2 工具选择:匹配最适合的工具
核心目标:LLM 根据需求和工具描述,选择唯一的、最合适的工具。
3.2.1 实现逻辑
- LLM 获取所有工具的
ToolDefinition(名称 + 描述);- 对比需求与工具描述的匹配度(如 "查天气" 匹配 "httpGet 工具(调用天气 API)");
- 输出工具名称(如 "httpGet"),供后续执行。
3.2.2 避坑点
- 工具描述必须唯一且不重叠(如 "查天气" 和 "查股票" 的描述要明确区分);
- 避免工具名称过于相似(如 "weatherTool" 和 "weatherApiTool"),否则 LLM 会混淆;
- 多工具匹配时(如 "查天气" 可通过 HTTP 工具或第三方天气工具),在 Prompt 中指定优先级。
3.3 参数生成:提取工具所需参数
核心目标:LLM 从用户需求中,提取工具的必填参数和可选参数。
3.3.1 实现逻辑
- LLM 读取工具的
ToolParameter(名称 + 描述 + 是否必填);- 从用户需求中提取参数(如需求 "查北京的天气",提取参数
url="https://api.weather.com/now",params="city=北京");- 参数缺失时,LLM 主动询问用户补充(如 "请告诉我你要查询哪个城市的天气")。
3.3.2 避坑点
- 工具参数的描述要明确 "参数格式"(如 "城市名称,如北京、上海"),避免 LLM 提取错误格式;
- 必填参数必须标注
required=true,否则 LLM 可能忽略;- 复杂参数(如 JSON、多值参数)需在工具描述中给出示例。
3.4 结果处理:格式化并返回给用户
核心目标:将工具的执行结果(如 JSON 字符串)转化为自然语言,返回给用户。
3.4.1 实现逻辑
- 工具执行后返回原始结果(如 HTTP 接口返回的 JSON、数据库查询的 JSON);
- LLM 解析原始结果,提取关键信息(如天气接口返回
{"city":"北京","temp":25,"status":"晴"},提取 "北京实时天气:晴,25℃");- 用自然语言组织信息,返回给用户。
3.4.2 避坑点
- 原始结果可能包含冗余信息,需在 Prompt 中让 LLM "只保留关键信息";
- 工具执行失败时(如接口返回 404),LLM 需告知用户失败原因,而非直接返回错误码;
- 多工具调用结果(如 "查股票 + 保存到文件")需按步骤展示结果。
3.5 完整流程

四、实战:开发股票查询 + 天气预警双工具 AI 助手
本节将基于前面的技术铺垫,开发一个企业级 AI 助手:支持股票行情查询、天气预警两大功能,自动选择工具、提取参数、处理结果。代码完整可复用,包含环境搭建、工具开发、AI 助手整合、测试验证。
4.1 环境搭建与依赖配置
4.1.1 项目依赖(pom.xml)
XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-ai-tool-assistant</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-ai-tool-assistant</name>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M1</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring AI核心 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring AI Tools模块 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tool</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- 工具依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.48</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.1.2 配置文件(application.yml)
bash
spring:
application:
name: spring-ai-tool-assistant
# Spring AI配置(OpenAI兼容接口,如ChatGPT、智谱AI、通义千问)
ai:
openai:
api-key: sk-your-api-key
base-url: https://api.openai.com/v1 # 若用国内模型,替换为对应base-url
chat:
options:
model: gpt-3.5-turbo # 模型名称
temperature: 0.3 # 温度越低,结果越稳定
# 第三方API配置
third-party:
stock:
url: https://api.money.126.net/data/feed/1000001,1000002,000001?callback=jsonpCallback
headers: User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
weather:
url: https://restapi.amap.com/v3/weather/weatherInfo
key: your-amap-key # 高德地图API密钥(需自行申请)
4.2 核心工具开发
4.2.1 股票查询工具(调用新浪财经 API)
java
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.ai.annotation.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@Component
public class StockQueryTool {
@Autowired
private RestTemplate restTemplate;
@Value("${third-party.stock.url}")
private String stockApiUrl;
@Value("${third-party.stock.headers}")
private String stockHeaders;
/**
* 股票行情查询工具
* 适用场景:查询A股股票的实时行情(代码支持:6开头沪市、0开头深市、3开头创业板)
* @param stockCode 股票代码(必填,如贵州茅台:600519;平安银行:000001)
* @return 股票实时行情(包含股票名称、最新价、涨跌额、涨跌幅、成交量)
*/
@Tool(
name = "stockQuery",
description = "查询A股股票的实时行情,支持沪市、深市、创业板股票。参数说明:stockCode为股票代码(必填,如贵州茅台600519、平安银行000001)",
parameters = {
@ToolParameter(name = "stockCode", description = "股票代码,如600519(贵州茅台)、000001(平安银行),必填", required = true)
}
)
public String queryStock(String stockCode) {
try {
// 1. 处理股票代码(新浪API要求:沪市6开头加sh,深市0/3开头加sz)
String apiCode;
if (stockCode.startsWith("6")) {
apiCode = "sh" + stockCode;
} else if (stockCode.startsWith("0") || stockCode.startsWith("3")) {
apiCode = "sz" + stockCode;
} else {
return "错误:股票代码格式不正确,沪市6开头、深市0/3开头";
}
// 2. 构建请求URL(替换API中的默认股票代码)
String url = stockApiUrl.replace("1000001,1000002,000001", apiCode);
// 3. 构建请求头
HttpHeaders headers = new HttpHeaders();
String[] headerArray = stockHeaders.split(",");
for (String header : headerArray) {
String[] keyValue = header.split("=");
if (keyValue.length == 2) {
headers.add(keyValue[0], keyValue[1]);
}
}
// 4. 发送请求(新浪API返回格式:jsonpCallback({...}))
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, entity, String.class
);
// 5. 解析响应结果
if (response.getStatusCode().is2xxSuccessful()) {
String responseBody = response.getBody();
// 去除jsonpCallback包装
String jsonStr = responseBody.substring(
responseBody.indexOf("{"),
responseBody.lastIndexOf("}") + 1
);
JSONObject jsonObject = JSON.parseObject(jsonStr);
JSONObject stockData = jsonObject.getJSONObject(apiCode);
if (stockData == null) {
return "未查询到股票代码:" + stockCode + " 的行情数据";
}
// 提取关键字段
String stockName = stockData.getString("name"); // 股票名称
double currentPrice = stockData.getDoubleValue("price"); // 最新价
double change = stockData.getDoubleValue("updown"); // 涨跌额
double changeRate = stockData.getDoubleValue("percent"); // 涨跌幅(%)
long volume = stockData.getLongValue("volume"); // 成交量(手)
// 格式化结果
return String.format("""
%s(%s)实时行情:
最新价:%.2f 元
涨跌额:%.2f 元
涨跌幅:%.2f%%
成交量:%d 手
""", stockName, stockCode, currentPrice, change, changeRate, volume);
} else {
return "股票查询API请求失败,状态码:" + response.getStatusCodeValue();
}
} catch (Exception e) {
return "股票查询异常:" + e.getMessage();
}
}
}
4.2.2 天气预警工具(调用高德天气 API)
java
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.ai.annotation.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@Component
public class WeatherAlertTool {
@Autowired
private RestTemplate restTemplate;
@Value("${third-party.weather.url}")
private String weatherApiUrl;
@Value("${third-party.weather.key}")
private String weatherApiKey;
/**
* 天气预警工具
* 适用场景:查询指定城市的实时天气和预警信息(如暴雨预警、高温预警)
* @param city 城市名称(必填,如北京、上海、广州)
* @return 天气信息(包含温度、湿度、天气状况、预警信息)
*/
@Tool(
name = "weatherAlert",
description = "查询指定城市的实时天气和预警信息,支持国内主要城市。参数说明:city为城市名称(必填,如北京、上海、广州)",
parameters = {
@ToolParameter(name = "city", description = "城市名称,如北京、上海、广州,必填", required = true)
}
)
public String queryWeather(String city) {
try {
// 1. 构建请求URL(带参数)
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(weatherApiUrl)
.queryParam("key", weatherApiKey)
.queryParam("city", city)
.queryParam("extensions", "all"); // extensions=all 包含预警信息
// 2. 发送请求
HttpHeaders headers = new HttpHeaders();
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
builder.toUriString(), HttpMethod.GET, entity, String.class
);
// 3. 解析响应结果
if (response.getStatusCode().is2xxSuccessful()) {
String responseBody = response.getBody();
JSONObject jsonObject = JSON.parseObject(responseBody);
// 校验返回状态
String status = jsonObject.getString("status");
if (!"1".equals(status)) {
return "天气查询失败:" + jsonObject.getString("info");
}
// 提取实时天气信息
JSONArray lives = jsonObject.getJSONArray("lives");
if (lives.isEmpty()) {
return "未查询到城市:" + city + " 的天气信息";
}
JSONObject liveWeather = lives.getJSONObject(0);
String temperature = liveWeather.getString("temperature"); // 温度
String humidity = liveWeather.getString("humidity"); // 湿度
String weather = liveWeather.getString("weather"); // 天气状况
String windDirection = liveWeather.getString("winddirection"); // 风向
String windPower = liveWeather.getString("windpower"); // 风力
// 提取预警信息
JSONArray alerts = jsonObject.getJSONArray("alerts");
String alertInfo = alerts.isEmpty() ? "无预警信息" : "预警信息:" + alerts.getJSONObject(0).getString("info");
// 格式化结果
return String.format("""
%s 实时天气:
天气状况:%s
温度:%s ℃
湿度:%s %%
风向:%s
风力:%s 级
%s
""", city, weather, temperature, humidity, windDirection, windPower, alertInfo);
} else {
return "天气查询API请求失败,状态码:" + response.getStatusCodeValue();
}
} catch (Exception e) {
return "天气查询异常:" + e.getMessage();
}
}
}
4.3 AI 助手整合:自动选择工具并调用
4.3.1 工具调用配置
java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.ToolCallingAdvisor;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.tool.ToolRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AiAssistantConfig {
// 工具调用专用Prompt
private static final String TOOL_PROMPT = """
你是一个智能助手,具备股票查询和天气预警两大工具调用能力,请严格按照以下规则处理用户请求:
1. 需求分析:判断用户是要查股票还是查天气,或不需要调用工具;
2. 工具选择:
- 查股票:调用stockQuery工具,必填参数stockCode(股票代码);
- 查天气:调用weatherAlert工具,必填参数city(城市名称);
3. 参数提取:从用户需求中提取必填参数,若参数缺失,询问用户补充;
4. 结果处理:工具返回结果后,用自然语言简洁明了地展示,不要包含原始JSON;
5. 不需要调用工具:直接用自然语言回答,不要调用任何工具。
""";
@Bean
public ChatClient aiAssistantChatClient(ChatClient.Builder builder, ToolRegistry toolRegistry) {
return builder
// 配置Prompt
.prompt(PromptTemplate.from(TOOL_PROMPT))
// 添加工具调用Advisor(核心:自动处理工具调用流程)
.advisor(new ToolCallingAdvisor(toolRegistry))
.build();
}
// 配置RestTemplate
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
4.3.2 AI 助手服务实现
java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class AiAssistantService {
private final ChatClient aiAssistantChatClient;
// 构造函数注入ChatClient
public AiAssistantService(ChatClient aiAssistantChatClient) {
this.aiAssistantChatClient = aiAssistantChatClient;
}
// AI助手核心方法:处理用户请求
public String handleUserRequest(String userInput) {
try {
// 调用ChatClient,自动处理工具调用流程
return aiAssistantChatClient
.prompt(userInput)
.call()
.content();
} catch (Exception e) {
return "AI助手处理异常:" + e.getMessage();
}
}
}
4.3.3 控制器实现(HTTP 接口)
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
@RestController
@RequestMapping("/api/ai-assistant")
public class AiAssistantController {
@Autowired
private AiAssistantService aiAssistantService;
// 请求DTO
public record UserRequest(
@NotBlank(message = "请求内容不能为空")
String input
) {}
// 响应DTO
public record UserResponse(
int code,
String message,
String data
) {
public static UserResponse success(String data) {
return new UserResponse(200, "操作成功", data);
}
public static UserResponse fail(String message) {
return new UserResponse(500, message, null);
}
}
// 处理用户请求
@PostMapping("/chat")
public UserResponse chat(@Valid @RequestBody UserRequest request) {
try {
String response = aiAssistantService.handleUserRequest(request.input());
return UserResponse.success(response);
} catch (Exception e) {
return UserResponse.fail(e.getMessage());
}
}
}
4.4 测试验证:AI 助手是否能正确调用工具
4.4.1 测试用例 1:查询股票行情
- 请求 URL:
http://localhost:8080/api/ai-assistant/chat- 请求体:
{"input":"查询贵州茅台的股票行情"}- 预期响应:
bash{ "code": 200, "message": "操作成功", "data": "贵州茅台(600519)实时行情:\n最新价:1800.00 元\n涨跌额:+20.50 元\n涨跌幅:1.15%\n成交量:12345 手" }
4.4.2 测试用例 2:查询天气预警
- 请求 URL:
http://localhost:8080/api/ai-assistant/chat- 请求体:
{"input":"查询北京的天气预警"}- 预期响应:
bash{ "code": 200, "message": "操作成功", "data": "北京 实时天气:\n天气状况:晴\n温度:25 ℃\n湿度:50 %\n风向:北风\n风力:2 级\n无预警信息" }
4.4.3 测试用例 3:参数缺失(询问用户补充)
- 请求 URL:
http://localhost:8080/api/ai-assistant/chat- 请求体:
{"input":"查询股票行情"}- 预期响应:
bash{ "code": 200, "message": "操作成功", "data": "请告诉我你要查询的股票代码(如贵州茅台600519、平安银行000001)" }
4.4.4 测试用例 4:无需工具调用(直接回答)
- 请求 URL:
http://localhost:8080/api/ai-assistant/chat- 请求体:
{"input":"你好,介绍一下自己"}- 预期响应:
bash{ "code": 200, "message": "操作成功", "data": "你好!我是智能助手,具备股票查询和天气预警功能。你可以让我查询A股股票的实时行情,或查询国内城市的天气和预警信息,直接告诉我你的需求即可~" }
4.5 实战避坑总结
- 第三方 API 密钥不要硬编码,通过配置文件注入,生产环境用环境变量;
- 工具的
description必须和 AI 助手的 Prompt 对应,否则 LLM 会选错工具; - 股票代码、城市名称等参数需做格式校验,避免调用 API 失败;
- API 返回结果可能格式不规范(如新浪的 JSONP),需做好解析容错;
- 测试时先单独测试工具是否能正常运行,再测试 AI 助手的工具选择逻辑;
- 生产环境中,给工具调用添加日志打印,方便调试工具调用流程。
五、总结与展望
Spring AI 的 Tools 模块彻底重构了 AI 的外部调用能力 ------ 从 Function-call 的 "零散方法调用",升级为 "注解驱动 + 标准化注册 + 自动化流程" 的企业级方案。通过@Tool注解,开发者能快速将普通 Java 方法转化为 AI 可调用的工具;通过 ToolRegistry 和 ToolExecutor,实现工具的统一管理和标准化执行;通过 Prompt Engineering,让 LLM 能自动理解需求、选择工具、提取参数、处理结果。
本文从原理、工具开发、流程拆解到实战落地,全方位覆盖了 Spring AI 工具调用的核心技术。无论是 HTTP 客户端、数据库查询、文件操作等通用工具,还是股票查询、天气预警等业务工具,都能基于这套方案快速实现。
未来,Spring AI 的 Tools 模块将支持更多高级特性:工具链(多工具串联调用)、工具权限控制、工具调用日志审计等。建议开发者持续关注 Spring AI 官方文档,将工具调用与 RAG、Agent 等技术结合,构建更强大的企业级 AI 应用。
附录:常用配置与工具类
1. 高德地图 API 申请流程
- 访问高德开放平台(https://lbs.amap.com/),注册账号并登录;
- 进入 "控制台→应用管理→创建应用",填写应用名称;
- 为应用添加 "天气
