去提问»
提问背景 Question:
以下是ai聊天记录保存在数据库和内存的程序,正常情况下,它能回忆起之前的聊天内容,形成对话。但是浏览器刷新一下,之前的聊天内容就没有记忆了。为了解决这个问题,我刷新页面的时候重新从数据库读取了以前的聊天内容,再加入内存,但好像无效。帮我看看原因。package ai import ( "database/sql" "strings" "stv/fun" "sync" "time" "google.golang.org/genai" ) // 设置管理对象 var SessionConfig = struct { MaxHistoryPairs int }{ MaxHistoryPairs: 15, } var sessionStore sync.Map // GetUserHistory 依然保持公开,供外部读取上下文 func GetUserHistory(sessionID string) []genai.Content { history, ok := sessionStore.Load(sessionID) if !ok { return []genai.Content{} } return history.([]genai.Content) } // 内部私有方法:强制要求所有写入操作必须通过 append 逻辑 func saveUserHistory(sessionID string, history []genai.Content) { sessionStore.Store(sessionID, history) } // AppendPairAndTrim: 唯一的合法写入路径 func AppendPairAndTrim(sessionID string, userContent, modelContent genai.Content) { history := GetUserHistory(sessionID) history = append(history, userContent, modelContent) // 强制执行裁剪,确保不会超额 maxRecords := SessionConfig.MaxHistoryPairs if len(history) > maxRecords { history = history[len(history)-maxRecords:] } saveUserHistory(sessionID, history) } // 读取聊天记录到内存,并展示初始化到聊天窗口中 func LoadUserHistoryToMemory(userID int64, sessionID string) error { // 查询最近20条(对应10对对话,根据需求可调)记录 rows, err := fun.DB.Query( "SELECT question, description, content FROM cms_article WHERE user_id = ? ORDER BY id DESC LIMIT 15", userID, ) if err != nil { return err } defer rows.Close() for rows.Next() { var q, d, c string if err := rows.Scan(&q, &d, &c); err != nil { continue } userC := NewContent("user", q) modelC := NewContent("model", c) AppendPairAndTrim(sessionID, userC, modelC) // 写入会话数据库 } return nil } // msg_type,0为文本,1为语音 func SaveAIGeneratedData(user_id int, userQuestion string, resp AIStructuredResponse, msg_type int) error { // 开启事务 tx, err := fun.DB.Begin() if err != nil { return err } defer tx.Rollback() articleID := fun.SnowNode.Generate().Int64() // 生成雪花 ID now := time.Now().Unix() // 2. 插入文章主表 cms_article articleQuery := ` INSERT INTO cms_article (id, question, title, description, content, source, ip, score, status, created_at, updated_at, user_id, msg_type) VALUES (?, ?, ?, ?, ?, 'gemini', NULL, ?, ?, ?, ?, ?, ?) ` _, err = tx.Exec(articleQuery, articleID, userQuestion, resp.Title, resp.Desc, resp.Text, resp.Score, resp.Status, now, now, user_id, msg_type) if err != nil { return err } // 3. 处理标签 tags for _, tagName := range resp.Tags { tagName = strings.TrimSpace(tagName) if tagName == "" { continue } // 3.1 尝试在 cms_tag 中查找或创建该标签 var tagID int64 err = tx.QueryRow("SELECT id FROM cms_tag WHERE name = ?", tagName).Scan(&tagID) if err == sql.ErrNoRows { // 找不到则创建新标签(使用雪花 ID) tagID = fun.SnowNode.Generate().Int64() // 生成雪花 ID _, err = tx.Exec("INSERT INTO cms_tag (id, name, count, created_at, updated_at) VALUES (?, ?, 1, ?, ?)", tagID, tagName, now, now) if err != nil { return err } } else if err != nil { return err } else { // 找到了则将该标签下的文章计数自增 1 _, err = tx.Exec("UPDATE cms_tag SET count = count + 1, updated_at = ? WHERE id = ?", now, tagID) if err != nil { return err } } // 3.2 绑定文章和标签的关系到 cms_article_tag 关联表中 _, err = tx.Exec("INSERT OR IGNORE INTO cms_article_tag (article_id, tag_id) VALUES (?, ?)", articleID, tagID) if err != nil { return err } } // 提交事务 err = tx.Commit() if err != nil { return err } return nil } // 前端需要将 AI 的回复也存入历史时,调用这个简单的辅助函数 func NewContent(role string, text string) genai.Content { contents := genai.Text(text) if len(contents) > 0 && contents[0] != nil { res := *contents[0] res.Role = role return res } return genai.Content{Role: role} }

解决刷新页面AI失忆问题

发布时间:2026-08-17 07:19 👁️ 阅读:1

你的代码在浏览器刷新后“加载历史记忆无效”,主要存在以下几个致命原因

1. 致命原因分析

原因一:时序完全颠倒(最严重)

你的 SQL 查询使用的是 ORDER BY id DESC LIMIT 15,这会查出最新的 15 条记录(例如:第 15 条、第 14 条……第 1 条)。
LoadUserHistoryToMemory 的循环中,你直接按照这个倒序顺序调用 AppendPairAndTrim 写入内存。这导致内存中历史记录的时序变成了**“从新到旧”**。大模型读取上下文时,会看到“未来的对话发生在过去”,导致其逻辑混乱,无法正常回应。

原因二:裁剪长度变成单数,导致 Role 错位报错

在你的 AppendPairAndTrim 方法中:

maxRecords := SessionConfig.MaxHistoryPairs // 值为 15
if len(history) > maxRecords {
    history = history[len(history)-maxRecords:]
}

这里 maxRecords15(单数)。当你循环装载 8 对对话(16条内容)时,切片长度超过 15,触发裁剪。裁剪掉最前面的一条后,剩余 15 条,此时第一条记录的 Role 变成了 "model"。Google Gemini API 强制要求历史上下文的第一条必须是 "user",且必须是 user-model 交替。Role 错位会导致 API 直接请求失败或被过滤。

原因三:循环内高频操作 sync.Map 造成性能浪费

LoadUserHistoryToMemory 循环中,每次都从 sessionStore 读取、追加、裁剪再写回,没有必要。应该一次性从数据库正序组装好 []genai.Content 后,一次性写入内存。


2. 修复后的完整 Go 代码

请使用以下优化后的代码替换你原有的方法:

package ai

import ( 
	"database/sql"
	"strings"
	"stv/fun"
	"sync"
	"time"

	"google.golang.org/genai"
)

// 设置管理对象
var SessionConfig = struct {
	MaxHistoryPairs int // 保存的最大对话“对”数(1对 = 1个user + 1个model)
}{
	MaxHistoryPairs: 10, // 10对 = 20条记录
}

var sessionStore sync.Map

// GetUserHistory 依然保持公开,供外部读取上下文
func GetUserHistory(sessionID string) []genai.Content {
	history, ok := sessionStore.Load(sessionID)
	if !ok {
		return []genai.Content{}
	}
	return history.([]genai.Content)
}

// 内部私有方法:保存会话
func saveUserHistory(sessionID string, history []genai.Content) {
	sessionStore.Store(sessionID, history)
}

// AppendPairAndTrim: 唯一的合法写入路径
func AppendPairAndTrim(sessionID string, userContent, modelContent genai.Content) {
	history := GetUserHistory(sessionID)
	history = append(history, userContent, modelContent)

	// 强制执行裁剪,确保不会超额。必须乘以 2,保证裁剪后第一条依然是 user,且成对出现
	maxRecords := SessionConfig.MaxHistoryPairs * 2
	if len(history) > maxRecords {
		history = history[len(history)-maxRecords:]
	}

	saveUserHistory(sessionID, history)
}

// 读取聊天记录到内存(修复了顺序颠倒与角色截断问题)
func LoadUserHistoryToMemory(userID int64, sessionID string) error {
	// 使用子查询:先拿到最新的 10 对(20条)记录,再将其按 ID 正序(从旧到新)排列
	query := `
		SELECT question, content FROM (
			SELECT id, question, content 
			FROM cms_article 
			WHERE user_id = ? 
			ORDER BY id DESC 
			LIMIT ?
		) AS sub 
		ORDER BY id ASC
	`
	
	// 我们需要读取 MaxHistoryPairs 数量的对话
	rows, err := fun.DB.Query(query, userID, SessionConfig.MaxHistoryPairs)
	if err != nil {
		return err
	}
	defer rows.Close()

	var tempHistory []genai.Content

	for rows.Next() {
		var q, c string
		if err := rows.Scan(&q, &c); err != nil {
			continue
		}

		// 构造标准对话组
		userC := NewContent("user", q)
		modelC := NewContent("model", c)
		
		// 依次追加到临时切片中(此时已是绝对正序:旧 -> 新)
		tempHistory = append(tempHistory, userC, modelC)
	}

	// 一次性覆盖写入内存,避免循环内部重复读写锁
	saveUserHistory(sessionID, tempHistory)
	return nil
}

// 前端需要将 AI 的回复也存入历史时,调用这个简单的辅助函数
func NewContent(role string, text string) genai.Content {
	contents := genai.Text(text)
	if len(contents) > 0 && contents[0] != nil {
		res := *contents[0]
		res.Role = role
		return res
	}
	return genai.Content{Role: role}
}

3. 注意事项(前后端联调)

  1. SessionID 的生命周期:用户刷新浏览器时,前端建立的 WebSocket 连接或发送的 HTTP 请求中,携带的 sessionID 必须保持一致(例如存在浏览器的 localStorageCookie 中,而不是存在 JS 内存变量中。如果存在 JS 内存中,刷新页面后 sessionID 会变,后端将无法把读取的历史记录和新会话匹配)。
  2. 调用时机:当用户刷新页面,前端重新建立连接,并且传入 userIDsessionID 时,首先调用 LoadUserHistoryToMemory(userID, sessionID),然后再允许用户发送新消息。这样大模型就能完美回忆起先前的对话了。