Files
note/课题一/AI 视觉对话助手/项目实现/技术选型.md
2026-06-12 15:55:55 +08:00

352 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [技术选型, 数据库, PostgreSQL, 持久化, 前端, 边缘推理, 架构设计]
create time: 2026-06-12 15:33
---
# 技术选型
## 概述
本文档是 [[项目架构与技术栈]] 的补充阅读——记录项目中各项技术的**选型过程、替代方案对比和决策理由**。技术选型没有"绝对正确",只有"更适合",所以每个选型都会列出候选方案和取舍逻辑,方便后续回顾和复盘。
```mermaid
graph TD
A["技术选型"] --> B["持久化层"]
A --> C["前端边缘处理层"]
B --> B1["数据库选型: PostgreSQL"]
C --> C1["边缘推理: ONNX Runtime Web"]
C --> C2["语音检测: @ricky0123/vad-web"]
C --> C3["媒体采集: MediaDevices API"]
```
> [!info] 定位
> 持久化部分是**拓展选型文档**,不阻塞 MVP 开发。MVP 阶段用 Redis 做会话存储即可;当产品需要"历史可查、成本可算"时,再引入持久化方案。前端边缘处理部分则是 MVP 阶段就需要确定的技术栈。
## 正文
### 项目的数据特征
选数据库之前,先搞清楚我们的数据长什么样:
| 数据 | 结构特征 | 读写模式 | 数据量级 |
|------|---------|---------|---------|
| 对话消息 | 强结构化(角色/内容/时间/关联图像) | 写多读少,按会话聚合读取 | 中(每用户日均 ~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.contentJSONB中搜索包含"花"的用户消息
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 实现简单的异步写入队列。
---
## 前端边缘处理层选型
> [!info] 选型背景
> 项目的核心交互流程是"用户说话 → AI 看 → AI 回答"。前端需要完成**媒体采集、语音检测、轻量推理**三件事,然后才把"值得处理的数据"发给后端。这三个环节的技术选型直接影响**交互延迟和首屏加载速度**。
### 总览
| 能力 | 当前选型 | 选择理由 |
|------|---------|---------|
| 边缘推理 | **ONNX Runtime Web** | 通用推理引擎模型无关WASM 加速 |
| 语音检测 | **@ricky0123/vad-web** | 包装原生 WebRTC VAD零延迟体积极小 |
| 媒体采集 | **MediaDevices API** | 浏览器原生接口,无中间层,零依赖 |
### 边缘推理ONNX Runtime Web
在浏览器端跑 VAD 和关键帧检测,需要一个轻量推理引擎。候选方案如下:
```mermaid
graph TD
A["浏览器端推理需求"] --> B["ONNX Runtime Web"]
A --> C["TensorFlow.js"]
A --> D["MediaPipe"]
A --> E["Transformers.js"]
B --> B1["通用推理引擎"]
C --> C1["TF 生态专用"]
D --> D1["开箱即用 CV 任务"]
E --> E1["HuggingFace 生态"]
```
| 方案 | 特点 | 适用场景 |
|------|------|---------|
| **ONNX Runtime Web** | 通用推理引擎,支持任意 ONNX 模型WASM 加速 | 需要在浏览器跑**自定义模型**VAD、关键帧检测 |
| **TensorFlow.js** | Google 生态,支持 WebGL/WebGPU 加速 | 模型本身就是 TF 格式,或需要 GPU 加速 |
| **MediaPipe** | Google 出品,封装了姿态/手势/人脸等开箱即用方案 | 只需要常见 CV 任务(人脸检测、姿态估计),不需要自定义模型 |
| **Transformers.js** | Hugging Face 生态,直接跑 HuggingFace 上的模型 | 想快速集成 NLP/CV 预训练模型(如 Whisper、CLIP |
> [!question] 思考
> 为什么 ONNX Runtime Web 胜出?项目需要同时跑**两种**轻量模型——VAD 和关键帧检测,这是自定义 pipeline不是单一 CV 任务。ONNX 是跨框架的通用格式,可以在 Python 端用 PyTorch 训练,导出 ONNX然后在浏览器用统一引擎加载。TensorFlow.js 被锁死在 TF 生态MediaPipe 虽然开箱即用但灵活性不够(不能自定义模型逻辑)。**ONNX Runtime 的核心优势是"模型无关"**。
### 语音检测:@ricky0123/vad-web
VADVoice Activity Detection是交互流程的**起始触发器**——用户有没有在说话?触发必须又快又准。
| 方案 | 特点 | 适用场景 |
|------|------|---------|
| **@ricky0123/vad-web** | 基于 WebRTC VAD2KB纯前端零延迟 | 只需要"有没有人说话"的二分类判断 |
| **Web Audio API + 能量检测** | 用 AnalyserNode 计算音量 RMS阈值判断 | 极简场景,但抗噪能力差 |
| **ONNX 跑 Silero VAD** | 神经网络级 VAD准确率高但推理开销更大 | 嘈杂环境下需要更精准的检测 |
| **Picovoice Porcupine** | 商业级唤醒词引擎,支持自定义唤醒词 | 需要"嘿 Siri"式的唤醒词功能 |
```mermaid
graph LR
A["VAD 方案对比"] --> B["轻量级"]
A --> C["重量级"]
B --> B1["能量检测: 最轻, 不抗噪"]
B --> B2["vad-web: 轻量, WebRTC 原生算法"]
C --> C1["Silero VAD: 精准, 需加载 ONNX 模型"]
C --> C2["Porcupine: 商业级, 需付费"]
```
> [!question] 思考
> 用手写能量检测虽然更轻,但不抗噪(咳嗽、环境噪音都会误触发);用 Silero VAD 虽然更准,但需要加载 ONNX 模型,增加首屏时间和内存占用。**@ricky0123/vad-web 是"够用且最轻"的平衡点**——直接包装浏览器原生的 WebRTC VAD 算法C 代码编译为 WASM延迟接近零。
### 媒体采集MediaDevices API
摄像头和麦克风的采集是整个流程的源头。
| 方案 | 特点 | 适用场景 |
|------|------|---------|
| **MediaDevices API** | 浏览器原生 API零依赖直接拿 MediaStream | 标准的摄像头/麦克风采集 |
| **react-webcam 等封装库** | React 组件封装,减少胶水代码 | 快速原型,但灵活性受限 |
| **WebRTC含 getUserMedia** | 完整的点对点通信栈 | 需要浏览器之间直接传音视频(如视频会议) |
| **Capacitor/Cordova 原生桥** | 混合 App 方案,调用原生摄像头 | 目标不是浏览器而是移动 App |
> [!question] 思考
> `navigator.mediaDevices.getUserMedia()` 是所有浏览器音视频采集的**唯一标准入口**。所有上层封装库底层都是调这个 API。项目需要的是原始 MediaStream直接送进 VAD 和关键帧检测),不是封装好的组件。用封装库反而要多一层解包,**没有中间商**。
### 选型共同逻辑
这三个技术选择有一个共同的决策模式——**选择最薄的抽象层**
| 技术 | "最薄"体现在哪里 |
|------|----------------|
| **ONNX Runtime Web** | 不绑定特定框架,模型格式通用 |
| **@ricky0123/vad-web** | 包装原生 WebRTC VAD没有多余的模型加载 |
| **MediaDevices API** | 直接用浏览器原生接口,不加封装层 |
这与 [[项目架构与技术栈]] 中"前端做轻量预处理"的原则一致:前端层只需要采集和判断"有没有值得发给后端的数据"不需要复杂的模型推理能力。更重的方案TensorFlow.js、Silero VAD在后端 Go 网关和云端 AI 服务面前,属于在不该重的地方加重。
## 关联笔记
- [[项目架构与技术栈]]
- [[成本控制]]
- [[项目架构与技术栈/技术名词解释]]