394 lines
12 KiB
Markdown
394 lines
12 KiB
Markdown
---
|
||
tags: [AI, 架构设计, 技术栈, 端云协同, WebSocket, 微服务]
|
||
create time: 2026-06-12 14:32
|
||
---
|
||
|
||
# 项目架构与技术栈
|
||
|
||
## 概述
|
||
|
||
本文档设计 AI 视觉对话助手的**分层架构**与**技术选型**。核心设计原则:**前端做轻量预处理,后端做智能编排,云端 AI 服务按需调用**——在保证交互体验的同时控制成本。
|
||
|
||
## 正文
|
||
|
||
### 整体架构
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph Client["浏览器客户端"]
|
||
UI["React UI"]
|
||
CAM["摄像头/麦克风"]
|
||
EDGE["边缘预处理"]
|
||
WS_C["WebSocket Client"]
|
||
end
|
||
|
||
subgraph Gateway["Go 后端网关"]
|
||
WS_S["WebSocket Server"]
|
||
SESSION["会话管理"]
|
||
ROUTER["模型路由"]
|
||
ORCH["AI 编排器"]
|
||
end
|
||
|
||
subgraph AI["云端 AI 服务"]
|
||
LLM["多模态 LLM"]
|
||
STT["语音识别"]
|
||
TTS["语音合成"]
|
||
end
|
||
|
||
CAM --> EDGE
|
||
EDGE -->|"关键帧 + 语音片段"| WS_C
|
||
WS_C <-->|"双向实时通信"| WS_S
|
||
WS_S --> SESSION
|
||
SESSION --> ROUTER
|
||
ROUTER --> ORCH
|
||
ORCH --> LLM
|
||
ORCH --> STT
|
||
ORCH --> TTS
|
||
TTS -->|"音频流"| WS_S
|
||
LLM -->|"文本流"| WS_S
|
||
```
|
||
|
||
三层各司其职:
|
||
|
||
| 层级 | 职责 | 关键约束 |
|
||
|------|------|---------|
|
||
| **客户端** | 媒体采集、边缘预处理、UI 渲染 | 浏览器资源有限,模型需轻量 |
|
||
| **Go 网关** | 会话管理、模型路由、AI 服务编排 | 高并发、低延迟、状态管理 |
|
||
| **AI 服务** | LLM 推理、语音识别、语音合成 | 按量计费,需控制调用频率 |
|
||
|
||
> [!question] 思考
|
||
> 为什么要单独加一层 Go 网关,而不是让前端直连 AI API?原因有三:1)API Key 安全性;2)统一的速率限制和成本管控;3)多模型路由逻辑集中在一处便于维护。
|
||
|
||
### 技术栈选型
|
||
|
||
#### 前端
|
||
|
||
| 技术 | 选型 | 选择理由 |
|
||
|------|------|---------|
|
||
| 框架 | **React 18 + TypeScript** | 组件化开发,类型安全,生态成熟 |
|
||
| 构建 | **Vite** | 开发热更新快,构建产物小 |
|
||
| 实时通信 | **WebSocket (原生 API)** | 浏览器原生支持,无需额外依赖 |
|
||
| 边缘推理 | **ONNX Runtime Web** | 浏览器端跑轻量模型(VAD、关键帧检测) |
|
||
| 语音检测 | **@ricky0123/vad-web** | 基于 WebRTC VAD,纯前端零延迟 |
|
||
| 媒体采集 | **MediaDevices API** | 浏览器原生摄像头/麦克风访问 |
|
||
|
||
#### 后端
|
||
|
||
| 技术 | 选型 | 选择理由 |
|
||
|------|------|---------|
|
||
| 语言 | **Go** | 高并发 goroutine 模型,适合长连接管理 |
|
||
| WebSocket | **gorilla/websocket** | Go 生态最成熟的 WebSocket 库 |
|
||
| 会话存储 | **Redis** | 高速 KV 存储,适合会话状态和上下文缓存 |
|
||
| 持久化存储 | **PostgreSQL** | 对话历史、用量统计、用户偏好(MVP 阶段可选) |
|
||
| 配置管理 | **Viper** | 支持多格式配置,环境变量覆盖 |
|
||
| 日志 | **Zap** | 高性能结构化日志 |
|
||
|
||
#### AI 服务(按需选型)
|
||
|
||
| 能力 | 主选方案 | 备选方案 | 选型考量 |
|
||
|------|---------|---------|---------|
|
||
| 多模态 LLM | **GPT-4o** | Claude Sonnet | 视觉理解能力强,API 成熟 |
|
||
| 语音识别 STT | **Deepgram** | FunASR 自部署 | 流式识别延迟低(<500ms) |
|
||
| 语音合成 TTS | **OpenAI TTS** | Edge TTS(免费) | 音质自然,支持流式 |
|
||
| 轻量分类 | **GPT-4o-mini** | Haiku | 模型路由时的复杂度判断 |
|
||
|
||
> [!tip] 混合策略
|
||
> 不必绑定单一厂商。Go 网关的模型路由层可以统一封装不同 AI 服务的调用接口,按场景动态切换。比如简单识别用 GPT-4o-mini,深度分析用 GPT-4o,TTS 用免费的 Edge TTS 降低成本。
|
||
|
||
### 核心交互流程
|
||
|
||
一次完整的"用户提问 → AI 回答"流程:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant B as Browser
|
||
participant G as Go Gateway
|
||
participant S as STT Service
|
||
participant L as LLM Service
|
||
participant T as TTS Service
|
||
|
||
B->>B: VAD 检测到语音开始
|
||
B->>B: 捕获当前摄像头帧
|
||
B->>G: WebSocket 发送 [音频流 + 图像帧]
|
||
G->>S: 转发音频流
|
||
S-->>G: 流式返回识别文本
|
||
G->>L: 发送 [图像 + 识别文本 + 历史上下文]
|
||
L-->>G: 流式返回回答文本
|
||
G-->>B: WebSocket 推送回答文本
|
||
G->>T: 发送回答文本
|
||
T-->>G: 流式返回音频
|
||
G-->>B: WebSocket 推送音频流
|
||
B->>B: 播放音频 + 渲染文字
|
||
```
|
||
|
||
> [!info] 关键优化
|
||
> 注意 LLM 文本流和 TTS 音频流是**并行推送**的——客户端先展示文字,同时开始播放语音,用户感知延迟大幅降低。
|
||
|
||
### 后端架构设计
|
||
|
||
Go 网关的核心模块:
|
||
|
||
```mermaid
|
||
graph TD
|
||
subgraph Server["Go Gateway"]
|
||
WS["WebSocket Hub"]
|
||
SM["Session Manager"]
|
||
MR["Model Router"]
|
||
AO["AI Orchestrator"]
|
||
RL["Rate Limiter"]
|
||
CACHE["Context Cache"]
|
||
end
|
||
|
||
WS --> SM
|
||
SM --> MR
|
||
MR --> AO
|
||
SM --> RL
|
||
SM --> CACHE
|
||
```
|
||
|
||
各模块职责:
|
||
|
||
| 模块 | 职责 | 关键实现 |
|
||
|------|------|---------|
|
||
| **WebSocket Hub** | 管理所有客户端连接,广播/定向推送 | goroutine per connection |
|
||
| **Session Manager** | 维护用户会话状态、对话历史 | Redis + TTL 过期策略 |
|
||
| **Model Router** | 根据请求类型选择 AI 模型 | 规则引擎 + 成本阈值 |
|
||
| **AI Orchestrator** | 编排多路 AI 调用(并行/串行) | context 取消 + 超时控制 |
|
||
| **Rate Limiter** | 防止单用户过度消耗 API 额度 | 令牌桶算法 |
|
||
|
||
Go 后端核心代码结构:
|
||
|
||
```go
|
||
// AI 编排器:并行调用 LLM 和 TTS
|
||
func (o *Orchestrator) ProcessQuery(ctx context.Context, req *QueryRequest) (*QueryResponse, error) {
|
||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||
defer cancel()
|
||
|
||
// 并行:LLM 推理 + 准备 TTS
|
||
llmCh := make(chan string, 1)
|
||
go func() {
|
||
resp, _ := o.llm.Chat(ctx, req.Image, req.Text, req.History)
|
||
llmCh <- resp
|
||
}()
|
||
|
||
llmText := <-llmCh
|
||
// LLM 返回后,流式推送给客户端,同时启动 TTS
|
||
ttsCh := make(chan []byte, 1)
|
||
go func() {
|
||
audio, _ := o.tts.Synthesize(ctx, llmText)
|
||
ttsCh <- audio
|
||
}()
|
||
|
||
return &QueryResponse{Text: llmText, Audio: <-ttsCh}, nil
|
||
}
|
||
```
|
||
|
||
### 前端架构设计
|
||
|
||
```mermaid
|
||
graph TD
|
||
subgraph App["React App"]
|
||
MAIN["App Root"]
|
||
CAM_M["CameraManager"]
|
||
MIC_M["MicManager"]
|
||
EDGE_M["EdgeProcessor"]
|
||
WS_M["WebSocketManager"]
|
||
CHAT["ChatPanel"]
|
||
VIDEO["VideoPreview"]
|
||
end
|
||
|
||
MAIN --> CAM_M
|
||
MAIN --> MIC_M
|
||
MAIN --> WS_M
|
||
MAIN --> CHAT
|
||
MAIN --> VIDEO
|
||
CAM_M --> EDGE_M
|
||
MIC_M --> EDGE_M
|
||
EDGE_M --> WS_M
|
||
```
|
||
|
||
核心 Hook 设计:
|
||
|
||
```typescript
|
||
// useVisionSession —— 封装一次完整的视觉对话会话
|
||
function useVisionSession() {
|
||
const [messages, setMessages] = useState<Message[]>([]);
|
||
const wsRef = useWebSocket("ws://localhost:8080/ws");
|
||
|
||
// 摄像头管理
|
||
const videoRef = useRef<HTMLVideoElement>(null);
|
||
const { captureFrame } = useCamera(videoRef);
|
||
|
||
// VAD 语音检测
|
||
const { isSpeaking } = useVAD({
|
||
onSpeechEnd: async (audio) => {
|
||
const frame = captureFrame();
|
||
// 同时发送图像帧和语音片段
|
||
wsRef.current?.send(JSON.stringify({
|
||
type: "query",
|
||
image: frame.toDataURL("image/jpeg", 0.7),
|
||
audio: encodeAudio(audio)
|
||
}));
|
||
}
|
||
});
|
||
|
||
// 接收 AI 回复(文本 + 音频)
|
||
useEffect(() => {
|
||
wsRef.current?.on("message", (data) => {
|
||
const { text, audio } = JSON.parse(data);
|
||
setMessages(prev => [...prev, { role: "assistant", text }]);
|
||
if (audio) playAudio(audio);
|
||
});
|
||
}, []);
|
||
|
||
return { messages, videoRef, isSpeaking };
|
||
}
|
||
```
|
||
|
||
### 部署架构
|
||
|
||
```mermaid
|
||
graph LR
|
||
subgraph CDN["CDN"]
|
||
STATIC["静态资源"]
|
||
end
|
||
|
||
subgraph LB["负载均衡"]
|
||
NGINX["Nginx"]
|
||
end
|
||
|
||
subgraph App["应用层"]
|
||
GW1["Gateway-1"]
|
||
GW2["Gateway-2"]
|
||
end
|
||
|
||
subgraph Storage["存储层"]
|
||
REDIS["Redis"]
|
||
end
|
||
|
||
USER["用户浏览器"] --> CDN
|
||
CDN --> STATIC
|
||
USER -->|"WebSocket"| NGINX
|
||
NGINX --> GW1
|
||
NGINX --> GW2
|
||
GW1 --> REDIS
|
||
GW2 --> REDIS
|
||
GW1 -->|"API Calls"| AI["AI Services"]
|
||
GW2 -->|"API Calls"| AI
|
||
```
|
||
|
||
> [!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,互不干扰。
|
||
|
||
## 关联笔记
|
||
|
||
- [[视觉理解]]
|
||
- [[语音交互]]
|
||
- [[成本控制]]
|
||
- [[用户故事]]
|
||
- [[项目架构与技术栈/技术名词解释]]
|
||
- [[持久化技术选型]]
|