1.对话机器人
1.1对话机器人 - 初步实现
1.1.1引入依赖
SpringBoot 创建项目引入 Ollama/OpenAI 的依赖。 手动写入 Lombok 依赖,自动导入的有 bug。
1.1.2配置模型信息
application.yaml 中配置信息,以 ollama 为例:
spring:
application:
name: ai-demo
ai:
ollama:
base-url: http://localhost:11434 # ollama 服务地址,这就是默认值
chat:
model: deepseek-r1:7b # 模型名称
options:
temperature: 0.8 # 模型温度,影响模型生成结果的随机性,越小越稳定
1.1.3编写配置类 CommonConfiguration
@Configuration
public class CommonConfiguration {
@Bean
public ChatClient chatClient(OllamaChatModel model) {
return ChatClient.builder(model)
.defaultSystem("你是一个傲娇的智能助手,身份是我的女友,请以女友的身份和傲娇的语气回答问题")
.build();
}
}
1.1.4同步调用
同步调用,需要所有响应结果全部返回后才能返回给前端。 启动项目,在浏览器中访问:http://localhost:8080/ai/chat?prompt=你好
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
private final ChatClient chatClient;
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public String chat(String prompt) {
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
1.1.5流式调用
SpringAI 中使用了 WebFlux 技术实现流式调用。
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
private final ChatClient chatClient;
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat(String prompt) {
return chatClient.prompt()
.user(prompt)
.stream()
.content();
}
}
1.2对话机器人 - 日志功能
1.2.1添加日志
修改 CommonConfiguration,给 ChatClient 添加日志 Advisor。
@Configuration
public class CommonConfiguration {
@Bean
public ChatClient chatClient(OllamaChatModel model) {
return ChatClient.builder(model)
.defaultSystem("你是合肥工业大学的一名资深老学长,十分熟悉校园,请以该身份的语气和性格回答问题")
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
}
1.2.2修改日志级别
在 application.yaml 中添加日志配置,更新日志级别:
logging:
level:
org.springframework.ai: debug # AI 对话的日志级别
com.itheima.ai: debug # 本项目的日志级别
1.3对接前端
1.3.1npm 运行(0 代码前端开发,待学)
1.3.2Nginx 运行
解压 Nginx 后,cmd 运行即可。
# 启动 Nginx
start nginx.exe
# 停止
nginx.exe -s stop
前端的端口是 5173,访问 http://localhost:5173/ 即可看到页面。
1.3.3解决 CORS 问题
CORS 问题即跨域问题。前后端分离的项目,默认本地端口不一样,前端是 5173,后端是 8080。浏览器会检查响应头中的 CORS 配置是否允许当前域名访问。
SpringBoot 当中解决 CORS 问题的三种方式:
1.针对某一个接口进行配置(加注解) 在接口的方法上添加@CrossOrigin 注解。
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
@CrossOrigin("http://localhost:5173")
public Flux<String> chat(String prompt) {
return chatClient.prompt()
.user(prompt)
.stream()
.content();
}
2.批量设置一批接口支持跨域(写配置类)
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD")
.allowHeaders(true);
}
}
1.4会话记忆功能
1.4.1实现原理
让 AI 有会话记忆的方式就是把每一次历史对话内容拼接到 Prompt 中,一起发送过去。SpringAI 自带了会话记忆功能,可以帮我们把历史会话保存下来,下一次请求 AI 时会自动拼接。
1.4.2注册 ChatMemory 对象(与视频有变动)
ChatMemory接口声明如下:
public interface ChatMemory {
default void add(String conversationId, Message message) {
this.add(conversationId, List.of(message));
}
void add(String conversationId, List<Message> messages);
List<Message> get(String conversationId, int lastN);
void clear(String conversationId);
}
所有的会话记忆都是与 conversationId有关联的,也就是会话 Id。现在统一为:MessageWindowChatMemory。
在 CommonConfiguration 中注册 ChatMemory 对象:
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository()) // 设置存储库
.maxMessages(10) // 记忆窗口大小(保留最近的 10 条消息)
.build();
}
也可以直接:
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder().build();
}
MessageWindowChatMemory默认使用的存储库就是 InMemory,默认窗口大小是 20。
1.4.3添加会话记忆 Advisor(与视频有变动)
因为使用的是 MessageWindowChatMemory,添加 advisor 的时候需要如下操作:
@Bean
public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultSystem("你是合肥工业大学的一名资深老学长,十分熟悉校园,请以该身份的语气和性格回答问题")
.defaultAdvisors(new SimpleLoggerAdvisor())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
在 chatClient 中传入参数 chatMemory,添加 .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())。
1.4.4设置会话 id
当前虽然实现了会话记忆功能,但是不同的人去对话,都会获取会话记忆。因此需要根据 id,区分不同的会话记忆。 前端每次发送会话请求的时候,除了发送提示词 prompt 之外,还会发送一个会话 id chatid。 在接收到 chatId 之后,将会话 id 配置到 chatClient 的 chatMemory 的 CONVERSATION_ID 属性当中。
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat(@RequestParam("prompt") String prompt, @RequestParam("chatId") String chatId) {
return chatClient.prompt()
.user(prompt)// 设置用户输入
.advisors(a->a.param(ChatMemory.CONVERSATION_ID,chatId))// 设置会话 ID
.stream()// 开启流式对话
.content();// 获取对话内容
}
Web 界面中测试,开启新对话后,无法获取之前对话的记忆。
1.5会话历史功能
会话历史与会话记忆是两个不同的事情: 会话记忆:是指让大模型记住每一轮对话的内容,不至于前一句刚问完,下一句就忘了。 会话历史:是指要记录总共有多少不同的对话。
查看 ChatMemory 可以发现,获取会话历史数据是通过 conversationId 获取的。
进入 AI 聊天时,发送请求:http://localhost:8080/ai/history/chat
创建新对话时,发送请求:http://localhost:8080/ai/history/chat/1748848508972
/chat 就是获取所有的会话历史,/chat/chatid 就是获取详细的某个 id 对应的会话历史。
1.5.1管理会话 id
定义一个 com.hfut.ai.repository包,然后新建一个 ChatHistoryRepository接口。
package com.hfut.ai.repository;
import java.util.List;
public interface ChatHistoryRepository {
void save(String type, String chatId);
void delete(String type, String chatId);
List<String> getChatIds(String type);
}
通过内存来保存 chatId
@Repository
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {
private final Map<String, List<String>> chatHistory = new HashMap<>();
@Override
public void save(String type, String chatId) {
List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList<>());
if (chatIds.contains(chatId)) return;
chatIds.add(chatId);
}
@Override
public List<String> getChatIds(String type) {
return chatHistory.getOrDefault(type, new ArrayList<>());
}
}
通过数据库来保存 chatId 创建数据库表:
CREATE TABLE chat_history (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
type VARCHAR(255) NOT NULL,
chat_id VARCHAR(255) NOT NULL
);
添加 sql 和 Mybatis 依赖,配置数据库连接,定义实体类,创建 Mapper。
@Mapper
public interface ChatHistoryMapper {
@Insert("INSERT INTO chat_history (type, chat_id) VALUES (#{type}, #{chatId})")
void insert(ChatHistory chatHistory);
@Delete("DELETE FROM chat_history WHERE type = #{type} AND chat_id = #{chatId}")
void delete(@Param("type") String type, @Param("chatId") String chatId);
@Select("SELECT chat_id FROM chat_history WHERE type = #{type}")
List<String> selectChatIdsByType(String type);
}
编写 InSqlChatHistoryRepository 实现类,将 chatId 保存到数据库中。
1.5.2保存会话 id
修改 ChatController 中的 chat 方法,做到 3 点:
- 添加一个请求参数:chatId,每次前端请求 AI 时都需要传递 chatId。
- 每次处理请求时,将 chatId 存储到 ChatRepository。
- 每次发请求到 AI 大模型时,都传递自定义的 chatId。
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
private final ChatClient chatClient;
@Autowired
@Qualifier("inMemoryChatHistoryRepository")
private ChatHistoryRepository chatHistoryRepository;
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat(@RequestParam("prompt") String prompt, @RequestParam("chatId") String chatId) {
chatHistoryRepository.save(ChatType.CHAT.getValue(), chatId);
return chatClient.prompt()
.user(prompt)
.advisors(a->a.param(ChatMemory.CONVERSATION_ID,chatId))
.stream()
.content();
}
}
1.5.3查询历史会话
历史会话保存在 ChatMemory 当中,通过 conversationId(chatId)获取。 前端代码的要求是返回一个 role 和 content,分别代表发言人和发言内容。 编写 entity 类作为返回类型:
@NoArgsConstructor
@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 = "unknown"; break;
}
this.content = message.getText();
}
}
编写新的接口:
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {
private final ChatMemory chatMemory;
@Autowired
@Qualifier("inMemoryChatHistoryRepository")
private ChatHistoryRepository chatHistoryRepository;
@RequestMapping("/{type}")
public List<String> getChatIds(@PathVariable("type") String type) {
return chatHistoryRepository.getChatIds(type);
}
@RequestMapping("/{type}/{chatId}")
public List<MessageVO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {
List<Message> messages = chatMemory.get(chatId);
if (messages == null) return List.of();
return messages.stream().map(MessageVO::new).toList();
}
}
通过数据库来保存历史会话(难点) 查看 MessageWindowChatMemory 的源码,默认使用的是 InMemoryChatMemoryRepository。 如果想要通过数据库来保存历史会话,需要以下步骤:
- 创建表
chat_message。 - 定义实体类
ChatMessage。 - 编写 Mapper 接口。
- 自定义 ChatMemory 的实现类 InSqlChatMemory。
@Component
public class InSqlChatMemory implements ChatMemory {
@Autowired
private ChatMessageMapper chatMessageMapper;
@Override
public void add(String conversationId, List<Message> messages) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
Assert.notNull(messages, "messages cannot be null");
for (Message message : messages) {
String role;
switch (message.getMessageType()) {
case USER: role = "user"; break;
case ASSISTANT: role = "assistant"; break;
default: role = "unknown"; break;
}
ChatMessage chatMessage = new ChatMessage();
chatMessage.setConversationId(conversationId);
chatMessage.setRole(role);
chatMessage.setContent(message.getText());
chatMessageMapper.save(chatMessage);
}
}
@Override
public List<Message> get(String conversationId) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
List<ChatMessage> chatMessages = chatMessageMapper.findByConversationId(conversationId);
List<Message> messages = new ArrayList<>();
for (ChatMessage chatMessage : chatMessages) {
switch (chatMessage.getRole()) {
: messages.add( (chatMessage.getContent())); ;
: messages.add( (chatMessage.getContent())); ;
: ( + chatMessage.getRole());
}
}
messages;
}
{
chatMessageMapper.deleteByConversationId(conversationId);
}
}
在 CommonConfiguration 里配置下 ChatMemory:
@Bean
public ChatMemory chatMemory() {
return new InSqlChatMemory();
}
1.6总结 - 对话机器人
1.6.1基本实现
1.1-1.3 属于是基本配置,需要注意的就是解决 CORS 问题。
1.6.2会话记忆实现
再次复盘会话记忆和会话历史的区别。 会话记忆:是指让大模型记住每一轮对话的内容。 会话历史:是指要记录总共有多少不同的对话。 会话记忆的实现,根据三步走就可以实现:
- 配置 ChatMemory。
- 在 ChatClient 当中通过 Advisor 加入 ChatMemory。
- 进行会话时设置会话 id。
1.6.3会话历史实现(难点)
会话历史分为两个部分:会话 id 和具体会话内容。 会话内容是保存在 ChatMemory 当中的,需要通过 ChatId(conversationId)去获取。 会话 id 是我们自己设计方式去保存的。 保存会话 id:内存保存或数据库保存。 保存会话内容:内存保存或数据库保存(仿造 MessageWindowChatMemory 写一个 ChatMemory 的实现类)。
2.哄哄模拟器(纯 prompt 开发)
2.1提示词工程
通过优化提示词,让大模型生成出尽可能理想的内容,这一过程就称为提示词工程(Prompt Engineering)。
2.2代码实现
2.2.1配置 OpenAI 参数
spring:
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${OPENAI_API_KEY}
chat:
options:
model: qwen-max-latest
为了防止 api-key 泄露,使用了${OPENAI_API_KEY}来读取环境变量。
2.2.2配置 ChatClient
我们可以配置多个 ChatClient 用于不同的场景。
@Bean
public ChatClient gameChatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultSystem(SystemConstants.GAME_SYSTEM_PROMPT)
.defaultAdvisors(new SimpleLoggerAdvisor())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
自定义提示词 SystemConstants.GAME_SYSTEM_PROMPT。
2.2.3编写 Controller
@RequestMapping("/ai")
@RestController
@RequiredArgsConstructor
public class GameController {
private final ChatClient gameChatClient;
@RequestMapping(value = "/game", produces = "text/html;charset=utf-8")
public Flux<String> chat(@RequestParam("prompt") String prompt, @RequestParam("chatId") String chatId) {
return gameChatClient.prompt()
.user(prompt)
.advisors(a->a.param(ChatMemory.CONVERSATION_ID,chatId))
.stream()
.content();
}
}
使用新的 gameChatClient,修改路径为/game。
2.3总结
需要注意的有以下几个地方:
- 提示词工程,也就是 prompt 文案的设计。
- 代码方面,如果自定义了一个 ChatMemory 的实现类,在 Client 里直接传入即可。
3.智能客服(Function Calling)
3.1实现思路
由于 AI 擅长的是非结构化数据的分析,如果需求中包含严格的逻辑校验或需要读写数据库,纯 Prompt 模式就难以实现了。此时就需要通过 Function Calling 来实现。
Function Calling 的作用是将数据库的操作定义为 Function(Tool),在提示词中告诉大模型什么情况下需要调用什么工具。 简化步骤:
- 编写基础提示词(不包括 Tool 的定义)。
- 编写 Tool(Function)。
- 配置 Advisor(SpringAI 利用 AOP 帮我们拼接 Tool 定义到提示词,完成 Tool 调用动作)。
3.2基础 CRUD
3.2.1数据库表
课程表、课程预约表、校区表等结构定义。
3.2.2引入依赖(已配置)
3.2.3配置数据库(已配置)
3.2.4基础代码(MyBatisPlus 生成)
实体类、Mapper 接口、Service 层代码由 MyBatisPlus 生成。
3.3定义 Function(与课程有变动)
定义了三个 Function:
- 根据条件筛选和查询课程。
- 根据校区名称查询当前校区的所有课程。
- 新增课程预约单。
3.3.1查询条件分析
封装查询条件的类 ElectiveCourseQuery,使用@ToolParam 注解。
3.3.2定义 Function(关键)
使用@Tool 注解标记特殊函数。
@Component
public class ElectiveCourseTools {
@Tool(description = "根据条件查询选修课程")
public List<ElectiveCourse> queryElectiveCourse(@ToolParam(required = false, description = "选修课程查询条件") ElectiveCourseQuery query) {
// ... 实现逻辑
}
// ... 其他工具方法
}
3.4System 提示词设计
3.4.1安全防范措施
防止 prompt 注入或指令绕过。
3.4.2调用规则设计(关键)
设计选修课程咨询规则和课程预约规则,引导大模型按流程调用工具。 发现漏洞并优化编写新的 Function,例如:
- 如果没有找到符合要求的课程,根据年级查询该年级可选的其他课程。
- 星期几和周几的转换问题,编写工具方法进行解析。
- 用户所选课程与所选校区不匹配的情况,增加判断方法。
3.4.3完整代码
完整的 Tools 代码和 System 提示词配置。
3.5配置 ChatClient
为智能客服定制一个 ChatClient,添加工具调用的功能。
@Bean
public ChatClient serviceChatClient(OpenAiChatModel model, ChatMemory chatMemory, ElectiveCourseTools electiveCourseTools) {
return ChatClient.builder(model)
.defaultSystem(SystemConstants.SERVICE_SYSTEM_PROMPT)
.defaultAdvisors(new SimpleLoggerAdvisor())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.defaultTools(electiveCourseTools)
.build();
}
3.6编写 Controller
编写 CustomerServiceController 类,对接前端接口。
3.7存储到数据库(再详谈)
复盘存储会话 id 和历史内容需要动的三个板块:
- ChatMemory:负责把会话内容存入和取出内存/数据库。
- ChatHistoryRepository:负责把会话 id 存入内存/数据库。
- ChatHistoryController:负责把会话 id 和会话内容取出。
3.8总结
总结一下 Function Calling 的整体流程:
- 数据库构造。
- Function 定义。
- System 提示词设计。
- 配置 ChatClient 和 Controller。
4.ChatPDF(RAG)
4.1RAG 原理
为了解决大模型的知识限制问题,外挂一个知识库。通过向量模型将文本向量化,利用向量距离来判断文本相似度,从庞大的知识库中找到与用户问题相关的内容。
4.1.1向量模型
向量模型可以将文本转化为坐标,推断两份数据的相似度。阿里云百炼平台提供了 text-embedding-v3 模型。
4.1.2向量模型测试
编写工具类计算向量之间的欧氏距离和余弦距离,测试向量化效果。
4.1.3向量数据库(进阶)
向量数据库的主要作用有两个:存储向量数据、基于相似度检索数据。 SpringAI 支持多种向量数据库,如 Redis Vector Store、SimpleVectorStore 等。
4.1.3.1安装 docker 和 Redis
搭建虚拟机,安装 Docker,配置 MySQL 和 Redis。
4.1.3.2 SimpleVectorStore(原教程)
基于内存实现,适合测试教学。
4.1.3.3 Redis Vector Store
引入依赖,配置 yaml 文件。
4.1.3.4.VectorStore 接口
VectorStore 操作向量化的基本单位是 Document。
4.1.4文件的读取和转化
SpringAI 提供了各种文档读取的工具 DocumentReader,如 PagePdfDocumentReader。 编写单元测试测试向量库功能。
注意:Redis Vector Store 如果是按照我的方法自动配置的,那么不需要去修改 CommonConfiguration。如果要使用 SimpleVectorStore,那么就需要去修改 CommonConfiguration。
今天就基本上更新到这里了,测试能过的情况,后续无非就是和前端输入对接了。剩下的内容只剩一点点了,我会加紧更新,马上要完结撒花了!


