目录
本篇论文讨论SpringAI这一整合全球多数大模型、对大模型开发技术架构有良好封装支持的工具,介绍其在多种大模型应用开发模式中的使用方法与相关案例。关键要点包括:
1.**SpringAI入门**:创建SpringBoot工程,引入对应大模型的starter依赖,配置模型参数信息,声明ChatClient实现与大模型的同步或流式调用,还可设置System信息、添加日志功能、对接前端并实现会话记忆和会话历史功能。
2.**纯Prompt开发**:通过优化提示词让大模型生成理想内容的过程叫提示词工程,需掌握核心策略与减少“幻觉”的技巧,防范多种Prompt攻击手段。以“哄哄模拟器”为例,选择合适模型,引入依赖、配置参数和ChatClient,编写Controller实现功能。
3.**Function Calling**:适用于需求含逻辑校验或数据库操作的场景。以智能客服为例,先实现课程、校区、预约单的CRUD功能,定义Function,设定System提示词,配置带工具调用功能的ChatClient并编写Controller。
4.**RAG**:为解决大模型知识限制问题,利用向量模型将文本向量化,通过向量数据库存储和检索向量。以ChatPDF为例,展示了PDF上传下载、向量化的实现过程,包括文件读取、转换为Document格式并写入向量数据库 。
前言
SpringAI整合了全球(主要是国外)的大多数大模型,而且对于大模型开发的三种技术架构都有比较好的封装和支持,开发起来非常方便。 不同的模型能够接收的输入类型、输出类型不一定相同。SpringAI根据模型的输入和输出类型不同对模型进行了分类:
1.SpringAI入门
1.1.快速入门
1.1.1.创建工程
创建一个新的SpringBoot工程,勾选Web、MySQL驱动即可。原始pom.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.itheima</groupId>
<artifactId>ai-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ai-demo</name>
<description>ai-demo</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.1.2.引入依赖
SpringAI完全适配了SpringBoot的自动装配功能,而且给不同的大模型提供了不同的starter,比如:
模型/平台 | starter |
---|---|
Anthropic | <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-anthropic-spring-boot-starter</artifactId> </dependency> |
Azure OpenAI | <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId> </dependency> |
DeepSeek | <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> |
Hugging Face | <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-huggingface-spring-boot-starter</artifactId> </dependency> |
Ollama | <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> </dependency> |
OpenAI | <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> |
我们可以根据自己选择的平台来选择引入不同的依赖。这里我们先以Ollama为例。首先,在项目pom.xml中添加spring-ai的版本信息:
<spring-ai.version>1.0.0-M6</spring-ai.version>
然后,添加spring-ai的依赖管理项:
<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>
最后,引入spring-ai-ollama的依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
最终,完整依赖如下:
<?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.itheima</groupId>
<artifactId>ai-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ai-demo</name>
<description>ai-demo</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.1.3.配置模型信息
接下来,要在配置文件中配置模型的参数信息。以ollama为例,我们将application.properties
修改为application.yaml
,然后添加下面的内容:
spring:
application:
name: ai-demo
ai:
ollama:
base-url: http://localhost:11434 # ollama服务地址, 这就是默认值
chat:
model: deepseek-r1:7b # 模型名称
options:
temperature: 0.8 # 模型温度,影响模型生成结果的随机性,越小越稳定
1.1.4.ChatClient
ChatClient
中封装了与AI大模型对话的各种API,同时支持同步式或响应式交互。不过,在使用之前,首先我们需要声明一个ChatClient
。新建一个包下,在该包新建一个CommonConfiguration
类,完整代码如下:
package com.itheima.ai.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CommonConfiguration {
// 注意参数中的model就是使用的模型,这里用了Ollama,也可以选择OpenAIChatModel
@Bean
public ChatClient chatClient(OllamaChatModel model) {
return ChatClient.builder(model) // 创建ChatClient工厂
.build(); // 构建ChatClient实例
}
}
代码解读:
-
ChatClient.builder
:会得到一个ChatClient.Builder
工厂对象,利用它可以自由选择模型、添加各种自定义配置 -
OllamaChatModel
:如果你引入了ollama的starter,这里就可以自动注入OllamaChatModel
对象。同理,OpenAI
也是一样的用法。
1.1.5.同步调用
接下来,定义一个Controller,在其中接收用户发送的提示词,然后把提示词发送给大模型,交给大模型处理,拿到结果后返回。代码如下:
package com.itheima.ai.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
private final ChatClient chatClient;
// 请求方式和路径不要改动,将来要与前端联调
@RequestMapping("/chat")
public String chat(@RequestParam(defaultValue = "讲个笑话") String prompt) {
return chatClient
.prompt(prompt) // 传入user提示词
.call() // 同步请求,会等待AI全部输出完才返回结果
.content(); //返回响应内容
}
}
注意,基于call()方法的调用属于同步调用,需要所有响应结果全部返回后才能返回给前端。启动项目,在浏览器中访问:http://localhost:8080/ai/chat?prompt=你好
1.1.6.流式调用
同步调用需要等待很长时间页面才能看到结果,用户体验不好。为了解决这个问题,我们可以改进调用方式为流式调用。在SpringAI中使用了WebFlux技术实现流式调用。修改刚才ChatController
中的chat方法:
// 注意看返回值,是Flux<String>,也就是流式结果,另外需要设定响应类型和编码,不然前端会乱码
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> chat(@RequestParam(defaultValue = "讲个笑话") String prompt) {
return chatClient
.prompt(prompt)
.stream() // 流式调用
.content();
}
重启测试,再次访问:
1.1.7.System设定
可以发现,当我们询问AI你是谁的时候,它回答自己是DeepSeek-R1,这是大模型底层的设定。如果我们希望AI按照新的设定工作,就需要给它设置System背景信息。在SpringAI中,设置System信息非常方便,不需要在每次发送时封装到Message,而是创建ChatClient时指定即可。我们修改CommonConfiguration
中的代码,给ChatClient
设定默认的System信息:
@Bean
public ChatClient chatClient(OllamaChatModel model) {
return ChatClient.builder(model) // 创建ChatClient工厂实例
.defaultSystem("假设你是小团团。请以友好、乐于助人和淘气的方式和用户对话。")
.defaultAdvisors(new SimpleLoggerAdvisor())
.build(); // 构建ChatClient实例
}
1.2.日志功能
默认情况下,应用于AI的交互时不记录日志的,我们无法得知SpringAI组织的提示词到底长什么样,有没有问题。这样不方便我们调试。
1.2.1.Advisor
SpringAI基于AOP机制实现与大模型对话过程的增强、拦截、修改等功能。所有的增强通知都需要实现Advisor接口。
Spring提供了一些Advisor的默认实现,来实现一些基本的增强功能:
-
SimpleLoggerAdvisor:日志记录的Advisor
-
MessageChatMemoryAdvisor:会话记忆的Advisor
-
QuestionAnswerAdvisor:实现RAG的Advisor
当然,我们也可以自定义Advisor,具体可以参考:Advisors API :: Spring AI Reference
1.2.2.添加日志Advisor
首先,我们需要修改CommonConfiguration
,给ChatClient
添加日志Advisor:
@Bean
public ChatClient chatClient(OllamaChatModel model) {
return ChatClient.builder(model) // 创建ChatClient工厂实例
.defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小团团,请以小团团的身份和语气回答问题。")
.defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志
.build(); // 构建ChatClient实例
}
1.2.3.修改日志级别
接下来,我们在application.yaml
中添加日志配置,更新日志级别:
logging:
level:
org.springframework.ai: debug # AI对话的日志级别
com.itheima.ai: debug # 本项目的日志级别
重启项目,再次聊天就能看到AI对话的日志信息了~。
1.2.4.解决CORS问题
前后端在不同域名,存在跨域问题,因此我们需要在服务端解决cors问题。在com.itheima.ai.config
包中添加一个MvcConfiguration
类,内容如下:
package com.itheima.ai.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Content-Disposition");
}
}
注意: 前端访问服务端的默认路径是:http://localhost:8080
聊天对话的接口是:POST /ai/chat,请确保你的服务端接口也是这样。
1.2.5.测试
启动前端后,访问 http://localhost:5173即可看到页面:
点击第一个卡片《AI聊天》进入对话机器人页面:
第一个AI对话机器人就完成了。
1.3.会话记忆功能
现在,我们的AI聊天机器人是没有记忆功能的,上一次聊天的内容,下一次就忘掉了。我们之前说过,让AI有会话记忆的方式就是把每一次历史对话内容拼接到Prompt中,一起发送过去。是不是还挺麻烦的。别担心,好消息是,我们并不需要自己来拼接,SpringAI自带了会话记忆功能,可以帮我们把历史会话保存下来,下一次请求AI时会自动拼接,非常方便。
1.3.1.ChatMemory
会话记忆功能同样是基于AOP实现,Spring提供了一个MessageChatMemoryAdvisor
的通知,我们可以像之前添加日志通知一样添加到ChatClient
即可。不过,要注意的是,MessageChatMemoryAdvisor
需要指定一个ChatMemory
实例,也就是会话历史保存的方式。ChatMemory
接口声明如下:
public interface ChatMemory {
// TODO: consider a non-blocking interface for streaming usages
default void add(String conversationId, Message message) {
this.add(conversationId, List.of(message));
}
// 添加会话信息到指定conversationId的会话历史中
void add(String conversationId, List<Message> messages);
// 根据conversationId查询历史会话
List<Message> get(String conversationId, int lastN);
// 清除指定conversationId的会话历史
void clear(String conversationId);
}
可以看到,所有的会话记忆都是与conversationId
有关联的,也就是会话Id,将来不同会话id的记忆自然是分开管理的。目前,在SpringAI中有两个ChatMemory的实现:
-
InMemoryChatMemory
:会话历史保存在内存中 -
CassandraChatMemory
:会话保存在Cassandra数据库中(需要引入额外依赖,并且绑定了向量数据库,不够灵活)
我们暂时选择用InMemoryChatMemory
来实现。
1.3.2.添加会话记忆Advisor
在CommonConfiguration
中注册ChatMemory
对象:
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
然后添加MessageChatMemoryAdvisor
到ChatClient
:
@Bean
public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model) // 创建ChatClient工厂实例
.defaultSystem("您是一家名为“黑马程序员”的职业教育公司的客户聊天助手,你的名字叫小黑。请以友好、乐于助人和愉快的方式解答学生的各种问题。")
.defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build(); // 构建ChatClient实例
}
OK,现在聊天会话已经有记忆功能了,不过现在的会话记忆还是不完善的,我们还有继续补充。
1.4.会话历史
会话历史与会话记忆是两个不同的事情:
会话记忆:是指让大模型记住每一轮对话的内容,不至于前一句刚问完,下一句就忘了。
会话历史:是指要记录总共有多少不同的对话
在ChatMemory中,会记录一个会话中的所有消息,记录方式是以conversationId
为key,以List<Message>
为value,根据这些历史消息,大模型就能继续回答问题,这就是所谓的会话记忆。
而会话历史,就是每一个会话的conversationId
,将来根据conversationId
再去查询List<Message>
。
注意,在接下来业务中,我们以chatId
来代指conversationId
.
1.4.1.管理会话id(会话历史)
由于会话记忆是以conversationId
来管理的,也就是会话id(以后简称为chatId)。将来要查询会话历史,其实就是查询历史中有哪些chatId.因此,为了实现查询会话历史记录,我们必须记录所有的chatId,我们需要定义一个管理会话历史的标准接口。我们定义一个com.itheima.ai.repository
包,然后新建一个ChatHistoryRepository
接口:
package com.itheima.ai.repository;
import java.util.List;
public interface ChatHistoryRepository {
/**
* 保存会话记录
* @param type 业务类型,如:chat、service、pdf
* @param chatId 会话ID
*/
void save(String type, String chatId);
/**
* 获取会话ID列表
* @param type 业务类型,如:chat、service、pdf
* @return 会话ID列表
*/
List<String> getChatIds(String type);
}
然后定义一个实现类InMemoryChatHistoryRepository
:
@Component
@RequiredArgsConstructor
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {
private Map<String, List<String>> chatHistory;
@Override
public void save(String type, String chatId) {
/*if (!chatHistory.containsKey(type)) {
chatHistory.put(type, new ArrayList<>());
}
List<String> chatIds = chatHistory.get(type);*/
List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList<>());
if (chatIds.contains(chatId)) {
return;
}
chatIds.add(chatId);
}
@Override
public List<String> getChatIds(String type) {
/*List<String> chatIds = chatHistory.get(type);
return chatIds == null ? List.of() : chatIds;*/
return chatHistory.getOrDefault(type, List.of());
}
}
【注意】:目前我们业务比较简单,没有用户概念,但是将来会有不同业务,因此简单采用内存保存type与chatId关系。将来大家也可以根据业务需要把会话id持久化保存到Redis、MongoDB、MySQL等数据库。如果业务中有user的概念,还需要记录userId、chatId、time等关联关系。
1.4.2.保存会话id
接下来,修改ChatController中的chat方法,做到3点:
-
添加一个请求参数:chatId,每次前端请求AI时都需要传递chatId
-
每次处理请求时,将chatId存储到ChatRepository
-
每次发请求到AI大模型时,都传递自定义的chatId
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;
@CrossOrigin("*")
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
private final ChatHistoryRepository chatHistoryRepository;
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> chat(@RequestParam(defaultValue = "讲个笑话") String prompt, String chatId) {
chatHistoryRepository.addChatId(chatId);
return chatClient
.prompt(prompt)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
.stream()
.content();
}
}
其中的CHAT_MEMORY_CONVERSATION_ID_KEY
是AbstractChatMemoryAdvisor中定义的常量key,将来MessageChatMemoryAdvisor
执行的过程中就可以拿到这个chatId了。
1.4.3.查询会话历史
接着,我们定义一个新的Controller,专门实现会话历史的查询。包含两个接口:
-
根据业务类型查询会话历史列表(我们将来有3个不同业务,需要分别记录历史。大家的业务可能是按userId记录,根据UserId查询)
-
根据chatId查询指定会话的历史消息
其中,查询会话历史消息,也就是Message集合。但是由于Message并不符合页面的需要,我们需要自己定义一个VO.
定义一个com.itheima.entity.vo
包,在其中定义一个MessageVO
类:
package com.itheima.ai.entity.vo;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.Message;
@NoArgsConstructor
@Data
public class MessageVO {
private String role;
private String content;
public MessageVO(Message message) {
this.role = switch (message.getMessageType()) {
case USER -> "user";
case ASSISTANT -> "assistant";
case SYSTEM -> "system";
default -> "";
};
this.content = message.getText();
}
}
然后在com.itheima.ai.controller
包下新建一个ChatHistoryController
:
package com.itheima.ai.controller;
import com.itheima.ai.entity.vo.MessageVO;
import com.itheima.ai.repository.ChatHistoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
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;
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {
private final ChatHistoryRepository chatHistoryRepository;
private final ChatMemory chatMemory;
/**
* 查询会话历史列表
* @param type 业务类型,如:chat,service,pdf
* @return chatId列表
*/
@GetMapping("/{type}")
public List<String> getChatIds(@PathVariable("type") String type) {
return chatHistoryRepository.getChatIds(type);
}
/**
* 根据业务类型、chatId查询会话历史
* @param type 业务类型,如:chat,service,pdf
* @param chatId 会话id
* @return 指定会话的历史消息
*/
@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();
}
}
OK,重启服务,现在AI聊天机器人就具备会话记忆和会话历史功能了!
持續更新中~~~。