doc/sql/20260609_knowledge_base_vector.sql
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,30 @@ -- ç¥è¯åºåéæ£ç´¢åè½æ°æ®åºåæ´ -- æ§è¡åè¯·ç¡®ä¿ knowledge_base 表已åå¨ -- 1. knowledge_base 表å¢å åæ®µ ALTER TABLE knowledge_base ADD COLUMN IF NOT EXISTS file_count INT DEFAULT 0 COMMENT 'æä»¶æ°é', ADD COLUMN IF NOT EXISTS total_chunk_count INT DEFAULT 0 COMMENT 'æ»åçæ°é', ADD COLUMN IF NOT EXISTS description VARCHAR(500) COMMENT 'ç¥è¯åºæè¿°'; -- 2. å建ç¥è¯åºæä»¶åéè®°å½è¡¨ CREATE TABLE IF NOT EXISTS knowledge_base_vector ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主é®ID', knowledge_base_id BIGINT NOT NULL COMMENT 'å ³èç¥è¯åºID', storage_blob_id BIGINT NOT NULL COMMENT 'å ³èæä»¶blob ID', file_name VARCHAR(255) NOT NULL COMMENT 'æä»¶åç§°', file_type VARCHAR(50) NOT NULL COMMENT 'æä»¶ç±»å(docx/pdf/xlsx/txtç)', vector_status TINYINT DEFAULT 0 COMMENT 'åéåç¶æ: 0-å¾ å¤ç, 1-å¤çä¸, 2-已宿, 3-失败', vector_error VARCHAR(500) COMMENT 'åéå失败åå ', chunk_count INT DEFAULT 0 COMMENT 'åçæ°é', namespace VARCHAR(100) COMMENT 'åéå½å空é´', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', create_user INT COMMENT 'å建人', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', update_user INT COMMENT 'æ´æ°äºº', tenant_id BIGINT COMMENT 'ç§æ·ID', dept_id BIGINT COMMENT 'é¨é¨ID', INDEX idx_knowledge_base_id (knowledge_base_id), INDEX idx_storage_blob_id (storage_blob_id), INDEX idx_vector_status (vector_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ç¥è¯åºæä»¶åéè®°å½è¡¨'; doc/֪ʶ¿âRAG¹¦ÄÜʵÏÖÎĵµ.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,1034 @@ # ç¥è¯åºRAGåéæ£ç´¢åè½å®ç°ææ¡£ ## ä¸ãåè½æ¦è¿° åºäº RAGï¼Retrieval-Augmented Generationï¼ææ¯å®ç°ç¥è¯åºé®çåè½ï¼æ¯æï¼ - ç¥è¯åºç®¡çï¼CRUDï¼ - æä»¶ä¸ä¼ ä¸åéåå¤ç - åºäºåéæ£ç´¢çæºè½é®ç - å¤ç§æä»¶æ ¼å¼æ¯æï¼txtãmdãdocxãxlsxãxlsãpdfï¼ ## äºãææ¯æ¶æ ### 2.1 ææ¯æ | ç»ä»¶ | ææ¯ | |------|------| | åéæ°æ®åº | Pinecone | | Embedding模å | é¿éäº DashScope text-embedding-v3 | | LLM | é¿éäºéä¹åé® qwen-max | | æ¡æ¶ | langchain4j | | ORM | MyBatis-Plus | ### 2.2 æ¶æå¾ ``` âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â å端åºç¨ â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â â¼ âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â Controller Layer â â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â â â KnowledgeBaseCtrl â â KnowledgeChatController â â â â (ç¥è¯åºç®¡ç) â â (ç¥è¯åºé®ç) â â â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â â¼ âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â Service Layer â â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â â âKnowledgeBaseService â â KnowledgeRagService â â â â (ç¥è¯åºCRUD) â â (åéå/æ£ç´¢) â â â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â â¼ âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â AI Layer â â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â â â KnowledgeChatAgent â â EmbeddingStore (Pinecone) â â â â (é®çAgent) â â (åéåå¨) â â â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââ â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ ``` --- ## ä¸ãå端å®ç° ### 3.1 æ°æ®åºè®¾è®¡ #### 3.1.1 ç¥è¯åºè¡¨ï¼knowledge_baseï¼ ```sql CREATE TABLE knowledge_base ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) COMMENT 'ç¥è¯æ é¢', type VARCHAR(50) COMMENT 'ç¥è¯ç±»å', scenario VARCHAR(255) COMMENT 'éç¨åºæ¯', efficiency VARCHAR(20) COMMENT 'è§£å³æç', problem TEXT COMMENT 'é®é¢æè¿°', solution TEXT COMMENT 'è§£å³æ¹æ¡', key_points TEXT COMMENT 'å ³é®è¦ç¹', creator VARCHAR(100) COMMENT 'å建人', usage_count INT DEFAULT 0 COMMENT 'ä½¿ç¨æ¬¡æ°', file_count INT DEFAULT 0 COMMENT 'æä»¶æ°é', total_chunk_count INT DEFAULT 0 COMMENT 'æ»åçæ°é', description VARCHAR(500) COMMENT 'ç¥è¯åºæè¿°', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, create_user INT, update_user INT, tenant_id BIGINT, dept_id BIGINT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ç¥è¯åºè¡¨'; ``` #### 3.1.2 ç¥è¯åºåéè®°å½è¡¨ï¼knowledge_base_vectorï¼ ```sql CREATE TABLE knowledge_base_vector ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主é®ID', knowledge_base_id BIGINT NOT NULL COMMENT 'å ³èç¥è¯åºID', storage_blob_id BIGINT NOT NULL COMMENT 'å ³èæä»¶blob ID', file_name VARCHAR(255) NOT NULL COMMENT 'æä»¶åç§°', file_type VARCHAR(50) NOT NULL COMMENT 'æä»¶ç±»å', vector_status TINYINT DEFAULT 0 COMMENT 'åéåç¶æ: 0-å¾ å¤ç, 1-å¤çä¸, 2-已宿, 3-失败', vector_error VARCHAR(500) COMMENT 'åéå失败åå ', chunk_count INT DEFAULT 0 COMMENT 'åçæ°é', namespace VARCHAR(100) COMMENT 'åéå½å空é´', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, create_user INT, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, update_user INT, tenant_id BIGINT, dept_id BIGINT, INDEX idx_knowledge_base_id (knowledge_base_id), INDEX idx_storage_blob_id (storage_blob_id), INDEX idx_vector_status (vector_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ç¥è¯åºæä»¶åéè®°å½è¡¨'; ``` ### 3.2 Mavenä¾èµ ```xml <!-- langchain4j BOM --> <dependencyManagement> <dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-bom</artifactId> <version>1.0.0-beta3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- langchain4j æ ¸å¿ --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> </dependency> <!-- Pinecone åéæ°æ®åº --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-pinecone</artifactId> </dependency> <!-- é¿éäº DashScope --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId> </dependency> </dependencies> ``` ### 3.3 é ç½®æä»¶ï¼application.ymlï¼ ```yaml # Pinecone åéæ°æ®åºé ç½® pinecone: api-key: your-pinecone-api-key index: your-index-name namespace: knowledge-base # langchain4j é ç½® langchain4j: community: dashscope: streaming-chat-model: api-key: your-dashscope-api-key model-name: "qwen-max" embedding-model: api-key: your-dashscope-api-key model-name: "text-embedding-v3" ``` ### 3.4 æ ¸å¿ä»£ç å®ç° #### 3.4.1 å®ä½ç±» **KnowledgeBase.java** ```java @Data @TableName("knowledge_base") public class KnowledgeBase implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String title; private String type; private String scenario; private String efficiency; private String problem; private String solution; private String keyPoints; private String creator; private Integer usageCount; private Integer fileCount; private Integer totalChunkCount; private String description; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableField(fill = FieldFill.INSERT) private Integer createUser; @TableField(fill = FieldFill.INSERT_UPDATE) private Integer updateUser; @TableField(fill = FieldFill.INSERT) private Long tenantId; @TableField(fill = FieldFill.INSERT) private Long deptId; } ``` **KnowledgeBaseVector.java** ```java @Data @TableName("knowledge_base_vector") public class KnowledgeBaseVector implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long knowledgeBaseId; private Long storageBlobId; private String fileName; private String fileType; private Integer vectorStatus; private String vectorError; private Integer chunkCount; private String namespace; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT) private Integer createUser; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Integer updateUser; @TableField(fill = FieldFill.INSERT) private Long tenantId; @TableField(fill = FieldFill.INSERT) private Long deptId; // åéåç¶æå¸¸é public static final int STATUS_PENDING = 0; public static final int STATUS_PROCESSING = 1; public static final int STATUS_COMPLETED = 2; public static final int STATUS_FAILED = 3; } ``` #### 3.4.2 EmbeddingStoreé ç½® **EmbeddingStoreConfig.java** ```java @Configuration public class EmbeddingStoreConfig { @Value("${pinecone.api-key}") private String pineconeApiKey; @Value("${pinecone.index}") private String indexName; @Value("${pinecone.namespace}") private String namespace; @Bean public Pinecone pinecone() { return new Pinecone.Builder(pineconeApiKey).build(); } @Bean public Index pineconeIndex(Pinecone pinecone) { return pinecone.getIndexConnection(indexName); } @Bean public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) { return PineconeEmbeddingStore.builder() .apiKey(pineconeApiKey) .index(indexName) .nameSpace(namespace) .createIndex(PineconeServerlessIndexConfig.builder() .cloud("AWS") .region("us-east-1") .dimension(embeddingModel.dimension()) .build()) .build(); } } ``` #### 3.4.3 RAGæå¡å®ç° **KnowledgeRagService.java** ```java public interface KnowledgeRagService { void processVectorAsync(Long vectorId); void processVector(Long vectorId); List<String> searchRelevantContent(String namespace, String query, int maxResults); void deleteEmbeddings(String namespace, Long storageBlobId); } ``` **KnowledgeRagServiceImpl.java**ï¼æ ¸å¿å®ç°ï¼ ```java @Slf4j @Service public class KnowledgeRagServiceImpl implements KnowledgeRagService { private final KnowledgeBaseVectorService knowledgeBaseVectorService; private final StorageBlobService storageBlobService; private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; private final FileProperties fileProperties; private final Index pineconeIndex; @Value("${pinecone.namespace}") private String namespace; private static final int CHUNK_SIZE = 500; private static final int CHUNK_OVERLAP = 100; private static final long CHUNK_THRESHOLD_BYTES = 80L * 1024 * 1024; private static final int EMBEDDING_MAX_LENGTH = 8000; @Override @Async("threadPoolTaskExecutor") public void processVectorAsync(Long vectorId) { processVector(vectorId); } @Override public void processVector(Long vectorId) { KnowledgeBaseVector vector = knowledgeBaseVectorService.getById(vectorId); if (vector == null) return; try { // æ´æ°ç¶æä¸ºå¤çä¸ knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_PROCESSING, null, null); // è·åæä»¶å 容 StorageBlob blob = storageBlobService.getById(vector.getStorageBlobId()); File file = getFile(blob); String content = extractFileContent(file, vector.getFileName()); if (content == null || content.trim().isEmpty()) { throw new RuntimeException("æä»¶å 容为空"); } // ææ¬åç List<TextSegment> chunks; boolean needChunk = file.length() > CHUNK_THRESHOLD_BYTES || content.length() > EMBEDDING_MAX_LENGTH; if (needChunk) { chunks = splitText(content, vector); } else { Map<String, Object> metadata = buildMetadata(vector); chunks = List.of(TextSegment.from(content, new Metadata(metadata))); } // çæåµå ¥åéå¹¶åå¨ int chunkCount = 0; for (TextSegment chunk : chunks) { Embedding embedding = embeddingModel.embed(chunk).content(); embeddingStore.add(embedding, chunk); chunkCount++; } // æ´æ°ç¶æä¸ºå®æ knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_COMPLETED, chunkCount, null); } catch (Exception e) { log.error("åéåå¤ç失败", e); knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_FAILED, null, e.getMessage()); } } @Override public List<String> searchRelevantContent(String namespace, String query, int maxResults) { Embedding queryEmbedding = embeddingModel.embed(query).content(); EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(maxResults) .minScore(0.7) .build(); EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest); return searchResult.matches().stream() .map(match -> match.embedded().text()) .collect(Collectors.toList()); } @Override public void deleteEmbeddings(String namespace, Long storageBlobId) { Struct filter = Struct.newBuilder() .putFields("storageBlobId", Value.newBuilder() .setStructValue(Struct.newBuilder() .putFields("$eq", Value.newBuilder() .setNumberValue(storageBlobId.doubleValue()) .build())) .build()) .build(); pineconeIndex.delete(new ArrayList<>(), false, this.namespace, filter); } private String extractFileContent(File file, String fileName) throws Exception { String ext = getFileExtension(fileName); if (isPlainText(ext)) { return readFileWithEncoding(file); } if ("docx".equals(ext)) { return extractDocx(file); } if ("xlsx".equals(ext) || "xls".equals(ext)) { return extractExcel(file); } return readFileWithEncoding(file); } // ... å ¶ä»è¾ 婿¹æ³ } ``` #### 3.4.4 ç¥è¯åºé®çAgent **KnowledgeChatAgent.java** ```java @AiService( wiringMode = EXPLICIT, streamingChatModel = "qwenStreamingChatModel", chatMemoryProvider = "chatMemoryProvider" ) public interface KnowledgeChatAgent { @SystemMessage(""" ä½ æ¯ä¼ä¸ç¥è¯åºé®ç婿ã ä½ éè¦åºäºæä¾çç¥è¯åºå 容åçç¨æ·é®é¢ã éµå¾ªä»¥ä¸è§åï¼ 1. ä¸¥æ ¼åºäºç¥è¯åºå 容åçï¼ä¸è¦ç¼é ä¿¡æ¯ 2. 妿ç¥è¯åºä¸æ²¡æç¸å ³ä¿¡æ¯ï¼æç¡®åç¥ç¨æ· 3. åçè¦åç¡®ãç®æ´ãææ¡ç 4. å¼ç¨æ¥æºæ¶æ³¨æ"æ ¹æ®ç¥è¯åºå 容" """) Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage); } ``` #### 3.4.5 Controllerå± **KnowledgeBaseController.java** ```java @RestController @RequestMapping("/knowledgeBase") @Tag(name = "ç¥è¯åºç®¡ç") public class KnowledgeBaseController { @GetMapping("/getList") public AjaxResult getList(@RequestParam(defaultValue = "1") long current, @RequestParam(defaultValue = "10") long size, KnowledgeBase knowledgeBase) { Page page = new Page(current, size); return AjaxResult.success(knowledgeBaseService.listpage(page, knowledgeBase)); } @PostMapping("/add") public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase) { return AjaxResult.success(knowledgeBaseService.save(knowledgeBase)); } @PostMapping("/update") public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase) { return AjaxResult.success(knowledgeBaseService.updateById(knowledgeBase)); } @DeleteMapping("/delete") public AjaxResult delete(@RequestBody List<Long> ids) { return AjaxResult.success(knowledgeBaseService.removeByIds(ids)); } @GetMapping("/vector/status/{knowledgeBaseId}") @Operation(summary = "æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æ") public AjaxResult getVectorStatus(@PathVariable Long knowledgeBaseId) { return AjaxResult.success(knowledgeBaseVectorService.getVectorStatusByKnowledgeBaseId(knowledgeBaseId)); } @PostMapping("/vector/reprocess/{vectorId}") @Operation(summary = "éæ°åéåæä»¶") public AjaxResult reprocessVector(@PathVariable Long vectorId) { knowledgeBaseVectorService.reprocessVector(vectorId); return AjaxResult.success("已鿰æäº¤åéåä»»å¡"); } @PostMapping("/file/save") @Operation(summary = "ä¿åç¥è¯åºæä»¶å ³è") public AjaxResult saveKnowledgeBaseFiles(@RequestBody KnowledgeBaseFileDTO dto) { // ä¿åéä»¶å ³è并触ååéå // ... } @DeleteMapping("/file/delete") @Operation(summary = "å é¤ç¥è¯åºæä»¶") public AjaxResult deleteKnowledgeBaseFiles(@RequestBody List<Long> vectorIds) { knowledgeBaseVectorService.deleteVectors(vectorIds); return AjaxResult.success(); } } ``` **KnowledgeChatController.java** ```java @RestController @RequestMapping("/ai/knowledge") @Tag(name = "ç¥è¯åºé®ç") public class KnowledgeChatController { private final KnowledgeChatAgent knowledgeChatAgent; private final KnowledgeRagService knowledgeRagService; private final KnowledgeBaseService knowledgeBaseService; @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8") @Operation(summary = "ç¥è¯åºé®ç") public Flux<String> chat(@RequestBody KnowledgeChatRequest request) { // æ£ç´¢ç¸å ³å 容 String namespace = "kb-" + request.getKnowledgeBaseId(); List<String> relevantContents = knowledgeRagService.searchRelevantContent( namespace, request.getQuestion(), 5); if (relevantContents.isEmpty()) { return Flux.just("ç¥è¯åºä¸æªæ¾å°ç¸å ³å 容"); } // æå»ºä¸ä¸æ StringBuilder context = new StringBuilder(); context.append("以䏿¯ä»ç¥è¯åºä¸æ£ç´¢å°çç¸å ³å 容ï¼\n\n"); for (int i = 0; i < relevantContents.size(); i++) { context.append("ãå 容").append(i + 1).append("ã\n"); context.append(relevantContents.get(i)).append("\n\n"); } context.append("---\n请åºäºä»¥ä¸ç¥è¯åºå 容åçï¼\n").append(request.getQuestion()); return knowledgeChatAgent.chat(request.getMemoryId(), context.toString()); } @GetMapping("/list") @Operation(summary = "ç¥è¯åºå表") public AjaxResult listKnowledgeBases() { return AjaxResult.success(knowledgeBaseService.list()); } } ``` --- ## åãAPIæ¥å£ææ¡£ ### 4.1 ç¥è¯åºç®¡çæ¥å£ | æ¥å£ | æ¹æ³ | è·¯å¾ | 说æ | |------|------|------|------| | è·åå表 | GET | /knowledgeBase/getList | å页æ¥è¯¢ç¥è¯åºå表 | | æ°å¢ç¥è¯åº | POST | /knowledgeBase/add | å建ç¥è¯åº | | æ´æ°ç¥è¯åº | POST | /knowledgeBase/update | æ´æ°ç¥è¯åºä¿¡æ¯ | | å é¤ç¥è¯åº | DELETE | /knowledgeBase/delete | æ¹éå é¤ç¥è¯åº | | æ¥è¯¢åéåç¶æ | GET | /knowledgeBase/vector/status/{id} | æ¥è¯¢æä»¶åéåç¶æ | | éæ°åéå | POST | /knowledgeBase/vector/reprocess/{id} | éæ°å¤ç失败çæä»¶ | | ä¿åæä»¶å ³è | POST | /knowledgeBase/file/save | ä¸ä¼ æä»¶åå ³èå°ç¥è¯åº | | å 餿件 | DELETE | /knowledgeBase/file/delete | å é¤ç¥è¯åºæä»¶ | ### 4.2 ç¥è¯åºé®çæ¥å£ | æ¥å£ | æ¹æ³ | è·¯å¾ | 说æ | |------|------|------|------| | ç¥è¯åºé®ç | POST | /ai/knowledge/chat | æµå¼è¿åé®çç»æ | | ç¥è¯åºå表 | GET | /ai/knowledge/list | è·åå¯éç¥è¯åºå表 | ### 4.3 æ¥å£è¯¦ç»è¯´æ #### 4.3.1 ä¿åç¥è¯åºæä»¶å ³è **请æ±** ```json POST /knowledgeBase/file/save { "knowledgeBaseId": 1, "storageBlobIds": [100, 101, 102] } ``` **ååº** ```json { "code": 200, "msg": "æä½æå" } ``` #### 4.3.2 ç¥è¯åºé®ç **请æ±** ```json POST /ai/knowledge/chat Content-Type: application/json { "knowledgeBaseId": 1, "memoryId": "session-uuid", "question": "å¦ä½å¤çåºåçç¹å·®å¼ï¼" } ``` **ååº**ï¼SSEæµå¼ï¼ ``` æ ¹æ®ç¥è¯åºå 容ï¼åºåçç¹å·®å¼çå¤çæµç¨å¦ä¸ï¼ 1. åç°å·®å¼åï¼é¦å æ ¸å¯¹çç¹è®°å½... 2. æ£æ¥æ¯å¦ææ¼çæéç... 3. ... ``` --- ## äºãå端å®ç° ### 5.1 ç¥è¯åºç®¡çé¡µé¢ ```vue <template> <div class="knowledge-base"> <!-- å表 --> <el-table :data="tableData" border> <el-table-column prop="title" label="ç¥è¯æ é¢" /> <el-table-column prop="type" label="ç¥è¯ç±»å" /> <el-table-column prop="fileCount" label="æä»¶æ°é" /> <el-table-column prop="totalChunkCount" label="åçæ°é" /> <el-table-column label="æä½"> <template #default="{ row }"> <el-button @click="handleEdit(row)">ç¼è¾</el-button> <el-button @click="handleFiles(row)">æä»¶ç®¡ç</el-button> <el-button @click="handleChat(row)">é®ç</el-button> <el-button type="danger" @click="handleDelete(row)">å é¤</el-button> </template> </el-table-column> </el-table> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { getKnowledgeBaseList, deleteKnowledgeBase } from '@/api/knowledge' const tableData = ref([]) const loadData = async () => { const res = await getKnowledgeBaseList({ current: 1, size: 10 }) tableData.value = res.data.records } onMounted(loadData) </script> ``` ### 5.2 æä»¶ä¸ä¼ ä¸åéåç¶æ ```vue <template> <div class="file-manager"> <!-- æä»¶ä¸ä¼ --> <el-upload :action="uploadUrl" :on-success="handleUploadSuccess" multiple > <el-button type="primary">ä¸ä¼ æä»¶</el-button> </el-upload> <!-- æä»¶å表ä¸åéåç¶æ --> <el-table :data="fileList"> <el-table-column prop="fileName" label="æä»¶å" /> <el-table-column label="åéåç¶æ"> <template #default="{ row }"> <el-tag :type="getStatusType(row.vectorStatus)"> {{ getStatusText(row.vectorStatus) }} </el-tag> </template> </el-table-column> <el-table-column prop="chunkCount" label="åçæ°" /> <el-table-column label="æä½"> <template #default="{ row }"> <el-button v-if="row.vectorStatus === 3" @click="reprocess(row)"> éæ°å¤ç </el-button> <el-button type="danger" @click="deleteFile(row)">å é¤</el-button> </template> </el-table-column> </el-table> </div> </template> <script setup> const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/upload' // ä¸ä¼ æååä¿åå ³è const uploadedBlobIds = ref([]) const handleUploadSuccess = (response, file) => { if (response.code === 200) { uploadedBlobIds.value.push(response.data.id) } } // ä¿åæä»¶å ³è const saveFiles = async () => { await saveKnowledgeBaseFiles({ knowledgeBaseId: props.knowledgeBaseId, storageBlobIds: uploadedBlobIds.value }) // å·æ°æä»¶å表 loadFileList() } // ç¶æææ¬æ å° const getStatusText = (status) => { const map = { 0: 'å¾ å¤ç', 1: 'å¤çä¸', 2: '已宿', 3: '失败' } return map[status] || 'æªç¥' } const getStatusType = (status) => { const map = { 0: 'info', 1: 'warning', 2: 'success', 3: 'danger' } return map[status] || 'info' } </script> ``` ### 5.3 ç¥è¯åºé®ççé¢ ```vue <template> <div class="knowledge-chat"> <!-- ç¥è¯åºéæ© --> <el-select v-model="selectedKbId" placeholder="éæ©ç¥è¯åº"> <el-option v-for="kb in knowledgeBases" :key="kb.id" :label="kb.title" :value="kb.id" /> </el-select> <!-- 对è¯åºå --> <div class="chat-messages"> <div v-for="(msg, index) in messages" :key="index" :class="['message', msg.role]" > <div class="content">{{ msg.content }}</div> </div> </div> <!-- è¾å ¥æ¡ --> <el-input v-model="inputQuestion" placeholder="请è¾å ¥é®é¢" @keyup.enter="sendMessage" > <template #append> <el-button @click="sendMessage" :loading="loading">åé</el-button> </template> </el-input> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { getKnowledgeBaseList, knowledgeChat } from '@/api/knowledge' const knowledgeBases = ref([]) const selectedKbId = ref(null) const messages = ref([]) const inputQuestion = ref('') const loading = ref(false) const memoryId = ref(crypto.randomUUID()) const sendMessage = async () => { if (!inputQuestion.value.trim()) return if (!selectedKbId.value) { ElMessage.warning('è¯·éæ©ç¥è¯åº') return } // æ·»å ç¨æ·æ¶æ¯ messages.value.push({ role: 'user', content: inputQuestion.value }) loading.value = true try { // æµå¼è¯·æ± const response = await fetch('/api/ai/knowledge/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ knowledgeBaseId: selectedKbId.value, memoryId: memoryId.value, question: inputQuestion.value }) }) // å¤çSSEæµå¼ååº const reader = response.body.getReader() const decoder = new TextDecoder() let aiContent = '' messages.value.push({ role: 'assistant', content: '' }) while (true) { const { done, value } = await reader.read() if (done) break const text = decoder.decode(value) aiContent += text messages.value[messages.value.length - 1].content = aiContent } } finally { loading.value = false inputQuestion.value = '' } } onMounted(async () => { const res = await getKnowledgeBaseList() knowledgeBases.value = res.data }) </script> ``` ### 5.4 APIå°è£ ```javascript // api/knowledge.js import request from '@/utils/request' // è·åç¥è¯åºå表 export function getKnowledgeBaseList(params) { return request({ url: '/knowledgeBase/getList', method: 'get', params }) } // æ°å¢ç¥è¯åº export function addKnowledgeBase(data) { return request({ url: '/knowledgeBase/add', method: 'post', data }) } // æ´æ°ç¥è¯åº export function updateKnowledgeBase(data) { return request({ url: '/knowledgeBase/update', method: 'post', data }) } // å é¤ç¥è¯åº export function deleteKnowledgeBase(ids) { return request({ url: '/knowledgeBase/delete', method: 'delete', data: ids }) } // è·åæä»¶åéåç¶æ export function getVectorStatus(knowledgeBaseId) { return request({ url: `/knowledgeBase/vector/status/${knowledgeBaseId}`, method: 'get' }) } // éæ°åéå export function reprocessVector(vectorId) { return request({ url: `/knowledgeBase/vector/reprocess/${vectorId}`, method: 'post' }) } // ä¿åæä»¶å ³è export function saveKnowledgeBaseFiles(data) { return request({ url: '/knowledgeBase/file/save', method: 'post', data }) } // å 餿件 export function deleteKnowledgeBaseFiles(vectorIds) { return request({ url: '/knowledgeBase/file/delete', method: 'delete', data: vectorIds }) } // ç¥è¯åºé®çï¼æµå¼ï¼ export async function knowledgeChat(data) { const response = await fetch('/api/ai/knowledge/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) return response.body } // è·åç¥è¯åºå表ï¼é®çç¨ï¼ export function getKnowledgeBaseListForChat() { return request({ url: '/ai/knowledge/list', method: 'get' }) } ``` --- ## å ãæ ¸å¿æµç¨ ### 6.1 æä»¶ä¸ä¼ ä¸åéåæµç¨ ``` 1. å端è°ç¨ /common/upload ä¸ä¼ æä»¶ â è¿å storageBlobId 2. å端è°ç¨ /knowledgeBase/file/save å ³èæä»¶å°ç¥è¯åº 3. å端å建 KnowledgeBaseVector è®°å½ï¼ç¶æï¼å¾ å¤çï¼ 4. åç«¯å¼æ¥è°ç¨ KnowledgeRagService.processVectorAsync() âââ æ´æ°ç¶æä¸º"å¤çä¸" âââ æåæä»¶å å®¹ï¼æ¯æå¤ç§æ ¼å¼ï¼ âââ èªå¨æ£æµæä»¶ç¼ç ï¼UTF-8/GBKï¼ âââ ææ¬åçï¼å¤§æä»¶æé¿å 容æåçï¼ âââ çæ Embedding åé âââ åå¨å° Pinecone âââ æ´æ°ç¶æä¸º"宿"æ"失败" ``` ### 6.2 ç¥è¯åºé®çæµç¨ ``` 1. ç¨æ·éæ©ç¥è¯åºï¼è¾å ¥é®é¢ 2. å端è°ç¨ /ai/knowledge/chatï¼æµå¼æ¥å£ï¼ 3. å端å¤çï¼ âââ æå»ºå½å空é´ï¼kb-{knowledgeBaseId} âââ è°ç¨ Embedding 模åçæé®é¢åé âââ ä» Pinecone æ£ç´¢ç¸å ³å 容ï¼minScore=0.7, maxResults=5ï¼ âââ æå»ºä¸ä¸æ Prompt âââ è°ç¨ LLM çæåç âââ æµå¼è¿åç»æ ``` --- ## ä¸ã注æäºé¡¹ 1. **Pinecone å½å空é´**ï¼ä¸è½ä½¿ç¨ `__default__`ï¼å¿ 须使ç¨èªå®ä¹å½åç©ºé´ 2. **æä»¶ç¼ç **ï¼èªå¨æ£æµ UTF-8/GBKï¼é¿å ä¹±ç 3. **åççç¥**ï¼ - æä»¶ > 80MB æå 容 > 8000 åç¬¦æ¶æåç - åçå¤§å° 500 å符ï¼éå 100 å符 - ä¼å å¨å¥åè¾¹çåå 4. **Embedding éå¶**ï¼é¿éäº DashScope éå¶å次è¾å ¥æå¤§ 8192 å符 5. **åéå é¤**ï¼ä½¿ç¨ Pinecone åç客æ·ç«¯ï¼éè¿ metadata filter å é¤ 6. **弿¥å¤ç**ï¼åéåä½¿ç¨ `@Async` 弿¥æ§è¡ï¼é¿å é»å¡æ¥å£ --- ## å «ãæä»¶æ¸ å ### å端æä»¶ ``` src/main/java/com/ruoyi/ âââ approve/ â âââ controller/ â â âââ KnowledgeBaseController.java â âââ pojo/ â â âââ KnowledgeBase.java â â âââ KnowledgeBaseVector.java â âââ service/ â â âââ KnowledgeBaseService.java â â âââ KnowledgeBaseVectorService.java â â âââ impl/ â â âââ KnowledgeBaseServiceImpl.java â â âââ KnowledgeBaseVectorServiceImpl.java â âââ mapper/ â â âââ KnowledgeBaseMapper.java â â âââ KnowledgeBaseVectorMapper.java â âââ dto/ â âââ KnowledgeBaseVectorVO.java âââ ai/ âââ config/ â âââ EmbeddingStoreConfig.java â âââ XiaozhiAgentConfig.java âââ controller/ â âââ KnowledgeChatController.java âââ assistant/ â âââ KnowledgeChatAgent.java âââ service/ â âââ KnowledgeRagService.java â âââ impl/ â âââ KnowledgeRagServiceImpl.java âââ dto/ âââ KnowledgeChatRequest.java ``` ### å端æä»¶ ``` src/views/knowledge/ âââ index.vue # ç¥è¯åºå表 âââ form.vue # æ°å¢/ç¼è¾è¡¨å âââ files.vue # æä»¶ç®¡ç âââ chat.vue # ç¥è¯åºé®ç src/api/knowledge.js # APIå°è£ ``` src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,35 @@ package com.ruoyi.ai.assistant; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.spring.AiService; import reactor.core.publisher.Flux; import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT; /** * ç¥è¯åºé®çAgent * åºäºRAGæ£ç´¢å¢å¼ºçæ */ @AiService( wiringMode = EXPLICIT, streamingChatModel = "qwenStreamingChatModel", chatMemoryProvider = "chatMemoryProviderXiaozhi" ) public interface KnowledgeChatAgent { @SystemMessage(""" ä½ æ¯ä¼ä¸ç¥è¯åºé®ç婿ã ä½ éè¦åºäºæä¾çç¥è¯åºå 容åçç¨æ·é®é¢ã éµå¾ªä»¥ä¸è§åï¼ 1. ä¸¥æ ¼åºäºç¥è¯åºå 容åçï¼ä¸è¦ç¼é ä¿¡æ¯ 2. 妿ç¥è¯åºä¸æ²¡æç¸å ³ä¿¡æ¯ï¼æç¡®åç¥ç¨æ· 3. åçè¦åç¡®ãç®æ´ãææ¡ç 4. 妿å 容è¾å¤ï¼ä½¿ç¨åç¹åè¡¨å½¢å¼ 5. å¼ç¨æ¥æºæ¶æ³¨æ"æ ¹æ®ç¥è¯åºå 容" """) Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage); } src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
@@ -5,31 +5,47 @@ import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.pinecone.PineconeEmbeddingStore; import dev.langchain4j.store.embedding.pinecone.PineconeServerlessIndexConfig; import org.springframework.beans.factory.annotation.Autowired; import io.pinecone.clients.Index; import io.pinecone.clients.Pinecone; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author :yys * @date : 2025/5/2 21:07 * åéåå¨é ç½® */ @Configuration public class EmbeddingStoreConfig { @Autowired private EmbeddingModel embeddingModel; @Value("${pinecone.api-key:pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9}") private String pineconeApiKey; @Value("${pinecone.index:xiaozhi-index}") private String indexName; @Value("${pinecone.namespace:knowledge-base}") private String namespace; @Bean public EmbeddingStore<TextSegment> embeddingStore() { //å建åéåå¨ public Pinecone pinecone() { return new Pinecone.Builder(pineconeApiKey).build(); } @Bean public Index pineconeIndex(Pinecone pinecone) { return pinecone.getIndexConnection(indexName); } @Bean public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) { return PineconeEmbeddingStore.builder() .apiKey("pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9") .index("xiaozhi-index")//妿æå®çç´¢å¼ä¸åå¨ï¼å°å建ä¸ä¸ªæ°çç´¢å¼ .nameSpace("xiaozhi-namespace") //妿æå®çå称空é´ä¸åå¨ï¼å°å建ä¸ä¸ªæ°çåç§° ç©ºé´ .apiKey(pineconeApiKey) .index(indexName) .nameSpace(namespace) .createIndex(PineconeServerlessIndexConfig.builder() .cloud("AWS") //æå®ç´¢å¼é¨ç½²å¨ AWS äºæå¡ä¸ã .region("us-east-1") //æå®ç´¢å¼æå¨ç AWS åºå为 us-east-1ã .dimension(embeddingModel.dimension()) //æå®ç´¢å¼çåé维度ï¼è¯¥ç»´åº¦ä¸ embeddedModel çæçåé维度ç¸åã .cloud("AWS") .region("us-east-1") .dimension(embeddingModel.dimension()) .build()) .build(); } src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,85 @@ package com.ruoyi.ai.controller; import com.ruoyi.ai.assistant.KnowledgeChatAgent; import com.ruoyi.ai.dto.KnowledgeChatRequest; import com.ruoyi.ai.service.KnowledgeRagService; import com.ruoyi.approve.pojo.KnowledgeBase; import com.ruoyi.approve.service.KnowledgeBaseService; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.web.domain.AjaxResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import java.util.List; /** * ç¥è¯åºé®çController */ @Slf4j @RestController @RequestMapping("/ai/knowledge") @RequiredArgsConstructor @Tag(name = "ç¥è¯åºé®ç") public class KnowledgeChatController { private final KnowledgeChatAgent knowledgeChatAgent; private final KnowledgeRagService knowledgeRagService; private final KnowledgeBaseService knowledgeBaseService; /** * ç¥è¯åºé®çï¼æµå¼è¿åï¼ */ @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8") @Operation(summary = "ç¥è¯åºé®ç") public Flux<String> chat(@RequestBody KnowledgeChatRequest request) { if (request.getKnowledgeBaseId() == null) { return Flux.just("ç¥è¯åºIDä¸è½ä¸ºç©º"); } if (!StringUtils.hasText(request.getMemoryId())) { return Flux.just("ä¼è¯IDä¸è½ä¸ºç©º"); } if (!StringUtils.hasText(request.getQuestion())) { return Flux.just("é®é¢ä¸è½ä¸ºç©º"); } KnowledgeBase knowledgeBase = knowledgeBaseService.getById(request.getKnowledgeBaseId()); if (knowledgeBase == null) { return Flux.just("ç¥è¯åºä¸åå¨"); } String namespace = "kb-" + request.getKnowledgeBaseId(); List<String> relevantContents = knowledgeRagService.searchRelevantContent( namespace, request.getQuestion(), 5); if (relevantContents.isEmpty()) { return Flux.just("ç¥è¯åºä¸æªæ¾å°ç¸å ³å 容ï¼è¯·å ä¸ä¼ ç¸å ³ææ¡£ã"); } StringBuilder contextBuilder = new StringBuilder(); contextBuilder.append("以䏿¯ä»ç¥è¯åºä¸æ£ç´¢å°çç¸å ³å 容ï¼\n\n"); for (int i = 0; i < relevantContents.size(); i++) { contextBuilder.append("ãå 容").append(i + 1).append("ã\n"); contextBuilder.append(relevantContents.get(i)).append("\n\n"); } contextBuilder.append("---\n"); contextBuilder.append("请åºäºä»¥ä¸ç¥è¯åºå 容åçç¨æ·é®é¢ï¼\n"); contextBuilder.append(request.getQuestion()); return knowledgeChatAgent.chat(request.getMemoryId(), contextBuilder.toString()); } /** * ç¥è¯åºå表ï¼ç¨äºéæ©ç¥è¯åºï¼ */ @GetMapping("/list") @Operation(summary = "ç¥è¯åºå表") public AjaxResult listKnowledgeBases() { List<KnowledgeBase> list = knowledgeBaseService.list(); return AjaxResult.success(list); } } src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,21 @@ package com.ruoyi.ai.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** * ç¥è¯åºé®çè¯·æ± */ @Data @Schema(description = "ç¥è¯åºé®ç请æ±") public class KnowledgeChatRequest { @Schema(description = "ç¥è¯åºID", required = true) private Long knowledgeBaseId; @Schema(description = "ä¼è¯IDï¼ç¨äºä¿æä¸ä¸æ", required = true) private String memoryId; @Schema(description = "ç¨æ·æé®å 容", required = true) private String question; } src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,34 @@ package com.ruoyi.ai.service; import java.util.List; /** * ç¥è¯åºRAGæå¡ * è´è´£æä»¶åéåå¤çåæ£ç´¢ */ public interface KnowledgeRagService { /** * 弿¥å¤çåéå */ void processVectorAsync(Long vectorId); /** * 忥å¤çåéå */ void processVector(Long vectorId); /** * æ£ç´¢ç¸å ³å 容 * @param namespace å½åç©ºé´ * @param query æ¥è¯¢ææ¬ * @param maxResults æå¤§ç»ææ° * @return ç¸å ³å 容å表 */ List<String> searchRelevantContent(String namespace, String query, int maxResults); /** * å 餿宿件çåéæ°æ® */ void deleteEmbeddings(String namespace, Long storageBlobId); } src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,343 @@ package com.ruoyi.ai.service.impl; import com.ruoyi.ai.service.KnowledgeRagService; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import com.ruoyi.approve.service.KnowledgeBaseVectorService; import com.ruoyi.basic.pojo.StorageBlob; import com.ruoyi.basic.service.StorageBlobService; import com.ruoyi.common.config.FileProperties; import com.google.protobuf.Struct; import com.google.protobuf.Value; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingSearchRequest; import dev.langchain4j.store.embedding.EmbeddingSearchResult; import dev.langchain4j.store.embedding.EmbeddingStore; import io.pinecone.clients.Index; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.io.File; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * ç¥è¯åºRAGæå¡å®ç° */ @Slf4j @Service public class KnowledgeRagServiceImpl implements KnowledgeRagService { private final KnowledgeBaseVectorService knowledgeBaseVectorService; private final StorageBlobService storageBlobService; private final EmbeddingModel embeddingModel; private final EmbeddingStore<TextSegment> embeddingStore; private final FileProperties fileProperties; private final Index pineconeIndex; @Value("${pinecone.namespace:knowledge-base}") private String namespace; public KnowledgeRagServiceImpl( KnowledgeBaseVectorService knowledgeBaseVectorService, StorageBlobService storageBlobService, EmbeddingModel embeddingModel, EmbeddingStore<TextSegment> embeddingStore, FileProperties fileProperties, Index pineconeIndex) { this.knowledgeBaseVectorService = knowledgeBaseVectorService; this.storageBlobService = storageBlobService; this.embeddingModel = embeddingModel; this.embeddingStore = embeddingStore; this.fileProperties = fileProperties; this.pineconeIndex = pineconeIndex; } private static final int CHUNK_SIZE = 500; private static final int CHUNK_OVERLAP = 100; private static final long CHUNK_THRESHOLD_BYTES = 80L * 1024 * 1024; private static final int EMBEDDING_MAX_LENGTH = 8000; @Override @Async("threadPoolTaskExecutor") public void processVectorAsync(Long vectorId) { log.info("å¼å§å¼æ¥åéåå¤ç: vectorId={}, thread={}", vectorId, Thread.currentThread().getName()); processVector(vectorId); } @Override public void processVector(Long vectorId) { log.info("å¼å§å¤çåéå: vectorId={}", vectorId); KnowledgeBaseVector vector = knowledgeBaseVectorService.getById(vectorId); if (vector == null) { log.error("åéè®°å½ä¸åå¨: {}", vectorId); return; } try { knowledgeBaseVectorService.updateVectorStatus(vectorId, KnowledgeBaseVector.STATUS_PROCESSING, null, null); StorageBlob blob = storageBlobService.getById(vector.getStorageBlobId()); if (blob == null) { throw new RuntimeException("æä»¶ä¸åå¨: " + vector.getStorageBlobId()); } File file = getFile(blob); log.info("æä»¶è·¯å¾: {}, æ¯å¦åå¨: {}", file.getAbsolutePath(), file.exists()); long fileSize = file.length(); String content = extractFileContent(file, vector.getFileName()); log.info("æä»¶å 容é¿åº¦: {}", content != null ? content.length() : 0); if (content == null || content.trim().isEmpty()) { throw new RuntimeException("æä»¶å 容为空"); } List<TextSegment> chunks; boolean needChunk = fileSize > CHUNK_THRESHOLD_BYTES || content.length() > EMBEDDING_MAX_LENGTH; if (needChunk) { log.info("å¼å§åç: fileSize={}, contentLength={}", fileSize, content.length()); chunks = splitText(content, vector); log.info("åç宿ï¼å ± {} 个å", chunks.size()); } else { log.info("æä»¶è¾å°ï¼ä¸è¿è¡åç"); Map<String, Object> metadata = buildMetadata(vector); chunks = List.of(TextSegment.from(content, new dev.langchain4j.data.document.Metadata(metadata))); } int chunkCount = 0; for (TextSegment chunk : chunks) { Embedding embedding = embeddingModel.embed(chunk).content(); embeddingStore.add(embedding, chunk); chunkCount++; } knowledgeBaseVectorService.updateVectorStatus(vectorId, KnowledgeBaseVector.STATUS_COMPLETED, chunkCount, null); log.info("åéåå¤ç宿: vectorId={}, chunkCount={}", vectorId, chunkCount); } catch (Exception e) { log.error("åéåå¤ç失败: vectorId={}", vectorId, e); knowledgeBaseVectorService.updateVectorStatus(vectorId, KnowledgeBaseVector.STATUS_FAILED, null, e.getMessage()); } } @Override public List<String> searchRelevantContent(String namespace, String query, int maxResults) { try { Embedding queryEmbedding = embeddingModel.embed(query).content(); EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(maxResults) .minScore(0.7) .build(); EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest); return searchResult.matches().stream() .map(match -> match.embedded().text()) .collect(Collectors.toList()); } catch (Exception e) { log.error("åéæ£ç´¢å¤±è´¥: namespace={}", namespace, e); return new ArrayList<>(); } } @Override public void deleteEmbeddings(String namespace, Long storageBlobId) { log.info("å é¤åéæ°æ®: namespace={}, storageBlobId={}", namespace, storageBlobId); try { Struct filter = Struct.newBuilder() .putFields("storageBlobId", Value.newBuilder() .setStructValue(Struct.newBuilder() .putFields("$eq", Value.newBuilder() .setNumberValue(storageBlobId.doubleValue()) .build())) .build()) .build(); List<String> emptyIds = new ArrayList<>(); pineconeIndex.delete(emptyIds, false, this.namespace, filter); log.info("åéå é¤å®æ: storageBlobId={}", storageBlobId); } catch (Exception e) { log.error("å é¤åéæ°æ®å¤±è´¥: namespace={}, storageBlobId={}", namespace, storageBlobId, e); } } private File getFile(StorageBlob blob) { String path = blob.getPath(); if (path != null && !path.isEmpty()) { return new File(new File(fileProperties.getPath(), path), blob.getUidFilename()); } return new File(fileProperties.getPath(), blob.getUidFilename()); } private String extractFileContent(File file, String fileName) throws Exception { String ext = getFileExtension(fileName); if (isPlainText(ext)) { return readFileWithEncoding(file); } if ("docx".equals(ext)) { return extractDocx(file); } if ("xlsx".equals(ext)) { return extractXlsx(file); } if ("xls".equals(ext)) { return extractXls(file); } return readFileWithEncoding(file); } private String readFileWithEncoding(File file) throws Exception { byte[] bytes = Files.readAllBytes(file.toPath()); String utf8Content = new String(bytes, StandardCharsets.UTF_8); if (isValidUtf8(utf8Content)) { log.debug("æä»¶ç¼ç : UTF-8"); return utf8Content; } try { Charset gbk = Charset.forName("GBK"); String gbkContent = new String(bytes, gbk); log.debug("æä»¶ç¼ç : GBK"); return gbkContent; } catch (Exception e) { log.warn("ç¼ç æ£æµå¤±è´¥ï¼ä½¿ç¨ UTF-8"); return utf8Content; } } private boolean isValidUtf8(String decoded) { // æ£æ¥æ¿æ¢å符 U+FFFD (UTF-8 è§£ç 失败æ¶åºç°) if (decoded.contains("�")) { return false; } int invalidCount = 0; int checkLen = Math.min(decoded.length(), 1000); for (int i = 0; i < checkLen; i++) { char c = decoded.charAt(i); // æ£æ¥ç§æä½¿ç¨åºå (U+E000-U+F8FF) æå¼å¸¸æ§å¶å符 if ((c >= 'î' && c <= '') || (c < ' ' && c != '\n' && c != '\r' && c != '\t')) { invalidCount++; } } return invalidCount < checkLen * 0.05; } private String getFileExtension(String fileName) { if (fileName == null || !fileName.contains(".")) { return ""; } return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); } private boolean isPlainText(String ext) { return "txt".equals(ext) || "md".equals(ext) || "json".equals(ext) || "csv".equals(ext) || "xml".equals(ext) || "yaml".equals(ext) || "yml".equals(ext); } private String extractDocx(File file) throws Exception { try (var doc = new org.apache.poi.xwpf.usermodel.XWPFDocument(new java.io.FileInputStream(file)); var extractor = new org.apache.poi.xwpf.extractor.XWPFWordExtractor(doc)) { return extractor.getText(); } } private String extractXlsx(File file) throws Exception { try (var workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook(file)) { return extractWorkbook(workbook); } } private String extractXls(File file) throws Exception { try (var workbook = new org.apache.poi.hssf.usermodel.HSSFWorkbook(new java.io.FileInputStream(file))) { return extractWorkbook(workbook); } } private String extractWorkbook(org.apache.poi.ss.usermodel.Workbook workbook) { StringBuilder text = new StringBuilder(); var formatter = new org.apache.poi.ss.usermodel.DataFormatter(); for (int i = 0; i < workbook.getNumberOfSheets(); i++) { var sheet = workbook.getSheetAt(i); text.append("Sheet: ").append(sheet.getSheetName()).append("\n"); for (var row : sheet) { for (var cell : row) { text.append(formatter.formatCellValue(cell)).append("\t"); } text.append("\n"); } } return text.toString(); } private List<TextSegment> splitText(String content, KnowledgeBaseVector vector) { List<TextSegment> chunks = new ArrayList<>(); if (content.length() <= CHUNK_SIZE) { Map<String, Object> metadata = buildMetadata(vector); chunks.add(TextSegment.from(content, new dev.langchain4j.data.document.Metadata(metadata))); return chunks; } int start = 0; int chunkIndex = 0; while (start < content.length()) { int end = Math.min(start + CHUNK_SIZE, content.length()); if (end < content.length()) { int lastPeriod = content.lastIndexOf('ã', end); int lastNewline = content.lastIndexOf('\n', end); int boundary = Math.max(lastPeriod, lastNewline); if (boundary > start + CHUNK_SIZE / 2) { end = boundary + 1; } } String chunkText = content.substring(start, end).trim(); if (!chunkText.isEmpty()) { Map<String, Object> metadata = buildMetadata(vector); metadata.put("chunkIndex", chunkIndex); chunks.add(TextSegment.from(chunkText, new dev.langchain4j.data.document.Metadata(metadata))); chunkIndex++; } start = end - CHUNK_OVERLAP; if (start < 0) start = 0; if (start >= content.length() - CHUNK_OVERLAP) break; } return chunks; } private Map<String, Object> buildMetadata(KnowledgeBaseVector vector) { Map<String, Object> metadata = new HashMap<>(); metadata.put("knowledgeBaseId", vector.getKnowledgeBaseId()); metadata.put("storageBlobId", vector.getStorageBlobId()); metadata.put("fileName", vector.getFileName()); metadata.put("namespace", vector.getNamespace()); return metadata; } } src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java
@@ -2,16 +2,25 @@ import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.approve.dto.KnowledgeBaseVectorVO; import com.ruoyi.approve.pojo.KnowledgeBase; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import com.ruoyi.approve.service.KnowledgeBaseService; import com.ruoyi.approve.service.KnowledgeBaseVectorService; import com.ruoyi.basic.dto.StorageAttachmentDTO; import com.ruoyi.basic.dto.StorageBlobDTO; import com.ruoyi.basic.pojo.StorageBlob; import com.ruoyi.basic.service.StorageAttachmentService; import com.ruoyi.basic.service.StorageBlobService; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.framework.web.domain.AjaxResult; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; @RestController @@ -20,10 +29,12 @@ @Tag(name = "ç¥è¯åºç®¡ç") public class KnowledgeBaseController { private KnowledgeBaseService knowledgeBaseService; private KnowledgeBaseVectorService knowledgeBaseVectorService; private StorageAttachmentService storageAttachmentService; private StorageBlobService storageBlobService; /**ã /** * è·åå表 * @return */ @GetMapping("/getList") public AjaxResult getList(@RequestParam(defaultValue = "1") long current, @@ -31,25 +42,25 @@ Page page = new Page(current, size); return AjaxResult.success(knowledgeBaseService.listpage(page,knowledgeBase)); } /**ã * 墿·» * @return /** * æ°å¢ç¥è¯åº */ @PostMapping("/add") public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase){ return AjaxResult.success(knowledgeBaseService.save(knowledgeBase)); } /** * æ´æ° * @return * æ´æ°ç¥è¯åº */ @PostMapping("/update") public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase){ return AjaxResult.success(knowledgeBaseService.updateById(knowledgeBase)); } /** * å é¤ * @return * å é¤ç¥è¯åº */ @DeleteMapping("/delete") public AjaxResult delete(@RequestBody List<Long> ids){ @@ -65,4 +76,101 @@ util.exportExcel(response, accountExpenses, "ç¥è¯åºç®¡ç导åº"); } /** * æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æ */ @GetMapping("/vector/status/{knowledgeBaseId}") @Operation(summary = "æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æ") public AjaxResult getVectorStatus(@PathVariable Long knowledgeBaseId) { List<KnowledgeBaseVectorVO> list = knowledgeBaseVectorService.getVectorStatusByKnowledgeBaseId(knowledgeBaseId); return AjaxResult.success(list); } /** * éæ°åéåæä»¶ */ @PostMapping("/vector/reprocess/{vectorId}") @Operation(summary = "éæ°åéåæä»¶") public AjaxResult reprocessVector(@PathVariable Long vectorId) { knowledgeBaseVectorService.reprocessVector(vectorId); return AjaxResult.success("已鿰æäº¤åéåä»»å¡"); } /** * ä¿åç¥è¯åºæä»¶å ³èï¼æä»¶ä¸ä¼ åè°ç¨ï¼ * ä¸ä¼ æµç¨ï¼ * 1. å è°ç¨ /common/upload ä¸ä¼ æä»¶ï¼è·å storageBlobDTOs * 2. åè°ç¨æ¤æ¥å£å ³èæä»¶å°ç¥è¯åºå¹¶è§¦ååéå */ @PostMapping("/file/save") @Operation(summary = "ä¿åç¥è¯åºæä»¶å ³è") public AjaxResult saveKnowledgeBaseFiles(@RequestBody KnowledgeBaseFileDTO dto) { if (dto.getKnowledgeBaseId() == null) { return AjaxResult.error("ç¥è¯åºIDä¸è½ä¸ºç©º"); } if (CollectionUtils.isEmpty(dto.getStorageBlobIds())) { return AjaxResult.error("æä»¶IDä¸è½ä¸ºç©º"); } // ä¿åéä»¶å ³è StorageAttachmentDTO attachmentDTO = new StorageAttachmentDTO(); attachmentDTO.setRecordType("knowledge_base"); attachmentDTO.setRecordId(dto.getKnowledgeBaseId()); attachmentDTO.setApplication("rag_file"); List<StorageBlobDTO> blobDTOs = new ArrayList<>(); for (Long blobId : dto.getStorageBlobIds()) { StorageBlobDTO blobDTO = new StorageBlobDTO(); blobDTO.setId(blobId); blobDTOs.add(blobDTO); } attachmentDTO.setStorageBlobDTOs(blobDTOs); storageAttachmentService.saveStorageAttachment(attachmentDTO); // å建åéè®°å½å¹¶è§¦ååéå for (Long blobId : dto.getStorageBlobIds()) { StorageBlob blob = storageBlobService.getById(blobId); if (blob != null) { String fileName = blob.getOriginalFilename(); String fileType = getFileExtension(fileName); knowledgeBaseVectorService.createVectorRecord( dto.getKnowledgeBaseId(), blobId, fileName, fileType ); } } return AjaxResult.success(); } private String getFileExtension(String fileName) { if (fileName == null || !fileName.contains(".")) { return "unknown"; } return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); } /** * å é¤ç¥è¯åºæä»¶ */ @DeleteMapping("/file/delete") @Operation(summary = "å é¤ç¥è¯åºæä»¶") public AjaxResult deleteKnowledgeBaseFiles(@RequestBody List<Long> vectorIds) { if (CollectionUtils.isEmpty(vectorIds)) { return AjaxResult.error("è¯·éæ©è¦å é¤çæä»¶"); } knowledgeBaseVectorService.deleteVectors(vectorIds); return AjaxResult.success(); } /** * ç¥è¯åºæä»¶DTO */ @lombok.Data public static class KnowledgeBaseFileDTO { private Long knowledgeBaseId; private List<Long> storageBlobIds; } } src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,21 @@ package com.ruoyi.approve.dto; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; /** * ç¥è¯åºæä»¶åéç¶æVO */ @Data @EqualsAndHashCode(callSuper = true) @Schema(description = "ç¥è¯åºæä»¶åéç¶æVO") public class KnowledgeBaseVectorVO extends KnowledgeBaseVector { @Schema(description = "æä»¶é¢è§URL") private String previewUrl; @Schema(description = "æä»¶ä¸è½½URL") private String downloadUrl; } src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,39 @@ package com.ruoyi.approve.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.approve.dto.KnowledgeBaseVectorVO; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; /** * ç¥è¯åºæä»¶åéè®°å½ Mapper */ @Mapper public interface KnowledgeBaseVectorMapper extends BaseMapper<KnowledgeBaseVector> { /** * æ¥è¯¢ç¥è¯åºçæä»¶åéç¶æå表 */ @Select("SELECT v.*, b.path as previewUrl " + "FROM knowledge_base_vector v " + "LEFT JOIN storage_blob b ON v.storage_blob_id = b.id " + "WHERE v.knowledge_base_id = #{knowledgeBaseId} " + "ORDER BY v.create_time DESC") List<KnowledgeBaseVectorVO> selectByKnowledgeBaseId(@Param("knowledgeBaseId") Long knowledgeBaseId); /** * ç»è®¡ç¥è¯åºçæä»¶æ°é */ @Select("SELECT COUNT(*) FROM knowledge_base_vector WHERE knowledge_base_id = #{knowledgeBaseId}") int countByKnowledgeBaseId(@Param("knowledgeBaseId") Long knowledgeBaseId); /** * ç»è®¡ç¥è¯åºçæ»åçæ°é */ @Select("SELECT COALESCE(SUM(chunk_count), 0) FROM knowledge_base_vector WHERE knowledge_base_id = #{knowledgeBaseId} AND vector_status = 2") int sumChunkCountByKnowledgeBaseId(@Param("knowledgeBaseId") Long knowledgeBaseId); } src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java
@@ -91,4 +91,19 @@ @TableField(fill = FieldFill.INSERT) private Long deptId; /** * æä»¶æ°é */ private Integer fileCount; /** * æ»åçæ°é */ private Integer totalChunkCount; /** * ç¥è¯åºæè¿° */ private String description; } src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,80 @@ package com.ruoyi.approve.pojo; import com.baomidou.mybatisplus.annotation.*; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; /** * ç¥è¯åºæä»¶åéè®°å½è¡¨ * knowledge_base_vector */ @Data @TableName("knowledge_base_vector") @Schema(description = "ç¥è¯åºæä»¶åéè®°å½") public class KnowledgeBaseVector implements Serializable { private static final long serialVersionUID = 1L; @TableId(type = IdType.AUTO) @Schema(description = "主é®ID") private Long id; @Schema(description = "å ³èç¥è¯åºID") private Long knowledgeBaseId; @Schema(description = "å ³èæä»¶blob ID") private Long storageBlobId; @Schema(description = "æä»¶åç§°") private String fileName; @Schema(description = "æä»¶ç±»å(docx/pdf/xlsx/txtç)") private String fileType; @Schema(description = "åéåç¶æ: 0-å¾ å¤ç, 1-å¤çä¸, 2-已宿, 3-失败") private Integer vectorStatus; @Schema(description = "åéå失败åå ") private String vectorError; @Schema(description = "åçæ°é") private Integer chunkCount; @Schema(description = "åéå½å空é´") private String namespace; @TableField(fill = FieldFill.INSERT) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Schema(description = "å建æ¶é´") private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT) @Schema(description = "å建人") private Integer createUser; @TableField(fill = FieldFill.INSERT_UPDATE) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Schema(description = "æ´æ°æ¶é´") private LocalDateTime updateTime; @TableField(fill = FieldFill.INSERT_UPDATE) @Schema(description = "æ´æ°äºº") private Integer updateUser; @TableField(fill = FieldFill.INSERT) @Schema(description = "ç§æ·ID") private Long tenantId; @TableField(fill = FieldFill.INSERT) @Schema(description = "é¨é¨ID") private Long deptId; // åéåç¶æå¸¸é public static final int STATUS_PENDING = 0; public static final int STATUS_PROCESSING = 1; public static final int STATUS_COMPLETED = 2; public static final int STATUS_FAILED = 3; } src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,43 @@ package com.ruoyi.approve.service; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.approve.dto.KnowledgeBaseVectorVO; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import java.util.List; /** * ç¥è¯åºæä»¶åéè®°å½ Service */ public interface KnowledgeBaseVectorService extends IService<KnowledgeBaseVector> { /** * æ¥è¯¢ç¥è¯åºçæä»¶åéç¶æå表 */ List<KnowledgeBaseVectorVO> getVectorStatusByKnowledgeBaseId(Long knowledgeBaseId); /** * å建åéè®°å½å¹¶è§¦å弿¥åéå */ KnowledgeBaseVector createVectorRecord(Long knowledgeBaseId, Long storageBlobId, String fileName, String fileType); /** * æ´æ°åéç¶æ */ void updateVectorStatus(Long id, Integer status, Integer chunkCount, String error); /** * éæ°å¤çåéå */ void reprocessVector(Long id); /** * å é¤åéè®°å½åç¸å ³åéæ°æ® */ void deleteVector(Long id); /** * æ¹éå é¤åéè®°å½ */ void deleteVectors(List<Long> ids); } src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,144 @@ package com.ruoyi.approve.service.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.approve.dto.KnowledgeBaseVectorVO; import com.ruoyi.approve.mapper.KnowledgeBaseVectorMapper; import com.ruoyi.approve.pojo.KnowledgeBase; import com.ruoyi.approve.pojo.KnowledgeBaseVector; import com.ruoyi.approve.service.KnowledgeBaseService; import com.ruoyi.approve.service.KnowledgeBaseVectorService; import com.ruoyi.ai.service.KnowledgeRagService; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * ç¥è¯åºæä»¶åéè®°å½ Serviceå®ç° */ @Slf4j @Service public class KnowledgeBaseVectorServiceImpl extends ServiceImpl<KnowledgeBaseVectorMapper, KnowledgeBaseVector> implements KnowledgeBaseVectorService { private final KnowledgeBaseService knowledgeBaseService; private final KnowledgeRagService knowledgeRagService; public KnowledgeBaseVectorServiceImpl( KnowledgeBaseService knowledgeBaseService, @Lazy KnowledgeRagService knowledgeRagService) { this.knowledgeBaseService = knowledgeBaseService; this.knowledgeRagService = knowledgeRagService; } @Override public List<KnowledgeBaseVectorVO> getVectorStatusByKnowledgeBaseId(Long knowledgeBaseId) { return baseMapper.selectByKnowledgeBaseId(knowledgeBaseId); } @Override public KnowledgeBaseVector createVectorRecord(Long knowledgeBaseId, Long storageBlobId, String fileName, String fileType) { KnowledgeBase knowledgeBase = knowledgeBaseService.getById(knowledgeBaseId); if (knowledgeBase == null) { throw new RuntimeException("ç¥è¯åºä¸åå¨: " + knowledgeBaseId); } KnowledgeBaseVector vector = new KnowledgeBaseVector(); vector.setKnowledgeBaseId(knowledgeBaseId); vector.setStorageBlobId(storageBlobId); vector.setFileName(fileName); vector.setFileType(fileType); vector.setVectorStatus(KnowledgeBaseVector.STATUS_PENDING); vector.setNamespace("kb-" + knowledgeBaseId); vector.setChunkCount(0); save(vector); // 弿¥è§¦ååéåå¤ç knowledgeRagService.processVectorAsync(vector.getId()); return vector; } @Override public void updateVectorStatus(Long id, Integer status, Integer chunkCount, String error) { KnowledgeBaseVector vector = getById(id); if (vector == null) { return; } vector.setVectorStatus(status); if (chunkCount != null) { vector.setChunkCount(chunkCount); } if (error != null) { vector.setVectorError(error); } updateById(vector); // å¦æå®æï¼æ´æ°ç¥è¯åºç»è®¡ if (status == KnowledgeBaseVector.STATUS_COMPLETED) { updateKnowledgeBaseStats(vector.getKnowledgeBaseId()); } } @Override @Transactional(rollbackFor = Exception.class) public void reprocessVector(Long id) { KnowledgeBaseVector vector = getById(id); if (vector == null) { throw new RuntimeException("åéè®°å½ä¸åå¨: " + id); } vector.setVectorStatus(KnowledgeBaseVector.STATUS_PENDING); vector.setVectorError(null); vector.setChunkCount(0); updateById(vector); // 弿¥éæ°å¤ç knowledgeRagService.processVectorAsync(id); } @Override @Transactional(rollbackFor = Exception.class) public void deleteVector(Long id) { KnowledgeBaseVector vector = getById(id); if (vector == null) { return; } // å é¤åéåºä¸çæ°æ® try { knowledgeRagService.deleteEmbeddings(vector.getNamespace(), vector.getStorageBlobId()); } catch (Exception e) { log.error("å é¤åéåºæ°æ®å¤±è´¥", e); } // å é¤è®°å½ removeById(id); // æ´æ°ç¥è¯åºç»è®¡ updateKnowledgeBaseStats(vector.getKnowledgeBaseId()); } @Override @Transactional(rollbackFor = Exception.class) public void deleteVectors(List<Long> ids) { for (Long id : ids) { deleteVector(id); } } private void updateKnowledgeBaseStats(Long knowledgeBaseId) { KnowledgeBase knowledgeBase = knowledgeBaseService.getById(knowledgeBaseId); if (knowledgeBase == null) { return; } int fileCount = baseMapper.countByKnowledgeBaseId(knowledgeBaseId); int totalChunkCount = baseMapper.sumChunkCountByKnowledgeBaseId(knowledgeBaseId); knowledgeBase.setFileCount(fileCount); knowledgeBase.setTotalChunkCount(totalChunkCount); knowledgeBaseService.updateById(knowledgeBase); } } src/main/resources/application.yml
@@ -7,6 +7,13 @@ allow-circular-references: true profiles: active: dev # Pinecone åéæ°æ®åºé ç½® pinecone: api-key: pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9 index: xiaozhi-index namespace: knowledge-base langchain4j: mcp: # MCP æå¡ç«¯å°åï¼æ ¹æ®å®é é¨ç½²ç MCP æå¡è°æ´ï¼