# 知识库模块设计与实施方案(修订版 2) ## 调研结论(方案依据) 项目已有可复用的完整基础设施,本方案**最大化复用现有模式**: - **隔离模式**:`agent` 表已有 `user_id + is_builtin` 口径(内置=全局可见只读、自建=仅创建者可见,后端过滤),`AgentExpertController` 的 CRUD 模式直接照搬。 - **RAG 组件**:`EmbeddingProvider`(本地 ONNX bge-m3 / 在线模型双模)、`AttachmentVectorMapper`(pgvector 动态表原生 SQL)、`AttachmentDocIndexer`(分块+并发闸门+预算超时)全部现成。 - **异步范式**:项目无 @Async/MQ,用 Spring 事件 + 后台线程(仿 `TemplateMetaChangedEvent` → `@EventListener` 异步索引)。 - **Agentic 工具范式**:`RagSchemaSearchTool`(@Tool 注解注册进 Toolkit,模型自主调用)+ `appendRagPrompt` 系统提示词注入。 - **文件下载范式**:后端 `LocalFileStorageUtil`(`/file-download/{path}`)+ 前端 `artifactApi.downloadArtifactFile`(手动带 token fetch blob → `downloadByData` 落盘)。 - **存储**:主库 PostgreSQL(pgvector 已装);文件落盘 `LocalFileStorageUtil`;SQL 脚本手工执行(无 Flyway)。 ## 已确认的决策与要求 1. Agent 绑定 **1 个**知识库(agent 表加 `knowledge_base_id`) 2. RAG 检索:**Agentic 工具式**;**工具默认注册到绑定了知识库的 agent 上**——作为基础设施工具(不进工具组白名单、不受 tools_allow_json 限制、无需手动勾选),绑定即自动生效 3. 前后端一起做;**前端样式布局参考专家页 `aiExpert/index.vue`** 卡片版式 4. 内置知识库由**管理员(userId=1)**维护 5. 已上传文档支持**下载**,**新建下载记录表**存下载行为 6. **文档列表按上传者隔离:用户只能看到/下载/管理自己上传的文档**(本次新增) ## 两层隔离模型(核心口径) | 层级 | 隔离规则 | |---|---| | 知识库本体 | 内置(is_builtin=1)全局可见可绑定;自建仅创建者可见可绑定 | | 库内文档 | **列表/详情/下载/删除/重试 只对上传者本人开放**(user_id 过滤)。内置库里管理员上传的文档清单对普通用户不可见 | | RAG 检索 | `search_knowledge` 检索**绑定知识库的全量文档**(与文档列表隔离解耦)——内置库对普通用户「知识可用、清单保密」,这正是共享库的价值 | | 绑定校验 | agent 绑定时校验知识库对当前用户可见(内置 or 自己的) | ## 需求补充(我补充的遗漏项) | 补充项 | 设计 | |---|---| | 文档解析落盘 | Tika 抽取全文存 sidecar .txt(重索引无需重新解析原件),落 `knowledge/{kbId}/doc/` | | 异步向量化 | 上传后事件驱动异步索引,状态机 PENDING→INDEXING→READY/FAILED + fail_reason | | 重新索引 | 手动 reindex 接口;`tableTag()` 按模型隔离向量表,换模型不污染旧向量 | | 删除级联 | 删知识库 → 删全库文档文件+sidecar+向量行+下载记录 + 解绑引用该 KB 的 agent;删文档 → 三清 + 下载记录 | | 内置库初始化 | SQL 插入内置知识库(固定 id=1,is_builtin=1,user_id=1) | | 限制配置 | `KnowledgeProperties`:max-docs-per-user-per-kb、max-file-size、chunk-size/overlap、index-concurrency | | 我的文档数 | KB 卡片展示「我的文档 N 篇」(按 kb_id+user_id group by 统计,不冗余全库计数) | ## 一、数据库(新建 `sql/knowledge_base.sql`,幂等) ```sql -- 知识库 create table if not exists knowledge_base ( id bigint primary key, -- 雪花ID;内置库固定 id=1 user_id bigint not null, -- 创建者(内置=1 管理员) is_builtin smallint not null default 0, -- 1=内置(全局可见只读) name varchar(100) not null, description varchar(500), create_time timestamp default current_timestamp, update_time timestamp ); create index if not exists idx_kb_user on knowledge_base(user_id); -- 知识库文档(元数据 + 解析/索引状态;user_id 即上传者,列表隔离依据) create table if not exists knowledge_doc ( id bigint primary key, kb_id bigint not null, user_id bigint not null, -- 上传者(列表/下载隔离依据) file_name varchar(500) not null, file_path varchar(1000) not null, -- 原件(下载用) sidecar_path varchar(1000), -- Tika 全文 .txt(向量化用) mime varchar(100), size_bytes bigint default 0, char_count integer default 0, chunk_count integer default 0, -- 已向量化块数 index_status varchar(20) default 'PENDING', -- PENDING/INDEXING/READY/FAILED fail_reason varchar(1000), download_count integer default 0, -- 冗余下载次数(我的文档列表展示) create_time timestamp default current_timestamp, update_time timestamp ); create index if not exists idx_kd_kb_user on knowledge_doc(kb_id, user_id); -- 下载记录表(谁在何时下载了哪个文档) create table if not exists knowledge_doc_download_log ( id bigint primary key, -- 雪花ID kb_id bigint not null, doc_id bigint not null, user_id bigint not null, -- 下载人 file_name varchar(500) not null, -- 下载时文件名快照 download_time timestamp default current_timestamp ); create index if not exists idx_kdl_doc on knowledge_doc_download_log(doc_id); create index if not exists idx_kdl_user on knowledge_doc_download_log(user_id); -- 内置知识库 insert into knowledge_base (id, user_id, is_builtin, name, description) values (1, 1, 1, '系统通用知识库', '系统内置,所有用户可见(管理员维护)') on conflict (id) do nothing; -- agent 绑定知识库 alter table agent add column if not exists knowledge_base_id bigint; ``` ## 二、后端(ai-server 新增 `module/knowledge` 包) ### 实体/Mapper - `KnowledgeBase`、`KnowledgeDoc`、`KnowledgeDocDownloadLog`(MyBatis-Plus,`IdType.ASSIGN_ID`,仿 `ChatAttachment`) - `KnowledgeVectorMapper`:pgvector 原生 SQL(动态表名建表/插入/cosine 检索/按 docId 删/按 kbId 删),仿 `AttachmentVectorMapper`;检索 WHERE `payload->>'kbId'` 双保险 ### Service - `KnowledgeBaseService`(隔离规则仿 `AgentService` 专家管理区,userId 显式传参便于单测): - `list(userId)`:内置 + 自己的,后端过滤,内置在前;VO 带「我的文档数」(group by 统计) - `create/update/delete`:内置拒绝编辑/删除;删除时级联(全库文档+sidecar+向量行+下载记录)+ `detachKnowledgeBase(kbId)` 解绑 agent 并 `evictAgentPool` - `requireVisible(userId, kbId)`:内置 or 自己的(绑定校验用) - `KnowledgeDocService`(**文档操作全部按上传者隔离**): - `listMine(userId, kbId)`:知识库可见性校验后,**只返回 user_id=当前用户 的文档** - `upload(userId, kbId, file)`:权限(自建库=库主人;内置库=仅管理员 userId=1 可传)→ 类型白名单 → 落盘 → Tika 抽全文存 sidecar → 登记 doc(PENDING)→ 发布 `KnowledgeDocIndexEvent` - `download(userId, docId)`:**requireOwnedDoc(只允许下载自己上传的)** → 写下载记录 + download_count+1 → 返回文件流 - `delete(userId, docId)`:requireOwnedDoc → DB 行 + 原件 + sidecar + 向量行三清 + 下载记录 - `reindex(userId, docId)`:requireOwnedDoc → 状态重置 PENDING 重发事件 - `KnowledgeDocIndexer`(`@EventListener` 异步,仿 `AttachmentDocIndexer`): - 读 sidecar → `chunk()` 分块(固定窗口+重叠+段落对齐)→ `EmbeddingProvider.embed` → 写 pgvector `kb_vec_{kbId}{tag}_d{dims}` → 更新 doc.chunk_count/READY;异常置 FAILED+fail_reason - 成员级公平 Semaphore 全局嵌入并发闸门 ### 工具与 Agent 接入 - `KnowledgeSearchTool`(仿 `RagSchemaSearchTool`):`@Tool(name="search_knowledge", readOnly=true)`,构造时绑定 kbId+kbName,参数 `query`/`top_k`,返回片段(content+来源文档名+相似度) - **默认注册语义**:agent 绑定了知识库 → `AgentService.buildAgent()` 自动把 `KnowledgeSearchTool` 注册为基础设施工具(与 `RagSchemaSearchTool` 同路径,不进任何工具组、不受 `tools_allow_json`/工具包白名单约束、`reset_equipped_tools` 不收起),并 `appendKbPrompt` 追加使用指引;未绑定知识库的 agent 不注册该工具(省 token)。EmbeddingProvider 不可用时跳过注册并 log.warn(fail-open) - `AgentEntity` 加 `knowledgeBaseId`;`SaveExpertDTO` 加可选 `knowledgeBaseId`;保存时校验存在性+可见性;`writeExpert` 显式 set 该列(null 照写,支持解绑) ### Controller(`KnowledgeBaseController`,`/knowledge-bases`,仿 `AgentExpertController`) - `GET /` 列表;`POST /` 创建;`PUT /{id}`(内置拒绝);`DELETE /{id}`(内置拒绝) - `GET /{id}/docs` **我的文档列表**;`POST /{id}/docs`(multipart 上传);`DELETE /{id}/docs/{docId}` - `GET /{id}/docs/{docId}/download` 下载(校验归属+记下载记录+文件流) ## 三、前端(样式布局对齐专家页 aiExpert/index.vue) 1. **api/knowledgeApi.ts** + `types.ts` 类型:KB CRUD、我的文档列表/上传/删除/下载(下载仿 `artifactApi.downloadArtifactFile`:带 token fetch blob → `downloadByData`) 2. **views/aiKnowledge/index.vue**:完全对齐专家页版式—— - 同款 panel 结构:卡头(标题「知识库」+ 摘要统计「内置 N · 我的 N」+ 名称搜索框 + 刷新/新建按钮) - 同款卡片网格:卡片头图标+名称+「内置/自定义」chip 标签、描述、chips(我的文档数/索引状态摘要)、卡底操作区(主操作「管理文档」+ 图标按钮:详情/编辑/删除,内置卡片不渲染编辑删除,后端另有双重校验) - 空态复用 `EmptyState`;详情弹框复用 `BasicModal` 3. **文档管理弹窗**(卡片「管理文档」打开,同款 Modal 版式):**标题标注「我的上传文档」**,表格列:文件名/大小/块数/索引状态 tag/失败原因/下载次数/操作[下载|重试|删除];上传按钮;文档列表接口天然只返回当前用户上传的数据,无需前端过滤 4. **aiWorkbench/index.vue**:`RAIL_ITEMS` 增加 `{ key: 'knowledge', icon: 'mdi:bookshelf', label: '知识库' }` 5. **AgentFormModal.vue**:加「绑定知识库」下拉(数据源=知识库列表,可清空=不绑定),随 `SaveExpertParams` 提交 ## 四、实施顺序 1. SQL 脚本 + 实体/Mapper(含下载记录表) 2. KnowledgeBaseService / KnowledgeDocService(隔离、级联、下载记录) 3. KnowledgeDocIndexer(异步索引)+ 事件 4. KnowledgeSearchTool + AgentService/AgentEntity/SaveExpertDTO 接入 5. KnowledgeBaseController(含下载接口) 6. 前端:API → 知识库页(对齐专家页版式)→ 文档管理弹窗 → 工作台入口 → AgentFormModal 绑定选择 7. 单测(隔离规则:内置只读/归属校验/文档按上传者隔离/下载权限/级联删除) ## 五、明确不做 - 不引入新向量库/MQ/@Async(与项目现状一致) - 不扩展权限系统(沿用 user_id + is_builtin + 管理员=1 口径) - 不做文档在线预览(仅列表管理 + 下载) - 对话链路(AgentChatServiceImpl)不改动——工具注册随 buildAgent 生效,池化驱逐已有机制