简介

在大模型(如 GPT、LLaMA)的应用场景中,流式输出(Streaming Output)是提升用户体验的关键技术。通过实时逐词返回结果,用户无需等待全部生成完成即可看到部分内容。本文将结合 Go 语言后端和 前端 JavaScript,详解实现方案并提供代码示例。


一、流式输出的核心挑战

  1. 低延迟:用户从输入到看到首个 token 的时间应尽可能短。
  2. 高吞吐:支持多用户并发请求。
  3. 稳定性:处理网络中断、生成错误等异常场景。
  4. 资源控制:避免大模型长时间占用 GPU/CPU。

二、技术方案与代码实现

方案 1:Server-Sent Events (SSE)

基于 HTTP 的单向流式通信协议,适合简单场景。

Go 后端代码(Gin 框架)
package main

import (
	"fmt"
	"time"
	"github.com/gin-gonic/gin"
)

// 模拟大模型生成数据(逐词返回)
func mockModelGenerate(prompt string, ch chan<- string) {
	defer close(ch)
	responses := []string{"Hello", " World", "!", " This is streaming."}
	for _, resp := range responses {
		time.Sleep(500 * time.Millisecond) // 模拟生成延迟
		ch <- resp
	}
}

func main() {
	r := gin.Default()

	r.GET("/stream", func(c *gin.Context) {
		// 设置 SSE 头部
		c.Header("Content-Type", "text/event-stream")
		c.Header("Cache-Control", "no-cache")
		c.Header("Connection", "keep-alive")

		// 创建通道传递生成结果
		dataChan := make(chan string)
		go mockModelGenerate("Example prompt", dataChan)

		// 实时推送数据
		c.Stream(func(w io.Writer) bool {
			if msg, ok := <-dataChan; ok {
				c.SSEvent("message", msg)
				return true
			}
			return false
		})
	})

	r.Run(":8080")
}
前端代码(JavaScript)
<!DOCTYPE html>
<html>
<body>
  <div id="output"></div>
  <script>
    const eventSource = new EventSource('http://localhost:8080/stream');
    const outputDiv = document.getElementById('output');

    eventSource.onmessage = (e) => {
      outputDiv.innerHTML += e.data;
      window.scrollTo(0, document.body.scrollHeight); // 自动滚动
    };

    eventSource.onerror = () => {
      eventSource.close();
      console.log('Stream closed');
    };
  </script>
</body>
</html>
SSE 方案优劣
优点 缺点
- 实现简单,兼容 HTTP- 自动重连机制- 轻量级,适合文本流 - 单向通信(仅服务端推送)- 部分浏览器兼容性问题(IE不支持)- 默认并发限制(HTTP/1.1 6连接)

方案 2:WebSocket

双向全双工通信协议,适合复杂交互场景。

Go 后端代码(Gorilla WebSocket)
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool { return true },
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("Upgrade error:", err)
		return
	}
	defer conn.Close()

	// 接收客户端初始 prompt
	_, promptBytes, err := conn.ReadMessage()
	if err != nil {
		log.Println("Read error:", err)
		return
	}
	prompt := string(promptBytes)

	// 模拟流式生成
	responses := []string{"Hello", " World", "!", " This is WebSocket."}
	for _, resp := range responses {
		time.Sleep(500 * time.Millisecond)
		if err := conn.WriteMessage(websocket.TextMessage, []byte(resp)); err != nil {
			log.Println("Write error:", err)
			break
		}
	}
}

func main() {
	http.HandleFunc("/ws", handleWebSocket)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
前端代码(JavaScript)
<!DOCTYPE html>
<html>
<body>
  <div id="output"></div>
  <script>
    const ws = new WebSocket('ws://localhost:8080/ws');
    const outputDiv = document.getElementById('output');

    ws.onopen = () => {
      ws.send("Example prompt"); // 发送初始 prompt
    };

    ws.onmessage = (e) => {
      outputDiv.innerHTML += e.data;
    };

    ws.onclose = () => {
      console.log('Connection closed');
    };
  </script>
</body>
</html>
WebSocket 方案优劣
优点 缺点
- 双向实时通信- 高并发支持- 低延迟二进制传输 - 实现复杂度高- 需额外处理连接状态- 需要独立的 WS 服务

三、进阶优化策略

1. Go 后端性能优化

// 使用通道实现生成与传输解耦
func generateWithPipeline(prompt string) <-chan string {
	ch := make(chan string)
	go func() {
		defer close(ch)
		// 真实场景调用模型生成
		for _, token := range []string{"Step1", "Step2", "Done"} {
			ch <- token
		}
	}()
	return ch
}

// 中间件禁用 Gin 的响应缓冲
func DisableResponseBuffering() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Writer = &unbufferedWriter{c.Writer} // 自定义 Writer
		c.Next()
	}
}

2. 前端用户体验优化

// 添加打字机动画效果
function appendWithAnimation(text) {
  const span = document.createElement('span');
  span.style.opacity = '0';
  outputDiv.appendChild(span);
  
  let i = 0;
  const timer = setInterval(() => {
    if (i < text.length) {
      span.textContent += text[i];
      span.style.opacity = (i / text.length).toFixed(2);
      i++;
    } else {
      clearInterval(timer);
      span.style.opacity = '1';
    }
  }, 50);
}

四、方案选型建议

场景 推荐方案 原因
简单文本流(如日志) SSE 实现简单,无需额外协议
实时对话系统 WebSocket 需要双向交互(如中断生成)
高并发 API SSE + HTTP/2 利用多路复用降低开销
二进制数据传输(如音频) WebSocket 支持二进制帧

总结

通过 Go 语言的高效并发模型(goroutine + channel)与前端的事件驱动机制,可以轻松实现大模型的流式输出。关键点在于:

  1. 后端:确保生成与传输的异步解耦,避免阻塞。
  2. 前端:平滑渲染和异常处理。
  3. 协议选择:根据场景权衡 SSE 和 WebSocket。

完整代码已托管至 GitHub 示例仓库,可直接运行测试。

Logo

在这里,我们一起交流AI,学习AI,用AI改变世界。如有AI产品需求,可访问讯飞开放平台,www.xfyun.cn。

更多推荐