
spring-ai
https://gitee.com/lzh1995/spring-ai-demo
依赖
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 https://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.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>ai-demo3</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo3</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
对接ollama
第一个对话接口
application.properties
properties
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.model=deepseek-r1:1.5b
配置
java
package com.example.ai.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CommonConfiguration {
@Bean
public ChatClient chatClient(OllamaChatModel model) {
return ChatClient
.builder(model)
.defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小林同学,请以小林同学的身份和语气回答问题。")
.build();
}
}
web
java
package com.example.ai.web;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/ai")
public class ChatController {
@Autowired
private ChatClient chatClient;
/**
* 同步返回
* @param prompt
* @return
*/
@RequestMapping(value = "/chat")
public String chat(@RequestParam("prompt") String prompt) {
return chatClient.prompt().user(prompt).call().content();
}
/**
* 流式返回
* @param prompt
* @return
*/
@RequestMapping(value = "/chat2", produces = "text/html;charset=utf-8")
public Flux<String> chat2(@RequestParam("prompt") String prompt) {
return chatClient.prompt().user(prompt).stream().content();
}
}
分别访问:http://localhost:8080/ai/chat?prompt=你是谁
http://localhost:8080/ai/chat2?prompt=你是谁
会话日志
如果我们想打印请求ai的入参和出参,可以增加一个ai自带的拦截器即可。注意需要将日志设置成debug模式
properties
logging.level.org.springframework.ai=debug
java
@Configuration
public class CommonConfiguration {
@Bean
public ChatClient chatClient(OllamaChatModel model) {
return ChatClient
.builder(model)
.defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小林同学,请以小林同学的身份和语气回答问题。")
// 新增自带的拦截器
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
}
会话记忆
其实上就是每次问ai的时候,将之前对话都给ai传过去,ai就知道上下文了。
会话记忆肯定是根据会话来的,如果你新建一个会话,就不会知道你上一个会话说的什么了。
那么如何存储会话呢?spring-ai框架自带的有会话内存
存储,当然你也可以存在mysql,redis中,只需要继承实现该ChatMemory
类就行。
大致实现的原理就是,我们需要指定一个Advisors,表明我们要用内存会话管理,然后每次会话的时候,也需要指定一个Advisors,这个Advisors的作用是 用来告诉ai当前对话是哪个会话id,框架就会在每次请求的时候根据会话id获取之前的会话信息组装之后请求ai了。
角色 | 描述 | 示例 |
---|---|---|
system | 优先于user指令之前的指令,也就是给大模型设定角色和任务背景的系统指令 | 你是一个乐于助人的编程助手,你的名字叫小团团,请以小团团的风格来回答用户的问题。 |
user | 终端用户输入的指令(类似于你在ChatGPT聊天框输入的内容) | 你好,你是谁? |
assistant | 由大模型生成的消息,可能是上一轮对话生成的结果 | 注意,用户可能与模型产生多轮对话,每轮对话模型都会生成不同结果。 |
配置文件
java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CommonConfiguration {
//指定聊天记忆 - 基于内存的方式
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
@Bean
public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {
return ChatClient
.builder(model)
.defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小林同学,请以小林同学的身份和语气回答问题。")
.defaultAdvisors(
new SimpleLoggerAdvisor(),
new MessageChatMemoryAdvisor(chatMemory)
)
.build();
}
}
接口
java
@RequestMapping(value = "/chat2", produces = "text/html;charset=utf-8")
public Flux<String> chat2(@RequestParam("prompt") String prompt,@RequestParam("chatId") String chatId) {
return chatClient.prompt()
.user(prompt)
// CHAT_MEMORY_CONVERSATION_ID_KEY 是规定死的key,代表 【聊天_记忆_对话key】, 然后去找对应的chatId
.advisors(advisorSpec -> advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
.stream().content();
}
会话历史
会话历史其实就是将用户的chatId保存一下,这个很简单。再一个就是根据chatId去查询该会话的内容,这个我们之前是保存到了内存中,所以我们 找的话,也是需要去内存中寻找的。我们之前指定了ChatMemory
,这个类正好有提供List<Message> get(String conversationId, int lastN);
方法 ,很简单,传递chatId和要查询的条数就行。需要注意的是,我们需要将它返回的格式转换成我们需要的格式。
主要接口
java
import com.example.ai.entity.vo.MessageVO;
import com.example.ai.service.ChatHistoryService;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {
@Autowired
private ChatHistoryService chatHistoryService;
@Autowired
private ChatMemory chatMemory;
@GetMapping("/{type}")
public List<String> getChatIds(@PathVariable("type") String type) {
return chatHistoryService.getChatIds(type);
}
@GetMapping("/{type}/{chatId}")
public List<MessageVO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {
List<Message> messages = chatMemory.get(chatId, Integer.MAX_VALUE);
if(messages == null) {
return List.of();
}
return messages.stream().map(MessageVO::new).toList();
}
}
返回结果类
java
import lombok.Data;
import org.springframework.ai.chat.messages.Message;
@Data
public class MessageVO {
private String role;
private String content;
public MessageVO(Message message) {
switch (message.getMessageType()) {
case USER:
role = "user";
break;
case ASSISTANT:
role = "assistant";
break;
default:
role = "";
break;
}
this.content = message.getText();
}
}
对接openAI
openAI是一个标准,通义千问就实现了这个标准,所以我们来对接一下阿里的通义千问。
注意 base_url:https://dashscope.aliyuncs.com/compatible-mode
第一个对话接口
依赖
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
application.properties
properties
## open-ai
spring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode
spring.ai.openai.api-key=sk-xxxxxxxxxxxxxxxxxxxx
spring.ai.openai.chat.options.model=qwen-plus
CommonConfiguration.java
java
package com.example.ai.config;
import com.example.ai.constants.SystemConstants;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CommonConfiguration {
//指定聊天记忆 - 基于内存
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
@Bean
public ChatClient gameChatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient
.builder(model)
.defaultSystem(SystemConstants.GAME_SYSTEM_PROMPT)
.defaultAdvisors(
new SimpleLoggerAdvisor(),
new MessageChatMemoryAdvisor(chatMemory)
)
.build();
}
}
接口
java
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class GameController {
private final ChatClient gameChatClient;
@RequestMapping(value = "/game", produces = "text/html;charset=utf-8")
public Flux<String> chat(String prompt, String chatId) {
return gameChatClient.prompt()
.user(prompt)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
.stream()
.content();
}
}
访问接口:http://localhost:8080/ai/game?prompt=开始游戏&chatId=123
function call
就是定义一个java接口,但是要有注释说明,然后要配置一下,告诉ai模型,ai就会根据用户的意思去调用对应的接口。 很简单,只是写一个tool,然后配置一下就OK
java
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import com.example.ai.entity.po.Course;
import com.example.ai.entity.po.CourseReservation;
import com.example.ai.entity.po.School;
import com.example.ai.entity.query.CourseQuery;
import com.example.ai.service.CourseReservationService;
import com.example.ai.service.CourseService;
import com.example.ai.service.SchoolService;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
@RequiredArgsConstructor
@Component
public class CourseTools {
private final CourseService courseService;
private final SchoolService schoolService;
private final CourseReservationService reservationService;
@Tool(description = "根据条件查询课程")
public List<Course> queryCourse(@ToolParam(description = "查询的条件", required = false) CourseQuery query) {
if (query == null) {
return courseService.list();
}
QueryChainWrapper<Course> wrapper = courseService.query()
.eq(query.getType() != null, "type", query.getType()) // type = '编程'
.le(query.getEdu() != null, "edu", query.getEdu());// edu <= 2
if (query.getSorts() != null && !query.getSorts().isEmpty()) {
for (CourseQuery.Sort sort : query.getSorts()) {
wrapper.orderBy(true, sort.getAsc(), sort.getField());
}
}
return wrapper.list();
}
@Tool(description = "查询所有校区")
public List<School> querySchool() {
return schoolService.list();
}
@Tool(description = "生成预约单,返回预约单号")
public Integer createCourseReservation(
@ToolParam(description = "预约课程") String course,
@ToolParam(description = "预约校区") String school,
@ToolParam(description = "学生姓名") String studentName,
@ToolParam(description = "联系电话") String contactInfo,
@ToolParam(description = "备注", required = false) String remark) {
CourseReservation reservation = new CourseReservation();
reservation.setCourse(course);
reservation.setSchool(school);
reservation.setStudentName(studentName);
reservation.setContactInfo(contactInfo);
reservation.setRemark(remark);
reservationService.save(reservation);
return reservation.getId();
}
}
配置
java
@Bean
public ChatClient serviceChatClient(OpenAiChatModel model, ChatMemory chatMemory, CourseTools courseTools) {
return ChatClient
.builder(model)
.defaultSystem(SystemConstants.SERVICE_SYSTEM_PROMPT)
.defaultAdvisors(
new SimpleLoggerAdvisor(),
new MessageChatMemoryAdvisor(chatMemory)
)
//重点配置
.defaultTools(courseTools)
.build();
}