上周三下午,一个改订单导出逻辑的会话跑到第三个小时。右下角的 token 计数爬到十七万八,上限二十万。变化不是突然发生的,而是渐进的:它开始重新读一个十分钟前刚读过的文件,然后把我一小时前说过的「不要动 migration」忘得干干净净,顺手写了个新的迁移脚本。
我第一个反应很粗暴——让它把前面的对话总结一下。八百字摘要回来了,读起来通顺,信息密度也不低。可漏掉的恰好是最贵的东西:我否掉的两个方案、一个接口的精确路径,还有那句「不要动 migration」。
那次之后我把这件事拆成两半:哪些内容可以压,哪些必须搬到上下文外面去。
三种「记得」,代价差得很远
我先试着把上下文里的东西分了个类,发现它们被压缩时的损失完全不是一个量级。
第一类是对话历史,也就是「我们聊过什么」。这部分水分最大,绕的弯路、重复确认、我打字时的口误,压掉九成都不心疼。
第二类是工作状态:目标是什么、做到第几步、下一步干什么、哪里卡住了。这部分字数很少,但一旦丢了,模型就开始原地打转——我见过它在我已经改完的函数上再改一遍。
第三类是事实与约束:接口签名、文件路径、错误信息原文、我明确否定过的方案。这类东西的特点是短、硬、不可推导。模型没法从别处猜出 orders 表的主键是复合键,也没法猜出我为什么拒绝那个方案。
压第一类叫整理,压第二类叫失忆,压第三类叫事故。我之前的「总结一下」,是不加区分地把三类揉在一起交给模型,然后祈祷它懂得轻重。它不懂。
现在的做法是先在项目目录里放一个 state.md,大概三四十行,长任务每推进一步就追加几行:
1 | ## 目标 |
这个文件放在上下文开头,任何一次压缩都绕着它走。它的作用是把我脑子里的那部分状态,变成一段不参与摘要的原文。
压缩是编码,不是复述
认清一件事之后我调整了参数:摘要模型的目标是「覆盖」,不是「保真」。它会优先保留信息量大、叙述完整的段落,天然倾向于省略否定句和短小的硬事实——「不要动 X」在摘要里特别容易蒸发,因为它在语义上不携带新信息。
我试过两版提示词。第一版是「请总结以上对话」,出来的东西像会议纪要。第二版改成给一份不可压缩清单,明确列出四类必须逐字保留的内容:错误信息原文、接口签名与路径、被否掉的方案及理由、我原话里的否定要求。同一段 6 万 token 的历史,第二版产出的摘要更长,也更罗列,但漏项明显少了。
代价是摘要本身变长了。压缩比从大概 20:1 掉到 8:1,但我不需要那么高的压缩比,我需要它压完之后还能接着干。
分层的思路也类似,只是按距离分配预算。我大致按这个比例切的:
1 | def plan_budget(total=200_000, reserve=25_000): |
最近六到十轮保留原文,这部分是模型理解「我现在在干什么」的锚点,压了它反而更贵。中段压成要点,早期只留结论和约束。最后留一块固定预算给索引和状态文件。
有一类东西我干脆不允许改写:路径、命令、报错。我吃过一次亏,/api/v2/orders 在摘要里被压成了「订单接口」,后面模型自己补了个 /api/orders,调试了半小时才发现是它编的。数字和标识符现在一律要求原样搬运。
外置:上下文是缓存,不是仓库
压缩再怎么小心也是有损的。更省事的办法是让大块内容一开始就别进来。
最直接的一条:不要把整个文件贴进对话。告诉它路径,让它自己去读。我早期习惯把两三百行的配置贴进去求安心,现在只贴路径加一句「这个文件定义了什么」。需要的时候它自己会去读,读错了还能重读。
第二条是把探索放进子上下文。让它去找「哪些地方调用了 export 逻辑」,这种搜索会翻十几个文件,每个文件的片段都堆进主上下文,几十 k token 就没了,最后有用的可能只有五行结论。我现在让它开一个子任务去搜,只把结论带回来。这个改动的收益比任何压缩技巧都大,尤其是探索性任务。
第三条是中间产物落地。生成的长文档、大段 SQL、试验脚本,全部写成文件,对话里只留路径和一句说明。滚动在对话里的草稿是最贵的垃圾——它占着 token,还随时可能被摘要模型误当成结论。
但外置不是免费的。它换来了两种新失败:一是模型找不到东西,二是它找到了却读错了版本。我踩过前者——把一份文档挪到 docs/design/ 下面,忘了在上下文里留索引,结果它绕了四轮才放弃。所以外置的东西必须满足两个条件:路径稳定,且上下文里有一句话说明它是什么。缺一个,模型就靠猜。
什么时候干脆开新会话
我一开始以为所有场景都该压缩,后来发现一半的情况该开新会话。
判断标准简单到有点土:目标变了就开新的,目标没变就压缩。同一个目标往下推,压缩是合理的,因为前面的结论还在。一旦目标换了——从「改导出逻辑」跳到「排查线上超时」——留在旧上下文里只会互相污染,模型会把旧任务的约束当成新任务的约束,这种事我遇到过不止一次。
新会话的启动成本被我低估了很久。读一遍 state.md、读两三个相关文件、确认一遍约束,大概两三轮的功夫。这个成本远低于在十七万 token 的上下文里跟一个记性变差的模型周旋。
折中的做法是同一个会话里分阶段,每阶段结束做一次「结算」:把这一段的结论写进 state.md,然后让上下文从下一段重新开始。这相当于手动控制压缩的时机,而不是等它撞到上限被动触发。被动触发的压缩最糟糕,它通常发生在任务做到一半、上下文最混乱的时候。
我现在的习惯是每个长任务目录里都放一个 state.md,最后一行永远写「下一步」。今天早上重开会话,它读到那行就直接接着干了,省掉十分钟的重新解释。问题是这一行我经常忘记更新,目前的办法是让它每次改完代码自己追加,攒到二十行左右我再手动合并一次。这个阈值还在试,二十行是不是太频繁,我还没有定论。
评论