diff --git a/课题一/AI 视觉对话助手.md b/课题一/AI 视觉对话助手.md index 50d9e12..f70e207 100644 --- a/课题一/AI 视觉对话助手.md +++ b/课题一/AI 视觉对话助手.md @@ -39,6 +39,7 @@ create time: 2026-06-12 11:13 ## 关联笔记 - [[项目架构与技术栈]] +- [[持久化技术选型]] - [[视觉理解]] - [[语音交互]] - [[成本控制]] diff --git a/课题一/AI 视觉对话助手/持久化技术选型.md b/课题一/AI 视觉对话助手/持久化技术选型.md new file mode 100644 index 0000000..21554b1 --- /dev/null +++ b/课题一/AI 视觉对话助手/持久化技术选型.md @@ -0,0 +1,250 @@ +--- +tags: [数据库, PostgreSQL, 持久化, 架构设计, 拓展功能] +create time: 2026-06-12 15:33 +--- + +# 持久化技术选型 + +## 概述 + +本文档是 [[项目架构与技术栈]] 的拓展阅读——在核心功能(实时视觉对话)跑通之后,如果需要**保存对话历史、统计用量成本、管理用户偏好**,就需要引入持久化层。本文对比主流数据库方案,解释为什么推荐 PostgreSQL,以及在什么情况下应该选择其他方案。 + +> [!info] 定位 +> 这是一篇**拓展选型文档**,不阻塞 MVP 开发。MVP 阶段用 Redis 做会话存储即可;当产品需要"历史可查、成本可算"时,再引入本节方案。 + +## 正文 + +### 项目的数据特征 + +选数据库之前,先搞清楚我们的数据长什么样: + +| 数据 | 结构特征 | 读写模式 | 数据量级 | +|------|---------|---------|---------| +| 对话消息 | 强结构化(角色/内容/时间/关联图像) | 写多读少,按会话聚合读取 | 中(每用户日均 ~100 条) | +| 会话元信息 | 强结构化(用户/时间/状态) | 写少读少 | 低 | +| 对话上下文 | 半结构化(JSON 数组,含图像引用) | 高频读写,TTL 过期 | 低(仅当前窗口) | +| 用量统计 | 强结构化(数字/日期/聚合) | 写多,定期聚合读 | 低(日粒度汇总后很小) | +| 用户偏好 | 强结构化(KV 配置) | 写极少读少 | 极低 | +| 关键帧图像 | 非结构化二进制 | 写少,按需读 | 大(单张 100KB~1MB) | + +> [!question] 思考 +> 从上表可以看出,核心数据(对话、会话、统计)都是**强结构化**的,有明确的字段和关联关系。这意味着关系型数据库天然适配。但"对话上下文"是半结构化 JSON,这就需要数据库对 JSON 有良好支持。 + +### 候选方案全景对比 + +```mermaid +graph TD + A["持久化技术选型"] --> B["关系型数据库"] + A --> C["文档型数据库"] + A --> D["嵌入式数据库"] + B --> B1["PostgreSQL"] + B --> B2["MySQL"] + B --> B3["TiDB"] + C --> C1["MongoDB"] + D --> D1["SQLite"] +``` + +| 维度 | PostgreSQL | MySQL | SQLite | MongoDB | TiDB | +|------|-----------|-------|--------|---------|------| +| **数据模型** | 关系型 + JSONB | 关系型 | 关系型(嵌入式) | 文档型(BSON) | 关系型(分布式) | +| **JSON 支持** | JSONB 原生索引 | JSON 类型,索引弱 | 无原生支持 | 天生擅长 | 兼容 MySQL JSON | +| **关联查询** | 强 | 强 | 强 | 弱(需嵌套/lookup) | 强 | +| **聚合统计** | 窗口函数/CTE 完善 | 基础聚合 | 基础聚合 | 聚合管道 | 强 | +| **并发能力** | 高(MVCC) | 中 | 低(单写锁) | 高 | 极高(分布式) | +| **运维复杂度** | 中 | 低 | 极低 | 中 | 高 | +| **Go 生态** | pgx / GORM | go-sql-driver | go-sqlite3 | mongo-go-driver | 兼容 MySQL 驱动 | +| **部署方式** | Docker / 云服务 | Docker / 云服务 | 单文件嵌入 | Docker / Atlas | 集群部署 | +| **成本** | 开源免费 | 开源免费 | 开源免费 | 社区版免费 | 开源免费 | + +### 逐个分析:为什么不选它们? + +#### SQLite —— 太轻了 + +```mermaid +graph LR + A["SQLite"] --> B["单文件数据库"] + B --> C{"适合本项目?"} + C -->|"否"| D["写并发受限"] + C -->|"否"| E["无法多实例共享"] +``` + +- **优点**:零配置,一个 `.db` 文件搞定,开发阶段极方便 +- **致命问题**:**写锁是全局的**——同一时刻只能有一个写操作。当 WebSocket 并发写入对话消息时,会频繁锁等待 +- **另一个问题**:多个 Go Gateway 实例无法共享同一个 SQLite 文件(除非用 NFS,但性能极差) + +> [!tip] 什么时候选 SQLite? +> 如果是**单机部署的桌面应用或 CLI 工具**,SQLite 是最佳选择。但我们的项目是 Web 服务、多实例部署,不合适。 + +#### MySQL —— 能用,但 JSON 处理弱 + +```mermaid +graph LR + A["MySQL"] --> B["JSON 支持"] + B --> C{"够用吗?"} + C -->|"勉强"| D["JSON 索引弱"] + C -->|"否"| E["无 JSONB 二进制存储"] +``` + +- **优点**:运维简单,社区庞大,很多团队更熟悉 +- **本项目的痛点**:对话上下文是 JSON 数组(含图像引用、角色标记),MySQL 的 JSON 类型支持**索引能力弱**,无法对 JSON 内部字段高效查询 +- **另一个痛点**:缺少 `gen_random_uuid()` 等原生函数,需要应用层生成 UUID + +> [!question] 思考 +> 如果团队只熟悉 MySQL,能不能用?**完全可以**。JSON 上的差异在 MVP 阶段几乎无感,只是后续做复杂查询(如"找出所有包含某关键词的对话")时会比 PostgreSQL 麻烦一些。 + +#### MongoDB —— 文档型,关联查询弱 + +```mermaid +graph LR + A["MongoDB"] --> B["天然 JSON 存储"] + B --> C{"适合本项目?"} + C -->|"否"| D["关联查询弱"] + C -->|"否"| E["聚合统计不如 SQL 直观"] +``` + +- **优点**:Schema-less,存 JSON 天然舒适,水平扩展能力强 +- **本项目的痛点**: + - `messages` 需要按 `session_id` 关联 `sessions`,再按 `user_id` 聚合——这在 MongoDB 中要用 `$lookup`,写法复杂且性能不如 SQL JOIN + - 用量统计的"按天聚合"用 SQL 的 `GROUP BY + SUM` 一句话搞定,MongoDB 的聚合管道代码量多 3-5 倍 + +> [!info] 什么时候选 MongoDB? +> 如果数据模型是**高度嵌套的文档**(如博客系统、CMS),且很少跨文档关联查询,MongoDB 是好选择。我们的数据关联性强,不太适合。 + +#### TiDB —— 杀鸡用牛刀 + +- **优点**:兼容 MySQL 协议,分布式架构,水平扩展无上限 +- **本项目的痛点**:部署复杂(至少 3 个 PD + 3 个 TiKV + 2 个 TiDB),运维成本高 +- **结论**:单机 PostgreSQL 完全够用,引入 TiDB 纯属过度设计 + +> [!tip] 什么时候选 TiDB? +> 数据量过亿、需要跨地域部署、单机 PostgreSQL 已经扛不住时。对于本项目,短期内不会遇到这个瓶颈。 + +### 为什么选 PostgreSQL? + +综合以上分析,PostgreSQL 在本项目的核心需求上**全面契合**: + +```mermaid +graph TD + A["项目需求"] --> B["强结构化数据"] + A --> C["JSON 半结构化"] + A --> D["关联查询"] + A --> E["聚合统计"] + A --> F["Go 生态"] + + B --> PG["PostgreSQL"] + C --> PG + D --> PG + E --> PG + F --> PG +``` + +逐条对应: + +| 项目需求 | PostgreSQL 的匹配点 | +|---------|-------------------| +| 对话历史是强结构化数据 | 原生关系型,SQL 标准完备 | +| 对话上下文含 JSON(图像引用/角色标记) | **JSONB** 类型支持索引、路径查询、部分更新 | +| messages ↔ sessions 外键关联 | 完整的 FK 约束 + JOIN 支持 | +| 用量按天/周/月聚合 | 窗口函数、CTE、`DATE_TRUNC` 等分析能力 | +| Go 后端对接 | `pgx` 驱动性能优秀,GORM/Ent ORM 支持成熟 | +| 后续可能存图像元信息 | 可结合对象存储,PG 只存引用路径 | +| 未来可能加全文搜索 | 内置 `tsvector` 全文检索,无需额外引入 Elasticsearch | + +### Go 后端集成示例 + +使用 `pgx` 驱动连接 PostgreSQL: + +```go +import "github.com/jackc/pgx/v5/pgxpool" + +// 初始化连接池 +pool, _ := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/vision_ai") + +// 保存一条对话消息 +func SaveMessage(ctx context.Context, pool *pgxpool.Pool, msg *Message) error { + _, err := pool.Exec(ctx, + `INSERT INTO messages (session_id, role, content, image_url, tokens_used) + VALUES ($1, $2, $3, $4, $5)`, + msg.SessionID, msg.Role, msg.Content, msg.ImageURL, msg.TokensUsed, + ) + return err +} + +// 查询用户近 7 天用量汇总 +func GetWeeklyUsage(ctx context.Context, pool *pgxpool.Pool, userID string) ([]UsageRow, error) { + rows, _ := pool.Query(ctx, + `SELECT date, llm_tokens, estimated_cost + FROM usage_daily + WHERE user_id = $1 AND date >= CURRENT_DATE - INTERVAL '7 days' + ORDER BY date`, userID) + defer rows.Close() + // ... scan rows +} +``` + +JSONB 查询示例——在对话上下文中搜索包含特定关键词的消息: + +```sql +-- 在 messages.content(JSONB)中搜索包含"花"的用户消息 +SELECT id, content, created_at +FROM messages +WHERE role = 'user' + AND content @> '{"text": "花"}' +ORDER BY created_at DESC +LIMIT 20; +``` + +### 选型决策流程图 + +遇到新项目时,可以按这个流程快速决策: + +```mermaid +flowchart TD + START["需要持久化?"] -->|"否"| REDIS["继续用 Redis"] + START -->|"是"| STRUCT{"数据强结构化?"} + STRUCT -->|"是"| SCALE{"数据量级?"} + STRUCT -->|"否, 高度嵌套"| MONGO["考虑 MongoDB"] + SCALE -->|"< 100GB, 单机可扛"| PG["PostgreSQL"] + SCALE -->|"海量, 需水平扩展"| TIDB["考虑 TiDB / CockroachDB"] + SCALE -->|"极小, 单文件即可"| SQLITE["考虑 SQLite"] + PG --> JSON{"有 JSON 需求?"} + JSON -->|"是"| PG_OK["PostgreSQL (JSONB)"] + JSON -->|"否"| MYSQL{"团队熟悉 MySQL?"} + MYSQL -->|"是"| MYSQL_OK["MySQL 也行"] + MYSQL -->|"否"| PG_OK +``` + +> [!question] 思考 +> 技术选型没有"绝对正确",只有"更适合"。PostgreSQL 在本项目中胜出,核心原因是**数据模型匹配 + JSONB 能力 + Go 生态成熟**这三点的交集。如果换一个纯 KV 场景(如缓存),Redis 才是正确答案。 + +### 与现有架构的整合 + +引入 PostgreSQL 后,存储层变为**冷热分离**架构: + +```mermaid +graph LR + subgraph Hot["热数据层"] + REDIS["Redis"] + end + subgraph Cold["冷数据层"] + PG["PostgreSQL"] + end + subgraph App["Go Gateway"] + WRITE["写入路径"] + READ["读取路径"] + end + + WRITE -->|"实时会话状态"| REDIS + WRITE -->|"对话历史 + 用量"| PG + READ -->|"当前上下文(快)"| REDIS + READ -->|"历史记录(慢)"| PG +``` + +> [!info] 写入策略 +> 建议采用**异步写入**——实时对话消息先写 Redis(快),然后异步批量刷入 PostgreSQL(慢)。这样不会因为数据库写入延迟影响对话体验。可以用 Go channel + goroutine 实现简单的异步写入队列。 + +## 关联笔记 + +- [[项目架构与技术栈]] +- [[成本控制]] +- [[项目架构与技术栈/技术名词解释]] diff --git a/课题一/AI 视觉对话助手/项目架构与技术栈.md b/课题一/AI 视觉对话助手/项目架构与技术栈.md index 682c1a8..a5680d6 100644 --- a/课题一/AI 视觉对话助手/项目架构与技术栈.md +++ b/课题一/AI 视觉对话助手/项目架构与技术栈.md @@ -79,6 +79,7 @@ graph TB | 语言 | **Go** | 高并发 goroutine 模型,适合长连接管理 | | WebSocket | **gorilla/websocket** | Go 生态最成熟的 WebSocket 库 | | 会话存储 | **Redis** | 高速 KV 存储,适合会话状态和上下文缓存 | +| 持久化存储 | **PostgreSQL** | 对话历史、用量统计、用户偏好(MVP 阶段可选) | | 配置管理 | **Viper** | 支持多格式配置,环境变量覆盖 | | 日志 | **Zap** | 高性能结构化日志 | @@ -279,6 +280,109 @@ graph LR > [!tip] WebSocket 与负载均衡 > WebSocket 是长连接,Nginx 需要配置 `proxy_set_header Upgrade` 和 `ip_hash` 或 sticky session,确保同一用户的请求始终路由到同一个 Gateway 实例。 +### 存储与持久化策略 + +当前架构使用 Redis 做会话存储,但 Redis 是**内存数据库**,默认不做持久化——服务重启数据即丢。是否需要持久化,取决于业务阶段: + +#### 分阶段策略 + +```mermaid +graph LR + A["MVP 阶段"] -->|"Redis 内存存储"| B["快速验证"] + C["上线阶段"] -->|"Redis + PostgreSQL"| D["持久化对话与用量"] + E["规模化阶段"] -->|"Redis + PG + 对象存储"| F["完整数据体系"] +``` + +| 阶段 | 存储方案 | 持久化内容 | 理由 | +|------|---------|-----------|------| +| **MVP** | Redis only | 无 | 快速验证核心功能,重启丢数据可接受 | +| **上线** | Redis + **PostgreSQL** | 对话历史、用户偏好、用量统计 | 用户需要查看历史,运营需要成本数据 | +| **规模化** | Redis + PG + **对象存储** | 图像帧、音频片段归档 | 大文件不适合存关系库 | + +#### 需要持久化的数据 + +| 数据类型 | 写入频率 | 查询模式 | 推荐存储 | +|---------|---------|---------|---------| +| 对话历史(文本) | 每轮对话 | 按用户+时间范围查询 | PostgreSQL | +| 用量统计(tokens/成本) | 每次 API 调用 | 聚合统计(日/周/月) | PostgreSQL | +| 用户偏好(语言/声音) | 低频 | 按 user_id 查询 | PostgreSQL | +| 实时会话状态 | 高频读写 | 按 session_id 查询 | Redis(不变) | +| 关键帧图像 | 按需 | 按对话 ID 关联 | 对象存储(S3/MinIO) | + +#### PostgreSQL 表设计要点 + +```sql +-- 对话会话表 +CREATE TABLE sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- 对话消息表 +CREATE TABLE messages ( + id BIGSERIAL PRIMARY KEY, + session_id UUID REFERENCES sessions(id), + role VARCHAR(16) NOT NULL, -- "user" | "assistant" + content TEXT NOT NULL, + image_url TEXT, -- 关联的关键帧(可选) + tokens_used INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT now() +); + +-- 用量统计表(按天聚合,方便成本分析) +CREATE TABLE usage_daily ( + user_id UUID NOT NULL, + date DATE NOT NULL, + llm_tokens BIGINT DEFAULT 0, + stt_seconds REAL DEFAULT 0, + tts_chars INTEGER DEFAULT 0, + estimated_cost NUMERIC(10,4) DEFAULT 0, + PRIMARY KEY (user_id, date) +); +``` + +> [!info] 为什么选 PostgreSQL 而不是 MySQL? +> PostgreSQL 对 JSON 类型支持更好(对话上下文可直接存 JSONB),且有 `gen_random_uuid()` 等原生函数,更适合这类 AI 应用场景。当然,如果团队更熟悉 MySQL,替换成本也很低。 + +#### 更新后的存储层架构 + +```mermaid +graph TD + subgraph App["Go Gateway"] + SM["Session Manager"] + HM["History Manager"] + UM["Usage Monitor"] + end + + subgraph Cache["热数据 - Redis"] + SESSION["会话状态"] + CTX["对话上下文窗口"] + end + + subgraph DB["冷数据 - PostgreSQL"] + HISTORY["对话历史"] + USAGE["用量统计"] + PREFS["用户偏好"] + end + + subgraph OSS["大文件 - 对象存储"] + IMG["关键帧图像"] + AUDIO["音频片段"] + end + + SM --> SESSION + SM --> CTX + HM --> HISTORY + HM --> IMG + UM --> USAGE + SM --> PREFS +``` + +> [!question] 思考 +> Redis 存"热数据"(当前对话上下文),PostgreSQL 存"冷数据"(历史记录)——这就是经典的**冷热分离**策略。实时对话走 Redis 微秒级读写,历史查询走 PostgreSQL,互不干扰。 + ## 关联笔记 - [[视觉理解]] @@ -286,3 +390,4 @@ graph LR - [[成本控制]] - [[用户故事]] - [[项目架构与技术栈/技术名词解释]] +- [[持久化技术选型]]