Files
CamTalk/docs/02-系统架构.md
hhs 0cafe94f3a docs: 补充跨域处理方案(Nginx 同源反代 + Vite proxy)
- 02-系统架构: 部署架构改为 Nginx 同源反代模型,新增完整 Nginx 配置和 Vite proxy 配置
- 03-接口文档: 连接管理新增跨域处理小节(生产 Nginx / 开发 Vite proxy / CheckOrigin 策略)
2026-06-13 14:05:57 +08:00

11 KiB
Raw Blame 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 模型,适合长连接管理
WebSocket gorilla/websocket Go 生态最成熟的 WebSocket 库
会话存储 Redis 高速 KV 存储,适合会话状态和上下文缓存
持久化存储 PostgreSQL 对话历史、用量统计、用户偏好MVP 阶段可选)
配置管理 Viper 支持 YAML + 环境变量覆盖,详见 03-接口文档.md 第六章
日志 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 Hash + List30 分钟 TTL详见 03-接口文档.md 第五章)
Model Router 根据请求类型选择 AI 模型 规则引擎 + 成本阈值
AI Orchestrator 编排多路 AI 调用(并行/串行) context 取消 + 超时控制
Rate Limiter 防止单用户过度消耗 API 额度 令牌桶算法

AI Orchestrator 核心代码(句子级流式并行):

func (o *Orchestrator) ProcessQuery(ctx context.Context, client MessageSender, req *QueryRequest) {
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    // Step 1: STT — 识别用户语音(串行)
    text, err := o.stt.Recognize(ctx, req.Audio, STTOptions{...})
    if err != nil {
        client.SendError(req.RequestID, "STT_ERROR", err.Error())
        return
    }
    client.SendSTTResult(req.RequestID, text, true)

    // Step 2: LLM 流式输出 + 句子切分(并行)
    llmStream, _ := o.llm.ChatStream(ctx, LLMRequest{Image: req.Image, Text: text, ...})
    sentenceCh := make(chan string, 4)
    go func() {
        defer close(sentenceCh)
        var buf strings.Builder
        for chunk := range llmStream {
            client.SendLLMChunk(req.RequestID, chunk.Delta) // 逐 token 推送文字
            buf.WriteString(chunk.Delta)
            if isSentenceEnd(chunk.Delta) { // 按 。!?\n 切分
                sentenceCh <- buf.String()
                buf.Reset()
            }
        }
        if buf.Len() > 0 { sentenceCh <- buf.String() }
    }()

    // Step 3: TTS 并行消费句子流
    ttsStream, _ := o.tts.SynthesizeStream(ctx, sentenceCh, TTSOptions{...})
    for chunk := range ttsStream {
        client.SendTTSAudio(req.RequestID, chunk.Audio, chunk.IsLast)
    }
}

关键优化LLM 文本流和 TTS 音频流并行推送——客户端先逐 token 展示文字,同时 TTS 逐句子合成并推送音频,用户感知延迟大幅降低。详细的 AI 服务层接口和编排策略见 03-接口文档.md 第三、四章。

前端组件

组件 职责
CameraManager 摄像头流采集
MicManager 麦克风音频采集
EdgeProcessor VAD + 关键帧检测ONNX Runtime
WebSocketManager WS 连接生命周期管理
ChatPanel 消息展示
VideoPreview 摄像头画面预览

核心 HookuseVisionSession() 封装一次完整的视觉对话会话摄像头、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)
);

部署架构

用户浏览器
    ↓
Nginx同源反代 + 负载均衡)
  ├── /            → 前端静态资源CDN 或本地 dist
  ├── /api/*       → Go GatewayREST API
  └── /ws          → Go GatewayWebSocket
        ├── Gateway-1 ──→ Redis
        ├── Gateway-2 ──→ Redis
        └── Gateway-N ──→ AI Services外部 API

跨域策略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 必须配置 UpgradeConnection 头。proxy_read_timeout 需要覆盖心跳间隔(客户端 30s ping否则 Nginx 会主动断开空闲连接。

开发环境Vite proxy

开发时前端Vite :5173和后端Gin :8080不同端口用 Vite 内置代理解决跨域:

// frontend/vite.config.ts
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": "http://localhost:8080",
      "/ws": {
        target: "ws://localhost:8080",
        ws: true,
      },
    },
  },
});

前端代码中 WebSocket 地址改为相对路径 ws://localhost:5173/wsVite 自动代理到后端。部署时 Nginx 同理,前端无需区分开发/生产地址。