plan-session_f942efbb046ee26c1fb0a8640651590d.md 11 KB

知识库模块设计与实施方案(修订版 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,幂等)

-- 知识库
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 生效,池化驱逐已有机制