基于SSE技术加 deepseek 实现打字机回复效果

具体代码,已上传到厂库

git clone https://gitee.com/rush_peng/sse-deepseek-demo.git

SSE 技术

go 后端实现

package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"
)

// Message 表示聊天消息结构
type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

// GenerateRequest 生成请求结构
type GenerateRequest struct {
	Model    string         `json:"model"`
	Prompt   string         `json:"prompt"`
	Stream   bool           `json:"stream"`
	Options  map[string]any `json:"options"`
	Messages []Message      `json:"messages"`
}

// ResponseMessage 响应消息结构
type ResponseMessage struct {
	Role     string `json:"role"`
	Content  string `json:"content"` // chat模式下返回的内容
	Thinking string `json:"thinking"`
}

// DeepseekResponse Deepseek API响应结构
type DeepseekResponse struct {
	Model      string          `json:"model"`
	Done       bool            `json:"done"`
	CreateAt   time.Time       `json:"create_at"`
	ResMessage ResponseMessage `json:"message"`
	Response   string          `json:"response"` // completion模式下返回的内容
}

// cat 模式下, 需要存储历史聊天消息
var chartHistory []Message

const (
	AskTypeCompletion = "completion"
	AskTypeChat       = "chat"
)

// deepseekStream 通过Deepseek API进行流式生成响应
func deepseekStream(w http.ResponseWriter, prompt string, askType string) (err error) {
	var apiURL string
	var completeResponse strings.Builder // 存储模型完整回复,使用strings.Builder 高效拼接字符串
	var getResContest func(resp DeepseekResponse) string
	reqBody := GenerateRequest{
		Model:  "deepseek-r1:8b",
		Stream: true,
		Options: map[string]any{
			"max_tokens":  1024,
			"temperature": 0.3,
			"top_p":       0.9,
			"raw":         true, // 这个如果false,会屏蔽 Thinking 字段
		},
	}

	switch askType {
	case AskTypeCompletion:
		apiURL = "http://localhost:11434/api/generate"
		reqBody.Prompt = prompt
		getResContest = func(resp DeepseekResponse) string {
			return resp.Response
		}
	case AskTypeChat:
		apiURL = "http://localhost:11434/api/chat"

		chartHistory = append(chartHistory, Message{Role: "user", Content: prompt})
		// 加入模型回复到历史记录
		defer func() {
			if err == nil {
				fmt.Println("模型的完整回复:", completeResponse.String())
				chartHistory = append(chartHistory, Message{Role: "assistant", Content: completeResponse.String()})
			}
		}()

		reqBody.Messages = chartHistory
		getResContest = func(resp DeepseekResponse) string {
			return resp.ResMessage.Content
		}
	}

	fmt.Printf("接受到了发送到的请求....%+v\n", reqBody)

	data, _ := json.Marshal(reqBody)
	resp, err := http.Post(apiURL, "application/json", bytes.NewReader(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	reader := bufio.NewReader(resp.Body)
	flusher, _ := w.(http.Flusher)

	for {
		line, err := reader.ReadBytes('\n')
		if err != nil {
			break
		}
		lineStr := strings.TrimSpace(string(line))
		if lineStr == "" {
			continue
		}

		var part DeepseekResponse
		if err := json.Unmarshal(line, &part); err != nil {
			continue
		}
		fmt.Printf("原始返回:%+v\n", part)
		// 检查是否完成,输出完了,跳出循环
		if part.Done {
			break
		}

		if content := getResContest(part); content != "" {

			// 一般浏览器都兼容,直接发送字符串即可,不需要逐字符发送
			// for _, ch := range content {
			// 	fmt.Fprintf(w, "data: %c\n\n", ch) //将字节写入 io.write 中
			// 	flusher.Flush()
			// 	fmt.Println("输出后端返回结果:", content)
			// 	time.Sleep(1 * time.Millisecond) // 可调节打字机速度
			// }
			fmt.Fprintf(w, "data: %s\n\n", content) //将字节写入 io.write 中
			flusher.Flush()
			completeResponse.WriteString(content) // 累加模型回复
		}
	}

	fmt.Fprintf(w, "data: \n\n")
	fmt.Fprintf(w, "data: __END__\n\n") //自定义的结束符,前端代码根据这个结束符判断是否结束
	flusher.Flush()

	return nil
}

// chatHandler 处理聊天请求的HTTP处理器
func chatHandler(w http.ResponseWriter, r *http.Request) {
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "Streaming not supported", http.StatusInternalServerError)
		return
	}
	// sse 响应头设置,基本都是固定的
	w.Header().Set("Access-Control-Allow-Origin", "*")  // 测试过程中,允许所有域名跨域访问
	w.Header().Set("Content-Type", "text/event-stream") // 要实现打字机效果,事件流格式比如为 text/event-stream
	w.Header().Set("Cache-Control", "no-cache")         // 禁用缓存,确保实时更新
	w.Header().Set("Connection", "keep-alive")          // sse 保持连接

	userMessage := r.URL.Query().Get("msg")

	if userMessage == "" {
		userMessage = "Hello DeepSeek"
	}

	err := deepseekStream(w, userMessage, AskTypeChat)
	if err != nil {
		fmt.Fprintf(w, "data: %s\n\n", err.Error())
		flusher.Flush()
	}
}

func main() {
	http.HandleFunc("/chat", chatHandler)
	fmt.Println("Server started at :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

posted @ 2025-11-27 04:01  沧海一声笑rush  阅读(11)  评论(0)    收藏  举报