AI 模型本身只能根据训练数据回答问题,无法获取实时信息、执行计算或操作外部系统。工具调用(Tool Calling) 让模型可以在对话过程中请求调用外部函数,由框架执行后将结果返回给模型,从而大幅扩展 AI 的能力边界。
接下来实现三个工具:获取当前时间、数学计算、联网搜索,并将它们集成到现有的聊天系统中。
1. 工具调用原理
2. 定义工具
Spring AI 使用 @Tool 注解定义工具,非常简洁。每个工具就是一个普通的 Java 方法。
2.1 时间工具
src/main/java/com/albertstack/aichat/tool/DateTimeTool.java
package com.albertstack.aichat.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@Component
public class DateTimeTool {
@Tool(description = "获取指定时区的当前日期和时间。如果用户没有指定时区,默认使用 Asia/Shanghai")
public String getCurrentDateTime(
@ToolParam(description = "时区 ID,例如 Asia/Shanghai、America/New_York", required = false)
String timezone) {
ZoneId zoneId = timezone != null ? ZoneId.of(timezone) : ZoneId.of("Asia/Shanghai");
return LocalDateTime.now(zoneId)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss (EEEE)"));
}
}2.2 数学计算工具
src/main/java/com/albertstack/aichat/tool/MathTool.java
package com.albertstack.aichat.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class MathTool {
@Tool(description = "计算数学表达式。支持加减乘除、幂运算、取余等。当需要精确计算时使用此工具")
public String calculate(
@ToolParam(description = "数学表达式,例如 (3 + 5) * 2、2^10、100 % 7")
String expression) {
try {
// 使用 JavaScript 引擎计算表达式(安全沙箱)
Object result = new javax.script.ScriptEngineManager()
.getEngineByName("js")
.eval(expression);
return expression + " = " + result;
} catch (Exception e) {
return "计算失败: " + e.getMessage();
}
}
}2.3 联网搜索工具
联网搜索是最实用的工具。我们使用 Tavily,一个专为 AI 设计的搜索 API,支持全网搜索并返回结构化结果。
在 application.yaml 中添加 Tavily 配置:
ai:
tool:
tavily:
api-key: your-tavily-api-keysrc/main/java/com/albertstack/aichat/tool/WebSearchTool.java
package com.albertstack.aichat.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.List;
import java.util.Map;
@Component
public class WebSearchTool {
private final WebClient webClient;
private final String apiKey;
public WebSearchTool(@Value("${ai.tool.tavily.api-key}") String apiKey) {
this.webClient = WebClient.builder()
.baseUrl("https://api.tavily.com")
.build();
this.apiKey = apiKey;
}
@Tool(description = "搜索互联网获取实时信息。当用户询问最新新闻、实时数据、不确定的事实时使用此工具")
public String searchWeb(
@ToolParam(description = "搜索关键词") String query) {
try {
Map<String, Object> response = webClient.post()
.uri("/search")
.bodyValue(Map.of(
"api_key", apiKey,
"query", query,
"max_results", 5,
"include_answer", true
))
.retrieve()
.bodyToMono(Map.class)
.block();
if (response == null) return "搜索无结果";
StringBuilder sb = new StringBuilder();
// Tavily 直接返回的 AI 摘要回答
String answer = (String) response.get("answer");
if (answer != null && !answer.isBlank()) {
sb.append("摘要: ").append(answer).append("\n\n");
}
// 搜索结果列表
List<Map<String, Object>> results = (List<Map<String, Object>>) response.get("results");
if (results != null && !results.isEmpty()) {
sb.append("搜索结果:\n");
for (Map<String, Object> result : results) {
String title = (String) result.get("title");
String content = (String) result.get("content");
String url = (String) result.get("url");
if (title != null) {
sb.append("- **").append(title).append("**\n");
if (content != null) sb.append(" ").append(content, 0, Math.min(200, content.length())).append("\n");
if (url != null) sb.append(" 来源: ").append(url).append("\n");
}
}
}
return sb.isEmpty() ? "未找到相关信息" : sb.toString();
} catch (Exception e) {
return "搜索失败: " + e.getMessage();
}
}
}Tavily 的 include_answer: true 参数会让 API 直接返回一个 AI 摘要,加上原始搜索结果,模型可以基于这些信息组织更准确的回答。
3. 注册工具到 ChatClient
工具需要注册到 ChatClient 才能被模型调用。更新 AiConfig,将工具注册为默认工具:
src/main/java/com/albertstack/aichat/config/AiConfig.java
package com.albertstack.aichat.config;
import com.albertstack.aichat.tool.DateTimeTool;
import com.albertstack.aichat.tool.MathTool;
import com.albertstack.aichat.tool.WebSearchTool;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Configuration
public class AiConfig {
@Value("${ai.model.no-think}")
private String noThinkModel;
@Value("${ai.prompt.default}")
private Resource defaultPrompt;
@Value("${ai.prompt.no-think}")
private Resource noThinkPrompt;
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
DateTimeTool dateTimeTool,
MathTool mathTool,
WebSearchTool webSearchTool) throws IOException {
return builder
.defaultSystem(defaultPrompt.getContentAsString(StandardCharsets.UTF_8))
.defaultTools(dateTimeTool, mathTool, webSearchTool)
.build();
}
@Bean
public ChatClient noThinkChatClient(ChatClient.Builder builder,
DateTimeTool dateTimeTool,
MathTool mathTool,
WebSearchTool webSearchTool) throws IOException {
return builder
.defaultSystem(noThinkPrompt.getContentAsString(StandardCharsets.UTF_8))
.defaultTools(dateTimeTool, mathTool, webSearchTool)
.defaultOptions(OllamaChatOptions.builder()
.model(noThinkModel)
.build())
.build();
}
}关键变化:
- 三个工具通过构造函数注入(它们是
@Component,Spring 自动管理) .defaultTools(dateTimeTool, mathTool, webSearchTool)将工具注册为默认工具,每次对话都可用- 两个 ChatClient 都注册了相同的工具,思考模型和非思考模型都能使用
4. 更新系统提示词
工具虽然已注册,但模型需要知道何时该使用它们。更新系统提示词,添加工具使用指引:
src/main/resources/prompts/default.txt
你是一个严谨的AI助手,请严格遵循以下规则:
【事实性要求】
- 只能基于已知信息或合理推断回答问题
- 不允许编造、虚构或猜测事实
- 若信息不足,请明确说明"无法确定"或"需要更多信息"
【表达要求】
- 使用简洁、清晰、专业的中文回答
- 避免冗余、废话和重复内容
- 优先给出结论,再补充必要解释
【风格要求】
- 保持客观、中立、技术导向
- 不使用情绪化表达或迎合性语言
【工具使用】
- 当用户询问当前时间、日期时,使用时间工具获取准确时间
- 当需要数学计算时,使用计算工具确保结果准确
- 当用户询问实时信息、最新新闻、不确定的事实时,使用搜索工具获取信息
- 基于工具返回的结果组织回答,注明信息来源
请按照以上规则回答用户问题。src/main/resources/prompts/no-think.txt
你是一个高效的AI助手,请严格遵循以下规则:
【核心要求】
- 直接给出结论,不展示推理过程
- 不编造事实,不确定时直接说明
【表达要求】
- 使用简洁、清晰的中文
- 控制输出长度,避免冗余解释
【行为约束】
- 不输出思考过程
- 不进行不必要的扩展分析
【工具使用】
- 需要当前时间时,使用时间工具
- 需要精确计算时,使用计算工具
- 需要实时信息时,使用搜索工具
请直接回答用户问题。5. 后端测试
5.1 ToolCallingTest
src/test/java/com/albertstack/aichat/ai/ToolCallingTest.java
package com.albertstack.aichat.ai;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import lombok.extern.slf4j.Slf4j;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@SpringBootTest
class ToolCallingTest {
@Autowired
private ChatClient chatClient;
@Test
void dateTimeTool_shouldReturnCurrentTime() {
String response = chatClient.prompt()
.user("现在几点了?")
.call()
.content();
log.info("时间查询回复: {}", response);
assertNotNull(response);
// 回复中应包含时间相关内容(数字、冒号等)
assertTrue(response.matches(".*\\d{2}.*"),
"回复应包含时间信息,实际: " + response);
log.info(" ✅ 时间工具调用成功");
}
@Test
void mathTool_shouldCalculateExpression() {
String response = chatClient.prompt()
.user("请帮我计算 (123 + 456) * 789 是多少?")
.call()
.content();
log.info("计算回复: {}", response);
assertNotNull(response);
// 123 + 456 = 579, 579 * 789 = 456831
assertTrue(response.contains("456831"),
"回复应包含正确计算结果 456831,实际: " + response);
log.info(" ✅ 数学工具调用成功");
}
@Test
void webSearchTool_shouldReturnSearchResults() {
String response = chatClient.prompt()
.user("请搜索一下 Spring Framework 是什么?")
.call()
.content();
log.info("搜索回复: {}", response);
assertNotNull(response);
assertFalse(response.isBlank(), "搜索回复不应为空");
log.info(" ✅ 联网搜索工具调用成功");
}
}6. 前端更新
工具调用完全在后端完成,对前端 透明无感,用户照常输入问题,AI 在需要时自动调用工具,前端只看到最终的流式回复。
不需要修改 ChatView 或任何前端代码。这正是 Spring AI 工具调用的优势:后端增加能力,前端无需改动。
7. 功能测试
- 启动后端,打开
http://localhost:5173,登录并创建话题 - 时间查询:输入 "现在几点了?",AI 应该返回准确的当前时间(而不是说"我无法获取时间")
- 数学计算:输入 "请帮我算一下 2 的 32 次方",AI 应该返回 4,294,967,296(而不是自己估算一个近似值)
- 联网搜索:输入 "帮我查询一下这最近这一周最火的 Github 项目有哪些?",AI 应该返回基于搜索结果的回答
- 组合使用:输入 "现在纽约时间几点?搜索一下纽约今天有什么大事件?",观察 AI 是否同时调用时间工具和搜索工具
- 普通对话:输入 "你好",AI 不应该调用任何工具(模型自己判断不需要)
8. 扩展方向
工具调用机制是可扩展的,你可以根据业务需求添加更多工具:
| 工具 | 用途 | 实现方式 |
|---|---|---|
| 天气查询 | 获取指定城市实时天气 | 调用天气 API(如和风天气) |
| 数据库查询 | 查询业务数据 | 注入 Repository,执行查询 |
| 邮件发送 | 让 AI 帮你发邮件 | 调用 JavaMail API |
| 代码执行 | 运行用户提供的代码片段 | 沙箱执行(需注意安全) |
| 文件操作 | 读写文件内容 | 操作本地或 OSS 文件 |
添加新工具只需三步:
- 创建
@Component类,方法加@Tool注解 - 在
AiConfig的.defaultTools()中注册 - (可选)更新系统提示词,引导模型使用
9. 小结
至此 AI 对话系统已接入工具调用能力:
| 层 | 新增内容 |
|---|---|
| 工具定义 | DateTimeTool(时间查询)、MathTool(数学计算)、WebSearchTool(联网搜索) |
| 配置 | AiConfig 注册 defaultTools,两个 ChatClient 均可使用工具 |
| 系统提示词 | default.txt 和 no-think.txt 新增【工具使用】指引 |
| 测试 | ToolCallingTest(3 个用例验证三种工具) |
| 前端 | 无需改动(工具调用对前端透明) |
新增的项目文件:
| 文件 | 用途 |
|---|---|
tool/DateTimeTool.java |
时间查询工具 |
tool/MathTool.java |
数学计算工具 |
tool/WebSearchTool.java |
联网搜索工具 |
ai/ToolCallingTest.java |
工具调用测试 |
最后一章是专栏总结,我们将回顾整个项目的架构演进,梳理技术要点,并探讨后续扩展方向。