mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-13 11:53:48 +00:00
refactor: 重构项目架构,优化向量服务
- 移除 Graph 知识图谱相关模块(Neo4j、GraphRAG等) - 移除 demo、job、wechat 示例模块,简化项目结构 - 修复向量维度获取方式,改为从数据库配置读取 - 添加 gRPC BOM 依赖管理,解决 Milvus SDK 版本冲突 - 新增 PPIO 服务和 Embedding 提供者支持 - 清理冗余代码和未使用的依赖 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -148,7 +148,6 @@ public class AgentChatHandler implements ChatHandler {
|
||||
messageBO.setContent(content);
|
||||
messageBO.setRole(role);
|
||||
messageBO.setModelName(chatRequest.getModel());
|
||||
messageBO.setBillingType(chatModelVo.getModelType());
|
||||
messageBO.setRemark(null);
|
||||
|
||||
chatMessageService.insertByBo(messageBO);
|
||||
|
||||
@@ -79,10 +79,8 @@ public class ChatMessageServiceImpl implements IChatMessageService {
|
||||
lqw.eq(bo.getUserId() != null, ChatMessage::getUserId, bo.getUserId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getContent()), ChatMessage::getContent, bo.getContent());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getRole()), ChatMessage::getRole, bo.getRole());
|
||||
lqw.eq(bo.getDeductCost() != null, ChatMessage::getDeductCost, bo.getDeductCost());
|
||||
lqw.eq(bo.getTotalTokens() != null, ChatMessage::getTotalTokens, bo.getTotalTokens());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getModelName()), ChatMessage::getModelName, bo.getModelName());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getBillingType()), ChatMessage::getBillingType, bo.getBillingType());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,11 +92,7 @@ public class ChatModelServiceImpl implements IChatModelService {
|
||||
lqw.like(StringUtils.isNotBlank(bo.getModelName()), ChatModel::getModelName, bo.getModelName());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getProviderCode()), ChatModel::getProviderCode, bo.getProviderCode());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelDescribe()), ChatModel::getModelDescribe, bo.getModelDescribe());
|
||||
lqw.eq(bo.getModelPrice() != null, ChatModel::getModelPrice, bo.getModelPrice());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelType()), ChatModel::getModelType, bo.getModelType());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelShow()), ChatModel::getModelShow, bo.getModelShow());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelFree()), ChatModel::getModelFree, bo.getModelFree());
|
||||
lqw.eq(bo.getPriority() != null, ChatModel::getPriority, bo.getPriority());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getApiHost()), ChatModel::getApiHost, bo.getApiHost());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getApiKey()), ChatModel::getApiKey, bo.getApiKey());
|
||||
return lqw;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* OPENAI服务调用
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class PPIOServiceImpl extends AbstractStreamingChatService {
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
return OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.returnThinking(chatRequest.getEnableThinking())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List<ChatMessage> messagesWithMemory, StreamingChatResponseHandler handler) {
|
||||
StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
streamingChatModel.chat(messagesWithMemory, handler);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.PPIO.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class AliBaiLianBaseEmbedProvider extends OpenAiEmbeddingProvider {
|
||||
return QwenEmbeddingModel.builder()
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.dimension(1024)
|
||||
.dimension(chatModelVo.getModelDimension())
|
||||
.build()
|
||||
.embedAll(textSegments);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public class OpenAiEmbeddingProvider implements BaseEmbedModelService {
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.dimensions(chatModelVo.getDimension())
|
||||
.dimensions(chatModelVo.getModelDimension())
|
||||
.build()
|
||||
.embedAll(textSegments);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.ruoyi.service.embed.impl;
|
||||
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @Author: Robust_H
|
||||
* @Date: 2025-09-30-下午3:59
|
||||
* @Description: 硅基流动(兼容 OpenAi)
|
||||
*/
|
||||
@Component("ppio")
|
||||
public class PPIOEmbeddingProvider extends OpenAiEmbeddingProvider {
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//package org.ruoyi.service.embed.impl;
|
||||
//
|
||||
//import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel;
|
||||
//import dev.langchain4j.data.embedding.Embedding;
|
||||
//import dev.langchain4j.data.segment.TextSegment;
|
||||
//import dev.langchain4j.model.output.Response;
|
||||
//import org.ruoyi.domain.vo.chat.ChatModelVo;
|
||||
//import org.ruoyi.enums.ModalityType;
|
||||
//import org.ruoyi.service.embed.BaseEmbedModelService;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//
|
||||
//import java.util.List;
|
||||
//import java.util.Set;
|
||||
//
|
||||
///**
|
||||
// * @Author: Robust_H
|
||||
// * @Date: 2025-09-30-下午4:02
|
||||
// * @Description: 智谱AI
|
||||
// */
|
||||
//@Component("zhipu")
|
||||
//public class ZhiPuAiEmbeddingProvider implements BaseEmbedModelService {
|
||||
// private ChatModelVo chatModelVo;
|
||||
//
|
||||
// @Override
|
||||
// public void configure(ChatModelVo config) {
|
||||
// this.chatModelVo = config;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Set<ModalityType> getSupportedModalities() {
|
||||
// return Set.of();
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
||||
// return ZhipuAiEmbeddingModel.builder()
|
||||
// .baseUrl(chatModelVo.getApiHost())
|
||||
// .apiKey(chatModelVo.getApiKey())
|
||||
// .model(chatModelVo.getModelName())
|
||||
// .dimensions(chatModelVo.getDimension())
|
||||
// .build()
|
||||
// .embedAll(textSegments);
|
||||
// }
|
||||
//}
|
||||
@@ -1,136 +0,0 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
|
||||
import org.ruoyi.domain.bo.graph.GraphBuildTask;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图谱构建任务服务接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphBuildTaskService {
|
||||
|
||||
/**
|
||||
* 创建构建任务
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param docId 文档ID(可选)
|
||||
* @param taskType 任务类型
|
||||
* @return 任务信息
|
||||
*/
|
||||
GraphBuildTask createTask(String graphUuid, String knowledgeId, String docId, Integer taskType);
|
||||
|
||||
/**
|
||||
* 启动构建任务(异步)
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @return 异步结果
|
||||
*/
|
||||
void startTask(String taskUuid);
|
||||
|
||||
/**
|
||||
* 根据UUID获取任务
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @return 任务信息
|
||||
*/
|
||||
GraphBuildTask getByUuid(String taskUuid);
|
||||
|
||||
/**
|
||||
* 根据图谱UUID获取任务列表
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 任务列表
|
||||
*/
|
||||
List<GraphBuildTask> listByGraphUuid(String graphUuid);
|
||||
|
||||
/**
|
||||
* 获取图谱的最新构建任务
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 最新任务
|
||||
*/
|
||||
GraphBuildTask getLatestTask(String graphUuid);
|
||||
|
||||
/**
|
||||
* 根据知识库ID获取任务列表
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 任务列表
|
||||
*/
|
||||
List<GraphBuildTask> listByKnowledgeId(String knowledgeId);
|
||||
|
||||
/**
|
||||
* 获取待执行和执行中的任务
|
||||
*
|
||||
* @return 任务列表
|
||||
*/
|
||||
List<GraphBuildTask> getPendingAndRunningTasks();
|
||||
|
||||
/**
|
||||
* 更新任务进度
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param progress 进度百分比
|
||||
* @param processedDocs 已处理文档数
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateProgress(String taskUuid, Integer progress, Integer processedDocs);
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param status 状态
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateStatus(String taskUuid, Integer status);
|
||||
|
||||
/**
|
||||
* 更新提取统计信息
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param extractedEntities 提取的实体数
|
||||
* @param extractedRelations 提取的关系数
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateExtractionStats(String taskUuid, Integer extractedEntities, Integer extractedRelations);
|
||||
|
||||
/**
|
||||
* 标记任务为成功
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param resultSummary 结果摘要
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean markSuccess(String taskUuid, String resultSummary);
|
||||
|
||||
/**
|
||||
* 标记任务为失败
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @param errorMessage 错误信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean markFailed(String taskUuid, String errorMessage);
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean cancelTask(String taskUuid);
|
||||
|
||||
/**
|
||||
* 重试失败的任务
|
||||
*
|
||||
* @param taskUuid 任务UUID
|
||||
* @return 新任务UUID
|
||||
*/
|
||||
String retryTask(String taskUuid);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
|
||||
import org.ruoyi.domain.dto.GraphExtractionResult;
|
||||
|
||||
/**
|
||||
* 图谱实体关系抽取服务接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphExtractionService {
|
||||
|
||||
/**
|
||||
* 从文本中抽取实体和关系
|
||||
*
|
||||
* @param text 输入文本
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult extractFromText(String text);
|
||||
|
||||
/**
|
||||
* 从文本中抽取实体和关系(自定义实体类型)
|
||||
*
|
||||
* @param text 输入文本
|
||||
* @param entityTypes 实体类型列表
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult extractFromText(String text, String[] entityTypes);
|
||||
|
||||
/**
|
||||
* 从文本中抽取实体和关系(使用指定的LLM模型)
|
||||
*
|
||||
* @param text 输入文本
|
||||
* @param modelName LLM模型名称
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult extractFromTextWithModel(String text, String modelName);
|
||||
|
||||
/**
|
||||
* 解析LLM响应为实体和关系
|
||||
*
|
||||
* @param response LLM响应文本
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult parseGraphResponse(String response);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.ruoyi.domain.bo.graph.GraphInstance;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图谱实例服务接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphInstanceService {
|
||||
|
||||
/**
|
||||
* 创建图谱实例
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param graphName 图谱名称
|
||||
* @param config 配置信息
|
||||
* @return 图谱实例
|
||||
*/
|
||||
GraphInstance createInstance(String knowledgeId, String graphName, String config);
|
||||
|
||||
/**
|
||||
* 根据主键ID获取图谱实例
|
||||
*
|
||||
* @param id 主键ID
|
||||
* @return 图谱实例
|
||||
*/
|
||||
GraphInstance getById(Long id);
|
||||
|
||||
/**
|
||||
* 根据UUID获取图谱实例
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 图谱实例
|
||||
*/
|
||||
GraphInstance getByUuid(String graphUuid);
|
||||
|
||||
/**
|
||||
* 更新图谱实例
|
||||
*
|
||||
* @param instance 图谱实例
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateInstance(GraphInstance instance);
|
||||
|
||||
/**
|
||||
* 根据知识库ID获取图谱列表
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 图谱实例列表
|
||||
*/
|
||||
List<GraphInstance> listByKnowledgeId(String knowledgeId);
|
||||
|
||||
/**
|
||||
* 条件查询图谱实例列表(分页)
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param instanceName 图谱名称(模糊查询)
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param graphStatus 图谱状态码
|
||||
* @return 分页结果
|
||||
*/
|
||||
Page<GraphInstance> queryPage(Page<GraphInstance> page, String instanceName, String knowledgeId, Integer graphStatus);
|
||||
|
||||
/**
|
||||
* 更新图谱状态
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param status 状态
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateStatus(String graphUuid, Integer status);
|
||||
|
||||
/**
|
||||
* 更新图谱统计信息
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param nodeCount 节点数量
|
||||
* @param relationshipCount 关系数量
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateCounts(String graphUuid, Integer nodeCount, Integer relationshipCount);
|
||||
|
||||
/**
|
||||
* 更新图谱配置
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param config 配置信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateConfig(String graphUuid, String config);
|
||||
|
||||
/**
|
||||
* 删除图谱实例(软删除)
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteInstance(String graphUuid);
|
||||
|
||||
/**
|
||||
* 物理删除图谱实例及其数据
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteInstanceAndData(String graphUuid);
|
||||
|
||||
/**
|
||||
* 获取图谱统计信息
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 统计信息
|
||||
*/
|
||||
java.util.Map<String, Object> getStatistics(String graphUuid);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
|
||||
/**
|
||||
* 图谱LLM服务接口
|
||||
* 参考 ruoyi-chat 的 IChatService 设计
|
||||
* 支持多种LLM模型(OpenAI、Qwen、Zhipu等)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-10-11
|
||||
*/
|
||||
public interface IGraphLLMService {
|
||||
|
||||
/**
|
||||
* 调用LLM进行图谱实体关系抽取
|
||||
*
|
||||
* @param prompt 提示词(包含文本和抽取指令)
|
||||
* @param chatModel 模型配置
|
||||
* @return LLM响应文本
|
||||
*/
|
||||
String extractGraph(String prompt, ChatModelVo chatModel);
|
||||
|
||||
/**
|
||||
* 获取此服务支持的模型类别
|
||||
* 例如: "openai", "qwen", "zhipu", "ollama"
|
||||
*
|
||||
* @return 模型类别标识
|
||||
*/
|
||||
String getCategory();
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
import org.ruoyi.domain.dto.GraphExtractionResult;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GraphRAG服务接口
|
||||
* 负责文档的图谱化处理和基于图谱的检索
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphRAGService {
|
||||
|
||||
/**
|
||||
* 将文本入库到图谱
|
||||
*
|
||||
* @param text 文本内容
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult ingestText(String text, String knowledgeId, Map<String, Object> metadata);
|
||||
|
||||
/**
|
||||
* 将文本入库到图谱(指定模型)
|
||||
*
|
||||
* @param text 文本内容
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @param modelName LLM模型名称
|
||||
* @return 抽取结果
|
||||
*/
|
||||
GraphExtractionResult ingestTextWithModel(String text, String knowledgeId, Map<String, Object> metadata, String modelName);
|
||||
|
||||
/**
|
||||
* 将文档入库到图谱(自动分片)
|
||||
*
|
||||
* @param documentText 文档内容
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @return 总抽取结果(合并所有分片)
|
||||
*/
|
||||
GraphExtractionResult ingestDocument(String documentText, String knowledgeId, Map<String, Object> metadata);
|
||||
|
||||
/**
|
||||
* 将文档入库到图谱(自动分片,指定模型)
|
||||
*
|
||||
* @param documentText 文档内容
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @param modelName LLM模型名称
|
||||
* @return 总抽取结果(合并所有分片)
|
||||
*/
|
||||
GraphExtractionResult ingestDocumentWithModel(String documentText, String knowledgeId, Map<String, Object> metadata, String modelName);
|
||||
|
||||
/**
|
||||
* 基于图谱检索相关内容
|
||||
*
|
||||
* @param query 查询文本
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param maxResults 最大结果数
|
||||
* @return 检索到的相关实体和关系
|
||||
*/
|
||||
String retrieveFromGraph(String query, String knowledgeId, int maxResults);
|
||||
|
||||
/**
|
||||
* 删除知识库的图谱数据
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteGraphData(String knowledgeId);
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package org.ruoyi.service.graph;
|
||||
|
||||
|
||||
import org.ruoyi.domain.bo.graph.GraphEdge;
|
||||
import org.ruoyi.domain.bo.graph.GraphVertex;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图存储服务接口
|
||||
* 核心服务:负责与Neo4j图数据库交互
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
public interface IGraphStoreService {
|
||||
|
||||
// ==================== 节点操作 ====================
|
||||
|
||||
/**
|
||||
* 添加单个节点
|
||||
*
|
||||
* @param vertex 节点信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean addVertex(GraphVertex vertex);
|
||||
|
||||
/**
|
||||
* 批量添加节点
|
||||
*
|
||||
* @param vertices 节点列表
|
||||
* @return 成功添加的节点数
|
||||
*/
|
||||
int addVertices(List<GraphVertex> vertices);
|
||||
|
||||
/**
|
||||
* 获取节点信息
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 节点信息
|
||||
*/
|
||||
GraphVertex getVertex(String nodeId, String graphUuid);
|
||||
|
||||
/**
|
||||
* 根据条件搜索节点
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param label 节点标签(可选)
|
||||
* @param limit 返回数量限制
|
||||
* @return 节点列表
|
||||
*/
|
||||
List<GraphVertex> searchVertices(String graphUuid, String label, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据名称搜索节点
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param name 节点名称
|
||||
* @return 节点列表
|
||||
*/
|
||||
List<GraphVertex> searchVerticesByName(String graphUuid, String name);
|
||||
|
||||
/**
|
||||
* 根据关键词和知识库ID搜索节点
|
||||
*
|
||||
* @param keyword 关键词
|
||||
* @param knowledgeId 知识库ID(可选)
|
||||
* @param limit 限制数量
|
||||
* @return 节点列表
|
||||
*/
|
||||
List<GraphVertex> searchVerticesByName(String keyword, String knowledgeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据知识库ID查询节点
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param limit 限制数量
|
||||
* @return 节点列表
|
||||
*/
|
||||
List<GraphVertex> queryVerticesByKnowledgeId(String knowledgeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 更新节点信息
|
||||
*
|
||||
* @param vertex 节点信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateVertex(GraphVertex vertex);
|
||||
|
||||
/**
|
||||
* 删除节点
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param includeEdges 是否同时删除相关关系
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteVertex(String nodeId, String graphUuid, boolean includeEdges);
|
||||
|
||||
// ==================== 关系操作 ====================
|
||||
|
||||
/**
|
||||
* 添加关系
|
||||
*
|
||||
* @param edge 关系信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean addEdge(GraphEdge edge);
|
||||
|
||||
/**
|
||||
* 批量添加关系
|
||||
*
|
||||
* @param edges 关系列表
|
||||
* @return 成功添加的关系数
|
||||
*/
|
||||
int addEdges(List<GraphEdge> edges);
|
||||
|
||||
/**
|
||||
* 获取关系信息
|
||||
*
|
||||
* @param edgeId 关系ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 关系信息
|
||||
*/
|
||||
GraphEdge getEdge(String edgeId, String graphUuid);
|
||||
|
||||
/**
|
||||
* 搜索关系
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param sourceNodeId 源节点ID(可选)
|
||||
* @param targetNodeId 目标节点ID(可选)
|
||||
* @param limit 返回数量限制
|
||||
* @return 关系列表
|
||||
*/
|
||||
List<GraphEdge> searchEdges(String graphUuid, String sourceNodeId, String targetNodeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据知识库ID查询关系
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param limit 限制数量
|
||||
* @return 关系列表
|
||||
*/
|
||||
List<GraphEdge> queryEdgesByKnowledgeId(String knowledgeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 获取节点的所有关系
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param direction 方向: IN(入边), OUT(出边), BOTH(双向)
|
||||
* @return 关系列表
|
||||
*/
|
||||
List<GraphEdge> getNodeEdges(String nodeId, String graphUuid, String direction);
|
||||
|
||||
/**
|
||||
* 更新关系信息
|
||||
*
|
||||
* @param edge 关系信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateEdge(GraphEdge edge);
|
||||
|
||||
/**
|
||||
* 删除关系
|
||||
*
|
||||
* @param edgeId 关系ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteEdge(String edgeId, String graphUuid);
|
||||
|
||||
// ==================== 图谱管理 ====================
|
||||
|
||||
/**
|
||||
* 创建图谱Schema
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean createGraphSchema(String graphUuid);
|
||||
|
||||
/**
|
||||
* 删除整个图谱数据
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteGraph(String graphUuid);
|
||||
|
||||
/**
|
||||
* 根据知识库ID删除图谱数据
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteByKnowledgeId(String knowledgeId);
|
||||
|
||||
/**
|
||||
* 获取图谱统计信息
|
||||
*
|
||||
* @param graphUuid 图谱UUID
|
||||
* @return 统计信息 {nodeCount, relationshipCount}
|
||||
*/
|
||||
java.util.Map<String, Object> getGraphStatistics(String graphUuid);
|
||||
|
||||
/**
|
||||
* 根据知识库ID获取统计信息
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @return 统计信息
|
||||
*/
|
||||
java.util.Map<String, Object> getStatistics(String knowledgeId);
|
||||
|
||||
// ==================== 高级查询 ====================
|
||||
|
||||
/**
|
||||
* 查找两个节点之间的路径
|
||||
*
|
||||
* @param sourceNodeId 源节点ID
|
||||
* @param targetNodeId 目标节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param maxDepth 最大深度
|
||||
* @return 路径列表
|
||||
*/
|
||||
List<List<GraphVertex>> findPaths(String sourceNodeId, String targetNodeId, String graphUuid, Integer maxDepth);
|
||||
|
||||
/**
|
||||
* 查找路径(简化版)
|
||||
*
|
||||
* @param startNodeId 起始节点ID
|
||||
* @param endNodeId 结束节点ID
|
||||
* @param maxDepth 最大深度
|
||||
* @return 路径列表
|
||||
*/
|
||||
List<List<GraphVertex>> findPaths(String startNodeId, String endNodeId, Integer maxDepth);
|
||||
|
||||
/**
|
||||
* 查找节点的邻居节点
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param graphUuid 图谱UUID
|
||||
* @param depth 深度(几度关系)
|
||||
* @return 邻居节点列表
|
||||
*/
|
||||
List<GraphVertex> findNeighbors(String nodeId, String graphUuid, Integer depth);
|
||||
|
||||
/**
|
||||
* 获取节点的邻居(简化版)
|
||||
*
|
||||
* @param nodeId 节点ID
|
||||
* @param knowledgeId 知识库ID(可选)
|
||||
* @param limit 限制数量
|
||||
* @return 邻居节点列表
|
||||
*/
|
||||
List<GraphVertex> getNeighbors(String nodeId, String knowledgeId, Integer limit);
|
||||
|
||||
/**
|
||||
* 执行自定义Cypher查询
|
||||
*
|
||||
* @param cypher Cypher查询语句
|
||||
* @param params 参数
|
||||
* @return 查询结果
|
||||
*/
|
||||
List<java.util.Map<String, Object>> executeCypher(String cypher, java.util.Map<String, Object> params);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.service.graph.IGraphLLMService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* DeepSeek 图谱LLM服务实现
|
||||
* 支持 DeepSeek 系列模型
|
||||
* <p>
|
||||
* 注意:使用 langchain4j 的 OpenAiStreamingChatModel,通过 CompletableFuture 转换为同步调用
|
||||
* 参考 DeepSeekChatImpl 的实现,但改为同步模式
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-10-13
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DeepSeekGraphLLMServiceImpl implements IGraphLLMService {
|
||||
|
||||
@Override
|
||||
public String extractGraph(String prompt, ChatModelVo chatModel) {
|
||||
log.info("DeepSeek模型调用: model={}, apiHost={}, 提示词长度={}",
|
||||
chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
|
||||
|
||||
try {
|
||||
// 使用 langchain4j 的 OpenAiStreamingChatModel(参考 DeepSeekChatImpl)
|
||||
StreamingChatModel streamingModel = OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(chatModel.getApiHost())
|
||||
.apiKey(chatModel.getApiKey())
|
||||
.modelName(chatModel.getModelName())
|
||||
.temperature(0.8)
|
||||
.logRequests(false)
|
||||
.logResponses(false)
|
||||
.build();
|
||||
|
||||
// 用于收集完整响应
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
CompletableFuture<String> responseFuture = new CompletableFuture<>();
|
||||
|
||||
// 发送流式消息,但通过 CompletableFuture 转换为同步
|
||||
long startTime = System.currentTimeMillis();
|
||||
streamingModel.chat(prompt, new StreamingChatResponseHandler() {
|
||||
@Override
|
||||
public void onPartialResponse(String partialResponse) {
|
||||
fullResponse.append(partialResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
String responseText = fullResponse.toString();
|
||||
log.info("DeepSeek模型响应成功: 耗时={}ms, 响应长度={}", duration, responseText.length());
|
||||
responseFuture.complete(responseText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
log.error("DeepSeek模型调用错误: {}", error.getMessage());
|
||||
responseFuture.completeExceptionally(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 同步等待结果(最多2分钟)
|
||||
return responseFuture.get(2, TimeUnit.MINUTES);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("DeepSeek模型调用失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("DeepSeek模型调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "deepseek"; // 对应 ChatModel 表中的 category 字段
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import io.github.imfangs.dify.client.DifyClient;
|
||||
import io.github.imfangs.dify.client.DifyClientFactory;
|
||||
import io.github.imfangs.dify.client.callback.ChatStreamCallback;
|
||||
import io.github.imfangs.dify.client.enums.ResponseMode;
|
||||
import io.github.imfangs.dify.client.event.ErrorEvent;
|
||||
import io.github.imfangs.dify.client.event.MessageEndEvent;
|
||||
import io.github.imfangs.dify.client.event.MessageEvent;
|
||||
import io.github.imfangs.dify.client.model.DifyConfig;
|
||||
import io.github.imfangs.dify.client.model.chat.ChatMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.service.graph.IGraphLLMService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Dify 图谱LLM服务实现
|
||||
* 支持 Dify 平台的对话模型
|
||||
* <p>
|
||||
* 注意:Dify 使用流式调用,通过 CompletableFuture 实现同步等待
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-10-11
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DifyGraphLLMServiceImpl implements IGraphLLMService {
|
||||
|
||||
@Override
|
||||
public String extractGraph(String prompt, ChatModelVo chatModel) {
|
||||
log.info("Dify模型调用: model={}, apiHost={}, 提示词长度={}",
|
||||
chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
|
||||
|
||||
try {
|
||||
// 创建 Dify 客户端配置
|
||||
DifyConfig config = DifyConfig.builder()
|
||||
.baseUrl(chatModel.getApiHost())
|
||||
.apiKey(chatModel.getApiKey())
|
||||
.connectTimeout(5000)
|
||||
.readTimeout(120000) // 2分钟超时
|
||||
.writeTimeout(30000)
|
||||
.build();
|
||||
|
||||
DifyClient chatClient = DifyClientFactory.createClient(config);
|
||||
|
||||
// 创建聊天消息(使用流式模式)
|
||||
ChatMessage message = ChatMessage.builder()
|
||||
.query(prompt)
|
||||
.user("graph-system") // 图谱系统用户
|
||||
.responseMode(ResponseMode.STREAMING) // 流式模式
|
||||
.build();
|
||||
|
||||
// 用于收集完整响应
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
CompletableFuture<String> responseFuture = new CompletableFuture<>();
|
||||
|
||||
// 发送流式消息
|
||||
long startTime = System.currentTimeMillis();
|
||||
chatClient.sendChatMessageStream(message, new ChatStreamCallback() {
|
||||
@Override
|
||||
public void onMessage(MessageEvent event) {
|
||||
fullResponse.append(event.getAnswer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageEnd(MessageEndEvent event) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
String responseText = fullResponse.toString();
|
||||
log.info("Dify模型响应成功: 耗时={}ms, 响应长度={}, messageId={}",
|
||||
duration, responseText.length(), event.getMessageId());
|
||||
responseFuture.complete(responseText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorEvent event) {
|
||||
log.error("Dify模型调用错误: {}", event.getMessage());
|
||||
responseFuture.completeExceptionally(
|
||||
new RuntimeException("Dify调用错误: " + event.getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Throwable throwable) {
|
||||
log.error("Dify模型调用异常: {}", throwable.getMessage(), throwable);
|
||||
responseFuture.completeExceptionally(throwable);
|
||||
}
|
||||
});
|
||||
|
||||
// 同步等待结果(最多2分钟)
|
||||
return responseFuture.get(2, TimeUnit.MINUTES);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Dify模型调用失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Dify模型调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "dify"; // 对应 ChatModel 表中的 category 字段
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,367 +0,0 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.config.GraphExtractPrompt;
|
||||
import org.ruoyi.constant.GraphConstants;
|
||||
import org.ruoyi.domain.dto.ExtractedEntity;
|
||||
import org.ruoyi.domain.dto.ExtractedRelation;
|
||||
import org.ruoyi.domain.dto.GraphExtractionResult;
|
||||
import org.ruoyi.factory.GraphLLMServiceFactory;
|
||||
import org.ruoyi.service.graph.IGraphExtractionService;
|
||||
import org.ruoyi.service.graph.IGraphLLMService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/**
|
||||
* 图谱实体关系抽取服务实现
|
||||
* 使用工厂模式支持多种LLM模型(参考 ruoyi-chat 设计)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
|
||||
public class GraphExtractionServiceImpl implements IGraphExtractionService {
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
private final GraphLLMServiceFactory llmServiceFactory;
|
||||
|
||||
/**
|
||||
* 实体匹配正则表达式
|
||||
* 格式: ("entity"<|>ENTITY_NAME<|>ENTITY_TYPE<|>ENTITY_DESCRIPTION)
|
||||
*/
|
||||
private static final Pattern ENTITY_PATTERN = Pattern.compile(
|
||||
"\\(\"entity\"" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^)]+)\\)"
|
||||
);
|
||||
/**
|
||||
* 关系匹配正则表达式
|
||||
* 格式: ("relationship"<|>SOURCE<|>TARGET<|>DESCRIPTION<|>STRENGTH)
|
||||
*/
|
||||
private static final Pattern RELATION_PATTERN = Pattern.compile(
|
||||
"\\(\"relationship\"" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^" + Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) + "]+)" +
|
||||
Pattern.quote(GraphConstants.GRAPH_TUPLE_DELIMITER) +
|
||||
"([^)]+)\\)"
|
||||
);
|
||||
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult extractFromText(String text) {
|
||||
return extractFromText(text, GraphConstants.DEFAULT_ENTITY_TYPES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult extractFromText(String text, String[] entityTypes) {
|
||||
log.info("开始从文本中抽取实体和关系,文本长度: {}", text.length());
|
||||
|
||||
try {
|
||||
// 1. 构建提示词
|
||||
String prompt = GraphExtractPrompt.buildExtractionPrompt(text, entityTypes);
|
||||
|
||||
// 2. 调用LLM(使用默认模型)
|
||||
String llmResponse = callLLM(prompt);
|
||||
|
||||
// 3. 解析响应
|
||||
GraphExtractionResult result = parseGraphResponse(llmResponse);
|
||||
result.setRawResponse(llmResponse);
|
||||
result.setSuccess(true);
|
||||
|
||||
log.info("抽取完成,实体数: {}, 关系数: {}",
|
||||
result.getEntities().size(), result.getRelations().size());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("实体关系抽取失败", e);
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(new ArrayList<>())
|
||||
.relations(new ArrayList<>())
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult extractFromTextWithModel(String text, String modelName) {
|
||||
log.info("开始从文本中抽取实体和关系,使用模型: {}, 文本长度: {}", modelName, text.length());
|
||||
|
||||
try {
|
||||
// 1. 获取模型配置
|
||||
ChatModelVo chatModel = chatModelService.selectModelByName(modelName);
|
||||
if (chatModel == null) {
|
||||
log.warn("未找到模型: {}, 使用默认模型", modelName);
|
||||
return extractFromText(text);
|
||||
}
|
||||
|
||||
// 2. 构建提示词
|
||||
String prompt = GraphExtractPrompt.buildExtractionPrompt(text, GraphConstants.DEFAULT_ENTITY_TYPES);
|
||||
|
||||
// 3. 调用LLM(使用指定模型)
|
||||
String llmResponse = callLLMWithModel(prompt, chatModel);
|
||||
|
||||
// 4. 解析响应
|
||||
GraphExtractionResult result = parseGraphResponse(llmResponse);
|
||||
result.setRawResponse(llmResponse);
|
||||
result.setSuccess(true);
|
||||
|
||||
log.info("抽取完成,实体数: {}, 关系数: {}, 使用模型: {}",
|
||||
result.getEntities().size(), result.getRelations().size(), modelName);
|
||||
|
||||
// ⭐ 调试:如果没有关系,记录原始响应(便于诊断)
|
||||
if (result.getRelations().isEmpty() && !result.getEntities().isEmpty()) {
|
||||
log.warn("⚠️ LLM 提取到 {} 个实体,但没有提取到任何关系!", result.getEntities().size());
|
||||
log.warn("LLM 原始响应预览(前500字符): {}",
|
||||
llmResponse.length() > 500 ? llmResponse.substring(0, 500) + "..." : llmResponse);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("实体关系抽取失败,模型: {}", modelName, e);
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(new ArrayList<>())
|
||||
.relations(new ArrayList<>())
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult parseGraphResponse(String response) {
|
||||
log.debug("开始解析图谱响应,响应长度: {}", response != null ? response.length() : 0);
|
||||
|
||||
List<ExtractedEntity> entities = new ArrayList<>();
|
||||
List<ExtractedRelation> relations = new ArrayList<>();
|
||||
|
||||
if (StrUtil.isBlank(response)) {
|
||||
log.warn("响应为空,无法解析");
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(entities)
|
||||
.relations(relations)
|
||||
.success(false)
|
||||
.errorMessage("LLM响应为空")
|
||||
.build();
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 解析实体
|
||||
Matcher entityMatcher = ENTITY_PATTERN.matcher(response);
|
||||
while (entityMatcher.find()) {
|
||||
String name = entityMatcher.group(1).trim();
|
||||
String type = entityMatcher.group(2).trim();
|
||||
String description = entityMatcher.group(3).trim();
|
||||
|
||||
// ⭐ 过滤无效实体(N/A 或包含特殊字符)
|
||||
if (isInvalidEntity(name, type)) {
|
||||
log.debug("跳过无效实体: name={}, type={}", name, type);
|
||||
continue;
|
||||
}
|
||||
|
||||
ExtractedEntity entity = ExtractedEntity.builder()
|
||||
.name(name)
|
||||
.type(type)
|
||||
.description(description)
|
||||
.build();
|
||||
|
||||
entities.add(entity);
|
||||
log.debug("解析到实体: name={}, type={}", name, type);
|
||||
}
|
||||
|
||||
// 2. 解析关系
|
||||
Matcher relationMatcher = RELATION_PATTERN.matcher(response);
|
||||
while (relationMatcher.find()) {
|
||||
String sourceEntity = relationMatcher.group(1).trim();
|
||||
String targetEntity = relationMatcher.group(2).trim();
|
||||
String description = relationMatcher.group(3).trim();
|
||||
String strengthStr = relationMatcher.group(4).trim();
|
||||
|
||||
Integer strength = parseStrength(strengthStr);
|
||||
Double confidence = calculateConfidence(strength);
|
||||
|
||||
ExtractedRelation relation = ExtractedRelation.builder()
|
||||
.sourceEntity(sourceEntity)
|
||||
.targetEntity(targetEntity)
|
||||
.description(description)
|
||||
.strength(strength)
|
||||
.confidence(confidence)
|
||||
.build();
|
||||
|
||||
relations.add(relation);
|
||||
log.debug("解析到关系: sourceEntity={}, targetEntity={}, strength={}",
|
||||
sourceEntity, targetEntity, strength);
|
||||
}
|
||||
|
||||
log.info("解析完成,实体数: {}, 关系数: {}", entities.size(), relations.size());
|
||||
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(entities)
|
||||
.relations(relations)
|
||||
.success(true)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析图谱响应失败", e);
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(entities)
|
||||
.relations(relations)
|
||||
.success(false)
|
||||
.errorMessage("解析失败: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用LLM获取响应(使用默认模型)
|
||||
*
|
||||
* @param prompt 提示词
|
||||
* @return LLM响应
|
||||
*/
|
||||
private String callLLM(String prompt) {
|
||||
// 获取聊天分类的最高优先级模型作为默认模型
|
||||
// 如果没有chat分类的模型,尝试查询任意可用模型
|
||||
ChatModelVo defaultModel = chatModelService.queryList(new ChatModelBo()).get(0);
|
||||
|
||||
if (defaultModel == null) {
|
||||
log.error("未找到可用的LLM模型");
|
||||
throw new RuntimeException("未找到可用的LLM模型,请先配置聊天模型");
|
||||
}
|
||||
|
||||
log.info("使用默认模型: {}", defaultModel.getModelName());
|
||||
return callLLMWithModel(prompt, defaultModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定模型调用LLM获取响应(使用工厂模式,支持多种LLM)
|
||||
*
|
||||
* @param prompt 提示词
|
||||
* @param chatModel 模型配置
|
||||
* @return LLM响应
|
||||
*/
|
||||
private String callLLMWithModel(String prompt, ChatModelVo chatModel) {
|
||||
log.info("调用LLM模型: model={}, category={}, 提示词长度={}",
|
||||
chatModel.getModelName(), chatModel.getCategory(), prompt.length());
|
||||
|
||||
try {
|
||||
// 根据模型类别获取对应的LLM服务实现
|
||||
IGraphLLMService llmService = llmServiceFactory.getLLMService(chatModel.getCategory());
|
||||
|
||||
// 调用LLM进行图谱抽取
|
||||
String responseText = llmService.extractGraph(prompt, chatModel);
|
||||
|
||||
log.info("LLM调用成功: model={}, category={}, 响应长度={}",
|
||||
chatModel.getModelName(), chatModel.getCategory(), responseText.length());
|
||||
|
||||
return responseText;
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 不支持的模型类别,降级到默认实现
|
||||
log.warn("不支持的模型类别: {}, 尝试使用OpenAI兼容模式", chatModel.getCategory());
|
||||
|
||||
try {
|
||||
IGraphLLMService openAiService = llmServiceFactory.getLLMService("openai");
|
||||
return openAiService.extractGraph(prompt, chatModel);
|
||||
} catch (Exception fallbackEx) {
|
||||
log.error("降级调用也失败: {}", fallbackEx.getMessage(), fallbackEx);
|
||||
throw new RuntimeException("LLM调用失败: " + fallbackEx.getMessage(), fallbackEx);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("LLM调用失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("LLM调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析关系强度
|
||||
*
|
||||
* @param strengthStr 强度字符串
|
||||
* @return 强度值(0-10)
|
||||
*/
|
||||
private Integer parseStrength(String strengthStr) {
|
||||
try {
|
||||
// 尝试解析为整数
|
||||
int strength = Integer.parseInt(strengthStr);
|
||||
// 限制在0-10范围内
|
||||
return Math.max(0, Math.min(10, strength));
|
||||
} catch (NumberFormatException e) {
|
||||
log.debug("无法解析关系强度: {}, 使用默认值5", strengthStr);
|
||||
return 5; // 默认中等强度
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证实体是否有效
|
||||
* 过滤 N/A 以及包含 Neo4j 不支持的特殊字符的实体
|
||||
*
|
||||
* @param name 实体名称
|
||||
* @param type 实体类型
|
||||
* @return true=无效,false=有效
|
||||
*/
|
||||
private boolean isInvalidEntity(String name, String type) {
|
||||
// 1. 检查是否为 N/A
|
||||
if ("N/A".equalsIgnoreCase(name) || "N/A".equalsIgnoreCase(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. 检查是否为空或纯空格
|
||||
if (StrUtil.isBlank(name) || StrUtil.isBlank(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. 检查类型是否包含 Neo4j Label 不支持的字符
|
||||
// Neo4j Label 规则:不能包含 / : & | 等特殊字符
|
||||
if (type.matches(".*[/:&|\\\\].*")) {
|
||||
log.warn("⚠️ 实体类型包含非法字符,将被过滤: type={}", type);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. 检查名称是否过长(Neo4j 建议 < 256)
|
||||
if (name.length() > 255 || type.length() > 64) {
|
||||
log.warn("⚠️ 实体名称或类型过长,将被过滤: name.length={}, type.length={}",
|
||||
name.length(), type.length());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关系强度计算置信度
|
||||
*
|
||||
* @param strength 关系强度(0-10)
|
||||
* @return 置信度(0.0-1.0)
|
||||
*/
|
||||
private Double calculateConfidence(Integer strength) {
|
||||
if (strength == null) {
|
||||
return 0.5;
|
||||
}
|
||||
// 将0-10的强度映射到0.3-1.0的置信度
|
||||
return 0.3 + (strength / 10.0) * 0.7;
|
||||
}
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.domain.bo.graph.GraphInstance;
|
||||
import org.ruoyi.mapper.graph.GraphInstanceMapper;
|
||||
import org.ruoyi.service.graph.IGraphInstanceService;
|
||||
import org.ruoyi.service.graph.IGraphStoreService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 图谱实例服务实现
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
|
||||
public class GraphInstanceServiceImpl implements IGraphInstanceService {
|
||||
|
||||
private final GraphInstanceMapper graphInstanceMapper;
|
||||
private final IGraphStoreService graphStoreService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public GraphInstance createInstance(String knowledgeId, String graphName, String config) {
|
||||
// 检查是否已存在
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId);
|
||||
GraphInstance existing = graphInstanceMapper.selectOne(wrapper);
|
||||
|
||||
if (existing != null) {
|
||||
log.warn("知识库 {} 已存在图谱实例", knowledgeId);
|
||||
return existing;
|
||||
}
|
||||
|
||||
// 创建新实例
|
||||
GraphInstance instance = new GraphInstance();
|
||||
instance.setGraphUuid(String.valueOf(IdUtil.getSnowflake().nextId())); // UUID
|
||||
instance.setKnowledgeId(knowledgeId);
|
||||
instance.setGraphName(StringUtils.isNotBlank(graphName) ? graphName : "知识图谱-" + knowledgeId);
|
||||
instance.setGraphStatus(0); // 0-未构建(新建时状态为未构建,需手动点击"构建"按钮)
|
||||
instance.setNodeCount(0);
|
||||
instance.setRelationshipCount(0);
|
||||
|
||||
// 解析配置
|
||||
if (StringUtils.isNotBlank(config)) {
|
||||
instance.setConfig(config);
|
||||
}
|
||||
|
||||
graphInstanceMapper.insert(instance);
|
||||
|
||||
// 创建 Neo4j Schema
|
||||
graphStoreService.createGraphSchema(knowledgeId);
|
||||
|
||||
log.info("创建图谱实例成功: knowledgeId={}, instanceId={}", knowledgeId, instance.getId());
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphInstance getById(Long id) {
|
||||
return graphInstanceMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphInstance getByUuid(String graphUuid) {
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
return graphInstanceMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateInstance(GraphInstance instance) {
|
||||
try {
|
||||
int rows = graphInstanceMapper.updateById(instance);
|
||||
log.info("更新图谱实例: id={}, rows={}", instance.getId(), rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新图谱实例失败: id={}", instance.getId(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GraphInstance> listByKnowledgeId(String knowledgeId) {
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId);
|
||||
wrapper.orderByDesc(GraphInstance::getCreateTime);
|
||||
return graphInstanceMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<GraphInstance> queryPage(Page<GraphInstance> page, String instanceName, String knowledgeId, Integer graphStatus) {
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 图谱名称模糊查询
|
||||
if (StringUtils.isNotBlank(instanceName)) {
|
||||
wrapper.like(GraphInstance::getGraphName, instanceName.trim());
|
||||
}
|
||||
|
||||
// 知识库ID精确查询
|
||||
if (StringUtils.isNotBlank(knowledgeId)) {
|
||||
wrapper.eq(GraphInstance::getKnowledgeId, knowledgeId.trim());
|
||||
}
|
||||
|
||||
// 状态精确查询
|
||||
if (graphStatus != null) {
|
||||
wrapper.eq(GraphInstance::getGraphStatus, graphStatus);
|
||||
}
|
||||
|
||||
// 只查询未删除的记录
|
||||
wrapper.eq(GraphInstance::getDelFlag, "0");
|
||||
|
||||
// 按创建时间倒序
|
||||
wrapper.orderByDesc(GraphInstance::getCreateTime);
|
||||
|
||||
return graphInstanceMapper.selectPage(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(String graphUuid, Integer status) {
|
||||
try {
|
||||
LambdaUpdateWrapper<GraphInstance> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
wrapper.set(GraphInstance::getGraphStatus, status);
|
||||
|
||||
int rows = graphInstanceMapper.update(null, wrapper);
|
||||
|
||||
log.info("更新图谱状态: graphUuid={}, status={}, rows={}", graphUuid, status, rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新图谱状态失败: graphUuid={}, status={}", graphUuid, status, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateCounts(String graphUuid, Integer nodeCount, Integer relationshipCount) {
|
||||
try {
|
||||
LambdaUpdateWrapper<GraphInstance> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
|
||||
if (nodeCount != null) {
|
||||
wrapper.set(GraphInstance::getNodeCount, nodeCount);
|
||||
}
|
||||
if (relationshipCount != null) {
|
||||
wrapper.set(GraphInstance::getRelationshipCount, relationshipCount);
|
||||
}
|
||||
|
||||
int rows = graphInstanceMapper.update(null, wrapper);
|
||||
|
||||
log.info("更新图谱统计: graphUuid={}, nodeCount={}, relationshipCount={}, rows={}",
|
||||
graphUuid, nodeCount, relationshipCount, rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新图谱统计失败: graphUuid={}", graphUuid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateConfig(String graphUuid, String config) {
|
||||
try {
|
||||
LambdaUpdateWrapper<GraphInstance> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
wrapper.set(GraphInstance::getConfig, config);
|
||||
|
||||
int rows = graphInstanceMapper.update(null, wrapper);
|
||||
|
||||
log.info("更新图谱配置: graphUuid={}, rows={}", graphUuid, rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("更新图谱配置失败: graphUuid={}", graphUuid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean deleteInstance(String graphUuid) {
|
||||
try {
|
||||
log.info("🗑️ 开始删除图谱实例及数据,graphUuid: {}", graphUuid);
|
||||
|
||||
// ⭐ 1. 先获取实例信息(获取knowledgeId)
|
||||
GraphInstance instance = getByUuid(graphUuid);
|
||||
if (instance == null) {
|
||||
log.warn("⚠️ 图谱实例不存在: graphUuid={}", graphUuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
String knowledgeId = instance.getKnowledgeId();
|
||||
|
||||
// ⭐ 2. 删除Neo4j中的图数据(通过knowledgeId)
|
||||
if (StrUtil.isNotBlank(knowledgeId)) {
|
||||
log.info("删除Neo4j图数据,knowledgeId: {}", knowledgeId);
|
||||
boolean neo4jDeleted = graphStoreService.deleteByKnowledgeId(knowledgeId);
|
||||
if (neo4jDeleted) {
|
||||
log.info("✅ Neo4j图数据删除成功");
|
||||
} else {
|
||||
log.warn("⚠️ Neo4j图数据删除失败(可能是没有数据)");
|
||||
}
|
||||
} else {
|
||||
log.warn("⚠️ 实例没有关联知识库ID,跳过Neo4j数据删除");
|
||||
}
|
||||
|
||||
// 3. 删除MySQL中的实例记录
|
||||
LambdaQueryWrapper<GraphInstance> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(GraphInstance::getGraphUuid, graphUuid);
|
||||
int rows = graphInstanceMapper.delete(wrapper);
|
||||
|
||||
log.info("✅ 删除图谱实例成功: graphUuid={}, knowledgeId={}, rows={}",
|
||||
graphUuid, knowledgeId, rows);
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 删除图谱实例失败: graphUuid={}", graphUuid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean deleteInstanceAndData(String graphUuid) {
|
||||
try {
|
||||
// 1. 删除 Neo4j 中的图谱数据
|
||||
boolean graphDeleted = graphStoreService.deleteGraph(graphUuid);
|
||||
|
||||
// 2. 删除 MySQL 中的实例记录
|
||||
boolean instanceDeleted = deleteInstance(graphUuid);
|
||||
|
||||
log.info("删除图谱实例及数据: graphUuid={}, graphDeleted={}, instanceDeleted={}",
|
||||
graphUuid, graphDeleted, instanceDeleted);
|
||||
|
||||
return graphDeleted && instanceDeleted;
|
||||
} catch (Exception e) {
|
||||
log.error("删除图谱实例及数据失败: graphUuid={}", graphUuid, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getStatistics(String graphUuid) {
|
||||
try {
|
||||
// 从 Neo4j 获取实时统计
|
||||
Map<String, Object> stats = graphStoreService.getGraphStatistics(graphUuid);
|
||||
|
||||
// 更新到 MySQL(异步)
|
||||
if (stats.containsKey("nodeCount") && stats.containsKey("relationshipCount")) {
|
||||
updateCounts(
|
||||
graphUuid,
|
||||
(Integer) stats.get("nodeCount"),
|
||||
(Integer) stats.get("relationshipCount")
|
||||
);
|
||||
}
|
||||
|
||||
// 添加实例信息
|
||||
GraphInstance instance = getByUuid(graphUuid);
|
||||
if (instance != null) {
|
||||
stats.put("graphName", instance.getGraphName());
|
||||
stats.put("status", instance.getGraphStatus());
|
||||
stats.put("createTime", instance.getCreateTime());
|
||||
stats.put("updateTime", instance.getUpdateTime());
|
||||
}
|
||||
|
||||
return stats;
|
||||
} catch (Exception e) {
|
||||
log.error("获取图谱统计信息失败: graphUuid={}", graphUuid, e);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,462 +0,0 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.constant.GraphConstants;
|
||||
import org.ruoyi.domain.bo.graph.GraphEdge;
|
||||
import org.ruoyi.domain.bo.graph.GraphVertex;
|
||||
import org.ruoyi.domain.dto.ExtractedEntity;
|
||||
import org.ruoyi.domain.dto.ExtractedRelation;
|
||||
import org.ruoyi.domain.dto.GraphExtractionResult;
|
||||
import org.ruoyi.service.graph.IGraphExtractionService;
|
||||
import org.ruoyi.service.graph.IGraphRAGService;
|
||||
import org.ruoyi.service.graph.IGraphStoreService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* GraphRAG服务实现
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-09-30
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "knowledge.graph", name = "enabled", havingValue = "true")
|
||||
public class GraphRAGServiceImpl implements IGraphRAGService {
|
||||
|
||||
private final IGraphExtractionService graphExtractionService;
|
||||
private final IGraphStoreService graphStoreService;
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult ingestText(String text, String knowledgeId, Map<String, Object> metadata) {
|
||||
return ingestTextWithModel(text, knowledgeId, metadata, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult ingestTextWithModel(String text, String knowledgeId, Map<String, Object> metadata, String modelName) {
|
||||
log.info("开始将文本入库到图谱,知识库ID: {}, 模型: {}, 文本长度: {}",
|
||||
knowledgeId, modelName != null ? modelName : "默认", text.length());
|
||||
|
||||
try {
|
||||
// 1. 从文本中抽取实体和关系
|
||||
GraphExtractionResult extractionResult;
|
||||
if (StrUtil.isNotBlank(modelName)) {
|
||||
extractionResult = graphExtractionService.extractFromTextWithModel(text, modelName);
|
||||
} else {
|
||||
extractionResult = graphExtractionService.extractFromText(text);
|
||||
}
|
||||
|
||||
if (!extractionResult.getSuccess()) {
|
||||
log.error("实体抽取失败: {}", extractionResult.getErrorMessage());
|
||||
return extractionResult;
|
||||
}
|
||||
|
||||
// 2. 将抽取的实体转换为图节点
|
||||
List<GraphVertex> vertices = convertEntitiesToVertices(
|
||||
extractionResult.getEntities(),
|
||||
knowledgeId,
|
||||
metadata
|
||||
);
|
||||
|
||||
// 3. 批量添加节点到Neo4j,并建立实体名称→nodeId的映射
|
||||
Map<String, String> entityNameToNodeIdMap = new HashMap<>();
|
||||
if (!vertices.isEmpty()) {
|
||||
int addedCount = graphStoreService.addVertices(vertices);
|
||||
log.info("成功添加 {} 个节点到图谱", addedCount);
|
||||
|
||||
// ⭐ 建立映射:实体名称 → nodeId
|
||||
for (GraphVertex vertex : vertices) {
|
||||
entityNameToNodeIdMap.put(vertex.getName(), vertex.getNodeId());
|
||||
}
|
||||
log.debug("建立实体名称映射: {} 个实体", entityNameToNodeIdMap.size());
|
||||
}
|
||||
|
||||
// 4. 将抽取的关系转换为图边,使用映射填充nodeId
|
||||
List<GraphEdge> edges = convertRelationsToEdges(
|
||||
extractionResult.getRelations(),
|
||||
knowledgeId,
|
||||
metadata,
|
||||
entityNameToNodeIdMap // ⭐ 传入映射
|
||||
);
|
||||
|
||||
// 5. 批量添加关系到Neo4j
|
||||
if (!edges.isEmpty()) {
|
||||
int addedCount = graphStoreService.addEdges(edges);
|
||||
log.info("成功添加 {} 个关系到图谱", addedCount);
|
||||
}
|
||||
|
||||
return extractionResult;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("文本入库失败", e);
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(new ArrayList<>())
|
||||
.relations(new ArrayList<>())
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult ingestDocument(String documentText, String knowledgeId, Map<String, Object> metadata) {
|
||||
return ingestDocumentWithModel(documentText, knowledgeId, metadata, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphExtractionResult ingestDocumentWithModel(String documentText, String knowledgeId, Map<String, Object> metadata, String modelName) {
|
||||
log.info("开始将文档入库到图谱,知识库ID: {}, 模型: {}, 文档长度: {}",
|
||||
knowledgeId, modelName != null ? modelName : "默认", documentText.length());
|
||||
|
||||
// 如果文档较短,直接处理
|
||||
if (documentText.length() < GraphConstants.RAG_MAX_SEGMENT_SIZE_IN_TOKENS * 4) {
|
||||
return ingestTextWithModel(documentText, knowledgeId, metadata, modelName);
|
||||
}
|
||||
|
||||
// 文档较长,需要分片处理
|
||||
List<String> chunks = splitDocument(documentText);
|
||||
log.info("文档已分割为 {} 个片段", chunks.size());
|
||||
|
||||
// 合并结果
|
||||
List<ExtractedEntity> allEntities = new ArrayList<>();
|
||||
List<ExtractedRelation> allRelations = new ArrayList<>();
|
||||
int totalTokenUsed = 0;
|
||||
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
String chunk = chunks.get(i);
|
||||
log.debug("处理第 {}/{} 个片段", i + 1, chunks.size());
|
||||
|
||||
// 为每个片段添加序号元数据
|
||||
Map<String, Object> chunkMetadata = new HashMap<>(metadata);
|
||||
chunkMetadata.put("chunk_index", i);
|
||||
chunkMetadata.put("total_chunks", chunks.size());
|
||||
|
||||
GraphExtractionResult result = ingestTextWithModel(chunk, knowledgeId, chunkMetadata, modelName);
|
||||
|
||||
if (result.getSuccess()) {
|
||||
allEntities.addAll(result.getEntities());
|
||||
allRelations.addAll(result.getRelations());
|
||||
if (result.getTokenUsed() != null) {
|
||||
totalTokenUsed += result.getTokenUsed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重实体(基于名称和类型)
|
||||
List<ExtractedEntity> uniqueEntities = deduplicateEntities(allEntities);
|
||||
log.info("去重后实体数: {} -> {}", allEntities.size(), uniqueEntities.size());
|
||||
|
||||
return GraphExtractionResult.builder()
|
||||
.entities(uniqueEntities)
|
||||
.relations(allRelations)
|
||||
.tokenUsed(totalTokenUsed)
|
||||
.success(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String retrieveFromGraph(String query, String knowledgeId, int maxResults) {
|
||||
log.info("从图谱检索相关内容,查询: {}, 知识库ID: {}", query, knowledgeId);
|
||||
|
||||
try {
|
||||
// 1. 从查询中抽取关键词(简单分词)
|
||||
List<String> keywords = extractKeywords(query);
|
||||
log.debug("提取的关键词: {}", keywords);
|
||||
|
||||
if (keywords.isEmpty()) {
|
||||
return "未能从查询中提取关键信息";
|
||||
}
|
||||
|
||||
// 2. 在图谱中搜索相关实体节点
|
||||
List<GraphVertex> matchedNodes = new ArrayList<>();
|
||||
for (String keyword : keywords) {
|
||||
List<GraphVertex> nodes = graphStoreService.searchVerticesByName(
|
||||
keyword, knowledgeId, Math.min(5, maxResults)
|
||||
);
|
||||
matchedNodes.addAll(nodes);
|
||||
}
|
||||
|
||||
if (matchedNodes.isEmpty()) {
|
||||
return "图谱中未找到相关实体";
|
||||
}
|
||||
|
||||
log.info("找到 {} 个匹配的实体节点", matchedNodes.size());
|
||||
|
||||
// 3. 去重(按nodeId)
|
||||
Map<String, GraphVertex> uniqueNodes = new HashMap<>();
|
||||
for (GraphVertex node : matchedNodes) {
|
||||
uniqueNodes.putIfAbsent(node.getNodeId(), node);
|
||||
}
|
||||
matchedNodes = new ArrayList<>(uniqueNodes.values());
|
||||
|
||||
// 限制结果数量
|
||||
if (matchedNodes.size() > maxResults) {
|
||||
matchedNodes = matchedNodes.subList(0, maxResults);
|
||||
}
|
||||
|
||||
// 4. 为每个匹配节点获取邻居,构建子图上下文
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("### 图谱检索结果\n\n");
|
||||
result.append(String.format("查询: %s\n", query));
|
||||
result.append(String.format("找到 %d 个相关实体:\n\n", matchedNodes.size()));
|
||||
|
||||
for (int i = 0; i < matchedNodes.size(); i++) {
|
||||
GraphVertex node = matchedNodes.get(i);
|
||||
result.append(String.format("**%d. %s** (%s)\n", i + 1, node.getName(), node.getLabel()));
|
||||
|
||||
if (StrUtil.isNotBlank(node.getDescription())) {
|
||||
result.append(String.format(" 描述: %s\n", node.getDescription()));
|
||||
}
|
||||
|
||||
// 获取邻居节点(1跳)
|
||||
List<GraphVertex> neighbors = graphStoreService.getNeighbors(
|
||||
node.getNodeId(), knowledgeId, 5
|
||||
);
|
||||
|
||||
if (!neighbors.isEmpty()) {
|
||||
result.append(" 关联实体: ");
|
||||
List<String> neighborNames = neighbors.stream()
|
||||
.map(GraphVertex::getName)
|
||||
.limit(5)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
result.append(String.join(", ", neighborNames));
|
||||
result.append("\n");
|
||||
}
|
||||
|
||||
result.append("\n");
|
||||
}
|
||||
|
||||
// 5. 添加统计信息
|
||||
result.append("---\n");
|
||||
result.append(String.format("总计: %d 个实体节点\n", matchedNodes.size()));
|
||||
|
||||
return result.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("图谱检索失败", e);
|
||||
return "检索失败: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从查询中提取关键词
|
||||
*
|
||||
* @param query 查询文本
|
||||
* @return 关键词列表
|
||||
*/
|
||||
private List<String> extractKeywords(String query) {
|
||||
List<String> keywords = new ArrayList<>();
|
||||
|
||||
// 简单的中文分词策略
|
||||
// 1. 去除标点符号
|
||||
String cleaned = query.replaceAll("[\\p{Punct}\\s]+", " ");
|
||||
|
||||
// 2. 按空格分割
|
||||
String[] words = cleaned.split("\\s+");
|
||||
|
||||
// 3. 过滤停用词和短词
|
||||
Set<String> stopWords = new HashSet<>(Arrays.asList(
|
||||
"的", "了", "和", "是", "在", "我", "有", "个", "这", "那", "为",
|
||||
"与", "或", "但", "等", "及", "而", "中", "如", "一", "二", "三"
|
||||
));
|
||||
|
||||
for (String word : words) {
|
||||
word = word.trim();
|
||||
if (word.length() >= 2 && !stopWords.contains(word)) {
|
||||
keywords.add(word);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 如果没有提取到关键词,尝试按2-3字切分
|
||||
if (keywords.isEmpty() && query.length() >= 2) {
|
||||
for (int i = 0; i <= query.length() - 2; i++) {
|
||||
String chunk = query.substring(i, Math.min(i + 3, query.length()));
|
||||
if (chunk.length() >= 2 && !stopWords.contains(chunk)) {
|
||||
keywords.add(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去重并限制数量
|
||||
return keywords.stream()
|
||||
.distinct()
|
||||
.limit(5)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteGraphData(String knowledgeId) {
|
||||
log.info("删除知识库图谱数据,知识库ID: {}", knowledgeId);
|
||||
|
||||
try {
|
||||
// 删除该知识库的所有节点和关系
|
||||
return graphStoreService.deleteByKnowledgeId(knowledgeId);
|
||||
} catch (Exception e) {
|
||||
log.error("删除图谱数据失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将抽取的实体转换为图节点
|
||||
*/
|
||||
private List<GraphVertex> convertEntitiesToVertices(
|
||||
List<ExtractedEntity> entities,
|
||||
String knowledgeId,
|
||||
Map<String, Object> metadata) {
|
||||
|
||||
List<GraphVertex> vertices = new ArrayList<>();
|
||||
|
||||
for (ExtractedEntity entity : entities) {
|
||||
GraphVertex vertex = new GraphVertex();
|
||||
vertex.setNodeId(IdUtil.simpleUUID()); // 生成唯一ID
|
||||
vertex.setName(entity.getName());
|
||||
vertex.setLabel(entity.getType());
|
||||
vertex.setDescription(entity.getDescription());
|
||||
vertex.setKnowledgeId(knowledgeId);
|
||||
vertex.setConfidence(entity.getConfidence() != null ? entity.getConfidence() : 1.0);
|
||||
|
||||
// 添加元数据
|
||||
if (metadata != null && !metadata.isEmpty()) {
|
||||
vertex.setMetadata(metadata);
|
||||
}
|
||||
|
||||
vertices.add(vertex);
|
||||
}
|
||||
|
||||
return vertices;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将抽取的关系转换为图边
|
||||
*
|
||||
* @param relations 抽取的关系列表
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param metadata 元数据
|
||||
* @param entityNameToNodeIdMap 实体名称到节点ID的映射
|
||||
* @return 图边列表
|
||||
*/
|
||||
private List<GraphEdge> convertRelationsToEdges(
|
||||
List<ExtractedRelation> relations,
|
||||
String knowledgeId,
|
||||
Map<String, Object> metadata,
|
||||
Map<String, String> entityNameToNodeIdMap) {
|
||||
|
||||
List<GraphEdge> edges = new ArrayList<>();
|
||||
int skippedCount = 0;
|
||||
|
||||
for (ExtractedRelation relation : relations) {
|
||||
// ⭐ 通过实体名称查找对应的nodeId
|
||||
String sourceNodeId = entityNameToNodeIdMap.get(relation.getSourceEntity());
|
||||
String targetNodeId = entityNameToNodeIdMap.get(relation.getTargetEntity());
|
||||
|
||||
// 如果找不到对应的节点ID,跳过这个关系
|
||||
if (sourceNodeId == null || targetNodeId == null) {
|
||||
log.warn("⚠️ 跳过关系(节点未找到): {} -> {}",
|
||||
relation.getSourceEntity(), relation.getTargetEntity());
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
GraphEdge edge = new GraphEdge();
|
||||
edge.setEdgeId(IdUtil.simpleUUID());
|
||||
edge.setSourceNodeId(sourceNodeId); // ⭐ 设置源节点ID
|
||||
edge.setTargetNodeId(targetNodeId); // ⭐ 设置目标节点ID
|
||||
edge.setSourceName(relation.getSourceEntity());
|
||||
edge.setTargetName(relation.getTargetEntity());
|
||||
edge.setLabel("RELATED_TO"); // 默认关系类型
|
||||
edge.setDescription(relation.getDescription());
|
||||
edge.setWeight(relation.getStrength() / 10.0); // 转换为0-1的权重
|
||||
edge.setKnowledgeId(knowledgeId);
|
||||
edge.setConfidence(relation.getConfidence() != null ? relation.getConfidence() : 1.0);
|
||||
|
||||
// 添加元数据
|
||||
if (metadata != null && !metadata.isEmpty()) {
|
||||
edge.setMetadata(metadata);
|
||||
}
|
||||
|
||||
edges.add(edge);
|
||||
}
|
||||
|
||||
if (skippedCount > 0) {
|
||||
log.warn("⚠️ 共跳过 {} 个关系(对应的实体节点未找到)", skippedCount);
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割文档为多个片段
|
||||
*/
|
||||
private List<String> splitDocument(String text) {
|
||||
List<String> chunks = new ArrayList<>();
|
||||
int chunkSize = GraphConstants.RAG_MAX_SEGMENT_SIZE_IN_TOKENS * 4; // 简单估算字符数
|
||||
int overlap = GraphConstants.RAG_SEGMENT_OVERLAP_IN_TOKENS * 4;
|
||||
|
||||
int start = 0;
|
||||
while (start < text.length()) {
|
||||
int end = Math.min(start + chunkSize, text.length());
|
||||
|
||||
// 尝试在句子边界分割
|
||||
if (end < text.length()) {
|
||||
int lastPeriod = text.lastIndexOf('。', end);
|
||||
int lastNewline = text.lastIndexOf('\n', end);
|
||||
int boundary = Math.max(lastPeriod, lastNewline);
|
||||
|
||||
if (boundary > start) {
|
||||
end = boundary + 1;
|
||||
}
|
||||
}
|
||||
|
||||
chunks.add(text.substring(start, end));
|
||||
|
||||
// ⭐ 修复死循环:确保 start 一定会增加
|
||||
// 如果已经到达文本末尾,直接退出
|
||||
if (end >= text.length()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 计算下一个起始位置,确保至少前进1个字符
|
||||
int nextStart = end - overlap;
|
||||
if (nextStart <= start) {
|
||||
// 如果 overlap 太大导致无法前进,强制前进到 end
|
||||
start = end;
|
||||
} else {
|
||||
start = nextStart;
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去重实体
|
||||
*/
|
||||
private List<ExtractedEntity> deduplicateEntities(List<ExtractedEntity> entities) {
|
||||
Map<String, ExtractedEntity> entityMap = new HashMap<>();
|
||||
|
||||
for (ExtractedEntity entity : entities) {
|
||||
String key = entity.getName() + "|" + entity.getType();
|
||||
|
||||
if (!entityMap.containsKey(key)) {
|
||||
entityMap.put(key, entity);
|
||||
} else {
|
||||
// 如果已存在,保留置信度更高的
|
||||
ExtractedEntity existing = entityMap.get(key);
|
||||
double entityConf = entity.getConfidence() != null ? entity.getConfidence() : 1.0;
|
||||
double existingConf = existing.getConfidence() != null ? existing.getConfidence() : 1.0;
|
||||
if (entityConf > existingConf) {
|
||||
entityMap.put(key, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(entityMap.values());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
package org.ruoyi.service.graph.impl;
|
||||
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.service.graph.IGraphLLMService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
/**
|
||||
* OpenAI 图谱LLM服务实现
|
||||
* 支持 OpenAI 兼容的模型(GPT-3.5, GPT-4, 自定义OpenAI兼容接口等)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-10-11
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OpenAIGraphLLMServiceImpl implements IGraphLLMService {
|
||||
|
||||
@Override
|
||||
public String extractGraph(String prompt, ChatModelVo chatModel) {
|
||||
log.info("OpenAI模型调用: model={}, apiHost={}, 提示词长度={}",
|
||||
chatModel.getModelName(), chatModel.getApiHost(), prompt.length());
|
||||
try {
|
||||
OpenAiChatModel model = OpenAiChatModel.builder()
|
||||
.baseUrl(chatModel.getApiHost())
|
||||
.apiKey(chatModel.getApiKey())
|
||||
.modelName(chatModel.getModelName())
|
||||
.build();
|
||||
String content = model.chat(prompt);
|
||||
String responseText = content != null ? content : "";
|
||||
log.info("OpenAI模型响应成功:, 响应长度={}", responseText.length());
|
||||
return responseText;
|
||||
} catch (Exception e) {
|
||||
log.error("OpenAI模型调用失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("OpenAI模型调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "openai"; // 对应 ChatModel 表中的 category 字段
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.config.VectorStoreProperties;
|
||||
import org.ruoyi.factory.EmbeddingModelFactory;
|
||||
import org.ruoyi.service.vector.VectorStoreService;
|
||||
@@ -22,6 +23,9 @@ public abstract class AbstractVectorStoreStrategy implements VectorStoreService
|
||||
|
||||
private final EmbeddingModelFactory embeddingModelFactory;
|
||||
|
||||
protected final IChatModelService chatModelService;
|
||||
|
||||
|
||||
/**
|
||||
* 将float数组转换为Float对象数组
|
||||
*/
|
||||
@@ -37,8 +41,8 @@ public abstract class AbstractVectorStoreStrategy implements VectorStoreService
|
||||
* 获取向量模型
|
||||
*/
|
||||
@SneakyThrows
|
||||
protected EmbeddingModel getEmbeddingModel(String modelName, Integer dimension) {
|
||||
return embeddingModelFactory.createModel(modelName, dimension);
|
||||
protected EmbeddingModel getEmbeddingModel(String modelName) {
|
||||
return embeddingModelFactory.createModel(modelName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,8 @@ import io.milvus.param.IndexType;
|
||||
import io.milvus.param.MetricType;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.config.VectorStoreProperties;
|
||||
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
import org.ruoyi.domain.bo.vector.StoreEmbeddingBo;
|
||||
@@ -31,8 +33,9 @@ import java.util.stream.IntStream;
|
||||
public class MilvusVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||
|
||||
public MilvusVectorStoreStrategy(VectorStoreProperties vectorStoreProperties,
|
||||
IChatModelService chatModelService,
|
||||
EmbeddingModelFactory embeddingModelFactory) {
|
||||
super(vectorStoreProperties, embeddingModelFactory);
|
||||
super(vectorStoreProperties, embeddingModelFactory, chatModelService);
|
||||
}
|
||||
|
||||
// 缓存不同集合与 autoFlush 配置的 Milvus 连接
|
||||
@@ -42,38 +45,27 @@ public class MilvusVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||
* 获取 Milvus Store,支持动态维度
|
||||
*/
|
||||
private EmbeddingStore<TextSegment> getMilvusStore(String collectionName, int dimension, boolean autoFlushOnInsert) {
|
||||
String key = collectionName + "|" + dimension + "|" + autoFlushOnInsert;
|
||||
return storeCache.computeIfAbsent(key, k ->
|
||||
MilvusEmbeddingStore.builder()
|
||||
.uri(vectorStoreProperties.getMilvus().getUrl())
|
||||
.collectionName(collectionName)
|
||||
.dimension(dimension)
|
||||
.indexType(IndexType.IVF_FLAT)
|
||||
.metricType(MetricType.L2)
|
||||
.autoFlushOnInsert(autoFlushOnInsert)
|
||||
.idFieldName("id")
|
||||
.textFieldName("text")
|
||||
.metadataFieldName("metadata")
|
||||
.vectorFieldName("vector")
|
||||
.build()
|
||||
);
|
||||
|
||||
return MilvusEmbeddingStore.builder()
|
||||
.uri(vectorStoreProperties.getMilvus().getUrl())
|
||||
.collectionName(collectionName)
|
||||
.dimension(dimension)
|
||||
.indexType(IndexType.IVF_FLAT)
|
||||
.metricType(MetricType.L2)
|
||||
.autoFlushOnInsert(autoFlushOnInsert)
|
||||
.idFieldName("id")
|
||||
.textFieldName("text")
|
||||
.metadataFieldName("metadata")
|
||||
.vectorFieldName("vector")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 embedding 模型的实际维度
|
||||
*/
|
||||
private int getModelDimension(String modelName) {
|
||||
try {
|
||||
EmbeddingModel model = getEmbeddingModel(modelName, null);
|
||||
// 使用一个测试文本获取向量维度
|
||||
Embedding testEmbedding = model.embed("test").content();
|
||||
int dimension = testEmbedding.dimension();
|
||||
log.info("Detected embedding model dimension: {} for model: {}", dimension, modelName);
|
||||
return dimension;
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to detect model dimension for: {}, using default 1024", modelName, e);
|
||||
return 1024; // 默认使用 1024 (bge-m3 的维度)
|
||||
}
|
||||
ChatModelVo modelConfig = chatModelService.selectModelByName(modelName);
|
||||
return modelConfig.getModelDimension();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,7 +80,7 @@ public class MilvusVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||
@Override
|
||||
public void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) {
|
||||
int dimension = getModelDimension(storeEmbeddingBo.getEmbeddingModelName());
|
||||
EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName(), dimension);
|
||||
EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName());
|
||||
|
||||
List<String> chunkList = storeEmbeddingBo.getChunkList();
|
||||
List<String> fidList = storeEmbeddingBo.getFids();
|
||||
@@ -121,7 +113,7 @@ public class MilvusVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||
@Override
|
||||
public List<String> getQueryVector(QueryVectorBo queryVectorBo) {
|
||||
int dimension = getModelDimension(queryVectorBo.getEmbeddingModelName());
|
||||
EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName(), dimension);
|
||||
EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName());
|
||||
|
||||
Embedding queryEmbedding = embeddingModel.embed(queryVectorBo.getQuery()).content();
|
||||
String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + queryVectorBo.getKid();
|
||||
|
||||
@@ -7,6 +7,7 @@ import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||
import io.weaviate.client.WeaviateClient;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.config.VectorStoreProperties;
|
||||
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
@@ -41,8 +42,9 @@ public class WeaviateVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||
private WeaviateClient client;
|
||||
|
||||
public WeaviateVectorStoreStrategy(VectorStoreProperties vectorStoreProperties,
|
||||
IChatModelService chatModelService,
|
||||
EmbeddingModelFactory embeddingModelFactory) {
|
||||
super(vectorStoreProperties, embeddingModelFactory);
|
||||
super(vectorStoreProperties, embeddingModelFactory,chatModelService);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,12 +93,12 @@ public class WeaviateVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||
@Override
|
||||
public void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) {
|
||||
createSchema(storeEmbeddingBo.getKid(), storeEmbeddingBo.getEmbeddingModelName());
|
||||
EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName(), null);
|
||||
EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName());
|
||||
List<String> chunkList = storeEmbeddingBo.getChunkList();
|
||||
List<String> fidList = storeEmbeddingBo.getFids();
|
||||
String kid = storeEmbeddingBo.getKid();
|
||||
String docId = storeEmbeddingBo.getDocId();
|
||||
log.info("向量存储条数记录: " + chunkList.size());
|
||||
log.info("向量存储条数记录: {}", chunkList.size());
|
||||
long startTime = System.currentTimeMillis();
|
||||
for (int i = 0; i < chunkList.size(); i++) {
|
||||
String text = chunkList.get(i);
|
||||
@@ -123,7 +125,7 @@ public class WeaviateVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||
@Override
|
||||
public List<String> getQueryVector(QueryVectorBo queryVectorBo) {
|
||||
createSchema(queryVectorBo.getKid(), queryVectorBo.getEmbeddingModelName());
|
||||
EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName(), null);
|
||||
EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName());
|
||||
Embedding queryEmbedding = embeddingModel.embed(queryVectorBo.getQuery()).content();
|
||||
float[] vector = queryEmbedding.vector();
|
||||
List<String> vectorStrings = new ArrayList<>();
|
||||
|
||||
Reference in New Issue
Block a user