自学内容网 自学内容网

接上回,AI外挂知识库

SpringAi整合大模型(进阶版)_springai会话记忆功能-CSDN博客
这里使用最方便扩展的文本格式txt文档,作为外扩知识库的来源。

扩展方向有很多

如使用服务器硬盘存储,云端文件存储,数据库DB存储等等。

主要思路:

  1. 增加管理员API,用于对知识库的新增和更新。
  2. 让ai在回答问题前先参考外挂的知识库进行回答
  3. 需保留之前的历史记录等功能特性。

1,增加管理员API

package org.example.springaidemo.controller;

import org.example.springaidemo.config.MychatMemory;
import org.example.springaidemo.config.knowledge.KnowledgeBase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/admin/knowledge")
public class AdminController {

    @Autowired
    private MychatMemory mychatMemory;
    
    @Autowired
    @Qualifier("textKnowledgeBase")
    private KnowledgeBase textKnowledgeBase;

    // 添加文本知识
    @PostMapping("/text")
    public ResponseEntity<String> addTextKnowledge(@RequestBody Map<String, String> request) {
        String content = request.get("content");
        if (content == null || content.trim().isEmpty()) {
            return ResponseEntity.badRequest().body("内容不能为空");
        }
        
        try {
            mychatMemory.addToTextKnowledge(content.trim());
            return ResponseEntity.ok("添加成功");
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body("添加失败:" + e.getMessage());
        }
    }

    // 搜索文本知识
    @GetMapping("/text/search")
    public ResponseEntity<?> searchTextKnowledge(@RequestParam String query) {
        try {
            List<String> results = mychatMemory.searchTextKnowledge(query);
            return ResponseEntity.ok(results);
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body("搜索失败:" + e.getMessage());
        }
    }

    // 修改文本知识
    @PutMapping("/text")
    public ResponseEntity<String> updateTextKnowledge(
            @RequestBody Map<String, String> request) {
        String oldContent = request.get("oldContent");
        String newContent = request.get("newContent");
        
        if (oldContent == null || newContent == null || 
            oldContent.trim().isEmpty() || newContent.trim().isEmpty()) {
            return ResponseEntity.badRequest().body("原内容和新内容都不能为空");
        }
        
        try {
            boolean updated = textKnowledgeBase.updateKnowledge(oldContent.trim(), newContent.trim());
            return updated ? 
                ResponseEntity.ok("修改成功") : 
                ResponseEntity.notFound().build();
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body("修改失败:" + e.getMessage());
        }
    }
} 
package org.example.springaidemo.config;

import org.example.springaidemo.config.knowledge.KnowledgeBase;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 聊天过程中会自动给记录,我们重写了记录的方法,就是让按照我们的设想进行历史记录的保存
 * 0 利用 Spring AI Chat 框架,通过会话 ID(token)管理不同用户的上下文和对话。
 */
@Component
public class MychatMemory implements ChatMemory {

    Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();

    @Autowired
    @Qualifier("textKnowledgeBase")
    private KnowledgeBase textKnowledgeBase;

    @Override
    public void add(String conversationId, List<Message> messages) {
        this.conversationHistory.computeIfAbsent(conversationId, id -> Collections.synchronizedList(new ArrayList<>()))
                .addAll(messages);
    }

    @Override
    public void add(String conversationId, Message message) {
        this.conversationHistory.computeIfAbsent(conversationId, id -> Collections.synchronizedList(new ArrayList<>()))
                .add(message);
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {
        List<Message> allMessages = conversationHistory.get(conversationId);
        if (allMessages == null || allMessages.isEmpty()) {
            return List.of(); // 如果没有历史记录,返回空列表
        }

        // 计算获取的起始位置
        int start = Math.max(0, allMessages.size() - lastN);
        return new ArrayList<>(allMessages.subList(start, allMessages.size())); // 返回一个新列表,避免外部修改

    }

    @Override
    public void clear(String conversationId) {
        conversationHistory.remove(conversationId); // 移除该会话的历史记录
    }

    public void addToTextKnowledge(String content) {
        textKnowledgeBase.addKnowledge(content);
    }

    public List<String> searchTextKnowledge(String query) {
        return textKnowledgeBase.searchKnowledge(query);
    }

    public boolean updateTextKnowledge(String oldContent, String newContent) {
        return textKnowledgeBase.updateKnowledge(oldContent, newContent);
    }
}
package org.example.springaidemo.config.knowledge;

import java.util.List;

public interface KnowledgeBase {
    void addKnowledge(String content);
    List<String> searchKnowledge(String query);
    boolean updateKnowledge(String oldContent, String newContent);
} 
package org.example.springaidemo.config.knowledge;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.*;
import java.nio.file.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

@Component
public class TextKnowledgeBase implements KnowledgeBase {
    private final List<String> textKnowledge = new CopyOnWriteArrayList<>();
    
    @Value("${knowledge.text.base-path:knowledge}")
    private String basePath;
    
    @Value("${knowledge.text.file-name:knowledge.txt}")
    private String fileName;
    
    private Path getFilePath() {
        try {
            Path directory = Paths.get(basePath);
            if (!Files.exists(directory)) {
                Files.createDirectories(directory);
            }
            Path filePath = directory.resolve(fileName);
            if (!Files.exists(filePath)) {
                Files.createFile(filePath);
            }
            return filePath;
        } catch (IOException e) {
            throw new RuntimeException("无法创建知识库文件", e);
        }
    }

    @Override
    public void addKnowledge(String content) {
        textKnowledge.add(content);
        // 将新知识写入文件
        try {
            Files.write(getFilePath(), 
                       (content + System.lineSeparator()).getBytes(), 
                       StandardOpenOption.APPEND);
        } catch (IOException e) {
            throw new RuntimeException("写入知识库失败", e);
        }
    }

    @Override
    public List<String> searchKnowledge(String query) {
        try {
            // 从文件读取所有内容               ·
            return Files.readAllLines(getFilePath());
        } catch (IOException e) {
            throw new RuntimeException("读取知识库失败", e);
        }
    }

    @Override
    public boolean updateKnowledge(String oldContent, String newContent) {
        try {
            Path filePath = getFilePath();
            List<String> lines = Files.readAllLines(filePath);
            
            boolean found = false;
            for (int i = 0; i < lines.size(); i++) {
                if (lines.get(i).equals(oldContent)) {
                    lines.set(i, newContent);
                    found = true;
                }
            }
            
            if (found) {
                // 更新文件
                Files.write(filePath, lines, StandardOpenOption.TRUNCATE_EXISTING);
                
                // 更新内存中的数据
                textKnowledge.removeIf(content -> content.equals(oldContent));
                textKnowledge.add(newContent);
                
                return true;
            }
            
            return false;
        } catch (IOException e) {
            throw new RuntimeException("更新知识库失败", e);
        }
    }
} 

2,让ai在回答问题前先参考外挂的知识库进行回答

package org.example.springaidemo.impl;

import org.example.springaidemo.config.AiMyFunction;
import org.example.springaidemo.config.knowledge.TextKnowledgeBase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SimpleControllerImpl {

    @Autowired
    private AiMyFunction aiMyFunction;

    @Autowired
    private TextKnowledgeBase textKnowledgeBase;

    public String chat(String message, String token) {
        // 首先搜索外挂知识库
        List<String> knowledgeResults = textKnowledgeBase.searchKnowledge(message);

        // 构建提示词
        StringBuilder promptBuilder = new StringBuilder();
        if (!knowledgeResults.isEmpty()) {
            promptBuilder.append("根据以下参考知识回答问题:\n\n");
            for (String knowledge : knowledgeResults) {
                promptBuilder.append("- ").append(knowledge).append("\n");
            }
            promptBuilder.append("\n问题是:").append(message);
        } else {
            promptBuilder.append(message);
        }

        // 调用AI接口获取回答
        String aiResponse = aiMyFunction.generate(promptBuilder.toString(), token);

        return aiResponse;
    }

    public Double priceAll(int count) {
        double price = 3.25;
        double re = price * count;
        System.out.println("打印这条内容,代表已经执行了priceAll该方法。");
        return re;
    }
}

3,保留之前的特性

package org.example.springaidemo.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;

@Component
public class AiMyFunction {
    // AI 对话客户端实例
    private final ChatClient client;

    // 自定义的对话存储实现,用于保存用户会话上下文
    private final MychatMemory mychatMemory;

    public AiMyFunction(ChatClient.Builder clientBuilder, MychatMemory mychatMemory) {
        this.mychatMemory = mychatMemory;
        // 初始化 ChatClient,并设置默认系统提示和对话存储
        this.client = clientBuilder.defaultSystem(
                        """
                你是一家名叫“Rojer”的淘宝客服。
                当用户语句中包含“退款”时,你需要回复“不好意思,本店不支持7天无理由退款”
                """
                )
                .defaultAdvisors(new PromptChatMemoryAdvisor(mychatMemory))
                .build();
    }

    /**
     * 根据外挂知识库,聊天历史记录搜索回答问题
     *
     * @param msg
     * @param token
     * @return
     */
    public String generate(String msg, String token) {
        String content = client.prompt()
                .user(msg) // 用户的输入
                .advisors(adv -> adv
                        // 设置检索的上下文记录条数
                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
                        // 指定会话唯一标识,用于区分不同的用户对话
                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, token))
                .call() // 调用 AI 服务,生成回复
                .content();// 获取生成的文本内容
        return content;
    }


    /**
     * 以流式方式生成基于用户消息和会话 token 的 AI 回复。
     * 适用于需要逐步接收回复内容的场景,例如聊天应用中的实时响应。
     *
     * @param msg   用户输入的消息
     * @param token 表示会话唯一标识,用于区分不同用户的上下文
     * @return Flux<String> 流式的回复内容
     */
    public Flux<String> generateStream(String msg, String token) {
        return this.client.prompt()
                .user(msg) // 用户的输入
                .advisors(adv -> adv
                        // 设置检索的上下文记录条数
                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
                        // 指定会话唯一标识,用于区分不同的用户对话
                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, token))
                .functions("getPrice")// 指定需要调用的功能
                .stream() // 以流式模式调用 AI 服务
                .content(); // 获取生成的文本流内容
    }
}

4,没钱不测了,穷!


原文地址:https://blog.csdn.net/weixin_54925172/article/details/145167573

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!