接上回,AI外挂知识库
SpringAi整合大模型(进阶版)_springai会话记忆功能-CSDN博客
这里使用最方便扩展的文本格式txt文档,作为外扩知识库的来源。
扩展方向有很多
如使用服务器硬盘存储,云端文件存储,数据库DB存储等等。
主要思路:
- 增加管理员API,用于对知识库的新增和更新。
- 让ai在回答问题前先参考外挂的知识库进行回答
- 需保留之前的历史记录等功能特性。
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)!