Files
CamTalk/docs/02-系统架构.md

257 lines
12 KiB
Markdown
Raw Normal View History

# 系统架构
## 概述
三层架构:**前端做轻量预处理,后端做智能编排,云端 AI 服务按需调用**。在保证交互体验的同时控制成本。
## 三层架构
| 层级 | 职责 | 关键约束 |
|------|------|---------|
| **客户端(浏览器)** | 媒体采集、边缘预处理、UI 渲染 | 浏览器资源有限,模型需轻量 |
| **Go 网关** | 会话管理、AI 服务编排、流式管道 | 高并发、低延迟、状态管理 |
| **AI 服务** | LLM 推理、语音识别、语音合成 | 按量计费,需控制调用频率 |
> 为什么要单独加一层 Go 网关,而不是让前端直连 AI API1API Key 安全性2统一的速率限制和成本管控3多模型路由逻辑集中在一处便于维护。
## 技术栈
### 前端
| 技术 | 选型 | 选择理由 |
|------|------|---------|
| 框架 | React 18 + TypeScript | 组件化开发,类型安全,生态成熟 |
| 构建 | Vite | 开发热更新快,构建产物小 |
| 实时通信 | WebSocket原生 API + 自封装连接管理 | 浏览器原生支持,封装心跳/重连/消息分发 |
| 边缘推理 | ONNX Runtime Web | 浏览器端跑轻量模型VAD、关键帧检测 |
| 语音检测 | @ricky0123/vad-web | 基于 WebRTC VAD纯前端零延迟 |
| 媒体采集 | MediaDevices API | 浏览器原生摄像头/麦克风访问 |
### 后端
| 技术 | 选型 | 选择理由 |
|------|------|---------|
| 语言 | Go | 高并发 goroutine 模型,适合长连接管理 |
| HTTP 框架 | Gin | 高性能 HTTP 路由,中间件生态成熟 |
| WebSocket | gorilla/websocket | Go 生态最成熟的 WebSocket 库 |
| 会话存储 | Redis已实现 / Memory默认 | 高速 KV 存储Memory 为默认实现Redis 已实现可通过配置切换 |
| 持久化存储 | PostgreSQL已实现 | 对话历史、用户数据、会话持久化。MemoryManager 支持 Write-Through 到 PG |
| 配置管理 | Viper + godotenv | 支持 YAML + .env + 环境变量覆盖,详见 `03-接口文档.md` 第六章 |
| 日志 | Zap | 高性能结构化日志 |
### AI 服务
| 能力 | 主选方案 | 备选方案 | 选型考量 |
|------|---------|---------|---------|
| 多模态 LLM | GPT-4o默认 | 通义千问等 OpenAI 兼容模型 | 通过 OpenAI 兼容接口,可灵活切换 |
| 语音识别 STT | Deepgram默认 | MiMo ASR小米 | 支持多 provider 切换 |
| 语音合成 TTS | OpenAI TTS默认 | MiMo TTS小米 | 支持多 provider 切换 |
> 不必绑定单一厂商。Go 网关的 AI 服务层统一封装不同服务商的调用接口,通过配置切换 provider。
## 核心交互流程
一次完整的"用户提问 → AI 回答"流程:
```
Browser Go Gateway STT LLM TTS
| | | | |
|-- VAD 检测到语音结束 --->| | | |
| | | | |
|-- [音频+图像] -------->| | | |
| |--- 音频流 ------->| | |
| |<-- 流式文本 ------| | |
| | | | |
| |--- [图像+文本+上下文] -------->| |
| |<-- 流式回答文本 --------------| |
|<-- 推送回答文本 --------| | | |
| |--- 回答文本 ---------------------------->|
| |<-- 流式音频 --------------------------------|
|<-- 推送音频流 ----------| | | |
| | | | |
|-> 播放音频 + 渲染文字 | | | |
```
**关键优化**LLM 文本流和 TTS 音频流是**并行推送**的——客户端先展示文字,同时开始播放语音,用户感知延迟大幅降低。
## 后端模块
| 模块 | 职责 | 关键实现 | 状态 |
|------|------|---------|------|
| WebSocket Handler | 管理客户端连接生命周期,单播消息推送 | goroutine per connectionJWT 认证conversation_id 恢复 | ✅ 已完成 |
| Session Manager | 维护用户会话状态、对话历史 | Memory默认/ Redis可切换30 分钟 TTLWrite-Through 到 PG详见 `03-接口文档.md` 第五章) | ✅ 已完成 |
| AI Orchestrator | 编排 STT→LLM→TTS 流式并行管道 | context 取消 + 超时控制 + 句子切分 | ✅ 已完成 |
| AI Service Layer | AI 服务抽象层STT/LLM/TTS | 多 provider 支持Deepgram/MiMo/OpenAI 等) | ✅ 已完成 |
| Auth | 用户认证与授权 | JWT (HS256) 双 token 轮转bcrypt 密码哈希Gin 中间件 | ✅ 已完成 |
| Store | 持久化存储层 | UserRepository / MessageRepository / SessionRepository内存 + PostgreSQL 双实现 | ✅ 已完成 |
| REST API | 健康检查、认证、对话管理端点 | Gin 路由,输入校验,权限校验 | ✅ 已完成 |
| Error Handler | 统一错误码定义与发送 | 错误码枚举 | ✅ 已完成 |
| Logger | 日志初始化封装 | Zap 结构化日志 | ✅ 已完成 |
| Models | 数据模型定义 | WebSocket 消息、会话、配置、用户等 | ✅ 已完成 |
| Migrations | 数据库版本化迁移 | 嵌入式 SQL 文件,自动执行,版本跟踪 | ✅ 已完成 |
| Model Router | 根据请求类型选择 AI 模型 | 规则引擎 + 成本阈值 | 📋 规划中 |
| Rate Limiter | 防止单用户过度消耗 API 额度 | 令牌桶算法 | 📋 规划中 |
AI Orchestrator 核心接口(`internal/orchestrator/orchestrator.go`
```go
// Orchestrator AI 编排器接口。
type Orchestrator interface {
ProcessQuery(ctx context.Context, sessionID string, req models.WsQuery,
history []models.Message, sender Sender) error
}
```
Pipeline 实现(`internal/orchestrator/pipeline.go`)流程:
1. Base64 解码音频/图片
2. 调用 `stt.Recognize()` → 发送 `stt_result`
3. 调用 `llm.ChatStream()` 获取流式输出goroutine 消费 token → 发送 `llm_chunk` + 句子切分
4. 另一 goroutine 从句子 channel 读取 → 调用 `tts.SynthesizeStream()` → 发送 `tts_audio`
5. 流结束 → 发送 `llm_done`
6. TTS 失败静默跳过STT/LLM 失败发送对应 error 消息
> **关键优化**LLM 文本流和 TTS 音频流**并行推送**——客户端先逐 token 展示文字,同时 TTS 逐句子合成并推送音频,用户感知延迟大幅降低。详细的 AI 服务层接口和编排策略见 `03-接口文档.md` 第三、四章。
## 前端组件
| 组件 | 职责 |
|------|------|
| AuthPage | 登录/注册表单前端校验Tab 切换 |
| CameraManager | 摄像头流采集 |
| MicManager | 麦克风音频采集 |
| EdgeProcessor | VAD + 关键帧检测Canvas 像素比较) |
| WebSocketManager | WS 连接生命周期管理 |
| ChatPanel | 消息展示、流式回复、文本输入、场景选择 |
| VideoPreview | 摄像头画面预览 |
| SessionSidebar | 左侧抽屉式对话列表(搜索、重命名、删除) |
| ConfigPanel | 右侧抽屉式配置面板主题、TTS 开关、detail level、语言、场景、账户 |
| Toast | 轻量通知提示3 秒自动消失) |
核心 Hook`useVisionSession()` 封装一次完整的视觉对话会话摄像头、VAD、WebSocket、消息状态、认证、场景模式
```typescript
// useVisionSession 核心职责(简化示意)
function useVisionSession() {
// 组合useCamera + useMicrophone + useVAD + useWebSocketManager + useObservationMode
// 管理:消息状态、流式回复、处理标志、配置、统计、模式
// VAD onSpeechEnd: 捕获帧 + 音频 → 发送 query 消息
// 服务端消息处理stt_result / llm_chunk / llm_done / tts_audio / error
// 文本输入sendTextMessage() 支持手动输入文字(跳过 STT
// 场景模式config 消息支持 scenario 字段free_chat / interviewer / english_teacher 等)
// 打断interrupt() 发送中断消息 + 停止 TTS + 保存部分回复
// 认证WebSocket 连接携带 JWT token支持 conversation_id 恢复历史对话
}
```
## 存储策略(分阶段)
| 阶段 | 存储方案 | 持久化内容 | 理由 |
|------|---------|-----------|------|
| 当前默认 | Memory进程内 | 会话状态 + 对话历史 | 零依赖快速启动。MemoryManager 支持 Write-Through 到 PG |
| 已实现 | Memory + PostgreSQL | 用户数据、对话历史、会话元数据 | 通过 `storage.driver: postgres` 启用MemoryManager 注入 PG Repository |
| 已实现 | Redis独立 | 会话状态 + 对话历史 | 通过配置切换到 RedisManager适合多实例部署 |
冷热分离Redis/Memory 存"热数据"当前对话上下文微秒级读写PostgreSQL 存"冷数据"历史记录。MemoryManager 的 Write-Through 机制确保每次 AppendMessage 同时写入 PG重启后可从 PG 恢复会话。
### PostgreSQL 表设计(已实现)
实际迁移文件位于 `backend/migrations/`,通过 `go:embed` 嵌入,启动时自动执行:
```sql
-- 001_users.up.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(64) NOT NULL UNIQUE,
password_hash VARCHAR(256) NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(256) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 002_messages.up.sql
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
session_id UUID NOT NULL,
role VARCHAR(16) NOT NULL,
content TEXT NOT NULL,
tokens_used INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 003_sessions.up.sql
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(128) DEFAULT '新对话',
config JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
```
## 部署架构
```
用户浏览器
Nginx同源反代 + 负载均衡)
├── / → 前端静态资源CDN 或本地 dist
├── /api/* → Go GatewayREST API
└── /ws → Go GatewayWebSocket
├── Gateway-1 ──→ Redis
├── Gateway-2 ──→ Redis
└── Gateway-N ──→ AI Services外部 API
```
**跨域策略**Nginx 将前端和后端统一到同一域名下,浏览器无跨域问题。
### Nginx 配置
```nginx
server {
listen 80;
server_name camtalk.example.com;
# 前端静态资源
location / {
root /var/www/camtalk/dist;
try_files $uri $uri/ /index.html;
}
# REST API 反代
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# WebSocket 反代
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s; # 长连接超时 24h
proxy_send_timeout 86400s;
}
}
```
> WebSocket 是长连接Nginx 必须配置 `Upgrade` 和 `Connection` 头。`proxy_read_timeout` 需要覆盖心跳间隔(客户端 30s ping否则 Nginx 会主动断开空闲连接。
### 开发环境
开发时前端Vite :5173和后端Gin :8080不同端口。前端 WebSocket 地址基于 `window.location.host` 动态构建,通过 Vite `server.proxy` 转发到后端,无需硬编码端口。
`vite.config.ts` 中配置了 `/ws`WebSocket`/api`REST的代理目标为 `http://localhost:8080`