完整数据模型关系图:
User ──1:N──> KnowledgeBase ──1:N──> Source ──1:N──> Document ──1:N──> Chunk
│ │
├─1:N──> ChatSession ──1:N──> ChatMessage │
└─1:N──> StudioTask Document ──1:1─┴─> DocumentIndexStatus
9 张表(ChatSession 挂在 KnowledgeBase 下,非 Source)。身份认证 ≠ 权限鉴权——有 User,但没有 Team / Role / Permission / Audit。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID | PK |
| username | str(64) | 唯一 |
| password_hash | str(256) | bcrypt |
| created_at | datetime | |
| updated_at | datetime |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID | PK |
| user_id | UUID FK → user | 归属用户 |
| name | str(255) | 名称 |
| description | text? | 描述 |
| created_at | datetime | |
| updated_at | datetime |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID | PK |
| knowledge_base_id | UUID FK → knowledge_base | 归属 |
| type | enum(upload, url, github) |
数据源类型(v0.1.0 启用 upload/url,github 预留给 v0.2.0) |
| config | JSON | 类型相关配置 |
| source_hash | str(64)? | SHA-256 原始文件哈希(预留字段,v0.2.0 启用去重逻辑) |
| status | enum(pending, active, invalid) |
配置有效性(非索引进度) |
| created_at | datetime | |
| updated_at | datetime |
Source status 反映配置有效性(S3 对象还在吗?URL 可访问吗?),不反映文档是否已向量化。索引进度归 Document 和 DocumentIndexStatus 管。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID | PK |
| source_id | UUID FK → source (nullable, SET NULL on delete) | 来源数据源 |
| knowledge_base_id | UUID FK → knowledge_base | 所属知识库(source_id 可为 NULL,此字段是 document→KB 的唯一可靠路径) |
| title | str(255) | 文档标题 / 文件名 |
| path | text? | 原始路径或 URL |
| source_format | str(50) | 原始文件格式(pdf / markdown / text),解析后内容统一为纯文本 |
| full_text | text | 解析后的全文(清洗后) |
| text_hash | str(64) | SHA-256 解析文本哈希,决定是否重新 chunk + embed |
| status | enum(pending, processing, ready, failed) |
内容生命周期(不含 chunk/embed 状态) |
| doc_version | str(32) | 版本号,默认 "1" |
| created_at | datetime | |
| updated_at | datetime |
Document.status 只管内容解析(parse 是否成功),chunk 和 embed 的状态拆分到 DocumentIndexStatus 1:1 扩展表。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID | PK |
| document_id | UUID FK → document (unique, CASCADE) | 1:1 关联 |
| chunk_status | enum(pending, running, done, failed) |
分块阶段状态 |
| chunk_count | int? | 分块数量 |
| chunked_at | datetime? | 分块完成时间 |
| embed_status | enum(pending, running, done, failed) |
向量化阶段状态 |
| embed_count | int? | 向量化数量 |
| embedded_at | datetime? | 向量化完成时间 |
| error_stage | str(20)? | 失败阶段(chunk / embed) |
| error_message | text? | 错误详情 |
| embedding_model | str(100)? | 使用的 embedding 模型名 |
| index_version | str(32)? | 索引版本号 |
| created_at | datetime | |
| updated_at | datetime |
为什么拆成 1:1 扩展表:Document 管内容生命周期,DocumentIndexStatus 管派生数据(chunk + embed)的管线执行状态。分开避免把业务状态和管线执行状态混在一张表里。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID | PK |
| doc_id | UUID FK → document | 归属文档 |
| chunk_index | int | 切片序号 |
| content | text | 文本内容 |
| token_count | int | token 数 |
| embedding | vector(N) | pgvector 向量(nullable,embed 阶段写入;维度由 KS_EMBEDDING__DIMENSION 配置,默认 1024) |
| index_version | str(32) | 索引版本,默认 "1" |
| created_at | datetime |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID | PK |
| knowledge_base_id | UUID FK → knowledge_base | 所属知识库 |
| user_id | UUID FK → user | 所属用户 |
| title | str(255) | 会话标题(首条消息自动生成) |
| reference_document_ids | JSON | 限定检索的文档 ID 列表 |
| last_message_at | datetime? | 最后一条消息时间 |
| created_at | datetime | |
| updated_at | datetime |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID | PK |
| session_id | UUID FK → chat_session (CASCADE) | 所属会话 |
| role | str(20) | user / assistant |
| content | text | 消息内容 |
| citations | JSON | 引用来源列表 |
| created_at | datetime |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | UUID | PK |
| knowledge_base_id | UUID FK → knowledge_base (CASCADE, indexed) | 所属知识库 |
| user_id | UUID FK → user | 所属用户 |
| task_type | str(20) | report(v0.1.0 仅报告;ppt/analysis 预留) |
| title | str(255) | 产出物标题 |
| config | JSON | 任务配置(instruction / document_ids / style / length 等) |
| status | str(20) | pending → running → completed / failed |
| progress | float | 0.0 ~ 1.0 |
| status_message | str(500)? | 当前阶段描述,如"正在收集第 2/3 章的素材..." |
| output_format | str(20) | markdown(默认;v0.1.0 只产出 markdown) |
| output_s3_key | str(500)? | MinIO object key(studio/{kb}/{task}/report.md) |
| output_metadata | JSON? | { file_size, char_count, chapter_count } |
| error_message | str(2000)? | 失败详情 |
| started_at | datetime? | 开始执行时间 |
| completed_at | datetime? | 完成时间 |
| created_at | datetime | |
| updated_at | datetime |
Studio 是数据消费层:输入知识库文档,输出文件。StudioTaskRunner 以 FastAPI BackgroundTasks 异步执行 ReportWorkflow(plan → gather → generate → assemble → store),前端轮询 progress。v0.1.0 只实现 report,PPT 未做。
处理管线(函数) 数据模型(表)
───────────── ────────────
Source 配置 → source 表
↓
拉取原始文件 → 对象存储/MinIO(不建表)
↓
解析 + 清洗 → 纯文本 → document.full_text + text_hash
↓
分块 → chunk 表(content + chunk_index)
↓ document_index_status.chunk_status
向量化 → chunk.embedding
document_index_status.embed_status
中间步骤是管道里的临时变量,只需在 document、document_index_status 和 chunk 三层持久化。不要为每个管线阶段建表。
两层哈希,职责不同:
source_hash → "原始文件变没变?" → 决定是否重新解析
text_hash → "解析结果变没变?" → 决定是否重新 chunk + embedding
| Hash | 位置 | 算法 | v0.1.0 状态 |
|---|---|---|---|
source.source_hash |
Source 表 | SHA-256 原始文件 | 预留字段,暂不启用逻辑 |
document.text_hash |
Document 表 | SHA-256 解析后的纯文本 | 已启用 |
v0.1.0 只启用 text_hash,因为它直接决定要不要重新 chunk 和 embedding——这是 RAG 管道里最贵的步骤。
示例:
第一次上传 A.pdf → 解析出 "Hello World" → text_hash = "abc123" → 索引
第二次上传同一个 A.pdf → 解析出 "Hello World" → text_hash = "abc123" → 跳过
升级解析器后 A.pdf → 解析出 "Hello World !" → text_hash = "def456" ≠ "abc123" → 重新索引
source_hash 的逻辑(原始文件去重)留到 v0.2.0 启用,那时候数据源类型多了(GitHub/URL 定时同步),文件级去重才有实际价值。
- 身份认证 vs 权限鉴权分离——有 User,无 Role/Team/Permission
knowledge_base.user_id标注归属,多用户隔离靠查询过滤document.text_hash去重(启用),source.source_hash预留(v0.2.0)- pgvector
vector(N),维度可配置(KS_EMBEDDING__DIMENSION),<=>余弦距离 DocumentIndexStatus1:1 解耦内容生命周期与管线执行状态ChatSession+ChatMessage管理多轮对话历史与引用
架构决策(检索策略、agent 运行时、SSE 流式)见 @docs/architecture.md。