# 技术选型

## 概述

本文档记录项目中各项技术的**选型过程、替代方案对比和决策理由**。技术选型没有"绝对正确"，只有"更适合"。

各技术选型章节包含关键术语解释，帮助快速理解技术概念。

### 后端核心技术栈

| 名词 | 解释 |
|------|------|
| **Go (Golang)** | 高并发后端语言，Google 开发，杀手锏是 goroutine——极轻量协程，一个程序可轻松开几万个，每个只占几 KB 内存，适合管理大量 WebSocket 长连接 |
| **gorilla/websocket** | Go WebSocket 库，Go 标准库无内置 WebSocket 支持，此库是社区最成熟的选择，处理了协议握手、帧解析等底层细节 |
| **Viper** | Go 配置管理库，读取 JSON/YAML/TOML 配置，支持环境变量覆盖，方便开发/测试/生产环境用不同配置 |
| **Zap** | Go 结构化日志库，Uber 开源，输出 JSON 格式日志，方便工具搜索分析，性能远超标准库 log |

```
技术选型
├── AI 编排框架
│   └── CloudWeGo Eino Graph（声明式 DAG 编排，替代手写 goroutine 管道）
├── AI 服务栈
│   ├── STT: MiMo ASR（默认） / Deepgram
│   ├── LLM: DashScope qwen3-vl-plus（默认） / GPT-4o 等 OpenAI 兼容模型
│   └── TTS: MiMo TTS（默认） / OpenAI TTS
├── 持久化层
│   ├── 数据库: PostgreSQL（pgx/v5，手写 SQL）
│   ├── 迁移: 嵌入式 SQL 文件，自动执行
│   └── 存储模式: 三级存储 TieredManager（L1 Memory → L2 Redis → L3 PostgreSQL）
├── 认证与用户系统
│   ├── 认证方案: JWT (HS256), access 15min + refresh 7day
│   ├── JWT 库: golang-jwt/jwt/v5
│   ├── 密码哈希: bcrypt
│   ├── 数据库驱动: pgx/v5（手写 SQL，不用 ORM）
│   └── 前端 Token 存储: localStorage
└── 前端边缘处理层
    ├── 关键帧检测: Canvas 像素比较（160x120 降采样）
    ├── 语音检测: @ricky0123/vad-web
    └── 媒体采集: MediaDevices API
```

---

## 一、AI 编排框架选型

### 关键术语

| 名词 | 解释 |
|------|------|
| **Eino** | 字节跳动开源的 Go AI 应用开发框架（CloudWeGo Eino），提供 Graph DAG 编排、组件抽象（ChatModel/Tool 等）、流式处理和 Callback AOP 机制 |
| **compose.Graph** | Eino 的 DAG 编排器，声明式有向无环图，节点可以是 Lambda、ChatModel、ToolsNode 等，边定义数据流向 |
| **Lambda** | Graph 中的可组合函数单元，四种模式：InvokableLambda（同步）、StreamableLambda（流式输出）、CollectableLambda（流式输入）、TransformableLambda（双向流式） |
| **StreamReader** | Eino 的流式数据抽象 `schema.StreamReader[T]`，类似 io.Reader 的语义，`Recv()` 读取一帧，`io.EOF` 表示流结束 |
| **Callback** | Eino 的 AOP 机制，类似中间件的钩子，支持节点生命周期回调（OnStart/OnEnd/OnError/OnEndWithStreamOutput） |

> 更多 Eino 相关概念详见 [10-Eino框架与编排设计.md](10-Eino框架与编排设计.md)

### 候选方案对比

| 框架 | 语言 | 特点 | CamTalk 适用性 |
|------|------|------|---------------|
| **CloudWeGo Eino** | Go | Go 原生、类型安全、流式原生、Graph DAG 编排 | ✅ 完美匹配 |
| LangChain Go | Go | 生态丰富但较重，抽象层多 | ❌ 过度抽象 |
| 自研编排 | Go | 完全可控 | ❌ 维护成本高 |

### 选择 Eino 的理由

| 维度 | 手写 goroutine（旧方案） | Eino Graph（新方案） |
|------|------------------------|---------------------|
| 编排方式 | 手动 `go func()` + `sync.WaitGroup` | 声明式 DAG，类型安全 |
| 流式处理 | 自定义 `chan` 传递 | `StreamReader` + `Pipe`，自动转换 |
| 错误处理 | 各节点独立处理，不一致 | Graph 级别统一错误传播 |
| 回调/AOP | 日志散落各处 | `callbacks.Handler` 统一注入 |
| 配置灵活性 | Pipeline 创建时固定 | 每请求 `Option` 动态注入 |
| 可测试性 | 需启动 goroutine | `Graph.Invoke()` 直接测试 |
| 扩展性 | 修改 Pipeline 代码 | 添加节点 + 边，无侵入 |

### 核心依赖

```
github.com/cloudwego/eino v0.9.9                              # 核心框架
github.com/cloudwego/eino-ext/components/model/openai v0.1.13  # OpenAI 兼容 ChatModel
```

**核心理由**：
1. Go 原生，泛型支持，编译时类型检查
2. 原生流式处理（`StreamReader`），适合 LLM token 级推送
3. Graph 支持分支、并行、循环，满足当前和未来需求
4. Callback 机制实现 AOP（日志、指标、消息推送）
5. eino-ext 提供 OpenAI ChatModel 实现，直接对接 DashScope

> 详细的 Eino 框架使用文档见 [11-Eino框架技术文档](11-Eino框架技术文档.md)，重构方案见 [10-Eino重构方案](10-Eino重构方案.md)，实施记录见 [12-Eino重构实施记录](12-Eino重构实施记录.md)。

---

## 二、AI 服务栈选型

### 关键术语

| 名词 | 解释 |
|------|------|
| **多模态 LLM** | 能读文字又能看图片的大语言模型，如 GPT-4o（OpenAI）、Claude Sonnet（Anthropic），给照片+问题能"看懂"照片再回答 |
| **STT** | Speech-to-Text，语音转文字。流式识别延迟可低于 500ms |
| **TTS** | Text-to-Speech，文字转语音。支持流式——边生成边读，不必等全部生成完 |

### STT（语音识别）

| 方案 | 延迟 | 成本 | 特点 |
|------|------|------|------|
| **MiMo ASR**（默认） | ~1s | 按量计费 | 国产替代，兼容 OpenAI chat/completions 格式，HTTP 非流式 |
| **Deepgram** | <500ms | 按分钟计费 | 流式识别，延迟极低，WebSocket 接口 |
| Whisper API | 1-3s | 按分钟计费 | 准确率高，支持多语言 |
| FunASR | <500ms | 自部署免费 | 阿里开源，中文优化 |

当前默认使用 MiMo ASR（mimo-v2.5-asr），可通过 `ai.stt.provider` 配置切换到 Deepgram。

### LLM（多模态大模型）

| 方案 | 成本 | 特点 |
|------|------|------|
| **DashScope qwen3-vl-plus**（默认） | 按量计费 | 阿里云，通过 OpenAI 兼容接口调用，视觉理解能力强 |
| GPT-4o | $2.5/1M tokens | OpenAI，API 成熟，流式推理 |
| Claude Sonnet | $3/1M tokens | Anthropic，长上下文能力强 |

LLM 通过 Eino 框架的 `eino-ext/components/model/openai` ChatModel 组件接入，支持任何 OpenAI 兼容接口。配置 `ai.llm.provider`、`ai.llm.model`、`ai.llm.endpoint` 即可切换。

### TTS（语音合成）

| 方案 | 成本 | 特点 |
|------|------|------|
| **MiMo TTS**（默认） | 按量计费 | 国产替代，通过配置切换，模型 mimo-v2.5-tts |
| OpenAI TTS | $15/1M 字符 | 音质自然，支持流式，默认模型 tts-1，语音 alloy |

当前默认使用 MiMo TTS（mimo-v2.5-tts），可通过 `ai.tts.provider` 配置切换到 OpenAI TTS。

---

## 三、持久化层选型

### 关键术语

| 名词 | 解释 |
|------|------|
| **PostgreSQL** | 关系型数据库，支持 JSONB（JSON 二进制格式，可建索引）、窗口函数、CTE 等高级特性 |
| **Redis** | 内存 KV 数据库，数据放在内存里，读写微秒级。支持 TTL 过期自动清理 |
| **MVCC** | Multi-Version Concurrency Control，多版本并发控制，PostgreSQL 用此实现高并发读写而不阻塞 |

### 数据特征分析

| 数据 | 结构特征 | 读写模式 | 数据量级 |
|------|---------|---------|---------|
| 对话消息 | 强结构化 | 写多读少，按会话聚合读取 | 中（每用户日均 ~100 条） |
| 会话元信息 | 强结构化 | 写少读少 | 低 |
| 对话上下文 | 半结构化 JSON | 高频读写，TTL 过期 | 低（仅当前窗口） |
| 用量统计 | 强结构化 | 写多，定期聚合读 | 低（日粒度汇总后很小） |
| 用户偏好 | 强结构化 KV | 写极少读少 | 极低 |
| 关键帧图像 | 非结构化二进制 | 写少，按需读 | 大（单张 100KB~1MB） |

核心数据（对话、会话、统计）都是**强结构化**的，关系型数据库天然适配。"对话上下文"是半结构化 JSON，需要数据库对 JSON 有良好支持。

### 候选方案对比

| 维度 | 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 驱动 |

### 淘汰理由

**SQLite** — 写锁是全局的，并发写入会频繁锁等待。多个 Go Gateway 实例无法共享同一 SQLite 文件。适合单机桌面应用，不适合 Web 服务。

**MySQL** — JSON 类型索引能力弱，无法对 JSON 内部字段高效查询。缺少 `gen_random_uuid()` 等原生函数。如果团队只熟悉 MySQL，MVP 阶段完全可用，后续复杂查询会比 PostgreSQL 麻烦。

**MongoDB** — `messages` 需按 `session_id` 关联 `sessions`，MongoDB 中要用 `$lookup`，写法复杂且性能不如 SQL JOIN。用量统计的"按天聚合"用 SQL 一句话搞定，MongoDB 聚合管道代码量多 3-5 倍。

**TiDB** — 部署复杂（至少 3 PD + 3 TiKV + 2 TiDB），单机 PostgreSQL 完全够用，过度设计。

### 选择 PostgreSQL 的理由

| 项目需求 | PostgreSQL 匹配点 |
|---------|------------------|
| 强结构化数据 | 原生关系型，SQL 标准完备 |
| JSON 半结构化 | JSONB 支持索引、路径查询、部分更新 |
| messages ↔ sessions 关联 | 完整的 FK 约束 + JOIN |
| 用量按天/周/月聚合 | 窗口函数、CTE、`DATE_TRUNC` |
| Go 后端对接 | pgx 驱动性能优秀，GORM/Ent 支持成熟 |
| 未来全文搜索 | 内置 `tsvector`，无需额外引入 ES |

### Go 集成示例

```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
}

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
SELECT id, content, created_at
FROM messages
WHERE role = 'user'
  AND content @> '{"text": "花"}'
ORDER BY created_at DESC
LIMIT 20;
```

### 冷热分离架构（三级存储）

```
Go Gateway (TieredManager)
  ├── L1: Memory（进程内缓存，微秒级）
  ├── L2: Redis（分布式缓存，毫秒级）
  └── L3: PostgreSQL（持久化存储，冷数据）

读取路径：L1 → L2 → L3，逐级回源，命中后向上回填
写入路径：L1 → L2（同步） → L3（异步）
```

`TieredManager` 自动管理三级存储，后台 goroutine 每 30 秒 ping Redis 健康状态，Redis 故障时自动降级为 L1+L3 模式。

### 决策流程

```
需要持久化?
  ├── 否 → 继续用 Redis
  └── 是 → 数据强结构化?
        ├── 否, 高度嵌套 → 考虑 MongoDB
        └── 是 → 数据量级?
              ├── < 100GB, 单机可扛 → PostgreSQL
              ├── 海量, 需水平扩展 → TiDB / CockroachDB
              └── 极小, 单文件即可 → SQLite
```

---

## 四、前端边缘处理层选型

### 关键术语

| 名词 | 解释 |
|------|------|
| **React 18** | 组件化 UI 框架，Facebook 开源，把页面拆成组件搭积木拼装。18 版本支持并发渲染 |
| **TypeScript** | 带类型的 JavaScript，在 JS 基础上增加类型声明，编译阶段就能发现类型错误 |
| **Vite** | 前端构建工具，利用浏览器原生 ES Module，开发时毫秒级热更新（HMR），构建产物小 |
| **WebSocket** | 浏览器与服务器的双向通道。HTTP 是"一问一答"，WebSocket 像打电话——接通后双方随时互发消息，适合实时对话场景 |
| **ONNX Runtime Web** | 浏览器端 AI 推理引擎，微软定义的通用模型格式 ONNX 的运行引擎，可在浏览器中用 WASM 加速跑轻量模型（如 VAD、关键帧检测），零延迟、不耗服务器资源 |
| **VAD** | Voice Activity Detection，语音活动检测，检测"人有没有在说话"。WebRTC 内置了高效的 VAD 算法 |
| **MediaDevices API** | 浏览器摄像头/麦克风接口，`navigator.mediaDevices.getUserMedia()` 是浏览器音视频采集的唯一标准入口，无需插件 |

### 总览

| 能力 | 当前选型 | 选择理由 |
|------|---------|---------|
| 边缘推理 | ONNX Runtime Web | 通用推理引擎，模型无关，WASM 加速 |
| 语音检测 | @ricky0123/vad-web | 包装原生 WebRTC VAD，零延迟，体积极小 |
| 媒体采集 | MediaDevices API | 浏览器原生接口，无中间层，零依赖 |

### 边缘推理：ONNX Runtime Web

| 方案 | 特点 | 适用场景 |
|------|------|---------|
| **ONNX Runtime Web** | 通用推理引擎，支持任意 ONNX 模型，WASM 加速 | 自定义模型 pipeline |
| TensorFlow.js | Google 生态，WebGL/WebGPU 加速 | 模型本身就是 TF 格式 |
| MediaPipe | 开箱即用 CV 任务 | 只需常见 CV 任务，不需自定义模型 |
| Transformers.js | HuggingFace 生态 | 快速集成预训练模型 |

项目需要同时跑 VAD 和关键帧检测两种自定义模型。ONNX 是跨框架通用格式，核心优势是**模型无关**。

### 语音检测：@ricky0123/vad-web

| 方案 | 特点 | 适用场景 |
|------|------|---------|
| **@ricky0123/vad-web** | 基于 WebRTC VAD，~100KB 含 WASM，纯前端零延迟 | "有没有人说话"二分类 |
| Web Audio API + 能量检测 | AnalyserNode 计算 RMS | 极简但不抗噪 |
| Silero VAD (ONNX) | 神经网络级 VAD | 嘈杂环境需更精准 |
| Picovoice Porcupine | 商业级唤醒词引擎 | 需要唤醒词功能 |

vad-web 是"够用且最轻"的平衡点——直接包装浏览器原生 WebRTC VAD 算法。

### 媒体采集：MediaDevices API

`navigator.mediaDevices.getUserMedia()` 是所有浏览器音视频采集的**唯一标准入口**。所有上层封装库底层都是调这个 API。项目需要原始 MediaStream，用封装库反而要多一层解包。

### 选型共同逻辑

三个技术选择的共同决策模式——**选择最薄的抽象层**：

| 技术 | "最薄"体现在 |
|------|------------|
| ONNX Runtime Web | 不绑定特定框架，模型格式通用 |
| @ricky0123/vad-web | 包装原生 WebRTC VAD，没有多余的模型加载 |
| MediaDevices API | 直接用浏览器原生接口，不加封装层 |

与"前端做轻量预处理"原则一致：前端层只需采集和判断"有没有值得发给后端的数据"。

---

## 五、认证与用户系统选型

### 关键术语

| 名词 | 解释 |
|------|------|
| **JWT** | JSON Web Token，无状态 token，服务端不存 session，分布式友好 |
| **HS256** | HMAC-SHA256，JWT 对称签名算法，用同一密钥签名和验证 |
| **bcrypt** | 密码哈希算法，自适应 cost factor，抗暴力破解 |
| **pgx** | Go 生态性能最优的 PostgreSQL 驱动，原生协议实现，内置连接池 pgxpool |

### 总览

| 能力 | 选型 | 选择理由 |
|------|------|---------|
| 认证方案 | JWT (HS256) | 无状态，分布式友好，实现简单 |
| JWT 库 | golang-jwt/jwt/v5 | 社区主流，v5 活跃维护 |
| 密码哈希 | bcrypt | Go 标准库直接可用，安全性足够 |
| 数据库驱动 | pgx/v5 | Go 生态性能最优的 PostgreSQL 驱动 |
| 数据库迁移 | 手写 SQL | MVP 阶段足够，后续可引入 golang-migrate |

### 认证方案：JWT

| 方案 | 特点 | 适用场景 |
|------|------|---------|
| **JWT (HS256)** | 无状态 token，服务端不存 session，水平扩展友好 | 分布式部署、前后端分离 |
| Session + Cookie | 有状态，服务端存 session（通常 Redis） | 传统 Web 应用、需要服务端控制会话 |
| OAuth2 | 第三方登录授权 | 需要接入微信/GitHub 等第三方登录 |

选择 JWT 的核心理由：项目架构是前后端分离 + WebSocket 长连接，JWT 无需服务端维护 session 状态，天然适配。HS256 对称签名足以满足安全需求，实现比 RS256 简单。

Token 策略采用 **access (15min) + refresh (7day) 双 token**：access_token 短生命周期降低泄露风险，refresh_token 支持无感续期。

### JWT 库：golang-jwt/jwt/v5

| 方案 | 状态 | 特点 |
|------|------|------|
| **golang-jwt/jwt/v5** | 活跃维护 | dgrijalva/jwt-go 的官方继任，社区主流 |
| dgrijalva/jwt-go | 已停维护 | 原始库，不再更新 |
| lestrrat-go/jwx | 活跃 | 功能更全（JWE/JWS），但项目只需签名，过度引入 |

v5 是 Go 生态中 JWT 的事实标准，API 简洁，文档完善。

### 密码哈希：bcrypt

| 方案 | 特点 | 选择理由 |
|------|------|---------|
| **bcrypt** | 自适应 cost factor，抗暴力破解 | Go 标准库 `golang.org/x/crypto/bcrypt` 直接可用 |
| argon2 | 2015 年密码哈希竞赛冠军，抗 GPU/ASIC | 安全性更高，但 Go 生态库不如 bcrypt 成熟 |
| scrypt | 内存硬哈希 | 参数调优复杂，bcrypt 已足够 |

bcrypt 的 `cost` 参数可随硬件升级调大，当前默认 cost=10 足够安全。

### 数据库驱动：pgx/v5

| 方案 | 特点 | 适用场景 |
|------|------|---------|
| **pgx/v5** | 原生 PostgreSQL 协议实现，连接池 pgxpool，性能最优 | 需要高性能、直接写 SQL |
| GORM | 全功能 ORM，自动迁移、关联预加载 | 快速开发、不想写 SQL |
| Ent | Facebook 出品，类型安全的 ORM | 大型项目、强类型需求 |
| database/sql + lib/pq | 标准接口，但 lib/pq 已停维护 | 简单场景 |

项目规模不大（4 张表），手写 SQL 更可控，避免 ORM 的抽象泄漏和性能黑盒。pgx 原生支持 `pgxpool` 连接池，无需额外引入。

### 数据库迁移：手写 SQL

| 方案 | 特点 | 适用场景 |
|------|------|---------|
| **手写 SQL** | 零依赖，完全可控 | 表少（<10 张）、团队小 |
| golang-migrate | CLI + 库双模式，支持版本回滚 | 表多、需要严格版本管理 |
| Atlas | 声明式迁移，HCL 定义 schema | 大型项目、多环境管理 |

MVP 阶段 4 张表，手写 `schema.sql` 即可。后续表结构复杂后可引入 golang-migrate。

### 前端 Token 存储

| 方案 | 特点 | 选择理由 |
|------|------|---------|
| **localStorage** | 持久化存储，刷新不丢失，JS 可直接读写 | 简单直接，SPA 应用标准做法 |
| httpOnly Cookie | 防 XSS 读取，但需防 CSRF | 传统 Web 应用，需额外 CSRF 防护 |
| sessionStorage | 仅当前标签页有效 | 关闭标签页需重新登录，体验差 |

JWT 存 localStorage，配合请求拦截器统一附加 `Authorization: Bearer <token>` header。refresh_token 同样存 localStorage，401 时自动触发刷新流程。
