docs: 扁平化目录结构,优化文档为 Coding Agent 可读格式
- 移除 Obsidian 特有语法(callout、wikilinks、YAML frontmatter) - 目录结构从 3 层嵌套扁平化为编号文件(01~09) - 新增 docs/README.md 文档索引与推荐阅读顺序 - 精简冗余解释,保留所有代码块和实现参考 - CLAUDE.md 全面中文化
This commit is contained in:
136
CLAUDE.md
136
CLAUDE.md
@@ -1,106 +1,104 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
本文件为 Claude Code (claude.ai/code) 在本仓库中工作时提供指引。
|
||||||
|
|
||||||
## Project Overview
|
## 项目概述
|
||||||
|
|
||||||
CamTalk is a multimodal real-time AI visual dialogue assistant. Users interact via camera and microphone — the app captures visual scenes and voice input, sends them to AI services, and responds with both text and speech. The project is currently in the design-document phase; source code is being built incrementally.
|
CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头和麦克风与 AI 交互,AI 理解视觉场景和语音输入后,以文字和语音形式给出自然回应。项目目前处于设计文档阶段,源代码正在逐步构建。
|
||||||
|
|
||||||
> **文档优先原则:** 执行任何开发任务前,先读取 `docs/` 下的相关设计文档(架构、接口、技术选型等),以文档为最高依据。代码实现应与文档一致;若有偏差,优先更新文档(尤其是接口文档)。
|
> **文档优先原则:** 执行任何开发任务前,先读取 `docs/` 下的相关设计文档(架构、接口、技术选型等),以文档为最高依据。代码实现应与文档一致;若有偏差,优先更新文档(尤其是接口文档)。
|
||||||
|
|
||||||
**Design docs (Chinese):** `docs/` contains the full architecture, API contracts, user stories, cost control strategies, and technology selection rationale.
|
## 架构
|
||||||
|
|
||||||
## Architecture
|
三层系统:
|
||||||
|
|
||||||
Three-layer system:
|
1. **浏览器客户端**(React 18 + TypeScript, Vite)—— 媒体采集、边缘预处理(VAD 通过 `@ricky0123/vad-web`、关键帧检测通过 ONNX Runtime Web)、UI 渲染。核心 Hook:`useVisionSession()`
|
||||||
|
2. **Go 网关**(gorilla/websocket, Redis, Viper, Zap)—— WebSocket 服务器、会话管理、模型路由、AI 编排、速率限制。每个 WebSocket 连接一个 goroutine。
|
||||||
|
3. **云端 AI 服务** —— GPT-4o(LLM)、Deepgram(STT)、OpenAI TTS。仅通过 Go 网关访问,浏览器不直连。
|
||||||
|
|
||||||
1. **Browser Client** (React 18 + TypeScript, Vite) — media capture, edge preprocessing (VAD via `@ricky0123/vad-web`, keyframe detection via ONNX Runtime Web), UI rendering. Core hook: `useVisionSession()`.
|
**关键模式**:LLM 文本流和 TTS 音频流并行推送给客户端,以最小化感知延迟。
|
||||||
2. **Go Gateway** (gorilla/websocket, Redis, Viper, Zap) — WebSocket server, session management, model routing, AI orchestration, rate limiting. One goroutine per WebSocket connection.
|
|
||||||
3. **Cloud AI Services** — GPT-4o (LLM), Deepgram (STT), OpenAI TTS. Accessed only through the Go gateway, never directly from the browser.
|
|
||||||
|
|
||||||
**Key pattern:** LLM text chunks and TTS audio are streamed in parallel to the client to minimize perceived latency.
|
**存储**:冷热分离 —— Redis 存实时会话状态,PostgreSQL 存对话历史和用量统计(MVP 后引入)。Repository 接口模式(`HistoryRepository`、`UsageRepository`),MVP 用内存实现。
|
||||||
|
|
||||||
**Storage:** Cold/hot separation — Redis for real-time session state, PostgreSQL for conversation history and usage stats (deferred past MVP). Repository interface pattern (`HistoryRepository`, `UsageRepository`) with in-memory MVP implementations.
|
## 技术栈
|
||||||
|
|
||||||
## Tech Stack
|
| 层级 | 技术 |
|
||||||
|
|------|------|
|
||||||
|
| 前端 | React 18, TypeScript, Vite, ONNX Runtime Web, @ricky0123/vad-web |
|
||||||
|
| 后端 | Go, gorilla/websocket, Redis, Viper, Zap |
|
||||||
|
| LLM | GPT-4o(主), Claude Sonnet(备) |
|
||||||
|
| STT | Deepgram(主), FunASR(自部署备选) |
|
||||||
|
| TTS | OpenAI TTS(主), Edge TTS(免费替代) |
|
||||||
|
| 模型路由 | GPT-4o-mini 用于轻量分类 |
|
||||||
|
|
||||||
| Layer | Tech |
|
## 构建与运行命令
|
||||||
|-------|------|
|
|
||||||
| Frontend | React 18, TypeScript, Vite, ONNX Runtime Web, @ricky0123/vad-web |
|
|
||||||
| Backend | Go, gorilla/websocket, Redis, Viper, Zap |
|
|
||||||
| LLM | GPT-4o (primary), Claude Sonnet (backup) |
|
|
||||||
| STT | Deepgram (primary), FunASR (self-hosted backup) |
|
|
||||||
| TTS | OpenAI TTS (primary), Edge TTS (free alternative) |
|
|
||||||
| Model routing | GPT-4o-mini for lightweight classification |
|
|
||||||
|
|
||||||
## Build & Run Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Frontend
|
# 前端
|
||||||
cd frontend && npm install
|
cd frontend && npm install
|
||||||
npm run dev # Vite dev server
|
npm run dev # Vite 开发服务器
|
||||||
npm run build # Production build
|
npm run build # 生产构建
|
||||||
npm run lint # ESLint
|
npm run lint # ESLint 检查
|
||||||
npm run test # Vitest
|
npm run test # Vitest 测试
|
||||||
|
|
||||||
# Backend
|
# 后端
|
||||||
cd backend && go mod download
|
cd backend && go mod download
|
||||||
go run ./cmd/server # Start gateway on :8080
|
go run ./cmd/server # 启动网关,监听 :8080
|
||||||
go build -o bin/camtalk ./cmd/server
|
go build -o bin/camtalk ./cmd/server
|
||||||
go test ./... # Run all tests
|
go test ./... # 运行所有测试
|
||||||
go test -run TestName ./path # Run single test
|
go test -run TestName ./path # 运行单个测试
|
||||||
go vet ./... # Static analysis
|
go vet ./... # 静态分析
|
||||||
```
|
```
|
||||||
|
|
||||||
Infrastructure: Redis required for session state. PostgreSQL optional for MVP (in-memory fallback).
|
基础设施:Redis 为会话状态必需。PostgreSQL 为 MVP 可选(内存回退)。
|
||||||
|
|
||||||
## WebSocket Protocol
|
## WebSocket 协议
|
||||||
|
|
||||||
Endpoint: `ws://localhost:8080/ws`
|
端点:`ws://localhost:8080/ws`
|
||||||
|
|
||||||
All messages are JSON text frames with `{type, request_id?, timestamp?}` envelope. See `docs/AI 视觉对话助手/项目实现/接口文档.md` for the full contract.
|
所有消息为 JSON 文本帧,统一信封格式 `{type, request_id?, timestamp?}`。完整契约见 `docs/03-接口文档.md`。
|
||||||
|
|
||||||
**Client → Server:** `query` (image Base64 + audio Base64), `config`, `interrupt`, `ping`
|
**客户端 → 服务端**:`query`(图像 Base64 + 音频 Base64)、`config`、`interrupt`、`ping`
|
||||||
**Server → Client:** `connected`, `stt_result`, `llm_chunk`, `llm_done`, `tts_audio`, `error`, `pong`
|
**服务端 → 客户端**:`connected`、`stt_result`、`llm_chunk`、`llm_done`、`tts_audio`、`error`、`pong`
|
||||||
|
|
||||||
**Heartbeat:** Client pings every 30s. Server disconnects after 60s of silence.
|
**心跳**:客户端每 30 秒 ping,服务端 60 秒无 ping 断开连接。
|
||||||
**Reconnection:** Exponential backoff with jitter — 1s, 2s, 4s, 8s… max 30s.
|
**重连**:指数退避 + 抖动 —— 1s, 2s, 4s, 8s… 最大 30s。
|
||||||
|
|
||||||
## REST API (Auxiliary)
|
## REST API(辅助)
|
||||||
|
|
||||||
- `GET /api/health` — health check (version, uptime, active sessions)
|
- `GET /api/health` — 健康检查(版本、运行时间、活跃会话数)
|
||||||
- `POST /api/sessions` — create session (optional, MVP auto-creates on WS connect)
|
- `POST /api/sessions` — 创建会话(可选,MVP 在 WS 连接时自动创建)
|
||||||
- `DELETE /api/sessions/{id}` — destroy session
|
- `DELETE /api/sessions/{id}` — 销毁会话
|
||||||
|
|
||||||
## Error Codes
|
## 错误码
|
||||||
|
|
||||||
`INVALID_MESSAGE`, `SESSION_NOT_FOUND`, `RATE_LIMITED`, `IMAGE_TOO_LARGE`, `AUDIO_TOO_SHORT`, `LLM_TIMEOUT`, `LLM_ERROR`, `STT_ERROR`, `TTS_ERROR`, `INTERNAL_ERROR`
|
`INVALID_MESSAGE`、`SESSION_NOT_FOUND`、`RATE_LIMITED`、`IMAGE_TOO_LARGE`、`AUDIO_TOO_SHORT`、`LLM_TIMEOUT`、`LLM_ERROR`、`STT_ERROR`、`TTS_ERROR`、`INTERNAL_ERROR`
|
||||||
|
|
||||||
## Frontend Component Structure
|
## 前端组件结构
|
||||||
|
|
||||||
| Component | Responsibility |
|
| 组件 | 职责 |
|
||||||
|-----------|---------------|
|
|------|------|
|
||||||
| `CameraManager` | Camera stream capture |
|
| `CameraManager` | 摄像头流采集 |
|
||||||
| `MicManager` | Microphone audio capture |
|
| `MicManager` | 麦克风音频采集 |
|
||||||
| `EdgeProcessor` | VAD + keyframe detection (ONNX Runtime) |
|
| `EdgeProcessor` | VAD + 关键帧检测(ONNX Runtime) |
|
||||||
| `WebSocketManager` | WS connection lifecycle |
|
| `WebSocketManager` | WebSocket 连接生命周期管理 |
|
||||||
| `ChatPanel` | Message display |
|
| `ChatPanel` | 消息展示 |
|
||||||
| `VideoPreview` | Camera feed display |
|
| `VideoPreview` | 摄像头画面预览 |
|
||||||
|
|
||||||
## Backend Module Structure
|
## 后端模块结构
|
||||||
|
|
||||||
| Module | Responsibility |
|
| 模块 | 职责 |
|
||||||
|--------|---------------|
|
|------|------|
|
||||||
| WebSocket Hub | Connection management, broadcast/direct push |
|
| WebSocket Hub | 连接管理、广播/定向推送 |
|
||||||
| Session Manager | Session state, conversation history (Redis + TTL) |
|
| Session Manager | 会话状态、对话历史(Redis + TTL) |
|
||||||
| Model Router | Select AI model per request (rule engine + cost threshold) |
|
| Model Router | 按请求选择 AI 模型(规则引擎 + 成本阈值) |
|
||||||
| AI Orchestrator | Parallel/sequential AI calls with context timeout |
|
| AI Orchestrator | 并行/串行 AI 调用编排,context 超时控制 |
|
||||||
| Rate Limiter | Per-user token bucket rate limiting |
|
| Rate Limiter | 按用户的令牌桶速率限制 |
|
||||||
|
|
||||||
## Coding Conventions
|
## 编码规范
|
||||||
|
|
||||||
- **Go:** Follow standard Go conventions. Use `context.Context` for cancellation/timeout in all AI calls. Use `sync.RWMutex` for concurrent map access. Struct tags use `json:"snake_case"`.
|
- **Go**:遵循标准 Go 规范。所有 AI 调用使用 `context.Context` 做取消/超时。并发 map 访问使用 `sync.RWMutex`。结构体标签用 `json:"snake_case"`。
|
||||||
- **TypeScript:** Strict mode. Interfaces for all data models. WebSocket message types as discriminated unions (`type` field).
|
- **TypeScript**:严格模式。所有数据模型用接口定义。WebSocket 消息类型用可辨识联合类型(`type` 字段)。
|
||||||
- **Commit messages:** Conventional commits 格式,描述用中文。示例:`feat: 添加 WebSocket 连接管理`, `fix: 修复心跳超时判断`, `docs: 更新接口文档`
|
- **提交信息**:Conventional Commits 格式,描述用中文。示例:`feat: 添加 WebSocket 连接管理`、`fix: 修复心跳超时判断`、`docs: 更新接口文档`
|
||||||
- **No auto-push:** 禁止自动 push,除非用户明确要求。
|
- **禁止自动 push**:除非用户明确要求。
|
||||||
- **Docs-first:** 实现功能前先读取 `docs/` 下的相关设计文档,以文档为依据进行开发。实现与文档不一致时,优先更新 `docs/` 下的接口文档。
|
- **文档优先**:实现功能前先读取 `docs/` 下的相关设计文档。实现与文档不一致时,优先更新 `docs/` 下的接口文档。
|
||||||
|
|||||||
28
docs/01-项目概述.md
Normal file
28
docs/01-项目概述.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 项目概述
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
开发一款**多模态实时对话应用**——通过摄像头与麦克风捕获用户的视觉场景与语音输入,由 AI 理解并给出自然、流畅的回应。
|
||||||
|
|
||||||
|
核心挑战在于三个维度之间的张力:
|
||||||
|
|
||||||
|
| 维度 | 关键问题 | 详见 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 视觉理解 | 如何准确理解摄像头画面中的人物、物体、场景? | `07-视觉理解.md` |
|
||||||
|
| 语音交互 | 如何让对话像真人交流一样自然、低延迟? | `06-语音交互.md` |
|
||||||
|
| 成本控制 | 实时视频流 + LLM 推理,如何避免账单爆炸? | `08-成本控制.md` |
|
||||||
|
|
||||||
|
> 提升视觉精度意味着更高分辨率和更频繁的采样,但这会直接推高带宽和推理成本。架构设计需要在三者之间做好取舍。
|
||||||
|
|
||||||
|
## 项目目标
|
||||||
|
|
||||||
|
1. **用户故事规划**:明确"AI 能看、能听、能说"需要覆盖哪些场景 → `05-用户故事.md`
|
||||||
|
2. **成本控制策略**:从架构设计层面融入运营成本意识 → `08-成本控制.md`
|
||||||
|
|
||||||
|
## 交付物
|
||||||
|
|
||||||
|
- 可运行的应用程序(摄像头 + 麦克风 → AI 回应)
|
||||||
|
- 设计文档,覆盖:
|
||||||
|
- 计划实现 vs 最终实现的用户故事
|
||||||
|
- 成本控制技巧的构思 vs 实际采用的方案
|
||||||
|
- 项目架构设计与技术选型
|
||||||
207
docs/02-系统架构.md
Normal file
207
docs/02-系统架构.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# 系统架构
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
三层架构:**前端做轻量预处理,后端做智能编排,云端 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<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 表设计
|
||||||
|
|
||||||
|
```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 实例。
|
||||||
433
docs/03-接口文档.md
Normal file
433
docs/03-接口文档.md
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
# 接口文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
前后端通信接口定义。以 WebSocket 承载实时对话,REST 端点支撑基础运维。**暂不实现持久化**,但通过 Repository 接口模式为后续扩展预留接入点。
|
||||||
|
|
||||||
|
**设计原则**:
|
||||||
|
- WebSocket 为主:所有对话数据走 WebSocket
|
||||||
|
- REST 为辅:仅用于健康检查、会话管理等低频操作
|
||||||
|
- 接口先行:先定义契约,再填充实现——前后端可并行开发
|
||||||
|
|
||||||
|
## 接口全景
|
||||||
|
|
||||||
|
```
|
||||||
|
浏览器 Go Gateway :8080
|
||||||
|
WebSocket Client <--> /ws (实时对话)
|
||||||
|
HTTP Client --> GET /api/health
|
||||||
|
HTTP Client <--> POST/DELETE /api/sessions
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、WebSocket 协议
|
||||||
|
|
||||||
|
连接地址:`ws://localhost:8080/ws`
|
||||||
|
|
||||||
|
### 消息格式约定
|
||||||
|
|
||||||
|
所有 WebSocket 消息均为 JSON 文本帧,统一结构:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface WsMessage {
|
||||||
|
type: string; // 消息类型,必填
|
||||||
|
request_id?: string; // 可选,用于请求-响应关联
|
||||||
|
timestamp?: number; // 可选,毫秒时间戳
|
||||||
|
[key: string]: any; // 类型特定字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 客户端 → 服务端消息
|
||||||
|
|
||||||
|
#### `query` — 发起一次视觉对话
|
||||||
|
|
||||||
|
用户说完话后,客户端同时发送当前图像帧和语音片段:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface QueryMessage {
|
||||||
|
type: "query";
|
||||||
|
request_id: string; // 客户端生成的 UUID
|
||||||
|
image: string; // Base64 编码的 JPEG 图像(不含 data: 前缀)
|
||||||
|
audio: string; // Base64 编码的音频片段(PCM 16kHz)
|
||||||
|
mime_type?: string; // 音频格式,默认 "audio/pcm"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 为什么图像和音频放在同一条消息里?因为 VAD 检测到用户说完话时,需要同时捕获"此刻的画面"和"说的话",拆成两条消息会增加时序同步的复杂度。
|
||||||
|
|
||||||
|
#### `config` — 更新会话配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ConfigMessage {
|
||||||
|
type: "config";
|
||||||
|
payload: {
|
||||||
|
tts_enabled?: boolean; // 是否开启语音合成,默认 true
|
||||||
|
detail_level?: "low" | "high"; // 图像精度,默认 "low"
|
||||||
|
language?: string; // 交互语言,默认 "zh-CN"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `interrupt` — 打断当前回复
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface InterruptMessage {
|
||||||
|
type: "interrupt";
|
||||||
|
request_id?: string; // 可选,指定打断哪次请求
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `ping` — 心跳保活
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PingMessage {
|
||||||
|
type: "ping";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务端 → 客户端消息
|
||||||
|
|
||||||
|
#### `connected` — 连接建立确认
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ConnectedMessage {
|
||||||
|
type: "connected";
|
||||||
|
session_id: string; // 服务端生成的会话 ID
|
||||||
|
server_version: string; // 服务端版本号,如 "0.1.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `stt_result` — 语音识别结果
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface STTResultMessage {
|
||||||
|
type: "stt_result";
|
||||||
|
request_id: string;
|
||||||
|
text: string; // 识别出的用户语音文本
|
||||||
|
is_final: boolean; // 是否为最终结果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `llm_chunk` — LLM 流式输出片段
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface LLMChunkMessage {
|
||||||
|
type: "llm_chunk";
|
||||||
|
request_id: string;
|
||||||
|
delta: string; // 本次增量文本
|
||||||
|
role: "assistant";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `llm_done` — LLM 输出完成
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface LLMDoneMessage {
|
||||||
|
type: "llm_done";
|
||||||
|
request_id: string;
|
||||||
|
full_text: string; // 完整回复文本
|
||||||
|
tokens_used: {
|
||||||
|
prompt: number;
|
||||||
|
completion: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
model: string; // 实际使用的模型名
|
||||||
|
latency_ms: number; // 端到端延迟(毫秒)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tts_audio` — TTS 音频流片段
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TTSAudioMessage {
|
||||||
|
type: "tts_audio";
|
||||||
|
request_id: string;
|
||||||
|
audio: string; // Base64 编码的音频片段
|
||||||
|
mime_type: string; // "audio/mp3" 或 "audio/pcm"
|
||||||
|
is_last: boolean; // 是否为最后一片
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `error` — 错误通知
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ErrorMessage {
|
||||||
|
type: "error";
|
||||||
|
request_id?: string;
|
||||||
|
code: string; // 错误码,见下方错误码表
|
||||||
|
message: string; // 人类可读的错误描述
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `pong` — 心跳响应
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PongMessage {
|
||||||
|
type: "pong";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息流时序
|
||||||
|
|
||||||
|
一次完整交互:
|
||||||
|
|
||||||
|
```
|
||||||
|
Client Server
|
||||||
|
| |
|
||||||
|
|-- query {image, audio} ------>|
|
||||||
|
|<-- stt_result {text} ---------|
|
||||||
|
| |
|
||||||
|
|<-- llm_chunk {delta: "这"} ---| (LLM 流式输出)
|
||||||
|
|<-- llm_chunk {delta: "是一"} -|
|
||||||
|
|<-- llm_chunk {delta: "朵花"} -|
|
||||||
|
|<-- llm_done {full_text} ------|
|
||||||
|
| |
|
||||||
|
|<-- tts_audio {audio} ---------| (TTS 音频流)
|
||||||
|
|<-- tts_audio {is_last: true} -|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、REST API
|
||||||
|
|
||||||
|
### 健康检查
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"uptime_seconds": 3600,
|
||||||
|
"active_sessions": 42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建会话(可选,MVP 自动创建)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/sessions
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": "optional-user-id",
|
||||||
|
"config": {
|
||||||
|
"tts_enabled": true,
|
||||||
|
"detail_level": "low",
|
||||||
|
"language": "zh-CN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"created_at": "2026-06-12T15:41:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 销毁会话
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /api/sessions/{session_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:`204 No Content`
|
||||||
|
|
||||||
|
### 预留端点(暂不实现)
|
||||||
|
|
||||||
|
| 端点 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/sessions/{id}/messages` | GET | 查询对话历史 |
|
||||||
|
| `/api/usage` | GET | 查询用量统计 |
|
||||||
|
| `/api/users/{id}/preferences` | GET/PUT | 用户偏好管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、数据模型
|
||||||
|
|
||||||
|
### Go 后端模型
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ---- 核心模型(MVP 实现)----
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
ID string `json:"session_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Config SessionConfig `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionConfig struct {
|
||||||
|
TTSEnabled bool `json:"tts_enabled"`
|
||||||
|
DetailLevel string `json:"detail_level"` // "low" | "high"
|
||||||
|
Language string `json:"language"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryRequest struct {
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
Image []byte `json:"-"` // Base64 解码后
|
||||||
|
Audio []byte `json:"-"` // Base64 解码后
|
||||||
|
MimeType string `json:"mime_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Role string `json:"role"` // "user" | "assistant"
|
||||||
|
Content string `json:"content"`
|
||||||
|
ImageURL string `json:"image_url,omitempty"`
|
||||||
|
TokensUsed int `json:"tokens_used,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript 前端模型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Session {
|
||||||
|
sessionId: string;
|
||||||
|
createdAt: string;
|
||||||
|
config: SessionConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionConfig {
|
||||||
|
ttsEnabled: boolean;
|
||||||
|
detailLevel: "low" | "high";
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
timestamp: number;
|
||||||
|
tokensUsed?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket 消息联合类型
|
||||||
|
type ServerMessage =
|
||||||
|
| ConnectedMessage
|
||||||
|
| STTResultMessage
|
||||||
|
| LLMChunkMessage
|
||||||
|
| LLMDoneMessage
|
||||||
|
| TTSAudioMessage
|
||||||
|
| ErrorMessage
|
||||||
|
| PongMessage;
|
||||||
|
|
||||||
|
type ClientMessage =
|
||||||
|
| QueryMessage
|
||||||
|
| ConfigMessage
|
||||||
|
| InterruptMessage
|
||||||
|
| PingMessage;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、扩展接口设计
|
||||||
|
|
||||||
|
通过 Repository 接口隔离存储层,MVP 用内存实现,后续替换为数据库——业务逻辑零改动。
|
||||||
|
|
||||||
|
```go
|
||||||
|
// HistoryRepository — 对话历史存储契约
|
||||||
|
// MVP: 内存实现(session 内有效,断开即丢)
|
||||||
|
// 后续: PostgreSQL 实现
|
||||||
|
type HistoryRepository interface {
|
||||||
|
SaveMessage(ctx context.Context, sessionID string, msg Message) error
|
||||||
|
GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsageRepository — 用量统计存储契约
|
||||||
|
// MVP: 内存计数器
|
||||||
|
// 后续: PostgreSQL 按天聚合
|
||||||
|
type UsageRepository interface {
|
||||||
|
RecordUsage(ctx context.Context, sessionID string, usage UsageRecord) error
|
||||||
|
GetDailyUsage(ctx context.Context, userID string, days int) ([]UsageDaily, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
MVP 内存实现:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type InMemoryHistory struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
sessions map[string][]Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InMemoryHistory) SaveMessage(ctx context.Context, sessionID string, msg Message) error {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
h.sessions[sessionID] = append(h.sessions[sessionID], msg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InMemoryHistory) GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
msgs := h.sessions[sessionID]
|
||||||
|
if limit > 0 && len(msgs) > limit {
|
||||||
|
msgs = msgs[len(msgs)-limit:]
|
||||||
|
}
|
||||||
|
return msgs, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注入点(应用启动时根据配置选择实现):
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewApp(cfg *Config) *App {
|
||||||
|
var history HistoryRepository
|
||||||
|
var usage UsageRepository
|
||||||
|
|
||||||
|
switch cfg.Storage.Driver {
|
||||||
|
case "postgres":
|
||||||
|
pool, _ := pgxpool.New(ctx, cfg.Storage.DSN)
|
||||||
|
history = &PgHistory{pool: pool}
|
||||||
|
usage = &PgUsage{pool: pool}
|
||||||
|
default: // "memory" — MVP 默认
|
||||||
|
history = &InMemoryHistory{sessions: make(map[string][]Message)}
|
||||||
|
usage = &InMemoryUsage{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
orchestrator: NewOrchestrator(cfg.AI, history, usage),
|
||||||
|
sessionMgr: NewSessionManager(cfg.Session, history),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 依赖倒置原则——业务层依赖接口,不依赖具体实现。MVP 注入 `InMemoryHistory`,上线时一行代码换成 `PgHistory`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、错误码
|
||||||
|
|
||||||
|
| 错误码 | 含义 | 客户端处理建议 |
|
||||||
|
|--------|------|--------------|
|
||||||
|
| `INVALID_MESSAGE` | 消息格式不合法 | 检查 JSON 结构,不重试 |
|
||||||
|
| `SESSION_NOT_FOUND` | 会话不存在或已过期 | 重新建立 WebSocket 连接 |
|
||||||
|
| `RATE_LIMITED` | 请求频率超限 | 延迟后重试,提示用户稍等 |
|
||||||
|
| `IMAGE_TOO_LARGE` | 图像超过 4MB 限制 | 降低分辨率或压缩质量 |
|
||||||
|
| `AUDIO_TOO_SHORT` | 音频片段 < 250ms | 忽略,等待下次语音输入 |
|
||||||
|
| `LLM_TIMEOUT` | LLM 推理超时(>10s) | 提示用户重试 |
|
||||||
|
| `LLM_ERROR` | LLM 服务异常 | 提示用户重试,服务端记录日志 |
|
||||||
|
| `STT_ERROR` | 语音识别失败 | 回退到纯文本输入模式 |
|
||||||
|
| `TTS_ERROR` | 语音合成失败 | 静默回退到纯文本回复 |
|
||||||
|
| `INTERNAL_ERROR` | 服务端内部错误 | 提示用户重试 |
|
||||||
|
|
||||||
|
## 六、连接管理
|
||||||
|
|
||||||
|
**心跳机制**:客户端每 30 秒发送 `ping`,服务端回复 `pong`。超过 60 秒无 `ping`,服务端判定连接断开并清理会话资源。
|
||||||
|
|
||||||
|
**重连策略**(指数退避 + 抖动):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function reconnect(attempt: number) {
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, attempt), 30000); // 最大 30s
|
||||||
|
const jitter = Math.random() * 1000;
|
||||||
|
setTimeout(() => connect(), delay + jitter);
|
||||||
|
}
|
||||||
|
// attempt: 0 → 1s, 1 → 2s, 2 → 4s, 3 → 8s, ... 最大 30s
|
||||||
|
```
|
||||||
178
docs/04-技术选型.md
Normal file
178
docs/04-技术选型.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# 技术选型
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档记录项目中各项技术的**选型过程、替代方案对比和决策理由**。技术选型没有"绝对正确",只有"更适合"。
|
||||||
|
|
||||||
|
**定位**:持久化部分是拓展选型,不阻塞 MVP(MVP 用 Redis 即可)。前端边缘处理部分是 MVP 阶段就需要确定的技术栈。
|
||||||
|
|
||||||
|
```
|
||||||
|
技术选型
|
||||||
|
├── 持久化层 → 数据库选型: PostgreSQL
|
||||||
|
└── 前端边缘处理层
|
||||||
|
├── 边缘推理: ONNX Runtime Web
|
||||||
|
├── 语音检测: @ricky0123/vad-web
|
||||||
|
└── 媒体采集: MediaDevices API
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、持久化层选型
|
||||||
|
|
||||||
|
### 数据特征分析
|
||||||
|
|
||||||
|
| 数据 | 结构特征 | 读写模式 | 数据量级 |
|
||||||
|
|------|---------|---------|---------|
|
||||||
|
| 对话消息 | 强结构化 | 写多读少,按会话聚合读取 | 中(每用户日均 ~100 条) |
|
||||||
|
| 会话元信息 | 强结构化 | 写少读少 | 低 |
|
||||||
|
| 对话上下文 | 半结构化 JSON | 高频读写,TTL 过期 | 低(仅当前窗口) |
|
||||||
|
| 用量统计 | 强结构化 | 写多,定期聚合读 | 低(日粒度汇总后很小) |
|
||||||
|
| 用户偏好 | 强结构化 KV | 写极少读少 | 极低 |
|
||||||
|
| 关键帧图像 | 非结构化二进制 | 写少,按需读 | 大(单张 100KB~1MB) |
|
||||||
|
|
||||||
|
核心数据(对话、会话、统计)都是**强结构化**的,关系型数据库天然适配。"对话上下文"是半结构化 JSON,需要数据库对 JSON 有良好支持。
|
||||||
|
|
||||||
|
### 候选方案对比
|
||||||
|
|
||||||
|
| 维度 | PostgreSQL | MySQL | SQLite | MongoDB | TiDB |
|
||||||
|
|------|-----------|-------|--------|---------|------|
|
||||||
|
| 数据模型 | 关系型 + JSONB | 关系型 | 关系型(嵌入式) | 文档型(BSON) | 关系型(分布式) |
|
||||||
|
| JSON 支持 | JSONB 原生索引 | JSON 类型,索引弱 | 无原生支持 | 天生擅长 | 兼容 MySQL JSON |
|
||||||
|
| 关联查询 | 强 | 强 | 强 | 弱(需 $lookup) | 强 |
|
||||||
|
| 聚合统计 | 窗口函数/CTE | 基础聚合 | 基础聚合 | 聚合管道 | 强 |
|
||||||
|
| 并发能力 | 高(MVCC) | 中 | 低(单写锁) | 高 | 极高(分布式) |
|
||||||
|
| Go 生态 | pgx / GORM | go-sql-driver | go-sqlite3 | mongo-go-driver | 兼容 MySQL 驱动 |
|
||||||
|
|
||||||
|
### 淘汰理由
|
||||||
|
|
||||||
|
**SQLite** — 写锁是全局的,并发写入会频繁锁等待。多个 Go Gateway 实例无法共享同一 SQLite 文件。适合单机桌面应用,不适合 Web 服务。
|
||||||
|
|
||||||
|
**MySQL** — JSON 类型索引能力弱,无法对 JSON 内部字段高效查询。缺少 `gen_random_uuid()` 等原生函数。如果团队只熟悉 MySQL,MVP 阶段完全可用,后续复杂查询会比 PostgreSQL 麻烦。
|
||||||
|
|
||||||
|
**MongoDB** — `messages` 需按 `session_id` 关联 `sessions`,MongoDB 中要用 `$lookup`,写法复杂且性能不如 SQL JOIN。用量统计的"按天聚合"用 SQL 一句话搞定,MongoDB 聚合管道代码量多 3-5 倍。
|
||||||
|
|
||||||
|
**TiDB** — 部署复杂(至少 3 PD + 3 TiKV + 2 TiDB),单机 PostgreSQL 完全够用,过度设计。
|
||||||
|
|
||||||
|
### 选择 PostgreSQL 的理由
|
||||||
|
|
||||||
|
| 项目需求 | PostgreSQL 匹配点 |
|
||||||
|
|---------|------------------|
|
||||||
|
| 强结构化数据 | 原生关系型,SQL 标准完备 |
|
||||||
|
| JSON 半结构化 | JSONB 支持索引、路径查询、部分更新 |
|
||||||
|
| messages ↔ sessions 关联 | 完整的 FK 约束 + JOIN |
|
||||||
|
| 用量按天/周/月聚合 | 窗口函数、CTE、`DATE_TRUNC` |
|
||||||
|
| Go 后端对接 | pgx 驱动性能优秀,GORM/Ent 支持成熟 |
|
||||||
|
| 未来全文搜索 | 内置 `tsvector`,无需额外引入 ES |
|
||||||
|
|
||||||
|
### Go 集成示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
pool, _ := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/vision_ai")
|
||||||
|
|
||||||
|
func SaveMessage(ctx context.Context, pool *pgxpool.Pool, msg *Message) error {
|
||||||
|
_, err := pool.Exec(ctx,
|
||||||
|
`INSERT INTO messages (session_id, role, content, image_url, tokens_used)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)`,
|
||||||
|
msg.SessionID, msg.Role, msg.Content, msg.ImageURL, msg.TokensUsed,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWeeklyUsage(ctx context.Context, pool *pgxpool.Pool, userID string) ([]UsageRow, error) {
|
||||||
|
rows, _ := pool.Query(ctx,
|
||||||
|
`SELECT date, llm_tokens, estimated_cost
|
||||||
|
FROM usage_daily
|
||||||
|
WHERE user_id = $1 AND date >= CURRENT_DATE - INTERVAL '7 days'
|
||||||
|
ORDER BY date`, userID)
|
||||||
|
defer rows.Close()
|
||||||
|
// ... scan rows
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
JSONB 包容查询:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT id, content, created_at
|
||||||
|
FROM messages
|
||||||
|
WHERE role = 'user'
|
||||||
|
AND content @> '{"text": "花"}'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 冷热分离架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Go Gateway
|
||||||
|
├── 写入路径 → Redis(实时会话状态)
|
||||||
|
│ → PostgreSQL(对话历史 + 用量)
|
||||||
|
└── 读取路径 → Redis(当前上下文,快)
|
||||||
|
→ PostgreSQL(历史记录,慢)
|
||||||
|
```
|
||||||
|
|
||||||
|
建议异步写入——实时消息先写 Redis(快),异步批量刷入 PostgreSQL(慢),不影响对话体验。
|
||||||
|
|
||||||
|
### 决策流程
|
||||||
|
|
||||||
|
```
|
||||||
|
需要持久化?
|
||||||
|
├── 否 → 继续用 Redis
|
||||||
|
└── 是 → 数据强结构化?
|
||||||
|
├── 否, 高度嵌套 → 考虑 MongoDB
|
||||||
|
└── 是 → 数据量级?
|
||||||
|
├── < 100GB, 单机可扛 → PostgreSQL
|
||||||
|
├── 海量, 需水平扩展 → TiDB / CockroachDB
|
||||||
|
└── 极小, 单文件即可 → SQLite
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、前端边缘处理层选型
|
||||||
|
|
||||||
|
### 总览
|
||||||
|
|
||||||
|
| 能力 | 当前选型 | 选择理由 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 边缘推理 | ONNX Runtime Web | 通用推理引擎,模型无关,WASM 加速 |
|
||||||
|
| 语音检测 | @ricky0123/vad-web | 包装原生 WebRTC VAD,零延迟,体积极小 |
|
||||||
|
| 媒体采集 | MediaDevices API | 浏览器原生接口,无中间层,零依赖 |
|
||||||
|
|
||||||
|
### 边缘推理:ONNX Runtime Web
|
||||||
|
|
||||||
|
| 方案 | 特点 | 适用场景 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **ONNX Runtime Web** | 通用推理引擎,支持任意 ONNX 模型,WASM 加速 | 自定义模型 pipeline |
|
||||||
|
| TensorFlow.js | Google 生态,WebGL/WebGPU 加速 | 模型本身就是 TF 格式 |
|
||||||
|
| MediaPipe | 开箱即用 CV 任务 | 只需常见 CV 任务,不需自定义模型 |
|
||||||
|
| Transformers.js | HuggingFace 生态 | 快速集成预训练模型 |
|
||||||
|
|
||||||
|
项目需要同时跑 VAD 和关键帧检测两种自定义模型。ONNX 是跨框架通用格式,核心优势是**模型无关**。
|
||||||
|
|
||||||
|
### 语音检测:@ricky0123/vad-web
|
||||||
|
|
||||||
|
| 方案 | 特点 | 适用场景 |
|
||||||
|
|------|------|---------|
|
||||||
|
| **@ricky0123/vad-web** | 基于 WebRTC VAD,~100KB 含 WASM,纯前端零延迟 | "有没有人说话"二分类 |
|
||||||
|
| Web Audio API + 能量检测 | AnalyserNode 计算 RMS | 极简但不抗噪 |
|
||||||
|
| Silero VAD (ONNX) | 神经网络级 VAD | 嘈杂环境需更精准 |
|
||||||
|
| Picovoice Porcupine | 商业级唤醒词引擎 | 需要唤醒词功能 |
|
||||||
|
|
||||||
|
vad-web 是"够用且最轻"的平衡点——直接包装浏览器原生 WebRTC VAD 算法。
|
||||||
|
|
||||||
|
### 媒体采集:MediaDevices API
|
||||||
|
|
||||||
|
`navigator.mediaDevices.getUserMedia()` 是所有浏览器音视频采集的**唯一标准入口**。所有上层封装库底层都是调这个 API。项目需要原始 MediaStream,用封装库反而要多一层解包。
|
||||||
|
|
||||||
|
### 选型共同逻辑
|
||||||
|
|
||||||
|
三个技术选择的共同决策模式——**选择最薄的抽象层**:
|
||||||
|
|
||||||
|
| 技术 | "最薄"体现在 |
|
||||||
|
|------|------------|
|
||||||
|
| ONNX Runtime Web | 不绑定特定框架,模型格式通用 |
|
||||||
|
| @ricky0123/vad-web | 包装原生 WebRTC VAD,没有多余的模型加载 |
|
||||||
|
| MediaDevices API | 直接用浏览器原生接口,不加封装层 |
|
||||||
|
|
||||||
|
与"前端做轻量预处理"原则一致:前端层只需采集和判断"有没有值得发给后端的数据"。
|
||||||
58
docs/05-用户故事.md
Normal file
58
docs/05-用户故事.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# 用户故事
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
用户故事按**优先级分层**,标注哪些属于 MVP 范围、哪些可后续迭代。
|
||||||
|
|
||||||
|
## 核心用户故事列表
|
||||||
|
|
||||||
|
### P0 - MVP 必做
|
||||||
|
|
||||||
|
| 编号 | 用户故事 | 验收标准 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| US-01 | 对着摄像头提问"这是什么",AI 能识别画面中的物体并回答 | 准确识别常见物体,响应 < 3s |
|
||||||
|
| US-02 | 用语音与 AI 对话,无需打字 | VAD 准确检测语音,STT 准确率 > 95% |
|
||||||
|
| US-03 | AI 能"看到"摄像头拍到的画面 | 每次提问时自动捕获当前帧 |
|
||||||
|
| US-04 | AI 用语音回答,而不仅是文字 | TTS 自然流畅,延迟 < 1s |
|
||||||
|
|
||||||
|
### P1 - 增强体验
|
||||||
|
|
||||||
|
| 编号 | 用户故事 | 验收标准 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| US-05 | AI 能持续"看着"画面,主动提示重要变化 | 关键帧检测 + 主动推送 |
|
||||||
|
| US-06 | AI 能读出画面中的文字(OCR) | 中英文混合识别准确率 > 90% |
|
||||||
|
| US-07 | 连续对话时 AI 能记住上下文 | 支持多轮对话,上下文窗口 > 10 轮 |
|
||||||
|
|
||||||
|
### P2 - 进阶探索
|
||||||
|
|
||||||
|
| 编号 | 用户故事 | 验收标准 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| US-08 | 视障用户:AI 描述周围环境并提示障碍物 | 实时环境描述 + 安全警告 |
|
||||||
|
| US-09 | AI 翻译画面中的外语内容 | 支持主流语言实时翻译 |
|
||||||
|
| US-10 | 切换 AI 的"观察模式"和"对话模式" | 一键切换,模式状态清晰可见 |
|
||||||
|
|
||||||
|
## 用户旅程示例(US-01)
|
||||||
|
|
||||||
|
```
|
||||||
|
用户打开应用, 授权摄像头
|
||||||
|
→ 启动摄像头预览
|
||||||
|
→ 对着花朵说 "这是什么花"
|
||||||
|
→ VAD 检测语音结束
|
||||||
|
→ 捕获当前帧 + STT 识别
|
||||||
|
→ 发送图像 + "这是什么花" 到 LLM
|
||||||
|
→ LLM 返回 "这是一朵红色的玫瑰..."
|
||||||
|
→ TTS 合成语音
|
||||||
|
→ 播放语音回答
|
||||||
|
```
|
||||||
|
|
||||||
|
> "响应 < 3s"这个验收标准约束了整条链路——帧采样、网络传输、LLM 推理、TTS 合成都必须在这个预算内完成。用户故事的价值:**用体验目标倒推技术方案**。
|
||||||
|
|
||||||
|
## 优先级决策依据
|
||||||
|
|
||||||
|
用两个维度交叉评估:
|
||||||
|
- **用户价值**:这个功能对用户有多大帮助?
|
||||||
|
- **实现成本**:需要多少开发工作量和 API 调用成本?
|
||||||
|
|
||||||
|
P0 = 高价值 + 合理成本(MVP 必须有)
|
||||||
|
P1 = 高价值 + 较高成本(第二版加入)
|
||||||
|
P2 = 探索性(验证后再投入)
|
||||||
77
docs/06-语音交互.md
Normal file
77
docs/06-语音交互.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# 语音交互
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
语音交互全链路:**VAD(语音活动检测)** → **STT(语音转文字)** → **LLM 推理** → **TTS(文字转语音)**。
|
||||||
|
|
||||||
|
用户感知延迟 = VAD 响应 + STT 耗时 + LLM 首 token + TTS 首包。人类对话中停顿超过 300ms 就会感到"对方在想"。
|
||||||
|
|
||||||
|
**延迟目标**:端到端 **1.5~2 秒**(用户说完话到听到 AI 回应);流式 TTS 下,LLM 开始生成后 **0.5 秒**听到第一个词。
|
||||||
|
|
||||||
|
## 全链路
|
||||||
|
|
||||||
|
```
|
||||||
|
麦克风 → VAD → STT → LLM → TTS → 扬声器
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环节一:VAD(语音活动检测)
|
||||||
|
|
||||||
|
从持续音频流中检测"人什么时候在说话",避免将环境噪音当作有效输入。**浏览器端完成**,节省 ~70% 带宽。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MicVAD } from "@ricky0123/vad-web";
|
||||||
|
|
||||||
|
const vad = await MicVAD.new({
|
||||||
|
onSpeechStart: () => console.log("用户开始说话"),
|
||||||
|
onSpeechEnd: (audio) => {
|
||||||
|
// audio: Float32Array,送入 STT
|
||||||
|
sendToSTT(audio);
|
||||||
|
},
|
||||||
|
positiveSpeechThreshold: 0.5, // 检测灵敏度
|
||||||
|
minSpeechDuration: 250 // 最短语音时长 ms
|
||||||
|
});
|
||||||
|
|
||||||
|
vad.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环节二:STT(语音转文字)
|
||||||
|
|
||||||
|
| 方案 | 延迟 | 成本 | 特点 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| Whisper API | 1-3s | 按分钟计费 | 准确率高,支持多语言 |
|
||||||
|
| **Deepgram** | <500ms | 按分钟计费 | 流式识别,延迟极低 |
|
||||||
|
| 浏览器原生 | ~1s | 免费 | 中文效果一般 |
|
||||||
|
| FunASR | <500ms | 自部署免费 | 阿里开源,中文优化 |
|
||||||
|
|
||||||
|
流式 STT 是低延迟的关键——不必等用户说完,边说边识别:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Deepgram 流式识别示例
|
||||||
|
const ws = new WebSocket("wss://api.deepgram.com/v1/listen", {
|
||||||
|
headers: { Authorization: `Token ${API_KEY}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const { transcript, is_final } = JSON.parse(event.data).channel.alternatives[0];
|
||||||
|
if (is_final) {
|
||||||
|
onFinalTranscript(transcript); // 一句完整语音,送入 LLM
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环节三:TTS(文字转语音)
|
||||||
|
|
||||||
|
流式 TTS:检测 LLM 输出中的句子边界,每检测到一句就立即送入 TTS 合成并播放,不必等全部生成完。
|
||||||
|
|
||||||
|
方案选择:
|
||||||
|
- **OpenAI TTS**:音质好,延迟中等,按字符计费
|
||||||
|
- **Edge TTS**:微软免费方案,音质不错,延迟略高
|
||||||
|
- **Fish Speech / CosyVoice**:开源方案,支持声音克隆,可自部署
|
||||||
|
|
||||||
|
## 延迟优化要点
|
||||||
|
|
||||||
|
- VAD 浏览器端处理(减少无效传输)
|
||||||
|
- 流式 STT(边说边识别)
|
||||||
|
- LLM 流式输出
|
||||||
|
- TTS 句子级流式合成
|
||||||
|
- STT 与上下文准备并行处理
|
||||||
73
docs/07-视觉理解.md
Normal file
73
docs/07-视觉理解.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# 视觉理解
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
从摄像头视频流到 AI 语义理解的技术链路:**帧采样** → **图像编码** → **多模态 LLM** → **语义结果**。
|
||||||
|
|
||||||
|
摄像头每秒 30 帧,全部送入 LLM 不现实也不经济,帧采样是第一个需要解决的问题。
|
||||||
|
|
||||||
|
## 帧采样策略
|
||||||
|
|
||||||
|
| 策略 | 原理 | 适用场景 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 固定间隔采样 | 每 N 秒取一帧 | 画面变化缓慢 |
|
||||||
|
| 关键帧检测 | 对比相邻帧差异,变化超阈值时触发 | 画面动态变化较多 |
|
||||||
|
| 事件驱动采样 | 用户主动触发(如拍照按钮) | 精确提问场景 |
|
||||||
|
| **混合策略** | 低频定时 + 高频事件触发 | **通用推荐方案** |
|
||||||
|
|
||||||
|
关键帧检测核心逻辑:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def is_keyframe(prev_frame, curr_frame, threshold=30):
|
||||||
|
"""通过帧间像素差异判断是否为关键帧"""
|
||||||
|
diff = np.mean(np.abs(prev_frame.astype(int) - curr_frame.astype(int)))
|
||||||
|
return diff > threshold
|
||||||
|
```
|
||||||
|
|
||||||
|
> 实际开发中,先降低分辨率(如 320x240)做关键帧检测,再对命中帧保留原始分辨率送入 LLM,兼顾速度与精度。
|
||||||
|
|
||||||
|
## 图像编码与多模态输入
|
||||||
|
|
||||||
|
主流多模态 LLM(GPT-4o、Claude)接受图片的两种方式:
|
||||||
|
|
||||||
|
| 方式 | 适用场景 |
|
||||||
|
|------|---------|
|
||||||
|
| Base64 内联 | 本地/实时场景 |
|
||||||
|
| URL 引用 | 已有图床的场景 |
|
||||||
|
|
||||||
|
OpenAI 兼容接口调用示例:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await openai.chat.completions.create({
|
||||||
|
model: "gpt-4o",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "请描述画面中的内容" },
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: {
|
||||||
|
url: `data:image/jpeg;base64,${base64Image}`,
|
||||||
|
detail: "low" // "low" | "high" | "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**detail 参数影响**:
|
||||||
|
- `low`:65x65 缩略图,约 85 tokens,适合快速识别
|
||||||
|
- `high`:按 512px 方块切分,细节丰富但 token 数激增
|
||||||
|
- 实时对话场景建议默认 `low`,仅在用户追问细节时切换 `high`
|
||||||
|
|
||||||
|
## 视觉理解的局限性
|
||||||
|
|
||||||
|
- **运动模糊**:快速移动物体在低帧率下容易模糊
|
||||||
|
- **光线变化**:逆光、暗光环境下识别率显著下降
|
||||||
|
- **细小文字**:低分辨率下 OCR 能力受限
|
||||||
|
- **空间推理**:精确的距离、尺寸判断仍是短板
|
||||||
77
docs/08-成本控制.md
Normal file
77
docs/08-成本控制.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# 成本控制
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
实时视频流 + 多模态 LLM 推理的成本极易失控。从**视觉链路**、**语音链路**、**推理链路**三个维度梳理成本控制策略,核心思想是**端云协同**——将适合的计算前置到客户端,降低对云端 API 的依赖。
|
||||||
|
|
||||||
|
**成本对比**:优化前(1fps 全量发送)vs 优化后(0.2fps + 端侧筛选 + 模型分级)→ 月成本从 **$5000 降至 $300~500**,降幅约 90%。
|
||||||
|
|
||||||
|
## 成本构成
|
||||||
|
|
||||||
|
```
|
||||||
|
总成本
|
||||||
|
├── 视觉链路:图像编码与传输、视觉 token 消耗
|
||||||
|
├── 语音链路:STT 按分钟计费、TTS 按字符计费
|
||||||
|
└── 推理链路:LLM 输入 tokens、LLM 输出 tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
假设:10 分钟/天/用户,1fps,每次 1000 tokens → 一天 60 万 tokens。1000 用户时成本不可控。
|
||||||
|
|
||||||
|
## 策略一:智能采样——少发图,发好图
|
||||||
|
|
||||||
|
| 策略 | 降本幅度 | 实现复杂度 | 说明 |
|
||||||
|
|------|---------|-----------|------|
|
||||||
|
| 提高采样间隔 | 高 | 低 | 从 1fps 降到 0.2fps |
|
||||||
|
| 关键帧过滤 | 中 | 中 | 画面不变时不发送 |
|
||||||
|
| 用户触发 | 高 | 低 | 只在用户提问时拍照 |
|
||||||
|
| 本地预筛选 | 中 | 高 | 用轻量模型判断"是否值得问 LLM" |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 混合策略:定时低频 + 事件高频
|
||||||
|
const NORMAL_INTERVAL = 5000; // 正常 5 秒一帧
|
||||||
|
const ACTIVE_INTERVAL = 1000; // 用户说话时 1 秒一帧
|
||||||
|
|
||||||
|
let isUserSpeaking = false;
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
captureAndSend(isUserSpeaking ? "low" : "high");
|
||||||
|
}, isUserSpeaking ? ACTIVE_INTERVAL : NORMAL_INTERVAL);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 策略二:端云协同——把计算推到边缘
|
||||||
|
|
||||||
|
不是所有计算都需要上云。可前置到客户端的计算:
|
||||||
|
|
||||||
|
- **VAD 语音检测**:浏览器端完成,减少无效音频上传(节省 ~70% 带宽)
|
||||||
|
- **人脸/物体检测**:用 ONNX Runtime 跑轻量模型(如 YOLOv8-nano ~6MB,推理 ~30ms),只在检测到新物体时触发 LLM
|
||||||
|
- **重复画面过滤**:计算帧间相似度,相似度 > 90% 直接跳过
|
||||||
|
- **敏感内容过滤**:NSFW 检测前置,避免无效 API 调用
|
||||||
|
|
||||||
|
## 策略三:模型分级——用对模型做对事
|
||||||
|
|
||||||
|
不是每个问题都需要最贵的模型:
|
||||||
|
|
||||||
|
```
|
||||||
|
用户提问 → 问题复杂度判断
|
||||||
|
├── 简单识别 → GPT-4o-mini ($0.15/1M tokens)
|
||||||
|
├── 深度分析 → GPT-4o ($2.5/1M tokens)
|
||||||
|
└── 代码/推理 → o1 ($15/1M tokens)
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function routeQuery(image: string, question: string) {
|
||||||
|
const complexity = await classifyComplexity(question);
|
||||||
|
const modelMap = {
|
||||||
|
simple: "gpt-4o-mini", // "这是什么?"
|
||||||
|
moderate: "gpt-4o", // "分析这张图"
|
||||||
|
complex: "o1" // "推理/规划"
|
||||||
|
};
|
||||||
|
return callLLM(modelMap[complexity], image, question);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 策略四:缓存与复用
|
||||||
|
|
||||||
|
- **语义缓存**:相似问题直接返回缓存结果(如反复问"这是什么")
|
||||||
|
- **上下文复用**:连续对话中,未变化的图像不必重复发送
|
||||||
|
- **Prompt 压缩**:精简 system prompt,减少每轮的固定 token 开销
|
||||||
36
docs/09-技术名词解释.md
Normal file
36
docs/09-技术名词解释.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 技术名词解释
|
||||||
|
|
||||||
|
对架构文档中技术选型表里出现的所有关键名词的简明解释。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端相关
|
||||||
|
|
||||||
|
| 名词 | 一句话 | 展开 |
|
||||||
|
|------|--------|------|
|
||||||
|
| **React 18** | 组件化 UI 框架 | Facebook 开源,把页面拆成组件搭积木拼装。18 版本支持并发渲染。 |
|
||||||
|
| **TypeScript** | 带类型的 JavaScript | 在 JS 基础上增加类型声明,编译阶段就能发现类型错误。 |
|
||||||
|
| **Vite** | 前端构建工具 | 利用浏览器原生 ES Module,开发时毫秒级热更新(HMR),构建产物小。 |
|
||||||
|
| **WebSocket** | 浏览器与服务器的双向通道 | HTTP 是"一问一答",WebSocket 像打电话——接通后双方随时互发消息,适合实时对话场景。 |
|
||||||
|
| **ONNX Runtime Web** | 浏览器端 AI 推理引擎 | 微软定义的通用模型格式 ONNX 的运行引擎,可在浏览器中用 WASM 加速跑轻量模型(如 VAD、关键帧检测),零延迟、不耗服务器资源。 |
|
||||||
|
| **VAD** | 语音活动检测 | Voice Activity Detection,检测"人有没有在说话"。WebRTC 内置了高效的 VAD 算法,本项目用 @ricky0123/vad-web 包装。 |
|
||||||
|
| **MediaDevices API** | 浏览器摄像头/麦克风接口 | `navigator.mediaDevices.getUserMedia()` 是浏览器音视频采集的唯一标准入口,无需插件。 |
|
||||||
|
|
||||||
|
## 后端相关
|
||||||
|
|
||||||
|
| 名词 | 一句话 | 展开 |
|
||||||
|
|------|--------|------|
|
||||||
|
| **Go (Golang)** | 高并发后端语言 | Google 开发,杀手锏是 goroutine——极轻量协程,一个程序可轻松开几万个,每个只占几 KB 内存,适合管理大量 WebSocket 长连接。 |
|
||||||
|
| **gorilla/websocket** | Go WebSocket 库 | Go 标准库无内置 WebSocket 支持,此库是社区最成熟的选择,处理了协议握手、帧解析等底层细节。 |
|
||||||
|
| **Redis** | 内存 KV 数据库 | 数据放在内存里,读写微秒级。本项目用于会话状态和对话上下文缓存,支持 TTL 过期自动清理。多 Gateway 实例通过 Redis 共享状态。 |
|
||||||
|
| **Viper** | Go 配置管理 | 读取 JSON/YAML/TOML 配置,支持环境变量覆盖,方便开发/测试/生产环境用不同配置。 |
|
||||||
|
| **Zap** | Go 结构化日志 | Uber 开源,输出 JSON 格式日志,方便工具搜索分析,性能远超标准库 log。 |
|
||||||
|
|
||||||
|
## AI 服务相关
|
||||||
|
|
||||||
|
| 名词 | 一句话 | 展开 |
|
||||||
|
|------|--------|------|
|
||||||
|
| **多模态 LLM** | 能读文字又能看图片的大语言模型 | GPT-4o(OpenAI)/ Claude Sonnet(Anthropic),给照片+问题能"看懂"照片再回答。 |
|
||||||
|
| **STT** | 语音转文字 | Speech-to-Text。Deepgram 流式识别延迟 <500ms。备选 FunASR(阿里开源,可自部署)。 |
|
||||||
|
| **TTS** | 文字转语音 | Text-to-Speech。OpenAI TTS 音质接近真人。Edge TTS 免费。支持流式——边生成边读,不必等全部生成完。 |
|
||||||
|
| **GPT-4o-mini** | 轻量分类模型 | 又快又便宜的小模型,用于模型路由——先用小模型判断问题复杂度,简单问题走小模型省 API 费用。 |
|
||||||
27
docs/README.md
Normal file
27
docs/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# CamTalk 设计文档
|
||||||
|
|
||||||
|
CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头和麦克风与 AI 交互,AI 理解视觉场景和语音输入后给出自然回应。
|
||||||
|
|
||||||
|
## 文档索引
|
||||||
|
|
||||||
|
| 文档 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| [01-项目概述](01-项目概述.md) | 项目目标、核心挑战、交付物 |
|
||||||
|
| [02-系统架构](02-系统架构.md) | 三层架构、技术栈、核心交互流程、前后端模块、存储策略、部署架构 |
|
||||||
|
| [03-接口文档](03-接口文档.md) | WebSocket 协议、REST API、数据模型、错误码、连接管理(**实现时首先阅读**) |
|
||||||
|
| [04-技术选型](04-技术选型.md) | 持久化层(PostgreSQL)和前端边缘处理层的选型对比与决策理由 |
|
||||||
|
| [05-用户故事](05-用户故事.md) | P0/P1/P2 用户故事、验收标准、优先级决策依据 |
|
||||||
|
| [06-语音交互](06-语音交互.md) | VAD → STT → LLM → TTS 全链路、延迟优化 |
|
||||||
|
| [07-视觉理解](07-视觉理解.md) | 帧采样策略、图像编码、多模态 LLM 输入机制 |
|
||||||
|
| [08-成本控制](08-成本控制.md) | 智能采样、端云协同、模型分级、缓存复用 |
|
||||||
|
| [09-技术名词解释](09-技术名词解释.md) | 前端/后端/AI 服务技术名词简明解释 |
|
||||||
|
|
||||||
|
## 推荐阅读顺序
|
||||||
|
|
||||||
|
1. **01-项目概述** — 了解项目目标
|
||||||
|
2. **02-系统架构** — 理解三层架构和技术栈全貌
|
||||||
|
3. **03-接口文档** — 前后端通信契约,实现时的最高依据
|
||||||
|
4. **04-技术选型** — 了解为什么选这些技术
|
||||||
|
5. **05-用户故事** — 明确功能优先级
|
||||||
|
6. **06~08** — 各技术领域的详细设计
|
||||||
|
7. **09-技术名词解释** — 遇到不熟悉的名词时查阅
|
||||||
Reference in New Issue
Block a user