必须使用持久化存储(如badger或redis)保存多轮对话上下文,每次读取消息后立即加载、追加、按token裁剪并写回,避免进程重启或断连导致历史丢失。

WebSocket 连接里怎么存多轮对话上下文
不能用 map[string][]ChatMessage 直接存在内存里。进程一重启,所有对话历史全丢,用户刷新页面就变成“你刚才说啥?”。
必须选有持久化能力的存储层:
- 小规模本地测试用
github.com/dgraph-io/badger/v4:嵌入式、零网络开销、支持 TTL - K8s 多副本或已有 Redis 基建就上
github.com/go-redis/redis/v9,但得显式调SetEX设过期时间,否则KEYS *扫描会越来越慢
每次 conn.ReadJSON() 后,先用 session_id 从存储加载历史 []ChatMessage;把新 user 消息 append 进去;再调 TrimToMaxTokens(32768, "qwen2") 裁剪;裁剪完立刻写回存储——别等响应结束才存,断连时会丢最后一条。
为什么 ChatMessage 切片要按 token 裁剪,而不是按条数
硬删前 5 条消息可能把 system 指令或刚返回的 tool_calls 结果一起干掉,模型直接报 400: context_length_exceeded 或胡说八道。
token 数才是模型真正“看见”的长度,len() 或字节数都不准:
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- 用
ollama/api.CountTokens(本地 Ollama)或openai.CountTokens(v1.40+)算真实消耗 - 裁剪逻辑必须从最老的
user/assistant对开始删,跳过system和必须保留的tool相关消息 -
Role字符本身占 token(比如"user"是 2 个 token) - 不同模型对
tool_calls编码方式不同,Qwen2 和 Llama3 的 JSON 序列化开销差 30%+ - 别在每次
conn.WriteJSON()前都重算全部 token——缓存每条消息的TokenCount字段
如何让 WebSocket 支持“停止生成”和“换模型”这类控制指令
SSE 只能服务器推,做不到这个。WebSocket 双向特性刚好补上缺口:
- 客户端随时发
{"action":"stop"}或{"action":"switch_model","model":"qwen2:14b"} - 后端收到就中断当前
http.Client请求(比如调req.Cancel()),或更新 session 绑定的模型名 - 必须配合
context.WithCancel实现请求级取消,不能只靠连接关闭 - 前端需在发送控制指令后,主动清空未完成的流式渲染状态(比如清除
typing标志)
gorilla/websocket 心跳不生效的常见原因
服务端用 time.Ticker 每 30 秒调 conn.WriteMessage(websocket.PingMessage, nil) 不够。
必须设 Upgrader.KeepAlive = 30 * time.Second,否则底层 TCP keepalive 不生效——浏览器或代理可能在 60 秒无数据后静默断连,而你的 Ping 还没发出去。
另外注意:
-
gorilla/websocket默认处理Pong,前端不用写逻辑,但必须确保WriteDeadline设置合理(建议 ≥ 45s) - 不要依赖
OnPing回调做业务逻辑,它只用于响应心跳,不是连接活跃判断依据 - Nginx 等反向代理默认 60 秒超时,需同步配置
proxy_read_timeout和proxy_send_timeout
token 裁剪逻辑和心跳配置是两个最容易被跳过的点,线上出问题基本都卡在这儿。










