如何设计 AI Chat 的数据库模型

很多人做 AI Chat 的第一张表,会从 messages 开始。

字段也很直觉:idconversation_idrolecontentcreated_at。用户发一条消息,插入一行;模型回一条消息,再插入一行。页面读取消息列表,渲染出来。

这个模型能支撑 Demo,但支撑不了真实 AI Chat。

真实 AI Chat 里,一条助手消息背后可能包含一次 run、多次 streaming delta、一次或多次工具调用、检索结果、token 用量、失败重试、用户中断、模型 fallback 和最终写入的消息内容。

所以 AI Chat 的数据库模型不能只围绕“聊天记录”设计,还要围绕“生成过程”设计。

AI Chat 和普通 IM 的区别

普通 IM 的消息通常是确定的。

用户输入内容,点击发送,服务端保存消息,其他客户端同步消息。消息一旦发送成功,主要状态就是已发送、已读、撤回、删除。

AI Chat 不一样。助手消息不是用户输入的现成内容,而是一次生成任务的结果。

这带来几个差异:

  • 助手消息会经历生成中、部分完成、完成、失败、取消等状态。
  • 一次回复可能边生成边展示,最终内容逐步累积。
  • 生成过程中可能调用工具或检索文档。
  • 同一条用户消息可能触发多次重试,产生多个候选助手回复。
  • 用户可能编辑历史消息,从某个节点分叉出新对话。
  • 数据库要同时服务页面展示、上下文组装、审计排查和成本统计。

如果只用一张 messages 表,很快会把所有状态都塞进 metadata,最后变成难以查询和维护的半结构化泥团。

核心实体怎么拆

一个可维护的 AI Chat 模型,至少可以拆成五类实体:

  • chat_threads:一段对话或会话。
  • chat_messages:用户可见的消息。
  • ai_runs:一次 AI 生成任务。
  • ai_run_steps:生成过程中的步骤,如模型调用、检索、工具调用。
  • ai_usage:token、延迟、成本等用量数据。

如果支持工具调用,还需要:

  • tool_calls:模型请求执行的工具。
  • tool_results:工具执行结果。

如果支持文件和引用,还需要:

  • message_attachments:消息关联的文件、图片、文档。
  • message_sources:助手回复引用的来源、检索片段或 citation。

这不是唯一设计,但它体现了一个重要原则:消息是用户看到的结果,run 是系统执行的过程。两者要关联,但不要混成一个概念。

会话表:chat_threads

chat_threads 表示一个对话容器。

它通常包含:

字段含义
id会话 id
tenant_id租户或组织
user_id创建者
title会话标题,可由模型生成
statusactive、archived、deleted
modechat、copilot、document_qa 等
summary长对话滚动摘要,可选
metadata业务扩展信息
created_at创建时间
updated_at更新时间

会话表不要承担太多生成细节。它主要回答:这是谁的对话、属于哪个业务场景、当前是否可见、列表页如何展示。

如果应用支持多租户,tenant_id 非常重要。后续权限、配额、成本统计、数据隔离都离不开它。

消息表:chat_messages

chat_messages 保存用户可见的消息。

常见字段包括:

字段含义
id消息 id
thread_id所属会话
parent_message_id父消息,用于分叉或追踪上下文
roleuser、assistant、system、tool、event
content展示内容
content_formattext、markdown、json、rich_text
statuspending、streaming、completed、failed、cancelled
position会话内排序
created_by用户、系统或 AI
created_at创建时间
updated_at更新时间

这里有几个设计点。

第一,role 不一定只有 user 和 assistant。

前端页面上主要展示 user 和 assistant,但系统内部可能需要 system event、tool result、error event。是否放在同一张消息表,取决于产品是否要展示它们。如果只是运行日志,可以放到 run steps;如果用户需要看到“正在查询订单”“已读取 3 篇文档”,可以用 event message 或 step event 支持 UI。

第二,助手消息应该允许先创建、后填充。

Streaming 场景下,后端可以先插入一条 status=streaming 的 assistant message,再逐步更新内容。生成完成后改为 completed。失败或取消时保留已生成的部分,状态改为 failedcancelled

第三,parent_message_id 可以支持分叉。

用户编辑历史消息后重新生成,原来的后续消息不一定要删除。更好的方式是形成一棵消息树或使用 branch 标识。简单产品可以先不做完整分叉,但至少要意识到“会话不是永远线性的”。

Run 表:ai_runs

ai_runs 记录一次 AI 生成任务。

一条 assistant message 可以由一个 run 生成,也可能经过失败重试后关联多个 run。一个 run 也可能还没生成最终 assistant message,比如在工具调用失败前就终止了。

常见字段包括:

字段含义
idrun id
thread_id所属会话
trigger_message_id触发 run 的用户消息
assistant_message_idrun 输出到哪条助手消息
statuscreated、queued、running、requires_action、completed、failed、cancelled
featurechat、rewrite、summary、agent
model使用模型
providerProvider
prompt_versionprompt 模板版本
context_snapshot_id本次上下文快照,可选
error_code归一化错误码
error_message内部错误摘要
started_at开始时间
completed_at结束时间

run 的价值是让一次生成可追踪。当前端反馈“这次回答卡住了”,你应该能通过 run id 找到:

  • 用户发了哪条消息。
  • 系统选择了哪个模型。
  • 上下文用了哪些消息和检索片段。
  • 是否调用了工具。
  • 是否发生 retry 或 fallback。
  • 首 token 花了多久。
  • 最终为什么失败或成功。

没有 run 表,AI Chat 的排查通常只能靠翻日志。

Step、工具调用和检索记录

现代 AI Chat 经常不是一次模型调用结束。

它可能先检索文档,再调用模型;模型要求调用工具,工具返回结果后再调用模型;中途还可能等待用户确认。

这时需要 ai_run_steps

字段含义
idstep id
run_id所属 run
typemodel_call、retrieval、tool_call、tool_result、guardrail
statusrunning、completed、failed、skipped
inputstep 输入摘要或结构化参数
outputstep 输出摘要
started_at开始时间
completed_at结束时间

工具调用可以单独建表,因为它通常需要更强审计。

tool_calls 可以记录工具名、参数、调用状态、是否需要用户确认、执行者、幂等 key。tool_results 可以记录结果摘要、错误、外部系统返回 id。

注意,不建议把大型工具结果完整塞进下一轮 prompt,也不一定要完整塞进消息内容。数据库可以保存完整结果,但上下文组装时只注入必要摘要和关键字段。

用量与成本:ai_usage

AI 应用一定要从早期就记录用量。

ai_usage 可以按 run 记录:

字段含义
run_id对应 run
input_tokens输入 token
output_tokens输出 token
total_tokens总 token
estimated_cost估算成本
currency货币
first_token_ms首 token 时间
total_latency_ms总耗时
provider_latency_msProvider 耗时
retry_count重试次数
fallback_used是否 fallback

这些数据不只是给财务看,也会影响产品和工程决策。

例如,某个入口成本突然上升,可能是 prompt 版本变长、检索片段过多、历史裁剪失效或模型输出失控。没有 usage 表,就很难定位。

Context Snapshot 是否需要存

很多 AI 问题需要复现,但复现 AI 输出并不容易。

模型可能升级,随机采样可能不同,外部工具结果可能变化,检索库内容也可能更新。因此,生产系统常常需要保存本次请求的上下文快照。

可以设计 ai_context_snapshots

字段含义
idsnapshot id
run_id对应 run
prompt_versionprompt 版本
messages实际发送给模型的消息
tools暴露给模型的工具定义
retrieval_refs使用的检索片段引用
token_budgettoken 预算配置
redacted是否已脱敏

是否保存完整上下文,要看安全和合规要求。包含用户隐私、商业数据或密钥风险的内容必须脱敏或只保存引用。不要为了调试方便,把所有敏感上下文原样写进日志和数据库。

前端状态如何映射数据库

前端看到的是消息列表,但底层状态来自 message 和 run 的组合。

一个常见流程是:

前端可以把 assistant_message_id 作为 UI 上正在生成的消息,把 run_id 作为操作对象。点击停止、重试、查看日志、反馈质量时,都应该带上 run id。

对于前端工程师来说,最重要的是不要只把 AI 回复当成一个最终字符串。它是一个持续变化的对象,有生命周期、有错误、有来源、有成本。

常见坑

坑一:只有 messages 表,没有 runs 表。

这样很难追踪一次生成为什么失败、用了哪个模型、消耗多少 token、是否调用工具。消息表负责展示,run 表负责过程。

坑二:把所有信息都塞进 metadata

metadata 很适合放低频扩展字段,但不适合承载核心查询。状态、模型、token、错误码、耗时、租户、用户这些字段应该结构化。

坑三:不区分展示内容和模型上下文。

数据库里完整保存的消息,不等于下一次要完整发给模型。上下文组装应该有自己的策略和快照。

坑四:失败消息直接删除。

失败、取消、中断是重要状态。删除它们会让用户困惑,也会让排查失去线索。更好的做法是保留状态和可重试入口。

坑五:没有幂等设计。

网络重试、浏览器刷新、用户连点发送,都可能创建重复用户消息或重复 run。发送消息接口应该支持客户端生成 id 或 idempotency key。

坑六:忽略删除和合规。

AI Chat 往往包含敏感内容。删除会话时,消息、附件、上下文快照、日志、工具结果和向量索引引用都需要有一致策略。

坑七:过早追求复杂消息树。

分叉、编辑、版本历史很有用,但不是所有产品第一天都需要完整实现。可以先保留 parent_message_idbranch_id 的扩展空间,避免后续完全重构。

小结

AI Chat 的数据库模型,不只是保存聊天记录。它要同时支持用户可见消息、AI 生成过程、工具调用、上下文追踪、用量统计、错误排查和未来扩展。

一个实用的起点是把 chat_threadschat_messagesai_runsai_run_stepsai_usage 分开。消息负责展示,run 负责生命周期,step 负责过程细节,usage 负责成本和性能。

对前端工程师来说,理解这套模型会直接改善 UI 设计:你会知道为什么助手消息需要 streaming 状态,为什么停止和重试要基于 run,为什么错误不能只显示一句失败,为什么一次 AI 回复背后其实是一条可追踪的系统链路。