Я пытаюсь создать чат-робота с искусственным интеллектом, чтобы обогатить свое резюме. Фреймворк использует Spring AI, а векторная база данных использует стек Redis. Моя текущая идея заключается в том, что пользователи загружают такие документы, как PDF и Word. После фонового анализа и сохранения документ будет разрезан на части и сохранен в стеке Redis. Чтобы различать разные сеансы разных пользователей, каждый сеанс имеет идентификатор сеанса, привязанный к пользователям, и каждый документ также привязан к идентификатору сеанса, чтобы не путать. Чтобы отличить локальную базу знаний, я установил для sessionId локальной базы знаний значение -1.
Мой подход заключается в связывании во время векторизации и фильтрации во время запроса. Если пользователь говорит «только документ», идентификатор сеанса документа должен совпадать с идентификатором сеанса, переданным из внешнего интерфейса. В противном случае идентификатор сеанса документа совпадает с идентификатором сеанса, переданным из внешнего интерфейса, или идентификатор сеанса равен -1. Теперь я столкнулся с проблемой: я могу записать его в векторную базу данных или просмотреть в инструменте визуализации векторной базы данных, но фильтрация не может подействовать. Идентификатор сеанса имеет значение null. Ниже приведены мои коды синтаксического анализа документов и контроллера чата.
[spring log][1]
[Визуализация Redis][2]
[1]: https://i.sstatic.net/ApCTMi8J.png
[2]: https://i.sstatic.net/tCdBRhXy.png
@Override
public Long uploadAndParseDocument(MultipartFile file, Long sessionId) throws Exception { // 注意:此处抛出Exception(适配Tika的异常类型)
// 这里先获取真实的存储路径,而非返回的带前缀路径
String baseDir = FileUploadUtils.getDefaultBaseDir();
// 生成真实的文件名(和工具类内部逻辑一致:日期目录 + UUID + 后缀)
String fileName = FileUploadUtils.uuidFilename(file); // 直接生成真实文件名
// 生成真实存储的文件对象
File storedFile = FileUploadUtils.getAbsoluteFile(baseDir, fileName);
try {
// 执行文件保存
file.transferTo(storedFile.toPath());
} catch (Exception e) {
throw new Exception("文件上传失败:" + e.getMessage());
}
// 2. 上传后强制校验文件是否存在且有效
if (!storedFile.exists() || !storedFile.isFile() || storedFile.length() == 0) {
throw new Exception("文件上传失败,未找到有效文件:" + storedFile.getAbsolutePath());
}
// 3. 读取存储的文件并解析(此时路径正确,文件存在)
String textContent;
try {
textContent = documentParseUtil.parseDocument(storedFile);
} catch (Exception e) {
throw new Exception("文档解析失败:" + e.getMessage());
}
System.out.println("文本内容"+textContent);
String fileStoragePath = baseDir + fileName;
// 4. 插入文档记录到业务表,获取file_id(主键)
ParsedDocument parsedDoc = new ParsedDocument();
parsedDoc.setFileName(file.getOriginalFilename());
parsedDoc.setFileType(FileUploadUtils.getExtension(file)); // 获取文件后缀(.pdf/.docx等)
parsedDoc.setFileSize(file.getSize() / 1024 + "KB"); // 转换为KB显示
parsedDoc.setFileStoragePath(fileStoragePath); // 存储文件路径
parsedDoc.setParseTime(new Date()); // 解析完成时间
parsedDoc.setStatus("SUCCESS"); // 标记解析成功
parsedDoc.setCreateBy(SecurityUtils.getUserId().toString()); // 创建人(当前用户ID)
parsedDoc.setCreateTime(new Date()); // 记录创建时间
parsedDocumentMapper.insertParsedDocument(parsedDoc); // 插入数据库
Long documentId = parsedDoc.getFileId(); // 获取自增主键file_id
// 5. 文本分割(解决大文本Token超限问题)
TokenTextSplitter splitter = new TokenTextSplitter(); // Spring AI的文本分割器
Document originalDoc = new Document(textContent);
List documentList = splitter.split(originalDoc); // 分割为多个文档片段
// 6. 为每个片段添加元数据(用于向量库检索时过滤和显示)
documentList.forEach(doc -> {
doc.getMetadata().put("sessionId", sessionId.toString()); // 关键:绑定当前会话ID
doc.getMetadata().put("documentId", documentId.toString()); // 关联业务表的file_id
doc.getMetadata().put("fileName", parsedDoc.getFileName()); // 文件名(前端显示用)
doc.getMetadata().put("uploadTime", DateUtils.getDate()); // 上传时间
System.out.println("插入时的元数据:"+doc.getMetadata());
});
// 7. 存入向量库(供后续RAG检索使用)
vectorStore.add(documentList);
return documentId; // 返回文档ID,用于后续会话关联
}` @PreAuthorize("@ss.hasPermi('model:message:list')") // 恢复权限注解
@PostMapping("/chat")
public String chat(@RequestParam String msg, @RequestParam Long sessionId) {
Long userId = SecurityUtils.getUserId();
String systemInfo = """
你是一个小助手,可以回答任何问题。
""";
PromptTemplate customPrompt = getPromptTemplate();
// 1. 判断用户是否在询问“当前会话的文档”(需排除公共文档)
boolean isAskingSessionDocs = isAskingCurrentSessionDocs(msg);
// 2. 动态构建过滤条件
FilterExpressionBuilder b = new FilterExpressionBuilder();
Filter.Expression expression;
if (isAskingSessionDocs) {
// 场景1:仅当前会话的文档(排除公共文档)
expression = b.eq("sessionId", sessionId.toString()).build();
} else {
// 场景2:当前会话文档 + 公共文档
expression = b.or(
b.eq("sessionId", sessionId.toString()), b.eq("sessionId", "-1") // 公共文档(如ops.txt)
).build();
}
// RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
// .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).filterExpression(expression).
// build())
// .build();
// 3. 构建带过滤器的文档检索器
VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
// .filterExpression(expression)
.build();
Query query = new Query(msg);
// 4. 手动执行检索并打印结果(核心:打印检索到的文档信息)
List retrievedDocs = retriever.retrieve(query);
log.info("=== 检索结果日志 ===");
log.info("检索到的文档总数:{}", retrievedDocs.size());
// 打印每条文档的元数据(sessionId、文件名)和内容片段
for (int i = 0; i < retrievedDocs.size(); i++) {
Document doc = retrievedDocs.get(i);
log.info("文档{}:", i + 1);
log.info(" - 查询时元数据:{}", doc.getMetadata());
log.info(" - 元数据(sessionId):{}", doc.getMetadata().get("sessionId"));
log.info(" - 元数据(文件名):{}", doc.getMetadata().get("fileName"));
}
log.info("===================");
// 5. 构建Advisor时直接使用上面创建的retriever
RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(retriever) // 复用同一个实例
.build();
MessageChatMemoryAdvisor memoryAdvisor = MessageChatMemoryAdvisor.builder(windowChatMemory)
.conversationId(userId.toString())
.build();
//得到AI回复
String aiReply = chatClient.prompt().system(systemInfo)
.user(msg).advisors(memoryAdvisor, advisor).call().content();
//存储用户消息
ChatMessage userMessage = new ChatMessage();
userMessage.setSessionId(sessionId); // 关联当前会话
userMessage.setSenderType(0L); // 0=用户
userMessage.setMessageType(0L); // 0=文本
userMessage.setContent(msg);
userMessage.setSendTime(new Date());
userMessage.setStatus(1L); // 1=成功
chatMessageService.insertChatMessage(userMessage);
//存储AI消息
ChatMessage aiMessage = new ChatMessage();
aiMessage.setSessionId(sessionId); // 关联当前会话
aiMessage.setSenderType(1L); // 1=AI
aiMessage.setMessageType(0L); // 0=文本
aiMessage.setContent(aiReply);
aiMessage.setSendTime(new Date());
aiMessage.setStatus(1L); // 1=成功
chatMessageService.insertChatMessage(aiMessage);
// 返回完整AI回复给前端
return aiReply;
}
Подробнее здесь: https://stackoverflow.com/questions/798 ... -spring-ai
Проблема использования векторной базы данных Redis в Spring AI ⇐ JAVA
Программисты JAVA общаются здесь
-
Anonymous
1763445392
Anonymous
Я пытаюсь создать чат-робота с искусственным интеллектом, чтобы обогатить свое резюме. Фреймворк использует Spring AI, а векторная база данных использует стек Redis. Моя текущая идея заключается в том, что пользователи загружают такие документы, как PDF и Word. После фонового анализа и сохранения документ будет разрезан на части и сохранен в стеке Redis. Чтобы различать разные сеансы разных пользователей, каждый сеанс имеет идентификатор сеанса, привязанный к пользователям, и каждый документ также привязан к идентификатору сеанса, чтобы не путать. Чтобы отличить локальную базу знаний, я установил для sessionId локальной базы знаний значение -1.
Мой подход заключается в связывании во время векторизации и фильтрации во время запроса. Если пользователь говорит «только документ», идентификатор сеанса документа должен совпадать с идентификатором сеанса, переданным из внешнего интерфейса. В противном случае идентификатор сеанса документа совпадает с идентификатором сеанса, переданным из внешнего интерфейса, или идентификатор сеанса равен -1. Теперь я столкнулся с проблемой: я могу записать его в векторную базу данных или просмотреть в инструменте визуализации векторной базы данных, но фильтрация не может подействовать. Идентификатор сеанса имеет значение null. Ниже приведены мои коды синтаксического анализа документов и контроллера чата.
[spring log][1]
[Визуализация Redis][2]
[1]: https://i.sstatic.net/ApCTMi8J.png
[2]: https://i.sstatic.net/tCdBRhXy.png
@Override
public Long uploadAndParseDocument(MultipartFile file, Long sessionId) throws Exception { // 注意:此处抛出Exception(适配Tika的异常类型)
// 这里先获取真实的存储路径,而非返回的带前缀路径
String baseDir = FileUploadUtils.getDefaultBaseDir();
// 生成真实的文件名(和工具类内部逻辑一致:日期目录 + UUID + 后缀)
String fileName = FileUploadUtils.uuidFilename(file); // 直接生成真实文件名
// 生成真实存储的文件对象
File storedFile = FileUploadUtils.getAbsoluteFile(baseDir, fileName);
try {
// 执行文件保存
file.transferTo(storedFile.toPath());
} catch (Exception e) {
throw new Exception("文件上传失败:" + e.getMessage());
}
// 2. 上传后强制校验文件是否存在且有效
if (!storedFile.exists() || !storedFile.isFile() || storedFile.length() == 0) {
throw new Exception("文件上传失败,未找到有效文件:" + storedFile.getAbsolutePath());
}
// 3. 读取存储的文件并解析(此时路径正确,文件存在)
String textContent;
try {
textContent = documentParseUtil.parseDocument(storedFile);
} catch (Exception e) {
throw new Exception("文档解析失败:" + e.getMessage());
}
System.out.println("文本内容"+textContent);
String fileStoragePath = baseDir + fileName;
// 4. 插入文档记录到业务表,获取file_id(主键)
ParsedDocument parsedDoc = new ParsedDocument();
parsedDoc.setFileName(file.getOriginalFilename());
parsedDoc.setFileType(FileUploadUtils.getExtension(file)); // 获取文件后缀(.pdf/.docx等)
parsedDoc.setFileSize(file.getSize() / 1024 + "KB"); // 转换为KB显示
parsedDoc.setFileStoragePath(fileStoragePath); // 存储文件路径
parsedDoc.setParseTime(new Date()); // 解析完成时间
parsedDoc.setStatus("SUCCESS"); // 标记解析成功
parsedDoc.setCreateBy(SecurityUtils.getUserId().toString()); // 创建人(当前用户ID)
parsedDoc.setCreateTime(new Date()); // 记录创建时间
parsedDocumentMapper.insertParsedDocument(parsedDoc); // 插入数据库
Long documentId = parsedDoc.getFileId(); // 获取自增主键file_id
// 5. 文本分割(解决大文本Token超限问题)
TokenTextSplitter splitter = new TokenTextSplitter(); // Spring AI的文本分割器
Document originalDoc = new Document(textContent);
List documentList = splitter.split(originalDoc); // 分割为多个文档片段
// 6. 为每个片段添加元数据(用于向量库检索时过滤和显示)
documentList.forEach(doc -> {
doc.getMetadata().put("sessionId", sessionId.toString()); // 关键:绑定当前会话ID
doc.getMetadata().put("documentId", documentId.toString()); // 关联业务表的file_id
doc.getMetadata().put("fileName", parsedDoc.getFileName()); // 文件名(前端显示用)
doc.getMetadata().put("uploadTime", DateUtils.getDate()); // 上传时间
System.out.println("插入时的元数据:"+doc.getMetadata());
});
// 7. 存入向量库(供后续RAG检索使用)
vectorStore.add(documentList);
return documentId; // 返回文档ID,用于后续会话关联
}` @PreAuthorize("@ss.hasPermi('model:message:list')") // 恢复权限注解
@PostMapping("/chat")
public String chat(@RequestParam String msg, @RequestParam Long sessionId) {
Long userId = SecurityUtils.getUserId();
String systemInfo = """
你是一个小助手,可以回答任何问题。
""";
PromptTemplate customPrompt = getPromptTemplate();
// 1. 判断用户是否在询问“当前会话的文档”(需排除公共文档)
boolean isAskingSessionDocs = isAskingCurrentSessionDocs(msg);
// 2. 动态构建过滤条件
FilterExpressionBuilder b = new FilterExpressionBuilder();
Filter.Expression expression;
if (isAskingSessionDocs) {
// 场景1:仅当前会话的文档(排除公共文档)
expression = b.eq("sessionId", sessionId.toString()).build();
} else {
// 场景2:当前会话文档 + 公共文档
expression = b.or(
b.eq("sessionId", sessionId.toString()), b.eq("sessionId", "-1") // 公共文档(如ops.txt)
).build();
}
// RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
// .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).filterExpression(expression).
// build())
// .build();
// 3. 构建带过滤器的文档检索器
VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
// .filterExpression(expression)
.build();
Query query = new Query(msg);
// 4. 手动执行检索并打印结果(核心:打印检索到的文档信息)
List retrievedDocs = retriever.retrieve(query);
log.info("=== 检索结果日志 ===");
log.info("检索到的文档总数:{}", retrievedDocs.size());
// 打印每条文档的元数据(sessionId、文件名)和内容片段
for (int i = 0; i < retrievedDocs.size(); i++) {
Document doc = retrievedDocs.get(i);
log.info("文档{}:", i + 1);
log.info(" - 查询时元数据:{}", doc.getMetadata());
log.info(" - 元数据(sessionId):{}", doc.getMetadata().get("sessionId"));
log.info(" - 元数据(文件名):{}", doc.getMetadata().get("fileName"));
}
log.info("===================");
// 5. 构建Advisor时直接使用上面创建的retriever
RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(retriever) // 复用同一个实例
.build();
MessageChatMemoryAdvisor memoryAdvisor = MessageChatMemoryAdvisor.builder(windowChatMemory)
.conversationId(userId.toString())
.build();
//得到AI回复
String aiReply = chatClient.prompt().system(systemInfo)
.user(msg).advisors(memoryAdvisor, advisor).call().content();
//存储用户消息
ChatMessage userMessage = new ChatMessage();
userMessage.setSessionId(sessionId); // 关联当前会话
userMessage.setSenderType(0L); // 0=用户
userMessage.setMessageType(0L); // 0=文本
userMessage.setContent(msg);
userMessage.setSendTime(new Date());
userMessage.setStatus(1L); // 1=成功
chatMessageService.insertChatMessage(userMessage);
//存储AI消息
ChatMessage aiMessage = new ChatMessage();
aiMessage.setSessionId(sessionId); // 关联当前会话
aiMessage.setSenderType(1L); // 1=AI
aiMessage.setMessageType(0L); // 0=文本
aiMessage.setContent(aiReply);
aiMessage.setSendTime(new Date());
aiMessage.setStatus(1L); // 1=成功
chatMessageService.insertChatMessage(aiMessage);
// 返回完整AI回复给前端
return aiReply;
}
Подробнее здесь: [url]https://stackoverflow.com/questions/79823018/the-problem-of-using-redis-stack-vector-database-in-spring-ai[/url]
Ответить
1 сообщение
• Страница 1 из 1
Перейти
- Кемерово-IT
- ↳ Javascript
- ↳ C#
- ↳ JAVA
- ↳ Elasticsearch aggregation
- ↳ Python
- ↳ Php
- ↳ Android
- ↳ Html
- ↳ Jquery
- ↳ C++
- ↳ IOS
- ↳ CSS
- ↳ Excel
- ↳ Linux
- ↳ Apache
- ↳ MySql
- Детский мир
- Для души
- ↳ Музыкальные инструменты даром
- ↳ Печатная продукция даром
- Внешняя красота и здоровье
- ↳ Одежда и обувь для взрослых даром
- ↳ Товары для здоровья
- ↳ Физкультура и спорт
- Техника - даром!
- ↳ Автомобилистам
- ↳ Компьютерная техника
- ↳ Плиты: газовые и электрические
- ↳ Холодильники
- ↳ Стиральные машины
- ↳ Телевизоры
- ↳ Телефоны, смартфоны, плашеты
- ↳ Швейные машинки
- ↳ Прочая электроника и техника
- ↳ Фототехника
- Ремонт и интерьер
- ↳ Стройматериалы, инструмент
- ↳ Мебель и предметы интерьера даром
- ↳ Cантехника
- Другие темы
- ↳ Разное даром
- ↳ Давай меняться!
- ↳ Отдам\возьму за копеечку
- ↳ Работа и подработка в Кемерове
- ↳ Давай с тобой поговорим...
Мобильная версия