From 26d6f9823b1b8a3f1592393d74306b29155629a1 Mon Sep 17 00:00:00 2001 From: wanghanlin <1533525126@qq.com> Date: Sat, 27 Jun 2026 10:31:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E5=88=9D=E5=A7=8B=E5=8C=96sql=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=89=8D=E7=AB=AF=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=20=E8=AE=BE=E7=BD=AE=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=90=91=E9=87=8F=E7=BB=B4=E5=BA=A6=E4=B8=BA1024=EF=BC=88?= =?UTF-8?q?=E8=B1=86=E5=8C=85=E6=A8=A1=E5=9E=8B=E4=BD=8E=E5=90=91=E9=87=8F?= =?UTF-8?q?=E7=BB=B4=E5=BA=A6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 4 +- DEPLOY.md | 10 +- README.md | 4 +- .../supportbot/config/ChatModelFactory.java | 18 + .../config/EmbeddingConfigFixer.java | 2 +- .../config/EmbeddingModelFactory.java | 4 +- .../rag/load/PgVectorStoreConfig.java | 4 +- .../service/AiModelConfigService.java | 32 ++ src/main/resources/add-comments.sql | 2 +- src/main/resources/application.yml | 4 +- src/main/resources/init-database.sql | 350 ++++++++++++++++++ .../static/components/ModelConfigManager.js | 10 +- src/main/resources/support-bot.sql | 4 +- 13 files changed, 424 insertions(+), 24 deletions(-) create mode 100644 src/main/resources/init-database.sql diff --git a/CLAUDE.md b/CLAUDE.md index 3a65cb8..e7b69d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 - MyBatis Plus 逻辑删除字段: `isDelete`,主键策略: `assign_id`(雪花算法) - **雪花 ID 精度问题**: `KnowledgeDocument.id`、`categoryId` 和 `KnowledgeCategory.id`、`parentId` 已添加 `@JsonSerialize(using = ToStringSerializer.class)`,序列化为字符串避免前端 JS 精度丢失。新增 Long ID 字段时务必加上此注解 - PostgreSQL JSONB 字段使用自定义 `PostgresJsonTypeHandler`(期望 JSON 对象 `'{}'`,非数组 `'[]'`) -- **向量维度**: 由 `knowledge.vector.dimension` 配置(默认 1536)。修改后需执行 `DROP TABLE IF EXISTS vector_store CASCADE` 重建向量表,并重新上传知识库文档。距离类型: COSINE_DISTANCE,索引: HNSW +- **向量维度**: 由 `knowledge.vector.dimension` 配置(默认 1024)。修改后需执行 `DROP TABLE IF EXISTS vector_store CASCADE` 重建向量表,并重新上传知识库文档。距离类型: COSINE_DISTANCE,索引: HNSW - **分块配置**: `knowledge.chunk.*` 配置项(`ChunkConfig`),默认 chunkSize=200, overlap=100, minChunkSizeChars=10, maxNumChunks=5000, keepSeparator=true - **上传校验**: `ALLOWED_EXTENSIONS` 白名单 + 50MB 大小限制(`spring.servlet.multipart` 配置),前后端双重校验 - **文档去重**: `KnowledgeDocument.contentHash` 字段(SHA-256),上传时自动计算并查重 @@ -132,4 +132,4 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 - `DocumentService.searchDocuments()`: Spring AI 1.0.1 的 filter 支持有限,分类过滤暂未实现 - `CompressionQueryRewriter`: 当前传入空历史列表 - MyBatis Plus 3.5.12 的 `mybatis-plus-spring-boot3-starter` 不含 `PaginationInnerInterceptor`,分页通过 SQL `LIMIT/OFFSET` 手动实现 -- `PgVectorStoreConfig.dimensions(1536)` 硬编码了向量维度,切换非 1536 维的 Embedding 模型时需修改并重建 vector_store 表 → **已修复:维度由 `knowledge.vector.dimension` 配置,启动时自动检测不匹配并告警** +- `PgVectorStoreConfig.dimensions(1024)` 硬编码了向量维度,切换非 1024 维的 Embedding 模型时需修改并重建 vector_store 表 → **已修复:维度由 `knowledge.vector.dimension` 配置,启动时自动检测不匹配并告警** diff --git a/DEPLOY.md b/DEPLOY.md index d602a95..2a2c60a 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -337,8 +337,8 @@ knowledge: keep-separator: true vector: # 向量维度,需与 Embedding 模型输出维度一致 - # 千问 text-embedding-v2: 1536 | 豆包 doubao-embedding-text-240515: 2048 - dimension: 1536 + # 千问 text-embedding-v2: 1024 | 豆包 doubao-embedding-text-240515: 2048 + dimension: 1024 role: strict-isolation: false @@ -468,9 +468,9 @@ tail -f /opt/support-bot/logs/startup.out - **提供商**:dashscope - **API Key**:在 [DashScope 控制台](https://dashscope.console.aliyun.com/) 获取,格式 `sk-xxxx` - **Base URL**:留空(DashScope 内置) -- **EMBEDDING 类型**需在「向量维度」填 `1536`(与 `knowledge.vector.dimension` 一致) +- **EMBEDDING 类型**需在「向量维度」填 `1024`(与 `knowledge.vector.dimension` 一致) -> 配置后**热切换生效,无需重启**。若切换非 1536 维的 Embedding 模型,需修改 `application.yml` 的 `knowledge.vector.dimension` 并重建 `vector_store` 表(见运维章节)。 +> 配置后**热切换生效,无需重启**。若切换非 1024 维的 Embedding 模型,需修改 `application.yml` 的 `knowledge.vector.dimension` 并重建 `vector_store` 表(见运维章节)。 ### 9.4 其他提供商(可选) @@ -607,7 +607,7 @@ echo "0 3 * * * docker exec support-bot-postgres pg_dump -U postgres support_bot ### 12.3 切换 Embedding 模型 / 重建向量表 -若更换非 1536 维的 Embedding 模型: +若更换非 1024 维的 Embedding 模型: ```bash # 1. 修改 application.yml 中 knowledge.vector.dimension diff --git a/README.md b/README.md index 4b26ab9..49b7a07 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ | **后端框架** | Spring Boot | 3.4.4 | 主框架,提供依赖注入和自动配置 | | **AI框架** | Spring AI Alibaba | 1.0.0-M6.1 | AI集成框架,简化大模型调用 | | **大语言模型** | 阿里云通义千问 | qwen-turbo | 对话生成和文本理解 | -| **Embedding模型** | 阿里云DashScope | text-embedding-v2 | 文本向量化(1536维) | +| **Embedding模型** | 阿里云DashScope | text-embedding-v2 | 文本向量化(1024维) | | **数据库** | PostgreSQL + PGVector | 12+ | 关系数据存储 + 向量存储 | | **ORM框架** | MyBatis Plus | 3.5.12 | 数据库操作和对象映射 | | **API文档** | Knife4j | 4.4.0 | Swagger UI增强版 | @@ -65,7 +65,7 @@ CREATE TABLE vector_store ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, -- 向量记录ID content TEXT NOT NULL, -- 原始文档内容 metadata JSONB DEFAULT '{}', -- 文档元数据 - embedding VECTOR(1536) NOT NULL, -- 1536维向量嵌入 + embedding VECTOR(1024) NOT NULL, -- 1024维向量嵌入 create_time TIMESTAMP DEFAULT NOW(), -- 创建时间 update_time TIMESTAMP DEFAULT NOW() -- 更新时间 ); diff --git a/src/main/java/com/wok/supportbot/config/ChatModelFactory.java b/src/main/java/com/wok/supportbot/config/ChatModelFactory.java index e1a8041..a5e6323 100644 --- a/src/main/java/com/wok/supportbot/config/ChatModelFactory.java +++ b/src/main/java/com/wok/supportbot/config/ChatModelFactory.java @@ -92,8 +92,12 @@ public class ChatModelFactory { * 创建 ChatModel 实例 * - dashscope:手动构造 DashScopeApi + DashScopeChatModel,按 DB 配置指定 model/temperature/maxTokens * - 其他提供商:通过 OpenAI 兼容 API 创建 + * + * 会校验模型名称,防止 Embedding 模型被误用为 Chat 模型(会导致 API 返回 404) */ private ChatModel createChatModel(AiModelConfig config) { + // 校验:Embedding 模型不应作为 Chat 模型使用 + validateNotEmbeddingModel(config); if ("dashscope".equalsIgnoreCase(config.getProvider())) { log.info("创建 DashScope ChatModel: model={}, temperature={}, maxTokens={}", config.getModelName(), config.getTemperature(), config.getMaxTokens()); @@ -179,4 +183,18 @@ public class ChatModelFactory { chatModelCache.clear(); log.info("ChatModel 缓存已清除"); } + + /** + * 校验模型名称,防止 Embedding 模型被误用为 Chat 模型 + * Embedding 模型不支持 /chat/completions 端点,调用会导致 API 返回 404 + */ + private void validateNotEmbeddingModel(AiModelConfig config) { + String modelName = config.getModelName(); + if (modelName != null && modelName.toLowerCase().contains("embedding")) { + throw new IllegalArgumentException( + "模型 [" + modelName + "] 是 Embedding 模型,不支持对话功能。" + + "请在「AI 大模型配置管理」页面将该模型的应用类型改为 EMBEDDING," + + "并为 CHAT 类型选择一个对话模型(如 doubao-pro、doubao-lite 等)"); + } + } } diff --git a/src/main/java/com/wok/supportbot/config/EmbeddingConfigFixer.java b/src/main/java/com/wok/supportbot/config/EmbeddingConfigFixer.java index c49a8c0..2809b07 100644 --- a/src/main/java/com/wok/supportbot/config/EmbeddingConfigFixer.java +++ b/src/main/java/com/wok/supportbot/config/EmbeddingConfigFixer.java @@ -31,7 +31,7 @@ public class EmbeddingConfigFixer implements ApplicationListener application.yml knowledge.vector.dimension(默认 1536)。 + * 向量维度优先级:DB ai_model_config.extra_config.dimensions(用户前端配置) > application.yml knowledge.vector.dimension(默认 1024)。 */ @Configuration public class PgVectorStoreConfig { @@ -29,7 +29,7 @@ public class PgVectorStoreConfig { @Autowired private AiModelConfigService configService; - @Value("${knowledge.vector.dimension:1536}") + @Value("${knowledge.vector.dimension:1024}") private int defaultVectorDimension; @Bean diff --git a/src/main/java/com/wok/supportbot/service/AiModelConfigService.java b/src/main/java/com/wok/supportbot/service/AiModelConfigService.java index 2a2e072..881a8ee 100644 --- a/src/main/java/com/wok/supportbot/service/AiModelConfigService.java +++ b/src/main/java/com/wok/supportbot/service/AiModelConfigService.java @@ -143,6 +143,8 @@ public class AiModelConfigService { */ @Transactional(rollbackFor = Exception.class) public AiModelConfig createConfig(AiModelConfig config) { + // 校验:Embedding 模型不能用于非 EMBEDDING 类型 + validateModelTypeMatch(config); // 如果新配置标记为活跃,先禁用同类型的其他配置 if (Boolean.TRUE.equals(config.getIsActive())) { deactivateByAppType(config.getAppType()); @@ -172,6 +174,15 @@ public class AiModelConfigService { throw new RuntimeException("配置不存在"); } + // 校验:Embedding 模型不能用于非 EMBEDDING 类型 + // 合并已有配置和更新内容进行校验 + AiModelConfig merged = AiModelConfig.builder() + .appType(config.getAppType() != null ? config.getAppType() : existing.getAppType()) + .modelName(config.getModelName() != null ? config.getModelName() : existing.getModelName()) + .provider(config.getProvider() != null ? config.getProvider() : existing.getProvider()) + .build(); + validateModelTypeMatch(merged); + // 如果要激活此配置,先禁用同类型的其他配置 if (Boolean.TRUE.equals(config.getIsActive())) { deactivateByAppType(existing.getAppType()); @@ -201,6 +212,7 @@ public class AiModelConfigService { /** * 激活指定配置(同 app_type 互斥) + * 校验:Embedding 模型不能作为 CHAT / PRODUCT_EXTRACT / RAG_REWRITE 类型激活 * * @param id 配置ID */ @@ -211,6 +223,9 @@ public class AiModelConfigService { throw new RuntimeException("配置不存在"); } + // 校验:Embedding 模型不能用于非 EMBEDDING 类型 + validateModelTypeMatch(config); + // 先禁用同类型的所有配置 deactivateByAppType(config.getAppType()); @@ -275,6 +290,23 @@ public class AiModelConfigService { return apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4); } + /** + * 校验模型名称与应用类型的匹配关系 + * Embedding 模型(模型名含 "embedding")不能用于 CHAT / PRODUCT_EXTRACT / RAG_REWRITE 类型, + * 否则调用 /chat/completions 端点会返回 404 + */ + private void validateModelTypeMatch(AiModelConfig config) { + String modelName = config.getModelName(); + String appType = config.getAppType(); + if (modelName != null && appType != null + && modelName.toLowerCase().contains("embedding") + && !"EMBEDDING".equals(appType)) { + throw new IllegalArgumentException( + "模型 [" + modelName + "] 是 Embedding 模型,不能用于 [" + appType + "] 类型。" + + "请选择一个对话模型(如 doubao-pro、doubao-lite、qwen-turbo 等)"); + } + } + /** * 显式持久化 extraConfig JSONB 字段 * MyBatis Plus 的 insert/updateById 对带 typeHandler 的 JSONB 字段可能不触发写入, diff --git a/src/main/resources/add-comments.sql b/src/main/resources/add-comments.sql index eb534ab..57a07b4 100644 --- a/src/main/resources/add-comments.sql +++ b/src/main/resources/add-comments.sql @@ -16,6 +16,6 @@ COMMENT ON TABLE vector_store IS '向量存储表(存储文档的向量表示 COMMENT ON COLUMN vector_store.id IS '主键ID(UUID格式的唯一标识符)'; COMMENT ON COLUMN vector_store.content IS '文档内容(原始的文本内容)'; COMMENT ON COLUMN vector_store.metadata IS '元数据(JSON格式,存储文档来源、标题、标签等附加信息)'; -COMMENT ON COLUMN vector_store.embedding IS '向量嵌入(1536维向量表示,适配OpenAI embedding模型)'; +COMMENT ON COLUMN vector_store.embedding IS '向量嵌入(1024维向量表示,适配Embedding模型)'; COMMENT ON COLUMN vector_store.create_time IS '创建时间'; COMMENT ON COLUMN vector_store.update_time IS '更新时间'; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c4f302e..8620fc1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -62,8 +62,8 @@ knowledge: keep-separator: true vector: # 向量维度,需与 Embedding 模型输出维度一致 - # 千问 text-embedding-v2: 1536 | 豆包 doubao-embedding-text-240515: 2048 | OpenAI text-embedding-3-small: 1536 - dimension: 1536 + # 千问 text-embedding-v2: 1024 | 豆包 doubao-embedding-text-240515: 2048 | OpenAI text-embedding-3-small: 1536 + dimension: 1024 role: # 严格隔离:true=角色未绑定知识库分类时禁止检索任何内容;false=可检索全部知识库 strict-isolation: false diff --git a/src/main/resources/init-database.sql b/src/main/resources/init-database.sql new file mode 100644 index 0000000..bcbb208 --- /dev/null +++ b/src/main/resources/init-database.sql @@ -0,0 +1,350 @@ +-- ============================================================ +-- AI 智能客服系统 - 数据库初始化脚本 +-- 适用环境: PostgreSQL 12+ 且已安装 pgvector 扩展 +-- 使用方法: psql -U postgres -f init-database.sql +-- ============================================================ + +-- 1. 创建数据库(需在 postgres 默认库下执行) +-- 如果已在目标库中,注释掉下面这行 +CREATE DATABASE support_bot ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0; + +-- 切换到 support_bot 数据库(psql 命令,非 SQL) +\c support_bot + +-- 2. 安装必要扩展 +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS vector; + +-- ============================================================ +-- 表 1: chat_message — 聊天消息表 +-- ============================================================ +CREATE TABLE IF NOT EXISTS chat_message ( + id BIGINT PRIMARY KEY, + conversation_id VARCHAR(64) NOT NULL, + message_type VARCHAR(20) NOT NULL, + content TEXT NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}', + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_delete BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT chk_message_type CHECK (message_type IN ('USER', 'ASSISTANT', 'SYSTEM')) +); + +CREATE INDEX IF NOT EXISTS idx_chat_message_conversation_id ON chat_message (conversation_id); +CREATE INDEX IF NOT EXISTS idx_chat_message_create_time ON chat_message (create_time DESC); +CREATE INDEX IF NOT EXISTS idx_chat_message_type ON chat_message (message_type); +CREATE INDEX IF NOT EXISTS idx_chat_message_not_deleted ON chat_message (conversation_id) WHERE is_delete = FALSE; + +COMMENT ON TABLE chat_message IS '聊天消息表'; +COMMENT ON COLUMN chat_message.id IS '主键(雪花算法生成)'; +COMMENT ON COLUMN chat_message.conversation_id IS '会话ID'; +COMMENT ON COLUMN chat_message.message_type IS '消息类型: USER / ASSISTANT / SYSTEM'; +COMMENT ON COLUMN chat_message.content IS '消息内容'; +COMMENT ON COLUMN chat_message.metadata IS '元数据(JSON)'; +COMMENT ON COLUMN chat_message.create_time IS '创建时间'; +COMMENT ON COLUMN chat_message.update_time IS '更新时间'; +COMMENT ON COLUMN chat_message.is_delete IS '逻辑删除: FALSE=正常 TRUE=已删除'; + +-- ============================================================ +-- 表 2: knowledge_category — 知识库分类表 +-- ============================================================ +CREATE TABLE IF NOT EXISTS knowledge_category ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + parent_id BIGINT NOT NULL DEFAULT 0, + sort_order INTEGER NOT NULL DEFAULT 0, + document_count INTEGER NOT NULL DEFAULT 0, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_delete BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX IF NOT EXISTS idx_knowledge_category_parent ON knowledge_category (parent_id); + +COMMENT ON TABLE knowledge_category IS '知识库分类表'; +COMMENT ON COLUMN knowledge_category.id IS '主键'; +COMMENT ON COLUMN knowledge_category.name IS '分类名称'; +COMMENT ON COLUMN knowledge_category.description IS '分类描述'; +COMMENT ON COLUMN knowledge_category.parent_id IS '父分类ID(0 表示顶级分类)'; +COMMENT ON COLUMN knowledge_category.sort_order IS '排序权重'; +COMMENT ON COLUMN knowledge_category.document_count IS '关联文档数(冗余字段)'; +COMMENT ON COLUMN knowledge_category.create_time IS '创建时间'; +COMMENT ON COLUMN knowledge_category.is_delete IS '逻辑删除'; + +-- ============================================================ +-- 表 3: knowledge_document — 知识文档表 +-- ============================================================ +CREATE TABLE IF NOT EXISTS knowledge_document ( + id BIGSERIAL PRIMARY KEY, + title VARCHAR(500) NOT NULL, + source_name VARCHAR(500), + file_type VARCHAR(20) NOT NULL, + file_size BIGINT NOT NULL DEFAULT 0, + content TEXT, + category_id BIGINT NOT NULL DEFAULT 0, + tags JSONB NOT NULL DEFAULT '{}', + chunk_count INTEGER NOT NULL DEFAULT 0, + status VARCHAR(20) NOT NULL DEFAULT 'PROCESSING', + error_message TEXT, + content_hash VARCHAR(64), + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_delete BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX IF NOT EXISTS idx_knowledge_document_category ON knowledge_document (category_id); +CREATE INDEX IF NOT EXISTS idx_knowledge_document_status ON knowledge_document (status); +CREATE INDEX IF NOT EXISTS idx_knowledge_document_create_time ON knowledge_document (create_time DESC); +CREATE INDEX IF NOT EXISTS idx_knowledge_document_content_hash ON knowledge_document (content_hash); + +COMMENT ON TABLE knowledge_document IS '知识文档表'; +COMMENT ON COLUMN knowledge_document.id IS '主键'; +COMMENT ON COLUMN knowledge_document.title IS '文档标题'; +COMMENT ON COLUMN knowledge_document.source_name IS '原始文件名'; +COMMENT ON COLUMN knowledge_document.file_type IS '文件类型'; +COMMENT ON COLUMN knowledge_document.file_size IS '文件大小(字节)'; +COMMENT ON COLUMN knowledge_document.content IS '原文内容(截断预览)'; +COMMENT ON COLUMN knowledge_document.category_id IS '所属分类ID'; +COMMENT ON COLUMN knowledge_document.tags IS '标签(JSON)'; +COMMENT ON COLUMN knowledge_document.chunk_count IS '分块数量'; +COMMENT ON COLUMN knowledge_document.status IS '状态: PROCESSING / READY / FAILED'; +COMMENT ON COLUMN knowledge_document.error_message IS '处理失败时的错误信息'; +COMMENT ON COLUMN knowledge_document.content_hash IS '内容SHA-256哈希(用于去重)'; +COMMENT ON COLUMN knowledge_document.create_time IS '创建时间'; +COMMENT ON COLUMN knowledge_document.update_time IS '更新时间'; +COMMENT ON COLUMN knowledge_document.is_delete IS '逻辑删除'; + +-- ============================================================ +-- 表 4: customer_service_role — 客服角色表 +-- ============================================================ +CREATE TABLE IF NOT EXISTS customer_service_role ( + id BIGSERIAL PRIMARY KEY, + role_key VARCHAR(64) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description TEXT, + prompt TEXT, + sort_order INTEGER NOT NULL DEFAULT 0, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_delete BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX IF NOT EXISTS idx_customer_service_role_enabled ON customer_service_role (enabled, sort_order); + +COMMENT ON TABLE customer_service_role IS '客服角色表'; +COMMENT ON COLUMN customer_service_role.id IS '主键'; +COMMENT ON COLUMN customer_service_role.role_key IS '角色标识符(唯一)'; +COMMENT ON COLUMN customer_service_role.name IS '角色名称'; +COMMENT ON COLUMN customer_service_role.description IS '角色描述'; +COMMENT ON COLUMN customer_service_role.prompt IS '系统提示词'; +COMMENT ON COLUMN customer_service_role.sort_order IS '排序权重'; +COMMENT ON COLUMN customer_service_role.enabled IS '是否启用'; +COMMENT ON COLUMN customer_service_role.create_time IS '创建时间'; +COMMENT ON COLUMN customer_service_role.update_time IS '更新时间'; +COMMENT ON COLUMN customer_service_role.is_delete IS '逻辑删除'; + +-- 默认角色种子数据 +INSERT INTO customer_service_role (role_key, name, description, sort_order, enabled) +VALUES ('general', '客服', '通用客服角色', 0, TRUE) +ON CONFLICT (role_key) DO NOTHING; + +INSERT INTO customer_service_role (role_key, name, description, sort_order, enabled) +VALUES ('finance', '财务', '财务相关客服角色', 1, TRUE) +ON CONFLICT (role_key) DO NOTHING; + +INSERT INTO customer_service_role (role_key, name, description, sort_order, enabled) +VALUES ('administration', '行政', '行政相关客服角色', 2, TRUE) +ON CONFLICT (role_key) DO NOTHING; + +-- ============================================================ +-- 表 5: customer_service_role_category — 角色知识库关联表 +-- ============================================================ +CREATE TABLE IF NOT EXISTS customer_service_role_category ( + id BIGSERIAL PRIMARY KEY, + role_id BIGINT NOT NULL, + category_id BIGINT NOT NULL, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_delete BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT uq_role_category UNIQUE (role_id, category_id) +); + +CREATE INDEX IF NOT EXISTS idx_customer_service_role_category_role ON customer_service_role_category (role_id); +CREATE INDEX IF NOT EXISTS idx_customer_service_role_category_category ON customer_service_role_category (category_id); + +COMMENT ON TABLE customer_service_role_category IS '客服角色知识库关联表'; +COMMENT ON COLUMN customer_service_role_category.id IS '主键'; +COMMENT ON COLUMN customer_service_role_category.role_id IS '角色ID'; +COMMENT ON COLUMN customer_service_role_category.category_id IS '分类ID'; +COMMENT ON COLUMN customer_service_role_category.create_time IS '创建时间'; +COMMENT ON COLUMN customer_service_role_category.is_delete IS '逻辑删除'; + +-- ============================================================ +-- 表 6: customer_account — 客服账号表 +-- ============================================================ +CREATE TABLE IF NOT EXISTS customer_account ( + id BIGSERIAL PRIMARY KEY, + account_key VARCHAR(64) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description TEXT, + role_id BIGINT, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_delete BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX IF NOT EXISTS idx_customer_account_role ON customer_account (role_id); +CREATE INDEX IF NOT EXISTS idx_customer_account_enabled ON customer_account (enabled, id); + +COMMENT ON TABLE customer_account IS '客服账号表'; +COMMENT ON COLUMN customer_account.id IS '主键'; +COMMENT ON COLUMN customer_account.account_key IS '账号标识符(唯一)'; +COMMENT ON COLUMN customer_account.name IS '账号名称'; +COMMENT ON COLUMN customer_account.description IS '账号描述'; +COMMENT ON COLUMN customer_account.role_id IS '关联角色ID'; +COMMENT ON COLUMN customer_account.enabled IS '是否启用'; +COMMENT ON COLUMN customer_account.create_time IS '创建时间'; +COMMENT ON COLUMN customer_account.update_time IS '更新时间'; +COMMENT ON COLUMN customer_account.is_delete IS '逻辑删除'; + +-- 默认账号种子数据(引用角色表的 ID) +INSERT INTO customer_account (account_key, name, description, role_id, enabled) +SELECT 'service', '客服账号', '通用客服账号', id, TRUE +FROM customer_service_role WHERE role_key = 'general' +ON CONFLICT (account_key) DO NOTHING; + +INSERT INTO customer_account (account_key, name, description, role_id, enabled) +SELECT 'finance', '财务账号', '财务客服账号', id, TRUE +FROM customer_service_role WHERE role_key = 'finance' +ON CONFLICT (account_key) DO NOTHING; + +INSERT INTO customer_account (account_key, name, description, role_id, enabled) +SELECT 'administration', '行政账号', '行政客服账号', id, TRUE +FROM customer_service_role WHERE role_key = 'administration' +ON CONFLICT (account_key) DO NOTHING; + +-- ============================================================ +-- 表 7: conversation_session — 会话归属表 +-- ============================================================ +CREATE TABLE IF NOT EXISTS conversation_session ( + conversation_id VARCHAR(64) PRIMARY KEY, + account_id BIGINT, + role_id BIGINT, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_conversation_session_account ON conversation_session (account_id); +CREATE INDEX IF NOT EXISTS idx_conversation_session_role ON conversation_session (role_id); + +COMMENT ON TABLE conversation_session IS '会话归属表'; +COMMENT ON COLUMN conversation_session.conversation_id IS '会话ID(主键)'; +COMMENT ON COLUMN conversation_session.account_id IS '归属账号ID'; +COMMENT ON COLUMN conversation_session.role_id IS '归属角色ID'; +COMMENT ON COLUMN conversation_session.create_time IS '创建时间'; +COMMENT ON COLUMN conversation_session.update_time IS '更新时间'; + +-- ============================================================ +-- 表 8: ai_model_config — AI 大模型配置表 +-- ============================================================ +CREATE TABLE IF NOT EXISTS ai_model_config ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + app_type VARCHAR(50) NOT NULL, + provider VARCHAR(50) NOT NULL DEFAULT 'dashscope', + api_key VARCHAR(512) NOT NULL, + model_name VARCHAR(100) NOT NULL, + temperature DOUBLE PRECISION DEFAULT 0.7, + max_tokens INTEGER DEFAULT 2000, + base_url VARCHAR(512), + extra_config JSONB NOT NULL DEFAULT '{}', + is_active BOOLEAN NOT NULL DEFAULT FALSE, + priority INTEGER NOT NULL DEFAULT 0, + description TEXT, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_delete BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX IF NOT EXISTS idx_ai_model_config_app_type ON ai_model_config (app_type); +CREATE INDEX IF NOT EXISTS idx_ai_model_config_is_active ON ai_model_config (is_active); +CREATE INDEX IF NOT EXISTS idx_ai_model_config_provider ON ai_model_config (provider); + +COMMENT ON TABLE ai_model_config IS 'AI 大模型配置表'; +COMMENT ON COLUMN ai_model_config.id IS '主键'; +COMMENT ON COLUMN ai_model_config.name IS '配置名称'; +COMMENT ON COLUMN ai_model_config.app_type IS '应用类型: CHAT / PRODUCT_EXTRACT / EMBEDDING / RAG_REWRITE'; +COMMENT ON COLUMN ai_model_config.provider IS '模型提供商'; +COMMENT ON COLUMN ai_model_config.api_key IS 'API Key'; +COMMENT ON COLUMN ai_model_config.model_name IS '模型名称'; +COMMENT ON COLUMN ai_model_config.temperature IS '温度参数'; +COMMENT ON COLUMN ai_model_config.max_tokens IS '最大Token数'; +COMMENT ON COLUMN ai_model_config.base_url IS 'API 基础地址'; +COMMENT ON COLUMN ai_model_config.extra_config IS '扩展配置(JSON,如向量维度等)'; +COMMENT ON COLUMN ai_model_config.is_active IS '是否活跃(同类型互斥)'; +COMMENT ON COLUMN ai_model_config.priority IS '优先级'; +COMMENT ON COLUMN ai_model_config.description IS '描述'; +COMMENT ON COLUMN ai_model_config.create_time IS '创建时间'; +COMMENT ON COLUMN ai_model_config.update_time IS '更新时间'; +COMMENT ON COLUMN ai_model_config.is_delete IS '逻辑删除'; + +-- 默认模型配置种子数据(api_key 占位,部署后在前端修改) +INSERT INTO ai_model_config (name, app_type, provider, api_key, model_name, temperature, max_tokens, is_active, description) +VALUES ('Chat Default', 'CHAT', 'dashscope', 'sk-placeholder', 'qwen-turbo', 0.7, 2000, TRUE, '默认对话模型配置') +ON CONFLICT DO NOTHING; + +INSERT INTO ai_model_config (name, app_type, provider, api_key, model_name, temperature, max_tokens, is_active, description) +VALUES ('Product Extract Default', 'PRODUCT_EXTRACT', 'dashscope', 'sk-placeholder', 'qwen-turbo', 0.1, 2000, TRUE, '默认产品信息提取模型配置') +ON CONFLICT DO NOTHING; + +INSERT INTO ai_model_config (name, app_type, provider, api_key, model_name, temperature, max_tokens, is_active, extra_config, description) +VALUES ('Embedding Default', 'EMBEDDING', 'dashscope', 'sk-placeholder', 'text-embedding-v2', 0.0, 2000, TRUE, '{"dimensions": 1024}', '默认向量化模型配置') +ON CONFLICT DO NOTHING; + +INSERT INTO ai_model_config (name, app_type, provider, api_key, model_name, temperature, max_tokens, is_active, description) +VALUES ('RAG Rewrite Default', 'RAG_REWRITE', 'dashscope', 'sk-placeholder', 'qwen-turbo', 0.3, 2000, TRUE, '默认 RAG 查询重写模型配置') +ON CONFLICT DO NOTHING; + +-- ============================================================ +-- 表 9: vector_store — 向量存储表 +-- 注意: Spring AI PgVectorStore 配置了 initializeSchema(true), +-- 应用启动时会自动建表。此处手动建表作为备份方案。 +-- 向量维度默认 1024,如使用不同维度的 Embedding 模型 +-- 需修改下面的 VECTOR(1024) 并重建。 +-- ============================================================ +CREATE TABLE IF NOT EXISTS vector_store ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + content TEXT NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}', + embedding VECTOR(1024) NOT NULL, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- HNSW 向量索引(余弦距离) +CREATE INDEX IF NOT EXISTS idx_vector_store_embedding ON vector_store + USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); + +CREATE INDEX IF NOT EXISTS idx_vector_store_create_time ON vector_store (create_time DESC); + +-- JSONB 元数据 GIN 索引 +CREATE INDEX IF NOT EXISTS idx_vector_store_metadata ON vector_store USING gin (metadata); + +COMMENT ON TABLE vector_store IS '向量存储表(RAG 知识库向量化数据)'; +COMMENT ON COLUMN vector_store.id IS '主键(UUID)'; +COMMENT ON COLUMN vector_store.content IS '文档内容'; +COMMENT ON COLUMN vector_store.metadata IS '元数据(JSON)'; +COMMENT ON COLUMN vector_store.embedding IS '向量嵌入(维度 1024)'; +COMMENT ON COLUMN vector_store.create_time IS '创建时间'; +COMMENT ON COLUMN vector_store.update_time IS '更新时间'; + +-- ============================================================ +-- 完成 +-- ============================================================ +\echo '==========================================' +\echo '数据库 support_bot 初始化完成!' +\echo '共创建 9 张表及对应索引。' +\echo '默认 api-key 为占位符,请在前端' +\echo '「AI 大模型配置管理」页面修改为真实值。' +\echo '==========================================' diff --git a/src/main/resources/static/components/ModelConfigManager.js b/src/main/resources/static/components/ModelConfigManager.js index 265b254..74fb32f 100644 --- a/src/main/resources/static/components/ModelConfigManager.js +++ b/src/main/resources/static/components/ModelConfigManager.js @@ -242,10 +242,10 @@ export default {
- +
⚠️ 修改维度后需重建向量表:DROP TABLE IF EXISTS vector_store CASCADE,然后重启服务并重新上传知识库文档。 - 常用维度:千问 text-embedding-v2=1536 | 豆包 doubao-embedding-text=2048 | OpenAI text-embedding-3-small=1536 + 常用维度:千问 text-embedding-v2=1024 | 豆包 doubao-embedding-text=2048 | OpenAI text-embedding-3-small=1536
@@ -309,7 +309,7 @@ export default { temperature: 0.7, max_tokens: 2000, base_url: '', - embeddingDimensions: 1536, + embeddingDimensions: 1024, priority: 0, is_active: false, description: '' @@ -407,7 +407,7 @@ export default { temperature: config.temperature, max_tokens: config.max_tokens, base_url: config.base_url || '', - embeddingDimensions: extraConfig.dimensions || 1536, + embeddingDimensions: extraConfig.dimensions || 1024, priority: config.priority || 0, is_active: config.is_active || false, description: config.description || '' @@ -444,7 +444,7 @@ export default { // EMBEDDING 类型:将向量维度写入 extraConfig if (form.app_type === 'EMBEDDING') { data.extraConfig = { - dimensions: form.embeddingDimensions || 1536 + dimensions: form.embeddingDimensions || 1024 } } return data diff --git a/src/main/resources/support-bot.sql b/src/main/resources/support-bot.sql index 3e6d07b..deeea1f 100644 --- a/src/main/resources/support-bot.sql +++ b/src/main/resources/support-bot.sql @@ -56,7 +56,7 @@ CREATE TABLE vector_store ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, content TEXT NOT NULL, metadata JSONB NOT NULL DEFAULT '{}', - embedding VECTOR(1536) NOT NULL, + embedding VECTOR(1024) NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL ); @@ -68,7 +68,7 @@ COMMENT ON TABLE vector_store IS '向量存储表 - 存储文档内容的向量 COMMENT ON COLUMN vector_store.id IS '主键ID - UUID格式的唯一标识符'; COMMENT ON COLUMN vector_store.content IS '文档内容 - 原始的文本内容'; COMMENT ON COLUMN vector_store.metadata IS '元数据 - 文档的附加信息(来源、标题、标签等)'; -COMMENT ON COLUMN vector_store.embedding IS '向量嵌入 - 1536维的向量表示(适配OpenAI embedding模型)'; +COMMENT ON COLUMN vector_store.embedding IS '向量嵌入 - 1024维的向量表示(适配Embedding模型)'; COMMENT ON COLUMN vector_store.create_time IS '创建时间'; COMMENT ON COLUMN vector_store.update_time IS '更新时间';