背景
以往Java后端调用AI的方式基本上是通过定义prompt,然后通过chat的方式给大模型,然后大模型基于提示词做处理返回,但是如果涉及复杂的处理流程,且需要结合业务数据时,单靠提示词恐怕难以胜任,需要类似工作流的方式进行处理,在SpringAI 1.0版本之前使用Function Calling处理,1.0.0.M6版本中,官方废弃了Function Calling,统一使用Tool Calling,其实二者在底层原理上是相同的,后面统一学习一下。今天先学习一下Tool的实现。
引入依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
简单开发
按照官方文档,只需要简单定义一个类,方法上使用@Tool注解即可实现。(默认已经配置了ai相关)。
官方文档:https://www.spring-doc.cn/spring-ai/1.1.0/api_tools.html
java
public class DateTimeTools {
/**
* 获取当前时间tool
* @return
*/
@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
/**
* 设置闹钟tool
* @param time
*/
@Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
void setAlarm(String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("Alarm set for " + alarmTime);
}
}
调用
java
@GetMapping("/getDateTime")
public String getDateTime(@RequestParam String prompt){
String response = ChatClient.create(deepSeekAiChatModel)
.prompt(prompt)
.tools(new DateTimeTools())
.call()
.content();
return response;
}
在浏览器调用
http://localhost:8080/ai-service/api/v1/demo/getDateTime?prompt=我是1992年出生的,现在应该多少岁,明年是哪一年?
后的结果:
bash
您出生于1992年,现在是2025年12月23日,您的年龄是33岁(如果生日已过)或32岁(如果生日未到)。明年将是2026年。
可以看到,大模型是可以基于Tool获取到的内容,然后对内容进行封装回复的。
进阶用法
文档中有如下说明(中文版有点别扭):
1.当我们想让模型使用某个工具时,会在聊天请求中包含其定义(提示)并调用聊天模型API 向 AI 模型发送请求。
2.当模型决定调用工具时,会发送响应(聊天回应工具名称和输入参数均依据定义的模式建模。
3.这聊天模型将工具调用请求发送给ToolCallingManager应用程序接口。
4.这ToolCallingManager负责识别调用的工具,并以提供的输入参数执行。
5.工具调用的结果返回给ToolCallingManager.
6.这ToolCallingManager返回工具执行结果聊天模型.
7.这聊天模型将工具执行结果返回给 AI 模型(工具响应信息).
8.AI模型利用工具调用结果作为额外上下文生成最终响应,并将其发送给呼叫者(聊天回应)通过ChatClient.
基于这个内容,我觉得可以在调用的时候,在提示词中定义要调用的tool信息,来实现整个工具流程的调用。
实践样例:
现在需求是要根据用户输入信息,做出json字符串的提取,需要判断用户输入的不能是时间段,并与页面固有json字符串做整合,形成一个最终完整的json信息。
工具定义:
1.find_datetime_from_text :提取时间信息
2.extract_query_from_text:进行字段抽取(使用ai提取)
3.merge_query_with_base_json:将抽取结果和baseJson合并(使用ai合并)
代码实现
java
@Tool(name = "find_datetime_from_text",description = "根据用户自然语言描述,提取时间信息")
public String findDateTimeFromText(String userText) {
if (userText == null || userText.trim().isEmpty()) {
return "可以正常回复";
}
if (DATE_RANGE_PATTERN.matcher(userText).find()) {
return "我无法回答您的问题,可尝试手工分析";
}
return "可以正常回复";
}
/**
* 基于用户自然语言描述,并抽取结构化查询条件。
*/
@Tool(name = "extract_query_from_text",
description = "根据用户自然语言描述,抽取查询字段(产业、工厂、时间、KPI、图表设置等)。")
public String extractQuery(String userText) {
//提取产业或者工厂或者子产业
IndustryFactoryDTO industryAndFactoryList = getIndustryAndFactoryList();
List<String> factories = industryAndFactoryList.getFactories();
List<String> industries = industryAndFactoryList.getIndustries();
List<String> subIndustries = industryAndFactoryList.getSubIndustries();
List<String> matchedSubIndustries = subIndustries.stream()
.filter(userText::contains)
.collect(Collectors.toList());
List<String> matchedFactories = factories.stream()
.filter(userText::contains)
.collect(Collectors.toList());
List<String> matchedIndustries = industries.stream()
.filter(userText::contains)
.collect(Collectors.toList());
// 1) 给数据提取的ai定义提示词 buildAnalysisQueryPromptForTool(userText)
String prompt = buildAnalysisQueryPromptForTool(userText,matchedIndustries,matchedFactories,matchedSubIndustries);
// 2) 调用大模型做"字段抽取"
String aiResp = deepSeekAiChatModel.call(prompt);
log.info("AI提取用户信息json返回内容:{}",aiResp);
if (aiResp.contains("无法回答")) {
return "我无法回答您的问题,可尝试手工分析";
}
// 3) 解析 AI 返回的 JSON
// 让 AI 只返回 JSON(不带 ```),然后用 Jackson 直接转。
String json = JsonUtil.extractPureJson(aiResp);
return json;
}
@Tool(name = "merge_query_with_base_json",
description = "将抽取出的 query 与前端提供的 baseJson 合并,字段冲突则覆盖,相同列表进行聚合去重。")
public String mergeQueryWithBaseJson(String query, JSONObject baseJsonStr) {
//Java对象转json
String dealUserMessage = JacksonUtil.toJson(query);
String str = buildAnalysisQueryPrompt(dealUserMessage, baseJsonStr);
String aiResp = deepSeekAiChatModel.call(str);
log.info("AI合并json返回内容:{}",aiResp);
String json = JsonUtil.extractPureJson(aiResp);
return json;
}
调用
java
public String chatNew(ChatVO vo) {
String userPrompt = vo.getPrompt();
JSONObject baseJson = vo.getData();
String orchestratorPrompt = AiUtil.buildTopOrchestrationPrompt(userPrompt, baseJson);
String response = ChatClient.create(deepSeekAiChatModel)
.prompt(orchestratorPrompt)
.tools(new ChatAndBuildJsonTools(deepSeekAiChatModel))
.call()
.content();
return JsonUtil.extractPureJson(response).trim();
}
AiUtil.buildTopOrchestrationPrompt
java
public static String buildTopOrchestrationPrompt(String userText, JSONObject baseJson) {
return """
你是一个后端编排助手,你可以使用如下工具:
1)find_datetime_from_text:进行时间条件过滤抽取;
2)extract_query_from_text:进行字段抽取;
3)merge_query_with_base_json:将抽取结果和baseJson合并.
说明:
- 抽取流程:
(1) 调用 find_datetime_from_text 得到 一个结果字符串,如果返回结果是"我无法回答您的问题,可尝试手工分析",就停止后面所有操作,直接返回{"error","我无法回答您的问题,可尝试手工分析"}
(2) 若 find_datetime_from_text 返回"可以正常回复",执行extract_query_from_text;
(3) extract_query_from_text执行数据抽取时:
- 然后将最终extract_query_from_text的返回结果与和baseJson 传入 merge_query_with_base_json。
(4) 最终只输出 merge_query_with_base_json 的返回结果 JSON。
输出要求:
- 最终响应必须为合法 JSON 字符串,不要输出任何解释文字或 ```包裹。
userText:
%s
baseJson:
%s
""".formatted(userText, baseJson.toJSONString());
}
以上只是个人开发简单记录,仅代表个人观点,具体实现逻辑后面学习一下Spring AI源码再整理.