压缩上下文,删字为何比摘要稳

Claude Code 内置的 /compact 把文件路径、报错、约束都压成摘要,后面的模型于是开始编故事。fast-jev-compaction 用 Jev 一次快请求逐条判定,只删字不总结。

源仓库: tamaratran/fast-jev-compaction

这项目在干什么

fast-jev-compaction 是一个 Claude Code 插件,同时也是一个 npm 库,4 天前上线,GitHub 涨到 5011 颗星。它的卖点只有一句话:把 Claude Code 自带的摘要式 /compact 换成”判定式删字”。做法是把每一条 tool_use / tool_result 一次性发给 TypeSafe 的 Jev 模型打分,stale 的丢掉或截断,留下的逐字保留,user / assistant 的文本块一行都不动。这波热度来自过去一周集中爆发的几个 issue——#65 报告压缩后模型 22 分钟内连续 9 个 turn 报告”已完成”的工作,零 tool 调用;#56 指出默认 keepThreshold=0.5 让 keep 几乎不可能命中,结果刚被判定最重要的工具结果反被丢掉;#70 揭示反复压缩时候选集永远覆盖不到新增内容。

现象 1:摘要式压缩为什么会丢关键证据

压缩到一半,模型突然引用一个不存在的文件路径,或者忘了上一轮报错里那行 auth.go:142: token.ExpiresAt compared with time.Now().Unix() but ExpiresAt is milliseconds。根因很简单:让 LLM 做总结等于在有限 token 预算下让细节先被砍,文件名、错误码、命令约束这些”低语义密度”内容最先消失。fast-jev-compaction 的对策是不重写文本——applyDecisions() 只在 Message 上把被判定 drop_calltool_use 过滤掉,把 drop_result 的内容换成 truncatedResultText() 的 note,剩下的对象引用原样返回:

export function applyDecisions(
  messages: readonly Message[],
  decisions: readonly CallDecision[],
  calls: readonly ToolCall[],
  headChars: number,
): Message[] {
  // untouched messages returned as the same objects they came in as
  if (!touched) { kept.push(message); continue }
}

现象 2:Jev 的”一次快请求”如何打分所有工具调用

上千条 tool_use 逐个问 LLM 又慢又贵。解法是 noul——一个无标签的多问题 prompt,对每个非 pinned 的 call 提两个问题(这条 call 还重要吗?这条 result 还能省吗?),把若干个 call 打包到一个请求里,state + questions 控制在 maxRequestTokens=30000(Jev 32k 上限之下),多个请求并发跑:

// src/request.ts: batchCalls() 把 questions 切成多个并发请求
const batches = batchCalls(questions, maxRequestTokens);
const answers = await Promise.all(batches.map((b) => jevAsker.ask(state, b)));

同一个完整 state 在每个请求里都重新发一份,所以一个长 session 大概是”一次请求问十几条 call”的代价,不是 N 次单轮调用。

现象 3:25k 状态预算下原始对话怎么塞得下

完整对话动辄上百 k tokens,fitState() 用五阶段瀑布式压缩,每阶只在上一阶不够时才触发:tool 输入从 1000 → 200 → 60 字符;长文本只留 head + tail;旧消息折叠成 [… N chars omitted …];只剩 tool call 的旧消息折成一行 t12 Read file_path=src/a.ts → ok 480ch;再不够就抛错(不静默丢信息)。token 估算用字符启发式(每 6 字母约 1 token,每数字半 token),刻意略高于 Jev 实际报告值,避免低估。

现象 4:keepThreshold=0.5 为何让 keep 几乎不可能

issue #56 的复现脚本显示,Jev 给出的 keepResult 普遍 0.20.4,keepCall 普遍 0.50.9,两者在不同量级,但 decideCall() 把两者都跟同一个 0.5 比较——结果 keep 永远落空,绝大多数 call 走 drop_call / drop_result。修法是 resultQuestion 选项(PR #61)让两个问题用不同的打分维度,或者双阈值(PR #68):

// 双阈值示例(issue #56 + PR #68)
keepThreshold: { call: 0.5, result: 0.25 }

现象 5:drop_call 不留 marker 为何让模型开始编

issue #65 描述了一个 22 分钟的事故:压缩后模型看到的是 9 条”我开了 PR、reviewer 给 REQUEST_CHANGES”的 assistant 文本,但这些文本对应的 tool_use 全被 drop_call 删了,没留任何痕迹。模型把这些叙述当成 few-shot,于是一直用嘴汇报工作。drop_result 已经会留 note:[fast-jev-compaction truncated N chars …; re-run the tool if needed]drop_call 什么都没留。最小修法是在 applyDecisions() 给丢 call 的 assistant turn 追加 [fast-jev-compaction removed N tool call(s) from this turn],让模型知道这段叙述原来是有证据的。

安装与最小可用

npm install fast-jev-compaction
export TYPESAFE_API_KEY=sk-...
import { compactMessages, reductionRatio, type Message } from 'fast-jev-compaction';

const transcript: Message[] = [/* Claude Code SessionMessage 子集 */];
const result = await compactMessages(transcript, { preserveRecentMessages: 4 });
if (reductionRatio(result) < 0.25) {
  // 压缩收益不够,退回原始 transcript 或走摘要
}

想接 Claude Code 插件,把仓库加为 marketplace,重启即可:CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 + claude plugin install fast-jev-compaction@fast-jev-compaction。toast 会显示 fast-jev-compaction: kept N/M messages, no summary (...),压缩比例不够时自动 fallback 到内置摘要。

Sources

本文参考 GitHub Trending 2026-09-21、tamaratran/fast-jev-compaction 仓库 README、Issues #56 #65 #70 #72、PR #61 #68,以及 Reddit r/ClaudeAI 上关于”压缩后幻觉”的讨论串。