Spring AI + ollama 本地搭建聊天 AI
不知道怎麼搭建 ollama 的可以檢視上一篇Spring AI 初學。
專案可以檢視gitee
前期準備
新增依賴
建立 SpringBoot 專案,新增主要相關依賴(spring-boot-starter-web、spring-ai-ollama-spring-boot-starter)
Spring AI supports Spring Boot 3.2.x and 3.3.x
Spring Boot 3.2.11 requires at least Java 17 and is compatible with versions up to and including Java 23
<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>
<version>1.0.0-M3</version>
</dependency>
配置檔案
application.properties、yml配置檔案中新增,也可以在專案中指定模型等引數,具體引數可以參考 OllamaChatProperties
# properties,模型 qwen2.5:14b 根據自己下載的模型而定
spring.ai.ollama.chat.options.model=qwen2.5:14b
#yml
spring:
ai:
ollama:
chat:
model: qwen2.5:14b
聊天實現
主要使用 org.springframework.ai.chat.memory.ChatMemory 介面儲存對話資訊。
一、採用 Java 快取對話資訊
支援功能:聊天對話、切換對話、刪除對話
controller
import com.yb.chatai.domain.ChatParam;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
/*
*@title Controller
*@description 使用記憶體進行對話
*@author yb
*@version 1.0
*@create 2024/11/12 14:39
*/
@Controller
public class ChatController {
//注入模型,配置檔案中的模型,或者可以在方法中指定模型
@Resource
private OllamaChatModel model;
//聊天 client
private ChatClient chatClient;
// 模擬資料庫儲存會話和訊息
private final ChatMemory chatMemory = new InMemoryChatMemory();
//首頁
@GetMapping("/index")
public String index(){
return "index";
}
//開始聊天,生成唯一 sessionId
@GetMapping("/start")
public String start(Model model){
//新建聊天模型
// OllamaOptions options = OllamaOptions.builder();
// options.setModel("qwen2.5:14b");
// OllamaChatModel chatModel = new OllamaChatModel(new OllamaApi(), options);
//建立隨機會話 ID
String sessionId = UUID.randomUUID().toString();
model.addAttribute("sessionId", sessionId);
//建立聊天client
chatClient = ChatClient.builder(this.model).defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory, sessionId, 10)).build();
return "chatPage";
}
//聊天
@PostMapping("/chat")
@ResponseBody
public String chat(@RequestBody ChatParam param){
//直接返回
return chatClient.prompt(param.getUserMsg()).call().content();
}
//刪除聊天
@DeleteMapping("/clear/{id}")
@ResponseBody
public void clear(@PathVariable("id") String sessionId){
chatMemory.clear(sessionId);
}
}
效果圖
二、採用資料庫儲存對話資訊
支援功能:聊天對話、切換對話、刪除對話、撤回訊息
實體類
import lombok.Data;
import java.util.Date;
@Data
public class ChatEntity {
private String id;
/** 會話id */
private String sessionId;
/** 會話內容 */
private String content;
/** AI、人 */
private String type;
/** 建立時間 */
private Date time;
/** 是否刪除,Y-是 */
private String beDeleted;
/** AI會話時,獲取人對話ID */
private String userChatId;
}
configuration
import com.yb.chatai.domain.ChatEntity;
import com.yb.chatai.service.IChatService;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/*
*@title DBMemory
*@description 實現 ChatMemory,注入 spring,方便採用 service 方法
*@author yb
*@version 1.0
*@create 2024/11/12 16:15
*/
@Configuration
public class DBMemory implements ChatMemory {
@Resource
private IChatService chatService;
@Override
public void add(String conversationId, List<Message> messages) {
for (Message message : messages) {
chatService.saveMessage(conversationId, message.getContent(), message.getMessageType().getValue());
}
}
@Override
public List<Message> get(String conversationId, int lastN) {
List<ChatEntity> list = chatService.getLastN(conversationId, lastN);
if(list != null && !list.isEmpty()) {
return list.stream().map(l -> {
Message message = null;
if (MessageType.ASSISTANT.getValue().equals(l.getType())) {
message = new AssistantMessage(l.getContent());
} else if (MessageType.USER.getValue().equals(l.getType())) {
message = new UserMessage(l.getContent());
}
return message;
}).collect(Collectors.<Message>toList());
}else {
return new ArrayList<>();
}
}
@Override
public void clear(String conversationId) {
chatService.clear(conversationId);
}
}
services實現類
import com.yb.chatai.domain.ChatEntity;
import com.yb.chatai.service.IChatService;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.stereotype.Service;
import java.util.*;
/*
*@title ChatServiceImpl
*@description 儲存使用者會話 service 實現類
*@author yb
*@version 1.0
*@create 2024/11/12 15:50
*/
@Service
public class ChatServiceImpl implements IChatService {
Map<String, List<ChatEntity>> map = new HashMap<>();
@Override
public void saveMessage(String sessionId, String content, String type) {
ChatEntity entity = new ChatEntity();
entity.setId(UUID.randomUUID().toString());
entity.setContent(content);
entity.setSessionId(sessionId);
entity.setType(type);
entity.setTime(new Date());
//改成常量
entity.setBeDeleted("N");
if(MessageType.ASSISTANT.getValue().equals(type)){
entity.setUserChatId(getLastN(sessionId, 1).get(0).getId());
}
//todo 儲存資料庫
//模擬儲存到資料庫
List<ChatEntity> list = map.getOrDefault(sessionId, new ArrayList<>());
list.add(entity);
map.put(sessionId, list);
}
@Override
public List<ChatEntity> getLastN(String sessionId, Integer lastN) {
//todo 從資料庫獲取
//模擬從資料庫獲取
List<ChatEntity> list = map.get(sessionId);
return list != null ? list.stream().skip(Math.max(0, list.size() - lastN)).toList() : List.of();
}
@Override
public void clear(String sessionId) {
//todo 資料庫更新 beDeleted 欄位
map.put(sessionId, new ArrayList<>());
}
@Override
public void deleteById(String id) {
//todo 資料庫直接將該 id 資料 beDeleted 改成 Y
for (Map.Entry<String, List<ChatEntity>> next : map.entrySet()) {
List<ChatEntity> list = next.getValue();
list.removeIf(chat -> id.equals(chat.getId()) || id.equals(chat.getUserChatId()));
}
}
}
controller
import com.yb.chatai.configuration.DBMemory;
import com.yb.chatai.domain.ChatEntity;
import com.yb.chatai.domain.ChatParam;
import com.yb.chatai.service.IChatService;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
/*
*@title ChatController2
*@description 使用資料庫(快取)進行對話
*@author yb
*@version 1.0
*@create 2024/11/12 16:12
*/
@Controller
public class ChatController2 {
//注入模型,配置檔案中的模型,或者可以在方法中指定模型
@Resource
private OllamaChatModel model;
//聊天 client
private ChatClient chatClient;
//操作聊天資訊service
@Resource
private IChatService chatService;
//會話儲存方式
@Resource
private DBMemory dbMemory;
//開始聊天,生成唯一 sessionId
@GetMapping("/start2")
public String start(Model model){
//新建聊天模型
// OllamaOptions options = OllamaOptions.builder();
// options.setModel("qwen2.5:14b");
// OllamaChatModel chatModel = new OllamaChatModel(new OllamaApi(), options);
//建立隨機會話 ID
String sessionId = UUID.randomUUID().toString();
model.addAttribute("sessionId", sessionId);
//建立聊天 client
chatClient = ChatClient.builder(this.model).defaultAdvisors(new MessageChatMemoryAdvisor(dbMemory, sessionId, 10)).build();
return "chatPage2";
}
//切換會話,需要傳入 sessionId
@GetMapping("/exchange2/{id}")
public String exchange(@PathVariable("id")String sessionId){
//切換聊天 client
chatClient = ChatClient.builder(this.model).defaultAdvisors(new MessageChatMemoryAdvisor(dbMemory, sessionId, 10)).build();
return "chatPage2";
}
//聊天
@PostMapping("/chat2")
@ResponseBody
public List<ChatEntity> chat(@RequestBody ChatParam param){
//todo 判斷 AI 是否返回會話,從而判斷使用者是否可以輸入
chatClient.prompt(param.getUserMsg()).call().content();
//獲取返回最新兩條,一條使用者問題(使用者獲取使用者傳送ID),一條 AI 返回結果
return chatService.getLastN(param.getSessionId(), 2);
}
//撤回訊息
@DeleteMapping("/revoke2/{id}")
@ResponseBody
public void revoke(@PathVariable("id") String id){
chatService.deleteById(id);
}
//清空訊息
@DeleteMapping("/del2/{id}")
@ResponseBody
public void clear(@PathVariable("id") String sessionId){
dbMemory.clear(sessionId);
}
}
效果圖
總結
主要實現 org.springframework.ai.chat.memory.ChatMemory 方法,實際專案過程需要實現該介面重寫方法。