# 系统架构 ## 概述 三层架构:**前端做轻量预处理,后端做智能编排,云端 AI 服务按需调用**。在保证交互体验的同时控制成本。 ## 三层架构 | 层级 | 职责 | 关键约束 | |------|------|---------| | **客户端(浏览器)** | 媒体采集、边缘预处理、UI 渲染 | 浏览器资源有限,模型需轻量 | | **Go 网关** | 会话管理、模型路由、AI 服务编排 | 高并发、低延迟、状态管理 | | **AI 服务** | LLM 推理、语音识别、语音合成 | 按量计费,需控制调用频率 | > 为什么要单独加一层 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 | 模型路由时的复杂度判断 | > 不必绑定单一厂商。Go 网关的模型路由层统一封装不同 AI 服务的调用接口,按场景动态切换。 ## 核心交互流程 一次完整的"用户提问 → AI 回答"流程: ``` Browser Go Gateway STT LLM TTS | | | | | |-- VAD 检测到语音结束 --->| | | | | | | | | |-- [音频+图像] -------->| | | | | |--- 音频流 ------->| | | | |<-- 流式文本 ------| | | | | | | | | |--- [图像+文本+上下文] -------->| | | |<-- 流式回答文本 --------------| | |<-- 推送回答文本 --------| | | | | |--- 回答文本 ---------------------------->| | |<-- 流式音频 --------------------------------| |<-- 推送音频流 ----------| | | | | | | | | |-> 播放音频 + 渲染文字 | | | | ``` **关键优化**:LLM 文本流和 TTS 音频流是**并行推送**的——客户端先展示文字,同时开始播放语音,用户感知延迟大幅降低。 ## 后端模块 | 模块 | 职责 | 关键实现 | |------|------|---------| | WebSocket Hub | 管理所有客户端连接,广播/定向推送 | goroutine per connection | | Session Manager | 维护用户会话状态、对话历史 | Redis + TTL 过期策略 | | Model Router | 根据请求类型选择 AI 模型 | 规则引擎 + 成本阈值 | | AI Orchestrator | 编排多路 AI 调用(并行/串行) | context 取消 + 超时控制 | | Rate Limiter | 防止单用户过度消耗 API 额度 | 令牌桶算法 | AI Orchestrator 核心代码: ```go 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 } ``` ## 前端组件 | 组件 | 职责 | |------|------| | CameraManager | 摄像头流采集 | | MicManager | 麦克风音频采集 | | EdgeProcessor | VAD + 关键帧检测(ONNX Runtime) | | WebSocketManager | WS 连接生命周期管理 | | ChatPanel | 消息展示 | | VideoPreview | 摄像头画面预览 | 核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话(摄像头、VAD、WebSocket、消息状态)。 ```typescript function useVisionSession() { const [messages, setMessages] = useState([]); const wsRef = useWebSocket("ws://localhost:8080/ws"); const videoRef = useRef(null); const { captureFrame } = useCamera(videoRef); 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) })); } }); 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 }; } ``` ## 存储策略(分阶段) | 阶段 | 存储方案 | 持久化内容 | 理由 | |------|---------|-----------|------| | MVP | Redis only | 无 | 快速验证核心功能,重启丢数据可接受 | | 上线 | Redis + PostgreSQL | 对话历史、用户偏好、用量统计 | 用户需要查看历史,运营需要成本数据 | | 规模化 | Redis + PG + 对象存储 | 图像帧、音频片段归档 | 大文件不适合存关系库 | 冷热分离:Redis 存"热数据"(当前对话上下文,微秒级读写),PostgreSQL 存"冷数据"(历史记录)。 ### 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) ); ``` ## 部署架构 ``` CDN(静态资源) ← 用户浏览器 Nginx 负载均衡(sticky session for WebSocket) ├── Gateway-1 ──→ Redis ├── Gateway-2 ──→ Redis └── Gateway-N ──→ AI Services(外部 API) ``` WebSocket 是长连接,Nginx 需要配置 `proxy_set_header Upgrade` 和 sticky session,确保同一用户的请求始终路由到同一个 Gateway 实例。