AI 项目中的数据类型
要点
- AI 项目的数据类型比传统 CRUD 更复杂——除了用户、订单,还有向量、embedding、对话历史
- 结构化数据(用户、配置)适合关系型数据库,非结构化数据(文档、图片)适合对象存储
- 向量数据(embedding)需要专门的向量数据库或支持向量检索的扩展
- 对话历史和 prompt 日志是高写入、低读取的场景,需要权衡存储成本
- AI 项目的数据通常有三种访问模式:点查、范围扫描、相似度检索
- 选型前先把数据分类,再决定存储方案,不要一开始就引入过多存储组件
1. AI 项目的数据分类
AI 项目(特别是 LLM 应用)的数据可以分成几类:
1.1 业务数据
用户、账号、订阅、配额、API Key 等。这类数据与传统 SaaS 无异,关系型数据库是标准选择。
// 用户表
type User = {
id: string
email: string
name: string
createdAt: Date
}
// API Key 表
type ApiKey = {
id: string
userId: string
key: string
permissions: string[]
createdAt: Date
}1.2 对话数据
用户与 AI 的对话历史、prompt、completion。这类数据的特点是:
- 写入量大(每次对话都是多条记录)
- 读取模式主要是「按会话 ID 查询完整历史」
- 数据量随时间线性增长
// 会话表
type Conversation = {
id: string
userId: string
title: string
model: string
createdAt: Date
}
// 消息表
type Message = {
id: string
conversationId: string
role: 'user' | 'assistant' | 'system'
content: string
tokens: number
createdAt: Date
}1.3 向量数据
文档的 embedding、图片的特征向量。用于相似度检索(RAG、语义搜索)。
// 文档 embedding
type DocumentEmbedding = {
id: string
documentId: string
chunk: string
embedding: number[] // 1536 维(OpenAI ada-002)
metadata: Record<string, unknown>
}向量数据需要专门的存储——普通关系型数据库做全表扫描计算余弦相似度,性能无法接受。
1.4 文件数据
上传的文档、图片、音频。体积大,不适合直接存数据库。
通常用对象存储(S3、R2、OSS),数据库只存元数据和访问路径:
type File = {
id: string
userId: string
name: string
size: number
mimeType: string
storageKey: string // S3 key
createdAt: Date
}1.5 日志与追踪
prompt 日志、token 用量、API 调用记录、错误日志。
这类数据写入频率高、读取频率低、保留周期有限。可以用专门的日志系统(Loki、ClickHouse),也可以先存关系型数据库,后期再迁移。
2. 结构化数据:关系型数据库
用户、API Key、订阅、配额等结构化数据,关系型数据库是标准选择:
- PostgreSQL:功能全面,JSON 支持好,扩展丰富
- MySQL:生态成熟,云服务商支持好
- SQLite:轻量,适合开发和小项目
选型考量:
- 团队熟悉度:团队熟悉哪个就用哪个
- 云服务商:某些云平台的托管服务有偏好
- Edge 环境:Cloudflare Workers 只能用 D1(SQLite)或远程连接
- JSON 需求:如果数据里经常有半结构化字段,PostgreSQL 的 JSONB 比 MySQL 的 JSON 更强大
AI 项目通常同时有结构化和半结构化数据(例如 prompt 的元数据、模型的配置参数)。PostgreSQL 的 JSONB 可以在关系型表里存储 JSON,查询时又能用 SQL,比单独维护一个文档数据库更简单。
3. 向量数据:向量数据库
向量检索是 AI 项目的核心需求。embedding 是高维向量(通常 768-3072 维),需要计算相似度(余弦相似度、欧氏距离)。
3.1 专用向量数据库
- Pinecone:全托管,免运维,按用量计费
- Weaviate:开源,可自托管,支持混合检索
- Qdrant:开源,Rust 实现,性能好
- Milvus:开源,支持大规模部署
3.2 关系型数据库的向量扩展
- PostgreSQL + pgvector:PostgreSQL 的向量扩展,可以在同一个数据库里存储业务数据和向量
- SQLite + sqlite-vss:SQLite 的向量搜索扩展
对于中小规模的 AI 项目,pgvector 是一个折中选择——不需要额外维护一个向量数据库,PostgreSQL 同时承担业务数据和向量数据的存储。
-- 启用 pgvector 扩展
CREATE EXTENSION vector;
-- 创建带向量的表
CREATE TABLE document_embeddings (
id UUID PRIMARY KEY,
document_id UUID REFERENCES documents(id),
chunk TEXT,
embedding vector(1536), -- OpenAI ada-002 的维度
created_at TIMESTAMP DEFAULT NOW()
);
-- 创建向量索引(IVFFlat)
CREATE INDEX ON document_embeddings
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 相似度查询
SELECT id, chunk, 1 - (embedding <=> $1) AS similarity
FROM document_embeddings
ORDER BY embedding <=> $1
LIMIT 10;3.3 选择向量存储的考量
- 数据规模:少于 100 万向量,pgvector 够用;超过 100 万,考虑专用向量数据库
- 团队运维能力:专用向量数据库需要额外运维,pgvector 复用已有的 PostgreSQL 运维能力
- 检索性能要求:专用向量数据库的检索性能通常优于 pgvector
- 混合检索需求:如果需要同时按向量 + 元数据过滤,pgvector 的 SQL 能力更强
4. 对话数据的存储
对话数据是 AI 项目特有的高写入场景。每条用户消息都对应一条或多条消息记录。
4.1 直接存关系型数据库
最简单的方案。对话和消息存在两张表,通过外键关联:
// Prisma schema
model Conversation {
id String @id @default(uuid())
userId String
title String
model String
messages Message[]
createdAt DateTime @default(now())
}
model Message {
id String @id @default(uuid())
conversationId String
conversation Conversation @relation(fields: [conversationId], references: [id])
role String // user, assistant, system
content String
tokens Int
createdAt DateTime @default(now())
@@index([conversationId, createdAt])
}适合小规模项目(日活几千用户)。数据量大时,消息表会成为热点。
4.2 消息独立存储
消息量大时,可以把消息表迁移到独立的数据库或文档数据库:
- MongoDB:文档模型适合对话结构,每条对话是一个文档,消息是数组
- ClickHouse:列式存储,适合日志型数据的高写入
- DynamoDB:按会话 ID 分区,适合高并发写入
4.3 对话数据的生命周期
对话数据通常有保留策略——超过一定时间后归档或删除。需要考虑:
- 数据保留期限:30 天?90 天?永久?
- 归档策略:过期数据是删除、还是移到冷存储?
- 用户隐私:用户删除对话时,数据是否真正删除?
5. Prompt 日志的存储
Prompt 日志记录每次 API 调用的输入和输出,用于:
- 成本核算:统计 token 用量
- 质量分析:分析 prompt 效果
- 问题排查:复现用户遇到的问题
Prompt 日志的特点:
- 写入量大(每次 API 调用一条或多条)
- 读取频率低(主要是管理员排查问题时读取)
- 数据体积大(prompt + completion 可能有几千 token)
5.1 存关系型数据库
简单直接,但大字段(text 类型的 prompt 和 completion)会影响查询性能。
model PromptLog {
id String @id @default(uuid())
userId String
model String
prompt String // 大字段
completion String // 大字段
promptTokens Int
completionTokens Int
latencyMs Int
createdAt DateTime @default(now())
@@index([userId, createdAt])
@@index([model, createdAt])
}5.2 存日志系统
数据量大时,可以把 prompt 日志写到专门的日志系统:
- Loki + Grafana:轻量日志系统,配合 Grafana 查询
- ClickHouse:列式存储,适合大量结构化日志
- S3 + 日志采集:直接写到对象存储,按日期分区
5.3 采样策略
如果日志量太大,可以只记录部分请求:
- 按用户 ID 采样(例如只记录 10% 的用户)
- 按模型采样(某些模型调用量大,只记录部分)
- 按状态码采样(只记录错误或慢请求)
6. 文件与对象存储
AI 项目里用户上传的文件(PDF、图片、音频)通常存对象存储:
- AWS S3:标准选择,生态最完善
- Cloudflare R2:与 Workers 集成好,无出站流量费
- 阿里云 OSS:国内项目常用
- MinIO:可自托管,兼容 S3 API
数据库只存元数据,实际文件通过 storageKey 访问:
// 文件元数据
type FileMetadata = {
id: string
userId: string
name: string // 原始文件名
size: number // 字节数
mimeType: string // 例如 application/pdf
storageKey: string // S3 key,例如 "uploads/2026/06/abc123.pdf"
createdAt: Date
}文件访问通过预签名 URL 或直接流式转发,不要把文件内容读到内存再返回。
7. 存储方案的组合
一个典型的 AI 项目可能同时使用多种存储:
┌─────────────────────────────────────────────────┐
│ Hono App │
├─────────────────────────────────────────────────┤
│ PostgreSQL │ pgvector │
│ - 用户、API Key │ - 文档 embedding │
│ - 对话、消息 │ - 相似度检索 │
│ - 配额、订阅 │ │
├─────────────────────────────────────────────────┤
│ 对象存储 (S3/R2) │ 日志系统 (Loki) │
│ - 用户上传文件 │ - prompt 日志 │
│ - 生成的图片/音频 │ - API 调用日志 │
└─────────────────────────────────────────────────┘
初期可以用 PostgreSQL 统一存储(包括 pgvector 处理向量),日志先存数据库。随着数据量增长,再把日志迁移到专门的日志系统,文件迁移到对象存储。
不要一开始就引入过多存储组件。每多一个存储,运维复杂度就增加一层。先跑通业务,再按需拆分。
8. 数据类型的访问模式
不同数据有不同的访问模式,影响存储选型和索引设计:
8.1 点查
按主键或唯一键查询单条记录。例如:
- 按 ID 查用户
- 按 API Key 查租户
关系型数据库的主键索引天然支持点查。
8.2 范围扫描
按某个范围查询多条记录。例如:
- 查询某用户的最近 10 条对话
- 查询某天的 prompt 日志
需要建索引,按查询条件排序。
8.3 相似度检索
按向量相似度查询最近的 N 条记录。例如:
- 查询与用户问题最相似的 5 个文档片段
需要向量索引(IVFFlat、HNSW 等)。
8.4 聚合统计
统计 token 用量、调用次数、错误率等。例如:
- 某用户本月的 token 总用量
- 某模型今天的调用次数
聚合查询通常慢,可以用物化视图或定时聚合任务预计算。
9. 数据类型与 TypeScript 类型
AI 项目的数据通常需要在 TypeScript 类型和数据库 schema 之间同步。两种常见做法:
9.1 从数据库生成类型
Prisma 和 Drizzle 都支持从 schema 生成 TypeScript 类型:
// Prisma: 从 schema.prisma 生成
import { PrismaClient, User, Message } from '@prisma/client'
const user: User = { id: '1', email: '[email protected]', name: 'Alice' }9.2 从 TypeScript 生成 schema
Drizzle 支持从 TypeScript 类型定义 schema:
import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow(),
})
// 自动推导类型
type User = typeof users.$inferSelect两种方式各有优劣。从数据库生成类型,schema 是单一事实来源;从 TypeScript 生成 schema,类型和 schema 可以一起 review。
总结
AI 项目的数据类型比传统 CRUD 更复杂,涉及结构化数据、向量数据、对话数据、文件数据和日志数据。
这一节涉及到的几个分类:
- 业务数据(用户、API Key、订阅):关系型数据库
- 对话数据(会话、消息):关系型数据库,大规模时考虑独立存储
- 向量数据(embedding):向量数据库或 pgvector 扩展
- 文件数据(文档、图片):对象存储,数据库只存元数据
- 日志数据(prompt 日志、API 调用):日志系统或关系型数据库
选型原则:
- 先分类,再选型:不同数据用不同存储,不要试图用一个数据库解决所有问题
- 不要过早引入复杂存储:初期 PostgreSQL + pgvector 可以覆盖大部分场景
- 按访问模式设计索引:点查、范围扫描、相似度检索、聚合统计各有不同的索引策略
- TypeScript 类型与数据库 schema 同步:选择一种生成方式,保持一致性
下一篇看关系型数据库选型——PostgreSQL、MySQL、SQLite 的差异和适用场景。