18 KiB
架构设计
项目概述
CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头和麦克风与 AI 交互,AI 理解视觉场景和语音输入后,以文字和语音形式给出自然回应。
核心挑战在于三个维度之间的张力:
| 维度 | 关键问题 |
|---|---|
| 视觉理解 | 如何准确理解摄像头画面中的人物、物体、场景? |
| 语音交互 | 如何让对话像真人交流一样自然、低延迟? |
| 成本控制 | 实时视频流 + LLM 推理,如何避免账单爆炸? |
系统架构
三层架构:前端做轻量预处理,后端做智能编排,云端 AI 服务按需调用。
graph TB
subgraph Browser["浏览器客户端"]
UI["UI 渲染层<br/>React 18 + TypeScript"]
Edge["边缘预处理层<br/>VAD / 关键帧检测"]
Media["媒体采集层<br/>Camera / Microphone"]
end
subgraph Gateway["Go 网关"]
WS["WebSocket Handler<br/>连接管理 / 消息分发"]
Session["Session Manager<br/>会话状态 / 对话历史"]
Orch["AI Orchestrator<br/>Eino Graph 声明式编排"]
Auth["Auth 模块<br/>JWT / bcrypt"]
REST["REST API<br/>健康检查 / 对话管理"]
Store["Store 层<br/>Repository 接口"]
end
subgraph AI["云端 AI 服务"]
STT["STT<br/>Deepgram / MiMo ASR"]
LLM["LLM<br/>GPT-4o / 通义千问"]
TTS["TTS<br/>OpenAI TTS / MiMo TTS"]
end
subgraph Storage["存储层"]
Mem["Memory<br/>进程内缓存"]
Redis["Redis<br/>会话状态"]
PG["PostgreSQL<br/>持久化存储"]
end
Media --> Edge
Edge -->|"query (image+audio)"| WS
UI <-->|"WebSocket"| WS
WS --> Session
WS --> Orch
Orch --> STT
Orch --> LLM
Orch --> TTS
Session --> Store
Store --> Mem
Store --> Redis
Store --> PG
REST --> Session
WS --> Auth
为什么单独加一层 Go 网关,而不是让前端直连 AI API?1)API Key 安全性;2)统一的速率限制和成本管控;3)多模型路由逻辑集中在一处便于维护。
核心交互流程
一次完整的"用户提问 → AI 回答"流程:
sequenceDiagram
participant B as 浏览器
participant G as Go 网关(Eino Graph)
participant S as STT
participant L as LLM(ChatModel)
participant T as TTS
B->>B: VAD 检测到语音结束
B->>G: query {image, audio}
Note over G: EinoOrchestrator 启动 Graph.Stream()
G->>S: STT Lambda:音频 → 文本
S-->>G: 识别文本
G-->>B: stt_result {text}
G->>G: History Lambda:组装提示词 + 历史 + 多模态消息
G->>L: ChatModel Node:流式推理
loop LLM 流式输出(Callback OnEndWithStreamOutput)
L-->>G: token delta
G-->>B: llm_chunk {delta}
end
G->>G: Msg2Str + Splitter Lambda:句子切分
G->>T: TTS Lambda:逐句合成
T-->>G: 音频 chunk
G-->>B: tts_audio {audio}
G->>G: Done Lambda:发送完成通知
G-->>B: llm_done {full_text, tokens}
G-->>B: tts_audio {final: true}
关键优化:Eino Graph 以 Stream 模式运行,ChatModel 的 token 流通过 Callback 的 OnEndWithStreamOutput 实时推送到客户端(llm_chunk),同时 Splitter 节点将 token 流切分为句子,TTS 节点逐句合成并推送音频。LLM 文本流和 TTS 音频流并行推送,用户感知延迟大幅降低。
技术栈
前端
| 技术 | 选型 | 选择理由 |
|---|---|---|
| 框架 | React 18 + TypeScript | 组件化开发,类型安全,生态成熟 |
| 构建 | Vite | 开发热更新快,构建产物小 |
| 实时通信 | WebSocket(原生 API) + 自封装连接管理 | 浏览器原生支持,封装心跳/重连/消息分发 |
| 语音检测 | @ricky0123/vad-web | 基于 WebRTC VAD,纯前端零延迟 |
| 媒体采集 | MediaDevices API | 浏览器原生摄像头/麦克风访问 |
后端
| 技术 | 选型 | 选择理由 |
|---|---|---|
| 语言 | Go | 高并发 goroutine 模型,适合长连接管理 |
| HTTP 框架 | Gin | 高性能 HTTP 路由,中间件生态成熟 |
| WebSocket | gorilla/websocket | Go 生态最成熟的 WebSocket 库 |
| 会话存储 | Memory / Redis / PostgreSQL 三级存储 | 进程内存零依赖,Redis 支持多实例,PG 持久化。TieredManager 自动降级 |
| AI 编排 | CloudWeGo Eino Graph | 声明式 DAG 编排,Stream 模式,Callback AOP |
| 持久化存储 | PostgreSQL | 对话历史、用户数据、会话元数据 |
| 配置管理 | Viper + godotenv | 支持 YAML + .env + 环境变量覆盖 |
| 日志 | Zap | 高性能结构化日志 |
AI 服务
| 能力 | 默认方案 | 备选方案 |
|---|---|---|
| 多模态 LLM | DashScope qwen3-vl-plus | GPT-4o 等 OpenAI 兼容模型 |
| 语音识别 STT | MiMo ASR(小米) | Deepgram |
| 语音合成 TTS | MiMo TTS(小米) | OpenAI TTS |
Go 网关的 AI 服务层统一封装不同服务商的调用接口,通过配置切换 provider。LLM 通过 Eino 框架的
eino-ext/components/model/openai组件接入,支持任何 OpenAI 兼容接口。
后端模块
graph LR
subgraph Entry["入口层"]
Main["main.go<br/>依赖注入 / 启动"]
end
subgraph Transport["传输层"]
WSH["WebSocket Handler<br/>连接管理 / 认证"]
APH["REST API Handlers<br/>Auth / Conversation / Health"]
end
subgraph Business["业务层"]
SM["Session Manager<br/>会话生命周期"]
ORCH["EinoOrchestrator<br/>Eino Graph 编排"]
AS["Auth Service<br/>注册/登录/刷新/登出"]
end
subgraph Eino_Layer["Eino 编排层"]
PG["PipelineGraph<br/>7 节点 DAG"]
CB["Callback Handler<br/>LLM token 推送"]
ST["PipelineState<br/>跨节点状态"]
end
subgraph AI_Layer["AI 服务层"]
STT_S["STT Service<br/>MiMo / Deepgram"]
LLM_S["ChatModel<br/>eino-ext OpenAI 兼容"]
TTS_S["TTS Service<br/>MiMo / OpenAI"]
end
subgraph Data["数据层"]
UR["UserRepository"]
MR["MessageRepository"]
SR["SessionRepository"]
end
Main --> WSH
Main --> APH
Main --> SM
Main --> ORCH
Main --> AS
WSH --> SM
WSH --> ORCH
APH --> SM
APH --> AS
ORCH --> PG
PG --> CB
PG --> ST
PG --> STT_S
PG --> LLM_S
PG --> TTS_S
SM --> MR
SM --> SR
AS --> UR
| 模块 | 职责 |
|---|---|
| WebSocket Handler | 管理客户端连接生命周期,JWT 认证,conversation_id 恢复,单播消息推送 |
| Session Manager | 维护用户会话状态、对话历史。三级存储(Memory → Redis → PostgreSQL),30 分钟 TTL,Write-Through 到 PG |
| Eino 编排层 | 基于 CloudWeGo Eino Graph 的声明式 AI 编排。7 节点 DAG(STT→History→ChatModel→Msg2Str→Splitter→TTS→Done),Stream 模式调用,Callback 实现 LLM token 实时推送 |
| AI Orchestrator | EinoOrchestrator 适配器,包装 Eino Graph 实现 Orchestrator 接口。context 取消 + 超时控制 |
| AI Service Layer | AI 服务抽象层,多 provider 支持(Deepgram/MiMo/OpenAI 等) |
| Auth | 用户认证与授权。JWT (HS256) 双 token 轮转,bcrypt 密码哈希,Gin 中间件 |
| Store | 持久化存储层。UserRepository / MessageRepository / SessionRepository,内存 + PostgreSQL 双实现 |
| REST API | 健康检查、认证、对话管理端点 |
| Logger | Zap 结构化日志 |
| Models | 数据模型定义 |
| Migrations | 数据库版本化迁移,嵌入式 SQL 文件自动执行 |
| Model Router | 根据请求类型选择 AI 模型(待实现) |
| Rate Limiter | 令牌桶限流。详细设计见 令牌桶限流设计 |
前端组件
| 组件 | 职责 |
|---|---|
| LandingPage | 未登录时的着陆页(营销展示),内嵌 LoginModal 登录/注册弹窗 |
| AuthPage | 登录/注册表单(备用,已被 LandingPage + LoginModal 替代) |
| CameraManager | 摄像头流采集 |
| MicManager | 麦克风音频采集 |
| EdgeProcessor | VAD + 关键帧检测(Canvas 像素比较) |
| WebSocketManager | WS 连接生命周期管理 |
| ChatPanel | 消息展示、流式回复、文本输入、场景选择 |
| VideoPreview | 摄像头画面预览 |
| SessionSidebar | 左侧抽屉式对话列表(搜索、重命名、删除、时间分组) |
| ConfigPanel | 右侧抽屉式配置面板(主题、TTS 开关、detail level、语言、场景、登出) |
| Toast | 轻量通知提示(3 秒自动消失) |
核心 Hook:useVisionSession() 封装一次完整的视觉对话会话(摄像头、VAD、WebSocket、消息状态、认证、场景模式)。useSessionList() 通过 REST API 管理对话列表 CRUD(列表、创建、删除、重命名、加载消息)。
前端会话状态模型(三态)
前端 UI 存在三个会话状态,由 isConnected 和 isCameraOn 联合决定:
┌──────────┐ startSession() ┌──────────┐
│ initial │ ──────────────────→ │ video │
│ 初始态 │ │ 视频通话 │
└──────────┘ └──────────┘
↑ │
│ stopSession() stopVideo()
│ │
│ ▼
│ ┌──────────┐
└──────────────────────── │ textOnly │
│ 文字对话 │
└──────────┘
│
startSession()
│
▼
┌──────────┐
│ video │
└──────────┘
| 状态 | 条件 | WebSocket | 摄像头 | 消息 | 文字输入 |
|---|---|---|---|---|---|
initial |
!isConnected && messages.length === 0 |
断开 | 关闭 | 空 | 可用(自动连接) |
video |
isConnected && isCameraOn |
连接 | 开启 | 有 | 可用 |
textOnly |
isConnected && !isCameraOn |
连接 | 关闭 | 保留 | 可用 |
stopVideo():停止摄像头/麦克风/VAD,保持 WebSocket 连接和消息历史,用户可继续文字对话stopSession():完全断开 WebSocket、清空消息、重置状态,回到初始态
数据库设计
ER 关系
erDiagram
users ||--o{ sessions : "1:N"
users ||--o{ refresh_tokens : "1:N"
sessions ||--o{ messages : "1:N"
users {
uuid id PK
varchar username UK
varchar password_hash
timestamptz created_at
timestamptz updated_at
}
sessions {
uuid id PK
uuid user_id FK
varchar title
jsonb config
timestamptz created_at
timestamptz updated_at
}
messages {
bigserial id PK
uuid session_id FK
varchar role
text content
integer tokens_used
timestamptz created_at
}
refresh_tokens {
bigserial id PK
uuid user_id FK
varchar token_hash UK
timestamptz expires_at
timestamptz created_at
}
表结构
-- 用户表
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 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()
);
-- 消息表
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
role VARCHAR(16) NOT NULL,
content TEXT NOT NULL,
tokens_used INTEGER DEFAULT 0,
created_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()
);
存储策略
| 场景 | 存储方案 | 说明 |
|---|---|---|
| 默认 | Memory(进程内) | 零依赖,快速启动。MemoryManager 支持 Write-Through 到 PG |
| 持久化 | Memory + PostgreSQL | 通过 storage.persistence.enabled: true 启用,MemoryManager 注入 PG Repository |
| 多实例 | Redis(独立) | 通过配置切换到 RedisManager,适合多实例部署 |
| 三级存储 | TieredManager | L1 Memory → L2 Redis → L3 PostgreSQL,自动降级 |
三级存储架构(TieredManager):
TieredManager
├── L1: Memory(进程内缓存,微秒级读写)
├── L2: Redis(分布式缓存,毫秒级读写)
└── L3: PostgreSQL(持久化存储,冷数据)
- 读取路径:L1 → L2 → L3,逐级回源,命中后向上回填
- 写入路径:L1 → L2(同步) → L3(异步)
- 健康检查:后台 goroutine 每 30 秒 ping Redis,故障时自动降级为 L1+L3 模式
- 冷热分离:L1/L2 存"热数据"(当前对话上下文),L3 存"冷数据"(历史记录)
认证设计
采用 JWT 双 token 轮转认证机制,结合 bcrypt 密码哈希和 Refresh Token Rotation 安全策略。详细设计见 鉴权体系设计。
核心组件
| 组件 | 职责 |
|---|---|
| TokenManager | JWT 生成与验证(HS256 算法) |
| AuthService | 认证业务逻辑(注册/登录/刷新/登出) |
| AuthMiddleware | Gin 中间件,校验 access_token 并注入用户信息 |
| PasswordUtil | bcrypt 密码哈希(cost=10) |
认证流程
sequenceDiagram
participant C as 客户端
participant G as Go 网关
participant DB as PostgreSQL
Note over C,DB: 注册流程
C->>G: POST /api/auth/register {username, password}
G->>G: bcrypt hash 密码
G->>DB: INSERT users
G->>G: 生成 access_token + refresh_token
G->>DB: 存 SHA256(refresh_token)
G-->>C: {user, access_token, refresh_token}
Note over C,DB: 登录流程
C->>G: POST /api/auth/login {username, password}
G->>DB: 查 users by username
G->>G: bcrypt.CompareHashAndPassword
G->>G: 生成 token pair
G->>DB: 存 SHA256(refresh_token)
G-->>C: {user, access_token, refresh_token}
Note over C,DB: Token 刷新(轮转)
C->>G: POST /api/auth/refresh {refresh_token}
G->>G: 校验签名和过期
G->>DB: 验证 hash 存在
G->>DB: 撤销旧 refresh_token
G->>G: 生成新 token pair
G->>DB: 存新 refresh_token hash
G-->>C: {access_token, refresh_token}
Token 策略
- access_token:15 分钟有效,用于 API 认证和 WebSocket 连接
- refresh_token:7 天有效,用于刷新 access_token
- Refresh Token Rotation:每次 refresh 都生成新的 token pair,旧 refresh_token 立即失效
- 复用检测:检测到已删除的 refresh_token 被复用时,吊销该用户的所有 refresh_token
安全机制
- 密码安全:bcrypt 算法(cost=10),自动生成盐值,防彩虹表攻击
- Token 安全:
- access_token 短有效期(15 分钟),降低泄露风险
- refresh_token 使用 SHA256 哈希存储,不存储原始 token
- Refresh Token Rotation 防重放攻击
- 复用检测 + 自动吊销机制
- 传输安全:HTTPS 强制,CORS 限制,HttpOnly Cookie 存储 refresh_token
- 防攻击策略:
- 防暴力破解:可选速率限制
- 防枚举攻击:统一错误信息
- 防 Token 泄露:复用检测 + 自动吊销
WebSocket 认证
连接地址:ws://host/ws?token=<access_token>&conversation_id=<uuid>
- HTTP Upgrade 前校验 token
- 校验失败返回 401 Unauthorized
- 校验成功后,user_id 和 username 注入到连接上下文
配置
auth:
jwt_secret: "" # JWT 签名密钥(必须通过 CAMTALK_AUTH_JWT_SECRET 环境变量设置)
access_ttl: 15 # access_token 有效期(分钟)
refresh_ttl: 10080 # refresh_token 有效期(分钟,7天)
安全要求:
JWT_SECRET必须通过环境变量设置,不能写入配置文件。生产环境使用openssl rand -hex 32生成随机密钥。
部署架构
graph TB
User["用户浏览器"] --> Nginx
subgraph Nginx["Nginx 反向代理"]
Static["/ → 前端静态资源"]
API["/api/* → Go Gateway"]
WS_Proxy["/ws → Go Gateway"]
end
subgraph Gateway_Pool["Go Gateway 实例"]
G1["Gateway-1"]
G2["Gateway-2"]
GN["Gateway-N"]
end
Nginx --> G1
Nginx --> G2
Nginx --> GN
G1 --> Redis
G2 --> Redis
GN --> Redis
G1 --> PG_DB["PostgreSQL"]
G2 --> PG_DB
GN --> PG_DB
G1 --> AI_Services["AI Services(外部 API)"]
G2 --> AI_Services
GN --> AI_Services
跨域策略:Nginx 将前端(/)、REST API(/api/*)、WebSocket(/ws)统一反代到同一域名,浏览器无跨域问题。
开发环境:前端 Vite :5173 通过 server.proxy 转发 /ws 和 /api 到后端 :8080,无需硬编码端口。