一、为什么需要 Streaming
非流式(传统方式): ┌──────────────────────────────────────────────┐ │ 用户:帮我写一封邮件给客户 │ │ │ │ 时间轴: │ │ 0s ──── 发送请求 │ │ 5s ──── 还在等 │ │ 10s ─── 还在等 │ │ 15s ─── 收到完整回复(用户已经不耐烦了) │ └──────────────────────────────────────────────┘ 流式(Streaming): ┌──────────────────────────────────────────────┐ │ 用户:帮我写一封邮件给客户 │ │ │ │ 时间轴: │ │ 0s ──── 发送请求 │ │ 1s ──── 看到"好的," │ │ 2s ──── 看到"我正在为您起草邮件" │ │ 3s ──── 看到"尊敬的客户," │ │ ... ──── 逐字显示,用户感知几乎无延迟 │ └──────────────────────────────────────────────┘Streaming 的核心价值:将 TTFB(首字节时间)从 10-20s 降到 1-2s,大幅提升用户体验。
二、SSE 协议基础
SSE(Server-Sent Events)是 Streaming 的标准协议,基于 HTTP 长连接。
SSE 数据格式: ─────────────────────────────────────────────── data: {"choices":[{"delta":{"content":"你好"}}]} data: {"choices":[{"delta":{"content":","}}]} data: {"choices":[{"delta":{"content":"世界"}}]} data: [DONE] ─────────────────────────────────────────────── 特点: 1. 每个事件以 "data: " 开头 2. 事件之间用空行分隔 3. 以 "[DONE]" 标记结束 4. 浏览器原生支持 EventSource API三、服务端 Streaming 实现
// streaming/server.go package streaming import ( "bufio" "bytes" "encoding/json" "fmt" "io" "log" "net/http" "sync" "time" ) // StreamEvent SSE 事件 type StreamEvent struct { ID string `json:"id,omitempty"` Event string `json:"event,omitempty"` Data json.RawMessage `json:"data"` Retry int `json:"retry,omitempty"` // 重连间隔(毫秒) } // StreamWriter SSE 写入器 type StreamWriter struct { writer http.ResponseWriter flusher http.Flusher mu sync.Mutex closed bool } // NewStreamWriter 创建 SSE 写入器 func NewStreamWriter(w http.ResponseWriter) (*StreamWriter, error) { flusher, ok := w.(http.Flusher) if !ok { return nil, fmt.Errorf("streaming not supported") } // 设置 SSE 必需的响应头 w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("X-Accel-Buffering", "no") // 禁用 Nginx 缓冲 return &StreamWriter{ writer: w, flusher: flusher, }, nil } // WriteEvent 写入一个 SSE 事件 func (sw *StreamWriter) WriteEvent(event StreamEvent) error { sw.mu.Lock() defer sw.mu.Unlock() if sw.closed { return fmt.Errorf("stream already closed") } var buf bytes.Buffer if event.ID != "" { buf.WriteString(fmt.Sprintf("id: %s\n", event.ID)) } if event.Event != "" { buf.WriteString(fmt.Sprintf("event: %s\n", event.Event)) } if event.Retry > 0 { buf.WriteString(fmt.Sprintf("retry: %d\n", event.Retry)) } // data 字段支持多行 dataStr := string(event.Data) for _, line := range bytes.Split([]byte(dataStr), []byte("\n")) { buf.WriteString(fmt.Sprintf("data: %s\n", line)) } buf.WriteString("\n") // 空行分隔事件 _, err := sw.writer.Write(buf.Bytes()) if err != nil { return err } sw.flusher.Flush() return nil } // WriteData 写入纯数据(最常用) func (sw *StreamWriter) WriteData(data []byte) error { return sw.WriteEvent(StreamEvent{ Data: data, }) } // WriteDone 写入结束标记 func (sw *StreamWriter) WriteDone() error { sw.mu.Lock() defer sw.mu.Unlock() if sw.closed { return nil } _, err := sw.writer.Write([]byte("data: [DONE]\n\n")) sw.closed = true if err != nil { return err } sw.flusher.Flush() return nil } // WriteError 写入错误事件 func (sw *StreamWriter) WriteError(code int, message string) error { errData, _ := json.Marshal(map[string]interface{}{ "error": map[string]interface{}{ "code": code, "message": message, }, }) return sw.WriteData(errData) } // Close 关闭流 func (sw *StreamWriter) Close() { sw.mu.Lock() defer sw.mu.Unlock() sw.closed = true } // ─── LLM Streaming Adapter ─── // LLMStreamAdapter LLM 流式适配器 type LLMStreamAdapter struct { provider string apiKey string baseURL string client *http.Client } // NewLLMStreamAdapter 创建流式适配器 func NewLLMStreamAdapter(provider, apiKey string) *LLMStreamAdapter { return &LLMStreamAdapter{ provider: provider, apiKey: apiKey, baseURL: getBaseURL(provider), client: &http.Client{Timeout: 60 * time.Second}, } } // ChatStream 流式聊天 func (a *LLMStreamAdapter) ChatStream( messages []map[string]string, onToken func(token string), onDone func(), onError func(error), ) { reqBody := map[string]interface{}{ "model": "gpt-4", "messages": messages, "stream": true, } body, _ := json.Marshal(reqBody) httpReq, _ := http.NewRequest("POST", a.baseURL+"/chat/completions", bytes.NewReader(body)) httpReq.Header.Set("Authorization", "Bearer "+a.apiKey) httpReq.Header.Set("Content-Type", "application/json") resp, err := a.client.Do(httpReq) if err != nil { onError(fmt.Errorf("request failed: %w", err)) return } defer resp.Body.Close() if resp.StatusCode != 200 { bodyBytes, _ := io.ReadAll(resp.Body) onError(fmt.Errorf("API error %d: %s", resp.StatusCode, string(bodyBytes))) return } reader := bufio.NewReader(resp.Body) for { line, err := reader.ReadString('\n') if err != nil { if err == io.EOF { onDone() return } onError(fmt.Errorf("read error: %w", err)) return } line = trimSSELine(line) if line == "" || line == "[DONE]" { if line == "[DONE]" { onDone() return } continue } // 解析 SSE 数据 var streamResp struct { Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` FinishReason string `json:"finish_reason"` } `json:"choices"` } if err := json.Unmarshal([]byte(line), &streamResp); err != nil { continue } for _, choice := range streamResp.Choices { if choice.Delta.Content != "" { onToken(choice.Delta.Content) } if choice.FinishReason == "stop" { onDone() return } } } } func getBaseURL(provider string) string { switch provider { case "openai": return "https://api.openai.com/v1" case "claude": return "https://api.anthropic.com/v1" default: return "https://api.openai.com/v1" } } func trimSSELine(line string) string { if len(line) > 5 && line[:5] == "data:" { line = line[5:] } line = bytes.TrimSpace([]byte(line)) return string(line) }四、HTTP 端点实现
// streaming/handler.go package streaming import ( "encoding/json" "log" "net/http" "time" ) // StreamChatHandler 流式聊天 HTTP 处理器 type StreamChatHandler struct { adapter *LLMStreamAdapter } // NewStreamChatHandler 创建处理器 func NewStreamChatHandler(adapter *LLMStreamAdapter) *StreamChatHandler { return &StreamChatHandler{adapter: adapter} } // ServeHTTP 处理流式聊天请求 func (h *StreamChatHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // 1. 解析请求 var req struct { Messages []map[string]string `json:"messages"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // 2. 创建 SSE 写入器 sw, err := NewStreamWriter(w) if err != nil { http.Error(w, "Streaming not supported", http.StatusInternalServerError) return } // 3. 监听客户端断开 notify := r.Context().Done() go func() { <-notify sw.Close() log.Println("[Stream] Client disconnected") }() // 4. 发送开始事件 startData, _ := json.Marshal(map[string]string{ "type": "start", "message": "开始生成回复", }) sw.WriteData(startData) // 5. 发起流式调用 tokenCount := 0 startTime := time.Now() h.adapter.ChatStream( req.Messages, func(token string) { // 每个 token 作为一个事件发送 tokenData, _ := json.Marshal(map[string]interface{}{ "type": "token", "token": token, }) if err := sw.WriteData(tokenData); err != nil { log.Printf("[Stream] Write error: %v", err) return } tokenCount++ }, func() { // 完成事件 elapsed := time.Since(startTime) doneData, _ := json.Marshal(map[string]interface{}{ "type": "done", "token_count": tokenCount, "elapsed_ms": elapsed.Milliseconds(), "tokens_per_s": float64(tokenCount) / elapsed.Seconds(), }) sw.WriteData(doneData) sw.WriteDone() log.Printf("[Stream] Completed: %d tokens in %v (%.1f tok/s)", tokenCount, elapsed, float64(tokenCount)/elapsed.Seconds()) }, func(err error) { log.Printf("[Stream] Error: %v", err) sw.WriteError(500, err.Error()) sw.WriteDone() }, ) } // ─── 启动 HTTP 服务 ─── func StartStreamingServer(addr string) { adapter := NewLLMStreamAdapter("openai", "sk-your-key") handler := NewStreamChatHandler(adapter) http.Handle("/v1/chat/stream", handler) http.Handle("/health", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"ok"}`)) })) log.Printf("[Stream] Server starting on %s", addr) log.Fatal(http.ListenAndServe(addr, nil)) }五、客户端实现
// streaming/client.go package streaming import ( "bufio" "bytes" "encoding/json" "fmt" "io" "net/http" "strings" "sync" ) // StreamClient SSE 客户端 type StreamClient struct { baseURL string client *http.Client } // NewStreamClient 创建客户端 func NewStreamClient(baseURL string) *StreamClient { return &StreamClient{ baseURL: baseURL, client: &http.Client{}, } } // ChatCompletion 流式聊天(回调方式) func (c *StreamClient) ChatCompletion( messages []map[string]string, onToken func(string), onDone func(StreamStats), onError func(error), ) { reqBody, _ := json.Marshal(map[string]interface{}{ "messages": messages, }) req, _ := http.NewRequest("POST", c.baseURL+"/v1/chat/stream", bytes.NewReader(reqBody)) req.Header.Set("Content-Type", "application/json") resp, err := c.client.Do(req) if err != nil { onError(fmt.Errorf("request failed: %w", err)) return } defer resp.Body.Close() if resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) onError(fmt.Errorf("server error %d: %s", resp.StatusCode, string(body))) return } // 检查 Content-Type ct := resp.Header.Get("Content-Type") if !strings.HasPrefix(ct, "text/event-stream") { body, _ := io.ReadAll(resp.Body) onError(fmt.Errorf("unexpected content type: %s, body: %s", ct, string(body))) return } reader := bufio.NewReader(resp.Body) stats := StreamStats{StartTime: now()} for { line, err := reader.ReadString('\n') if err != nil { if err == io.EOF { onDone(stats) return } onError(fmt.Errorf("read error: %w", err)) return } line = strings.TrimSpace(line) if !strings.HasPrefix(line, "data: ") { continue } data := strings.TrimPrefix(line, "data: ") if data == "[DONE]" { onDone(stats) return } var event struct { Type string `json:"type"` Token string `json:"token"` TokenCount int `json:"token_count"` ElapsedMs int64 `json:"elapsed_ms"` } if err := json.Unmarshal([]byte(data), &event); err != nil { continue } switch event.Type { case "token": onToken(event.Token) stats.TokenCount++ case "done": stats.TokenCount = event.TokenCount stats.ElapsedMs = event.ElapsedMs onDone(stats) return case "error": onError(fmt.Errorf("server error: %s", data)) return } } } // StreamStats 流式统计 type StreamStats struct { TokenCount int ElapsedMs int64 StartTime time.Time } // ─── JavaScript 客户端示例 ─── const jsClientCode = ` // 前端 JS 示例 class StreamChat { constructor(url) { this.url = url; this.abortController = null; } async send(messages, callbacks) { const { onToken, onDone, onError } = callbacks; try { const response = await fetch(this.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages }), signal: this.abortController?.signal }); if (!response.ok) { throw new Error('HTTP ' + response.status); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const data = line.slice(6); if (data === '[DONE]') { onDone?.(); return; } try { const event = JSON.parse(data); switch (event.type) { case 'token': onToken?.(event.token); break; case 'done': onDone?.(event); break; case 'error': onError?.(new Error(event.message)); break; } } catch (e) { // skip invalid JSON } } } } catch (err) { if (err.name !== 'AbortError') { onError?.(err); } } } abort() { this.abortController?.abort(); } } // 使用示例 const chat = new StreamChat('/v1/chat/stream'); chat.send([ { role: 'user', content: '帮我写一封邮件' } ], { onToken: (token) => { document.getElementById('output').textContent += token; }, onDone: () => { console.log('完成'); }, onError: (err) => { console.error('错误:', err); } }); `六、流式场景下的工具调用
// streaming/fcall.go package streaming import ( "encoding/json" "fmt" "log" "strings" ) // StreamFCallHandler 流式 Function Calling 处理器 type StreamFCallHandler struct { fcEngine *fcall.Engine adapter *LLMStreamAdapter } // HandleStreamFCall 处理流式场景下的工具调用 func (h *StreamFCallHandler) HandleStreamFCall( messages []map[string]string, sw *StreamWriter, ) { fullResponse := &strings.Builder{} pendingToolCalls := make([]*fcall.ToolCall, 0) inToolCall := false toolCallBuffer := &strings.Builder{} h.adapter.ChatStream( messages, func(token string) { // 检测是否进入工具调用模式 if strings.Contains(token, "<tool_call>") { inToolCall = true toolCallBuffer.Reset() return } if inToolCall { if strings.Contains(token, "</tool_call>") { // 工具调用结束,解析并执行 inToolCall = false tc := parseToolCall(toolCallBuffer.String()) if tc != nil { pendingToolCalls = append(pendingToolCalls, tc) // 发送工具调用标记给客户端 tcData, _ := json.Marshal(map[string]interface{}{ "type": "tool_call", "tool": tc.Name, "args": string(tc.Args), }) sw.WriteData(tcData) } return } toolCallBuffer.WriteString(token) return } // 普通 token,转发给客户端 fullResponse.WriteString(token) tokenData, _ := json.Marshal(map[string]interface{}{ "type": "token", "token": token, }) sw.WriteData(tokenData) }, func() { // 首次生成完成,如果有工具调用则执行 if len(pendingToolCalls) > 0 { log.Printf("[StreamFCall] Executing %d tool calls", len(pendingToolCalls)) // 执行工具 results := h.fcEngine.ExecuteToolCalls(nil, pendingToolCalls) // 将工具结果加入消息 for i, tr := range results { resultJSON, _ := json.Marshal(tr.Result) messages = append(messages, map[string]string{ "role": "tool", "content": string(resultJSON), }) // 发送工具结果给客户端 resData, _ := json.Marshal(map[string]interface{}{ "type": "tool_result", "tool": tr.Name, "result": tr.Result, }) sw.WriteData(resData) } // 第二次流式生成 h.adapter.ChatStream( messages, func(token string) { fullResponse.WriteString(token) tokenData, _ := json.Marshal(map[string]interface{}{ "type": "token", "token": token, }) sw.WriteData(tokenData) }, func() { // 最终完成 doneData, _ := json.Marshal(map[string]interface{}{ "type": "done", "full_response": fullResponse.String(), }) sw.WriteData(doneData) sw.WriteDone() }, func(err error) { sw.WriteError(500, err.Error()) sw.WriteDone() }, ) return } // 没有工具调用,直接完成 doneData, _ := json.Marshal(map[string]interface{}{ "type": "done", "full_response": fullResponse.String(), }) sw.WriteData(doneData) sw.WriteDone() }, func(err error) { sw.WriteError(500, err.Error()) sw.WriteDone() }, ) } // parseToolCall 解析工具调用标记 func parseToolCall(raw string) *fcall.ToolCall { raw = strings.TrimSpace(raw) // 期望格式: {"name": "xxx", "args": {...}} var tc struct { Name string `json:"name"` Args json.RawMessage `json:"args"` } if err := json.Unmarshal([]byte(raw), &tc); err != nil { log.Printf("[StreamFCall] Failed to parse tool call: %v", err) return nil } return &fcall.ToolCall{ ID: fmt.Sprintf("call_stream_%d", time.Now().UnixNano()), Name: tc.Name, Args: tc.Args, } }七、性能优化
// streaming/optimize.go package streaming import ( "sync" "time" ) // TokenBucket 令牌桶限流(发送频率控制) type TokenBucket struct { rate float64 burst int tokens float64 lastRefill time.Time mu sync.Mutex } func NewTokenBucket(rate float64, burst int) *TokenBucket { return &TokenBucket{ rate: rate, burst: burst, tokens: float64(burst), lastRefill: time.Now(), } } func (tb *TokenBucket) Allow() bool { tb.mu.Lock() defer tb.mu.Unlock() now := time.Now() elapsed := now.Sub(tb.lastRefill).Seconds() tb.tokens = min(float64(tb.burst), tb.tokens+elapsed*tb.rate) tb.lastRefill = now if tb.tokens >= 1 { tb.tokens-- return true } return false } // BatchSender 批量发送器(合并小 token 减少 I/O) type BatchSender struct { sw *StreamWriter buffer []byte maxBatch int timer *time.Timer mu sync.Mutex } func NewBatchSender(sw *StreamWriter, maxBatch int, flushInterval time.Duration) *BatchSender { bs := &BatchSender{ sw: sw, maxBatch: maxBatch, } bs.timer = time.AfterFunc(flushInterval, bs.flush) return bs } func (bs *BatchSender) Add(token string) { bs.mu.Lock() defer bs.mu.Unlock() bs.buffer = append(bs.buffer, []byte(token)...) if len(bs.buffer) >= bs.maxBatch { bs.flushNow() } } func (bs *BatchSender) flush() { bs.mu.Lock() defer bs.mu.Unlock() bs.flushNow() } func (bs *BatchSender) flushNow() { if len(bs.buffer) == 0 { return } data, _ := json.Marshal(map[string]interface{}{ "type": "batch", "tokens": string(bs.buffer), }) bs.sw.WriteData(data) bs.buffer = bs.buffer[:0] } func (bs *BatchSender) Close() { bs.timer.Stop() bs.flush() } // 心跳机制(防止代理/负载均衡器断开连接) func startHeartbeat(sw *StreamWriter, interval time.Duration, stop chan struct{}) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: sw.WriteEvent(StreamEvent{ Event: "heartbeat", Data: json.RawMessage(`{}`), }) case <-stop: return } } }八、生产坑位
# | 坑位 | 现象 | 解决方案 |
|---|---|---|---|
1 | 代理缓冲 | Nginx/反向代理缓存 SSE 数据,客户端收不到流式效果 | 设置 |
2 | 连接断开 | 用户刷新页面或网络波动导致连接中断 | 客户端自动重连 + 服务端心跳保活 |
3 | 并发写入 | 多个 goroutine 同时写入 ResponseWriter 导致 panic | 用 mutex 保护写入操作 |
4 | 内存泄漏 | 客户端断开后 goroutine 仍在运行 | 监听 Context.Done() 及时清理资源 |
5 | Token 碎片 | 每个 token 单独发送导致大量小包 I/O | 合并批次发送(100ms 窗口或累积 50 字符) |
6 | 工具调用时序 | 流式过程中插入工具调用,打乱前端展示逻辑 | 使用事件类型区分 token/tool_call/tool_result |
九、本讲小结
┌─────────────────────────────────────────────────────────────┐ │ 第6讲 核心收获 │ │ │ │ ✅ 理解了 SSE 协议原理和数据格式 │ │ ✅ 实现了服务端 StreamWriter(写入/心跳/关闭) │ │ ✅ 搭建了 LLM 流式适配器(OpenAI 兼容) │ │ ✅ 实现了 HTTP 端点 + 客户端(Go/JavaScript) │ │ ✅ 实现了流式场景下的 Function Calling 集成 │ │ ✅ 完成了性能优化(令牌桶限流、批量发送、心跳保活) │ │ ✅ 了解了 6 个常见坑位及其解决方案 │ │ │ │ 核心理念:Streaming 将用户等待时间从"转圈圈"变为"逐字阅读" │ └─────────────────────────────────────────────────────────────┘下一讲预告:《可观测性与 AI 监控——你的 Agent 到底在干什么》
你会学到:LLM 调用链路追踪、Token 用量监控、响应质量评估、告警体系搭建、日志审计与合规。
准备好了告诉我"继续"。