From ad346a7e1f1c35b09a5550c1b60cebe68f0619bf Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 09 六月 2026 15:26:33 +0800
Subject: [PATCH] feat(ai): 集成 Pinecone 向量数据库并实现知识库 RAG 功能
---
src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java | 21
src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java | 15
src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java | 43 +
src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java | 343 +++++++++
src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java | 140 +++
src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java | 39 +
src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java | 80 ++
src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java | 85 ++
doc/知识库RAG功能实现文档.md | 1034 ++++++++++++++++++++++++++++
src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java | 34
src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java | 144 ++++
src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java | 21
doc/sql/20260609_knowledge_base_vector.sql | 30
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java | 44
src/main/resources/application.yml | 7
src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java | 35
16 files changed, 2,085 insertions(+), 30 deletions(-)
diff --git a/doc/sql/20260609_knowledge_base_vector.sql b/doc/sql/20260609_knowledge_base_vector.sql
new file mode 100644
index 0000000..7b4323f
--- /dev/null
+++ b/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 '鍏宠仈鐭ヨ瘑搴揑D',
+ 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='鐭ヨ瘑搴撴枃浠跺悜閲忚褰曡〃';
\ No newline at end of file
diff --git "a/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md" "b/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..4a94cc5
--- /dev/null
+++ "b/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md"
@@ -0,0 +1,1034 @@
+# 鐭ヨ瘑搴揜AG鍚戦噺妫�绱㈠姛鑳藉疄鐜版枃妗�
+
+## 涓�銆佸姛鑳芥杩�
+
+鍩轰簬 RAG锛圧etrieval-Augmented Generation锛夋妧鏈疄鐜扮煡璇嗗簱闂瓟鍔熻兘锛屾敮鎸侊細
+- 鐭ヨ瘑搴撶鐞嗭紙CRUD锛�
+- 鏂囦欢涓婁紶涓庡悜閲忓寲澶勭悊
+- 鍩轰簬鍚戦噺妫�绱㈢殑鏅鸿兘闂瓟
+- 澶氱鏂囦欢鏍煎紡鏀寔锛坱xt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df锛�
+
+## 浜屻�佹妧鏈灦鏋�
+
+### 2.1 鎶�鏈爤
+| 缁勪欢 | 鎶�鏈� |
+|------|------|
+| 鍚戦噺鏁版嵁搴� | Pinecone |
+| Embedding妯″瀷 | 闃块噷浜� DashScope text-embedding-v3 |
+| LLM | 闃块噷浜戦�氫箟鍗冮棶 qwen-max |
+| 妗嗘灦 | langchain4j |
+| ORM | MyBatis-Plus |
+
+### 2.2 鏋舵瀯鍥�
+
+```
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鍓嶇搴旂敤 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� Controller Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� KnowledgeBaseCtrl 鈹� 鈹� KnowledgeChatController 鈹� 鈹�
+鈹� 鈹� (鐭ヨ瘑搴撶鐞�) 鈹� 鈹� (鐭ヨ瘑搴撻棶绛�) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� Service Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹侹nowledgeBaseService 鈹� 鈹� KnowledgeRagService 鈹� 鈹�
+鈹� 鈹� (鐭ヨ瘑搴揅RUD) 鈹� 鈹� (鍚戦噺鍖�/妫�绱�) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� AI Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� KnowledgeChatAgent 鈹� 鈹� EmbeddingStore (Pinecone) 鈹� 鈹�
+鈹� 鈹� (闂瓟Agent) 鈹� 鈹� (鍚戦噺瀛樺偍) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+---
+
+## 涓夈�佸悗绔疄鐜�
+
+### 3.1 鏁版嵁搴撹璁�
+
+#### 3.1.1 鐭ヨ瘑搴撹〃锛坘nowledge_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 '瑙e喅鏁堢巼',
+ problem TEXT COMMENT '闂鎻忚堪',
+ solution TEXT COMMENT '瑙e喅鏂规',
+ 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 鐭ヨ瘑搴撳悜閲忚褰曡〃锛坘nowledge_base_vector锛�
+
+```sql
+CREATE TABLE knowledge_base_vector (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '涓婚敭ID',
+ knowledge_base_id BIGINT NOT NULL COMMENT '鍏宠仈鐭ヨ瘑搴揑D',
+ 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 閰嶇疆鏂囦欢锛坅pplication.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 鏍稿績浠g爜瀹炵幇
+
+#### 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 鐭ヨ瘑搴撻棶绛擜gent
+
+**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());
+ }
+}
+```
+
+---
+
+## 鍥涖�丄PI鎺ュ彛鏂囨。
+
+### 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": "濡備綍澶勭悊搴撳瓨鐩樼偣宸紓锛�"
+}
+```
+
+**鍝嶅簲**锛圫SE娴佸紡锛�
+```
+鏍规嵁鐭ヨ瘑搴撳唴瀹癸紝搴撳瓨鐩樼偣宸紓鐨勫鐞嗘祦绋嬪涓嬶細
+
+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锛堟祦寮忔帴鍙o級
+3. 鍚庣澶勭悊锛�
+ 鈹溾攢鈹� 鏋勫缓鍛藉悕绌洪棿锛歬b-{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灏佽
+```
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java b/src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java
new file mode 100644
index 0000000..2d4072c
--- /dev/null
+++ b/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;
+
+/**
+ * 鐭ヨ瘑搴撻棶绛擜gent
+ * 鍩轰簬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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java b/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
index ca5156d..eaa487c 100644
--- a/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
+++ b/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
@@ -5,32 +5,48 @@
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();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java b/src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java
new file mode 100644
index 0000000..2738439
--- /dev/null
+++ b/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;
+
+/**
+ * 鐭ヨ瘑搴撻棶绛擟ontroller
+ */
+@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("鐭ヨ瘑搴揑D涓嶈兘涓虹┖");
+ }
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java b/src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java
new file mode 100644
index 0000000..134c2c8
--- /dev/null
+++ b/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 = "鐭ヨ瘑搴揑D", required = true)
+ private Long knowledgeBaseId;
+
+ @Schema(description = "浼氳瘽ID锛岀敤浜庝繚鎸佷笂涓嬫枃", required = true)
+ private String memoryId;
+
+ @Schema(description = "鐢ㄦ埛鎻愰棶鍐呭", required = true)
+ private String question;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java b/src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java
new file mode 100644
index 0000000..44d5f87
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java
@@ -0,0 +1,34 @@
+package com.ruoyi.ai.service;
+
+import java.util.List;
+
+/**
+ * 鐭ヨ瘑搴揜AG鏈嶅姟
+ * 璐熻矗鏂囦欢鍚戦噺鍖栧鐞嗗拰妫�绱�
+ */
+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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java b/src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java
new file mode 100644
index 0000000..44f9e45
--- /dev/null
+++ b/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;
+
+/**
+ * 鐭ヨ瘑搴揜AG鏈嶅姟瀹炵幇
+ */
+@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 瑙g爜澶辫触鏃跺嚭鐜�)
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java b/src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java
index 3ebb782..ba93fb4 100644
--- a/src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java
+++ b/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,40 +29,42 @@
@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,
@RequestParam(defaultValue = "10") long size, KnowledgeBase knowledgeBase) {
Page page = new Page(current, size);
- return AjaxResult.success(knowledgeBaseService.listpage(page,knowledgeBase));
+ return AjaxResult.success(knowledgeBaseService.listpage(page, knowledgeBase));
}
- /**銆�
- * 澧炴坊
- * @return
+
+ /**
+ * 鏂板鐭ヨ瘑搴�
*/
@PostMapping("/add")
- public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase){
+ public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase) {
return AjaxResult.success(knowledgeBaseService.save(knowledgeBase));
}
+
/**
- * 鏇存柊
- * @return
+ * 鏇存柊鐭ヨ瘑搴�
*/
@PostMapping("/update")
- public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase){
+ public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase) {
return AjaxResult.success(knowledgeBaseService.updateById(knowledgeBase));
}
+
/**
- * 鍒犻櫎
- * @return
+ * 鍒犻櫎鐭ヨ瘑搴�
*/
@DeleteMapping("/delete")
- public AjaxResult delete(@RequestBody List<Long> ids){
- if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("璇蜂紶鍏ヨ鍒犻櫎鐨処D");
+ public AjaxResult delete(@RequestBody List<Long> ids) {
+ if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("璇蜂紶鍏ヨ鍒犻櫎鐨処D");
return AjaxResult.success(knowledgeBaseService.removeByIds(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("鐭ヨ瘑搴揑D涓嶈兘涓虹┖");
+ }
+ 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();
+ }
+
+ /**
+ * 鐭ヨ瘑搴撴枃浠禗TO
+ */
+ @lombok.Data
+ public static class KnowledgeBaseFileDTO {
+ private Long knowledgeBaseId;
+ private List<Long> storageBlobIds;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java b/src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java
new file mode 100644
index 0000000..9cfb721
--- /dev/null
+++ b/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;
+
+/**
+ * 鐭ヨ瘑搴撴枃浠跺悜閲忕姸鎬乂O
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "鐭ヨ瘑搴撴枃浠跺悜閲忕姸鎬乂O")
+public class KnowledgeBaseVectorVO extends KnowledgeBaseVector {
+
+ @Schema(description = "鏂囦欢棰勮URL")
+ private String previewUrl;
+
+ @Schema(description = "鏂囦欢涓嬭浇URL")
+ private String downloadUrl;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java b/src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java
new file mode 100644
index 0000000..5fb19fe
--- /dev/null
+++ b/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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java b/src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java
index 06dec8e..b15da7a 100644
--- a/src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java
+++ b/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;
+
}
diff --git a/src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java b/src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java
new file mode 100644
index 0000000..1db31fc
--- /dev/null
+++ b/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 = "鍏宠仈鐭ヨ瘑搴揑D")
+ 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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java b/src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java
new file mode 100644
index 0000000..10f2ca9
--- /dev/null
+++ b/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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java b/src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java
new file mode 100644
index 0000000..52bc0b5
--- /dev/null
+++ b/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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 63fe8c2..ad3d4bc 100644
--- a/src/main/resources/application.yml
+++ b/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 鏈嶅姟璋冩暣锛�
--
Gitblit v1.9.3