引言
好的,今天我们继续聊一下Spring AI的相关内容。在10月的时候,我使用Spring AI搭建了一个简易版的个人助理系统,整体来说效果还是非常不错的。通过这次尝试,我对业务系统与AI结合的探索有了更为明确的理解和实践。虽然目前功能上还相对简单,整体系统也缺乏较多可操作的交互方式,特别是在数据库操作方面,功能较为基础,目前主要实现了一个简单的查询功能。
但就在10月末,Spring AI迎来了一个重要的更新,更新后不仅增强了函数调用的能力,还引入了全局参数的概念。这两个新特性为系统的扩展性和可玩性带来了极大的提升,开启了更多可能性。
图片
那么,今天我们就利用这个全局参数的特性,来实现一个数据库插件。具体来说,我们将实现一个完整的增删改查(CRUD)操作。对于目前的智能体系统来说,数据库操作已经是一个至关重要的功能,尤其是在业务系统中,智能体能够与数据库进行交互,不仅提升了系统的灵活性和智能化程度,也大大增强了业务处理的效率。因此,我们今天的目标就是通过Spring AI的强大功能,实现一套基础的数据库操作框架,完成增、删、改、查四个功能模块。
需要特别注意的是,这里我们仅仅是通过一个简单的使用案例来进行分析和讲解,当然,这并不代表只能局限于此,实际上对于大部分业务场景来说,这样的数据库操作已经足够满足需求,并且可以根据具体的业务需求进一步扩展和优化功能。
如果有小伙伴还不太清楚如何使用Spring AI搭建自己的智能体系统,或者对于Spring AI的基本功能还不太了解,欢迎查看我们之前分享的相关文章,了解更多相关内容:https://www.cnblogs.com/guoxiaoyu/p/18453559
个人助理大优化
首先,让我们来看一下我们计划实现的个人助理功能。目前,我们已经实现了旅游攻略查询和天气查询功能。今天,我们将在此基础上新增一个“个人待办”功能。由于数据库模块较为庞大且复杂,针对这个部分我们将单独进行详细讲解。以下是该功能的大致实现效果及相关流程示意图:
图片
效果演示
首先,让我们来看看经过半天调整后的效果演示。经过一段时间的优化和调试,最终呈现的效果基本符合我的预期。
这里只演示了下待办的增删改查,并没有演示天气查询和旅游攻略,可以看上一章节的演示。
图片
开始优化
提示词
当然,我们的优化工作从入口部分开始,首先,提示词的设计是不可或缺的。回顾上一章节,由于当时功能较少,我们并没有对提示词做过多的修饰,因此整体的交互和模型的响应相对简单。然而,随着本次新增功能的增多,模型的回答可能会变得较为杂乱无序。
因此,为了确保模型的输出能够更精准、有序,我们在本次优化中提前准备并生成了详细的提示词。
String conversation_id = "123";
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
.withModel("hunyuan-pro").withTemperature(0.5)
.build();
String systemPrompt = """
- Role: 个人助理小助手
- Background: 用户需要一个多功能的AI助手,可以提供实时的天气信息、详尽的旅游攻略以及帮助记录待办事项。
- Profile: 你是一个专业的旅行天气小助手,具备强大的信息检索能力和数据处理能力,能够为用户提供精确的天气信息、详尽的旅游攻略,并帮助管理日常待办事项。
- Skills: 你拥有强大的网络搜索能力、数据处理能力以及用户交互能力,能够快速准确地为用户提供所需信息。
- Goals: 提供准确的天气信息,制定包含航班、酒店、火车信息的详尽旅游攻略,并帮助用户记录和管理待办事项。
- Constrains: 提供的信息必须准确无误,旅游攻略应详尽实用,待办事项管理应简洁高效。
- OutputFormat: 友好的对话式回复,包含必要的详细信息和格式化的数据。
- Workflow:
1. 接收用户的天气查询请求,并提供准确的天气信息。
2. 根据用户的旅游目的地,搜索并提供包括航班、酒店、火车在内的旅游攻略。
3. 接收用户的待办事项,并提供简洁的记录和提醒服务。
""";
ChatMemory chatMemory1 = messageChatMemoryAdvisor.getChatMemory();
String content = this.myChatClientWithSystem
.prompt()
.system(systemPrompt)
.user(userInput)
.options(openAiChatOptions)
.advisors(messageChatMemoryAdvisor,myLoggerAdvisor,promptChatKnowledageAdvisor,promptChatDateAdvisor)
.advisors(advisor -> advisor.param("chat_memory_conversation_id", conversation_id)
.param("chat_memory_response_size", 100))
.functions("CurrentWeather","TravelPlanning","toDoListFunctionWithContext")
.toolContext(Map.of("sessionId", conversation_id, "userMemory", chatMemory1,"client",chatClient))
.call()
.content();
log.info("content: {}", content);
ChatDataPO chatDataPO = ChatDataPO.builder().code("text").data(ChildData.builder().text(content).build()).build();
return chatDataPO;
好的,刚才我们新增了一些参数,我现在来详细解释一下每个参数的作用和使用场景:
- advisor.param:这是用于单独修改我们默认增强器(增强型顾问)的参数。它与上面提到的 Advisor 类相关联,允许用户在不修改核心代码的情况下,自定义和调整增强器的行为和配置。
- toolContent:这个参数是我们在新增函数回调功能时引入的全局参数,主要用于处理回调时的各种工具内容。在函数调用中,toolContent 可以传递不同的工具数据,确保回调过程的正确执行。
- sessionId:这个参数用于标识每个独立的会话,它帮助我们控制每个用户的会话状态。通过给每个会话分配一个唯一的 sessionId,我们可以确保每个函数调用是针对特定用户的,而非共享的全局数据。例如,我们的待办事项是个人化的,只有对应用户可以看到和操作自己的待办列表。这里为了演示的方便,我们将 sessionId 设置为固定值,并未接入登录接口,实际应用中应根据用户身份动态生成。
- userMemory:此参数用于传递用户的历史上下文,使得回调函数能够使用到之前的对话或操作记录。例如,在待办事项管理中,我们可能需要根据历史数据判断某个任务是否已经完成。
- chatClient:该参数将待办函数与一个大模型连接,借助大模型生成SQL查询或其他复杂操作。chatClient 负责与大模型进行交互,生成所需的SQL,而外层的思考模型则专注于调用接口并处理业务逻辑。
好的,再次提醒一下,如果有些小伙伴之前没有接触过智能体的相关内容,建议你们先去浏览一下我之前的第一篇文章,了解一下基础知识,补充相关的背景信息,这样对接下来的内容会更加容易理解。
那么,接下来我们继续探讨与待办事项相关的内容。
数据表
个人待办事项的目的非常明确,主要是针对待办事项表进行增、删、改、查等基本操作。为了保证系统的高效性与简洁性,我们在设计数据库时,所需的数据字段也非常简洁直观。这是我们的建表语句:
create table todo_info(
id int(11) auto_increment primary key,
todo_info varchar(1000) not null,
todo_Date date not null,
done boolean not null default false
)
在生成完这个建表语句之后,一定要妥善保存,以便后续给大模型提供参考。
函数回调
首先,我们需要明确待办函数的回调必须能够支持四种基本操作:增删改查,。此外,回调函数还需要具备生成SQL语句的能力,并能执行这些SQL语句,以便与数据库进行交互。只有在这些操作顺利完成之后,我们才能将最终的结果数据返回给外层的思考模型,以供进一步的处理和分析。
基于这些要求,我们现在可以开始具体操作,逐步实现所需功能。
图片
好的,我们一步一步来。
待办函数
根据上述信息,我们需要设计一个函数,该函数需要包含一个入参和一个回参。入参的主要作用是传递一个标识符,用于唯一标识某一操作,而回参则是一个字符串类型的返回值,用于向调用方反馈相应的结果或状态信息。
public class ToDoListInfoService implements BiFunction<ToDoListInfoService.ToDoRequest, ToolContext, ToDoListInfoService.ToDoResponse> {
private JdbcTemplate jdbcTemplate;
public ToDoListInfoService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@JsonClassDescription("crud:c 代表增加;r:代表查询,u:代表更新,d:代表删除")
public record ToDoRequest(String crud) {}
public record ToDoResponse(String message) {}
@Override
public ToDoResponse apply(ToDoListInfoService.ToDoRequest request, ToolContext toolContext) {}
}
可以观察到,在这里我们使用的是 BiFunction 接口,而不是之前使用的 Function 接口,因为我们需要使用ToolContext。
❝
需要特别注意的是,尽管在这里我们使用了 JsonClassDescription,但它的主要目的是为了提高代码的可读性和可维护性,方便开发人员理解和查看结构。实际上,大模型并不会依赖于 JsonClassDescription 来判断或解析传递的具体参数。
图片
Bean装配
由于我们在这里使用了 JdbcTemplate 进行数据库操作,这就需要在项目中引入相应的 Maven 依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
我这里仍然选择使用 Bean 装配的方式,主要是因为它能够提供更高的灵活性与可维护性。此外,维护好入参的定义和配置,也能够在后续的开发中为大模型提供有效的参考和支持。
@Bean
public FunctionCallback toDoListFunctionWithContext(JdbcTemplate jdbcTemplate) {
return FunctionCallbackWrapper.builder(new ToDoListInfoService(jdbcTemplate))
.withName("toDoListFunctionWithContext") // (1) function name
.withDescription("添加待办,crud:c 代表增加;r:代表查询,u:代表更新,d:代表删除") // (2) function description
.build();
}
接下来,我们将着手实现函数内部的具体方法。
上下文信息
默认提供的 MessageChatMemoryAdvisor 类并不支持直接获取历史聊天记录。因此,我们需要自定义一个类来实现这一功能。为了方便使用,下面的代码将集成该功能并暴露一个接口供调用。
@Slf4j
public class MyMessageChatMemoryAdvisor extends MessageChatMemoryAdvisor {
private ChatMemory chatMemory;
public MyMessageChatMemoryAdvisor(ChatMemory chatMemory) {
super(chatMemory);
this.chatMemory = chatMemory;
}
public MyMessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int chatHistoryWindowSize) {
super(chatMemory, defaultConversationId, chatHistoryWindowSize);
this.chatMemory = chatMemory;
}
public MyMessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int chatHistoryWindowSize, int order) {
super(chatMemory, defaultConversationId, chatHistoryWindowSize, order);
this.chatMemory = chatMemory;
}
public ChatMemory getChatMemory() {
return chatMemory;
}
}
为了能够顺利访问历史聊天记录,我们专门编写了一个方法,该方法会将历史上下文对象返回。通过这种方式,我们可以在后续的操作中方便地获取到完整的聊天信息,从而实现对历史对话内容的正常访问和处理。
数据库操作
接下来,我们将进行数据库操作,但在此过程中,必须依赖大模型的帮助来生成SQL语句。原因在于,外层的大模型具备强大的能力,可以准确地分析并理解需求,从而判断出具体的操作类型是增、删、改还是查。接下来,我们将详细介绍如何实现这一过程。
public ToDoResponse apply(ToDoListInfoService.ToDoRequest request, ToolContext toolContext) {
String tableinfo = """
- Role: SQL语句生成专家
- Background: 用户需要根据特定的表结构和参数信息生成精准的MySQL查询或修改语句,以实现数据库操作的自动化和效率化。
- Profile: 你是一位经验丰富的数据库管理员和SQL专家,精通MySQL数据库的各种查询和修改语句,能够根据用户提供的表结构和参数信息快速生成正确的SQL语句。
- Skills: 你具备深厚的数据库理论知识和丰富的实践经验,能够理解复杂的表结构,准确把握用户需求,并据此生成高效、准确的SQL语句。
- Goals: 根据用户提供的表结构和参数信息,生成可以直接执行的MySQL查询或修改语句,确保语句的正确性和执行的成功率。
- Constrains: 生成的SQL语句必须符合MySQL的语法规则,能够直接在MySQL数据库中执行,且不包含任何额外的信息或提示。
- OutputFormat: 纯SQL文本语句,格式规范,无多余信息。禁止使用markdown格式。
- Workflow:
1. 分析用户提供的表结构信息和参数信息。
2. 根据分析结果,确定需要执行的数据库操作类型(查询、插入、更新或删除)。
3. 结合操作类型和用户提供的信息,生成符合MySQL语法的SQL语句。
-example:
q.帮我创建一个待办:明天9点提醒我读书。今天日期是2024-11-07 20:20:11
a:INSERT INTO todo_info (todo_info, todo_date) VALUES ('明天8点读书', '2024-11-08 08:00:00');
- tableinfo:
create table todo_info(
id int(11) auto_increment primary key,
todo_info varchar(1000) not null,
todo_Date date not null,
done boolean not null default false
)
""";
String crud = request.crud;
ChatMemory chatMemory = (ChatMemory)toolContext.getContext().get("userMemory");
String conversation_id = (String)toolContext.getContext().get("sessionId");
ChatClient client = (ChatClient)toolContext.getContext().get("client");
List<Message> messages = chatMemory.get(conversation_id, 1);
var userqa = messages.get(0).getContent();
String jsonString = "执行成功";
String content = client.prompt()
.system(tableinfo)
.user("请根据当前问题生成相应SQL文本即可,禁止生成SQL以外的内容:"+userqa+",今天日期是:"+ DateUtil.now())
.call().content();
try {
if (crud.equals("r")) {
jsonString = "查询到待办内容如下:" + JSONObject.toJSONString(jdbcTemplate.queryForList(content));
} else {
jdbcTemplate.execute(content);
}
}catch (Exception e){
log.info("ToDoListInfoService:{}", e.getMessage());
jsonString = "执行失败了";
}
log.info("ToDoListInfoService:{}", content);
return new ToDoResponse(jsonString);
这段代码的实现完全依赖于通过全局参数传递过来的信息,以便更好地处理历史上下文的问题。传统的回调函数方法在处理多轮对话的历史上下文时存在很大的局限性,无法有效地追踪会话中的上下文,因此难以解决这类问题。在这种情况下,我们通过全局参数传递的方式,能够跨越多次交互,确保在每个步骤中都能访问到最新的上下文信息。
让我们简单解释一下这段代码的流程:
- 获取当前会话中的用户提问:我们从当前会话中获取最近一次用户提出的问题,确保我们不会误取到其他会话的上下文。
- 将问题提交给大模型生成SQL:我们将用户的提问传递给大模型,利用其能力帮助我们生成适当的SQL查询语句。
- 判断是否为查询请求:我们检查生成的SQL语句是否属于查询操作。如果是查询,则执行查询并将查询结果返回给用户。
- 其他操作的处理:如果不是查询请求,则说明用户发出的指令可能是更新或执行类的操作,在这种情况下,我们返回一个"执行成功"的响应。
可以看到,提示词中的 tableinfo 是我硬编码写死的。实际上,我们完全可以将其设计成一个可传入的参数,这样不仅提升了插件的灵活性和可复用性,而且使得该插件不再仅仅局限于待办事项的使用场景,而能够作为一个通用的数据库操作插件,适应不同的需求和应用场景。
为了能够在演示过程中展示效果,目前我将某些部分做了临时的硬编码处理。这只是为了给大家提供一个初步的思路和参考框架,后续我会逐步完善这些功能。
接下来,让我们一起来看看调试过程中得到的效果。以下是我在调试时的截图:
图片
接下来,我们将检查数据库是否已经成功存储并正常更新了数据。
图片
接下来,我将展示查询的实际效果,同时生成的 SQL 语句也相当优秀,能够高效地满足查询需求。
图片
可以看到,此处已成功将数据或结果正常返回给前端,系统运行状态良好。
图片
总结
在本文中,我们深入探讨了如何利用 Spring AI 的新功能,特别是全局参数和增强函数调用能力,来构建一个智能化的个人助理系统。通过这个系统,我们实现了基础的增、删、改、查(CRUD)功能,特别聚焦在数据库交互与待办事项管理上。我们展示了如何将 Spring AI 集成到实际业务流程中,通过模型生成 SQL 查询语句,提升数据库操作的自动化程度和灵活性。
首先,我们介绍了 Spring AI 在功能更新后如何简化和扩展业务逻辑处理,特别是在处理多轮对话、用户历史数据以及复杂数据库操作时的优势。通过全局参数,系统能够更精准地捕捉用户需求,并在数据库层面执行相关操作,真正实现了智能化的交互和自动化的业务流程。
我们还设计了一个待办事项管理功能,其中通过精心设计的提示词和模型优化,使得待办功能的增删改查操作更加高效与准确。
总结来说,这次基于 Spring AI 的系统优化,不仅为我们提供了一个强大的智能助手框架,也为实际业务中的智能化系统提供了可借鉴的方案。未来,我们可以基于此进一步扩展功能,打造更加智能化和个性化的业务解决方案。