如何保存AI生成记录
保存用户的生成记录,是 AI 产品不可或缺的基础能力。用户花了几十秒等待模型生成一张图片、一段文案或一篇分析报告,如果刷新页面后记录消失,信任感会瞬间崩塌。更现实的问题是——生成记录不仅关系到用户体验,还是计费依据、质量回溯的数据基础,以及后续模型微调的素材来源。
从技术角度看,AI 生成记录的保存涉及存储策略选择、数据库表结构设计、历史查询性能优化、数据导出能力和存储成本控制等多个环节。这一节会系统梳理这些环节的设计思路和工程实践,帮助你在产品早期就搭建起一套可靠的记录保存体系。
记录保存策略
AI 生成记录的保存时机并不是「生成完再存」这么简单。不同产品的交互模式、用户对延迟的敏感度和数据的可靠性要求,会直接影响保存策略的选择。
实时保存(Synchronous Save)
实时保存是指 AI 模型完成输出后,立即将记录写入数据库,用户收到响应时数据已经持久化。
适用场景: 计费敏感型产品(按次扣费)、企业级应用(需要审计追踪)、多端同步场景。
实现要点:
- 在 API 响应返回前完成数据库写入,确保「返回成功 = 已保存」
- 使用事务保证 prompt、output、元数据原子写入
- 写入延迟通常增加 10-50ms,对大多数场景可接受
风险: 如果数据库出现抖动,会直接拖慢用户请求的响应时间。
延迟保存(Asynchronous Save)
延迟保存是指先将生成结果返回给用户,再通过后台队列异步写入数据库。
适用场景: 对响应延迟极度敏感的对话类产品、生成频率高但单条记录价值低的场景。
实现要点:
- 使用消息队列(如 Redis Stream、RabbitMQ、Kafka)缓冲写入请求
- 消费端批量写入,减少数据库压力
- 需要处理「用户看到了结果但记录尚未落库」的中间状态——比如用户立刻刷新历史记录页面时可能看不到刚才的生成
风险: 队列积压或消费者宕机时可能丢失记录。需要至少一次(at-least-once)的投递保证和幂等写入机制。
手动保存(Manual Save)
手动保存是指生成结果先存储在客户端或临时缓存中,用户主动点击「保存」后才写入数据库。
适用场景: 创意工具类产品(用户会大量生成但只保留满意的结果)、免费增值产品的免费额度场景。
实现要点:
- 客户端使用 IndexedDB 或 localStorage 做本地缓存
- 服务端提供临时存储(TTL 缓存),过期自动清理
- 保存动作涉及客户端上传 + 服务端持久化两步
风险: 用户忘记保存、清除浏览器数据或换设备后记录丢失。需要在 UI 上给出明确的保存提示。
保存策略对比
| 维度 | 实时保存 | 延迟保存 | 手动保存 |
|---|---|---|---|
| 数据可靠性 | 最高(返回即落库) | 中等(依赖队列可靠性) | 最低(依赖用户操作) |
| 响应延迟影响 | 增加 10-50ms | 几乎无影响 | 用户感知为即时 |
| 实现复杂度 | 低 | 中(需处理队列和重试) | 中(需处理客户端缓存和同步) |
| 存储成本 | 高(全量保存) | 高(全量保存) | 低(只保存用户选择的内容) |
| 适合产品类型 | 企业应用、计费产品 | 对话助手、高频生成 | 创意工具、图片生成 |
对于出海产品,还需要考虑用户所在地区的网络延迟。东南亚、拉美等地区的数据库访问延迟可能显著高于北美和欧洲。如果你的数据库部署在 AWS us-east-1,雅加达用户的写入延迟可能超过 200ms。这种情况下,延迟保存或异地写入同步是更务实的选择。
数据库表设计
AI 生成记录的数据模型看起来简单——无非是「谁、什么时候、输入了什么、输出了什么」——但在实际工程中,需要兼顾查询性能、存储效率和扩展性。
核心表结构
一个典型的生成记录表包含以下字段:
CREATE TABLE generation_records (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
session_id VARCHAR(64),
feature_type VARCHAR(32) NOT NULL, -- 功能类型:text/image/code/chat
model_id VARCHAR(64) NOT NULL, -- 模型标识:gpt-4o、dall-e-3 等
prompt TEXT NOT NULL, -- 用户输入(prompt)
output MEDIUMTEXT, -- 模型输出内容
output_type VARCHAR(16), -- 输出类型:text/json/image_url/audio_url
token_count INT DEFAULT 0, -- token 消耗量
latency_ms INT DEFAULT 0, -- 生成耗时(毫秒)
status VARCHAR(16) DEFAULT 'success', -- success/failed/timeout
metadata JSON, -- 扩展参数(温度、种子值等)
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_created (user_id, created_at DESC),
INDEX idx_feature_type (feature_type, created_at DESC),
INDEX idx_status (status, created_at DESC)
);设计时需要注意几个关键点:
user_id 必须有索引。 绝大多数查询都是「某个用户的历史记录」,这是最高频的查询模式。没有索引的话,随着数据量增长,查询会迅速恶化。
output 字段要考虑大小限制。 文本类输出通常不会太大,但如果是图片的 Base64 编码或长音频,单条记录可能达到数 MB。这种情况建议将大体积内容存储到对象存储(如 AWS S3),数据库只存引用 URL。
metadata 用 JSON 类型而非固定列。 AI 模型的参数经常变化——今天用的是 temperature 和 top_p,明天可能新增 response_format 或 seed。用 JSON 字段可以避免频繁的 DDL 变更。MySQL 5.7+、PostgreSQL 9.3+ 都支持 JSON 类型和 JSON 路径查询。
关联表设计
当产品功能增多后,单表结构往往不够用。常见的扩展是拆出会话表和附件表:
-- 会话表:记录用户的对话上下文
CREATE TABLE sessions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
title VARCHAR(256),
feature_type VARCHAR(32) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_updated (user_id, updated_at DESC)
);
-- 附件表:存储生成产物中的文件引用
CREATE TABLE generation_assets (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
record_id BIGINT NOT NULL,
asset_type VARCHAR(16) NOT NULL, -- image/audio/video/document
storage_url VARCHAR(512) NOT NULL,
file_size INT DEFAULT 0,
mime_type VARCHAR(64),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_record (record_id)
);会话表的作用是将同一次对话的多轮生成归组。用户查看历史时,通常希望看到「某次对话」,而不是散落的单条记录。附件表则用于处理多模态输出——一次生成可能同时产出文本和多张图片,需要分别记录存储位置。
索引策略
索引设计要匹配实际的查询模式。以下是 AI 产品中最常见的几种查询:
- 用户历史记录列表 —
WHERE user_id = ? ORDER BY created_at DESC LIMIT 20 - 按功能类型筛选 —
WHERE user_id = ? AND feature_type = ? ORDER BY created_at DESC - 全文搜索 —
WHERE user_id = ? AND MATCH(prompt) AGAINST('关键词') - 管理后台统计 —
WHERE created_at BETWEEN ? AND ? AND status = 'success' GROUP BY feature_type
前两种查询通过组合索引 idx_user_created 和 idx_feature_type 就能覆盖。全文搜索需要额外的全文索引(MySQL 的 FULLTEXT 或 PostgreSQL 的 tsvector)。管理后台的统计查询则适合用独立的统计表或定期聚合来优化,避免在大量记录上实时 GROUP BY。
历史记录查询
查询设计的核心矛盾是:数据量会随时间线性增长,但用户对查询速度的期望不会降低。一条 SQL 在 1 万条记录时跑得飞快,到 1000 万条时可能就需要几秒甚至更久。
分页方案
最常见的查询是「历史记录列表」,分页方案的选择至关重要。
Offset 分页是最直观的写法:LIMIT 20 OFFSET 100。问题是 offset 越大越慢——数据库需要先扫描前 120 条再丢弃前 100 条。当用户翻到第 100 页(offset = 2000)时,性能已经开始下降;翻到第 10000 页时,延迟可能超过一秒。
游标分页(Cursor-based Pagination) 是更好的方案:
-- 第一页
SELECT * FROM generation_records
WHERE user_id = 123
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- 后续页:用上一页最后一条记录的 created_at 和 id 作为游标
SELECT * FROM generation_records
WHERE user_id = 123
AND (created_at, id) < ('2026-06-15 10:30:00', 45678)
ORDER BY created_at DESC, id DESC
LIMIT 20;游标分页的性能不随页数增长,因为每次查询都从索引的精确位置开始扫描。缺点是用户无法「跳转到第 N 页」,但对于历史记录这种场景,用户几乎不会跳页。
搜索与筛选
当用户的历史记录超过几十条后,搜索功能就变得必要了。
基础筛选通过组合条件实现——按功能类型、时间范围、状态过滤。这些查询依赖前面提到的组合索引,性能通常没有问题。
全文搜索的实现方式取决于数据规模和产品阶段:
| 方案 | 适用规模 | 延迟 | 实现复杂度 | 搜索质量 |
|---|---|---|---|---|
| 数据库 LIKE / ILIKE | < 10 万条 | 低 | 最低 | 差(仅精确匹配) |
| MySQL FULLTEXT / PostgreSQL tsvector | < 500 万条 | 低-中 | 中 | 中(支持分词) |
| Elasticsearch / Meilisearch | 不限 | 低 | 高 | 高(分词+相关性排序+高亮) |
早期产品用数据库自带全文索引就够了。当搜索请求成为性能瓶颈或需要更好的搜索体验(模糊匹配、拼音搜索、结果高亮)时,再引入专用搜索引擎。
查询性能优化
几个实用的优化手段:
- 读写分离:历史查询走从库(replica),生成写入走主库。PostgreSQL 和 MySQL 都支持这种模式,云数据库(如 AWS Aurora、Neon)通常开箱即用。
- 热冷分离:最近 7 天或 30 天的记录放在主表中,更早的记录归档到历史表。查询默认只扫描热数据,需要时再手动或自动切换到历史表。
- 计数缓存:「你共有 N 条生成记录」这个数字如果每次都
COUNT(*)会很慢。可以用一个计数器字段,每次新增或刪除时增减,或者用 Redis 缓存计数结果,设置 5 分钟的 TTL。
数据导出
让用户能够导出自己的生成记录,不仅是用户体验的加分项,在很多市场也是合规要求——GDPR 的「数据可携带权」(Right to Data Portability)明确要求用户能够获取自己存储的结构化数据。
导出格式
| 格式 | 适用内容 | 优点 | 缺点 |
|---|---|---|---|
| JSON | 结构化数据(含 prompt、元数据、token 数) | 完整保留所有信息,程序可直接解析 | 普通用户难以阅读 |
| CSV | 表格型数据(生成时间、类型、内容摘要) | Excel 可直接打开,用户易读 | 不适合嵌套结构和大文本 |
| 图文混排的报告型内容 | 排版美观,适合分享和存档 | 生成成本高,数据不可编辑 | |
| Markdown | 文本类生成内容 | 轻量可读,版本管理友好 | 不支持复杂表格和元数据 |
导出实现
导出的核心挑战是「不要在导出时拖垮线上数据库」。如果用户有 10 万条记录,一次性查出来再生成文件,数据库连接和内存都会成为问题。
推荐的做法是异步导出:
关键点:
- 分批查询:用流式查询(MySQL 的 streaming query 或 PostgreSQL 的 cursor)避免一次性加载大量数据到内存
- 临时文件存储:导出的文件放到 S3 或类似的对象存储,设置生命周期策略自动清理(比如 24 小时后删除)
- 导出限流:限制单个用户的导出频率(比如每小时最多 3 次),避免恶意或频繁导出消耗资源
存储优化
AI 生成记录的体积增长速度往往超出预期。一次图片生成可能产生数 MB 的数据,一个活跃用户每天可能产生数百条记录。如果不做存储优化,存储成本会随用户增长线性上升,最终成为不可忽视的开支。
压缩策略
对于文本类输出,数据库层面可以开启行压缩(Row Compression)。MySQL 的 InnoDB 和 PostgreSQL 的 TOAST 机制都支持透明的数据压缩,文本数据的压缩比通常在 3:1 到 5:1 之间。
对于图片和视频等多媒体产物,不建议存在数据库中。更好的方案是直接存到对象存储,数据库只保留 URL 引用。对象存储本身已经针对大文件做了优化,成本也远低于数据库存储。
数据归档
随着时间推移,大量历史记录的访问频率会急剧下降。一个月前的生成记录,用户可能只在特殊情况下才会翻看。这时候就可以考虑归档。
分层存储方案:
| 层级 | 数据范围 | 存储位置 | 查询延迟 | 存储成本 |
|---|---|---|---|---|
| 热数据 | 最近 30 天 | 主数据库(SSD) | < 50ms | 最高 |
| 温数据 | 30 天 - 1 年 | 从数据库或只读副本 | < 200ms | 中 |
| 冷数据 | 1 年以上 | 对象存储(S3 Glacier)或归档表 | 秒级到分钟级 | 最低 |
归档的实现可以是定期任务(如每天凌晨执行一次),将超过 30 天的记录从主表迁移到归档表。归档表的结构可以与主表相同,但放在更廉价的存储介质上,或者直接从主表中删除并将完整数据导出到 Parquet 格式文件存到 S3。
成本控制
存储成本不只是硬盘费用。在云环境中,还需要考虑备份成本、复制流量费用和查询产生的计算费用。
几个实际的成本控制手段:
- 定期清理无价值的临时数据:比如生成失败的记录、用户未保存的草稿,设置 7 天 TTL 自动删除
- 限制免费用户的记录保留时长:免费用户的记录保留 30 天,付费用户保留全部。这既是成本控制手段,也是付费转化的推动因素
- 使用对象存储存储大体积产物:1 GB 的数据库存储成本大约是 S3 的 5-10 倍。图片和视频产物应该尽早迁出数据库
- 监控存储增长趋势:建立告警机制,当单表数据量超过阈值(如 5000 万条或 100 GB)时触发归档或分区操作
案例
案例 1:Midjourney 的生成记录管理
Midjourney 的生成记录保存有一个独特之处——它的主要交互发生在 Discord 中,而非独立的 Web 应用。每条 /imagine 命令的生成结果直接在 Discord 频道中展示,记录保存依赖 Discord 的消息历史。
这种模式的优势是实现简单——不需要自建消息存储,Discord 的消息系统天然提供了历史记录。缺点也很明显:用户无法高效搜索和筛选历史生成,图片与对话内容混杂在一起。
Midjourney 后来推出的 Web 端 Image Creator 和 Personalization 页面弥补了这个问题,将用户的生成记录同步到独立的 Web 界面,支持按时间排序和风格筛选。这个演进说明,即使早期可以用外部平台「偷懒」,生成记录的独立管理和检索能力最终都是绕不开的。
案例 2:Jasper 的企业级记录体系
Jasper 作为面向营销团队的 AI 写作工具,其记录保存需要满足企业级需求:完整的审计追踪、团队协作中的记录归属、按项目分组的组织方式。
Jasper 的设计思路是将每条生成记录关联到具体的项目(Project)、文档(Document)和品牌配置(Brand Voice)。用户在查看历史时,不仅可以看到「我什么时候生成了什么」,还能看到这条记录属于哪个营销项目、使用了什么品牌语调、是哪个团队成员创建的。
这种设计增加了表关联的复杂度(记录表 → 文档表 → 项目表 → 品牌配置表),但换来了企业客户需要的可管理性和可追溯性。对于 B 端出海产品,这种设计几乎是必须的——企业客户不会因为功能好用就买单,但如果缺少审计和追溯能力,他们一定不会买单。
AI 记录保存与查询完整流程
下面用一个完整的流程图展示从用户发起生成到记录保存、查询、导出的全链路:
检查清单
在上线 AI 生成记录保存功能之前,逐项检查以下问题:
- 是否确定了保存策略(实时 / 延迟 / 手动),并与产品形态匹配?
- 生成记录表是否包含 user_id 索引和 created_at 组合索引?
- output 字段是否根据内容类型选择了合适的数据类型(TEXT / MEDIUMTEXT / URL 引用)?
- 多模态产物(图片、音频、视频)是否使用了对象存储而非数据库直接存储?
- 历史记录查询是否使用了游标分页而非 offset 分页?
- 是否实现了全文搜索能力,还是只能做条件筛选?
- 导出功能是否为异步实现,不会拖垮线上数据库?
- 导出文件是否有有效期限制,自动清理以节省存储?
- 是否制定了数据归档策略(热 / 温 / 冷分层)?
- 是否监控了单表数据量和存储总量,设置了告警阈值?
- 免费用户和付费用户的记录保留策略是否做了区分?
- 是否满足目标市场的合规要求(GDPR 数据可携带权、数据删除权)?
- 是否考虑了数据库读写分离,避免查询影响生成写入?
- 是否对 metadata 字段使用了 JSON 类型,以适应模型参数的变化?
小结
AI 生成记录的保存看似是一个简单的 CRUD 操作,但在产品规模增长后,它涉及的存储策略、查询性能、导出能力和成本控制都会成为工程挑战。
核心原则是:早期从简(一张主表 + 基本索引 + 实时保存),但预留扩展空间(JSON 字段存元数据、对象存储分离大文件、异步导出框架)。当数据量和用户量增长到一定规模后,再逐步引入游标分页、全文搜索、数据归档和分层存储。
对于出海产品,额外的考虑是:用户分布在全球各地时的写入延迟、GDPR 等合规框架下的数据导出和删除要求,以及多区域部署时的数据同步策略。这些在产品设计之初就纳入考量,远比事后补救要高效。
参考资料
- Amazon DynamoDB Data Models for Generative AI Chatbots — AWS
- How to Design a Database Schema for AI Chatbot — OpenAI Community
- Role of Databases in GenAI Applications — arXiv
- AI Storage Cost Optimization: A Complete Strategy Guide — Scality
- AI Data Storage: Challenges & Strategies — TierPoint
- Data Compression: Streamlining Archiving & Reducing Costs — ShareArchiver
- 一站式 AI Agent 长记忆方案 — 阿里云文档
- AI 记忆架构探索:从工程原型到架构蓝图 — 巴别之塔