强调一下 :本文把 5 维框架 当作工具箱之一 ------用来教「怎么想表」、团队对齐名词、验收有没有漏维;不是 推导表结构的唯一权威方法。做过知识库系统的熟手往往可以直接用例/API 或参考成熟项目出表;本文重在讲过程化思维 和 MVP → 升级的取舍。
第一部分:搞懂5维框架
先简单解释一下啥是5维框架,5维框架其实是把一个系统拆成5个维度:资产 、流程 、产物 、交互 、运营。
甭管系统多复杂,拆到底层都是这 5 类东西在转:资产 是长期存的(文档、用户、配置),流程 是会跑一段过程的(任务、状态机),产物 是跑完留下的(索引、答案),交互 是用户怎么用系统的(会话、消息、反馈),运营是摊开账本看跑得顺不顺(指标、花费、告警)。五类缺一类,系统就不闭环。
这样的话,当我们在拿到需求可以别急着建表,先套五个问句:存什么?什么会变?变完留下什么?用户怎么用?跑得顺不顺?------然后表结构就这么一步一步推导出来。
第二部分:用知识库问答系统练手
2.1 这个系统长什么样?(用户视角)
先不要想数据库,先想象你作为用户,怎么用这个系统:
- 你创建一个知识库,比如叫"公司规章制度"
- 你上传几份文档(PDF、Word......)
- 系统后台自动处理这些文档
- 你问一个问题:"请假流程是什么?"
- 系统给你一个答案,还告诉你"引用自员工手册第3页"
就这么简单。
2.2 系统里都有什么?(先把名词列出来)
用户的操作,对应系统里哪些"东西"?
先看用户能直接感知的:
| 用户操作 | 系统里的"东西" |
|---|---|
| 创建知识库 | 知识库 |
| 上传文档 | 文档 |
| 提问对话 | 会话 + 消息 |
| 收到答案 | 答案 |
| 点赞/点踩 | 反馈 |
| 登录使用 | 用户 |
再看用户间接感知的(看到状态,但不知道实现细节):
| 用户看到的 | 背后的系统概念 |
|---|---|
| "处理中"状态 | 任务(后台在跑) |
最后是用户完全感知不到的(纯实现细节)。
| 为什么需要? | 技术上怎么做 | 系统概念 |
|---|---|---|
| 文档存在哪? | 文件存对象存储,用 hash 标识同一内容 | 文件对象 |
| 长文档怎么搜? | 解析后切成小段(chunk) | 切片 |
| 小段怎么快搜? | 每段转向量,写入向量库 | 索引 |
| 提问怎么找段落? | 问题转向量,与索引做 Top-K 相似度检索 | 检索 |
这样我们就得到了 MVP 阶段约 12 个核心概念(不必一次列全,10~15 个都正常;企业级还会拆出上传记录、引用明细等,后面章节会展开):
- 知识库
- 文档
- 文件对象
- 切片
- 索引
- 检索
- 答案
- 用户
- 会话
- 消息
- 反馈
- 任务
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_member (kb_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_version 与 document.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
上传流程:
- 计算文件 sha256,若
file_object里已有则复用,否则新建一条。 - 在
kb_file里新建一条:kb_id + file_object_id + original_filename + uploaded_by。 - 在
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_file比file_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 理解"流程"是什么
流程 = 有状态变化的东西
它有三个特点:
- 有状态(排队中→处理中→成功/失败)
- 会失败,会重试(处理失败要重来)
- 需要可观测(不然你不知道卡在哪)
举个例子:
- 文档上传后,要解析、切片、建索引
- 这个过程可能失败(文件损坏了?)
- 失败了要重试
- 你还要知道:现在处理到哪一步了?
上传、解析、切片、建索引任一步都可能失败并重试,所以流程是状态机 ,不是开关:流程 = 状态变化 + 失败重试 + 可观测。
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/ 助手回的assistant;system可选,用于注入提示词等系统消息,MVP 可暂不建) - 它有内容
最小字段:
id // uuid 唯一标识
conversation_id // 属于哪个会话
role // 角色(user/assistant;system 可选)
content // 消息内容
created_at // 什么时候发送的
注意 :本节只展开 role=user;assistant 及反馈在第六部分(问答链:产物 → 交互)一起讲。
第六部分:问答产物与反馈闭环
承接第五部分的 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_id 与 question_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。
重新生成答案时:
- 将旧版本的
is_current设为 false - 插入新记录,
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 与团队对齐;熟练的人往往可以直接用例/契约或参考开源项目先出表,再用五问查漏。