Compare commits
9 Commits
94c38dcf9f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dc6a405f2 | |||
| d4cdfe962e | |||
| 04c0172038 | |||
| 655eefb198 | |||
| ee004ff628 | |||
| d198667975 | |||
| 74c1a345c2 | |||
| 54a57d3b60 | |||
| acd581bd12 |
352
课题一/AI 视觉对话助手/项目实现/技术选型.md
Normal file
352
课题一/AI 视觉对话助手/项目实现/技术选型.md
Normal file
@@ -0,0 +1,352 @@
|
||||
---
|
||||
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 包容查询示例——精确匹配 JSONB 子结构:
|
||||
|
||||
```sql
|
||||
-- @> 是包容操作符,检查 content 是否包含 {"text": "花"} 这个子结构
|
||||
-- 适合"字段精确匹配"场景;若需模糊关键词搜索,应使用 tsvector 全文检索
|
||||
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 是跨框架的通用格式,无论模型用什么框架训练,都可以导出为 ONNX 并在浏览器中用统一引擎加载。TensorFlow.js 被锁死在 TF 生态,MediaPipe 虽然开箱即用但灵活性不够(不能自定义模型逻辑)。**ONNX Runtime 的核心优势是"模型无关"**。
|
||||
|
||||
### 语音检测:@ricky0123/vad-web
|
||||
|
||||
VAD(Voice Activity Detection)是交互流程的**起始触发器**——用户有没有在说话?触发必须又快又准。
|
||||
|
||||
| 方案 | 特点 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| **@ricky0123/vad-web** | 基于 WebRTC VAD,体积极小(~100KB 含 WASM),纯前端零延迟 | 只需要"有没有人说话"的二分类判断 |
|
||||
| **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 服务面前,属于在不该重的地方加重。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[项目架构与技术栈]]
|
||||
- [[成本控制]]
|
||||
- [[项目架构与技术栈/技术名词解释]]
|
||||
607
课题一/AI 视觉对话助手/项目实现/接口文档.md
Normal file
607
课题一/AI 视觉对话助手/项目实现/接口文档.md
Normal file
@@ -0,0 +1,607 @@
|
||||
---
|
||||
tags: [API, WebSocket, 接口设计, MVP, Go, TypeScript, 扩展性]
|
||||
create time: 2026-06-12 15:41
|
||||
---
|
||||
|
||||
# 接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档定义 AI 视觉对话助手的**前后端通信接口**。以 MVP 为核心目标:用 WebSocket 承载实时对话,用最少的 REST 端点支撑基础运维。**暂不实现持久化**,但通过 Repository 接口模式为后续扩展(对话历史、用量统计)预留干净的接入点。
|
||||
|
||||
> [!info] 设计原则
|
||||
> - **WebSocket 为主**:实时对话是核心场景,所有对话数据走 WebSocket
|
||||
> - **REST 为辅**:仅用于健康检查、会话管理等低频操作
|
||||
> - **接口先行**:先定义契约,再填充实现——前后端可并行开发
|
||||
|
||||
## 正文
|
||||
|
||||
### 接口全景
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Client["浏览器"]
|
||||
WS_C["WebSocket Client"]
|
||||
HTTP_C["HTTP Client"]
|
||||
end
|
||||
|
||||
subgraph Server["Go Gateway :8080"]
|
||||
WS_EP["/ws"]
|
||||
HEALTH_EP["/api/health"]
|
||||
SESSION_EP["/api/sessions"]
|
||||
end
|
||||
|
||||
WS_C <-->|"实时对话"| WS_EP
|
||||
HTTP_C -->|"GET"| HEALTH_EP
|
||||
HTTP_C <-->|"POST / DELETE"| SESSION_EP
|
||||
```
|
||||
|
||||
### 一、WebSocket 协议
|
||||
|
||||
连接地址:`ws://localhost:8080/ws`
|
||||
|
||||
#### 1.1 连接生命周期
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant S as Server
|
||||
|
||||
C->>S: WebSocket Upgrade 请求
|
||||
S-->>C: 101 Switching Protocols
|
||||
S-->>C: {"type":"connected","session_id":"..."}
|
||||
Note over C,S: 连接建立,进入对话
|
||||
|
||||
C->>S: {"type":"query",...}
|
||||
S-->>C: {"type":"stt_result",...}
|
||||
S-->>C: {"type":"llm_chunk",...}
|
||||
S-->>C: {"type":"llm_chunk",...}
|
||||
S-->>C: {"type":"llm_done",...}
|
||||
S-->>C: {"type":"tts_audio",...}
|
||||
|
||||
C->>S: {"type":"query",...}
|
||||
Note over C,S: 持续对话...
|
||||
|
||||
C->>S: {"type":"ping"}
|
||||
S-->>C: {"type":"pong"}
|
||||
|
||||
C->>S: WebSocket Close
|
||||
S-->>C: WebSocket Close Ack
|
||||
```
|
||||
|
||||
#### 1.2 消息格式约定
|
||||
|
||||
所有 WebSocket 消息均为 **JSON 文本帧**,统一结构:
|
||||
|
||||
```typescript
|
||||
// 通用消息信封
|
||||
interface WsMessage {
|
||||
type: string; // 消息类型,必填
|
||||
request_id?: string; // 可选,用于请求-响应关联
|
||||
timestamp?: number; // 可选,毫秒时间戳
|
||||
[key: string]: any; // 类型特定字段
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 客户端 → 服务端消息
|
||||
|
||||
##### `query` —— 发起一次视觉对话
|
||||
|
||||
用户说完话后,客户端同时发送当前图像帧和语音片段:
|
||||
|
||||
```typescript
|
||||
interface QueryMessage {
|
||||
type: "query";
|
||||
request_id: string; // 客户端生成的 UUID
|
||||
image: string; // Base64 编码的 JPEG 图像(不含 data: 前缀)
|
||||
audio: string; // Base64 编码的音频片段(PCM 16kHz)
|
||||
mime_type?: string; // 音频格式,默认 "audio/pcm"
|
||||
}
|
||||
```
|
||||
|
||||
> [!question] 思考
|
||||
> 为什么图像和音频放在同一条消息里?因为 VAD 检测到用户说完话时,需要同时捕获"此刻的画面"和"说的话",拆成两条消息会增加时序同步的复杂度。
|
||||
|
||||
##### `config` —— 更新会话配置
|
||||
|
||||
运行时调整 AI 行为参数,无需重建连接:
|
||||
|
||||
```typescript
|
||||
interface ConfigMessage {
|
||||
type: "config";
|
||||
payload: {
|
||||
tts_enabled?: boolean; // 是否开启语音合成,默认 true
|
||||
detail_level?: "low" | "high"; // 图像精度,默认 "low"
|
||||
language?: string; // 交互语言,默认 "zh-CN"
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
##### `interrupt` —— 打断当前回复
|
||||
|
||||
用户在 AI 回复过程中再次说话,打断正在进行的 LLM/TTS 流:
|
||||
|
||||
```typescript
|
||||
interface InterruptMessage {
|
||||
type: "interrupt";
|
||||
request_id?: string; // 可选,指定打断哪次请求
|
||||
}
|
||||
```
|
||||
|
||||
##### `ping` —— 心跳保活
|
||||
|
||||
```typescript
|
||||
interface PingMessage {
|
||||
type: "ping";
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 服务端 → 客户端消息
|
||||
|
||||
##### `connected` —— 连接建立确认
|
||||
|
||||
```typescript
|
||||
interface ConnectedMessage {
|
||||
type: "connected";
|
||||
session_id: string; // 服务端生成的会话 ID
|
||||
server_version: string; // 服务端版本号,如 "0.1.0"
|
||||
}
|
||||
```
|
||||
|
||||
##### `stt_result` —— 语音识别结果
|
||||
|
||||
LLM 推理前,先返回 STT 识别出的文本,让用户看到"我听到了什么":
|
||||
|
||||
```typescript
|
||||
interface STTResultMessage {
|
||||
type: "stt_result";
|
||||
request_id: string;
|
||||
text: string; // 识别出的用户语音文本
|
||||
is_final: boolean; // 是否为最终结果(流式场景下可能分多段)
|
||||
}
|
||||
```
|
||||
|
||||
##### `llm_chunk` —— LLM 流式输出片段
|
||||
|
||||
```typescript
|
||||
interface LLMChunkMessage {
|
||||
type: "llm_chunk";
|
||||
request_id: string;
|
||||
delta: string; // 本次增量文本
|
||||
role: "assistant";
|
||||
}
|
||||
```
|
||||
|
||||
##### `llm_done` —— LLM 输出完成
|
||||
|
||||
```typescript
|
||||
interface LLMDoneMessage {
|
||||
type: "llm_done";
|
||||
request_id: string;
|
||||
full_text: string; // 完整回复文本
|
||||
tokens_used: {
|
||||
prompt: number; // 输入 token 数
|
||||
completion: number; // 输出 token 数
|
||||
total: number;
|
||||
};
|
||||
model: string; // 实际使用的模型名
|
||||
latency_ms: number; // 端到端延迟(毫秒)
|
||||
}
|
||||
```
|
||||
|
||||
##### `tts_audio` —— TTS 音频流片段
|
||||
|
||||
```typescript
|
||||
interface TTSAudioMessage {
|
||||
type: "tts_audio";
|
||||
request_id: string;
|
||||
audio: string; // Base64 编码的音频片段
|
||||
mime_type: string; // "audio/mp3" 或 "audio/pcm"
|
||||
is_last: boolean; // 是否为最后一片
|
||||
}
|
||||
```
|
||||
|
||||
##### `error` —— 错误通知
|
||||
|
||||
```typescript
|
||||
interface ErrorMessage {
|
||||
type: "error";
|
||||
request_id?: string; // 关联的请求(可选)
|
||||
code: string; // 错误码,见下方错误码表
|
||||
message: string; // 人类可读的错误描述
|
||||
}
|
||||
```
|
||||
|
||||
##### `pong` —— 心跳响应
|
||||
|
||||
```typescript
|
||||
interface PongMessage {
|
||||
type: "pong";
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.5 消息流时序总览
|
||||
|
||||
一次完整交互的消息流转:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant S as Server
|
||||
|
||||
Note over C: VAD 检测到语音结束
|
||||
C->>S: query {image, audio}
|
||||
S-->>C: stt_result {text, is_final: true}
|
||||
|
||||
loop LLM 流式输出
|
||||
S-->>C: llm_chunk {delta: "这"}
|
||||
S-->>C: llm_chunk {delta: "是一"}
|
||||
S-->>C: llm_chunk {delta: "朵花..."}
|
||||
end
|
||||
|
||||
S-->>C: llm_done {full_text, tokens_used, latency_ms}
|
||||
|
||||
loop TTS 音频流
|
||||
S-->>C: tts_audio {audio, is_last: false}
|
||||
S-->>C: tts_audio {audio, is_last: true}
|
||||
end
|
||||
```
|
||||
|
||||
### 二、REST API
|
||||
|
||||
MVP 阶段仅暴露最少量的 HTTP 端点:
|
||||
|
||||
#### 2.1 健康检查
|
||||
|
||||
```http
|
||||
GET /api/health
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "0.1.0",
|
||||
"uptime_seconds": 3600,
|
||||
"active_sessions": 42
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 创建会话(可选)
|
||||
|
||||
MVP 阶段 WebSocket 连接即自动创建会话,此端点为**预留扩展**:
|
||||
|
||||
```http
|
||||
POST /api/sessions
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"user_id": "optional-user-id",
|
||||
"config": {
|
||||
"tts_enabled": true,
|
||||
"detail_level": "low",
|
||||
"language": "zh-CN"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"created_at": "2026-06-12T15:41:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 销毁会话
|
||||
|
||||
```http
|
||||
DELETE /api/sessions/{session_id}
|
||||
```
|
||||
|
||||
响应:`204 No Content`
|
||||
|
||||
#### 2.4 预留端点(暂不实现)
|
||||
|
||||
> [!info] 后续扩展
|
||||
> 引入持久化后,按需添加以下端点:
|
||||
|
||||
| 端点 | 方法 | 用途 | MVP 状态 |
|
||||
|------|------|------|---------|
|
||||
| `/api/sessions/{id}/messages` | GET | 查询对话历史 | 预留,暂不实现 |
|
||||
| `/api/usage` | GET | 查询用量统计 | 预留,暂不实现 |
|
||||
| `/api/users/{id}/preferences` | GET/PUT | 用户偏好管理 | 预留,暂不实现 |
|
||||
|
||||
### 三、数据模型
|
||||
|
||||
#### 3.1 Go 后端模型
|
||||
|
||||
```go
|
||||
// ---- 核心模型(MVP 实现)----
|
||||
|
||||
type Session struct {
|
||||
ID string `json:"session_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Config SessionConfig `json:"config"`
|
||||
}
|
||||
|
||||
type SessionConfig struct {
|
||||
TTSEnabled bool `json:"tts_enabled"`
|
||||
DetailLevel string `json:"detail_level"` // "low" | "high"
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
type QueryRequest struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Image []byte `json:"-"` // Base64 解码后
|
||||
Audio []byte `json:"-"` // Base64 解码后
|
||||
MimeType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"` // "user" | "assistant"
|
||||
Content string `json:"content"`
|
||||
ImageURL string `json:"image_url,omitempty"`
|
||||
TokensUsed int `json:"tokens_used,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// ---- 预留模型(持久化扩展)----
|
||||
|
||||
// HistoryRepository 定义对话历史的存储契约
|
||||
// MVP: 内存实现(session 内有效,断开即丢)
|
||||
// 后续: PostgreSQL 实现
|
||||
type HistoryRepository interface {
|
||||
SaveMessage(ctx context.Context, sessionID string, msg Message) error
|
||||
GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error)
|
||||
}
|
||||
|
||||
// UsageRepository 定义用量统计的存储契约
|
||||
// MVP: 内存计数器(仅当前进程可见)
|
||||
// 后续: PostgreSQL 按天聚合
|
||||
type UsageRepository interface {
|
||||
RecordUsage(ctx context.Context, sessionID string, usage UsageRecord) error
|
||||
GetDailyUsage(ctx context.Context, userID string, days int) ([]UsageDaily, error)
|
||||
}
|
||||
|
||||
type UsageRecord struct {
|
||||
SessionID string `json:"session_id"`
|
||||
LLMTokens int `json:"llm_tokens"`
|
||||
STTSeconds float64 `json:"stt_seconds"`
|
||||
TTSChars int `json:"tts_chars"`
|
||||
EstimatedCost float64 `json:"estimated_cost"`
|
||||
}
|
||||
|
||||
type UsageDaily struct {
|
||||
Date string `json:"date"`
|
||||
LLMTokens int `json:"llm_tokens"`
|
||||
EstimatedCost float64 `json:"estimated_cost"`
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 TypeScript 前端模型
|
||||
|
||||
```typescript
|
||||
// ---- 核心模型 ----
|
||||
|
||||
interface Session {
|
||||
sessionId: string;
|
||||
createdAt: string;
|
||||
config: SessionConfig;
|
||||
}
|
||||
|
||||
interface SessionConfig {
|
||||
ttsEnabled: boolean;
|
||||
detailLevel: "low" | "high";
|
||||
language: string;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
imageUrl?: string; // 关键帧(用户消息可附带)
|
||||
timestamp: number;
|
||||
tokensUsed?: number; // 仅 assistant 消息
|
||||
}
|
||||
|
||||
// ---- WebSocket 消息联合类型 ----
|
||||
|
||||
type ServerMessage =
|
||||
| ConnectedMessage
|
||||
| STTResultMessage
|
||||
| LLMChunkMessage
|
||||
| LLMDoneMessage
|
||||
| TTSAudioMessage
|
||||
| ErrorMessage
|
||||
| PongMessage;
|
||||
|
||||
type ClientMessage =
|
||||
| QueryMessage
|
||||
| ConfigMessage
|
||||
| InterruptMessage
|
||||
| PingMessage;
|
||||
```
|
||||
|
||||
### 四、扩展接口设计
|
||||
|
||||
通过**接口(Interface)模式**隔离存储层,MVP 用内存实现,后续替换为数据库实现——业务逻辑层零改动。
|
||||
|
||||
#### 4.1 Repository 接口
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Biz["业务逻辑层(不变)"]
|
||||
ORCH["AI Orchestrator"]
|
||||
SM["Session Manager"]
|
||||
end
|
||||
|
||||
subgraph Repo["存储接口层"]
|
||||
HR["HistoryRepository"]
|
||||
UR["UsageRepository"]
|
||||
end
|
||||
|
||||
subgraph Impl_MVP["MVP 实现"]
|
||||
MEM_H["InMemoryHistory"]
|
||||
MEM_U["InMemoryUsage"]
|
||||
end
|
||||
|
||||
subgraph Impl_Future["后续实现"]
|
||||
PG_H["PgHistory"]
|
||||
PG_U["PgUsage"]
|
||||
end
|
||||
|
||||
ORCH --> HR
|
||||
ORCH --> UR
|
||||
SM --> HR
|
||||
HR --> MEM_H
|
||||
UR --> MEM_U
|
||||
HR -.->|"替换"| PG_H
|
||||
UR -.->|"替换"| PG_U
|
||||
```
|
||||
|
||||
#### 4.2 MVP 内存实现
|
||||
|
||||
```go
|
||||
// InMemoryHistory —— MVP 阶段的对话历史实现
|
||||
// 数据存在内存 map 中,连接断开即丢
|
||||
type InMemoryHistory struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string][]Message // sessionID -> messages
|
||||
}
|
||||
|
||||
func (h *InMemoryHistory) SaveMessage(ctx context.Context, sessionID string, msg Message) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.sessions[sessionID] = append(h.sessions[sessionID], msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *InMemoryHistory) GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
msgs := h.sessions[sessionID]
|
||||
if limit > 0 && len(msgs) > limit {
|
||||
msgs = msgs[len(msgs)-limit:]
|
||||
}
|
||||
return msgs, nil
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 后续替换为 PostgreSQL
|
||||
|
||||
引入持久化时,只需新增一个实现,无需修改业务逻辑:
|
||||
|
||||
```go
|
||||
// PgHistory —— PostgreSQL 实现(后续扩展)
|
||||
type PgHistory struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func (p *PgHistory) SaveMessage(ctx context.Context, sessionID string, msg Message) error {
|
||||
_, err := p.pool.Exec(ctx,
|
||||
`INSERT INTO messages (session_id, role, content, image_url, tokens_used)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
sessionID, msg.Role, msg.Content, msg.ImageURL, msg.TokensUsed,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *PgHistory) GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) {
|
||||
rows, _ := p.pool.Query(ctx,
|
||||
`SELECT role, content, image_url, tokens_used
|
||||
FROM messages WHERE session_id = $1
|
||||
ORDER BY created_at DESC LIMIT $2`,
|
||||
sessionID, limit,
|
||||
)
|
||||
defer rows.Close()
|
||||
// ... scan and return
|
||||
}
|
||||
```
|
||||
|
||||
> [!question] 思考
|
||||
> 这就是**依赖倒置原则**——业务层依赖接口(`HistoryRepository`),不依赖具体实现。MVP 阶段注入 `InMemoryHistory`,上线时一行代码换成 `PgHistory`,其余逻辑完全不动。
|
||||
|
||||
#### 4.4 注入点示例
|
||||
|
||||
在应用启动时根据配置选择实现:
|
||||
|
||||
```go
|
||||
func NewApp(cfg *Config) *App {
|
||||
var history HistoryRepository
|
||||
var usage UsageRepository
|
||||
|
||||
switch cfg.Storage.Driver {
|
||||
case "postgres":
|
||||
pool, _ := pgxpool.New(ctx, cfg.Storage.DSN)
|
||||
history = &PgHistory{pool: pool}
|
||||
usage = &PgUsage{pool: pool}
|
||||
default: // "memory" — MVP 默认
|
||||
history = &InMemoryHistory{sessions: make(map[string][]Message)}
|
||||
usage = &InMemoryUsage{}
|
||||
}
|
||||
|
||||
return &App{
|
||||
orchestrator: NewOrchestrator(cfg.AI, history, usage),
|
||||
sessionMgr: NewSessionManager(cfg.Session, history),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 五、错误码定义
|
||||
|
||||
| 错误码 | 含义 | 客户端处理建议 |
|
||||
|--------|------|--------------|
|
||||
| `INVALID_MESSAGE` | 消息格式不合法 | 检查 JSON 结构,不重试 |
|
||||
| `SESSION_NOT_FOUND` | 会话不存在或已过期 | 重新建立 WebSocket 连接 |
|
||||
| `RATE_LIMITED` | 请求频率超限 | 延迟后重试,提示用户稍等 |
|
||||
| `IMAGE_TOO_LARGE` | 图像超过 4MB 限制 | 降低分辨率或压缩质量 |
|
||||
| `AUDIO_TOO_SHORT` | 音频片段 < 250ms | 忽略,等待下次语音输入 |
|
||||
| `LLM_TIMEOUT` | LLM 推理超时(>10s) | 提示用户重试 |
|
||||
| `LLM_ERROR` | LLM 服务异常 | 提示用户重试,服务端记录日志 |
|
||||
| `STT_ERROR` | 语音识别失败 | 回退到纯文本输入模式 |
|
||||
| `TTS_ERROR` | 语音合成失败 | 静默回退到纯文本回复 |
|
||||
| `INTERNAL_ERROR` | 服务端内部错误 | 提示用户重试 |
|
||||
|
||||
> [!tip] 错误处理原则
|
||||
> 客户端收到 `error` 消息后,应根据错误码分别处理:可恢复的(如 `RATE_LIMITED`)自动重试;不可恢复的(如 `IMAGE_TOO_LARGE`)提示用户调整;服务端异常(如 `INTERNAL_ERROR`)记录日志并提示重试。
|
||||
|
||||
### 六、连接管理
|
||||
|
||||
#### 心跳机制
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant S as Server
|
||||
|
||||
loop 每 30 秒
|
||||
C->>S: ping
|
||||
S-->>C: pong
|
||||
end
|
||||
|
||||
Note over S: 超过 60 秒无 ping
|
||||
S->>S: 判定连接断开
|
||||
S->>S: 清理会话资源
|
||||
```
|
||||
|
||||
#### 重连策略
|
||||
|
||||
客户端断线后按**指数退避**重连:
|
||||
|
||||
```typescript
|
||||
function reconnect(attempt: number) {
|
||||
const delay = Math.min(1000 * Math.pow(2, attempt), 30000); // 最大 30s
|
||||
const jitter = Math.random() * 1000; // 随机抖动
|
||||
setTimeout(() => connect(), delay + jitter);
|
||||
}
|
||||
// attempt: 0 → 1s, 1 → 2s, 2 → 4s, 3 → 8s, ... 最大 30s
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[项目架构与技术栈]]
|
||||
- [[技术选型]]
|
||||
- [[项目架构与技术栈/技术名词解释]]
|
||||
@@ -79,6 +79,7 @@ graph TB
|
||||
| 语言 | **Go** | 高并发 goroutine 模型,适合长连接管理 |
|
||||
| WebSocket | **gorilla/websocket** | Go 生态最成熟的 WebSocket 库 |
|
||||
| 会话存储 | **Redis** | 高速 KV 存储,适合会话状态和上下文缓存 |
|
||||
| 持久化存储 | **PostgreSQL** | 对话历史、用量统计、用户偏好(MVP 阶段可选) |
|
||||
| 配置管理 | **Viper** | 支持多格式配置,环境变量覆盖 |
|
||||
| 日志 | **Zap** | 高性能结构化日志 |
|
||||
|
||||
@@ -279,9 +280,115 @@ 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,互不干扰。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[视觉理解]]
|
||||
- [[语音交互]]
|
||||
- [[成本控制]]
|
||||
- [[用户故事]]
|
||||
- [[项目架构与技术栈/技术名词解释]]
|
||||
- [[技术选型]]
|
||||
- [[接口文档]]
|
||||
154
课题一/AI 视觉对话助手/项目架构与技术栈/技术名词解释.md
Normal file
154
课题一/AI 视觉对话助手/项目架构与技术栈/技术名词解释.md
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
tags: [AI, 前端, 后端, 技术栈, 入门, 概念解释]
|
||||
create time: 2026-06-12 15:00
|
||||
---
|
||||
|
||||
# 技术名词解释
|
||||
|
||||
## 概述
|
||||
|
||||
对 [[项目架构与技术栈]] 中技术选型表里出现的所有关键名词,用初学者能理解的方式逐一解释。分为**前端**、**后端**、**AI 服务**三大板块,每个名词配一句话概括 + 展开说明。
|
||||
|
||||
## 正文
|
||||
|
||||
### 前端相关
|
||||
|
||||
#### React 18 + TypeScript
|
||||
|
||||
- **一句话**:用 JavaScript(加了类型约束的版本)来搭建网页界面的工具库。
|
||||
- **展开**:**React** 是 Facebook 开源的 UI 框架,核心思想是把页面拆成一个个「组件」,像搭积木一样拼装。**TypeScript** 是 JavaScript 的「增强版」,多了类型声明(告诉编辑器变量是数字还是字符串),能在写代码时就提前发现 bug。**18** 是版本号,带来了并发渲染等新特性。
|
||||
|
||||
> [!question] 想一想
|
||||
> 如果没有类型系统,一个函数参数本来期望收到数字,却传了字符串,程序会怎样?TypeScript 就是在编译阶段帮你拦截这类问题。
|
||||
|
||||
#### Vite
|
||||
|
||||
- **一句话**:帮你把写好的代码「打包」成浏览器能运行的文件,并提供飞快的开发体验。
|
||||
- **展开**:开发时改一行代码,Vite 能在毫秒级刷新页面(叫**热更新 / HMR**)。构建发布时,它会压缩、优化代码,产出体积小的文件。相比老工具 Webpack,Vite 利用浏览器原生的 ES Module 能力,启动速度极快。
|
||||
|
||||
#### WebSocket(原生 API)
|
||||
|
||||
- **一句话**:让浏览器和服务器之间保持一条「持续在线」的双向通道。
|
||||
- **展开**:普通 HTTP 请求是「一问一答」——你发请求,服务器回一个响应,连接就断了。**WebSocket** 则像打电话:接通后双方可以随时互发消息,直到某一方挂断。这对实时对话、聊天、推送通知等场景非常关键。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["HTTP 模式"] -->|"请求1: 你好"| B["服务器"]
|
||||
B -->|"响应1: 你好"| A
|
||||
A -->|"请求2: 问题?"| B
|
||||
B -->|"响应2: 答案"| A
|
||||
C["WebSocket 模式"] -->|"连接建立"| D["服务器"]
|
||||
C <-->|"持续双向通信"| D
|
||||
```
|
||||
|
||||
#### ONNX Runtime Web
|
||||
|
||||
- **一句话**:在浏览器里直接运行 AI 模型,不用把数据发到服务器。
|
||||
- **展开**:**ONNX** 是微软定义的通用模型格式,**Runtime** 就是运行引擎。它让你可以把训练好的轻量模型(比如判断"有没有人在说话"的 VAD 模型)直接放在浏览器里跑。好处是零延迟、不耗服务器资源。缺点是浏览器算力有限,只能跑小模型。
|
||||
|
||||
#### VAD / WebRTC VAD
|
||||
|
||||
- **一句话**:自动检测「人有没有在说话」的技术。
|
||||
- **展开**:**VAD** 全称 Voice Activity Detection(语音活动检测)。**WebRTC** 是 Google 开源的实时通信技术栈,里面内置了一套高效的 VAD 算法。在我们的项目里,前端用它来判断用户什么时候开始/停止说话,省去了用户手动按按钮录音的操作。
|
||||
|
||||
#### MediaDevices API
|
||||
|
||||
- **一句话**:浏览器提供的标准接口,用来访问摄像头和麦克风。
|
||||
- **展开**:调用 `navigator.mediaDevices.getUserMedia({ video: true, audio: true })` 就能让浏览器弹出授权提示,拿到摄像头画面和麦克风音频流。这是浏览器原生能力,不需要装插件。
|
||||
|
||||
---
|
||||
|
||||
### 后端相关
|
||||
|
||||
#### Go(Golang)
|
||||
|
||||
- **一句话**:Google 开发的编程语言,擅长高并发场景。
|
||||
- **展开**:Go 的杀手锏是 **goroutine**——一种极轻量的「协程」,一个程序可以轻松开几万个 goroutine 同时工作,而每个只占几 KB 内存。对比传统的线程(一个就占几 MB),在管理大量 WebSocket 长连接时优势巨大。
|
||||
|
||||
> [!question] 想一想
|
||||
> 如果同时有 10000 个用户在线聊天,每人一个连接,用传统线程模型可能需要几 GB 内存;用 goroutine,几十 MB 就够了。这就是选 Go 的原因。
|
||||
|
||||
#### gorilla/websocket
|
||||
|
||||
- **一句话**:Go 语言里最流行的 WebSocket 库。
|
||||
- **展开**:Go 标准库没有内置 WebSocket 支持,`gorilla/websocket` 是社区维护的第三方包,API 简洁、文档完善、生产环境验证充分。它帮你处理了 WebSocket 协议的握手、帧解析等底层细节。
|
||||
|
||||
#### Redis
|
||||
|
||||
- **一句话**:一种超快的内存数据库,常用来做缓存和临时数据存储。
|
||||
- **展开**:Redis 是 **Key-Value 存储**(键值对),数据放在内存里所以读写极快(微秒级)。在本项目里用来存放用户的会话状态和对话上下文——比如用户聊到第几轮、历史消息等,保证多个后端实例能共享数据。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
GW1["Gateway-1"] -->|"写入会话状态"| REDIS["Redis"]
|
||||
GW2["Gateway-2"] -->|"读取会话状态"| REDIS
|
||||
REDIS -->|"TTL 过期自动清理"| CLEANUP["自动清理"]
|
||||
```
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
- **一句话**:功能强大的开源关系型数据库,适合持久化存储结构化数据。
|
||||
- **展开**:Redis 存的数据在内存里,重启就丢了。**PostgreSQL**(简称 PG)则把数据写到磁盘上,永久保存。它用**SQL**语言查询,支持复杂的联表查询和事务(保证数据一致性)。在本项目里,上线后用来存对话历史、用量统计、用户偏好等需要「永久保留」的数据。MVP 阶段可以先不用,等产品验证后再引入。
|
||||
|
||||
> [!question] 想一想
|
||||
> Redis 超快但重启丢数据,PostgreSQL 慢一点但数据不丢。为什么不二选一?因为它们擅长的事情不同——Redis 做「热数据缓存」(频繁读写的会话状态),PG 做「冷数据持久化」(需要长期保存的记录)。两者配合才是生产级架构。
|
||||
|
||||
#### Viper
|
||||
|
||||
- **一句话**:Go 语言的配置管理工具,帮你读取各种格式的配置文件。
|
||||
- **展开**:项目运行时需要读取端口号、数据库地址、API Key 等配置。**Viper** 能读 JSON、YAML、TOML 等格式,还支持用环境变量覆盖配置——方便在开发、测试、生产环境用不同配置而不用改代码。
|
||||
|
||||
#### Zap
|
||||
|
||||
- **一句话**:高性能的日志库,用来记录程序运行时的信息。
|
||||
- **展开**:程序运行时需要记录「谁在什么时候做了什么、出了什么错」,这就是日志。**Zap** 是 Uber 开源的日志库,输出的是**结构化日志**(JSON 格式),方便后续用工具搜索和分析。性能远超标准库 `log`。
|
||||
|
||||
---
|
||||
|
||||
### AI 服务相关
|
||||
|
||||
#### 多模态 LLM(GPT-4o / Claude Sonnet)
|
||||
|
||||
- **一句话**:既能读文字又能看图片的大语言模型。
|
||||
- **展开**:**LLM** = Large Language Model(大语言模型),就是 ChatGPT 背后的那种 AI。**多模态**意味着它不只是处理文本,还能理解图像——你给它一张照片和一个问题,它能「看懂」照片再回答。GPT-4o 是 OpenAI 的,Claude Sonnet 是 Anthropic 的,都是主流选择。
|
||||
|
||||
#### STT(语音转文字)/ Deepgram
|
||||
|
||||
- **一句话**:把人说的话自动转成文字。
|
||||
- **展开**:**STT** = Speech-to-Text。**Deepgram** 是一家专注语音识别的公司,它的流式识别延迟可低至 500 毫秒以内——用户还在说话时就开始转录,说完时文字已经出来了。备选方案 **FunASR** 是阿里开源的,可以自己部署,适合对数据隐私有要求的场景。
|
||||
|
||||
#### TTS(文字转语音)/ OpenAI TTS
|
||||
|
||||
- **一句话**:把 AI 的文字回复自动读出来。
|
||||
- **展开**:**TTS** = Text-to-Speech。OpenAI TTS 生成的语音自然度很高,听起来接近真人。**Edge TTS** 是微软提供的免费方案,音质也不错,适合成本敏感场景。**支持流式**意味着不用等全部文字生成完才开始读,边生成边读,用户体验更流畅。
|
||||
|
||||
#### GPT-4o-mini / Haiku(轻量分类模型)
|
||||
|
||||
- **一句话**:又快又便宜的小模型,用来做「初步判断」。
|
||||
- **展开**:不是每个请求都需要用最强的模型。比如判断「这个问题简单还是复杂」,用小模型几毫秒就能搞定,省下大模型的调用费。**模型路由**就是这个逻辑:先用小模型分类,简单问题走小模型,复杂问题再升级到大模型。
|
||||
|
||||
> [!tip] 成本思维
|
||||
> 在实际项目中,AI API 是按调用量计费的。用小模型做前置判断,能省 80% 以上的 API 费用。这也是为什么技术选型表里有「轻量分类」这一行。
|
||||
|
||||
---
|
||||
|
||||
### 横向对比速查
|
||||
|
||||
| 名词 | 类别 | 一句话定义 |
|
||||
|------|------|-----------|
|
||||
| React | 前端框架 | 组件化搭建 UI |
|
||||
| TypeScript | 编程语言 | 带类型的 JS,提前防 bug |
|
||||
| Vite | 构建工具 | 快速打包和热更新 |
|
||||
| WebSocket | 通信协议 | 浏览器与服务器的双向通道 |
|
||||
| ONNX Runtime | 推理引擎 | 浏览器端跑 AI 模型 |
|
||||
| VAD | 语音技术 | 检测人有没有在说话 |
|
||||
| Go | 后端语言 | 高并发,轻量协程 |
|
||||
| Redis | 数据库 | 内存 KV 缓存,读写极快 |
|
||||
| PostgreSQL | 数据库 | 关系型数据库,数据持久化 |
|
||||
| LLM | AI 模型 | 大语言模型 |
|
||||
| STT | AI 能力 | 语音转文字 |
|
||||
| TTS | AI 能力 | 文字转语音 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[项目架构与技术栈]]
|
||||
@@ -39,6 +39,8 @@ create time: 2026-06-12 11:13
|
||||
|
||||
## 关联笔记
|
||||
- [[项目架构与技术栈]]
|
||||
- [[接口文档]]
|
||||
- [[技术选型]]
|
||||
- [[视觉理解]]
|
||||
- [[语音交互]]
|
||||
- [[成本控制]]
|
||||
Reference in New Issue
Block a user