Go的context.Context不能当AI上下文窗口用,因其不可变、不存数据、无token管理能力;AI上下文需可读写、可裁剪、可序列化,并基于实际token数动态管理消息。

为什么 context.Context 不能直接当 AI 上下文窗口用
很多人一看到 “context” 就自然联想到 Go 的 context.Context,但这是个典型误用。Go 的 context.Context 是为控制 goroutine 生命周期、传递取消信号和超时而设计的,**不存储数据、不可变、不可扩展**。AI 应用里的“上下文窗口”指模型能看见的历史对话(messages)、工具调用记录、状态快照等——它必须可读写、可截断、可序列化,且需按 token 数而非时间做裁剪。
常见错误现象:context.WithValue(ctx, key, msg) 堆叠几十次后,既无法统计总 token 量,也无法按需丢弃最老的 user-assistant 对,最终触发 LLM API 的 400: context_length_exceeded 错误。
- AI 上下文是数据容器,不是控制流信号
- 必须自己维护
[]map[string]string或结构体切片(如[]ChatMessage) - 所有 token 计算必须走实际内容(含 role 字符、分隔符),不能依赖
len()或字节数
如何按 token 长度动态裁剪 messages 切片
硬截前 N 条消息会破坏对话逻辑(比如把 system 指令或最近一次 function call 结果删了)。正确做法是:从最旧开始删,直到剩余 token 总数 ≤ 模型窗口上限(如 gpt-4-turbo 是 128k,qwen2-7b 是 32k)。
关键点在于:不同模型 tokenizer 不同,但你**不需要自己实现 tokenizer**——用官方或社区维护的轻量库,例如 github.com/ollama/ollama/api 中的 CountTokens(适配本地模型),或 OpenAI 官方 openai.CountTokens(v1.40+)。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 先定义结构体:
type ChatMessage struct { Role string; Content string; ToolCalls []ToolCall } - 封装裁剪函数:
func (c *ChatContext) TrimToMaxTokens(max int, model string) error - 裁剪时跳过
system消息(通常必须保留),优先删最早的user/assistant对 - 注意:部分模型对
tool_calls和tool_responses的编码方式特殊,需单独测 token 占用
如何避免重复计算 token 导致性能掉坑
每次发请求前都对整个 messages 调用 CountTokens?在高频流式响应场景下,这会让 token 统计本身成为瓶颈。更合理的方式是:**增量更新 + 缓存**。
给 ChatMessage 加一个 TokenCount int 字段,在追加新消息时立即算好并累加到总和;删除时直接减去缓存值。这样 TrimToMaxTokens 只需遍历切片做减法,O(1) 判断是否越界。
- 初始化新消息时必须调用一次
CountTokens,别偷懒用估算(比如len(content)/4) - 如果消息含 base64 图片或 JSON 工具参数,token 数可能翻倍,务必实测
- 缓存失效场景:消息内容被原地修改(应禁止)或重排序(少见,但需检查)
本地模型与 API 模型的上下文管理差异在哪
OpenAI / Anthropic 等 API 返回的 usage.total_tokens 是真实消耗,可用于校准你的本地 token 计数器;但本地运行的 llama.cpp 或 text-generation-inference 通常不返回精确 token 数,你只能靠预估 + 留余量。
这意味着:同一套 TrimToMaxTokens 逻辑,在 API 场景下可设 max = 128000,而在本地 qwen2-7b 上建议设 max = 28000 并开启 truncate_to_max_tokens: true(若后端支持)。
- API 模型:信任
usage字段,可反向修正你的缓存值(如发现偏差 >5%,触发全量重算) - 本地模型:必须保守——提前 10% 截断,否则容易收到
incomplete response或静默截断 - 混合部署时,别共用同一个
ChatContext实例,要按 backend 类型隔离管理策略
真正麻烦的不是怎么存消息,而是怎么判断“哪条该留、哪条该扔”,以及扔完之后用户问“刚才我说的第三点你还记得吗”,你得答得上来——这要求裁剪逻辑带语义感知,比如保留带编号列表的那条 user 消息,或者不拆开 multi-turn function call 流程。这些没法靠通用库解决,得在业务层加标记或 hook。

















