From ee004ff628e5e37262a6a1a89e4f2a4d71350f23 Mon Sep 17 00:00:00 2001 From: hhs <386998068@qq.com> Date: Fri, 12 Jun 2026 15:55:55 +0800 Subject: [PATCH 1/2] vault backup: 2026-06-12 15:55:55 --- 课题一/AI 视觉对话助手.md | 3 +- .../{持久化技术选型.md => 项目实现/技术选型.md} | 109 +++- 课题一/AI 视觉对话助手/项目实现/接口文档.md | 607 ++++++++++++++++++ .../{ => 项目实现}/项目架构与技术栈.md | 3 +- 4 files changed, 716 insertions(+), 6 deletions(-) rename 课题一/AI 视觉对话助手/{持久化技术选型.md => 项目实现/技术选型.md} (60%) create mode 100644 课题一/AI 视觉对话助手/项目实现/接口文档.md rename 课题一/AI 视觉对话助手/{ => 项目实现}/项目架构与技术栈.md (99%) diff --git a/课题一/AI 视觉对话助手.md b/课题一/AI 视觉对话助手.md index f70e207..8852fcd 100644 --- a/课题一/AI 视觉对话助手.md +++ b/课题一/AI 视觉对话助手.md @@ -39,7 +39,8 @@ create time: 2026-06-12 11:13 ## 关联笔记 - [[项目架构与技术栈]] -- [[持久化技术选型]] +- [[接口文档]] +- [[技术选型]] - [[视觉理解]] - [[语音交互]] - [[成本控制]] diff --git a/课题一/AI 视觉对话助手/持久化技术选型.md b/课题一/AI 视觉对话助手/项目实现/技术选型.md similarity index 60% rename from 课题一/AI 视觉对话助手/持久化技术选型.md rename to 课题一/AI 视觉对话助手/项目实现/技术选型.md index 21554b1..bb72f6b 100644 --- a/课题一/AI 视觉对话助手/持久化技术选型.md +++ b/课题一/AI 视觉对话助手/项目实现/技术选型.md @@ -1,16 +1,26 @@ --- -tags: [数据库, PostgreSQL, 持久化, 架构设计, 拓展功能] +tags: [技术选型, 数据库, PostgreSQL, 持久化, 前端, 边缘推理, 架构设计] create time: 2026-06-12 15:33 --- -# 持久化技术选型 +# 技术选型 ## 概述 -本文档是 [[项目架构与技术栈]] 的拓展阅读——在核心功能(实时视觉对话)跑通之后,如果需要**保存对话历史、统计用量成本、管理用户偏好**,就需要引入持久化层。本文对比主流数据库方案,解释为什么推荐 PostgreSQL,以及在什么情况下应该选择其他方案。 +本文档是 [[项目架构与技术栈]] 的补充阅读——记录项目中各项技术的**选型过程、替代方案对比和决策理由**。技术选型没有"绝对正确",只有"更适合",所以每个选型都会列出候选方案和取舍逻辑,方便后续回顾和复盘。 + +```mermaid +graph TD + A["技术选型"] --> B["持久化层"] + A --> C["前端边缘处理层"] + B --> B1["数据库选型: PostgreSQL"] + C --> C1["边缘推理: ONNX Runtime Web"] + C --> C2["语音检测: @ricky0123/vad-web"] + C --> C3["媒体采集: MediaDevices API"] +``` > [!info] 定位 -> 这是一篇**拓展选型文档**,不阻塞 MVP 开发。MVP 阶段用 Redis 做会话存储即可;当产品需要"历史可查、成本可算"时,再引入本节方案。 +> 持久化部分是**拓展选型文档**,不阻塞 MVP 开发。MVP 阶段用 Redis 做会话存储即可;当产品需要"历史可查、成本可算"时,再引入持久化方案。前端边缘处理部分则是 MVP 阶段就需要确定的技术栈。 ## 正文 @@ -243,6 +253,97 @@ graph LR > [!info] 写入策略 > 建议采用**异步写入**——实时对话消息先写 Redis(快),然后异步批量刷入 PostgreSQL(慢)。这样不会因为数据库写入延迟影响对话体验。可以用 Go channel + goroutine 实现简单的异步写入队列。 +--- + +## 前端边缘处理层选型 + +> [!info] 选型背景 +> 项目的核心交互流程是"用户说话 → AI 看 → AI 回答"。前端需要完成**媒体采集、语音检测、轻量推理**三件事,然后才把"值得处理的数据"发给后端。这三个环节的技术选型直接影响**交互延迟和首屏加载速度**。 + +### 总览 + +| 能力 | 当前选型 | 选择理由 | +|------|---------|---------| +| 边缘推理 | **ONNX Runtime Web** | 通用推理引擎,模型无关,WASM 加速 | +| 语音检测 | **@ricky0123/vad-web** | 包装原生 WebRTC VAD,零延迟,体积极小 | +| 媒体采集 | **MediaDevices API** | 浏览器原生接口,无中间层,零依赖 | + +### 边缘推理:ONNX Runtime Web + +在浏览器端跑 VAD 和关键帧检测,需要一个轻量推理引擎。候选方案如下: + +```mermaid +graph TD + A["浏览器端推理需求"] --> B["ONNX Runtime Web"] + A --> C["TensorFlow.js"] + A --> D["MediaPipe"] + A --> E["Transformers.js"] + B --> B1["通用推理引擎"] + C --> C1["TF 生态专用"] + D --> D1["开箱即用 CV 任务"] + E --> E1["HuggingFace 生态"] +``` + +| 方案 | 特点 | 适用场景 | +|------|------|---------| +| **ONNX Runtime Web** | 通用推理引擎,支持任意 ONNX 模型,WASM 加速 | 需要在浏览器跑**自定义模型**(VAD、关键帧检测) | +| **TensorFlow.js** | Google 生态,支持 WebGL/WebGPU 加速 | 模型本身就是 TF 格式,或需要 GPU 加速 | +| **MediaPipe** | Google 出品,封装了姿态/手势/人脸等开箱即用方案 | 只需要常见 CV 任务(人脸检测、姿态估计),不需要自定义模型 | +| **Transformers.js** | Hugging Face 生态,直接跑 HuggingFace 上的模型 | 想快速集成 NLP/CV 预训练模型(如 Whisper、CLIP) | + +> [!question] 思考 +> 为什么 ONNX Runtime Web 胜出?项目需要同时跑**两种**轻量模型——VAD 和关键帧检测,这是自定义 pipeline,不是单一 CV 任务。ONNX 是跨框架的通用格式,可以在 Python 端用 PyTorch 训练,导出 ONNX,然后在浏览器用统一引擎加载。TensorFlow.js 被锁死在 TF 生态,MediaPipe 虽然开箱即用但灵活性不够(不能自定义模型逻辑)。**ONNX Runtime 的核心优势是"模型无关"**。 + +### 语音检测:@ricky0123/vad-web + +VAD(Voice Activity Detection)是交互流程的**起始触发器**——用户有没有在说话?触发必须又快又准。 + +| 方案 | 特点 | 适用场景 | +|------|------|---------| +| **@ricky0123/vad-web** | 基于 WebRTC VAD,2KB,纯前端零延迟 | 只需要"有没有人说话"的二分类判断 | +| **Web Audio API + 能量检测** | 用 AnalyserNode 计算音量 RMS,阈值判断 | 极简场景,但抗噪能力差 | +| **ONNX 跑 Silero VAD** | 神经网络级 VAD,准确率高,但推理开销更大 | 嘈杂环境下需要更精准的检测 | +| **Picovoice Porcupine** | 商业级唤醒词引擎,支持自定义唤醒词 | 需要"嘿 Siri"式的唤醒词功能 | + +```mermaid +graph LR + A["VAD 方案对比"] --> B["轻量级"] + A --> C["重量级"] + B --> B1["能量检测: 最轻, 不抗噪"] + B --> B2["vad-web: 轻量, WebRTC 原生算法"] + C --> C1["Silero VAD: 精准, 需加载 ONNX 模型"] + C --> C2["Porcupine: 商业级, 需付费"] +``` + +> [!question] 思考 +> 用手写能量检测虽然更轻,但不抗噪(咳嗽、环境噪音都会误触发);用 Silero VAD 虽然更准,但需要加载 ONNX 模型,增加首屏时间和内存占用。**@ricky0123/vad-web 是"够用且最轻"的平衡点**——直接包装浏览器原生的 WebRTC VAD 算法(C 代码编译为 WASM),延迟接近零。 + +### 媒体采集:MediaDevices API + +摄像头和麦克风的采集是整个流程的源头。 + +| 方案 | 特点 | 适用场景 | +|------|------|---------| +| **MediaDevices API** | 浏览器原生 API,零依赖,直接拿 MediaStream | 标准的摄像头/麦克风采集 | +| **react-webcam 等封装库** | React 组件封装,减少胶水代码 | 快速原型,但灵活性受限 | +| **WebRTC(含 getUserMedia)** | 完整的点对点通信栈 | 需要浏览器之间直接传音视频(如视频会议) | +| **Capacitor/Cordova 原生桥** | 混合 App 方案,调用原生摄像头 | 目标不是浏览器而是移动 App | + +> [!question] 思考 +> `navigator.mediaDevices.getUserMedia()` 是所有浏览器音视频采集的**唯一标准入口**。所有上层封装库底层都是调这个 API。项目需要的是原始 MediaStream(直接送进 VAD 和关键帧检测),不是封装好的组件。用封装库反而要多一层解包,**没有中间商**。 + +### 选型共同逻辑 + +这三个技术选择有一个共同的决策模式——**选择最薄的抽象层**: + +| 技术 | "最薄"体现在哪里 | +|------|----------------| +| **ONNX Runtime Web** | 不绑定特定框架,模型格式通用 | +| **@ricky0123/vad-web** | 包装原生 WebRTC VAD,没有多余的模型加载 | +| **MediaDevices API** | 直接用浏览器原生接口,不加封装层 | + +这与 [[项目架构与技术栈]] 中"前端做轻量预处理"的原则一致:前端层只需要采集和判断"有没有值得发给后端的数据",不需要复杂的模型推理能力。更重的方案(TensorFlow.js、Silero VAD)在后端 Go 网关和云端 AI 服务面前,属于在不该重的地方加重。 + ## 关联笔记 - [[项目架构与技术栈]] diff --git a/课题一/AI 视觉对话助手/项目实现/接口文档.md b/课题一/AI 视觉对话助手/项目实现/接口文档.md new file mode 100644 index 0000000..851a8d8 --- /dev/null +++ b/课题一/AI 视觉对话助手/项目实现/接口文档.md @@ -0,0 +1,607 @@ +--- +tags: [API, WebSocket, 接口设计, MVP, Go, TypeScript, 扩展性] +create time: 2026-06-12 15:41 +--- + +# 接口文档 + +## 概述 + +本文档定义 AI 视觉对话助手的**前后端通信接口**。以 MVP 为核心目标:用 WebSocket 承载实时对话,用最少的 REST 端点支撑基础运维。**暂不实现持久化**,但通过 Repository 接口模式为后续扩展(对话历史、用量统计)预留干净的接入点。 + +> [!info] 设计原则 +> - **WebSocket 为主**:实时对话是核心场景,所有对话数据走 WebSocket +> - **REST 为辅**:仅用于健康检查、会话管理等低频操作 +> - **接口先行**:先定义契约,再填充实现——前后端可并行开发 + +## 正文 + +### 接口全景 + +```mermaid +graph LR + subgraph Client["浏览器"] + WS_C["WebSocket Client"] + HTTP_C["HTTP Client"] + end + + subgraph Server["Go Gateway :8080"] + WS_EP["/ws"] + HEALTH_EP["/api/health"] + SESSION_EP["/api/sessions"] + end + + WS_C <-->|"实时对话"| WS_EP + HTTP_C -->|"GET"| HEALTH_EP + HTTP_C <-->|"POST / DELETE"| SESSION_EP +``` + +### 一、WebSocket 协议 + +连接地址:`ws://localhost:8080/ws` + +#### 1.1 连接生命周期 + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + + C->>S: WebSocket Upgrade 请求 + S-->>C: 101 Switching Protocols + S-->>C: {"type":"connected","session_id":"..."} + Note over C,S: 连接建立,进入对话 + + C->>S: {"type":"query",...} + S-->>C: {"type":"stt_result",...} + S-->>C: {"type":"llm_chunk",...} + S-->>C: {"type":"llm_chunk",...} + S-->>C: {"type":"llm_done",...} + S-->>C: {"type":"tts_audio",...} + + C->>S: {"type":"query",...} + Note over C,S: 持续对话... + + C->>S: {"type":"ping"} + S-->>C: {"type":"pong"} + + C->>S: WebSocket Close + S-->>C: WebSocket Close Ack +``` + +#### 1.2 消息格式约定 + +所有 WebSocket 消息均为 **JSON 文本帧**,统一结构: + +```typescript +// 通用消息信封 +interface WsMessage { + type: string; // 消息类型,必填 + request_id?: string; // 可选,用于请求-响应关联 + timestamp?: number; // 可选,毫秒时间戳 + [key: string]: any; // 类型特定字段 +} +``` + +#### 1.3 客户端 → 服务端消息 + +##### `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" +} +``` + +> [!question] 思考 +> 为什么图像和音频放在同一条消息里?因为 VAD 检测到用户说完话时,需要同时捕获"此刻的画面"和"说的话",拆成两条消息会增加时序同步的复杂度。 + +##### `config` —— 更新会话配置 + +运行时调整 AI 行为参数,无需重建连接: + +```typescript +interface ConfigMessage { + type: "config"; + payload: { + tts_enabled?: boolean; // 是否开启语音合成,默认 true + detail_level?: "low" | "high"; // 图像精度,默认 "low" + language?: string; // 交互语言,默认 "zh-CN" + }; +} +``` + +##### `interrupt` —— 打断当前回复 + +用户在 AI 回复过程中再次说话,打断正在进行的 LLM/TTS 流: + +```typescript +interface InterruptMessage { + type: "interrupt"; + request_id?: string; // 可选,指定打断哪次请求 +} +``` + +##### `ping` —— 心跳保活 + +```typescript +interface PingMessage { + type: "ping"; +} +``` + +#### 1.4 服务端 → 客户端消息 + +##### `connected` —— 连接建立确认 + +```typescript +interface ConnectedMessage { + type: "connected"; + session_id: string; // 服务端生成的会话 ID + server_version: string; // 服务端版本号,如 "0.1.0" +} +``` + +##### `stt_result` —— 语音识别结果 + +LLM 推理前,先返回 STT 识别出的文本,让用户看到"我听到了什么": + +```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; // 输入 token 数 + completion: number; // 输出 token 数 + 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"; +} +``` + +#### 1.5 消息流时序总览 + +一次完整交互的消息流转: + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + + Note over C: VAD 检测到语音结束 + C->>S: query {image, audio} + S-->>C: stt_result {text, is_final: true} + + loop LLM 流式输出 + S-->>C: llm_chunk {delta: "这"} + S-->>C: llm_chunk {delta: "是一"} + S-->>C: llm_chunk {delta: "朵花..."} + end + + S-->>C: llm_done {full_text, tokens_used, latency_ms} + + loop TTS 音频流 + S-->>C: tts_audio {audio, is_last: false} + S-->>C: tts_audio {audio, is_last: true} + end +``` + +### 二、REST API + +MVP 阶段仅暴露最少量的 HTTP 端点: + +#### 2.1 健康检查 + +```http +GET /api/health +``` + +响应: + +```json +{ + "status": "ok", + "version": "0.1.0", + "uptime_seconds": 3600, + "active_sessions": 42 +} +``` + +#### 2.2 创建会话(可选) + +MVP 阶段 WebSocket 连接即自动创建会话,此端点为**预留扩展**: + +```http +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" +} +``` + +#### 2.3 销毁会话 + +```http +DELETE /api/sessions/{session_id} +``` + +响应:`204 No Content` + +#### 2.4 预留端点(暂不实现) + +> [!info] 后续扩展 +> 引入持久化后,按需添加以下端点: + +| 端点 | 方法 | 用途 | MVP 状态 | +|------|------|------|---------| +| `/api/sessions/{id}/messages` | GET | 查询对话历史 | 预留,暂不实现 | +| `/api/usage` | GET | 查询用量统计 | 预留,暂不实现 | +| `/api/users/{id}/preferences` | GET/PUT | 用户偏好管理 | 预留,暂不实现 | + +### 三、数据模型 + +#### 3.1 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"` +} +``` + +```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) +} + +type UsageRecord struct { + SessionID string `json:"session_id"` + LLMTokens int `json:"llm_tokens"` + STTSeconds float64 `json:"stt_seconds"` + TTSChars int `json:"tts_chars"` + EstimatedCost float64 `json:"estimated_cost"` +} + +type UsageDaily struct { + Date string `json:"date"` + LLMTokens int `json:"llm_tokens"` + EstimatedCost float64 `json:"estimated_cost"` +} +``` + +#### 3.2 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; // 仅 assistant 消息 +} + +// ---- WebSocket 消息联合类型 ---- + +type ServerMessage = + | ConnectedMessage + | STTResultMessage + | LLMChunkMessage + | LLMDoneMessage + | TTSAudioMessage + | ErrorMessage + | PongMessage; + +type ClientMessage = + | QueryMessage + | ConfigMessage + | InterruptMessage + | PingMessage; +``` + +### 四、扩展接口设计 + +通过**接口(Interface)模式**隔离存储层,MVP 用内存实现,后续替换为数据库实现——业务逻辑层零改动。 + +#### 4.1 Repository 接口 + +```mermaid +graph TD + subgraph Biz["业务逻辑层(不变)"] + ORCH["AI Orchestrator"] + SM["Session Manager"] + end + + subgraph Repo["存储接口层"] + HR["HistoryRepository"] + UR["UsageRepository"] + end + + subgraph Impl_MVP["MVP 实现"] + MEM_H["InMemoryHistory"] + MEM_U["InMemoryUsage"] + end + + subgraph Impl_Future["后续实现"] + PG_H["PgHistory"] + PG_U["PgUsage"] + end + + ORCH --> HR + ORCH --> UR + SM --> HR + HR --> MEM_H + UR --> MEM_U + HR -.->|"替换"| PG_H + UR -.->|"替换"| PG_U +``` + +#### 4.2 MVP 内存实现 + +```go +// InMemoryHistory —— MVP 阶段的对话历史实现 +// 数据存在内存 map 中,连接断开即丢 +type InMemoryHistory struct { + mu sync.RWMutex + sessions map[string][]Message // sessionID -> messages +} + +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 +} +``` + +#### 4.3 后续替换为 PostgreSQL + +引入持久化时,只需新增一个实现,无需修改业务逻辑: + +```go +// PgHistory —— PostgreSQL 实现(后续扩展) +type PgHistory struct { + pool *pgxpool.Pool +} + +func (p *PgHistory) SaveMessage(ctx context.Context, sessionID string, msg Message) error { + _, err := p.pool.Exec(ctx, + `INSERT INTO messages (session_id, role, content, image_url, tokens_used) + VALUES ($1, $2, $3, $4, $5)`, + sessionID, msg.Role, msg.Content, msg.ImageURL, msg.TokensUsed, + ) + return err +} + +func (p *PgHistory) GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) { + rows, _ := p.pool.Query(ctx, + `SELECT role, content, image_url, tokens_used + FROM messages WHERE session_id = $1 + ORDER BY created_at DESC LIMIT $2`, + sessionID, limit, + ) + defer rows.Close() + // ... scan and return +} +``` + +> [!question] 思考 +> 这就是**依赖倒置原则**——业务层依赖接口(`HistoryRepository`),不依赖具体实现。MVP 阶段注入 `InMemoryHistory`,上线时一行代码换成 `PgHistory`,其余逻辑完全不动。 + +#### 4.4 注入点示例 + +在应用启动时根据配置选择实现: + +```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), + } +} +``` + +### 五、错误码定义 + +| 错误码 | 含义 | 客户端处理建议 | +|--------|------|--------------| +| `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` | 服务端内部错误 | 提示用户重试 | + +> [!tip] 错误处理原则 +> 客户端收到 `error` 消息后,应根据错误码分别处理:可恢复的(如 `RATE_LIMITED`)自动重试;不可恢复的(如 `IMAGE_TOO_LARGE`)提示用户调整;服务端异常(如 `INTERNAL_ERROR`)记录日志并提示重试。 + +### 六、连接管理 + +#### 心跳机制 + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + + loop 每 30 秒 + C->>S: ping + S-->>C: pong + end + + Note over S: 超过 60 秒无 ping + S->>S: 判定连接断开 + S->>S: 清理会话资源 +``` + +#### 重连策略 + +客户端断线后按**指数退避**重连: + +```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 +``` + +## 关联笔记 + +- [[项目架构与技术栈]] +- [[技术选型]] +- [[项目架构与技术栈/技术名词解释]] diff --git a/课题一/AI 视觉对话助手/项目架构与技术栈.md b/课题一/AI 视觉对话助手/项目实现/项目架构与技术栈.md similarity index 99% rename from 课题一/AI 视觉对话助手/项目架构与技术栈.md rename to 课题一/AI 视觉对话助手/项目实现/项目架构与技术栈.md index a5680d6..965bd90 100644 --- a/课题一/AI 视觉对话助手/项目架构与技术栈.md +++ b/课题一/AI 视觉对话助手/项目实现/项目架构与技术栈.md @@ -390,4 +390,5 @@ graph TD - [[成本控制]] - [[用户故事]] - [[项目架构与技术栈/技术名词解释]] -- [[持久化技术选型]] +- [[技术选型]] +- [[接口文档]] From 04c01720381d1b699b094e34a8be58f72d02a05d Mon Sep 17 00:00:00 2001 From: hhs <386998068@qq.com> Date: Fri, 12 Jun 2026 16:05:34 +0800 Subject: [PATCH 2/2] vault backup: 2026-06-12 16:05:34 --- 课题一/AI 视觉对话助手/项目实现/技术选型.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/课题一/AI 视觉对话助手/项目实现/技术选型.md b/课题一/AI 视觉对话助手/项目实现/技术选型.md index bb72f6b..0ef9cff 100644 --- a/课题一/AI 视觉对话助手/项目实现/技术选型.md +++ b/课题一/AI 视觉对话助手/项目实现/技术选型.md @@ -192,10 +192,11 @@ func GetWeeklyUsage(ctx context.Context, pool *pgxpool.Pool, userID string) ([]U } ``` -JSONB 查询示例——在对话上下文中搜索包含特定关键词的消息: +JSONB 包容查询示例——精确匹配 JSONB 子结构: ```sql --- 在 messages.content(JSONB)中搜索包含"花"的用户消息 +-- @> 是包容操作符,检查 content 是否包含 {"text": "花"} 这个子结构 +-- 适合"字段精确匹配"场景;若需模糊关键词搜索,应使用 tsvector 全文检索 SELECT id, content, created_at FROM messages WHERE role = 'user' @@ -292,7 +293,7 @@ graph TD | **Transformers.js** | Hugging Face 生态,直接跑 HuggingFace 上的模型 | 想快速集成 NLP/CV 预训练模型(如 Whisper、CLIP) | > [!question] 思考 -> 为什么 ONNX Runtime Web 胜出?项目需要同时跑**两种**轻量模型——VAD 和关键帧检测,这是自定义 pipeline,不是单一 CV 任务。ONNX 是跨框架的通用格式,可以在 Python 端用 PyTorch 训练,导出 ONNX,然后在浏览器用统一引擎加载。TensorFlow.js 被锁死在 TF 生态,MediaPipe 虽然开箱即用但灵活性不够(不能自定义模型逻辑)。**ONNX Runtime 的核心优势是"模型无关"**。 +> 为什么 ONNX Runtime Web 胜出?项目需要同时跑**两种**轻量模型——VAD 和关键帧检测,这是自定义 pipeline,不是单一 CV 任务。ONNX 是跨框架的通用格式,无论模型用什么框架训练,都可以导出为 ONNX 并在浏览器中用统一引擎加载。TensorFlow.js 被锁死在 TF 生态,MediaPipe 虽然开箱即用但灵活性不够(不能自定义模型逻辑)。**ONNX Runtime 的核心优势是"模型无关"**。 ### 语音检测:@ricky0123/vad-web @@ -300,7 +301,7 @@ VAD(Voice Activity Detection)是交互流程的**起始触发器**——用 | 方案 | 特点 | 适用场景 | |------|------|---------| -| **@ricky0123/vad-web** | 基于 WebRTC VAD,2KB,纯前端零延迟 | 只需要"有没有人说话"的二分类判断 | +| **@ricky0123/vad-web** | 基于 WebRTC VAD,体积极小(~100KB 含 WASM),纯前端零延迟 | 只需要"有没有人说话"的二分类判断 | | **Web Audio API + 能量检测** | 用 AnalyserNode 计算音量 RMS,阈值判断 | 极简场景,但抗噪能力差 | | **ONNX 跑 Silero VAD** | 神经网络级 VAD,准确率高,但推理开销更大 | 嘈杂环境下需要更精准的检测 | | **Picovoice Porcupine** | 商业级唤醒词引擎,支持自定义唤醒词 | 需要"嘿 Siri"式的唤醒词功能 |