1. 为什么“上下文压缩”是 AI 编码代理绕不过去的坎
先抛一个我自己的真实感受:用 AI 编码代理写代码,最爽的是前二十分钟,最崩溃的是第四十分钟。前二十分钟它记得你所有的约定——变量命名风格、目录结构、哪个模块不能动、上次那个 bug 是怎么修的;到了第四十分钟,上下文窗口塞满了,系统开始做上下文压缩,然后你会发现它突然“失忆”了:明明十分钟前刚说过的接口签名,它给你换了个参数名;明明强调过不要动的那张表,它又给你加了个字段。
这不是模型变笨了,而是上下文压缩这个动作本身在丢信息。所谓上下文压缩,说白了就是当对话历史、代码片段、工具调用记录加起来超过模型能吃的 token 上限时,系统必须做取舍——要么把老内容摘要成一段话,要么直接截断丢掉,要么按优先级保留一部分。这个过程对人是透明的,但对 AI 编码代理来说是致命的,因为它赖以工作的“记忆”被动了手脚。
我这次做的事情,就是拿一个真实的编码代理工作流,连续跑了10 天,留下了430 条公开可查的记录,专门观察一件事:上下文压缩之后,代理到底还能不能接得上之前的活。围绕这个目标,我重点折腾了几个方向——long_mem(长时记忆)、engram(记忆痕迹/记忆印迹的思路)、以及Codex这类编码代理在压缩前后的行为差异。热搜里那一堆codex安装、codex配置、codex登录、codex cli、codex接入deepseek之类的词,其实都指向同一批人:正在把编码代理往自己工作流里塞的开发者。这篇文章就是写给这批人的。
如果你只是偶尔让 AI 补个函数,那上下文压缩对你影响不大;但如果你像我一样,让代理连续几小时甚至跨天去推进一个项目,那“压缩后接不上”这个问题,你迟早会撞上。下面我把自己这 10 天的实验设计、观察到的现象、以及最后沉淀下来的接续方案,一条条摊开讲。
2. 实验整体设计与思路拆解
2.1 为什么要用“10 天 + 430 条记录”这种笨办法
很多人评估编码代理,喜欢跑几个 benchmark 就下结论。我不太信那套,因为 benchmark 里的任务大多是“单轮、短上下文、目标明确”,而真实开发是“多轮、长上下文、目标会漂移”。上下文压缩的伤害恰恰在长会话里才暴露,短任务根本测不出来。
所以我选了最笨但最可靠的办法:固定一个真实项目,让代理每天推进一部分,全程记录。10 天下来积累了 430 条记录,每条记录包含:当轮的任务描述、代理的响应、是否触发了压缩、压缩后代理是否出现“记忆断裂”、以及我手动补了多少上下文才让它接上。这 430 条不是随便凑的数,而是每天大约 40 到 50 轮交互自然累积出来的,覆盖了从“写新功能”到“改 bug”到“重构”的完整周期。
提示:如果你也想做类似观察,别一上来就追求样本量。先把“是否触发压缩”和“压缩后是否断裂”这两个字段定义清楚,否则记录一堆没法分析的流水账。
2.2 三个核心变量:long_mem、engram、Codex
实验里我主要盯三个东西,它们分别对应记忆问题的三个层次。
long_mem解决的是“跨会话记忆”。普通代理的上下文只在单次会话里有效,会话一关就归零。long_mem 的思路是把关键信息持久化到外部存储,下次会话开始时再捞回来。它的问题是:捞什么、捞多少、怎么保证捞回来的不过时。
engram这个词借的是神经科学的“记忆印迹”概念,指的是信息被编码后留下的物理痕迹。放到代理里,我把它理解成“对某段上下文的压缩表示”——不是原文,而是一个能唤起原文的索引。engram 的价值在于,它比摘要更轻,比原文更省,但前提是索引得准。
Codex在这里代表的是编码代理这一类工具的执行层。热搜里codex安装、codex cli、codex配置、codex接入deepseek这些词说明很多人正在把它接到自己的环境里。我关心的不是怎么装,而是装好之后,它在压缩场景下的表现。
2.3 方案选型背后的取舍逻辑
我一开始想直接用现成的记忆框架,试了两天发现不行——通用记忆框架是为“问答”设计的,不是为“编码”设计的。编码场景有个特殊之处:代码是有强结构依赖的。你记住了“有个函数叫 parseConfig”没用,你还得记住它的签名、它被谁调用、它依赖哪个类型。摘要式记忆很容易把这些结构关系压没。
所以最后我走的是“分层记忆”路线:短期用原始上下文,中期用 engram 式索引,长期用 long_mem 持久化。三层之间靠一套明确的“提升/降级”规则衔接。这个设计的核心考量是:不同时间尺度的信息,压缩策略必须不同。把长期记忆和短期上下文用同一套压缩逻辑处理,是很多方案失败的根本原因。
3. 核心细节解析与实操要点
3.1 上下文压缩到底压掉了什么
要解决问题,先得知道丢了什么。我把 430 条记录里所有“压缩后断裂”的案例做了归类,发现丢的信息集中在四类:
| 丢失类型 | 具体表现 | 出现频率 |
|---|---|---|
| 接口契约 | 函数签名、参数类型、返回值结构被改写 | 高频 |
| 约束条件 | “不要动这张表”“必须用现有工具类”被遗忘 | 高频 |
| 决策历史 | 为什么选方案 A 而不是 B,被压缩掉 | 中频 |
| 文件路径 | 具体改哪个文件、哪个目录,记混 | 中频 |
你会发现,这四类里没有一类是“代码本身”。代码代理写代码的能力其实没怎么退化,退化的是对项目状态的记忆。这给了我一个重要启发:压缩策略应该优先保护“状态类信息”,而不是“内容类信息”。内容可以重新生成,状态丢了就接不上了。
3.2 long_mem 的落地:存什么、怎么存、何时取
long_mem 最容易踩的坑是“什么都存”。我第一版把每轮对话都存进去,结果检索时噪音太大,捞回来的十条里八条没用。后来改成只存三类:
- 决策记录:每个重要选择及其理由,用一句话写清“选了什么、为什么”。
- 契约快照:接口签名、数据结构定义,按文件维度存。
- 未完成事项:当前进行到哪、下一步要做什么。
存储格式我用的是结构化文本而不是向量库。原因很实际:编码场景的检索往往是“按文件”“按模块”精确查,不是语义模糊查。向量检索在这里反而容易召回不相关内容。结构化存储配合关键词索引,实测命中率更高。
注意:long_mem 的写入时机很关键。别等会话结束才写,那时候上下文可能已经被压缩过了,写进去的就是残缺信息。我的做法是每完成一个“可交付小单元”就写一次。
3.3 engram 式索引:让压缩后的上下文能“唤起”原文
engram 这一层是我觉得最有意思的部分。它的作用是:当上下文被压缩成一段摘要后,摘要里保留一些“钩子”,这些钩子能指向 long_mem 里的完整信息。
举个具体例子。压缩后摘要里可能只剩一句“已按约定完成配置模块改造”。这句话本身信息量很低,但如果它带一个钩子[ref:config-contract-v3],代理就能顺着这个钩子去 long_mem 里把配置模块的完整契约捞回来。这样摘要虽然短,但“接得上”。
钩子的设计要点是稳定且唯一。我一开始用自然语言描述当钩子,比如“那个配置相关的约定”,结果检索时匹配到一堆东西。后来改成固定格式的 ID,命中率立刻上来了。这跟给代码打 tag 是一个道理,模糊描述永远不如精确标识。
3.4 Codex 类代理在压缩前后的行为差异
这部分是我观察得最细的。同一个任务,在压缩前和压缩后交给代理,行为差异非常明显。
压缩前,代理倾向于先确认再动手:它会复述一遍约束,确认理解无误,然后开始改。压缩后,代理倾向于直接动手:它不再复述约束,上来就改,而且改的时候经常违反之前定好的规则。这不是它“不听话”,而是那些规则已经不在它的上下文里了。
还有一个细节:压缩后代理对“否定式约束”的遗忘特别严重。“不要用 X”“禁止改 Y”这类信息,比“要用 Z”更容易丢。我猜是因为否定式约束在摘要时容易被当成“次要信息”过滤掉。所以我的应对是:把关键否定约束提升为钩子,强制它在摘要里保留。
4. 实操过程与核心环节实现
4.1 环境搭建与代理接入
先把基础环境说清楚,不然下面的步骤没法复现。我用的是命令行形态的编码代理,跑在本地开发机上,项目是一个中等规模的后端服务,大概两万行代码,模块划分清晰,适合观察。
接入过程里,热搜里那些codex安装、codex cli、codex配置的坑我基本都踩了一遍。总结下来几个关键点:
- 安装:优先用官方渠道的安装包,别图省事用第三方打包版,版本对不上会导致配置项识别失败。
- 配置:配置文件里的字段名要严格对照文档,多一个空格都可能触发“无法识别的配置项”警告。
- 登录:登录态失效是高频问题,建议把登录态检查做成启动脚本的一部分,别等跑到一半才发现掉线。
- 模型接入:如果接的是第三方模型,注意模型名要写对,写错会直接报“模型不支持”。
# 启动前先做一次配置自检,避免跑到一半才发现问题 agent-cli config check agent-cli auth status agent-cli model list这三条命令是我每天开工前的固定动作。别嫌麻烦,配置问题在长会话里暴露的代价,比开工前检查高得多。
4.2 分层记忆的具体实现
我的分层记忆是这么搭的。短期层就是代理自带的上下文,不动它。中期层是一个本地索引文件,每轮交互后由脚本自动更新。长期层是一个按天分文件的记录库。
中期索引的更新逻辑是这样的:每轮交互结束后,脚本扫描这轮内容,提取出“契约变更”“决策”“未完成项”三类信息,生成带 ID 的条目,写进索引。同时给这轮上下文打上对应的钩子 ID。这样即使后面上下文被压缩,钩子还在,就能反查。
# 简化版的中期索引写入逻辑 def update_index(turn_content, turn_id): entries = extract_entries(turn_content) # 提取契约/决策/未完成项 for e in entries: e["ref"] = f"turn-{turn_id}-{e['type']}" index_store.append(e) return [e["ref"] for e in entries] # 返回钩子列表,供上下文打标长期层我做了个“每日快照”,把当天所有索引条目合并去重,形成一份项目状态摘要。第二天开工时,代理先读这份摘要,再开始干活。这一步相当于给代理“恢复记忆”,实测能显著降低压缩后的断裂率。
4.3 压缩触发时的接续流程
真正关键的是压缩触发那一刻怎么处理。我的流程分四步:
- 检测:监控上下文占用率,超过阈值就预警。
- 固化:在压缩发生前,强制把当前关键状态写入 long_mem。
- 打标:确保压缩后的摘要里保留必要的钩子。
- 验证:压缩后让代理复述一遍当前任务和约束,确认它接上了。
第四步特别重要。我一开始省了这步,结果代理带着错误的记忆往下跑,越跑越偏。后来加了“复述验证”,一旦发现它复述的约束和实际不符,立刻用钩子把正确信息捞回来重新注入。
提示:复述验证不要问“你记得吗”,要问“请复述当前任务的三个约束”。开放式提问代理容易糊弄,具体提问才能暴露它到底记没记住。
4.4 430 条记录里跑出来的关键数据
10 天下来,430 条记录里触发压缩的有 87 次,其中压缩后出现明显断裂的有 31 次。加了分层记忆之后,后 5 天的断裂率比前 5 天下降了大约六成。这个数字不算完美,但足以说明分层记忆的方向是对的。
更值得说的是断裂的分布。31 次断裂里,有 22 次发生在“跨天会话”场景,也就是第二天接着第一天干的时候。这说明跨会话的记忆恢复比会话内的压缩接续更难。会话内至少还有残存上下文,跨天基本是从零开始,全靠 long_mem 捞。
5. 常见问题与排查技巧实录
5.1 代理“假装记得”怎么办
这是最坑的一种情况。代理不会说“我忘了”,它会自信地编一个看起来合理的记忆。比如你问它“上次那个接口叫什么”,它会给你编一个名字,而且语气非常肯定。
我的排查办法是交叉验证:让它复述的同时,我去 long_mem 里查实际记录,对不上就立刻纠正。纠正的时候不要只说“错了”,要把正确信息完整注入,否则它下次还会编。
5.2 压缩后约束被违反的排查
约束被违反通常有两个原因:要么约束没进摘要,要么进了摘要但被代理忽略了。区分方法很简单:看压缩后的摘要里有没有这条约束。没有,就是压缩策略问题,要调整钩子;有但还被违反,就是代理的注意力问题,需要把约束提到更靠前的位置。
5.3 常见问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 压缩后接口签名变了 | 契约未进 long_mem | 补写契约快照,加钩子 |
| 跨天会话接不上 | 未做每日快照恢复 | 开工前先读状态摘要 |
| 代理编造记忆 | 检索无结果但未报错 | 加交叉验证,强制查库 |
| 配置报错无法识别 | 配置项拼写或版本不符 | 对照文档逐项核对 |
| 登录态中途失效 | 长会话超时 | 启动脚本加状态检查 |
5.4 几个我踩过的坑
第一个坑是过度依赖摘要。我一开始觉得摘要越短越好,结果短到把关键约束都压没了。后来明白,摘要的目标不是短,是“够用”。宁可长一点,也别丢关键信息。
第二个坑是钩子 ID 不稳定。早期我用时间戳当 ID,结果同一件事在不同轮次生成了不同 ID,检索时对不上。后来改成基于内容哈希生成 ID,同一件事的 ID 就稳定了。
第三个坑是忘了清理过期记忆。long_mem 越存越多,检索越来越慢,而且会捞回过时的信息。后来加了“有效期”字段,超过一定时间的决策记录自动降权。
6. 关于记忆接续这件事,我最后想说的
跑了这 10 天,我最大的体会是:上下文压缩本身不是问题,问题是压缩之后没有接续机制。很多人把希望寄托在“模型上下文窗口越来越大”上,觉得窗口够大就不用压缩了。但真实项目里,代码量、对话量、工具调用量增长得比窗口快,压缩迟早会发生。
真正靠谱的做法,是把记忆当成一个独立的、需要主动管理的系统。long_mem 负责持久化,engram 负责索引,代理负责执行,三者之间靠明确的规则衔接。这套东西不复杂,但需要你愿意花时间把规则定清楚。
如果你现在正在用编码代理推进一个长项目,我的建议是:从今天开始,每完成一个小单元就写一条决策记录,别等。等压缩发生了再补,补出来的往往是残缺的。这个习惯看起来笨,但它是我这 430 条记录里唯一被反复验证有效的做法。