跳至主要内容

上下文快满时,我压缩什么、外置什么

上周三下午,一个改订单导出逻辑的会话跑到第三个小时。右下角的 token 计数爬到十七万八,上限二十万。变化不是突然发生的,而是渐进的:它开始重新读一个十分钟前刚读过的文件,然后把我一小时前说过的「不要动 migration」忘得干干净净,顺手写了个新的迁移脚本。

我第一个反应很粗暴——让它把前面的对话总结一下。八百字摘要回来了,读起来通顺,信息密度也不低。可漏掉的恰好是最贵的东西:我否掉的两个方案、一个接口的精确路径,还有那句「不要动 migration」。

那次之后我把这件事拆成两半:哪些内容可以压,哪些必须搬到上下文外面去。

三种「记得」,代价差得很远

我先试着把上下文里的东西分了个类,发现它们被压缩时的损失完全不是一个量级。

第一类是对话历史,也就是「我们聊过什么」。这部分水分最大,绕的弯路、重复确认、我打字时的口误,压掉九成都不心疼。

第二类是工作状态:目标是什么、做到第几步、下一步干什么、哪里卡住了。这部分字数很少,但一旦丢了,模型就开始原地打转——我见过它在我已经改完的函数上再改一遍。

第三类是事实与约束:接口签名、文件路径、错误信息原文、我明确否定过的方案。这类东西的特点是短、硬、不可推导。模型没法从别处猜出 orders 表的主键是复合键,也没法猜出我为什么拒绝那个方案。

压第一类叫整理,压第二类叫失忆,压第三类叫事故。我之前的「总结一下」,是不加区分地把三类揉在一起交给模型,然后祈祷它懂得轻重。它不懂。

现在的做法是先在项目目录里放一个 state.md,大概三四十行,长任务每推进一步就追加几行:

MARKDOWN12 行
1
2
3
4
5
6
7
8
9
10
11
12
## 目标
订单导出改成分页拉取,兼容旧接口

## 不可动的约束
- 不要新增 migration,schema 由 DBA 那边统一走
- /api/v2/orders/export 的响应结构不能变,前端已经上线

## 已完成
- 分页拉取写完,单测过

## 待办 / 已知坑
- 大数据量下的超时没测,本地只有 3 万行数据

这个文件放在上下文开头,任何一次压缩都绕着它走。它的作用是把我脑子里的那部分状态,变成一段不参与摘要的原文。

压缩是编码,不是复述

认清一件事之后我调整了参数:摘要模型的目标是「覆盖」,不是「保真」。它会优先保留信息量大、叙述完整的段落,天然倾向于省略否定句和短小的硬事实——「不要动 X」在摘要里特别容易蒸发,因为它在语义上不携带新信息。

我试过两版提示词。第一版是「请总结以上对话」,出来的东西像会议纪要。第二版改成给一份不可压缩清单,明确列出四类必须逐字保留的内容:错误信息原文、接口签名与路径、被否掉的方案及理由、我原话里的否定要求。同一段 6 万 token 的历史,第二版产出的摘要更长,也更罗列,但漏项明显少了。

代价是摘要本身变长了。压缩比从大概 20:1 掉到 8:1,但我不需要那么高的压缩比,我需要它压完之后还能接着干。

分层的思路也类似,只是按距离分配预算。我大致按这个比例切的:

PYTHON7 行
1
2
3
4
5
6
7
def plan_budget(total=200_000, reserve=25_000):
usable = total - reserve # 留给输出和工具返回
recent = int(usable * 0.25) # 最近几轮,保留原文
middle = int(usable * 0.45) # 中段,压成要点
early = int(usable * 0.20) # 早期,只留结论
index = usable - recent - middle - early
return recent, middle, early, index

最近六到十轮保留原文,这部分是模型理解「我现在在干什么」的锚点,压了它反而更贵。中段压成要点,早期只留结论和约束。最后留一块固定预算给索引和状态文件。

有一类东西我干脆不允许改写:路径、命令、报错。我吃过一次亏,/api/v2/orders 在摘要里被压成了「订单接口」,后面模型自己补了个 /api/orders,调试了半小时才发现是它编的。数字和标识符现在一律要求原样搬运。

外置:上下文是缓存,不是仓库

压缩再怎么小心也是有损的。更省事的办法是让大块内容一开始就别进来。

最直接的一条:不要把整个文件贴进对话。告诉它路径,让它自己去读。我早期习惯把两三百行的配置贴进去求安心,现在只贴路径加一句「这个文件定义了什么」。需要的时候它自己会去读,读错了还能重读。

第二条是把探索放进子上下文。让它去找「哪些地方调用了 export 逻辑」,这种搜索会翻十几个文件,每个文件的片段都堆进主上下文,几十 k token 就没了,最后有用的可能只有五行结论。我现在让它开一个子任务去搜,只把结论带回来。这个改动的收益比任何压缩技巧都大,尤其是探索性任务。

第三条是中间产物落地。生成的长文档、大段 SQL、试验脚本,全部写成文件,对话里只留路径和一句说明。滚动在对话里的草稿是最贵的垃圾——它占着 token,还随时可能被摘要模型误当成结论。

但外置不是免费的。它换来了两种新失败:一是模型找不到东西,二是它找到了却读错了版本。我踩过前者——把一份文档挪到 docs/design/ 下面,忘了在上下文里留索引,结果它绕了四轮才放弃。所以外置的东西必须满足两个条件:路径稳定,且上下文里有一句话说明它是什么。缺一个,模型就靠猜。

什么时候干脆开新会话

我一开始以为所有场景都该压缩,后来发现一半的情况该开新会话。

判断标准简单到有点土:目标变了就开新的,目标没变就压缩。同一个目标往下推,压缩是合理的,因为前面的结论还在。一旦目标换了——从「改导出逻辑」跳到「排查线上超时」——留在旧上下文里只会互相污染,模型会把旧任务的约束当成新任务的约束,这种事我遇到过不止一次。

新会话的启动成本被我低估了很久。读一遍 state.md、读两三个相关文件、确认一遍约束,大概两三轮的功夫。这个成本远低于在十七万 token 的上下文里跟一个记性变差的模型周旋。

折中的做法是同一个会话里分阶段,每阶段结束做一次「结算」:把这一段的结论写进 state.md,然后让上下文从下一段重新开始。这相当于手动控制压缩的时机,而不是等它撞到上限被动触发。被动触发的压缩最糟糕,它通常发生在任务做到一半、上下文最混乱的时候。

我现在的习惯是每个长任务目录里都放一个 state.md,最后一行永远写「下一步」。今天早上重开会话,它读到那行就直接接着干了,省掉十分钟的重新解释。问题是这一行我经常忘记更新,目前的办法是让它每次改完代码自己追加,攒到二十行左右我再手动合并一次。这个阈值还在试,二十行是不是太频繁,我还没有定论。

关于本文

由 Lu's Blog 撰写,采用 CC BY-NC 4.0 许可协议。

#Agent #上下文管理 #工程实践

评论