
1. 从“有损摘要”说起为什么上下文压缩需要换一种思路如果你最近半年深度用过 Claude Code大概率遇到过这种场景一个中等规模的重构任务聊到第三十轮左右模型开始“失忆”——前面明确说过的接口约定、文件路径、命名规范它要么记混要么直接忽略。你不得不反复提醒甚至把关键信息重新贴一遍。这不是模型变笨了而是上下文窗口被塞满了系统触发了自动压缩而压缩的方式恰恰是问题的根源。目前主流的上下文压缩方案基本都走“LLM 有损摘要”这条路把历史对话丢给一个模型让它生成一段精简摘要然后用摘要替换原始上下文。这个做法听起来合理但实际用下来问题不少。摘要本身是一次有损转换模型在压缩时会丢掉它认为“不重要”的细节而这些细节往往正是后续任务的关键约束。更麻烦的是摘要过程本身要消耗一次额外的模型调用延迟和成本都上去了压缩质量还不稳定。fast-jev-compaction这个项目走的是另一条路。它抛弃了 LLM 摘要改用Jev 决策来重构上下文压缩逻辑。核心思路是不生成新的自然语言摘要而是通过一套确定性的决策规则判断哪些上下文片段该保留、哪些该丢弃、哪些该折叠成结构化引用。整个压缩过程不依赖模型推理纯 TypeScript 实现速度快、结果可预测、零额外模型调用。这篇文章就把这个项目的设计思路、核心机制、实操要点和踩坑经验完整拆一遍适合正在用 Claude Code 做重度开发、或者对上下文管理机制感兴趣的工程师参考。2. 项目整体设计与思路拆解2.1 为什么“有损摘要”在编码场景下特别容易翻车编码任务和普通聊天有个本质区别约束密度极高。一次对话里可能同时存在文件路径、函数签名、类型定义、命名约定、依赖版本、错误码、环境变量等十几类硬约束。这些约束之间还有依赖关系比如某个类型定义改了引用它的三个文件都得跟着调整。LLM 摘要在处理这种高密度约束时天然会做“语义压缩”——它倾向于保留“意图”而丢弃“细节”。但编码场景下细节就是意图本身。你说“把 UserService 的 getUserById 返回值改成 Promise ”摘要可能变成“修改用户查询接口的返回类型”路径和类型名全丢了。下一轮模型就不知道具体改哪个文件、改成什么类型。fast-jev-compaction的判断是压缩不应该做语义理解而应该做结构识别。哪些内容是“可折叠的”比如已经完成的工具调用结果哪些是“必须原样保留的”比如用户显式给出的约束哪些是“可以丢弃的”比如中间过程的调试输出这些判断用规则就能做不需要模型。2.2 Jev 决策到底是什么一套确定性的保留/丢弃规则集Jev 决策在这个项目里指的是一套基于上下文片段元信息的确定性决策机制。每个上下文片段在进入压缩流程前会被打上若干标签来源类型用户输入/工具返回/模型输出、是否包含显式约束、是否已被后续操作消费、时间戳、token 占比等。Jev 决策根据这些标签对每个片段做出三选一的判断保留片段包含未被消费的硬约束或属于最近 N 轮的关键交互原样保留。折叠片段内容已被后续操作消费或属于中间过程折叠成一行结构化引用如[tool:read_file pathsrc/user.ts tokens340 consumed]。丢弃片段属于重复内容、纯调试输出、或已被完全覆盖的旧版本直接移除。这套规则的关键在于确定性同样的输入永远得到同样的压缩结果。这对调试和复现极其重要。你用 LLM 摘要同样的对话压缩两次可能得到不同结果出了问题很难定位。Jev 决策不会压缩结果可预测、可测试、可版本控制。2.3 为什么用 TypeScript 实现类型系统本身就是约束载体项目选 TypeScript 不是偶然。上下文片段的结构、决策规则的输入输出、折叠引用的格式这些都需要严格的类型定义。用 TS 实现的好处是决策逻辑的每个分支都有类型约束不会出现“某个字段忘了处理”的情况。而且 TS 的类型定义本身就可以作为文档新人读代码时能直接看懂每个片段的完整结构。另一个实际考虑是集成成本。Claude Code 的插件生态和工具链大量使用 TS/JS用 TS 实现压缩模块可以直接嵌入现有流程不需要跨语言调用。项目里核心的决策函数是纯函数输入片段数组输出压缩后的片段数组没有副作用方便单测和性能优化。2.4 整体架构三层分离的设计项目在架构上做了三层分离这个设计值得单独说采集层负责从对话流中提取上下文片段打标签计算 token 占比。这一层不关心压缩策略只负责把原始数据整理成结构化片段。决策层Jev 决策的核心纯规则引擎。输入结构化片段输出每个片段的处置指令保留/折叠/丢弃。这一层没有任何 IO纯计算。执行层根据决策结果实际执行压缩操作生成新的上下文数组并维护折叠引用的索引表。三层分离的好处是决策规则可以独立迭代和测试不影响采集和执行。比如你想调整“最近 N 轮”的 N 值只改决策层的一个常量采集和执行完全不用动。这种可维护性在长期项目里非常关键。3. 核心细节解析与实操要点3.1 上下文片段的标签体系压缩决策的数据基础Jev 决策的质量取决于片段标签的准确性。项目里每个片段至少携带以下标签标签名类型说明对决策的影响sourceType枚举user / tool / model / systemuser 和 system 默认高保留优先级hasConstraint布尔是否包含显式约束路径、类型、命名等true 时禁止丢弃consumed布尔是否已被后续操作消费true 时可折叠turnIndex数字所属对话轮次越新优先级越高tokenCount数字片段 token 数用于计算压缩收益refKey字符串折叠引用的唯一键折叠时生成供后续引用这里有个实操要点hasConstraint的判断不能靠模型要用正则加关键词表。项目里维护了一份约束模式表覆盖常见编码约束表达比如文件路径模式、类型声明模式、版本号模式、环境变量模式等。这份表需要根据你的实际使用习惯持续补充这是项目落地后最需要维护的部分。注意hasConstraint误判的代价不对称。把普通内容误判为约束只是多保留了一些 token把约束误判为普通内容可能导致关键信息被丢弃后续任务直接跑偏。所以模式表宁可宽松不要过严。3.2 折叠引用的格式设计让模型能“按图索骥”折叠不是简单删除而是把片段替换成一行结构化引用。这个引用的格式设计很讲究目标是让模型在后续轮次里看到引用就能知道“这里曾经有什么”必要时可以要求展开。项目采用的引用格式是[jev:fold refrefKey typesourceType turnturnIndex tokenstokenCount summary一句话描述]举个例子一次文件读取操作被折叠后[jev:fold reff7a3 typetool turn12 tokens340 summaryread src/services/user.ts]这个格式的好处是ref唯一模型可以用它要求展开type和turn提供上下文定位summary是一句话描述不是 LLM 生成的而是从工具调用参数里直接提取的零成本且准确。模型看到折叠引用后如果后续任务需要这个文件的完整内容可以在回复里写expand f7a3执行层会从索引表里取回原始片段重新注入。这个机制让压缩变成“可逆”的这是 LLM 摘要做不到的。3.3 决策优先级当规则冲突时听谁的Jev 决策的规则之间会有冲突。比如一个片段既是 user 来源高保留又已经被消费可折叠还包含约束禁止丢弃。这时候需要一套优先级来裁决。项目里的优先级从高到低是包含未消费约束→ 强制保留无视其他规则最近 3 轮内的 user 输入→ 保留已被消费且非约束→ 折叠重复内容或纯调试输出→ 丢弃其他情况→ 按 token 占比和轮次综合评分低分折叠这个优先级顺序是项目作者根据实际使用反馈调出来的。最初版本把“最近 N 轮”放在最高优先级结果发现有些早期约束被后续轮次挤掉了导致任务跑偏。改成“约束优先”后稳定性明显提升。3.4 压缩触发时机不是等到满了才压一个容易被忽略的细节是压缩触发时机。很多方案是等上下文快满了才触发压缩这时候往往已经太晚压缩压力大容易丢关键信息。fast-jev-compaction采用的是渐进式压缩每轮对话结束后如果 token 占比超过阈值默认 60%就执行一次轻量压缩只折叠已消费的片段不动约束和近期输入。这样做的逻辑是压缩应该是一个持续的小动作而不是一次性的急救。渐进压缩每次只处理少量片段决策压力小出错概率低。等到真正接近窗口上限时大部分可折叠内容已经被处理过了只需要做最后的收尾压缩。阈值 60% 这个数字不是拍脑袋定的。实测下来60% 时触发压缩压缩后通常能回落到 40% 左右留出足够空间给后续几轮对话。如果设到 80% 才触发压缩后可能还在 65% 以上下一轮又得压频繁触发反而影响体验。4. 实操过程与核心环节实现4.1 环境准备与项目接入项目是 TypeScript 实现接入 Claude Code 的方式是作为压缩模块挂载到对话流程的钩子上。基础环境要求Node.js 18 以上TypeScript 5.0 以上Claude Code 支持插件钩子的版本安装依赖npm install fast-jev-compaction核心接入代码大概长这样import { JevCompactor } from fast-jev-compaction; const compactor new JevCompactor({ triggerThreshold: 0.6, // token 占比超过 60% 触发 recentTurnsToKeep: 3, // 最近 3 轮 user 输入强制保留 constraintPatterns: [...], // 约束识别模式表 foldSummaryMaxLen: 60, // 折叠摘要最大长度 }); // 在每轮对话结束后调用 const compressedContext compactor.compact(currentContext);这里constraintPatterns是最需要你自定义的部分。项目提供了一份默认模式表但你的项目可能有特定的约束表达习惯比如你们团队用see标注依赖关系或者用特定前缀标记环境变量。把这些补充进去压缩质量会明显提升。4.2 约束模式表的编写与调优约束模式表是一个正则数组每条模式包含正则、约束类型、优先级。示例const defaultPatterns [ { regex: /(?:src|lib|test)\/[\w\/.-]\.\w/g, type: filepath, priority: 10 }, { regex: /(?:interface|type|enum)\s\w/g, type: typedecl, priority: 10 }, { regex: /(?:v|version)\d\.\d\.\d/gi, type: version, priority: 8 }, { regex: /[A-Z_]{3,}[\w.-]/g, type: envvar, priority: 8 }, { regex: /(?:必须|禁止|不要|务必|注意)\S*/g, type: explicit, priority: 9 }, ];调优方法是跑一批真实对话记录看压缩后哪些信息丢了导致后续任务出问题把对应的表达模式补进表里。这个过程需要迭代几轮但一旦稳定下来后续基本不用动。实操心得模式表的正则不要写得太贪婪。比如文件路径模式如果写成/[\w\/.-]/g会把普通单词也匹配进去导致大量误判。加上目录前缀限定src|lib|test后准确率明显提升。4.3 折叠索引表的维护与展开机制折叠索引表是一个 Map键是refKey值是原始片段。执行层在折叠时写入在展开时读取。索引表需要设置容量上限避免无限增长。项目默认上限是 200 条超过后按 LRU 淘汰。展开机制的实现要点模型在回复里写expand refKey后执行层在下一轮上下文组装时把对应原始片段重新注入到折叠引用的位置。注入后该引用标记为“已展开”不再参与后续折叠决策避免反复折叠展开。这里有个坑如果模型连续展开多个引用可能导致上下文瞬间膨胀又触发压缩。项目的处理方式是展开操作有配额限制单轮最多展开 3 个引用且展开后的片段在下一轮压缩时优先保留因为刚被展开说明模型需要它。4.4 压缩效果的实际测量项目内置了一个简单的测量工具压缩前后各记录一次 token 数和关键约束保留率。关键约束保留率的计算方式是压缩前标记为hasConstraint的片段压缩后有多少仍然以原样或折叠引用形式存在。实测数据基于一个中等规模重构任务的 40 轮对话指标LLM 摘要方案Jev 决策方案压缩后 token 占比35%42%关键约束保留率71%98%压缩耗时1.2s含模型调用8ms结果可复现否是额外模型调用每轮 1 次0 次Jev 方案压缩后 token 占比略高42% vs 35%因为它保留了更多约束。但关键约束保留率从 71% 提升到 98%这个收益远大于多占的 7% token。压缩耗时从秒级降到毫秒级且零额外模型调用成本优势明显。5. 常见问题与排查技巧实录5.1 压缩后模型仍然“失忆”怎么排查这是最常见的问题。排查顺序建议是检查约束模式表是否覆盖了丢失的信息类型。把丢失的信息拿出来看它属于哪类约束模式表里有没有对应正则。没有就补上。检查折叠引用的 summary 是否准确。如果 summary 是从工具参数提取的确认提取逻辑没问题。summary 不准模型看到引用也不知道是什么。检查展开机制是否正常工作。模型可能写了expand但执行层没响应。看日志确认展开请求有没有被处理。检查压缩触发阈值是否过低。如果 60% 触发太频繁可以调到 70%给约束更多保留空间。5.2 折叠引用被模型忽略怎么办有时候模型看到折叠引用但不主动展开而是凭 summary 猜测内容导致理解偏差。解决办法是在系统提示里明确告诉模型看到[jev:fold ...]引用时如果需要完整内容必须写expand refKey不要凭 summary 猜测。另一个技巧是对于高优先级约束的折叠引用在 summary 里保留关键标识符比如summaryread src/services/user.ts (contains UserDTO type)。这样即使不展开模型也能获取关键信息。5.3 索引表膨胀导致内存问题长时间对话100 轮以上后索引表可能积累大量条目。虽然默认 LRU 上限 200 条但如果每轮折叠很多片段淘汰速度跟不上写入速度。排查方法是打印索引表大小随时间的变化曲线。解决办法有两个一是降低折叠粒度把多个小片段合并成一个折叠引用二是对已展开且超过 10 轮未再被引用的条目主动清理。项目里这两个策略都有开关默认开启。5.4 约束识别误判的典型场景约束识别误判主要有两类误判为约束普通叙述里出现了类似文件路径的字符串比如“这个方案像 src 目录一样分层”。这种误判代价小只是多保留 token。漏判为约束约束表达太隐晦模式表没覆盖。比如“那个返回类型记得改一下”没有显式类型名但隐含了类型约束。这种漏判代价大需要靠补充模式表解决。对于隐晦约束一个实用技巧是维护一份“指代词表”把“那个”“这个”“之前说的”等指代词与最近提到的实体关联起来。当指代词出现时回溯最近实体判断是否涉及约束。5.5 常见问题速查表现象可能原因排查动作解决方式压缩后约束丢失模式表未覆盖对比丢失信息与模式表补充正则模式模型不展开引用系统提示未说明检查提示词明确 expand 语法索引表内存增长折叠粒度过细打印索引表大小合并折叠或开启清理压缩频繁触发阈值过低看触发日志调高阈值到 70%展开后上下文膨胀单轮展开过多看展开配额日志限制单轮展开数折叠 summary 不准提取逻辑有误对比 summary 与原文修正提取函数6. 这套方案适合谁以及后续可以怎么扩展fast-jev-compaction这套思路最适合两类人一是每天用 Claude Code 做重度开发、经常遇到上下文丢失问题的工程师二是对上下文管理机制感兴趣、想自己定制压缩策略的开发者。它的核心价值不在于“压缩得更狠”而在于“压缩得更准”——用确定性规则替代概率性摘要把关键约束的保留率从七成提到九成以上。后续扩展方向有几个我觉得比较有意思。一是把约束模式表做成可学习的从历史对话里自动挖掘高频约束表达减少手工维护。二是把 Jev 决策与任务类型关联不同任务重构/调试/新功能用不同的保留策略比如调试任务更看重错误信息和堆栈重构任务更看重类型和路径。三是把折叠引用做成可嵌套的一个折叠引用里可以包含子引用形成层次化的上下文结构进一步压缩 token 同时保留可追溯性。我在实际使用中最大的体会是上下文压缩这件事越早做越好而且要做成持续的小动作不要等满了再急救。渐进式压缩配合确定性决策是目前我用过的方案里在稳定性、成本和可调试性上平衡得最好的一个。如果你也在被上下文丢失困扰建议先把约束模式表搭起来跑一批真实对话看看效果再决定要不要接入完整的压缩流程。