一篇文章带你了解:SpringAI 竟然如此好玩(一)
本文介绍了大模型应用开发的四种主流技术架构:1. 纯Prompt模式:通过精心设计的提示词引导模型输出理想答案;2. Function Calling:将传统应用功能封装为函数,实现与大模型的协同工作;3. RAG(检索增强生成):结合信息检索技术解决模型知识局限性问题;4. Fine-tuning(模型微调):基于预训练模型使用领域数据进行二次训练。文章重点演示了如何利用SpringAI框架快速
大模型应用开发技术架构
基于大模型开发应用有多种方式,接下来我们就来了解下常见的大模型开发技术架构。
技术架构
目前,大模型应用开发的技术架构主要有四种:

纯Prompt模式
不同的提示词能够让大模型给出差异巨大的答案。
不断雕琢提示词,使大模型能给出最理想的答案,这个过程就叫做提示词工程(Prompt Engineering)。
很多简单的AI应用,仅仅靠一段足够好的提示词就能实现了,这就是纯Prompt模式。
其流程如图:

-
FunctionCalling
大模型虽然可以理解自然语言,更清晰弄懂用户意图,但是确无法直接操作数据库、执行严格的业务规则。这个时候我们就可以整合传统应用于大模型的能力了。
简单来说,可以分为以下步骤:
-
我们可以把传统应用中的部分功能封装成一个个函数(Function)。
-
然后在提示词中描述用户的需求,并且描述清楚每个函数的作用,要求AI理解用户意图,判断什么时候需要调用哪个函数,并且将任务拆解为多个步骤(Agent)。
-
当AI执行到某一步,需要调用某个函数时,会返回要调用的函数名称、函数需要的参数信息。
-
传统应用接收到这些数据以后,就可以调用本地函数。再把函数执行结果封装为提示词,再次发送给AI。
-
以此类推,逐步执行,直到达成最终结果。
流程如图:

RAG
RAG(Retrieval-Augmented Generation)叫做检索增强生成。简单来说就是把信息检索技术和大模型结合的方案。
大模型从知识角度存在很多限制:
-
时效性差:大模型训练比较耗时,其训练数据都是旧数据,无法实时更新
-
缺少专业领域知识:大模型训练数据都是采集的通用数据,缺少专业数据
可能有同学会说, 简单啊,我把最新的数据或者专业文档都拼接到提示词,一起发给大模型,不就可以了。
同学,你想的太简单了,现在的大模型都是基于Transformer神经网络,Transformer的强项就是所谓的注意力机制。它可以根据上下文来分析文本含义,所以理解人类意图更加准确。
但是,这里上下文的大小是有限制的,GPT3刚刚出来的时候,仅支持2000个token的上下文。现在领先一点的模型支持的上下文数量也不超过 200K token,所以海量知识库数据是无法直接写入提示词的。
怎么办呢?
RAG技术正是来解决这一问题的。
RAG就是利用信息检索技术来拓展大模型的知识库,解决大模型的知识限制。整体来说RAG分为两个模块:
-
检索模块(Retrieval):负责存储和检索拓展的知识库
-
文本拆分:将文本按照某种规则拆分为很多片段
-
文本嵌入(Embedding):根据文本片段内容,将文本片段归类存储
-
文本检索:根据用户提问的问题,找出最相关的文本片段
-
-
生成模块(Generation):
-
组合提示词:将检索到的片段与用户提问组织成提示词,形成更丰富的上下文信息
-
生成结果:调用生成式模型(例如DeepSeek)根据提示词,生成更准确的回答
-
由于每次都是从向量库中找出与用户问题相关的数据,而不是整个知识库,所以上下文就不会超过大模型的限制,同时又保证了大模型回答问题是基于知识库中的内容,完美!
流程如图:

Fine-tuning
Fine-tuning就是模型微调,就是在预训练大模型(比如DeepSeek、Qwen)的基础上,通过企业自己的数据做进一步的训练,使大模型的回答更符合自己企业的业务需求。这个过程通常需要在模型的参数上进行细微的修改,以达到最佳的性能表现。
在进行微调时,通常会保留模型的大部分结构和参数,只对其中的一小部分进行调整。这样做的好处是可以利用预训练模型已经学习到的知识,同时减少了训练时间和计算资源的消耗。微调的过程包括以下几个关键步骤:
-
选择合适的预训练模型:根据任务的需求,选择一个已经在大量数据上进行过预训练的模型,如Qwen-2.5。
-
准备特定领域的数据集:收集和准备与任务相关的数据集,这些数据将用于微调模型。
-
设置超参数:调整学习率、批次大小、训练轮次等超参数,以确保模型能够有效学习新任务的特征。
-
训练和优化:使用特定任务的数据对模型进行训练,通过前向传播、损失计算、反向传播和权重更新等步骤,不断优化模型的性能。
模型微调虽然更加灵活、强大,但是也存在一些问题:
-
需要大量的计算资源
-
调参复杂性高
-
过拟合风险
总之,Fine-tuning成本较高,难度较大,并不适合大多数企业。而且前面三种技术方案已经能够解决常见问题了。
我们选型的效果如下

SpringAI入门(对话机器人)
接下来,我们就利用SpringAI发起与大模型的第一次对话。
我们可以根据自己选择的平台来选择引入不同的依赖。这里我们先以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>
为了方便后续开发,我们再手动引入一个Lombok依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
接下来我们定义ChatClient
ChatClient中封装了与AI大模型对话的各种API,同时支持同步式或响应式交互。
不过,在使用之前,首先我们需要声明一个ChatClient。
在com.itheima.ai.config包下新建一个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实例
}
}
以及对应的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(); //返回响应内容
}
}
这样的效果不是流式调用,我们可以修改,使其变成流式调用
修改Controller
// 注意看返回值,是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();
}
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实例
}
日志功能
默认情况下,应用于AI的交互时不记录日志的,我们无法得知SpringAI组织的提示词到底长什么样,有没有问题。这样不方便我们调试。
SpringAI基于AOP机制实现与大模型对话过程的增强、拦截、修改等功能。所有的增强通知都需要实现Advisor接口。

Spring提供了一些Advisor的默认实现,来实现一些基本的增强功能:

-
SimpleLoggerAdvisor:日志记录的Advisor
-
MessageChatMemoryAdvisor:会话记忆的Advisor
-
QuestionAnswerAdvisor:实现RAG的Advisor
添加日志Advisor
首先,我们需要修改CommonConfiguration,给ChatClient添加日志Advisor:
@Bean
public ChatClient chatClient(OllamaChatModel model) {
return ChatClient.builder(model) // 创建ChatClient工厂实例
.defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小团团,请以小团团的身份和语气回答问题。")
.defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志
.build(); // 构建ChatClient实例
}
解决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");
}
}
会话记忆功能
现在,我们的AI聊天机器人是没有记忆功能的,上一次聊天的内容,下一次就忘掉了。
我们之前说过,让AI有会话记忆的方式就是把每一次历史对话内容拼接到Prompt中,一起发送过去。是不是还挺麻烦的。
别担心,好消息是,我们并不需要自己来拼接,SpringAI自带了会话记忆功能,可以帮我们把历史会话保存下来,下一次请求AI时会自动拼接,非常方便。
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来实现。
添加会话记忆Advisor
在CommonConfiguration中注册ChatMemory对象:
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
然后添加MessageChatMemoryAdvisor到ChatClient:
@Bean
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model) // 创建ChatClient工厂实例
.defaultSystem("你是一个热心的智能助手,你的名字叫小昂")
.defaultOptions(ChatOptions.builder().model("qwen-omni-turbo").build())
.defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build(); // 构建ChatClient实例
}
OK,现在聊天会话已经有记忆功能了,不过现在的会话记忆还是不完善的,我们还有继续补充。
会话历史
会话历史与会话记忆是两个不同的事情:
会话记忆:是指让大模型记住每一轮对话的内容,不至于前一句刚问完,下一句就忘了。
会话历史:是指要记录总共有多少不同的对话
在ChatMemory中,会记录一个会话中的所有消息,记录方式是以conversationId为key,以List<Message>为value,根据这些历史消息,大模型就能继续回答问题,这就是所谓的会话记忆。
而会话历史,就是每一个会话的conversationId,将来根据conversationId再去查询List<Message>。
比如上图中,有3个不同的会话历史,就会有3个conversationId,管理会话历史,就是记住这些conversationId,当需要的时候查询出conversationId的列表。
管理会话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:
package com.itheima.ai.repository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
@RequiredArgsConstructor
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {
private Map<String, List<String>> chatHistory=new HashMap<>();
@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());
}
}
保存会话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();
}
}
注意,这里传递chatId给Advisor的方式是通过AdvisorContext,也就是以key-value形式存入上下文:
chatClient.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
其中的CHAT_MEMORY_CONVERSATION_ID_KEY是AbstractChatMemoryAdvisor中定义的常量key,将来MessageChatMemoryAdvisor执行的过程中就可以拿到这个chatId了。
查询会话历史
接着,我们定义一个新的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聊天机器人就具备会话记忆和会话历史功能了!
更多推荐


所有评论(0)