- 移除 Obsidian 特有语法(callout、wikilinks、YAML frontmatter) - 目录结构从 3 层嵌套扁平化为编号文件(01~09) - 新增 docs/README.md 文档索引与推荐阅读顺序 - 精简冗余解释,保留所有代码块和实现参考 - CLAUDE.md 全面中文化
8.0 KiB
8.0 KiB
系统架构
概述
三层架构:前端做轻量预处理,后端做智能编排,云端 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 核心代码:
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、消息状态)。
function useVisionSession() {
const [messages, setMessages] = useState<Message[]>([]);
const wsRef = useWebSocket("ws://localhost:8080/ws");
const videoRef = useRef<HTMLVideoElement>(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 表设计
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 实例。