ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

告别LLM摘要幻觉:用Jev决策重构上下文压缩

告别LLM摘要幻觉:用Jev决策重构上下文压缩

1. 当上下文压缩开始"说谎":一个被忽视的工程问题

用Claude Code写代码的人,大概率都遇到过这样的场景:一个稍大的重构任务聊到三四十轮,模型突然开始"失忆"——前面明确说过的接口约定它记不住了,刚改过的函数名它又用回了旧的,甚至把已经删掉的依赖重新引入。你以为是模型能力不行,其实很多时候问题出在上下文压缩这一环。

主流做法是让LLM自己去做有损摘要:把历史对话喂给模型,让它"总结一下重点"。这个方案听起来优雅,实际用起来问题一大堆。摘要是有损的,模型在压缩时会丢掉它认为"不重要"的细节,而这些细节往往正是后续任务的关键约束。更麻烦的是,摘要过程本身会引入幻觉——模型会"脑补"一些没发生过的约定,或者把两个不同阶段的决策混在一起。等你发现的时候,代码已经跑偏了。

fast-jev-compaction这个项目提出的思路很有意思:抛弃LLM有损摘要,改用Jev决策来重构上下文压缩。核心主张是,上下文压缩不应该是一个"生成"任务,而应该是一个"决策"任务——判断哪些信息该保留、哪些该丢弃、哪些该折叠,这个判断过程可以用更轻量、更确定性的机制来完成,而不是交给一个会自由发挥的LLM。

这篇文章我会从工程实践的角度,把这件事拆开讲清楚:为什么LLM摘要会出问题、Jev决策到底在决策什么、fast-jev-compaction的压缩流程是怎么设计的、实际接入Claude Code时要注意哪些坑。如果你正在用Claude Code做长期项目,或者自己在搭Agent的记忆系统,这篇应该能帮你少走不少弯路。

2. LLM有损摘要为什么在长任务里必然翻车

2.1 摘要的本质是信息丢弃,而丢弃标准不可控

先想清楚一件事:上下文压缩的本质是什么?是把一个长度为N的token序列,映射成一个长度为M(M远小于N)的序列,同时尽量保留"对后续任务有用的信息"。这里的关键词是"有用",而"有用"是一个依赖未来的判断——你现在不知道后面会问什么,怎么知道哪些信息该留?

LLM摘要的做法是让模型基于"常识"和"当前语境"去猜。它会把"用户说要用TypeScript严格模式"这种约束压缩成"用户偏好TS",把"接口返回字段是user_id不是userId"这种细节直接丢掉。前者还算能接受,后者一旦丢了,后面生成的代码就是错的。

我实测过一个案例:一个涉及数据库迁移的任务,前面明确说了"不要动production库的schema",聊到第20轮让模型写迁移脚本,它直接生成了ALTER TABLE语句。回头看摘要记录,那句约束在第三次压缩时就被"优化"掉了。这不是模型不听话,是摘要机制本身就在做有损丢弃。

2.2 摘要幻觉:模型会"编造"没发生的共识

比丢信息更隐蔽的问题是摘要幻觉。LLM在生成摘要时,会倾向于让摘要读起来"连贯、合理",于是它会自动补全一些逻辑上说得通但实际没发生的内容。

举个真实的例子。一个前端项目里,用户先说了"用Vite",后来说"算了还是用Next.js吧",再后来又改回"其实Vite也行"。三轮压缩之后,摘要变成了"用户最终确定使用Next.js"。模型把中间那次反复当成了"最终决策",因为它无法判断哪次是最终态。后续所有代码都基于错误的框架假设生成。

这类问题的根源在于:LLM摘要是一个生成任务,生成任务天然带有"创造性"。你让它总结,它就会为了让总结好看而做平滑处理,而工程约束恰恰是不能被平滑的。

2.3 压缩成本被严重低估

还有一个常被忽略的点:LLM摘要本身要消耗token和时间。每次压缩都要把完整历史喂进去,输出一份摘要,这个过程的成本可能比你想的高。在一个高频交互的Agent场景里,压缩调用可能占到总调用量的20%以上。而且压缩是串行的——你必须等摘要生成完才能继续,这直接拖慢了响应。

所以问题的本质是:用LLM做压缩,等于用一个不确定的、昂贵的、会幻觉的组件,去处理一个需要确定性、低成本、零幻觉的工程问题。这个错配,就是fast-jev-compaction想解决的。

3. Jev决策机制:把"总结"换成"判断"

3.1 Jev在这里扮演什么角色

先说清楚,Jev在这个项目里不是一个"更小的LLM",而是一套决策框架。它的核心思想是:上下文压缩不需要"理解"内容,只需要对每个信息单元做出保留/丢弃/折叠的分类决策。

打个比方。LLM摘要像是让一个人读完一整本书然后写读后感,读后感里必然有他的主观取舍。而Jev决策像是给书里每一页贴标签:这页是"核心设定"、这页是"临时草稿"、这页是"已废弃方案"。贴标签不需要理解全书,只需要按规则判断每一页的属性。

这个思路的工程价值在于:决策是可枚举、可验证、可复现的。给定同样的输入和规则,Jev会做出同样的决策,不会今天保留明天丢弃。这对Agent的稳定性至关重要。

3.2 决策的输入:信息单元的结构化

Jev决策的第一步,是把原始对话拆成信息单元。不是按句子拆,而是按语义角色拆。常见的单元类型包括:

单元类型说明默认处理策略
约束(Constraint)用户明确的技术选型、禁止事项永久保留
决策(Decision)已确认的方案选择保留最新态,折叠历史态
事实(Fact)项目结构、接口定义、字段名保留,冲突时标记
过程(Process)试错、调试、中间讨论可折叠为一行摘要
噪音(Noise)寒暄、确认、重复表述直接丢弃

这个分类不是让LLM去做的,而是通过规则+轻量分类器完成。规则部分处理格式明确的(比如"不要用X"这种否定句),分类器处理模糊的。关键是分类器只做分类,不做生成,所以不会幻觉。

3.3 决策的输出:压缩后的上下文骨架

Jev决策的产出不是一段"摘要文本",而是一个结构化的上下文骨架。它长这样:

[约束区] - 语言: TypeScript strict - 禁止: 修改production schema - 框架: 最终确定 Vite [事实区] - 接口: GET /api/user 返回 {user_id, name} - 文件: src/utils/format.ts 已存在 [过程区] - 第3-8轮: 讨论框架选型,最终Vite - 第12轮: 修复了一个日期格式化bug

注意,这里没有"总结",只有归类后的原始信息。约束就是约束原文,事实就是事实原文,过程才做折叠。这样做的结果是:关键信息零损失,只有过程性内容被压缩。

3.4 为什么这比LLM摘要更可靠

对比一下两种方案在几个维度上的表现:

  • 确定性:Jev决策给定输入必得同输出;LLM摘要每次可能不同。
  • 幻觉风险:Jev不生成新内容,只归类;LLM会脑补。
  • 成本:Jev的分类器可以很小,甚至纯规则;LLM摘要要跑大模型。
  • 可调试:Jev的决策可以打日志、可以回溯"为什么这条被丢了";LLM摘要是个黑盒。
  • 速度:Jev可以做到毫秒级;LLM摘要要秒级。

这不是说Jev在所有场景都碾压LLM。对于"把一段长文压缩成一段通顺的话"这种任务,LLM确实更强。但上下文压缩的目标不是通顺,是保真。在保真这个目标下,决策框架天然优于生成模型。

4. fast-jev-compaction的压缩流水线拆解

4.1 整体流程:从原始对话到压缩上下文

fast-jev-compaction的流水线大致分四步,我按实际接入时的顺序讲:

  1. 切分:把对话历史按轮次和语义边界切成信息单元。
  2. 分类:对每个单元打上类型标签(约束/决策/事实/过程/噪音)。
  3. 决策:根据类型和冲突规则,决定保留、折叠还是丢弃。
  4. 重组:把保留的单元按区域重新拼装成压缩上下文。

这四步里,第一步和第四步是纯工程,第二步是分类,第三步是规则引擎。整个流程不涉及任何生成式模型,所以延迟极低。

4.2 切分阶段:语义边界怎么找

切分是最容易被低估的一步。切得太碎,单元之间失去上下文;切得太粗,一个单元里混了约束和噪音,分类就难做。

实践中比较稳的做法是按"意图转换"切分。一次意图转换的标志包括:用户提出新要求、模型给出方案、用户确认或否定、话题切换。这些边界通常有明显的语言信号,比如"好的那就这样"、"等等,改成"、"另外"、"回到刚才那个问题"。

我自己的经验是,切分规则不要追求完美,宁可稍微碎一点。因为碎了的单元在重组阶段还能合并,但一个混了多种意图的大单元,分类阶段就会出错,而且错了很难修。

4.3 分类阶段:规则和分类器怎么分工

分类是整个流水线里最需要调优的部分。我的建议是规则优先,分类器兜底。

规则能覆盖的情况其实比想象的多。比如:

  • 包含"不要"、"禁止"、"必须"、"一定要"的句子,大概率是约束。
  • 包含"就用"、"确定"、"最终"的,大概率是决策。
  • 包含具体字段名、路径、函数签名的,大概率是事实。
  • 纯确认性的"好的"、"收到"、"嗯",直接判噪音。

规则覆盖不到的模糊情况,再交给一个轻量分类器。这个分类器可以是一个小模型,也可以是一个基于关键词权重的打分器。关键是它只输出类别,不输出内容,所以不会引入幻觉。

提示:分类器的训练数据可以从你自己的历史对话里标注。标个几百条,效果就比通用模型好很多,因为你的项目有自己的语言习惯。

4.4 决策阶段:冲突消解是核心难点

决策阶段真正难的不是"保留还是丢弃",而是冲突消解。同一个约束被用户改了三次,保留哪个?同一个字段名在两个阶段不一样,信哪个?

fast-jev-compaction的处理原则是时间戳优先 + 显式覆盖标记。具体来说:

  • 对于决策类,永远保留最新的一次,历史决策折叠成一行"曾考虑X,最终Y"。
  • 对于事实类,如果出现冲突,两个都保留但标记冲突,让后续模型自己判断,而不是替它决定。
  • 对于约束类,如果新约束和旧约束矛盾,保留新的,但把旧的移到"已废弃约束"区,不直接删。

这个设计很关键。不直接删意味着信息可追溯,出问题时能查到"这条约束是什么时候被覆盖的"。很多压缩方案为了省token直接删,结果调试时完全不知道模型为什么做了某个选择。

4.5 重组阶段:区域化拼装

重组不是简单拼接,而是按区域组织。推荐的区域顺序是:约束区 → 事实区 → 决策区 → 过程区。这个顺序有讲究:约束和事实是"硬信息",放前面让模型优先看到;过程是"软信息",放后面,模型需要时才参考。

每个区域内部按时间倒序排列,最新的在前。这样模型读到的第一眼就是最新状态,减少它被历史信息误导的概率。

5. 接入Claude Code时的实操细节与踩坑

5.1 压缩触发时机:别等爆了才压

最常见的错误是等上下文快满了才触发压缩。这时候历史已经很长,切分和分类的成本都上去了,而且压缩本身要占token,容易触发二次压缩。

我的做法是按轮次触发,而不是按token触发。比如每8轮做一次增量压缩,把已经稳定的历史(比如5轮之前的)压掉,最近5轮保持原样。这样每次压缩的量都不大,延迟可控,而且最近上下文永远是完整的。

5.2 和Claude Code的上下文管理怎么配合

Claude Code本身有自己的上下文管理机制,fast-jev-compaction要做的是在它之上做一层预处理。具体接入方式有两种:

一种是前置拦截:在把历史发给Claude Code之前,先过一遍Jev压缩,把压缩后的骨架作为上下文传入。这种方式对Claude Code透明,改动最小。

另一种是旁路注入:保持Claude Code原有上下文,额外注入一份Jev压缩的"约束提醒"。这种方式适合不想动原有流程、只想加一层保险的场景。

实测下来,前置拦截效果更好,因为从源头就控制了上下文质量。但要注意,压缩后的骨架要保留足够的格式信息,别让Claude Code"看不懂"。

5.3 分类器的冷启动问题

刚接入时没有标注数据,分类器效果差,怎么办?我的建议是先纯规则跑一周,把规则判错的case记下来,攒够一两百条再训分类器。这一周里规则可能只有70%准确率,但比LLM摘要的不可控要好得多。

另外,规则要写成可配置的,别硬编码。因为不同项目的语言习惯差别很大,A项目里"就这样"是确认,B项目里可能是犹豫。把规则外置成配置文件,换项目时改配置就行。

5.4 一个真实的翻车案例

我踩过最坑的一次,是分类器把一条否定约束判成了噪音。用户说"这个字段不要加索引",分类器看到"不要"以为是寒暄,直接丢了。结果后面生成的迁移脚本给这个字段加了索引,上线前才发现。

复盘原因:那条约束的表述太口语化,规则没匹配上,分类器又没见过类似样本。修复方式是加了一条规则:任何包含"不要"、"别"、"禁止"且后面跟技术名词的句子,一律判约束,宁可误判也不放过。误判的代价是多留一条信息,漏判的代价是线上事故,这个权衡很清楚。

6. 效果验证:怎么知道压缩没压坏

6.1 用"约束召回率"代替"压缩率"

大多数人评估压缩方案看的是压缩率——压到原来的百分之多少。这个指标是错的。压缩率再高,把关键约束压没了也是白搭。

正确的指标是约束召回率:压缩后的上下文里,原始对话中出现过的所有约束,有多少被保留了。这个指标应该追求100%,一条都不能丢。事实召回率可以放宽到95%以上,过程信息无所谓。

我自己的验证方法是:准备一组带标准答案的对话,人工标出所有约束,然后跑压缩,看召回率。低于100%就回去查是哪条规则漏了。

6.2 端到端验证:让模型做"记忆测试"

光看召回率还不够,要看模型实际用起来怎么样。方法是设计一组依赖早期约束的任务:先聊20轮建立约束,然后问一个必须用到早期约束的问题,看模型答不答得对。

比如前面说"所有API都要加rate limit",聊20轮后让模型写一个新接口,看它加不加。加了说明约束保住了,没加说明压缩出了问题。这种测试比看指标直观得多。

6.3 压缩日志:出问题时能回溯

一定要给压缩过程打日志。每条信息单元被分成了什么类、做了什么决策、为什么,都要记下来。出问题时,你能直接查到"这条约束在第几次压缩时被折叠了"。

这个日志平时看着没用,一旦出事故就是救命稻草。我现在的做法是日志保留最近30天的压缩记录,按会话ID索引,查起来很快。

7. 这套思路还能往哪扩展

fast-jev-compaction的核心洞察——压缩是决策不是生成——其实可以推广到很多场景。

比如Agent的长期记忆管理。现在很多方案是把记忆丢给向量库,检索时按相似度召回。但相似度召回同样有"该记的没记住"的问题。如果用Jev决策的思路,把记忆按约束/事实/过程分类存储,检索时按类型优先级召回,效果会稳很多。

再比如多Agent协作时的信息传递。Agent之间传消息,如果每条都完整传,上下文爆炸;如果让LLM摘要,又会失真。用决策框架做一层"信息路由",只传对方需要的类型,是个值得试的方向。

还有一个我觉得很有潜力的点:把压缩决策和代码结构对齐。比如约束对应到项目的lint规则,事实对应到类型定义,过程对应到git历史。这样压缩就不是在压缩文本,而是在维护一份和代码同步的"项目状态"。这个方向我还在试,目前看思路是通的,但工程量大,适合有长期项目需求的团队做。

回到最开始那个问题:为什么Claude Code聊久了会"失忆"?因为它在用生成模型做决策任务,这个错配是结构性的。fast-jev-compaction给出的答案不一定是最优解,但它指出的方向——把压缩从生成问题重新定义为决策问题——我认为是对的。至少在我自己的项目里,换成决策式压缩之后,那种"模型突然忘了约定"的情况基本没再出现过。

返回列表