Spring AI + ollama 本地搭建聊天 AI

抱糖果彡發表於2024-11-13

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);
    }

}
效果圖

gif

二、採用資料庫儲存對話資訊

支援功能:聊天對話、切換對話、刪除對話、撤回訊息

實體類
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);
    }

}
效果圖

db

總結

主要實現 org.springframework.ai.chat.memory.ChatMemory 方法,實際專案過程需要實現該介面重寫方法。

相關文章