diff --git a/docs/03-接口文档.md b/docs/03-接口文档.md index 948dba2..f1b7e1c 100644 --- a/docs/03-接口文档.md +++ b/docs/03-接口文档.md @@ -256,7 +256,6 @@ POST /api/sessions Content-Type: application/json { - "user_id": "optional-user-id", "config": { "tts_enabled": true, "detail_level": "low", @@ -301,28 +300,18 @@ Go 网关内部与外部 AI 服务(Deepgram STT、GPT-4o、OpenAI TTS)的调 语音识别:接收前端采集的音频,返回识别文本。 ```go -// STTService 语音识别服务契约。 -type STTService interface { +// Service 语音识别服务契约。 +type Service interface { // Recognize 识别一段完整音频,返回最终文本。 - Recognize(ctx context.Context, audio []byte, opts STTOptions) (string, error) - - // RecognizeStream 流式识别(边说边识别,可选实现)。 - // audioStream 持续接收音频片段,返回的 channel 持续输出中间结果。 - RecognizeStream(ctx context.Context, audioStream <-chan []byte, opts STTOptions) (<-chan STTPartial, error) + Recognize(ctx context.Context, audio []byte, opts Options) (string, error) } -// STTOptions 语音识别参数。 -type STTOptions struct { +// Options 语音识别参数。 +type Options struct { Encoding string // "pcm_s16le" — 前端 VAD 输出格式 SampleRate int // 16000 — 前端麦克风采样率 Language string // "zh-CN" } - -// STTPartial 流式识别的中间/最终结果。 -type STTPartial struct { - Text string - IsFinal bool -} ``` **Deepgram 接入约定**: @@ -336,23 +325,23 @@ type STTPartial struct { 多模态推理:接收图像 + 文本 + 对话历史,流式返回回复。 ```go -// LLMService 多模态大模型服务契约。 -type LLMService interface { +// Service 多模态大模型服务契约。 +type Service interface { // ChatStream 流式推理,返回增量文本的 channel。 // 调用方必须消费 channel 直到 Done=true,否则需 cancel ctx 以释放连接。 - ChatStream(ctx context.Context, req LLMRequest) (<-chan LLMChunk, error) + ChatStream(ctx context.Context, req Request) (<-chan Chunk, error) } -// LLMRequest 推理请求。 -type LLMRequest struct { - Image []byte // JPEG 图片(已从 Base64 解码) - Text string // 用户语音识别后的文本 - History []Message // 最近 N 轮对话历史 - Language string // "zh-CN" +// Request 推理请求。 +type Request struct { + Image []byte // JPEG 图片(已从 Base64 解码) + Text string // 用户语音识别后的文本 + History []models.Message // 最近 N 轮对话历史 + Language string // "zh-CN" } -// LLMChunk 流式推理的一个增量片段。 -type LLMChunk struct { +// Chunk 流式推理的一个增量片段。 +type Chunk struct { Delta string // 增量文本 Done bool // 是否结束 TokensUsed *TokenUsage // 仅 Done=true 时有值 @@ -388,24 +377,24 @@ user: [图片 + 用户语音文本] 语音合成:接收文本流,输出音频 chunk 流。 ```go -// TTSService 语音合成服务契约。 -type TTSService interface { +// Service 语音合成服务契约。 +type Service interface { // SynthesizeStream 流式合成。 // textStream 接收句子级文本(由 Orchestrator 的句子切分器产出), // 返回的 channel 持续输出 MP3 音频 chunk。 - SynthesizeStream(ctx context.Context, textStream <-chan string, opts TTSOptions) (<-chan TTSChunk, error) + SynthesizeStream(ctx context.Context, textStream <-chan string, opts Options) (<-chan Chunk, error) } -// TTSOptions 合成参数。 -type TTSOptions struct { +// Options 合成参数。 +type Options struct { Voice string // "alloy" | "nova" | "shimmer" | ... Speed float64 // 1.0 为正常语速 OutputFmt string // "mp3" — 固定使用 MP3,浏览器原生支持 SampleRate int // 24000 } -// TTSChunk 一个音频片段。 -type TTSChunk struct { +// Chunk 一个音频片段。 +type Chunk struct { Audio []byte // MP3 音频数据(未 Base64 编码,由发送层编码) IsLast bool // 是否为最后一片 } @@ -446,73 +435,32 @@ LLM 流式输出: "这" "是一" "朵红色" "的花。" "它看起" "来很美 ### Orchestrator 接口 ```go -// Orchestrator AI 编排器,协调 STT → LLM → TTS 全链路。 -type Orchestrator struct { - stt STTService - llm LLMService - tts TTSService +// Orchestrator AI 编排器接口。 +type Orchestrator interface { + // ProcessQuery 处理一次完整的视觉对话请求。 + // 通过 sender 向前端实时推送 stt_result、llm_chunk、llm_done、tts_audio 消息。 + ProcessQuery(ctx context.Context, sessionID string, req models.WsQuery, + history []models.Message, sender Sender) error } -// ProcessQuery 处理一次完整的视觉对话请求。 -// 通过 client 向前端实时推送 stt_result、llm_chunk、llm_done、tts_audio 消息。 -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{ - Encoding: "pcm_s16le", SampleRate: 16000, Language: "zh-CN", - }) - 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, Language: "zh-CN", - }) - - sentenceCh := make(chan string, 4) - go func() { - defer close(sentenceCh) - var buf strings.Builder - var fullText strings.Builder - for chunk := range llmStream { - // 即时推送文字给客户端(逐 token 显示) - client.SendLLMChunk(req.RequestID, chunk.Delta) - fullText.WriteString(chunk.Delta) - buf.WriteString(chunk.Delta) - // 遇到句子边界就吐出 - if isSentenceEnd(chunk.Delta) { - sentenceCh <- buf.String() - buf.Reset() - } - } - // 最后一段不足一句的也吐出 - if buf.Len() > 0 { - sentenceCh <- buf.String() - } - // 推送 llm_done - client.SendLLMDone(req.RequestID, fullText.String(), chunk.TokensUsed, chunk.Model) - }() - - // Step 3: TTS 并行消费句子流 - ttsStream, _ := o.tts.SynthesizeStream(ctx, sentenceCh, TTSOptions{ - Voice: "alloy", OutputFmt: "mp3", SampleRate: 24000, - }) - for chunk := range ttsStream { - client.SendTTSAudio(req.RequestID, chunk.Audio, chunk.IsLast) - } -} - -// isSentenceEnd 判断 delta 中是否包含句子结束标志。 -func isSentenceEnd(delta string) bool { - return strings.ContainsAny(delta, "。!?\n.!?\n") +// Sender 抽象 WebSocket 消息推送能力,便于测试时 mock。 +type Sender interface { + SendSTTResult(result models.WsSTTResult) error + SendLLMChunk(chunk models.WsLLMChunk) error + SendLLMDone(done models.WsLLMDone) error + SendTTSAudio(audio models.WsTTSAudio) error + SendError(err models.WsError) error } ``` +**Pipeline 实现**(`internal/orchestrator/pipeline.go`): +1. Base64 解码音频/图片 +2. 调用 `stt.Recognize()` → 发送 `stt_result` +3. 调用 `llm.ChatStream()` 获取流式输出,goroutine 消费 token → 发送 `llm_chunk` + 句子切分 +4. 另一 goroutine 从句子 channel 读取 → 调用 `tts.SynthesizeStream()` → 发送 `tts_audio` +5. 流结束 → 发送 `llm_done` +6. TTS 失败静默跳过,STT/LLM 失败发送对应 error 消息 + ### 并发控制 - 每个 `ProcessQuery` 调用在独立 goroutine 中运行 @@ -584,9 +532,9 @@ session:{id}:history → List (对话历史) ### 接口定义 ```go -// SessionManager 会话管理器。 -// WebSocket Handler 通过此接口操作会话,不直接接触 Redis。 -type SessionManager interface { +// Manager 会话管理器接口。 +// WebSocket Handler 通过此接口操作会话,不直接接触存储层。 +type Manager interface { // Create 创建新会话,返回 session ID。 Create(ctx context.Context, config models.SessionConfig) (string, error) @@ -605,14 +553,20 @@ type SessionManager interface { // SetActiveRequest 标记当前正在处理的请求 ID(interrupt 用)。 SetActiveRequest(ctx context.Context, sessionID string, requestID string) error + // GetActiveRequestID 获取当前活跃请求 ID。 + GetActiveRequestID(ctx context.Context, sessionID string) (string, error) + // ClearActiveRequest 清除活跃请求标记(请求完成或中断后)。 ClearActiveRequest(ctx context.Context, sessionID string) error // Touch 刷新 TTL(心跳时调用)。 Touch(ctx context.Context, sessionID string) error - // Destroy 显式销毁会话(REST API DELETE 或连接断开清理)。 + // Destroy 显式销毁会话(REST API DELETE)。 Destroy(ctx context.Context, sessionID string) error + + // ActiveCount 返回当前活跃会话数(健康检查用)。 + ActiveCount() int } ``` @@ -629,7 +583,8 @@ case "query": history, _ := sessionMgr.GetHistory(ctx, sessionID, 20) // 获取对话上下文 - go orchestrator.ProcessQuery(ctx, client, &msg, history) // 异步编排 + sender := &WSClient{client: client, requestID: msg.RequestID} + go orch.ProcessQuery(ctx, sessionID, msg, history, sender) // 异步编排 // interrupt 分支 case "interrupt": @@ -648,26 +603,30 @@ case "interrupt": 联调阶段无 Redis 时,用同一接口的内存实现: ```go -type InMemorySessionManager struct { - mu sync.RWMutex - sessions map[string]*sessionEntry +type MemoryManager struct { + mu sync.RWMutex + sessions map[string]*sessionEntry + ttl time.Duration + maxHistory int + stopCleaner chan struct{} } type sessionEntry struct { session models.Session history []models.Message activeReqID string + lastActive time.Time } ``` 注入时根据配置切换: ```go -var sessionMgr SessionManager +var sessionMgr session.Manager if cfg.Redis.Addr != "" { - sessionMgr = NewRedisSessionManager(redisClient, 30*time.Minute, 20) + sessionMgr = session.NewRedisManager(redisClient, 30*time.Minute, 20) } else { - sessionMgr = NewInMemorySessionManager() + sessionMgr = session.NewMemoryManager(30*time.Minute, 20) } ``` @@ -931,10 +890,8 @@ type QueryRequest struct { } 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"` + Role string `json:"role"` // "user" | "assistant" + Content string `json:"content"` } ```