Files
note/课题一/AI 视觉对话助手/项目实现/接口文档.md
2026-06-12 15:55:55 +08:00

15 KiB
Raw Blame History

tags, create time
tags create time
API
WebSocket
接口设计
MVP
Go
TypeScript
扩展性
2026-06-12 15:41

接口文档

概述

本文档定义 AI 视觉对话助手的前后端通信接口。以 MVP 为核心目标:用 WebSocket 承载实时对话,用最少的 REST 端点支撑基础运维。暂不实现持久化,但通过 Repository 接口模式为后续扩展(对话历史、用量统计)预留干净的接入点。

[!info] 设计原则

  • WebSocket 为主:实时对话是核心场景,所有对话数据走 WebSocket
  • REST 为辅:仅用于健康检查、会话管理等低频操作
  • 接口先行:先定义契约,再填充实现——前后端可并行开发

正文

接口全景

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 连接生命周期

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 文本帧,统一结构:

// 通用消息信封
interface WsMessage {
  type: string;        // 消息类型,必填
  request_id?: string; // 可选,用于请求-响应关联
  timestamp?: number;  // 可选,毫秒时间戳
  [key: string]: any;  // 类型特定字段
}

1.3 客户端 → 服务端消息

query —— 发起一次视觉对话

用户说完话后,客户端同时发送当前图像帧和语音片段:

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 行为参数,无需重建连接:

interface ConfigMessage {
  type: "config";
  payload: {
    tts_enabled?: boolean;    // 是否开启语音合成,默认 true
    detail_level?: "low" | "high"; // 图像精度,默认 "low"
    language?: string;        // 交互语言,默认 "zh-CN"
  };
}
interrupt —— 打断当前回复

用户在 AI 回复过程中再次说话,打断正在进行的 LLM/TTS 流:

interface InterruptMessage {
  type: "interrupt";
  request_id?: string; // 可选,指定打断哪次请求
}
ping —— 心跳保活
interface PingMessage {
  type: "ping";
}

1.4 服务端 → 客户端消息

connected —— 连接建立确认
interface ConnectedMessage {
  type: "connected";
  session_id: string;       // 服务端生成的会话 ID
  server_version: string;   // 服务端版本号,如 "0.1.0"
}
stt_result —— 语音识别结果

LLM 推理前,先返回 STT 识别出的文本,让用户看到"我听到了什么"

interface STTResultMessage {
  type: "stt_result";
  request_id: string;
  text: string;        // 识别出的用户语音文本
  is_final: boolean;   // 是否为最终结果(流式场景下可能分多段)
}
llm_chunk —— LLM 流式输出片段
interface LLMChunkMessage {
  type: "llm_chunk";
  request_id: string;
  delta: string;       // 本次增量文本
  role: "assistant";
}
llm_done —— LLM 输出完成
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 音频流片段
interface TTSAudioMessage {
  type: "tts_audio";
  request_id: string;
  audio: string;         // Base64 编码的音频片段
  mime_type: string;     // "audio/mp3" 或 "audio/pcm"
  is_last: boolean;      // 是否为最后一片
}
error —— 错误通知
interface ErrorMessage {
  type: "error";
  request_id?: string;   // 关联的请求(可选)
  code: string;          // 错误码,见下方错误码表
  message: string;       // 人类可读的错误描述
}
pong —— 心跳响应
interface PongMessage {
  type: "pong";
}

1.5 消息流时序总览

一次完整交互的消息流转:

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 健康检查

GET /api/health

响应:

{
  "status": "ok",
  "version": "0.1.0",
  "uptime_seconds": 3600,
  "active_sessions": 42
}

2.2 创建会话(可选)

MVP 阶段 WebSocket 连接即自动创建会话,此端点为预留扩展

POST /api/sessions
Content-Type: application/json

{
  "user_id": "optional-user-id",
  "config": {
    "tts_enabled": true,
    "detail_level": "low",
    "language": "zh-CN"
  }
}

响应:

{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2026-06-12T15:41:00Z"
}

2.3 销毁会话

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 后端模型

// ---- 核心模型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"`
}
// ---- 预留模型(持久化扩展)----

// 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 前端模型

// ---- 核心模型 ----

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 接口

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 内存实现

// 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

引入持久化时,只需新增一个实现,无需修改业务逻辑:

// 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 注入点示例

在应用启动时根据配置选择实现:

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)记录日志并提示重试。

六、连接管理

心跳机制

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: 清理会话资源

重连策略

客户端断线后按指数退避重连:

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

关联笔记