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:轻量,适合开发和小项目

选型考量:

  1. 团队熟悉度:团队熟悉哪个就用哪个
  2. 云服务商:某些云平台的托管服务有偏好
  3. Edge 环境:Cloudflare Workers 只能用 D1(SQLite)或远程连接
  4. 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 选择向量存储的考量

  1. 数据规模:少于 100 万向量,pgvector 够用;超过 100 万,考虑专用向量数据库
  2. 团队运维能力:专用向量数据库需要额外运维,pgvector 复用已有的 PostgreSQL 运维能力
  3. 检索性能要求:专用向量数据库的检索性能通常优于 pgvector
  4. 混合检索需求:如果需要同时按向量 + 元数据过滤,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 对话数据的生命周期

对话数据通常有保留策略——超过一定时间后归档或删除。需要考虑:

  1. 数据保留期限:30 天?90 天?永久?
  2. 归档策略:过期数据是删除、还是移到冷存储?
  3. 用户隐私:用户删除对话时,数据是否真正删除?

5. Prompt 日志的存储

Prompt 日志记录每次 API 调用的输入和输出,用于:

  1. 成本核算:统计 token 用量
  2. 质量分析:分析 prompt 效果
  3. 问题排查:复现用户遇到的问题

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 更复杂,涉及结构化数据、向量数据、对话数据、文件数据和日志数据。

这一节涉及到的几个分类:

  1. 业务数据(用户、API Key、订阅):关系型数据库
  2. 对话数据(会话、消息):关系型数据库,大规模时考虑独立存储
  3. 向量数据(embedding):向量数据库或 pgvector 扩展
  4. 文件数据(文档、图片):对象存储,数据库只存元数据
  5. 日志数据(prompt 日志、API 调用):日志系统或关系型数据库

选型原则:

  1. 先分类,再选型:不同数据用不同存储,不要试图用一个数据库解决所有问题
  2. 不要过早引入复杂存储:初期 PostgreSQL + pgvector 可以覆盖大部分场景
  3. 按访问模式设计索引:点查、范围扫描、相似度检索、聚合统计各有不同的索引策略
  4. TypeScript 类型与数据库 schema 同步:选择一种生成方式,保持一致性

下一篇看关系型数据库选型——PostgreSQL、MySQL、SQLite 的差异和适用场景。