跳到主要内容

数据截至 (上游 commit b77d61291399)

压缩器全家桶 — 每种内容各有各的「不能破坏什么」

30 秒导读: 上一章讲的是「一段内容怎么被路由到对的压缩器」(01-pipeline-and-router)。这一章讲被路由到之后发生了什么——十来个压缩器,每个吃一类内容,各自用完全不同的算法,但设计套路是同一个:先钉死一条不能破坏的不变量,再围着它设计降级路径。JSON 数组不能改 schema,源码不能变得不可解析,表格不能把值挪错列,日志不能把两类错误合并成一类。做不到就退回原文,绝不交付「看起来压缩了但含义变了」的结果。


1. 一句话总纲:压缩器 = 一条不变量 + 一条降级路径

先把全家桶摆出来。看这张表的方法:第三列才是每个压缩器真正的设计约束,第二列的算法是为了满足第三列才长成那样的。

内容类型压缩手法不能破坏什么(不变量)破不了时的降级
JSON 数组无损列折叠 → 有损丢行 + CCR 哨兵输出里只能有原数组里的元素,不加包装、不加生成文本原样返回(lossless_only:uncompacted)
源码AST 解析 → 保签名 + 截函数体输出必须仍能被 tree-sitter / ast.parse 解析退回原文;或转交 Kompress(此时明确标 syntax_valid=False)
grep/rg 结果每文件保首末 + 按分打分选行file:line:content 三元组不能错位原样返回
日志 / 构建输出分级打分 + 保守去重 + 栈帧折叠两条不同类别的错误不能被折成一条只做前缀保留的保守归一化
git diff按 hunk 选,收缩上下文行增删行本身不删(always_keep_*)原样返回
YAML/TOML/INI三层:可逆折叠 → 注释省略 → schema 折叠引号里的 # 是数据不是注释检测到块标量就整层关掉
HTMLtrafilatura 抽正文这是抽取不是压缩,正文一个字不改抽不出来返回空串
CSV / markdown 表转 records → 交给 SmartCrusher行的单元格数必须等于表头数参差表判为「非表格」原样放行
纯文本(快)句子打分 + shingle 去近重抽取式:保留的是原句原词,不改写段数不够就不压
纯文本(准)ModernBERT 逐 token 保留打分数字/路径/错误码/标志位必须留下三级超时 → passthrough,连错三次熄火

共同的降级哲学只有一句:压缩失败是可接受的,压错是不可接受的。仓库里把这条写成了一份 feedback memory,反复被引用:feedback_no_silent_fallbacks.md(见 headroom/transforms/smart_crusher.py:269headroom/transforms/diff_compressor.py:83)。

三层降级模型长这样,从左往右是尝试顺序,命中即停:

┌──────────────┐ 够省 ┌────────────────────────┐
一段内容 ──▶│ ① 无损重排 │ ─────▶│ 交付,不需要任何召回 │
│ 同样的信息 │ └────────────────────────┘
│ 更少的字节 │
└──────┬───────┘
│ 不够省 / 不适用

┌──────────────┐ 允许 ┌────────────────────────┐
│ ② 有损裁剪 │ ─────▶│ 交付 + 留召回记号 │
│ 丢一部分 │ │ <<ccr:HASH ...>> │
└──────┬───────┘ └────────────────────────┘
│ 不允许(严格无损 / 保护行 / 语法坏了)

┌──────────────┐
│ ③ 原样返回 │ 这次不省了,但绝不出错
└──────────────┘

第 ② 步那个记号怎么兑现,是下一章 03-ccr 的内容;这里只关心「什么时候有资格走到第 ②/③ 步」。


2. 所有压缩器长的都是同一副骨架

不管吃的是 JSON 还是日志,代码结构都是三段。先建立这个心智模型,后面每一节就只是在填三个格子。

原始文本


┌──────────┐ 把无结构的字节变成有结构的单元
│ ① 解析 │ JSON→rows / 源码→AST / 日志→LogLine / 表→records
└────┬─────┘


┌──────────┐ 给每个单元打分,再决定保谁
│ ② 选择 │ 位置锚点 + 异常检测 + 用户问题相关性 + 自适应配额
└────┬─────┘


┌──────────┐ 渲染 + 自检;自检不过就整段回滚
│ ③ 渲染 │ csv-schema / 合法源码 / 折叠后的日志
└──────────┘

第 ② 步里的「用户问题相关性」和「自适应配额」是跨压缩器共享的两个模块,值得单独一节讲(见 §7)。它们是「同一份 grep 结果,你问的问题不同,留下来的行就不同」的原因。


3. JSON 数组 — SmartCrusher

JSON 数组是 agent 工具输出里最常见的形状:一次 list_issues 回来 800 条记录,每条 20 个字段,字段名逐行重复。这也是省得最多的地方。

3.1 它守的那条不变量:schema-preserving

配置类的 docstring 把话说死了:输出里只能包含原数组里的元素,没有包装层、没有生成的文字、没有额外的元数据键(headroom/transforms/smart_crusher.py:161 SmartCrusherConfig,docstring 第一行 SCHEMA-PRESERVING)。

为什么这条这么重要?因为下游代码会拿到压缩后的数组接着遍历:

# 示意,非源码 —— 下游消费者的典型写法
entries = json.loads(tool_output)
for e in entries:
print(e["level"], e["msg"]) # 它假设每一项都是同一个 schema

如果压缩器往数组里塞一个 {"summary": "共 800 条,略去 785 条"},这段循环立刻 KeyError。所以 SmartCrusher 只会少给几行,不会改行的形状

唯一的例外是丢行时补的那个哨兵对象,而且它被设计成尽量不破坏这条不变量:

{"_ccr_dropped": "<<ccr:abc123def456 785_rows_offloaded>>"}

它仍然是个 dict,数组仍然是「对象数组」,只是多了一个众所周知的键(Rust 侧生成于 crates/headroom-core/src/transforms/smart_crusher/crusher.rs:560-575)。Python 侧给出常量和过滤器,让下游一行代码把它摘掉:CCR_SENTINEL_KEY(headroom/transforms/smart_crusher.py:79)、is_ccr_sentinel(:82)、strip_ccr_sentinels(:87)。strip_ccr_sentinels 还刻意对非 list 输入原样返回,这样调用方可以直接包住 json.loads 的结果而不用先判形状。

3.2 先试无损:csv-schema 列折叠

有损丢行需要模型再花一次工具调用去取回,无损重排不需要。所以 SmartCrusher 是无损优先的。

一个 3 行的数组,原样序列化是这样(字段名重复 3 遍):

[{"id":1,"lvl":"INFO","msg":"start"},
{"id":2,"lvl":"WARN","msg":"slow"},
{"id":3,"lvl":"INFO","msg":"done"}]

csv-schema 渲染器把它折成一行声明 + 三行 CSV(crates/headroom-core/src/transforms/smart_crusher/compaction/formatter.rs:256 write_table):

[3]{id:int,lvl:str,msg:str}
1,INFO,start
2,WARN,slow
3,INFO,done

信息一个字没少,字节掉了一半以上。可选的渲染器有三种,由 CompactionStage::SUPPORTED_FORMAT_NAMES 声明(crates/headroom-core/src/transforms/smart_crusher/compaction/mod.rs:95):

渲染器形状什么时候选它
csv-schema声明行 + CSV 行默认。字节最省
json紧凑 JSON下游要结构化消费、调试
markdown-kv每行 - key: value用 token 换模型读取准确率

无损不是无条件采用的,得先赢过一个门槛。Rust 侧算出渲染后的字节数,和原数组的估算字节数比,省得不够就放弃无损、落到有损路径(crates/headroom-core/src/transforms/smart_crusher/crusher.rs:816-845):

// crusher.rs:833 —— 真实源码片段
if savings_ratio >= self.config.lossless_min_savings_ratio {

门槛默认 0.15,而且 Python 和 Rust 两侧必须保持一致——Python 那侧的注释直接点名 config.rs 说「两者必须步调一致」(headroom/transforms/smart_crusher.py:186-193)。为什么无损的门槛比有损低?注释也说了:无损输出不需要 CCR 往返,模型想看更多行时直接就在眼前,所以它值得一个更宽松的准入。

3.3 再退有损:选行策略与「必留」约束

无损没赢,才进有损。有损路径的核心是选哪些行留下,由四个策略分工(crates/headroom-core/src/transforms/smart_crusher/planning.rs:97 create_plan 分发):

策略适用形状主信号
TimeSeries有时间戳的序列变点前后 ±2 行全留
TopN带分数字段的排序结果按分数取前 N
ClusterSample日志式按 message 前 50 字符的 md5 分簇,每簇留 2 条
SmartSample兜底位置锚点 + 数值异常(> variance_threshold σ)+ 变点 ±1

四个策略的骨架是同一套,写在 planning.rs:5-20 的模块注释里:锚点 → 策略专属信号 → 错误关键词 → 查询锚点与相关性 → TOIN 学到的字段 → 去重收尾。

其中「错误关键词」这一步是必留约束,不受配额限制。 它以 Constraint trait 的形式注入(planning.rs:85 apply_constraints),OSS 默认装了两条:保留含错误关键词的行、保留结构异常的行。一个 800 条日志的数组里那唯一一条 FATAL,不会因为「统计上不显著」被采样掉。

统计上再聪明的采样,也不知道「这行是合规审计记录,消失了要出事」。所以 #1705 在 Python 侧额外加了一层 audit-safe 模式,而且刻意不下沉到 Rust:

┌── 压缩前:按 protected_patterns 扫出「保护行」
crush_array_json ──▶ Rust 统计采样 ──▶ 结果
└── 压缩后:比对,少了的保护行原样补回(splice)
└── 补完还是少 ──▶ fail closed:整段返回原文

三个方法各司其职:_scan_protected_rows(headroom/transforms/smart_crusher.py:581)扫描,_splice_missing_protected(:596)用 Counter多重度感知的补回(两条一模一样的保护行会被分别记账),_apply_audit_safe_protection(:625)做补回后的复核。复核仍然不够时,默认按 fail_closed_on_protected_loss=True 整段回滚(:669-678)——宁可不压,不可漏

配套的小细节也很讲究:_compile_protected_patterns(:546)在构造时就编译正则,编不过直接抛 ValueError。理由写在 docstring 里——把非法正则悄悄当成「没有保护行」,恰恰会让调用方以为自己保护了、其实没有。

还有一处只有踩过才知道的细节:补回后只在真的补了东西时才重新序列化(:654-660)。因为 Python 的 json.dumps 和 Rust 的 serde_json 在非 ASCII 转义上不一定一致,不改就别碰,保持和 Rust 输出字节相同。

3.4 两道「不许有损」的总闸

有两个开关能把整条有损路径关掉,语义不同。

lossless_only 是模式级的:无损压缩照做,但任何需要留 CCR 记号的路径(丢行、opaque blob 外置)一律改成「不压」。输出保证无记号、字节可完全还原(headroom/transforms/smart_crusher.py:194-200)。Rust 侧在有损路径入口留了一句 debug_assert!,把这条约束钉在代码里:

// crusher.rs:881 —— 真实源码片段
debug_assert!(
!self.config.lossless_only,
"lossy path reached under lossless_only — the early return \
above must keep this codepath (and its CCR store write) \
unreachable in strict lossless mode",
);

这句断言不是装饰。Python 侧的按调用覆盖(crush(..., lossless_only=True))靠的是临时换一个 Rust crusher 实例(_build_rust,headroom/transforms/smart_crusher.py:446),而那个备用实例的 CCR store 之所以能保持空,前提正是「严格无损模式下永远走不到写 store 的那行」。断言坏了,备用实例的 store 就会分叉。

enable_ccr_marker 是记号级的:ccr_config.enabled=Falseinject_retrieval_marker=False 任一为假,就把它压成 False(:397-399)。此时行照丢,但既不发记号也不写 store——发一个 prompt 里没人能引用的记号毫无意义,而在用户明确关掉的情况下偷偷存一份原文更是意外的副作用。

3.5 这个类如今只是 Rust 的一层壳

SmartCrusher 的 Python 实现已经退役(2026-04-27,Stage 3c.1b),整个类现在是 headroom._core.SmartCrusher 的 PyO3 壳(模块 docstring:headroom/transforms/smart_crusher.py:1-19)。退役前用 17 份录制 fixture 做过逐字节比对。

哪些还留在 Python,是有讲究的:

留在 Python 的部分为什么不下沉
SmartCrusherConfig / CrushResult 两个 dataclass调用方在用 asdict()__dict__、dataclass 模式匹配
apply() 的消息遍历、digest 标记插入、token 计数是围着压缩调用的胶水,不是压缩本身
TOIN 学习记录Rust 不知道 TOIN;不接回来这条学习链就断了
CCR 记号镜像到 Python store/v1/retrieve 查的是 Python store,和 Rust 进程内 store 是两个东西(#389)
audit-safe 保护纯 Python 后处理,_rust_cfg_kwargs 刻意不传这几个字段(:215-225)

导入是硬导入,没有 Python 兜底(:270-275)。轮子没编译就直接崩,理由还是那句:响亮地失败好过悄悄降级。

GIL 也是有意释放的。 PyO3 侧对 crush / smart_crush_content 都套了 py.detach(...)(crates/headroom-py/src/lib.rs:764、:777),这样代理里其它 Python 线程能在 JSON 解析 + 递归处理 + 逐数组压缩这段重活期间继续跑。

TOIN 记录还有一个容易漏的过滤条件:只在 was_modified=True 并且 strategy != "passthrough" 时才记(:512、:866)。因为 Rust 有时只是把 JSON 重新规范化了一遍空白,was_modified 就翻成 True 了——这种「压缩」没有任何学习价值。

3.6 两个 serde_json feature 是正确性要求,不是性能选项

工作区 Cargo.toml:50 给 serde_json 开了三个 feature,前两个直接关系到正确性:

feature干什么不开会怎样
preserve_orderValue::Object 底层换成 IndexMap,保留 JSON 的解析顺序默认 BTreeMap 会按键排序,每一个多键对象的输出都和 Python 的 dict 插入序不一致,逐字节比对全挂
arbitrary_precisionValue::Number 保留源文里的字面数字串1.0 会塌成 1,12345678901234567 会掉精度——违反「未改动的字节必须原样透传」这条 Realignment I1 不变量
raw_value暴露 RawValue,未解析的 JSON 片段未修改的 messages[*] 无法按原始字节切片转发

第二条尤其值得记住:一个压缩器如果把 1.0 变成 1,它就已经在说原文没说的话了,哪怕数值上相等。这和 §5.4 里表格「参差行一律不折」是同一个原则的两种表现。

3.7 为什么宁可抛 NotImplementedError

SmartCrusher.__init__ 的签名里还留着 relevance_configscorer 两个参数,但任一非 None 就直接抛异常:

# headroom/transforms/smart_crusher.py:345-352 —— 真实源码片段
if relevance_config is not None or scorer is not None:
raise NotImplementedError(
"SmartCrusher: custom `relevance_config` / `scorer` "
"overrides are not yet supported by the Rust-backed "
...

这是全仓库最能代表设计取向的一段。 参数保留是为了源码兼容,不静默忽略是因为:调用方如果依赖一个自定义打分函数,而我们把它丢了,他拿回来的压缩结果是错的,而且他看不见。压缩器的输出天生难以核对——你没法一眼看出「这 15 行是不是该留的那 15 行」。正因为看不见,静默降级在这里的代价比在别的系统里高得多

同一条逻辑还出现在另一处:compaction_format 即使在 with_compaction=False(该参数会被忽略)的情况下也照样校验,不认识的名字直接 ValueError(:430-438)。理由是「旋钮碰巧没被用到」不等于「配错了没关系」。


4. 源码 — CodeAwareCompressor

4.1 不变量:输出必须还能被解析

代码压缩的诱惑是「按 token 重要度删词」,但那样删出来的东西不是代码了,模型读到会开始编。所以这个压缩器的不变量是硬的:压缩后的字符串必须仍然是合法源码

它的实现思路可以一句话概括:保住骨架,削掉血肉。导入、签名、类型标注、装饰器、类型定义一律保留;函数体按重要度分配行数预算,超预算的部分折成一行注释。

一个 Python 函数压完长这样(_compress_function_ast 的输出形状,headroom/transforms/code_compressor.py:1857-1893):

def process_data(items: List[str]) -> List[str]:
"""Process a list of items."""
results = []
# [8 lines omitted; calls: normalize, validate]
pass

三处细节都不是随手写的:

  • 签名和类型标注一字不改——模型靠它判断怎么调用。
  • # [8 lines omitted; calls: ...] 里的 calls 列表来自符号分析,告诉模型这段被折叠的代码调用了谁(_make_omitted_comment,:2453)。
  • 冒号语言(Python)额外补一行 pass,否则空函数体语法就错了(:1890-1892)。

4.2 多语言靠数据表,不靠 if-else

11 种语言各声明一张 AST 节点类型表,如果每种写一套 _extract_python/_extract_go,这个文件会失控。它改成由表驱动,提取和压缩逻辑只有一份(LangConfig,:292;表本体 _LANG_CONFIGS,:328)。

数一下容易差一个:CodeLanguage 枚举有 12 个成员(:213),但其中 UNKNOWN 不是语言;_LANG_CONFIGS 实际只有 11 条(PYTHON / JAVASCRIPT / TYPESCRIPT / GO / RUST / JAVA / C / CPP / PERL / CSHARP / PHP)。再者,PERL 虽然配置在表里,却被 _UNSAFE_TREE_SITTER_LANGUAGES(:106)排除在 AST 路径之外——配置在,但不启用(详见 §9)。

一条语言配置声明这些东西:

字段声明什么Python 的取值
import_nodes哪些节点算导入import_statement
function_nodes / class_nodes哪些是函数 / 类function_definition / class_definition
body_node_types哪个子节点是函数体block
decorator_node装饰器包装节点decorated_definition
comment_prefix注释前缀#
uses_colon_after_signature冒号语言还是花括号语言True
opaque_node_types整块保留、不递归进去的节点(C# 用它挡 #if)

opaque_node_types 那条注释值得单独看(注释 :321-324,字段声明 :325):递归进 C# 的 #if 块会把它的后代逐个收集一遍,而顶层又会把整个包装节点原样再发一次,内容就重复了。所以宁可要这个漏检(不压 #if 里的内容),也不要那个重复。

4.3 预算怎么分:重要的函数留得多

不是每个函数都砍到 5 行,而是按重要度分配预算。两步:

第一步,给每个符号打分(_analyze_symbol_importance,:917)。信号是加起来的原始分,再做 min-max 归一化到 0~1——归一化是关键,它让分数在这个文件内部相对,所以工具库、测试文件、编排器都能自适应:

信号加多少直觉
被引用次数+refs文件里到处在用的符号更重要
是公开符号+1.0下划线开头的是内部实现
扇出(它调了几个本文件符号)+0.5 × fan_out编排型函数是主干
Python dunder / Go 首字母大写+2.0 / +1.0语言约定的重要性
用户问题里点了它的名字+3.0权重最大的单项

最后一条就是「用户问题改变保留谁」在代码压缩里的落点(_symbol_in_context,:2414),权重比其它所有信号都高。

第二步,把总预算按 score × body_size 加权分下去(_allocate_body_budget,:1072)。总预算 = 总行数 × target_compression_rate − 固定行数(导入、签名这些反正要留的)。分数有个 score_floor = 0.05 的地板,保证最不重要的函数也不至于分到 0 行。

max_body_lines 始终是硬上限,预算再多也不能超(_get_body_limit,:2438)。

4.4 按语句截断,不按行截断

拿到 body_limit 之后,最容易写错的实现是「切前 N 行」。切在 if x and ( 中间,输出就废了。

真实实现走的是 AST 语句边界(:1796-1856):遍历 body_node.children,每个 named child 就是一条完整语句,整条整条地留,加不下就停:

# headroom/transforms/code_compressor.py:1851 —— 真实源码片段
if kept_line_count + stmt_line_count > body_limit and stmts_kept > 0:
break

注意 and stmts_kept > 0:第一条语句哪怕超预算也要留下,不然函数体是空的。

另外两处只有踩过才知道的坑:

  • 行切片用行号,不用字节偏移。tree-sitter 的字节偏移不含首行的前导空白,直接切会丢掉嵌套方法的缩进(:1638-1642 的注释)。
  • Go 的 statement_list 包装节点要拆开(:1828-1836)。当成一条语句处理的话,它的行范围会吞掉 block 自己的闭花括号行,后面就多出一个 }

4.5 三道闸门:先自检,不过就回滚

压完之后是三道独立的检查,任何一道不过都返回原文。

压缩产物


① _verify_syntax ── 不过 ──▶ (Python 专属) 逐节点重试
│ 过 └── 还不过 ──▶ 返回原文

② ratio < 0.05 ? ── 是 ────▶ 返回原文(压过头 = 丢数据)
│ 否

③ ratio < 0.8 ? ── 是 ────▶ 存 CCR + 追加召回记号


交付

第 ① 道(_verify_syntax,:2084)对 Python 做双保险:ast.parse compile(),再过一遍 tree-sitter 查 ERROR 和 MISSING 节点(_has_syntax_issues,:2486)。查 MISSING 而不只查 ERROR 是必要的——tree-sitter 会「脑补」出缺失 token 让树看起来完整。

Python 专属的重试(recover_invalid_python_nodes,:1249)很巧:只有当原文本身合法、压缩后不合法时才启动,这时给每个节点装一个 candidate_validator——把候选文本拼回整个模块,整体 ast.parse 过了才采用这个候选,否则该节点退回原文(:1355-1372)。逐节点回滚,而不是整文件放弃

第 ② 道(:1275-1276)是「压过头等于丢数据」的经验闸:留下不到 5% 的 token,基本上意味着结构提取出了 bug。

第 ③ 道降级路径 _fallback_compress(:2103)有一个诚实得可贵的细节: 转交 Kompress 之后,结果里明确写 syntax_valid=False,并附注释「Kompress 不保证语法合法」。它没有假装自己仍然守着那条不变量。

Rust 端的移植(crates/headroom-core/src/transforms/code_compressor.rs)把最要命的约束写在模块头上:逐字节对齐要求两侧的 tree-sitter 语法版本产出节点级完全相同的 AST,所以 Cargo 里把 tree-sitter-python 之类精确 pin 到 =0.25.0,和录制 fixture 时的 PyPI wheel 版本一致(:11-20)。同一份注释也坦白了范围:非 ASCII 标识符下 Python 侧按字节切 str 本身就是隐性 bug,两边会分叉,所以非 ASCII 明确划在 parity 范围之外(:22-31)。


5. 半结构化六件套:各有各的折叠手法

这六个吃的都是「有行有列但不是 JSON」的文本。它们体量都不大,但每个都有一处非做不可的判断,下面按「折什么 / 守什么」两栏读。

5.1 search_compressor — 每文件保首末

grep/ripgrep 的输出是 file:line:content。折叠手法是按文件分组,每个文件保首尾各一条,剩下的名额按分数填(_select_matches,headroom/transforms/search_compressor.py:293)。首尾必留是因为它们标出了这个文件里匹配的范围。

打分很朴素(_score_matches,:247):查询词命中 +0.3、错误模式命中 +0.5 递减、配置关键词 +0.4,封顶 1.0。有意思的是它为 CJK 加了字符 bigram(_cjk_bigrams,:66)——中文查询没有空格,按词切分永远命中不了

三个 bug 修复记在模块 docstring 里(:20-38),都是解析层的:Windows 盘符 C:\ 被当成行号分隔符导致整段丢失、文件名里的 - 被排除在路径字符集之外、CCR 存储失败被静默吞掉。「解析器把行认错了」是这类压缩器最典型的失败模式——不是压得不好,是压了别的东西。

5.2 log_compressor — 保守去重与栈帧折叠

日志的折叠手法有三层:按 level 打分选行(_score_line,headroom/transforms/log_compressor.py:323)、相似 warning 去重、栈追踪折帧。

去重必须保守。 老实现把整行的数字/路径/十六进制都归一化,结果两条类别不同、只是尾部形状相似的错误被折成了一条。新实现在第一个 := 处切开,只归一化尾部,前缀原样保留(_dedupe_similar,:419):

# headroom/transforms/log_compressor.py:428-433 —— 真实源码片段
split_at = next((i for i, c in enumerate(content) if c in (":", "=")), len(content))
prefix = content[:split_at]
suffix = content[split_at:]
suffix = digit_re.sub("N", suffix)

栈追踪不是尾部截断,是折叠中间。 超过 stack_trace_max_lines 时,保住消息头 + 前 trace_head_frames 帧 + 至多 trace_app_frames 帧应用代码,把运行时/标准库帧折成一行 [... N frames collapsed](配置项与注释见 :112-119)。因为报错原因在栈顶,你的代码在栈中段,盲截尾巴恰好切掉后者。

还有一处 Rust 端修的老 bug:旧的状态机遇到空行就认为栈结束,而 Python 的链式异常回溯里空行是内部结构,一截就丢中段(模块 docstring :22-26)。

选完行之后还会补上下文:每条选中行前后各 error_context_lines 行一并带上(_add_context,:445)——错误信息本身经常没有信息量,信息在它上面那行。

5.3 diff_compressor — 增删行本身绝不动

git diff 的折叠手法是按 hunk 选、收缩上下文行,而 always_keep_additions / always_keep_deletions 默认 True(headroom/transforms/diff_compressor.py:37-38)。这就是它的不变量:+/- 行是 diff 的全部信息量,只有周围的上下文行可以砍

这个类同样已经全量下沉到 Rust,Python 只剩 dataclass 和一层壳(:1-19)。壳里最要紧的一个方法是 _persist_to_python_ccr(:129),它修的是一个很典型的双 store 问题:Rust 侧发出的记号带的是 MD5(original)[:24],而 Python store 自 PR #395 起默认用 SHA-256(original)[:24],不显式传 explicit_hash 的话,每一个 diff 记号在生产里都是悬空的(#816)。

它还额外暴露一个 compress_with_stats(:154),返回 Rust-only 的统计结构(每文件掉了几个 hunk、修剪了几行上下文)。Python 侧没有对应类型,所以老老实实标成 Any,不硬造一个假的镜像。

5.4 config_compressor — 三层,越往后越需要召回

YAML/TOML/INI 没有原生压缩器,magika 会把它们标成 SOURCE_CODE 然后落到有损散文路径去——那是不可接受的。这个模块补上三层,按「需不需要召回」排序(headroom/transforms/config_compressor.py:9-23):

干什么可逆性
Tier 1相同行 / 重复多行段落折到精确逆变换的记号后面,并自校验往返完全可逆,无 CCR 模式也能用
Tier 2整行注释和空行省略,留 Retrieve original: hash=…靠 CCR
Tier 3TOML 的 [[array-of-tables]]tomllib 解析,桥到 csv-schema靠 CCR

守的那条不变量是:引号里的 # 是数据,不是注释_elision_safe(:238)在 YAML 有块标量(|/>)或 TOML 有多行字符串时直接返回 False,把整个 Tier 2 关掉。注释写得很明白:检测故意过宽,拿不准就不省(:53-55)。

Tier 3 只桥 TOML,不桥 YAML/INI,理由同样是不变量:tomllib 是标准库参考解析器,解出来的记录是 ground truth;YAML/INI 需要非标准库或自制解析,一旦解错就会「说原文没说的话」(:17-23)。而且 Tier 3 必须先存住原文才敢发折叠结果——_store_original 返回 None 就整个放弃(:228-230)。

5.5 html_extractor — 这是抽取,不是压缩

模块 docstring 第一段就澄清了定位:这是在删结构噪声(script/style/nav/ads/footer),不是在删 token(headroom/transforms/html_extractor.py:1-7)。典型减少 70-90%,而正文零损失。

实现是薄薄一层 trafilatura 封装(extract,:109),加上元数据抽取(标题、作者、日期)。抽不出来时返回空串并 debug 日志一句(:144-146),不做任何猜测。

is_html_content(:199)是个纯启发式:<!doctype html / <html 开头直接判 True,否则数前 2000 字符里出现了几个 HTML 标签指示词,≥2 才算。

5.6 tabular_ingest — 参差表一律不碰

CSV/TSV、markdown 表、定宽表都没有原生压缩器。这个模块只做 text → records 的桥,压缩本身交回 SmartCrusher(:1-11)。

它的不变量是全家桶里最锋利的一条:行的单元格数必须等于表头数

# headroom/transforms/tabular_ingest.py:141-143 —— 真实源码片段
width = len(headers)
if any(len(row) != width for row in rows):
return None

注释解释得毫不含糊(:137-140):参差行 zip 成 records 会把值挪到错误的列下面,而压缩后的表绝不能陈述原表没有陈述过的事实(#1652)。所以判为「非表格」,原样放行。

采纳与否还有一道门:SmartCrusher 压的是 JSON 形式,得拿它的输出和原始表格文本比字节,省不到就不采纳(:194-195)——本来就紧凑的 CSV 未必赢得过它自己。

5.7 text_crusher — 抽取式,不改写

TextCrusher 是给大段散文准备的请求路径安全的替代品:毫秒级,而 ModernBERT 是分钟级(headroom/transforms/text_crusher.py:1-12)。

它的不变量写在类 docstring 里:输出是输入原句的逐字保留(各自 trim、用换行重连),按原顺序;它只做选择,不做改写(:36-39)。

算法在 Rust 侧:按句切段,用 recency + 查询相关 + 结构显著性三项加权打分,再用全局 word-shingle 索引压近重复,按 target_ratio 取头部(crates/headroom-core/src/transforms/text_crusher/crusher.rs:1-12)。相关性那一项复用的是 SmartCrusher 用的同一个 BM25 打分器,没有重新实现一份——这正是 §7 要讲的共享层。

CJK 走单独的 ICU(UAX#29 + 词典)分句分词路径,纯 ASCII 输入逐字节保持原行为(同上 :9-12)。


6. 纯文本 ML — Kompress

6.1 逐 token 打分,按词还原

Kompress 是一个微调过的 ModernBERT(chopratejas/kompress-v2-base),对每个 token 输出「该不该保留」。

工作流是词级的,不是 token 级的——因为最终要还原成人能读的文本:

content.split() → words[]

▼ 每 chunk_words 个词一批
tokenizer(is_split_into_words=True) → input_ids + word_ids


模型 → get_keep_mask() 或 get_scores()

▼ 用 word_ids 把 token 决策映射回词下标
kept_ids: set[int]


" ".join(words[w] for w in sorted(kept_ids))

关键在 word_ids:一个词可能被切成多个 subword,任一 subword 被保留,这个词就进 kept_ids(headroom/transforms/kompress_compressor.py:1656-1661)。最后按下标排序还原,保证词序不乱(:1682)。

6.2 target_ratio 的两种语义

同一个参数在两条分支下含义不同,这点容易读错(:1627-1661):

target_ratio走哪条语义
None(默认)get_keep_mask()模型自己定留多少,按内部分数阈值
设了值(如 0.3)get_scores()强制留这个比例:每个词取其 subword 的最高分,排序后取前 max(1, N × ratio)

docstring 补了一句运维含义:代理从不设这个参数,只有面向用户的 API 会设(:1467-1469)。也就是说生产路径上是模型自己决定压多狠。

6.3 must-keep:模型说了不算的那部分

有一类 token 丢了模型就没法重建,不管打分多低都得留。这条硬覆盖在每个 chunk 处理完之后无条件执行(_add_kompress_must_keep_words,:64;调用点 :1667):

# headroom/transforms/kompress_compressor.py:49-58 —— 真实源码片段(节选模式)
_KOMPRESS_MUST_KEEP_RE = re.compile(
r"\b0x[0-9A-Fa-f]+\b" # 十六进制地址/ID
r"|(?<![\w.])\d+(?:\.\d+)?(?![\w.])" # 独立数字
r"|[A-Z_]{2,}" # ALLCAPS: SIGILL, EOF, ERROR
r"|[a-z_][a-z0-9_]*\.[a-z0-9_]+" # 带点路径
...

覆盖的七类:十六进制、独立数字、全大写标识符、点分路径、unix 路径、扩展名、命令行标志、驼峰名。注释点明理由——这些携带的是 agent 无法从上下文重建的语义,丢了会降低推理正确性(:43-47)。可以用 HEADROOM_KOMPRESS_MUST_KEEP=0 关掉,但那是调试用途。

6.4 ONNX 运行时:长驻进程带来的四个约束

headroom/onnx_runtime.py 很短,但每个函数都对应一个线上事故。

函数干什么起因
cpu_arena_enabled(:33)默认关 CPU 内存 arena,Windows 上例外关 arena 能降常驻 RSS;但 Windows 上关掉会让每次 Run() 退化成逐节点 VirtualAlloc,慢 2-3 个数量级(onnxruntime#11627),压缩全部超时
onnx_thread_spinning_enabled(:50)默认让空闲线程阻塞而不是自旋ORT 线程池默认在推理间隙自旋等待,常驻代理把所有核跑满(#2495)
create_cpu_session_options(:156)组装上面两条 + 线程数长驻进程偏向可预测的内存而非峰值吞吐
trim_process_heap(:202)Linux 上调 malloc_trim(0) 把空闲堆页还给 OS变长推理之后的匿名 RSS 不还给系统

还有一条和压缩质量无关但同样重要:模型工件按 commit SHA 锁死(_PINNED_REVISIONS,:71)。上游 HF 仓库被改或被投毒时不会被静默拉下来。要升级模型必须手动改这张表。hf_hub_download_local_first(:91)先试 local_files_only=True 再回落网络,冷启动之外不发 HEAD 请求;allow_network=False 时缓存未命中直接抛,让启动预热永远不会阻塞端口绑定。

ONNX 工件本身也有降级顺序(注意这一条不在 onnx_runtime.py 里,而在 headroom/transforms/kompress_compressor.py:90 _DEFAULT_ONNX_FILENAMES):int8 权重量化(261MB)→ fp32(601MB)→ v1 时代的动态 int8。第一档的注释附了实测:测试集 n=500,f1 0.9130 vs fp32 的 0.9128,保留决策一致率 99.6%——等效质量,内存少 2.2 倍。老版本 onnxruntime 没有那个 8-bit 算子的话,会在 session 加载时失败并自动落到下一档。

6.5 三道时间闸 + 失败闩锁

ONNX 推理是 O(tokens) 且一旦 asyncio 超时触发就无法抢占。一个大块跑几分钟会占死 worker,连锁成执行器饱和 → 队列超时(#1171)。所以 compress() 里叠了三层时间控制(:1493-1610):

触发时机行为
_time_budget_seconds(默认 20s)chunk 边界直接 passthrough
HEADROOM_COMPRESSION_DEADLINE_MSchunk 边界 / 获取信号量前剩余的词原样保留然后 break——部分压缩立刻返回,好过完整压缩拖死线程
执行信号量获取超时并发满passthrough

再往外还有一个进程级的闸:连续 3 次推理失败就把 Kompress 熄火到进程结束(_INFERENCE_FAILURE_LATCH,:104;_record_inference_failure,:1742)。注释解释了为什么是固定次数而不是滑动窗口——坏掉的工件每次调用都会失败,窗口能告诉你的东西,三振同样能告诉你。成功一次就清零,所以只有连续失败才会闩上(:1725-1727)。

6.6 远程 Kompress:同一个契约,失败开路

RemoteKompressCompressor(headroom/transforms/kompress_remote.py:100)让一个没装 [ml] extra(没有 torch/onnx)的代理照样能用 Kompress:把推理 POST 到一个 /compress 端点。

两个设计点值得记:

CCR 留在本地。 端点是无状态的(enable_ccr=False),存储和召回记号都在代理侧生成(:215-230),所以 headroom_retrieve 照常工作,原文永远不出本机

失败一律开路(fail open)。 非 2xx、超时、字段畸形、缺 compressed——任何一种都原样透传(:211-213)。而且数值字段的强制转换放在 try 里面(:198-210),注释说明了为什么:一个 200 响应里 original_tokens 是显式 JSON null 的话,int(None) 会抛出去、把代理请求打断,那就违背了这个类承诺的开路契约。一句话总结在模块 docstring 里:端点坏了只该损失压缩,不该损失正确性(:50-51)。


7. 相关性排序 — 用户问题如何改变「保留谁」

前面每一节都提到「查询命中就留」。这一节把那层单独拆开:它是跨压缩器共享的一个模块,是「同一份工具输出,你问的问题不同,留下来的东西就不同」的技术来源。

7.1 三个打分器,一个协议

协议只有两个方法:score(item, context)score_batch(items, context),返回带解释的 RelevanceScore(headroom/relevance/base.py:35、:16)。分数在 __post_init__ 里被夹到 [0,1](:30-32)。

打分器依赖强在哪弱在哪
BM25Scorer(bm25.py:27)零依赖,纯 PythonUUID / 数字 ID 精确匹配,~0ms没有语义:errors 匹配不到 failed
EmbeddingScorer(embedding.py:106)可选语义近似要模型、要算力
HybridScorer(hybrid.py:25)两者融合

BM25 的分词模式专门为这个场景调过:UUID 整体作为一个 token,4 位以上数字作为一个 token,然后才是普通字母数字(_TOKEN_PATTERN,bm25.py:52)。不这么切的话,查一个 UUID 会被切成一堆无意义的 hex 段。IDF 用的是 Lucene/ES 那个带地板的变体 log((N-n+0.5)/(n+0.5)+1),保证词出现在过半文档时结果仍非负(_compute_idf,:100)。

Embedding 模型同样锁 commit SHA(_DEFAULT_MODEL_PINNED_REVISION,embedding.py:69),和 §6.4 一个道理。

7.2 自适应 alpha:查询里有 UUID 就别信语义

Hybrid 的融合权重不是常数,而是看查询长什么样现算(_compute_alpha,hybrid.py:115):

查询里出现alpha(BM25 权重)直觉
UUID≥ 0.85必须精确匹配,语义相似毫无意义
≥2 个数字 ID≥ 0.75像是在做查表
1 个数字 ID≥ 0.65
主机名 / 邮箱≥ 0.6
都没有(自然语言问句)0.5 基准语义权重上来

最终夹在 [0.3, 0.9](:151),两边都不会被完全关掉。

embedding 不可用时不是简单退成 BM25 就完事——BM25 的绝对分天生偏低,直接用会让所有东西都落到阈值以下。所以做了 boost:有任何命中就至少给 0.3,命中 ≥2 个词再 +0.2(:168-182)。

7.3 三个注入点:同一个查询,三处改变结果

用户的问题不是在一个地方起作用,而是在三个不同的层面上:

用户问题 + 工具调用参数

├──▶ ① 锚点权重 : 改变「从哪个位置取样」
│ recency 词 → 权重后移;historical 词 → 权重前移

├──▶ ② 每条记录打分 : 改变「哪几条越过阈值」
│ BM25 / Hybrid,阈值本身还随分数分布浮动(Otsu)

└──▶ ③ 保留数量 K : 由内容多样性定,查询通过 bias 缩放

① 锚点权重(adjust_weights_for_query,headroom/transforms/anchor_selector.py:441):查询里有 recency 关键词就把 0.15 的权重从 front 挪到 back,有 historical 关键词就反过来,然后归一化。也就是说,问「最近怎么了」和问「一开始是什么样」,取样的位置区域是不同的。锚点整体流程见 select_anchors(:494):算预算 → 定策略 → 按查询调权重 → 分 front/middle/back 三区取 → 中段可用信息密度打分(calculate_information_score,:132)→ 按 item hash 去重。

② 每条记录打分:SmartCrusher 侧是 apply_query_signals(crates/headroom-core/src/transforms/smart_crusher/planning.rs:478),先做确定性的查询锚点精确匹配,再做概率性的相关性打分。两者顺序不能反——精确匹配是保证,打分是补充。

散文侧则是 plan_relevance_split(headroom/transforms/relevance_split.py:134),它有两个值得学的设计:

查询由什么组成。 不只是用户 prompt,还拼上触发这次输出的工具调用参数(build_relevance_query,:32)。注释说明了为什么:grep 的 pattern、read 的路径才是「这一份输出到底在问什么」的最锐利信号,让 BM25 那半咬住精确 token,语义那半跟住意图。

阈值由数据自己定。 用 Otsu 法(_otsu_threshold,:93)找这份输出自身分数分布里的天然分界,再用一个绝对下限兜底(adaptive_threshold,:119)。好处是无参数——候选切点就是数据本身的取值,没有 bin 大小、没有魔数。全体分数相同时找不到分界,交给下限决定(全留或全丢)。

分段本身是无损划分:"".join(segment(content)) == content(:16-19),所以 KEEP 段能逐字节重建原文。分段还是边界感知的——空行分隔记录、缩进续行跟着父行(栈追踪、pretty JSON 不会被从中间切开)、没有空行的密集流按小窗口打包(:51-90)。

③ 保留数量 K(compute_optimal_k,headroom/transforms/adaptive_sizer.py:27):这一层不看查询,只看内容本身有多少信息,分三级:

手段判什么
Tier 1SimHash 聚类(count_unique_simhash,:248)全是近重复?那留 3 条就够
Tier 2唯一 bigram 覆盖曲线 + Kneedle 拐点(find_knee,:114)加到第几条之后,新信息增量陡降
Tier 3zlib 压缩率对比(_validate_with_zlib,:281)子集比全集压得「太好」= 子集更冗余 = 漏了多样性,K 上调 20%

找不到拐点时不是拍一个默认值,而是随多样性连续缩放:keep_fraction = 0.3 + 0.7 × diversity_ratio(:83)。全部互不相同就全留——「每条都带新信息,丢哪条都是丢信息」。

查询的影响通过 bias 乘子进来(:94),per-tool profile 决定:conservative 1.5 多留 50%,moderate 1.0 信统计,aggressive 0.7 压更狠(:11-14)。

覆盖曲线对 CJK 也做了处理:无空格的 CJK 项按词切只会得到一个巨型 token,曲线没有信号,所以改用字符 bigram(compute_unique_bigram_curve,:197-202)。


8. 巧妙之处(可以直接搬走的)

① 把「不变量」写进配置类的 docstring 第一行。 SmartCrusherConfig 的 docstring 开头就是大写的 SCHEMA-PRESERVING(headroom/transforms/smart_crusher.py:163)。约束写在类型旁边,而不是设计文档里,改代码的人躲不开。

② 让静默降级变成异常。 自定义 scorer 不支持就抛 NotImplementedError(:345),不认识的 compaction format 就抛 ValueError(:434),非法保护正则就抛 ValueError(:560)。输出难以核对的系统,静默降级的代价比崩溃高。

③ 用 debug_assert! 把跨语言的不变量钉住。 crusher.rs:881 那句断言保护的不是本函数,而是 Python 侧「备用 crusher 的 CCR store 保持为空」这个远程假设。跨语言边界上的隐含约定,值得一句可执行的注释。

④ 只在真的改了东西时才重新序列化。 audit-safe 补回后判 len(kept) != before_countjson.dumps(:653-660),否则保留 Rust 原字节。两个语言的 JSON 序列化器不必然一致,不改就别碰。

⑤ 参差数据宁可判为「不是这种类型」。 表格行数不齐就返回 None,当非表格处理(tabular_ingest.py:141)。比起「尽力对齐」,承认自己不认识更安全。

⑥ 部分完成好过拖死。 Kompress 撞 deadline 时把剩余的词原样保留再 break(kompress_compressor.py:1541-1552),而不是丢弃整次压缩。半个成果立刻交付,好过完整成果占死一个不可抢占的 worker。

⑦ 阈值让数据自己定。 Otsu 找 KEEP/DROP 分界、Kneedle 找信息饱和拐点,两处都避开了魔数。参数越少,越不用为不同工具单独调参。

⑧ 语义相关的 token 做硬覆盖。 Kompress 的 must-keep 正则骑在模型决策之上(:1667)。ML 打分器不知道「丢了 0x7fff2038 这条栈就废了」——这类知识用规则表达,比指望模型学会更可靠。


9. 边界与局限(诚实版)

逐字节 parity 只覆盖 ASCII。 代码压缩器的 Rust 移植明说:非 ASCII 标识符下 Python 侧按字节切 str 本身就是隐性 bug,两边会分叉,所以 parity fixture 全是 ASCII(crates/headroom-core/src/transforms/code_compressor.rs:22-31)。

search_compressor 的错误关键词集合两侧仍有分歧。 _score_matches 的 docstring 直言:词重叠和 CJK bigram 打分是逐字节相等的,但错误加分的关键词集合有几个词只在 Rust 侧修了,而且没有跨实现断言——这份相等是「测试钉住的,不是机制保证的」(headroom/transforms/search_compressor.py:249-258)。

自定义 relevance scorer 目前不可用。NotImplementedError,等 Stage 3c.2 的 relevance crate Python 桥(smart_crusher.py:345)。

TOIN 的 preserve_fields 在 Rust 侧是空壳。 create_plan 的调用方永远传 None,item_has_preserve_field_match 的完整语义写好了但没人喂数据(planning.rs:21-26 模块注释;函数定义 :560,唯一调用点 :534)。

Perl 配置在但不启用。 PERL 在 _LANG_CONFIGS 里有一张完整的节点表,但 _UNSAFE_TREE_SITTER_LANGUAGES 把它排除在 AST 路径之外,压缩时直接转 Kompress 或原样返回(code_compressor.py:106、:136、:1198-1210)。

audit-safe 只认 JSON 数组。 输入解不出数组就返回空保护列表(原始 CSV / 日志文本明确划在范围外,smart_crusher.py:581-585)。

Stage-3c.2 的 opaque-string CCR 替换不受 enable_ccr_marker 控制。 它总是发记号,因为没有 Python 对应实现、也没有生产调用方要求关掉(smart_crusher.py:326-330)。

Kompress 的 content_typequestion 两个参数是摆设。 签名里有,实现里忽略——docstring 直接标了 Ignored(:1466-1468)。


10. 代码地图(导航索引)

主题文件路径关键符号
JSON 数组压缩壳headroom/transforms/smart_crusher.pySmartCrusherSmartCrusherConfigcrushcrush_array_json
CCR 哨兵与过滤headroom/transforms/smart_crusher.pyCCR_SENTINEL_KEYis_ccr_sentinelstrip_ccr_sentinels
审计保护行headroom/transforms/smart_crusher.py_scan_protected_rows_splice_missing_protected_apply_audit_safe_protection
Rust→Python CCR 镜像headroom/transforms/smart_crusher.py_mirror_ccr_markers_in_text_mirror_single_hash_to_python_store
无损优先 / 有损分叉crates/headroom-core/src/transforms/smart_crusher/crusher.rscrush_array_with_sourcelossless_min_savings_ratio
四种选行策略crates/headroom-core/src/transforms/smart_crusher/planning.rscreate_planplan_smart_sampleplan_top_nplan_cluster_sampleplan_time_seriesapply_query_signals
无损渲染器crates/headroom-core/src/transforms/smart_crusher/compaction/CompactionStageSUPPORTED_FORMAT_NAMESCsvSchemaFormatterwrite_table
PyO3 桥 / GIL 释放crates/headroom-py/src/lib.rsPySmartCrusherPySmartCrusherConfig
serde_json feature 依据Cargo.tomlpreserve_orderarbitrary_precisionraw_value
源码 AST 压缩headroom/transforms/code_compressor.pyCodeAwareCompressorcompress_compress_with_ast
多语言配置表headroom/transforms/code_compressor.pyLangConfig_LANG_CONFIGSDocstringMode
符号重要度与预算headroom/transforms/code_compressor.py_analyze_symbol_importance_allocate_body_budget_get_body_limit
函数体 / 类体压缩headroom/transforms/code_compressor.py_compress_function_ast_compress_class_ast_make_omitted_comment
语法自检与兜底headroom/transforms/code_compressor.py_verify_syntax_has_syntax_issues_fallback_compress
源码压缩 Rust 移植crates/headroom-core/src/transforms/code_compressor.rs模块头的 grammar-parity 说明
搜索结果压缩headroom/transforms/search_compressor.pySearchCompressor_score_matches_select_matches_cjk_bigrams
日志压缩headroom/transforms/log_compressor.pyLogCompressor_score_line_select_lines_dedupe_similar_add_context
diff 压缩headroom/transforms/diff_compressor.pyDiffCompressor_persist_to_python_ccrcompress_with_stats
配置文件压缩headroom/transforms/config_compressor.pyConfigCompressor_schema_fold_elision_safe_strip_comment_lines
HTML 正文抽取headroom/transforms/html_extractor.pyHTMLExtractorextractis_html_content
表格桥接headroom/transforms/tabular_ingest.pyparse_tabularto_recordsTabularCompressor
快速散文压缩headroom/transforms/text_crusher.py + crates/headroom-core/src/transforms/text_crusher/crusher.rsTextCrusherTextCrusherConfig
ML token 压缩headroom/transforms/kompress_compressor.pyKompressCompressorcompress_KOMPRESS_MUST_KEEP_RE_add_kompress_must_keep_words
ONNX 运行时治理headroom/onnx_runtime.pycpu_arena_enabledonnx_thread_spinning_enabledcreate_cpu_session_optionstrim_process_heaphf_hub_download_local_first_PINNED_REVISIONS
远程 Kompressheadroom/transforms/kompress_remote.pyRemoteKompressCompressorparse_endpoint_headers
相关性协议headroom/relevance/base.pyRelevanceScorerRelevanceScore
关键词打分headroom/relevance/bm25.pyBM25Scorer_TOKEN_PATTERN_compute_idf
语义打分headroom/relevance/embedding.pyEmbeddingScorer_DEFAULT_MODEL_PINNED_REVISION
融合打分headroom/relevance/hybrid.pyHybridScorer_compute_alpha
散文 KEEP/DROP 划分headroom/transforms/relevance_split.pysegmentbuild_relevance_queryadaptive_thresholdplan_relevance_split
位置锚点headroom/transforms/anchor_selector.pyAnchorSelectorselect_anchorsadjust_weights_for_querycalculate_information_score
自适应保留数headroom/transforms/adaptive_sizer.pycompute_optimal_kfind_kneecount_unique_simhash_validate_with_zlib

接着读: 有损裁剪留下的那个 <<ccr:...>> 记号怎么变成模型可以真的取回原文的能力,见 03-ccr;压缩后的内容放进消息历史时为什么不能碰前缀缓存,见 04-cache-safety