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

16 KiB
Raw Blame History

tags, create time
tags create time
技术选型
数据库
PostgreSQL
持久化
前端
边缘推理
架构设计
2026-06-12 15:33

技术选型

概述

本文档是 项目架构与技术栈 的补充阅读——记录项目中各项技术的选型过程、替代方案对比和决策理由。技术选型没有"绝对正确",只有"更适合",所以每个选型都会列出候选方案和取舍逻辑,方便后续回顾和复盘。

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 有良好支持。

候选方案全景对比

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 —— 太轻了

graph LR
    A["SQLite"] --> B["单文件数据库"]
    B --> C{"适合本项目?"}
    C -->|"否"| D["写并发受限"]
    C -->|"否"| E["无法多实例共享"]
  • 优点:零配置,一个 .db 文件搞定,开发阶段极方便
  • 致命问题写锁是全局的——同一时刻只能有一个写操作。当 WebSocket 并发写入对话消息时,会频繁锁等待
  • 另一个问题:多个 Go Gateway 实例无法共享同一个 SQLite 文件(除非用 NFS但性能极差

[!tip] 什么时候选 SQLite 如果是单机部署的桌面应用或 CLI 工具SQLite 是最佳选择。但我们的项目是 Web 服务、多实例部署,不合适。

MySQL —— 能用,但 JSON 处理弱

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 —— 文档型,关联查询弱

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 在本项目的核心需求上全面契合

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

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 查询示例——在对话上下文中搜索包含特定关键词的消息:

-- 在 messages.contentJSONB中搜索包含"花"的用户消息
SELECT id, content, created_at
FROM messages
WHERE role = 'user'
  AND content @> '{"text": "花"}'
ORDER BY created_at DESC
LIMIT 20;

选型决策流程图

遇到新项目时,可以按这个流程快速决策:

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 后,存储层变为冷热分离架构:

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 和关键帧检测,需要一个轻量推理引擎。候选方案如下:

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"式的唤醒词功能
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 服务面前,属于在不该重的地方加重。

关联笔记