对话一长,AI 就忘了你说过的话?Claude Code、Codex、Aider、WES Code 怎么压缩上下文


用 AI 写代码,聊得久了很容易碰到这种事:开头明明交代过「不要修改 config.yaml」,几十轮以后,它还是把这个文件改了。

多数时候不是模型故意不听,而是这句话已经不在它眼前了。每次请求,工具都要把对话重新发给模型,而测试日志、文件内容这类工具输出越攒越多,迟早装不下。装不下就得腾地方,腾的时候丢了什么、留了什么,决定了 AI 还记不记得你说过的话。

各家的做法大体分两类:一类是让模型把前面的对话写成一段总结,Claude Code、Codex、Aider 都是这个思路;另一类是先把占地方的工具输出压缩掉,重要的话钉住不动,我日常用的 WES Code 走的是这条路。这篇对照官方文档讲清楚各家怎么做,用一个能直接运行的 Go 小程序演示最简单的删法为什么会丢掉约束,再看 WES Code 的分层压缩在引擎里跑出来是什么样。

一、为什么要压缩:上下文装满了就得腾地方

一次对话里,真正占地方的往往不是你和 AI 说的话,而是工具输出:跑一次测试几十行,读一个文件几百行,几轮下来就是几万个 token。腾地方的常见做法有三种:

做法 好处 代价
从最早的消息开始删 简单、不用调用模型 最早的往往是任务和约束,最先被删
让模型写一段总结 能保留前因后果 总结本身要调用一次模型,细节可能被概括掉
先压缩工具输出,重要的话钉住 不调用模型,约束原样保留 压缩掉的输出细节,需要时得重新读

二、让模型写总结:Claude Code、Codex、Aider

Claude Code 的文档写道,对话接近上下文窗口时会自动压缩,把历史对话写成总结。也可以手动输入 /compact,并在后面加一句话告诉它总结时保留什么,文档里的例子是 /compact Focus on code samples and API usage。还可以在项目根目录的 CLAUDE.md 里写一段压缩说明:

markdown 复制代码
# Compact instructions

When you are using compact, please focus on test output and code changes

同一页文档还给了几条控制上下文的办法:用 /context 看是什么占了地方;换到不相关的任务时用 /clear 重新开始;跑测试、处理日志这类输出很长的活交给子代理,长输出留在子代理那边,只把总结带回主对话。文档也提醒,/compact 要把整段对话读一遍才能总结,压缩一个很大的上下文,本身就是一次很大的请求。

Codex 的 /compact 也是这个思路:文档说它会把可见的对话写成总结来腾出 token,确认以后,用一段简洁的总结替换前面的轮次,同时保留关键细节。

Aider 把这件事做成了一个阈值:--max-chat-history-tokens 是聊天历史的软阈值,超过以后开始总结;做总结用的是 --weak-model 指定的那个模型,也就是同时用来写提交信息的那个辅助模型。

这类做法的好处是能保留前因后果,代价也很明确:总结的质量取决于模型,你那句「不要修改 config.yaml」会不会被写进总结,要看模型当时怎么概括。Claude Code 允许你写压缩说明,就是为了让你能左右它留什么。

三、最简单的删法为什么会丢掉约束

先看最省事的做法:从最早的消息开始删,删到装得下为止。我写了一个小程序,模拟一段四轮的对话:第一句交代任务和约束,后面每轮都跑一次测试、拿回四十多行失败日志。然后假设只装得下一半,对比两种删法:

go 复制代码
// 做法一:从最早的消息开始删,直到装得下
func dropOldest(ms []msg, budget int) []msg {
	for len(ms) > 1 && size(ms) > budget {
		ms = ms[1:]
	}
	return ms
}

// 做法二:带「不要」「必须」的消息钉住;旧的命令输出换成一行;还装不下,再删最早的、没钉住的消息
func layered(ms []msg, budget, keepRecent int) []msg {
	out := append([]msg(nil), ms...)
	seen := 0
	for i := len(out) - 1; i >= 0; i-- {
		if out[i].role != "tool" {
			continue
		}
		if seen++; seen <= keepRecent {
			continue
		}
		first := strings.SplitN(out[i].text, "\n", 2)[0]
		out[i].text = fmt.Sprintf("[已压缩] 共 %d 行,第一行:%s", strings.Count(out[i].text, "\n")+1, first)
	}
	// ...... 还装不下,就从前往后删没钉住的消息
	return out
}

运行结果:

text 复制代码
原始对话: 12 条, 8233 字符;只装得下 4116 字符
== 从最早的消息开始删:剩 6 条,4092 字符
第一条: 再跑一次全部测试
「不要修改 config.yaml」还在吗: false
== 钉住约束 + 压缩旧输出:剩 5 条,4101 字符
第一条: 帮我把订单模块的测试跑通。注意:不要修改 config.yaml。
「不要修改 config.yaml」还在吗: true

同样只装得下一半,第一种删法把开头的任务和约束删掉了,AI 接下来看到的第一句话是「再跑一次全部测试」;第二种删法先把旧的测试日志压成一行,再删没钉住的消息,那句约束一直留在最前面。

道理很简单:最占地方的是工具输出,最不能丢的是你交代的话,这两样不该按同一个规则处理。

四、WES Code:把「记住」和「腾地方」分开

WES Code 用的引擎把这件事拆成了两半。

记住交给会话状态:每轮对话结束后,引擎会在后台用同一个模型调用一次,把这一轮整理成一份结构化的状态,下一轮从这份状态接着做。这次调用不在界面上显示,失败了就退回一份不调用模型、直接生成的快照。状态里记的东西是固定的几类:

go 复制代码
type SettlementSnapshot struct {
	PendingTask string `json:"pending_task"`
	Goal        string `json:"goal"`
	// ......
	CurrentPhase string `json:"current_phase"`
	// ......
	CompletedActions []CompletedAction `json:"completed_actions"`
	KeyDecisions     []string          `json:"key_decisions"`
	RejectedPaths    []string          `json:"rejected_paths,omitempty"`
	FileStates       []FileState       `json:"file_states,omitempty"`
	OpenQuestions    []string          `json:"open_questions,omitempty"`
	Constraints      []string          `json:"constraints,omitempty"`

和「写一段总结」相比,这些字段把容易丢的东西单独列了出来:目标、做完了哪些操作、做过哪些关键决定、试过但放弃了哪些方案、有哪些约束。

腾地方交给一条不调用模型的压缩流水线,按代价从小到大排了五层:

go 复制代码
			pipeline := wcontext.NewCompressPipeline(0, budgetCfg, base.Logger)
			if budgetCfg.DeduplicatorEnabled {
				pipeline = pipeline.AddLayer(&compression.ToolResultDeduplicator{
					KeepRecent: budgetCfg.MicroCompactKeepRecent,
				})
			}
			pipeline = pipeline.
				AddLayer(&compression.FileCardStrategy{
					KeepRecent:      budgetCfg.MicroCompactKeepRecent,
					SymbolExtractor: s.fileCardSymbolExtractor,
				}).
				AddLayer(&compression.AgeBasedCompactStrategy{
					MaxAge:     budgetCfg.MicroCompactMaxAge,
					KeepRecent: budgetCfg.MicroCompactKeepRecent,
				}).
				AddLayer(&compression.DropOldestStrategy{}).
				AddLayer(&compression.InlineTruncateStrategy{
					MaxContentChars: 100,
					KeepRecent:      budgetCfg.MicroCompactKeepRecent,
				}).
				WithQualityGuard(wcontext.NewQualityGuard(0))
  • 去重:和后面某次结果一模一样的旧工具结果,换成一句「和后面那次内容相同」
  • 文件卡片:读过的旧文件换成一张卡片,记着文件路径、行数和顶层的函数、类型名,需要细节时再读一次
  • 按年龄压缩:模型还没看到的结果不碰,看过的结果里最近 3 条原样保留,更早的命令输出压成「跑了什么命令、退出码多少、多少行输出」,失败的会多留结尾 15 行报错;写文件这类操作的结果不压;能转存原文时,压缩后的内容还带着指向原文的指针
  • 丢最早的几轮:第一组(你最初交代的任务)和当前这一轮永远不丢,钉住的也不丢;丢之前先把这些轮里做过的操作整理成一条钉住的系统消息
  • 紧急截断:前面都不够时,才把旧内容截短

哪些话算「钉住」,有一份内置的关键词表,另外还会让模型判断哪些消息是目标、约束或偏好:

go 复制代码
var defaultSafetyKeywords = []string{
	"禁止", "不允许", "不要", "危险", "严禁", "必须", "绝不",
	"do not", "never", "must not", "forbidden", "prohibited",
	"dangerous", "critical constraint", "safety",
}

var defaultControlPatterns = []string{
	"在我说可以之前", "等我确认", "不要自动", "需要我的同意",
	"before i confirm", "wait for my approval", "do not proceed",
	"until i say", "don't do anything until",
}

我在 WES Code 用的引擎里写了个测试,同样用 -overlay 临时放进去跑、不往仓库写文件:构造一段四轮的对话,第一轮交代「不要修改 config.yaml」,后面六次跑测试,其中第一次和最后一次的输出一模一样,然后依次过钉住、去重、按年龄压缩、丢最早几轮:

text 复制代码
原始对话: 16 条消息,估算 3927 token
== 钉住:扫描关键词
第 0 条被钉住,原因 safety_constraint:user(钉住):帮我把订单模块的测试跑通。注意:不要修改 config.yaml。
== 第一层:去掉重复的工具结果
user:结果 t1,共 1 行,第一行 [Duplicate: same content as later call]
估算 3345 token
== 第三层:压缩旧的工具结果(模型还没看的不动,看过的最近 3 条原样保留)
user:结果 t2,共 17 行,第一行 [exec] ran `go test ./order/... -run TestTotal` → exit 1, 33 lines output
user:结果 t3,共 43 行,第一行     order_test.go:11: TestDiscount: got 90, want 100
......(t4、t5、t6 同样原样保留)
估算 3116 token
== 第四层:还不够,丢掉最早的几轮
system(钉住):[已完成操作摘要 --- 以下操作已执行,不要重复]
user(钉住):帮我把订单模块的测试跑通。注意:不要修改 config.yaml。
......(第一轮的两次调用和结果)
user:再跑一次全部测试
......
user:现在怎么样了?
......
估算 1860 token

几层下来,估算的 token 从 3927 降到 1860,第一条消息因为带着「不要」被钉住,始终在最前面;中间那一轮(看折扣的两次测试)被丢掉了,换来最前面一条钉住的「已完成操作摘要」,提醒模型这些操作已经做过、不要重复。

流水线还有一道熔断:同一个会话里连续 3 次压缩都省不下一成,就判定再压也没用,先停下来,照当前的上下文继续,免得每一轮都白白压一遍。

五、各自的边界

  • 让模型写总结(Claude Code、Codex、Aider):能保留前因后果,但总结本身要调用模型,压缩很大的上下文就是一次很大的请求;哪些细节进总结,要看模型怎么概括
  • WES Code 有三点要心里有数:
    • 关键词只认得明确的说法。「config.yaml 是线上配置,别碰」里有「别」没有「不要」,关键词表就匹配不到,只能靠模型判断和会话状态里的约束字段兜住
    • 压缩掉的旧输出只剩一行概要,模型需要细节时,得重新读文件或再跑一次命令
    • 每轮结束整理会话状态要在后台多调用一次模型,界面上看不到,但会多用一些 token

六、几条实用建议

  • 约束写得明确些:用「不要」「必须」「禁止」这类词,比委婉的说法更不容易在压缩里丢掉;长期有效的规矩,放进项目的规则文件,比如 Claude Code 的 CLAUDE.md
  • 换任务就开新对话:Claude Code 文档也建议换到不相关的任务时用 /clear,旧上下文只会占地方
  • 别让大段输出进主对话:跑全量测试、看长日志时,只让它看失败的部分或者结尾,或者交给子代理去处理
  • 手动压缩时告诉它留什么:用 Claude Code 的 /compact 时,在后面加一句要保留的内容

写在最后

上下文装不下是必然的,区别只在腾地方的时候怎么取舍。让模型写总结,好处是能留住前因后果,但留什么要看模型;最简单的从头删,最先丢的恰恰是你最早交代的话;WES Code 把「记住」交给结构化的会话状态,把「腾地方」交给不调用模型的分层压缩,你说过的约束被钉住,不参与压缩。了解这些规则之后,就知道哪些话该说得更明确、哪些输出不该让它进对话。

文中 WES Code 的部分来自我日常使用和它引擎里的代码,其余工具的说法来自各自的官方文档。你遇到过 AI 聊着聊着就忘了约定的情况吗?最后是怎么解决的?欢迎在评论区聊聊。

觉得有用的话,欢迎点赞、收藏。

相关推荐
用户5833339816681 小时前
影刀元素点击功能有什么替代方案?含 AI 自愈的几条实测路线
人工智能
趣说科技1 小时前
CMEF首日:探索BCI×AI产业应用新路径!熵基科技脑机全栈技术集中亮相
人工智能·科技
qinglongcn1 小时前
如何安全地停下一个正在工作的 Agent——中断与崩溃恢复
agent·ai编程
Rayna_Echoverse1 小时前
从Meta到Anthropic,AI巨头为什么突然开始争夺做你的私人助理
人工智能
野生码农AI实战1 小时前
FDE是什么?一个FDE垂直网站41分钟上线的全过程
人工智能
长弓三石1 小时前
企业级 Harness 平台的 IM 接入:从选型到上线,一篇讲完
人工智能
冬奇Lab1 小时前
开源项目第243期:ArtCraft —— 给艺术家的 AI 创作 IDE,先搭场景再生成,把「抽卡」变成「手艺」
人工智能·开源
闽林致远1 小时前
从文案语义到素材自动获取的工程链路-AutoVideoHub
人工智能
摩羯12301 小时前
AI提示词备查与原始输入输出记录:生成式人工智能合规的可验证实现
人工智能·aigc