从0到1:用5维框架推导出知识库问答系统的表设计

强调一下 :本文把 5 维框架 当作工具箱之一 ------用来教「怎么想表」、团队对齐名词、验收有没有漏维;不是 推导表结构的唯一权威方法。做过知识库系统的熟手往往可以直接用例/API 或参考成熟项目出表;本文重在讲过程化思维MVP → 升级的取舍。

第一部分:搞懂5维框架

先简单解释一下啥是5维框架,5维框架其实是把一个系统拆成5个维度:资产流程产物交互运营

甭管系统多复杂,拆到底层都是这 5 类东西在转:资产 是长期存的(文档、用户、配置),流程 是会跑一段过程的(任务、状态机),产物 是跑完留下的(索引、答案),交互 是用户怎么用系统的(会话、消息、反馈),运营是摊开账本看跑得顺不顺(指标、花费、告警)。五类缺一类,系统就不闭环。

这样的话,当我们在拿到需求可以别急着建表,先套五个问句:存什么?什么会变?变完留下什么?用户怎么用?跑得顺不顺?------然后表结构就这么一步一步推导出来。


第二部分:用知识库问答系统练手

2.1 这个系统长什么样?(用户视角)

先不要想数据库,先想象你作为用户,怎么用这个系统:

  1. 你创建一个知识库,比如叫"公司规章制度"
  2. 你上传几份文档(PDF、Word......)
  3. 系统后台自动处理这些文档
  4. 你问一个问题:"请假流程是什么?"
  5. 系统给你一个答案,还告诉你"引用自员工手册第3页"

就这么简单。


2.2 系统里都有什么?(先把名词列出来)

用户的操作,对应系统里哪些"东西"?

先看用户能直接感知的

用户操作 系统里的"东西"
创建知识库 知识库
上传文档 文档
提问对话 会话 + 消息
收到答案 答案
点赞/点踩 反馈
登录使用 用户

再看用户间接感知的(看到状态,但不知道实现细节):

用户看到的 背后的系统概念
"处理中"状态 任务(后台在跑)

最后是用户完全感知不到的(纯实现细节)。

为什么需要? 技术上怎么做 系统概念
文档存在哪? 文件存对象存储,用 hash 标识同一内容 文件对象
长文档怎么搜? 解析后切成小段(chunk) 切片
小段怎么快搜? 每段转向量,写入向量库 索引
提问怎么找段落? 问题转向量,与索引做 Top-K 相似度检索 检索

这样我们就得到了 MVP 阶段约 12 个核心概念(不必一次列全,10~15 个都正常;企业级还会拆出上传记录、引用明细等,后面章节会展开):

  1. 知识库
  2. 文档
  3. 文件对象
  4. 切片
  5. 索引
  6. 检索
  7. 答案
  8. 用户
  9. 会话
  10. 消息
  11. 反馈
  12. 任务

tips:不是所有概念用户都能感知。但设计表时,我们要把用户感知的和不感知的都想全


2.3 这些"东西"怎么归类?

现在把上面列出的概念,按 5 维归类:

5维 系统里的"东西" 为什么这么归类?
资产 知识库、文档、文件对象、切片、用户 这些东西是"存起来就行"的,长期保存
流程 任务(文档处理任务) 这个任务有"排队→处理中→完成/失败"的状态变化
产物 索引、检索、答案 建索引任务跑完产出索引;提问后检索命中段落,再生成答案
交互 会话、消息、反馈 用户怎么跟系统"交互"的
运营 (MVP 暂不展开) 用量、指标、审计等见第七部分

你看,不用想表结构,光用 5 维思考,主流程的概念就已经拆清楚了

2.4 两条业务链(后文按此展开)

系统其实有两条链 ,五维在链上的位置不同------别指望按「资产→流程→交互→产物」单线读完,按链读更清楚

链 A:上传建库(第三、四部分)

复制代码
资产(文档/切片)→ 流程(index_job)→ 产物(index_artifact)

链 B:提问问答(第五、六部分)

复制代码
交互(user message)→ 产物(retrieval_run / answer)→ 交互(assistant message / feedback)

第三部分:资产表设计

先给一个最简单的设计(MVP),再看它哪里不够用,最后升级到更稳妥的企业级设计。

本文 MVP 按单用户 / owner 推导(knowledge_base.owner_user_id);多人共享知识库时另扩 kb_memberkb_id + user_id + role),本篇不展开。

3.1 这个系统要长期存什么?

答案是:知识库、文档、文件、切片、用户。(先不区分"文件对象"和"知识库文件",后面会拆。)

3.2 理解"资产"是什么

资产 = 名词 = 长期保存的东西 。「上传文档」是功能,不需要 upload_document 表;document 才是资产------表设计里最稳定的部分。

3.3 资产表字段结构

你可以把资产表想成:"身份证 + 归属 + 时间 + 少量业务字段"。共用字段结构如下:

复制代码
id                  // 身份证:唯一标识
created_at          // 时间:什么时候创建的
updated_at          // 时间:什么时候改的(可选)
归属字段(按需)     // 比如 user_id、kb_id
业务字段            // 根据资产不同而不同

updated_at 什么时候不需要?

有些资产是"不可变"的------改了内容就是新记录,不是修改原记录。

  • 文件对象:改了内容 → 新的 sha256 → 新的 file_object
  • 切片:改了内容 → 新版本的切片

这些表只需要 created_at,不需要 updated_at。


3.4 MVP 设计

先不搞复杂。用最直觉的方式:文档直接带着"文件信息"和"归属"

用户(user)
复制代码
id                  // uuid 唯一标识
username            // 用户名
email               // 邮箱
password_hash       // 密码哈希值
created_at
updated_at
知识库(knowledge_base)
复制代码
id
name                // 知识库名字
owner_user_id       // 谁创建的(归属)
created_at
updated_at
文档(document)------MVP 版

一个文档是什么?属于某个知识库,有标题,有存好的文件路径,有状态和版本。

复制代码
id
kb_id               // 属于哪个知识库(归属)
title               // 文档标题
file_path           // 文件存在哪(存储路径)
file_size           // 文件大小(字节)
original_filename   // 上传时的文件名
uploaded_by         // 谁上传的(用户ID)
status              // 状态:draft=刚创建未处理,indexing=正在建索引,ready=处理完成可检索,failed=处理失败
version             // 版本号(1、2、3......)
created_at
updated_at
切片(chunk)
复制代码
id
document_id         // 属于哪个文档(归属)
document_version    // 属于哪个版本的文档
content             // 切片内容
page_no             // 第几页
start_offset        // 从第几个字开始
end_offset          // 到第几个字结束
created_at

chunk.document_versiondocument.version 对齐;文档升版后需清理旧版 chunk,见第四部分 index_job

就这 4 张核心表(user、knowledge_base、document、chunk),能跑起来。 上传 → 存路径 → 解析 → 切片 → 建索引,逻辑都说得通。


3.5 MVP 的局限性

等业务稍微复杂一点,你就会碰到这些问题。

场景 1:同一份文件被传了两次

知识库 A 上传了"员工手册.pdf",知识库 B 也上传了同一份。

在 MVP 里,两条 document 记录,两个 file_path磁盘上存了两份完全一样的文件------浪费存储,也不好维护。

场景 2:文件管理和文档管理混在一起

  • 想查"这个知识库下上传了哪些文件"(含已删除、重传)------只能从 document 里猜,文档和上传记录绑死在一起。
  • 删掉某次上传但保留"文档"概念(例如换一份文件重新解析)------也做不到,因为上传和文档是同一行。

所以:MVP 够用一阵子,但不够"稳"。 下面在不动业务语义的前提下,做两次升级。


3.6 升级一:拆出 file_object(文件去重)

思路 :把"物理文件"单独成表,用 sha256 做唯一标识,相同内容只存一份 ;文档通过 file_object_id 引用它。

文件对象(file_object)
复制代码
id                  // uuid 唯一标识
storage_path        // 文件存储路径
sha256              // 文件哈希(唯一约束!用于去重)
size_bytes          // 文件大小(字节)
created_at          // 什么时候上传的(无 updated_at,内容变了就是新记录)
文档(document)------升级一版

文档一定属于某个知识库kb_id 必填);来自上传时,用 file_object_id 指向物理文件。

复制代码
id
kb_id               // 必填:属于哪个知识库(直接归属)
file_object_id      // 来自哪个文件对象
title
content             // 解析后的正文
status
version
created_at
updated_at

效果

  • 知识库 A、B 都传同一份 PDF → 只建一条 file_object,两条 document 指向同一个 file_object_id磁盘只存一份

3.7 升级二:拆出 kb_file(上传记录独立)

思路 :再区分两件事------"谁在哪个知识库里上传了哪个文件" (上传记录)和**"这份内容作为可处理、可版本化的文档"**(文档)。

上传记录单独成表,方便做"文件列表、软删除、审计";文档只关心标题、状态、版本和内容来源。

知识库文件(kb_file)

记录一次上传:哪个知识库、用了哪个文件对象、原始文件名、谁传的。

复制代码
id
kb_id               // 属于哪个知识库(归属)
file_object_id      // 指向哪个文件对象(归属)
original_filename   // 上传时的文件名
uploaded_by         // 谁上传的(用户ID)
status              // 状态(active/deleted)
created_at
updated_at
deleted_at          // 软删除时间(可选)

kb_file 是资产还是交互?

读者可能会问:kb_file 记录的是"一次上传行为",不应该是"交互"吗?

答案是:kb_file 的主维度是资产,但它带有"交互痕迹"

判断标准是------会不会被后续引用?

  • kb_file 会被 document 引用(document.kb_file_id 指向它),是长期存在的"东西"
  • 纯粹的"交互"表(如点击日志、问答记录)一般不会被其他表引用,只是历史日志

类比:订单记录的是"一次购买行为",但订单是资产,因为它会被后续引用(发货、退款都关联订单号)。

会被引用 = 更偏向资产。kb_file 同理------它记录的是"上传成功"这个事实,长期保存,可被 document 引用。

document 同时存 kb_id 和可选的 kb_file_id:前者方便按知识库查文档列表;后者有值时表示文档来自某次上传(上传场景下与 kb_file.kb_id 重复,写入时保持一致即可)。

文档(document)------最终版(企业级)

文档直接归属知识库kb_id 必填);若来自某次上传,再关联 kb_file_id

复制代码
id
kb_id               // 必填:属于哪个知识库(直接归属)
kb_file_id          // 可选:来自哪次上传
title
content             // 解析后的正文
status
version
created_at
updated_at

上传流程

  1. 计算文件 sha256,若 file_object 里已有则复用,否则新建一条。
  2. kb_file 里新建一条:kb_id + file_object_id + original_filename + uploaded_by。
  3. document 里新建一条:kb_id + kb_file_id(指向刚建的 kb_file)+ title 等。

这样:

  • file_object:只管物理文件存储和 sha256 去重。
  • kb_file:只管"上传记录"(哪个 KB、哪个文件、谁传的、原始名)。
  • document :只管"文档"(标题、内容、状态、版本);有 kb_file_id 表示来自上传。

升级后的关系链:文档不再直接认识文件对象

引入 kb_file 后,三张表的关系变成了:

复制代码
document ──→ kb_file ──→ file_object
   │           │             │
 (kb_file_id)  (file_object_id)

升级一 时,文档直接存 file_object_id升级二 后,文档不再直接引用文件对象,而是通过 kb_file 这个"中间人"来关联。

这样做的好处:kb_filefile_object 多了"原始文件名"和"谁上传的"等信息。如果文档需要物理文件信息,JOIN 两次即可

换个角度说 :MVP 里「文档」这一张表,其实混在了一起的三件事------存到哪、谁传的、文档本身 。升级的过程,就是把「文档」这个对象拆成三个对象、三张表:文件对象 (file_object)、知识库文件 (kb_file)、文档(document)。表设计就是按对象来的------一个对象一类职责,一张表(或一族表);先想清楚系统里有哪些对象,再给每个对象设计表,结构就不会乱。


3.8 资产表小结

表名 用途 核心字段
user 用户 email, username
knowledge_base 知识库 name, owner_user_id
document 文档 kb_id(必填)、kb_file_id(可选)、title、content、status、version
file_object 文件对象(物理存储+去重) storage_path, sha256, size_bytes
kb_file 知识库文件(上传成功记录) kb_id, file_object_id, original_filename, uploaded_by
chunk 切片 document_id, content, page_no

设计要点

  • 资产 = 名词 = 长期保存的东西;共用字段:id + created_at + 归属 + 业务字段。
  • 先 MVP 再升级:先能跑(document 直接带 file_path),遇到同文件多份、上传与文档混在一起时,再拆出 file_object 和 kb_file。
  • 表设计是按对象来的:先想清楚系统里有哪些对象(名词),再给每个对象一张表、一类职责。MVP 里的「文档」被拆成三个对象------文件对象、知识库文件、文档------对应三张表,就是这个思路。
  • document 的归属kb_id 必填(按 KB 查列表);kb_file_id 可选(有值 = 来自某次上传)。
  • 文件去重用 file_object.sha256,上传记录用 kb_file。
  • 权限 :MVP 用 owner 即可;企业级多人协作加 kb_member

为什么要单独存切片?不是已经存向量数据库了吗?

向量数据库只负责检索,不擅长存元数据。你要知道"这个切片来自哪篇文档、第几页",就需要在关系型数据库里记录。

切片表的核心价值:让引用来源可追溯。用户看到答案后,能点开查看"这句话引用自哪篇文档、第几页"。


第四部分:流程表设计

4.1 哪些东西不是"存着就行",而是要"跑一段过程"?

答案是:文档处理流程(上传后解析、切片、向量化、建索引)。

4.2 理解"流程"是什么

流程 = 有状态变化的东西

它有三个特点:

  1. 有状态(排队中→处理中→成功/失败)
  2. 会失败,会重试(处理失败要重来)
  3. 需要可观测(不然你不知道卡在哪)

举个例子:

  • 文档上传后,要解析、切片、建索引
  • 这个过程可能失败(文件损坏了?)
  • 失败了要重试
  • 你还要知道:现在处理到哪一步了?

上传、解析、切片、建索引任一步都可能失败并重试,所以流程是状态机 ,不是开关:流程 = 状态变化 + 失败重试 + 可观测

4.3 流程表字段结构

共用字段:

复制代码
id                  // uuid 唯一标识
asset_id            // 处理哪个资产?(比如 document_id)
status              // 状态(queued/running/succeeded/failed)
progress            // 进度(可选,比如 0~100)
error_message       // 错误信息(如果失败了)
created_at          // 什么时候创建的
updated_at          // 什么时候改的
started_at          // 什么时候开始跑的
finished_at         // 什么时候结束的
idempotency_key     // 幂等键(关键!防重复触发)

流程表不是日志表。

流程表是状态机,记录任务当前状态并支持重试;

日志表记录历史事件,用于审计和追溯。

  • 流程表 = "任务现在怎么样了?"(可操作)
  • 日志表 = "过去发生了什么?"(只看)

4.4 具体到我们的系统

文档处理任务(index_job)

在 4.3 字段结构基础上,index_job 的业务含义是:属于某个知识库,处理某个文档的某个版本;idempotency_key 可取 document_id + document_version,防止同一版本重复建索引。文档内容变更时 document.version +1 并入队新任务;任务成功后清理该文档旧版 chunk,否则检索会命中过期切片。

复制代码
kb_id               // 属于哪个知识库
document_id         // 处理哪个文档
document_version    // 文档版本(1、2、3......)
// 其余字段(status、error_message、idempotency_key、时间戳等)同 4.3
进阶:如果你想更细(可选)

如果你想记录每个步骤的状态(解析→切片→向量化→建索引),可以再加一张表:

复制代码
job_step            // 任务步骤表
- id                // INTEGER
- index_job_id      // 属于哪个任务
- step_name         // 步骤名字(parse/chunk/embed/index)
- status            // 步骤状态(pending/running/succeeded/failed/skipped)
- duration_ms       // 耗时
- error_message     // 错误信息
- created_at, updated_at

或者记录每次重试

复制代码
job_run             // 任务执行表(记录每次重试)
- id                // INTEGER
- index_job_id      // 属于哪个任务
- attempt_number    // 第几次尝试
- status            // 结果(running/succeeded/failed)
- error_message     // 错误信息
- created_at

三张表是分层设计index_job 看任务整体,job_step 看卡在哪一步,job_run 看重试了几次------粒度不同,字段同名但含义不同。


4.5 流程跑完:索引产物(index_artifact)

index_job 成功后还会留下可复用、可回滚 的索引版本。上传链在此闭合:资产 → 流程 → 产物(问答链的产物见第六部分)。

产物 = 流程的产出结果 ,单独存的价值:可复用、可回滚、可追溯

索引产物(index_artifact)
复制代码
id                  // uuid 唯一标识
kb_id               // 属于哪个知识库
version             // 索引版本(1、2、3......)
index_path          // 索引存储位置
index_job_id        // 由哪个任务产生
created_at          // 什么时候创建的

可选:built_from_document_version --- 记录覆盖的文档版本范围,便于细粒度回滚。

当前生效的索引版本

有版本不等于能切换。必须有一个地方记录"当前系统检索到底用哪个版本"

做法一(推荐) :在 kb 表挂 current_index_artifact_id------查当前版本、原子切换都只改 kb 一行,历史 artifact 全保留。

做法二 :在 index_artifact 上加 is_active 标记------切换时要保证同一 kb 仅一条 active。


4.6 流程与索引产物小结

表名 维度 用途 核心字段
index_job 流程 文档处理任务 document_id, status, idempotency_key
job_step(可选) 流程 任务步骤 step_name, status, duration_ms
job_run(可选) 流程 任务重试记录 attempt_number, status
index_artifact 产物 索引版本 kb_id, version, index_path, index_job_id

设计要点

  • 流程 = 状态变化 + 失败重试 + 可观测
  • 流程表共用字段:id + asset_id + status + idempotency_key
  • 上传链:index_job 跑完 → 落 index_artifact,在 kb 上切换当前版本

第五部分:交互表设计------用户怎么"开口"

5.1 用户开口(会话与消息)

用户打开对话 → 会话(conversation) ;发一条问题 → 消息(message,role=user) 。开口表要留痕,才能支撑历史对话多轮上下文 ,出问题时也能还原用户当时问了什么。用户收到答案后的反馈放在第六部分,与系统回应一起讲。

5.2 开口表字段结构

共用字段:

复制代码
id                  // uuid 唯一标识
归属字段            // 比如 user_id、kb_id
业务字段            // 根据交互类型不同
created_at          // 什么时候创建的

5.3 具体到我们的系统

会话(conversation)

一个会话是什么?

  • 它属于某个知识库
  • 它属于某个用户
  • 它包含多条消息(问答对)

最小字段:

复制代码
id                  // uuid 唯一标识
kb_id               // 属于哪个知识库
user_id             // 哪个用户创建的
created_at          // 什么时候创建的
消息(message)

一条消息是什么?

  • 它属于某个会话
  • 它有角色(用户发的 user / 助手回的 assistantsystem 可选,用于注入提示词等系统消息,MVP 可暂不建)
  • 它有内容

最小字段:

复制代码
id                  // uuid 唯一标识
conversation_id     // 属于哪个会话
role                // 角色(user/assistant;system 可选)
content             // 消息内容
created_at          // 什么时候发送的

注意 :本节只展开 role=userassistant 及反馈在第六部分(问答链:产物 → 交互)一起讲。


第六部分:问答产物与反馈闭环

承接第五部分的 Conversation / Message(user),补全 链 B:交互 → 产物 → 交互

复制代码
用户提问 → Message(user,第五部分)
     ↓
检索 → retrieval_run(产物)
     ↓
生成 → answer(产物)
     ↓
展示 → Message(assistant)
     ↓
反馈 → feedback

索引产物(index_artifact)在上传链第四部分 4.5 已讲;本节只讲提问之后的产物与反馈。


6.1 问答产物(retrieval_run / answer)

用户提问后,系统产出检索记录答案 ------同属产物维,字段结构与 4.5 相同(id、归属、process_id 可选、created_at 等)。

切片是原材料 (资产),索引是查内容的工具 (产物,见 4.5),检索与答案是每次提问的产出(产物):

复制代码
用户提问 → 检索(retrieval_run)→ 生成答案(answer)
检索产物(retrieval_run)

一次检索是什么?

  • 它属于某个知识库
  • 它属于某次对话(或某个请求)
  • 它由哪条具体的消息触发(精确关联,避免多轮对话中混淆)
  • 它知道用户问了什么
  • 它知道取了多少个结果(top_k)
  • 它记录了命中了哪些切片

最小字段:

复制代码
id                  // uuid 唯一标识
kb_id               // 属于哪个知识库
conversation_id     // 属于哪次对话
question_message_id // 由哪条用户消息触发(精确关联到 Message 表)
query_text          // 记录本次检索实际使用的查询文本
top_k               // 取了多少个结果
hit_chunks          // 命中了哪些切片(JSON存:[{chunk_id, score}, ...])
created_at          // 什么时候创建的

conversation_idquestion_message_id 同时存在,属于查询便利的冗余------通过 message 也能 JOIN 得到会话;列表页按会话筛检索记录时,少一层 JOIN。

为什么需要 question_message_id

多轮对话中,一个会话可能有多次提问,甚至出现相同 query_text。只有关联到具体的消息 ID,才能稳定追溯"这条提问 -> 这次检索 -> 这个答案"的完整链路。

如果你想更细(可选),可以把 hit_chunks 拆成一张表:

复制代码
retrieval_hit       // 检索命中明细表
- id                // INTEGER
- retrieval_run_id  // 属于哪次检索
- chunk_id          // 命中了哪个切片
- score             // 相似度分数
- rank              // 排名(第几)
- created_at
答案产物(answer)

一个答案是什么?

  • 它属于某次对话
  • 它是针对哪条消息的回答
  • 它有答案内容
  • 它有引用(引用了哪些切片)
  • 它记录了用了哪个模型
  • 它支持多版本(用户对答案不满意可重新生成,并切换查看)

最小字段:

复制代码
id                  // uuid 唯一标识
conversation_id      // 属于哪次对话
question_message_id // 回答哪条消息
version             // 版本号(同一问题的第几个答案:1, 2, 3...)
is_current          // 是否当前展示(true/false)
answer_text         // 答案内容
citations           // 引用了哪些切片(JSON存:[chunk_id1, chunk_id2, ...])
model_name          // 用了哪个模型
created_at          // 什么时候创建的

answer 同样保留 conversation_id,理由与 retrieval_run 相同:按会话查答案列表时少 JOIN。

重新生成答案时

  1. 将旧版本的 is_current 设为 false
  2. 插入新记录,version + 1,is_current = true

查询时

question_message_id 查所有版本,根据 is_current 决定默认展示哪个。

如果你想更细(可选),可以把 citations 拆成一张表:

复制代码
answer_citation     // 答案引用明细表
- id                // INTEGER
- answer_id         // 属于哪个答案
- chunk_id          // 引用了哪个切片
- citation_text     // 引用的原文片段
- page_no           // 第几页
- created_at

6.2 产物写回交互层(assistant message)

答案如何让用户看见?

Answer 已是产物 ;用户要在对话里看到,还需写回 Message 表role=assistant)------不是又一条流程,是产物在交互层的展示副本

复制代码
用户提问 → Message 表(role=user)
     ↓
 系统处理 → Answer 表(生成答案)
     ↓
 返回答案 → Message 表(role=assistant, content=答案内容)

为啥要写两遍?

打个比方:Answer 是病历详案 (引用哪几段、用的啥模型、第几版答案),Message 是聊天记录里贴的那条气泡

  • 界面拉历史对话,只要按时间查 Message(你的问题 + 助手的回复排成一串)
  • 点「查看引用」、重新生成、用户点赞,得查 Answer------信息全在那
  • 所以答案内容会抄一份到 Message 里方便展示,再用 answer_id 挂回 Answer,免得两表对不上

Message 表的 assistant 角色:

复制代码
id                  // uuid 唯一标识
conversation_id     // 属于哪个会话
role                // assistant(助手回答)
content             // 答案内容(从 Answer 表复制)
answer_id           // 关联到 Answer 表(关键!建立显式关联)
created_at          // 什么时候发送的

answer_id 建立 Message 与 Answer 的显式关联;写入顺序:先 Answer,再 Message。


6.3 反馈闭环(交互)

用户如何"评价"?

用户收到答案后,可能觉得有用,也可能觉得没用。这种反馈是交互的终点,也是优化的起点。

一个反馈是什么?

  • 它通常是针对某条"助手消息"的
  • 它具体评价的是哪个答案版本(关键!因为答案可能有多版本)
  • 它有评分(有用/没用?)
  • 可能有评论(用户怎么说)

最小字段:

复制代码
id                  // uuid 唯一标识
message_id          // 针对哪条消息(assistant 角色的 message)
answer_id           // 针对哪个答案版本(关键!关联到 Answer 表)
user_id             // 哪个用户给的反馈
rating              // 评分(like/dislike 或 1~5)
comment             // 评论(可选)
created_at          // 什么时候给的

答案有多版本时,Feedback 也必须挂 answer_id,才能确定用户评价的是哪一版。


6.4 产物 vs 资产:5 维是启发式,不是硬分类

也许你会问:切片也是切分流程「生产」出来的,为什么归资产,索引却归产物?

5 维框架是推导用的启发式 ,边界会有交叉,不必套「被生产=产物」这种硬口诀。更实用的判据是主职责

对象 主职责 归类
文档、切片 长期被下游流程消费(切分、检索、引用溯源) 资产
索引、检索记录、答案 某次流程的可版本化输出,要回滚/复盘 产物

切片虽由流程生成,但入库后主要当作检索与引用的内容单元,和「某次建索引跑出来的 index_artifact」不同;后者带版本、可切换、可 diff。索引更特殊:既是产物,又承担检索资源------承认模糊性即可,不必强行自洽到一字不差。


表名 用途 核心字段
index_artifact 索引产物(见 4.5 kb_id, version, index_path
retrieval_run 检索产物 query_text, hit_chunks
answer 答案产物 answer_text, citations, version, is_current
message(assistant) 产物写回交互层 conversation_id, role, content, answer_id
feedback 用户反馈 message_id, answer_id, rating
retrieval_hit(可选) 检索命中明细 chunk_id, score, rank
answer_citation(可选) 答案引用明细 chunk_id, citation_text

设计要点

  • 链 A(上传):index_job → index_artifact(第四部分)
  • 链 B(问答):retrieval_run / answer → assistant message → feedback(本节)
  • 答案双写:Answer(完整信息)+ Message(对话展示)

第七部分:运营表设计

7.1 老板问你:系统用得怎么样?快不快?贵不贵?稳不稳?

如果回答不上来,说明运营表没设计好。

7.2 理解"运营"是什么

运营 = 知道系统"好不好用、贵不贵、快不快、稳不稳"------成本、性能、稳定性、审计合规,线上没运营表很难定位问题。

7.3 运营表字段结构

共用字段:

复制代码
id                  // uuid 唯一标识
request_id          // 每次请求的唯一标识(关键!用于追踪)
归属字段            // 比如 kb_id、user_id
action              // 做了什么动作
指标字段            // 耗时、成本、成功失败
created_at          // 什么时候发生的

7.4 具体到我们的系统

用量记录(usage_record)

一条用量记录是什么?

  • 它属于某次请求(用 request_id 关联)
  • 它属于某个知识库
  • 它属于某个用户
  • 它知道做了什么动作(聊天?建索引?)
  • 它记录了耗时、成本、是否成功

最小字段:

复制代码
id                  // uuid 唯一标识
request_id          // 请求唯一标识(关键!用于跨表追踪)
kb_id               // 属于哪个知识库
user_id             // 哪个用户的请求
model_name          // 使用的模型
action              // 动作(chat / build_index)
latency_ms          // 耗时(毫秒)
tokens_in           // 输入 token 数
tokens_out          // 输出 token 数
cost                // 花费(元或单位)
error_code          // 错误码(如果失败了)
created_at          // 什么时候发生的

7.5 进阶运营表(可选)

如果你想更"企业化",可以再加这些表:

审计日志(audit_log)

记录重要操作(删除文档、修改权限等):

复制代码
id                  // INTEGER
user_id             // 谁操作的
action              // 做了什么(delete_document / update_permission)
target_type         // 操作对象类型(document/knowledge_base)
target_id           // 操作对象 ID
details             // 详细信息(JSON)
created_at          // 什么时候操作的
事件日志(event_log)

埋点事件(用户点击了什么按钮,看了什么页面):

复制代码
id                  // INTEGER
user_id             // 哪个用户
event_type          // 事件类型(view_page / click_button / submit_question)
event_name          // 事件名字
details             // 详细信息(JSON)
created_at          // 什么时候发生的

第八部分:完整表族清单与常见坑

8.1 完整的表族清单

到这里,你已经自然推导出了一张完整的表结构:

表族 表名 用途
资产表族 knowledge_base 知识库
document 文档
file_object 文件对象(全局共享)
kb_file 知识库文件(归属关系层)
chunk 切片
user 用户
流程表族 index_job 文档处理任务
job_step(可选) 任务步骤
job_run(可选) 任务执行/重试
产物表族 index_artifact 索引产物
retrieval_run 检索产物
retrieval_hit(可选) 检索命中明细
answer 答案产物
answer_citation(可选) 答案引用明细
交互表族 conversation 会话
message 消息
feedback 反馈
运营表族 usage_record 用量记录
audit_log(可选) 审计日志
event_log(可选) 事件日志

如果你刚开始,不需要这么复杂。最小可用版本只有这些表:

复制代码
# 资产表族(6张)
knowledge_base
document
file_object
kb_file
chunk
user

# 流程表族(1张)
index_job

# 产物表族(3张)
index_artifact
retrieval_run
answer

# 交互表族(3张)
conversation
message
feedback

# 运营表族(1张)
usage_record

总共14张表,足够做一个知识库问答系统了。


8.2 初学者最常踩的4个坑

坑1:把流程塞进资产表

错误做法

复制代码
document 表里塞满字段:
- id
- title
- status                    // ← 流程字段
- progress                  // ← 流程字段
- error_message             // ← 流程字段
- job_started_at            // ← 流程字段
- job_finished_at           // ← 流程字段

结果Document 变成"宇宙表",后期你非常痛苦。

正确做法

复制代码
document 表只存文档本身:
- id
- title
- status(只存最终状态)
- version

index_job 表存流程:
- id
- document_id
- status
- progress
- error_message

坑2:把产物当资产存

错误做法

复制代码
把 index_artifact 的字段直接塞进 knowledge_base:
knowledge_base:
- id
- name
- index_version           // ← 产物字段
- index_path              // ← 产物字段
- index_job_id            // ← 产物字段

结果:索引/检索/答案混成一坨,回滚与追溯做不起来。

正确做法

复制代码
knowledge_base 表只存知识库本身:
- id
- name
- owner_user_id

index_artifact 表存产物:
- id
- kb_id
- index_version
- index_path

坑3:没有归属与权限字段

错误做法

复制代码
knowledge_base:
- id
- name
- created_at

document:
- id
- title
- created_at

结果:一加多用户/多租户就炸(越权风险巨大)。

正确做法

复制代码
knowledge_base:
- id
- name
- owner_user_id          // ← 归属字段

document:
- id
- kb_id                  // ← 归属字段
- title

坑4:没有 request_id 或幂等键

错误做法

复制代码
usage_record:
- id
- kb_id
- action
- latency_ms
- cost

结果

  • 线上问题无法追踪(这个错误是哪次请求产生的?)
  • 重复请求造成重复数据(用户点了两次按钮,记录了两条)

正确做法

复制代码
usage_record:
- id
- request_id            // ← 每次请求的唯一标识
- kb_id
- action
- latency_ms
- cost

index_job 同理加 idempotency_key(见第四部分流程篇)

结语

文章有点长,终于写完了。最后讲一下,以后拿到需求,你可以先问五个问题:存什么、什么会变、变完留下什么、用户怎么用、跑得顺不顺。本文推导的是 MVP(14 表、文档级 IndexJob);若落到实际业务开发时,可能需加 kb_member 权限表等演进。表结构可以先推导,然后再迭代。

当然,5 维不是唯一路径:更适合陌生 MVP 与团队对齐;熟练的人往往可以直接用例/契约或参考开源项目先出表,再用五问查漏。

相关推荐
Navicat中国1 个月前
Navicat Premium 17 在设计表结构时,未显示上移 / 下移功能按钮,什么原因?
postgresql·navicat·表设计
70asunflower2 个月前
GitHub 从 0 到 1 使用教程
github·从0到1
谁把我灯关了3 个月前
【Web安全】SSTI 从零到一:模板引擎原理深度拆解与服务端模板注入全流程解析
web安全·网络安全·ssti·从0到1·模板注入
胡图图不糊涂^_^3 个月前
MySQL学习笔记——数据库约束与数据库设计-表设计
数据库·笔记·学习·mysql·数据库约束·表设计
Agentcometoo4 个月前
智能体来了从 0 到 1:规则、流程与模型的工程化协作顺序
人工智能·从0到1·智能体来了·时代趋势
GuoDongOrange4 个月前
智能体来了从 0 到 1:工作流在智能体系统中的真实作用
ai·智能体·从0到1·智能体来了·智能体来了从0到1
GuoDongOrange4 个月前
重构认知——AI智能体来了从0到1的落地工程全指南
ai agent·智能体·从0到1·智能体来了·智能体来了从0到1
GuoDongOrange4 个月前
智能体来了从 0 到 1:数据、工具与规则的协同范式
ai agent·智能体·从0到1·智能体来了·智能体来了从0到1
Ytadpole8 个月前
MySQL 数据库优化设计:优化原理和数据库表设计技巧
数据库·mysql·优化·索引·查询·检索·表设计