JchatMind智能体数据模型设计:从会话存储到RAG检索的完整建表实践

发布时间:2026/10/3 18:07:58
JchatMind智能体数据模型设计:从会话存储到RAG检索的完整建表实践 这两天我一直在推进 JchatMind 这个带记忆与知识库能力的对话智能体前两天的需求梳理和技术选型落定之后今天正好走到“第三天数据模型设计”这一步。数据模型对智能体来说就是骨架骨架歪了后面所有功能都会跟着别扭。这篇文章我把完整的建模过程、字段取舍理由、建表SQL和踩过的坑全部整理出来给同样在搭智能体数据层的朋友一个可以直接抄作业的参考。JchatMind 的核心场景是“能记住上下文的 AI 助理”用户进来创建会话、持续对话、中间可能上传文档做知识库检索同时系统要能从对话里抽取出用户的偏好形成长期记忆。围绕这些能力数据模型必须同时支撑会话存储、流式消息记录、RAG 向量检索、记忆抽取四条链路。下面从设计思路开始讲再到具体表结构和落库实操。1. 从需求到模型先想清楚 JchatMind 到底要记住什么1.1 三个核心问题决定了表怎么拆我开始动手之前先给自己提了三个问题。第一对话上下文存在哪里怎么恢复会话第二知识库文件进去之后如何让检索又快又准第三用户长期记忆和短期的会话历史是不是一回事这三个问题不搞清楚表结构一定会越写越乱。对话上下文对应会话表和消息表知识库对应文档表和切片表长期记忆则是独立的记忆表跟消息表分开。很多人把“记忆”简单等同于聊天记录这是最大的误解。聊天记录是原始事实记忆是经过抽取和加工后的结构化信息两者在数据模型里天然分属不同层级。想明白这一点之后JchatMind 的数据实体就被拆成了五个域用户域、会话域、知识域、记忆域、反馈域。用户域管身份和权限会话域管每一轮对话的上下文知识域承接文档上传和分片检索记忆域存放从对话中抽取到的长期事实反馈域记录用户对回复的评分用来做后续效果优化。1.2 五类数据的边界怎么划边界划分上我遵循一个原则一张表只装一种“业务主语”。用户在表里会话在表里消息在表里互不混装。会话表只存会话元信息比如标题、创建时间、关联模型配置消息表只存每一条消息本体包括角色、内容、token数、回复耗时文档表只存文件元数据切片表只存分片后的文本块和向量记忆表只存抽取出来的事实条目。这样做的好处非常直接。后面每次迭代只动对应的表不会因为需求变化导致“一张大表牵一发动全身”。同时查询语义也清晰比如“列出这个用户最近10个会话”就走会话表加索引“查某条消息的完整上下文”就走消息表按会话ID排序。边界划得干净索引和查询优化才能跟得上。需要注意的是边界划分不等于物理隔离。JchatMind 后续要走多租户模式所有业务表都通过user_id做归属隔离而不是给每个用户单独建一套库表。这是省成本的做法也是绝大多数智能体项目的标准路线。2. 核心表结构设计与字段取舍2.1 用户与会话多租户隔离和上下文还原用户表不复杂但也不能太简陋。我把id设计成 UUID 字符串而不是自增整型理由是智能体经常要对接外部账号体系UUID 可以避免泄露注册量等业务数据也方便横向分库。密码字段存的必须是哈希值这个没什么好讨论的明文存密码等于裸奔。状态字段用整型枚举1 表示正常、0 表示禁用扩展示性好。会话表是 JchatMind 的重头戏。我有一个体会会话表不要只存“标题”和“时间”一定要把“模型配置快照”存进去。什么意思用户新建一个会话时选定了某个模型和一组温度参数后续整个会话都用这套配置哪怕后台默认配置改了历史会话翻出来也要按原配置继续。实现方式就是加一个config_snapshot字段类型用 JSONB把模型名、temperature、top_p、max_tokens 全塞进去查询的时候直接取出来用。这个技巧我实测非常实用避免了配置变更导致历史会话行为漂移的问题。会话表的标题我设置了自动生成机制新会话创建时标题为“新会话”等用户发出第一条消息后后台异步用大模型生成一个简短标题再更新进去。这样列表页看起来干净又不需要用户手动命名体验很顺。2.2 消息表把流式输出和 Token 消耗记清楚消息表是 JchatMind 读写最频繁的表之一我单独讲。每条消息的核心字段是会话ID、角色、内容、token数。角色只能是 system、user、assistant 三种我会用role字段存字符串枚举不加数字枚举原因是代码里直接可读排查日志时一眼就能看懂。特意加上input_tokens和output_tokens两个字段是为了算成本和做用量统计。很多项目一开始不做这个记录等账单出来才傻眼。我建议从第一天就存而且必须把一轮请求的提示词token和生成token分开存单条消息存一个总token数没有分析价值。关于流式输出还有一个细节智能体回复是边生成边吐字的前端实时渲染但落库的时候不能把每一片增量都写进去。正确做法是等流式结束后把完整内容一次性写入消息表。中间状态如果需要展示前端自己维护一个临时缓冲区即可。这个设计可以极大降低数据库写入频率避免因为高频写库把数据库连接池打满。消息表还要存一个message_order整型字段用来保证同一会话内消息的顺序。不要相信created_at排序同一个毫秒内可能插入多条消息时间戳精度不够。我曾经在线程并发写入场景下遇到过顺序错乱后来加message_order才彻底解决。具体的并发处理方式后面实操章节会讲。2.3 知识库与切片RAG 检索的地基JchatMind 的文档问答能力依赖 RAG而 RAG 的地基就是知识库两张表文档表和切片表。文档表记录文件名、文件类型、大小、存储路径、上传人、处理状态。状态字段特别关键因为文档上传后要经过解析、清洗、分片、向量化几个阶段每一步都可能失败状态机设计成 pending、processing、completed、failed 四种配合error_message字段记录失败原因。切片表是检索的真正入口。每个文档会被拆成若干文本块每块存原文内容、字符序号、token数、向量。向量字段类型取决于数据库选型我用的是 pgvector 的vector(1536)这个维度跟所用embedding模型的输出维度严格对应模型选 768 就用 vector(768)选 1536 就用 vector(1536)不能乱填。查的时候先按文档过滤再做向量相似度检索走 HNSW 索引速度很快。切片粒度是一个需要自己试的变量。我建议初期按 500 到 800 个字符作为一块重叠 100 到 150 个字符这个参数适配大多数通用文档。如果文档结构高度结构化可以适当调小切片长度换取检索精度。JchatMind 的表里我会额外加一个chunk_index记录切片的序号方便定位原始文档的上下文位置。2.4 长效记忆表让智能体从“聊过就忘”变成“越用越懂你”记忆表是 JchatMind 区别于普通 chatbot 的关键设计。需求来自一个很现实的场景用户昨天说自己喜欢简洁回复、对Python更熟、工作日晚上才有空今天再进来智能体如果能记住这些信息回答质量会完全不一样。我的做法是设计一张memories表字段包括用户ID、记忆内容、记忆类型、置信度、来源会话ID、记忆状态。记忆类型分为偏好类、事实类、行为类。偏好类比如“用户喜欢短回答”事实类比如“用户是一名后端开发工程师”行为类比如“用户通常在晚上九点活跃”。置信度是一个 0 到 1 的浮点数由抽取模型的打分决定低于 0.7 的候选记忆不写入。记忆不能只存不销。我加了status字段做生命周期管理默认active当用户在后续对话中反驳了该记忆相关内容时系统可以把它标记为 archived避免记忆“越记越错”。记忆表还有一个隐含价值它是自我进化训练数据的来源。长期积累下来的高置信度记忆可以抽出来做模型的偏好对齐数据。这里我要特别强调记忆表和会话消息表绝对不要合并。记忆是稀疏的结构化知识消息是稠密的原始记录两者生命周期和读写模式完全不同。合并成一张表后查询效率、维护成本、权限控制都会变得非常难受。3. 实操落地数据库选型与建表全过程3.1 为什么选了 PostgreSQL pgvector数据模型设计不能停留在纸面最终要落到可运行的数据库里。JchatMind 我选的是 PostgreSQL 加 pgvector 扩展而不是单独上一套向量数据库。原因是这个项目阶段数据一致性比向量检索的极限性能更重要。PostgreSQL 天然支持事务一把梭哈关系数据和向量数据不需要维护两套存储的同步等用户量和数据量真的涨上来再演进到独立的向量检索服务也来得及因为表结构隔离已经做好了。本地开发和轻量部署我还有一套备选方案SQLite 加 sqlite-vec。SQLite 版本省去服务端运维适合个人项目和演示场景但并发写入能力有限生产环境我不建议。我的推荐是开发环境用 SQLite测试和生产统一 PostgreSQL迁移工具用 Alembic 或直接执行 SQL 脚本。JchatMind 目前就是一套 PostgreSQL 实例跑到底。3.2 核心建表语句与索引设计下面是 JchatMind 核心表的建表 SQL我按实际生产标准写的字段注释完整可以直接复制后按需调整。用户表CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(64) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, email VARCHAR(128), avatar_url TEXT, status SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_users_created_at ON users(created_at);会话表CREATE TABLE sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(200) NOT NULL DEFAULT 新会话, config_snapshot JSONB NOT NULL DEFAULT {}::jsonb, status SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_sessions_user_created ON sessions(user_id, created_at DESC); CREATE INDEX idx_sessions_updated_at ON sessions(updated_at DESC);注意会话表两个索引的区别。idx_sessions_user_created服务“列出某个用户最近的会话列表”这个高频查询把 user_id 和 created_at 放在同一个索引里可以一次索引扫描出结果idx_sessions_updated_at服务后台的全局活跃会话扫描。联合索引的字段顺序是 user_id 在前、时间在后这个顺序不要反。消息表CREATE TABLE messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role VARCHAR(16) NOT NULL CHECK (role IN (system,user,assistant)), content TEXT NOT NULL, message_order INTEGER NOT NULL, input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0, latency_ms INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (session_id, message_order) ); CREATE INDEX idx_messages_session_order ON messages(session_id, message_order); CREATE INDEX idx_messages_user_created ON messages(user_id, created_at DESC);知识库文档表和切片表CREATE TABLE knowledge_docs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, filename VARCHAR(255) NOT NULL, file_type VARCHAR(32) NOT NULL, file_size BIGINT NOT NULL, storage_path TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT pending, error_message TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_knowledge_docs_user_status ON knowledge_docs(user_id, status); CREATE TABLE knowledge_chunks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), doc_id UUID NOT NULL REFERENCES knowledge_docs(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, chunk_index INTEGER NOT NULL, content TEXT NOT NULL, token_count INTEGER NOT NULL DEFAULT 0, embedding VECTOR(1536), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_knowledge_chunks_doc ON knowledge_chunks(doc_id); CREATE INDEX idx_knowledge_chunks_user ON knowledge_chunks(user_id); CREATE INDEX idx_knowledge_chunks_embedding ON knowledge_chunks USING hnsw (embedding vector_cosine_ops);记忆表和反馈表CREATE TABLE memories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, content TEXT NOT NULL, memory_type VARCHAR(16) NOT NULL CHECK (memory_type IN (preference,fact,behavior)), confidence FLOAT NOT NULL DEFAULT 0.0, source_session UUID REFERENCES sessions(id) ON DELETE SET NULL, status VARCHAR(16) NOT NULL DEFAULT active, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_memories_user_status ON memories(user_id, status); CREATE INDEX idx_memories_user_type ON memories(user_id, memory_type); CREATE TABLE feedbacks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5), comment TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_feedbacks_user_created ON feedbacks(user_id, created_at DESC); CREATE INDEX idx_feedbacks_message ON feedbacks(message_id);3.3 字段类型与索引细节的三点经验第一时间字段一律用TIMESTAMPTZ不要用TIMESTAMP。TIMESTAMP 不带时区信息同一个时间在不同环境下解析会出偏差排查起来非常痛苦。TIMESTAMPTZ 内部统一按 UTC 存储展示时再按客户端时区转换JchatMind 面向多地域用户时这是必须的。第二JSONB 字段是扩展性的好工具但要克制。我建议只用来存“确实无法确定结构”的配置类数据比如config_snapshot。核心业务字段一律显式建列否则时间一长所有人往 JSONB 里塞东西表结构失去约束查询也没法走索引。第三HNSW 索引的构建参数要按数据量调整。pgvector 创建向量索引时默认参数m 16, ef_construction 64小规模数据没问题文档切片超过十万条之后我建议调成m 32, ef_construction 128召回率会明显改善代价是索引占用空间增大和写入略微变慢这个取舍值得。3.4 消息顺序与并发写入的现场处理消息表有一个UNIQUE (session_id, message_order)约束并发写入时不能让两条消息拿到同一个 order。我的实现方式不是先查再插那样两个请求可能同时查到相同的最大值。正确做法是用 SQL 的原子操作取当前会话最大值加 1INSERT INTO messages (id, session_id, user_id, role, content, message_order) VALUES ( gen_random_uuid(), $1, $2, $3, $4, (SELECT COALESCE(MAX(message_order), 0) 1 FROM messages WHERE session_id $1) );这个写法在最坏情况下仍可能因为双事务同时读到相同最大值而产生唯一约束冲突。实际线上我加了一个重试机制捕获 unique violation 错误后重新执行一次。由于消息写入的冲突概率极低重试一次基本都能成功。这个方法虽然简单但很多人会忽略我特意记录一下。4. 常见问题与排查技巧实录4.1 字段类型选错引发的连锁问题我早期在消息表用VARCHAR存消息内容等到某天有用户贴了一大段代码进来直接报字符串超长。PostgreSQL 会硬性报错用户体验就是消息发不出去。内容类字段一开始就要用TEXT不要用VARCHAR限制长度。同样的坑在文档表的存储路径字段也踩过所以现在所有开放文本一律 TEXT只有明确长度上限的字段才用 VARCHAR。还有 token 数用INTEGER够不够的问题。单轮对话 token 数基本不会超过 4 万INTEGER 上限约 21 亿完全够用。但如果以后要做整个用户全生命周期的累积统计建议单列一张usage_stats表用 BIGINT 存累计值不要在 messages 表里累加。4.2 索引没建对查询慢三个量级有一次测试环境消息量到几十万条用户点开会话详情页面要好几秒问题出在查消息列表走了全表扫描。原因是消息表一开始只建了单列索引session_id没建联合索引。加了(session_id, message_order)联合索引后查询直接从几百毫秒降到十几毫秒。排查思路很简单用EXPLAIN ANALYZE看执行计划。如果看到Seq Scan就说明没走索引。索引不是越多越好每多一个索引写入性能就多一分负担我的原则是为“实际查询模式”建索引不为“将来可能用到的查询”建索引。4.3 软删除与唯一约束的冲突我一开始给用户表加了is_deleted软删除字段又在username上设了唯一约束。问题来了用户删除账号后重新注册同一个用户名插入直接违反唯一约束因为旧数据还在表里。这就是软删除和唯一约束打架的典型场景。解决方案有两个方向。一是唯一约束改成部分唯一索引只对未删除的记录生效CREATE UNIQUE INDEX idx_users_username_active ON users(username) WHERE is_deleted false;二是彻底物理删除用户数据但这种做法在智能体项目里不可取因为审计和复盘的原始数据很重要。JchatMind 采用第一种方案既保留历史数据又不影响新用户注册。4.4 向量维度不匹配的排查RAG 链路最常见的报错是 embedding 维度不匹配建表时用VECTOR(1536)但实际模型输出的向量是 768 维插入时报expected 1536 dimensions, not 768。这类错误信息其实很明确但新手容易懵。我的经验是向量维度不是一个随意选的数字它由 embedding 模型决定。用 OpenAI 的 text-embedding-3-small 是 1536 维用智谱的 embedding-3 是 1024 维换模型就必须迁移表字段。迁移方式就是ALTER TABLE knowledge_chunks ALTER COLUMN embedding TYPE VECTOR(1024);但注意旧数据要重新向量化这个操作最好写成脚本批量跑。另外一个容易被忽略的点同一个知识库里的所有切片必须用同一个 embedding 模型生成混用模型会导致向量空间不一致检索结果完全没有意义。这一点我会在代码层的向量化服务里强制指定模型版本并把它写入系统配置表。5. 一点后续想做的事数据模型设计阶段还有一个我主动砍掉的需求那就是多智能体共享知识库的权限模型。当前 JchatMind 每个用户的数据完全隔离但后续如果做团队版一个知识库要能被多个智能体引用就要引入资源授权表。这块我没放进第三天的范围因为技术栈还没完全稳定贸然加授权表容易过度设计。消息表的清理策略也值得提前规划。随着对话量增加消息表会越来越大单表突破千万行之后要考虑分区。我的预想方案是按月做分区旧分区可以归档或者冷存。不过在实际达到这个量级之前当前设计完全够用不要为了虚无缥缈的性能问题提前上复杂架构。数据模型设计这件事我的体会是“慢就是快”。多花一天把边界想清楚后面写业务代码会顺手很多反过来草草建几张表就开干中期改表结构的成本远高于一开始多思考的时间。这份设计会在 JchatMind 后续开发过程中持续迭代如果后面踩到新的数据层问题我再单独写文章记录。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询