上一篇说三段式 insight 解决的是”打完一局怎么沉淀经验”。这篇聊一个更要命的事——一局还没打完,历史就已经撑爆了。
游戏里一局训练动辄三四十回合,每回合都有局面快照、可选动作、模型决策、最后结果。要是把这一坨全堆进 message 数组里给 LLM 看,到第十回合左右上下文就开始告急;到第二十回合,要么自动截断丢前面(关键设定一起飞了),要么直接报 context length exceeded。
最早的版本就是这么炸的。
朴素方案为什么都不行
先说几个我试过、然后默默删掉的方案。
方案一:滑动窗口,只保留最近 N 轮
简单粗暴。问题也明显——开局阶段的人物设定、卡组构成、战术意图,全是后面决策的根基,丢了之后模型就开始”失忆型抽风”。第 15 回合突然忘了自己是哪个角色,出牌逻辑直接崩。
方案二:直接 summarize 全部历史
每隔 N 回合让 LLM 把前面的对话总结成一段话。听起来很美。
实际跑起来——总结质量飘忽。运气好的时候压缩比 5:1,运气差的时候把关键数值(“体力还剩多少”“场上 buff 是什么”)当背景细节给丢了。下一回合模型一看,“咦我的体力是多少来着”,开始瞎猜。
方案三:让模型自己决定记什么
试过,问题是模型太”贪心”,让它自己删它一条都舍不得删。
最后的做法:分层压缩
后来落地的方案,本质是把会话分层:
1 | |
对应代码就两块:
1 | |
每一层职责清清楚楚。永久层只在开局写一次,整局只读不改;最近层是滑动窗口;中间的压缩层是关键——由 compactor 负责,每隔若干回合把最早的几轮”挤”成结构化摘要。
Compactor 的 prompt 长什么样
这块的 prompt 设计是反复改了七八版才稳的。最早写得很自由:
请总结以下回合的关键信息……
模型回你一段散文。下一次再总结的时候格式又变了,下游解析根本接不住。
最终版本走的是强结构化输出,让模型填表,不让它自由发挥:
1 | |
填表式输出有几个好处:
- 数值不会丢——你让它填
hp,它就得给数;总结成”体力下降不少”这种话直接不合格 - 可机器读——压缩层本身可以再被读取、再被组合
- token 占用可预测——表格结构每条大致占多少 token 是已知的
实测压缩比稳定在 8:1 到 10:1 之间,关键信息保留率(事后人工抽检)大概九成五以上。
触发时机:不能压太勤也不能压太晚
压缩这事儿有调用成本(每次都是一次 LLM 调用),所以不能每回合都压。但拖太晚,原始历史已经长到 compactor 自己都吃不下了。
最后定的策略很土,但有效:
- 回合数触发:每过 5 个回合压一次
- token 数兜底:估算当前 message 数组的 token 数,超过阈值的 60% 就强制触发,不管回合数走到没
兜底很重要——有些卡片效果触发会刷一堆系统提示,单回合就能塞进来几千 token,光按回合数计算根本拦不住。
那些没想到的坑
压缩层也会越攒越长
跑长局的时候发现——压缩层本身也在涨。第 30 回合的时候,压缩层里堆了五六段摘要,加起来比原始最近层还长。
后来加了二级压缩:压缩层里的摘要超过 N 段,就触发 compactor 把最早的几段再合并成一段更粗的摘要。粗摘要里只留”宏观走势”——分数曲线、体力曲线、关键转折点,具体数值就不要了。
层级长这样:
1 | |
听起来像金字塔,实际就是不停做”更粗的总结”,让总 token 始终可控。
压缩失败要有兜底,不能让整局崩
LLM 调用偶尔会抽风——超时、返回格式不合规、返回内容明显错乱。压缩失败如果直接抛异常,整局训练流程都得停。
兜底很简单:压缩失败就退化成滑动窗口,直接丢掉最早那几个回合。会损失信息,但至少游戏能继续打。日志里把这次失败标红,事后排查 prompt 或者切换模型。
宁可信息有损,也别让流程断。
永久层不是”system prompt 一塞了事”
最早把人物设定、卡组之类的写进 system message,以为这就是”永久”了。结果发现某些 LLM 对 system message 里靠前的内容关注度不均匀,越长越容易”忘”开头。
后来改成每次发请求的时候,把永久层内容显式地、简短地重复一次,附在最近层之前。冗余了一点 token,但确保模型每一轮都”看得见”自己是谁、打的什么卡组。
回过头看
LLM 上下文管理这事,本质是带预算的存档问题——你有固定的 token 预算,要在里面塞下最重要的信息。和数据库的冷热分层、操作系统的缓存淘汰其实是一个套路:高频访问的全量保留,低频但重要的压缩归档,纯历史的可以糊掉甚至丢弃。
工程化的关键不是”用了多牛的 LLM”,而是把这套分层逻辑显式写出来——哪一层放什么、什么时候压缩、压缩失败怎么兜底,全得有明确规则。把这些规则交给模型自己决定,多半翻车。
下一篇说说更隐藏的事——既然 prompt 这么金贵,那一份 system prompt 走天下到底行不行?为什么我们最后拆成了 23 个?