这个问题本质是:

LLM 是流式输出(streaming),TTS 可能延迟开启,需要“缓存 + 实时转发”机制。

你需要一个 可切换的文本流广播器

🎯 目标行为

  1. LLM 正在 streaming 生成文本

  2. 如果 TTS 没开启:

    • 文本先缓存到 buffer
  3. 当 TTS 开启:

    • 先把 buffer 里的内容全部发送给 TTS
    • 然后继续把后续 streaming 内容实时发给 TTS
  4. LLM 仍然持续生成,不受影响

🧠 正确架构(推荐)

用:

🏗 结构设计

type StreamDispatcher struct {
    mu        sync.Mutex
    buffer    strings.Builder   // 缓存历史文本
    ttsOn     bool              // 是否开启 TTS
    ttsChan   chan string       // 给 TTS 的文本流
}

1️⃣ 初始化

func NewStreamDispatcher() *StreamDispatcher {
    return &StreamDispatcher{
        ttsChan: make(chan string, 100),
    }
}

2️⃣ LLM 每次生成文本时调用

func (d *StreamDispatcher) OnLLMDelta(text string) {
    d.mu.Lock()
    defer d.mu.Unlock()

    // 始终保存
    d.buffer.WriteString(text)

    // 如果 TTS 已开启,实时发送
    if d.ttsOn {
        d.ttsChan <- text
    }
}

3️⃣ 开启 TTS

func (d *StreamDispatcher) EnableTTS() {
    d.mu.Lock()
    defer d.mu.Unlock()

    if d.ttsOn {
        return
    }

    d.ttsOn = true

    // 先发送历史缓存
    history := d.buffer.String()
    if history != "" {
        d.ttsChan <- history
    }
}

4️⃣ TTS 监听协程

func (d *StreamDispatcher) StartTTSListener() {
    go func() {
        for text := range d.ttsChan {
            SendToTTS(text)
        }
    }()
}

5️⃣ 使用方式

dispatcher := NewStreamDispatcher()
dispatcher.StartTTSListener()

// LLM streaming
go func() {
    for delta := range llmStream {
        dispatcher.OnLLMDelta(delta)
    }
}()

// 用户稍后开启 TTS
dispatcher.EnableTTS()

🔥 整个流程图

LLM streaming
     ↓
OnLLMDelta()
     ↓
buffer += text
     ↓
if ttsOn → send to ttsChan

当开启 TTS:

EnableTTS()
     ↓
把 buffer 全部发给 ttsChan
     ↓
后续实时文本继续进 ttsChan

⚡ 更高级版本(推荐生产级)

上面有一个小问题:

如果 buffer 很大,一次性发给 TTS 可能太长。

可以改成:

按句子切分发送

func splitSentence(text string) []string {
    return strings.Split(text, "。")
}

开启 TTS 时:

history := d.buffer.String()
sentences := splitSentence(history)

for _, s := range sentences {
    if strings.TrimSpace(s) != "" {
        d.ttsChan <- s
    }
}

🚀 更专业的架构(事件驱动版本)

如果你是做高并发系统(像你现在的架构):

建议做成:

这样:

LLM → EventBus → 
                → UI
                → Logger
                → TTS (可动态订阅)

开启 TTS 就是:

bus.Subscribe("tts")

🧠 为什么不能只用 channel?

因为:

channel 不能回放历史数据

而你需要:

“开启时重放历史 + 实时接收新数据”

这必须要有 buffer。

🎯 关键设计模式

这是典型的:

Replayable Stream / Hot Stream + Late Subscriber

类似:

💡 如果你想做成更优雅的结构

我可以给你写一个:

的完整 Engine 版本。

你要的是:

  1. 简单版本(够用就行)
  2. 生产级事件总线版本
  3. 高并发音频级版本(支持边生成边播放)

告诉我你的使用场景,我给你最合适的架构。