前言

在本系列的前两篇文章中,我们已经完成了一个基于 GolangOllama 部署的本地大模型 AI 聊天系统:

  • [第一篇]:《Golang玩转本地大模型实战(一):ollama部署模型及流式调用》,介绍如何在本地部署大模型并实现 Golang 端的 API 调用;
  • [第二篇]:《Golang玩转本地大模型实战(二):基于Golang + Web实现AI对话页面》,实现了前后端结合的实时聊天功能,支持 WebSocket 与 SSE。

目前系统已能实现基本的 AI 问答功能,但仍存在一个关键问题:缺乏上下文记忆。每次对话模型都只能“盲目作答”,无法结合历史对话内容生成连贯回应。

本篇我们将解决这个问题,实现多轮对话的“上下文记忆功能”,让聊天更加智能、自然。


一、为什么需要上下文记忆?

大模型的输出完全依赖于当前输入的 prompt。在没有上下文的情况下,模型无法“记住”之前的交流内容,导致回答缺乏连续性。

例如:

  • 问:“我喜欢蓝色。”
  • 接着问:“我喜欢什么颜色?”
  • 模型会答:“我不知道。”

这就需要我们在构建 prompt 时,将历史对话一起发送给模型,形成具有“记忆”的对话体验。


二、实现上下文记忆的核心逻辑

为实现上下文记忆,系统需支持以下能力:

1. 会话上下文管理

每个用户/会话维护一组对话历史,结构如下:

type ChatMessage struct {
	Role    string `json:"role"`    // user / assistant
	Content string `json:"content"` // 对话内容
}

var sessionContexts = make(map[string][]ChatMessage)

通过 sessionID(如用户IP、UUID等)区分多个会话,并将历史内容与新问题拼接后一并传给模型。


2. 滑动窗口机制:限制对话长度

为了避免无限增长导致上下文超限,我们采用滑动窗口策略,仅保留最近 N 轮对话(用户+AI)。

const maxHistoryMessages = 10

func manageHistory(sessionID string, newMsg ChatMessage) {
	history := append(sessionContexts[sessionID], newMsg)
	if len(history) > maxHistoryMessages {
		history = history[len(history)-maxHistoryMessages:]
	}
	sessionContexts[sessionID] = history
}

3. Token 数控制:防止模型上下文溢出

多数大模型(如 GPT)有最大 token 限制,我们需要估算当前上下文的 token 数,超限则裁剪:

func estimateTokens(msgs []ChatMessage) int {
	tokens := 0
	for _, m := range msgs {
		tokens += len(m.Content) / 4 // 简化估算:4 字符约等于 1 token
	}
	return tokens
}

func manageTokens(sessionID string, msgs []ChatMessage) {
	if estimateTokens(msgs) > 2048 {
		sessionContexts[sessionID] = msgs[len(msgs)/2:] // 丢弃前半段
	}
}

更严谨的实现可以使用如 tiktoken 的库做精确计算。


三、关键技术回顾

模块 作用
sessionContexts 存储每个会话的上下文
滑动窗口机制 限制历史消息数量,防止上下文溢出
token 控制 防止 prompt 超出模型最大输入长度

四、后端实现代码示例

以下是上下文管理完整 WebSocket 聊天服务示例代码(使用 Ollama + Deepseek 模型):

package main

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

	"github.com/gorilla/websocket"
)

type ChatRequest struct {
	Model    string        `json:"model"`
	Stream   bool          `json:"stream"`
	Messages []ChatMessage `json:"messages"`
}

type ChatMessage struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

// 使用 sessionID 管理多个会话;demo 里简单用 IP 地址做区分
var sessionContexts = make(map[string][]ChatMessage)

// 设置最大上下文轮数(每轮是用户+AI)
const (
	maxHistoryMessages = 10
	maxTokenMessage    = 102400
)

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

func chatHandler(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()
	clientID := r.RemoteAddr
	fmt.Println("New connection")
	for {
		_, msg, err := conn.ReadMessage()
		if err != nil {
			log.Println("Read error:", err)
			break
		}
		fmt.Println("收到消息:", string(msg))

		userInput := string(msg)

		// 获取当前会话的上下文
		history := sessionContexts[clientID]

		// 拼接历史 + 当前用户消息
		allMessages := append(history, ChatMessage{Role: "user", Content: userInput})

		// 准备请求体
		reqData := ChatRequest{
			Model:    "deepseek-r1:8b",
			Stream:   true,
			Messages: allMessages,
		}
		jsonData, err := json.Marshal(reqData)
		if err != nil {
			http.Error(w, "json marshal error", http.StatusInternalServerError)
			return
		}

		// 调用本地Ollama服务
		resp, err := http.Post("http://localhost:11434/api/chat", "application/json", bytes.NewBuffer(jsonData))
		if err != nil {
			http.Error(w, "call ollama error", http.StatusInternalServerError)
			return
		}
		defer resp.Body.Close()

		// 流式读取模型输出
		scanner := bufio.NewScanner(resp.Body)
		var fullReply string
		for scanner.Scan() {
			line := scanner.Text()
			if line == "" {
				continue
			}

			var chunk struct {
				Message struct {
					Content string `json:"content"`
				} `json:"message"`
				Done bool `json:"done"`
			}

			if err := json.Unmarshal([]byte(line), &chunk); err != nil {
				continue
			}

			err = conn.WriteMessage(websocket.TextMessage, []byte(chunk.Message.Content))
			if err != nil {
				log.Println("Write error:", err)
				break
			}
			fullReply += chunk.Message.Content
			time.Sleep(50 * time.Millisecond)

			if chunk.Done {
				break
			}
		}
		allMessages = append(allMessages, ChatMessage{Role: "assistant", Content: fullReply})

		// 管理上下文大小
		manageHistory(clientID, ChatMessage{Role: "user", Content: userInput})
		manageHistory(clientID, ChatMessage{Role: "assistant", Content: fullReply})

		// 如果超过 token 限制,裁剪历史
		manageTokens(clientID, allMessages)
	}
}

// 保证上下文不超过最大长度
func manageHistory(sessionID string, newMessage ChatMessage) {
	history := sessionContexts[sessionID]
	history = append(history, newMessage)
	if len(history) > maxHistoryMessages {
		sessionContexts[sessionID] = history[len(history)-maxHistoryMessages:]
	} else {
		sessionContexts[sessionID] = history
	}
}

// 简单估算 token 数
func estimateTokens(messages []ChatMessage) int {
	tokenCount := 0
	for _, msg := range messages {
		tokenCount += len(msg.Content) / 4
	}
	return tokenCount
}

// 如果 token 数过大,裁剪历史
func manageTokens(sessionID string, messages []ChatMessage) {
	if estimateTokens(messages) > 2048 { // 假设最大 token 数为 2048
		sessionContexts[sessionID] = sessionContexts[sessionID][len(sessionContexts[sessionID])/2:]
	}
}

// 提供静态HTML页面
func homePage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "./static/index.html") // 当前目录的index.html
}

func main() {
	http.HandleFunc("/", homePage)      // 网页入口
	http.HandleFunc("/ws", chatHandler) // WebSocket接口

	log.Println("服务器启动,访问:http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}


五、效果验证

我们在前端发送两轮对话:

  1. “我喜欢蓝色”
  2. “我喜欢什么颜色?”

模型准确识别了“蓝色”作为用户喜好,说明上下文生效:

记忆锚点
再次提问


六、总结与展望

本文成果:

  • 实现了 AI 聊天的 上下文记忆能力
  • 支持按会话维护历史;
  • 实现滑动窗口 + token 限制,兼顾准确性与性能。

存在优化的空间:

由于本文仅仅是一个demo,所以存在很多需要优化的空间,这里简单列出,不做详细的代码实现了。

  • 使用 Redis 持久化上下文,支持多实例部署;
  • 引入上下文压缩(如摘要生成),提升信息保留能力;
  • 实现“清空对话”与“会话多标签”等增强功能。

下一篇预告

Golang玩转本地大模型实战(四):我们将封装完整服务为 Docker 镜像,实现“一键部署”。


参考资料

【手把手教会你如何通过ChatGPT API实现上下文对话】
【Spring AI进阶:AI聊天机器人之ChatMemory持久化(二)】
【多轮对话中让AI保持长期记忆的8种优化方式(附案例和代码)】


Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐