目录
1、Ollama 的下载配置 与 DeepSeek 的本地部署流程
1.1 下载安装 Ollama
1.2 搜索模型并进行本地部署
2、基于 SpringAI 调用 Ollama 模型
2.1 基于OpenAI 的接口规范(其他模型基本遵循)
2.2 在 IDEA 中进行创建 SpringAI 项目并调用 DS 模型
3、基于 SpringAI 实现会话记忆功能
4、基于 SpringAI 实现会话历史功能
需求一:需要进行展示历史会话的列表信息
需求二:点击会话列表,展示对应的历史会话记录信息
📜后端代码地址:
MySpringAI: 基于 SpringAI + 大模型 实现基本的对话功能(只提供后端)
https://gitee.com/MIMIDeK/my-spring-ai.git
1、Ollama 的下载配置 与 DeepSeek 的本地部署流程
很多云平台都提供了一键部署大模型的功能,这里不再赘述(阿里云百炼平台、火山方舟-火山引擎等)
官网地址如下:
Ollamahttps://ollama.com/
1.1 下载安装 Ollama
首先,我们需要下载一个Ollama的客户端,在官网提供了各种不同版本的Ollama,大家可以根据自己的需要下载
注意:
Ollama默认安装目录是C盘的用户目录,如果不希望安装在C盘的话(其实C盘如果足够大放C盘也没事),就不能直接双击安装了,需要通过命令行安装
在 OllamaSetup.exe 所在目录打开cmd命令行,然后命令如下:
OllamaSetup.exe /DIR=你要安装的目录位置
安装完成后,需要进行环境变量的配置:
OLLAMA_MODELS=你想要保存模型的目录
1.2 搜索模型并进行本地部署
Ollama 是一个模型管理工具和平台,它提供了很多国内外常见的模型,我们可以在其官网上搜索自己需要的模型,这里我们部署的是 DeepSeek R1 模型(我选择的是 7b,根据需求进行选择):
其中,Ollama 的命令很像 Docker 的,选择好对应的模型后,在控制台中进行运行命令:
ollama run deepseek-r1:7b
首次下载的话会有些慢,耐性等待即可
以下是本地下载部署完成后,进行对话的场景,ctrl + d 进行退出与 DeepSeek 的对话:
2、基于 SpringAI 调用 Ollama 模型
2.1 基于OpenAI 的接口规范(其他模型基本遵循)
目前大多数大模型都遵循 OpenAI 的接口规范,是基于Http协议的接口;因此请求路径、参数、返回值信息都是类似的,可能会有一些小的差别,具体需要查看大模型的官方API文档
以下是基于 Python 的代码示范:
# Please install OpenAI SDK first: `pip3 install openai`from openai import OpenAI# 1.初始化OpenAI客户端,要指定两个参数:api_key、base_url
client = OpenAI(api_key="<DeepSeek API Key>", base_url="https://api.deepseek.com")# 2.发送http请求到大模型,参数比较多
response = client.chat.completions.create(model="deepseek-chat", # 2.1.选择要访问的模型messages=[ # 2.2.发送给大模型的消息{"role": "system", "content": "You are a helpful assistant"},{"role": "user", "content": "Hello"},],stream=False # 2.3.是否以流式返回结果
)print(response.choices[0].message.content)
接口说明
- 请求方式):通常是POST,因为要传递JSON风格的参数
- 请求路径):与平台有关
- DeepSeek官方平台:https://api.deepseek.com
- 阿里云百炼平台:https://dashscope.aliyuncs.com/compatible-mode/v1
- 本地ollama部署的模型:http://localhost:11434
- 安全校验):开放平台 都需要提供 API_KEY 来校验权限,本地 ollama 则不需要
- 请求参数)(这里只列举常用的):
- model:要访问的模型名称
- messages:发送给大模型的消息,是一个数组
- stream:true,代表响应结果流式返回;false,代表响应结果一次性返回,但需要等待
- temperature:取值范围[0:2),代表大模型生成结果的随机性,越小随机性越低。DeepSeek-R1不支持
- 这里请求参数中的 messages 是一个消息数组,其中包含两个属性):
- role:消息对应的角色
- content:消息内容
2.2 在 IDEA 中进行创建 SpringAI 项目并调用 DS 模型
以下是创建项目的基本依赖需求(基于SpringBoot3、JDK17):
.yaml 配置:
这里是基于 Ollama 的(默认端口是11434)
这里是基于 openAI 的,进行调用开放平台的 API,需要输入 API-KEY(后面就不具体写明了,这里主要是基于 Ollama 的调用演示)
Config 配置类:(固定写法)
@Configuration
public class CommonConfig {@Beanpublic ChatClient chatClient(OllamaChatModel model) {return ChatClient.builder(model).build();}
}
Controller 控制响应类:(这里是以流式的方式进行响应)
@RestController
@RequestMapping("/ai")
public class ChatController {@Resourceprivate ChatClient chatClient;// 由于以流式方式调用模型,展示的数据会出现乱码,需要进行规定编码格式@GetMapping(value = "/chat", produces = "text/html; charset=utf-8")public Flux<String> chat(String msg) {return chatClient.prompt().user(msg).stream().content();}
}
进行页面的调用(若非流式访问,则需要等待 DS 生成完毕,才会出现响应信息):
注意,进行接口的调用时,需要启动 ollama 中的模型
🔖也可以加上日志的控制台打印输出,方便调试
.yml 中的配置:
然后在 Config 配置类中添加这一行代码,即可开启日志的打印:
3、基于 SpringAI 实现会话记忆功能
大致步骤:
定义会话存储方式(默认基于Map集合) ➡️ 配置会话记忆(关联上下文) ➡️ 添加会话ID(保证每个会话独立)
这里使用默认方式,由源码可知是基于 Map 直接存入内存中的;当然也可以重写 ChatMemory,比如存入到 Redis 中做缓存之类的(根据业务需求)
然后将 "存储方式" 配置到 "会话的上下文" 中,以处理会话的内容
运行结果:
之前的msg提问信息是:现在有15个苹果,6个人应该怎么分才好
我现在直接问它x个人应该怎么分,很明显它会进行关联之前的会话,根据上下文,来进行作答
存在问题:
然而,以上会话的处理会比较混乱,不管是谁来进行提问,模型都会自动关联所有的会话上下文来进行回答,这显然是不符合需求的;这时就需要与前端进行交接,即传递对应的会话 ID 过来进行处理,以实现用户的每个独立会话内容互不干扰
由前端响应接口可知,不同的会话对应着不同的会话ID
这时需要在 Controller 接口中添加以下配置,来接收存储不同会话下的ID
4、基于 SpringAI 实现会话历史功能
存在问题:目前,只要页面一刷新,之前的会话列表和对话记录都会被清除,不会被保留展示
前言:以下的 type 参数可根据具体需求进行更换,chatId 建议作保留以记录唯一会话
需求一:需要进行展示历史会话的列表信息
以下是对应的 UI 页面,以及对应的接口地址(模拟):
以下是 自定义 的 方法接口 与 实现类,主要用于 保存 和 获取 会话信息
public interface ChatHistoryService {/*** 保存会话记录(这里的 type 可根据需求进行更改,chatId 建议保留以作为会话的唯一ID)*/void save(String type, String chatId);/*** 获取会话列表(这里的 type 可根据需求进行更改)*/List<String> getChatIds(String type);
}
@Service
public class ChatHistoryServiceImpl implements ChatHistoryService {// 使用 map 集合进行存储private final Map<String, List<String>> chatHistoryMap = new HashMap<>();/*** 保存会话记录(这里的 type 可根据需求进行更改,chatId 建议保留以作为会话的唯一ID)*/@Overridepublic void save(String type, String chatId) {// 1.判断当前是否存在对应的 type 类型,若不存在则新增if (! chatHistoryMap.containsKey(type)) {chatHistoryMap.put(type, new ArrayList<>());}// 2.判断当前的 type 对应的 会话集合ID 是否含有当前的会话IDList<String> chatIds = chatHistoryMap.get(type);if (chatIds.contains(chatId)) {return;}chatIds.add(chatId);}/*** 获取会话列表信息(这里的 type 可根据需求进行更改)*/@Overridepublic List<String> getChatIds(String type) {List<String> chatIds = chatHistoryMap.get(type);return chatIds == null ? new ArrayList<>() : chatIds;}
}
每次用户在进行模型对话的时候,都需要将会话ID做保存(这里的 key 根据自己的业务需求来定,比如使用 用户ID 来进行拼接等,这里先写死)
然后获取当前对应 key 的会话 ID 集合,进行会话列表信息展示
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {@Resourceprivate ChatHistoryService chatHistoryService;/*** 获取历史会话列表信息(这里的 type 可根据需求进行更改)*/@RequestMapping("/{type}")public List<String> getChatIds(@PathVariable("type") String type) {return chatHistoryService.getChatIds(type);}
}
需求二:点击会话列表,展示对应的历史会话记录信息
像以下UI所示,随便点击其中一个会话列表,对应的历史会话记录依然存在,并进行全部展示
接口地址(模拟):
会话的存储是在 ChatMemory 类中进行的,所以需要跟进源码进行查看
主要还是这个 get 方法,来进行获取对应会话的对话记录
然后 返回值 Message 类型,内部有继承以及实现的类
其中的 Content 代表着对话记录的内容,MessageType 代表着当前对话记录的类型
以上结论:由于需要满足当前接口的返回值,所以需要创建一个 DTO 来进行满足
@Data
@NoArgsConstructor
public class MessageDTO implements Serializable {/*** 角色类型*/private String role;/*** 对应的内容信息*/private String content;public MessageDTO(Message message) {// 判断当前会话的类型switch (message.getMessageType()) {case USER:this.role = "user";break;case ASSISTANT:this.role = "assistant";break;case SYSTEM:this.role = "system";break;case TOOL:this.role = "tool";break;default:this.role = "";break;}this.content = message.getText();}@Serialprivate static final long serialVersionUID = 1L;
}
接下来实现 “查询对话记录” 接口,与前端交互,即可实现展现历史对话记录
/*** 查询会话记录详情(这里的 type 可根据需求进行更改,chatId 建议保留以作为会话的唯一ID)*/@GetMapping("/{type}/{chatId}")public List<MessageDTO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {// 这里的展示记录条数最大值规定,直接使用的 int 型最大值,具体自己根据需求规定List<Message> messages = chatMemory.get(chatId, Integer.MAX_VALUE);if (messages == null) {return List.of();}// 进行封装返回类型return messages.stream().map(new Function<Message, MessageDTO>() {@Overridepublic MessageDTO apply(Message message) {return new MessageDTO(message);}}).toList();}