8.3 KiB
8.3 KiB
tags, create time
| tags | create time | ||||||
|---|---|---|---|---|---|---|---|
|
2026-06-12 14:32 |
项目架构与技术栈
概述
本文档设计 AI 视觉对话助手的分层架构与技术选型。核心设计原则:前端做轻量预处理,后端做智能编排,云端 AI 服务按需调用——在保证交互体验的同时控制成本。
正文
整体架构
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 存储,适合会话状态和上下文缓存 |
| 配置管理 | 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 回答"流程:
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 网关的核心模块:
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 后端核心代码结构:
// 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
}
前端架构设计
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 设计:
// 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 };
}
部署架构
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 实例。