必须使用持久化存储层(如Badger或Redis)保存多轮对话上下文,按token裁剪而非消息条数,缓存每条消息TokenCount,结合context.WithCancel实现停止生成与换模型控制,并用sync.Mutex保障前端消息顺序。

WebSocket 连接里怎么存多轮对话上下文
不能用 map[string][]ChatMessage 直接存在内存里,进程一重启,所有对话历史全丢。用户发完“帮我订明天的机票”,刷新页面就变成“你刚才说啥?”。必须选有持久化能力的存储层。
小规模本地测试用 github.com/dgraph-io/badger/v4 就够:嵌入式、零网络开销、支持 TTL;K8s 多副本或已有基建就上 github.com/go-redis/redis/v9,但得显式调 SetEX 设过期时间,否则 KEYS * 扫描会越来越慢。
- 每次 WebSocket
ReadMessage后,先用session_id从存储加载历史[]ChatMessage - 把新
user消息 append 进去,再调TrimToMaxTokens(32768, "qwen2")裁剪 - 裁剪完立刻写回存储,别等响应结束才存——断连时会丢最后一条
为什么 ChatMessage 切片要按 token 裁剪,而不是按条数
硬删前 5 条消息可能把 system 指令或刚返回的 tool_calls 结果一起干掉,模型直接报 400: context_length_exceeded 或胡说八道。token 数才是模型真正“看见”的长度,len() 或字节数都不准。
用 ollama/api.CountTokens(本地 Ollama)或 openai.CountTokens(v1.40+)算真实消耗,裁剪逻辑必须从最老的 user/assistant 对开始删,跳过 system 和必须保留的 tool 相关消息。
将小说章节转换为电影分镜剧本。用户上传txt/md/docx文本,AI分析场景、角色、情绪、镜头语言,输出专业分镜脚本。适用于用户提及“分镜”“storyboard”“小说转分镜”“影视改编”“镜头脚本”或需要将小说改编为分镜的场景。
-
Role字符本身占 token(比如"user"是 2 个 token) - 不同模型对
tool_calls编码方式不同,Qwen2 和 Llama3 的 JSON 序列化开销差 30%+ - 别在每次
WriteJSON前都重算全部 token——缓存每条消息的TokenCount字段
如何让 WebSocket 支持“停止生成”和“换模型”这类控制指令
SSE 只能服务器推,做不到这个。WebSocket 双向特性刚好补上缺口:客户端随时发 {"action":"stop"} 或 {"action":"switch_model","model":"qwen2:14b"},后端收到就中断当前 http.Client 请求或更新 session 绑定的模型名。
关键不是协议本身,而是 handler 里得用 context.WithCancel 包住下游调用,并把 cancel 函数存在 session 结构体里。收到 stop 指令时调一次 cancel(),Ollama/vLLM 接口就会断开流式响应。
- 别用全局
context.CancelFunc,每个连接独享一个 - 换模型指令要校验
model是否在白名单里,防止恶意请求打崩本地 Ollama - 前端发控制指令时,WebSocket
send()后别等响应——服务端处理是异步的
流式响应下怎么保证前端消息顺序不乱
WebSocket 本身保序,但 Go 后端如果并发写多个 conn.WriteMessage,可能因 goroutine 调度导致分片错乱。比如“正在思考…”提示还没刷出,大段回复就顶上来。
必须加锁:每个连接配一个 sync.Mutex,所有 WriteJSON 或 WriteMessage 都得先 mu.Lock()。别用 chan 中转——增加延迟且没解决根本问题。
- 前端收到流式 chunk 后,用
textContent += chunk拼接,别直接 innerHTML —— XSS 风险 - 服务端返回的
Content字段若含换行,前端要用white-space: pre-wrap渲染 - 超时断连时,前端
onclose里要清空未完成的textContent,避免残留“正在思考…”
ChatMessage 的生命周期和 token 计算、裁剪、存储、过期全部对齐——错一步,轻则响应错乱,重则整条会话不可恢复。

















