前情提要:在《第二学》中我们已经知道怎么联合记忆方式,整合上下文结合redis实现我们大模型上下文联合对话,不知道到的同学可以查看我发布上一篇文章整合redis大模型全过程.
本篇文章我将介绍,怎么结合Tool结合大模型的方式实现大模型自主调用工具
基于上篇文章我们已经实现对话和上下文,但是当我们提问当前日期的时候,发现大模型回复就会胡言乱语了

这是因为发布大模型的数据训练往往不是当前日期,都是基于上个时期,训练好的数据再发布,所以在数据方面往往都具有滞后性,所以出现工具的方式,让我们大模型自主思考怎么去解决这个问题。
这个时候我可以定义一个工具类用来访问
public class DateTimeTools {
// 获取当前日期时间
@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
}
修改ChatClient的访问地址用来修改问题
温馨提示 建议跟着上一篇文章做完,这里可以实现无缝连接
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
MessageChatMemoryAdvisor messageHistoryAdvisor) {
return builder
// 设定系统角色
.defaultSystem(Constant.SYSTEM_ROLE)
// 设定上下文记忆管理
.defaultAdvisors(messageHistoryAdvisor)
// 设定工具回调
.defaultTools(new DateTimeTools())
.build();
}
这个是时候我们直接查看效果

发现工具调用成功,这个时候已经是最新的时间

也就是说所有的动作,已经在SpringAI底层集成了
现在我们细致的了解下官方提供哪些工具方式给我们使用,通过官网观察我知道一共有两种方式

1.声明式地使用@Tool注解
2.以编程方式使用低级MethodToolCallback实现
目前我们倾向使用@Tool的方式去实现我们的功能,因为更加简单和方便,我们直接打开注解Tool的源码查看提供哪些工具

注释@Tool允许您提供有关该工具的关键信息:
- name:工具的名称。如果未提供,则使用方法名称。AI 模型使用此名称来识别调用该工具的工具。因此,同一个类中不允许有两个同名的工具。对于特定的聊天请求,该名称在模型可用的所有工具中必须是唯一的。
- description:工具的描述,模型可以通过该描述了解何时以及如何调用该工具。如果未提供,则方法名称将用作工具描述。但是,强烈建议提供详细的描述,因为这对于模型理解工具的用途及其使用方法至关重要。如果描述不充分,可能会导致模型在应该使用工具时不使用它,或者错误地使用它。
- returnDirect:工具结果应直接返回给客户端还是传递回模型。有关更多详细信息,请参阅直接返回。
- resultConverter:ToolCallResultConverter用于将工具调用的结果转换为可String object发送回 AI 模型的实现。更多详情,请参阅结果转换。
现在我们重新设置新工具参数来使用这些功能,现在我们来实现一个闹钟的工具类,让他在10秒后想起
name,description 理解和解释
public class DateTimeTools {
@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
@Tool(name = "Alarm", description = "Set a user alarm for the given time")
void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("Alarm set for " + alarmTime);
}
}
注释@ToolParam允许您提供有关工具参数的关键信息:
1.description:参数的描述,模型可以通过它来更好地理解如何使用参数。例如,参数应采用什么格式、允许使用哪些值等等。
2.required:参数是必需的还是可选的。默认情况下,所有参数都被视为必需的。
同时我们可以重新修改chatClient ,现在用的是最新的
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
MessageChatMemoryAdvisor messageHistoryAdvisor) {
return builder
// 设定系统角色
.defaultSystem(Constant.SYSTEM_ROLE)
// 设定上下文记忆管理
.defaultAdvisors(messageHistoryAdvisor)
// 设定工具回调
.defaultTools(new DateTimeTools())
.build();
}
我们使用另外一种方式来调用我们的工具也就是在调用chatClient时候显式调用我们的工具类
/**
* 处理用户问题并返回流式响应内容
*
* @param question 用户输入的问题内容
* @return 包含逐条响应内容和结束标记的响应流,每个元素为字符串格式
*/
@Override
public Flux<String> chatStream(String question, String sessionId) {
// 1. 调用聊天客户端获取流式响应
Flux<String> responseStream = this.chatClient.prompt()
.user(question)
.tools(new DateTimeTools())
.advisors(advisor -> advisor.param(CONVERSATION_ID, sessionId))
.stream()
.content();
// 2. 添加日志和本地拼接
StringBuilder completeResponse = new StringBuilder();
return responseStream
// 打印每段输出
.doOnNext(chunk -> {
log.info("[Stream Chunk] " + chunk); // 控制台日志
completeResponse.append(chunk); // 拼接完整响应
})
// 结束时打印完整内容
.concatWith(Flux.defer(() -> {
log.info("[Complete Response] " + completeResponse.toString()); // 完整日志
return Flux.just("DONE"); // 结束标记
}))
// 错误处理
.onErrorResume(e -> {
log.error("[Stream Error] " + e.getMessage());
return Flux.just("[ERROR] " + e.getMessage());
});
}
删除调用springAIConfig中的工具调用

@Bean
public ChatClient chatClient(ChatClient.Builder builder,
MessageChatMemoryAdvisor messageHistoryAdvisor) {
return builder
// 设定系统角色
.defaultSystem(Constant.SYSTEM_ROLE)
// 设定上下文记忆管理
.defaultAdvisors(messageHistoryAdvisor)
.build();
}
为什么需要这样操作呢,这是官方提供了两种方式进行调用工具,
1.第一种defaultTool,相当于builder中直接赋值,会让我们在不同的地方都会存在这个工具类的调用,
2.第二种在tools调用,只会在使用chatClient的时候在使用具体工具类,这就是只会调用一次,不会注册到对应的bean实列中,在实际业务开发中,可以根据不同的情况使用
这个时候我们观察效果


我们发现时间元素,按照描述内容已经提取出来在控制台输出了

这里说明我们的已经设置闹钟定义成功了
前面我们已经知道Tool的Desc, name 的属性,接下来解释下第三个参数returnDirect
什么情况会使用这个呢
returnDirect参数解释
在某些情况下,您更愿意将结果直接返回给调用者,而不是将其发送回模型。例如,如果您构建了一个依赖于 RAG
工具的代理,您可能希望将结果直接返回给调用者,而不是将其发送回模型进行不必要的后处理。或者,您可能拥有某些工具,应该终止代理的推理循环。
每个ToolCallback实现都可以定义工具调用的结果应该直接返回给调用者还是发送回模型。默认情况下,结果会发送回模型。但您可以根据工具更改此行为。
负责ToolCallingManager管理工具执行生命周期的
负责处理returnDirect与工具关联的属性。如果该属性设置为true,则工具调用的结果将直接返回给调用者。否则,结果将发送回模型。
修改参数为true
public class DateTimeTools {
@Tool(description = "Get the current date and time in the user's timezone", returnDirect = true)
String getCurrentDateTime() {
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
@Tool(name = "Alarm", description = "Set a user alarm for the given time", returnDirect = true)
void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
System.out.println("Alarm set for " + alarmTime);
}
}

我们可以观察到结果直接输出给我们不会给到大模型,如果还不能理解是什么意思,下面我举个通俗易懂的列子
- returnDirect: false(默认) 你问我:“设个明天 8 点的闹钟” → 我调用闹钟工具 → 工具返回"Alarm set for 2023-07-24T08:00" → 我会再加工成人类语言,比如回复你:“好的,已为您设置明早8点的闹钟”。
- returnDirect: true 你问我:“设个明天 8 点的闹钟” → 我调用闹钟工具 → 工具直接返回原始结果"Alarm set for 2023-07-24T08:00"给你,我不会再多说话。
resultConverter参数
现在我们实现我们需要转换的类的,这个类是我们将返回的时间变成具体:年-月-日
package org.oppo.chat.ai.converter;
import org.springframework.ai.tool.execution.ToolCallResultConverter;
import java.lang.reflect.Type;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class DateTimeResultConverter implements ToolCallResultConverter {
@Override
public String convert(Object result, Type returnType) {
// 1. 类型安全检查
if (!(result instanceof String)) {
throw new IllegalArgumentException("Result must be String type");
}
// 2. 解析原始时间字符串
String rawDateTime = (String) result;
ZonedDateTime zdt = ZonedDateTime.parse(rawDateTime);
// 3. 定义输出格式(示例:中文 + 时区)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss (ZZZZ)");
// 4. 返回格式化结果
return "当前时间:" + zdt.format(formatter);
}
}
重新修改DateTools中的方法
@Tool(
description = "Get the current date and time in the user's timezone",
returnDirect = true,
resultConverter = DateTimeResultConverter.class
)
String getCurrentDateTime() {
// 原始结果:返回 ISO 格式的日期时间(如 "2023-07-24T12:34:56+08:00")
return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
}
现在我们来检查查看效果,看看是否可以达到我们的目标

我们发现输出已经按照我们的要求输出成我们的效果,现在我们以及总结了@Tool中所有的参数的功能,可以帮助我们在实际开发中快速的实现我们小型智能体,现在我们可以开始思考下,如果每个工具都需要我们自己本地去写,效率是不是太低了,如果python写的工具集,能不能在我们项目中使用呢,在下篇文章中我将介绍怎么集成mcp,将工具集集成到我们项目中使用。希望本篇文章对您学习有帮助
160

被折叠的 条评论
为什么被折叠?



