Headroom Rust 核心 signals 模块详解:行级重要性检测 Trait 与 Tiered 组合式检测架构
本文基于 Headroom 仓库 crates/headroom-core 中的 signals 模块文档展开,讲清楚"压缩前如何判断一行文本值不值得保留"这一核心设计:按粒度划分的检测 Trait 家族、Tiered 组合器如何以置信度驱动短路/降级、KeywordDetector 的 Aho-Corasick 自动机实现细节,以及如何按项目约定把未来的 ML 检测器(如 BGE 分类头)无侵入地接入现有检测栈。读完后可掌握一套"关键词 → 结构化解析 → 小模型"的渐进式检测器扩展方法论。
为什么 signals 是 crate 顶层模块,而不是 transforms 的子模块
Rust 侧的压缩逻辑位于 transforms 目录,而检测分类逻辑独立放在 signals 目录。模块文档注释解释了分层理由:
- transforms 负责"变",signals 负责"判":transforms 里的压缩器负责变更数据,signals 里的检测器负责对数据做分类(打分、归类)。
- 同一个分类器喂给多个消费方:行级重要性打分同时被
text_compressor、search_compressor、diff_compressor、log_compressor使用。如果把它嵌套在某个 transform 之下,会暗示"归某一个消费方所有",造成错误的归属。 - 无静默回退(No silent fallbacks)是项目级约定:模块注释明确写了"没有
NoOpDetector、没有返回全零的桩 ML 实现、没有悄悄降级的 fallback 分类器"。某一级检测器要么真正干活,要么把"我没有把握"这件事通过confidence字段如实表达,而不是被强行扭转为阳性答案。
模块注释还给出了检测能力的成熟路径,这决定了 traits 的演进方向:
- 模式回退(Pattern fallback)——关键词/正则扫描,便宜但脆弱,是每个检测器的起点,即当前的
KeywordDetector; - 结构化解析(Structured parser)——输入有语法(diff、JSON、代码)时直接解析,
unidiff与tree-sitter已在项目其他部分落地; - ML 模型——对模糊类别(行重要性、锚点单元格、HTML 抽取),在小规模标注流量上训练的小分类器优于关键词,规范扩展路径是在已有的
bge-small-en-v1.5embedder 上加分类头。
三类实现都挂在同一个"按粒度"的 trait 之下,分层通过 Tiered 组合完成,从不使用继承。
Trait 家族:按粒度拆分,而不是按领域拆分
signals/README.md 中的 trait 家族表如下:
| Trait | 粒度 | 状态 |
|---|---|---|
LineImportanceDetector |
逐行 | 已交付(Phase 3e.1) |
ContentTypeDetector |
整个文本块 | 未来对 transforms::detection 的泛化 |
ItemImportanceDetector<I> |
&[I] 排序 |
未来,面向 SmartCrusher 单元格 / 搜索命中 |
设计要点是"按粒度、不按领域":把所有能力塞进一个 Detector<Any> 会迫使每个调用点都去 match 输入形态;拆成三个 trait 后,每个调用点都能被类型系统约束住。模块公开了 keyword_detector、line_importance、tiered 三个子模块,并在 mod.rs 中重导出 KeywordDetector、KeywordRegistry、ImportanceCategory、ImportanceContext、ImportanceSignal、LineImportanceDetector 与 Tiered。
ImportanceContext:一行文本"从哪里来"决定哪套模式生效
ImportanceContext 有四个变体,它决定了哪一组模式会触发:
Text——自由文本(text_compressor),markdown 结构有意义;Search——grep/ripgrep 输出(search_compressor),error/warn 关键词占主导;Diff——git diff(diff_compressor),error + security + importance 关键词生效;Log——日志输出(log_compressor),error/warn 关键词 + 级别前缀生效。
例如 markdown 标题在 Text 语境里算优先级信号,在 diff hunk 里就不算。
ImportanceSignal:绝不返回裸 bool
单个检测器对单行文本的输出是 ImportanceSignal,包含三个字段:
| 字段 | 含义 |
|---|---|
category: Option<ImportanceCategory> |
命中的类别(Error / Warning / Importance / Security / Markdown),未命中为 None |
priority: f32 |
压缩器排序依据:0.0 = 最先丢弃,1.0 = 无论如何保留 |
confidence: f32 |
Tiered 组合器用来决定是否继续问下一层:0.0 = 无信息,1.0 = 检测器确信 |
它提供两个构造器:ImportanceSignal::neutral()(category 为 None、priority 与 confidence 均为 0.0,表示"我对这行没有意见")和 ImportanceSignal::matched(category, priority, confidence)(命中检测)。is_match() 仅以 category.is_some() 判定是否命中。
trait 定义本身很薄:
pub trait LineImportanceDetector: Send + Sync {
fn score(&self, line: &str, ctx: ImportanceContext) -> ImportanceSignal;
}
注释明确要求实现必须 Send + Sync,因为压缩器会在 tokio 工作线程之间共享检测器实例;同时实现应当"便宜"(关键词自动机、词法特征)或"可摊销"(embedding + 分类头,配合批量推理)。
Tiered 组合器:以置信度为驱动的短路栈
Tiered<dyn Trait> 把一个有序检测器栈串起来,规则只有一条:ESCALATE_THRESHOLD 为 0.7。核心打分逻辑在 score 实现:
impl LineImportanceDetector for Tiered<dyn LineImportanceDetector> {
fn score(&self, line: &str, ctx: ImportanceContext) -> ImportanceSignal {
let mut best = ImportanceSignal::neutral();
for tier in &self.tiers {
let signal = tier.score(line, ctx);
if signal.confidence >= ESCALATE_THRESHOLD {
return signal;
}
if signal.confidence > best.confidence {
best = signal;
}
}
best
}
}
语义可以拆成三条:
- 短路:第一个
confidence >= 0.7的 tier 立即胜出,后续 tier 不再被咨询; - 落空:低置信度 tier 会被跳过,继续问下一层;
- 兜底:如果没有任何 tier 越过阈值,返回"见过的最高置信度信号",让调用方至少拿到一个最佳猜测,且 confidence 分数如实反映了整栈的不确定度。
栈是组合而非继承:KeywordDetector 不知道未来 ML 检测器的存在,ML 检测器也不知道关键词检测器的存在——双方各自实现 trait,Tiered 只负责排序。with 构建器要求"最精确的层放最前",with_detector 便捷方法则帮调用点省掉 as Box<dyn …> 的样板。
tiered.rs 的单元测试用两个合成检测器把上述语义逐条钉死:AlwaysFiresHigh(confidence 0.95)放在关键词层之前时,"ERROR: connection refused" 被判为 Security 而不是 Error,证明高置信层先短路;AlwaysFiresLow(confidence 0.5)则必须落到关键词层,结果变为 Error;没有任何层命中阈值时,返回 best-seen(0.5 的 Importance);空栈返回 neutral。
KeywordDetector:aho-corasick 自动机 + 词边界后过滤
当前唯一注册的检测层是 KeywordDetector,它是 Tier-3 模式检测器,用 aho-corasick 一次确定有限自动机扫描就能在一行里找出所有关键词,复杂度 O(n + m),替代了 Python 侧 error_detection.py 的独立正则逐个搜索——关键词集只有一份真源,不会出现"表"与"编译后的模式"漂移。
置信度与优先级常量
| 常量 | 值 | 作用 |
|---|---|---|
KEYWORD_CONFIDENCE |
0.7 | 恰好达到 ESCALATE_THRESHOLD:无歧义的关键词命中不会被下一层"再议",同时给未来 ML 层留出在边缘样本上覆盖的空间(见 L41) |
ERROR_PRIORITY |
0.95 | Error 类命中行的保留优先级 |
SECURITY_PRIORITY |
0.85 | Security 类 |
WARNING_PRIORITY |
0.75 | Warning 类 |
IMPORTANCE_PRIORITY |
0.6 | Importance 类(important/todo/fixme/bug…) |
MARKDOWN_PRIORITY |
0.45 | Markdown 结构类(仅 Text 语境) |
关键词注册表 KeywordRegistry
KeywordRegistry::default_set() 定义了默认词表(这也是"配置"与"检测逻辑"的分界——静态词表属于检测器的配置,与消费它的检测器放在一起):
- error:
error, exception, fail, failed, failure, fatal, critical, crash, panic, abort, timeout, denied, rejected - warning:
warn, warning - importance:
important, note, todo, fixme, hack, xxx, bug, fix - security:
security, auth, password, secret - markdown_prefixes:
"# ", "## ", "### ", "#### ", "**", "> "(按前缀匹配,不是整行关键词) - error_indicators:
error, fail, exception, traceback, fatal, panic, crash(供contains_error_indicator快速分诊用的子串集,独立于行打分集)
注册表还通过 as_map() 提供确定性快照(BTreeMap 保证迭代顺序稳定),供 PyO3 侧 Python shim 反射出旧的正则表,保持双语言行为一致。
语境相关的匹配规则
match_in_context 的查找顺序是:先查跨语境通用的 error/importance 自动机,再按语境分流——
Diff语境:security 自动机生效;Text/Search/Log语境:warning 自动机生效(注意 Diff 不含 warning,这与 Python 的PRIORITY_PATTERNS_DIFF形状保持一致);- 仅
Text语境:markdown 结构前缀生效。
自动机以 ascii_case_insensitive(true) 与 MatchKind::LeftmostLongest 构建,命中的字节偏移再经过 is_word_boundary 做整词校验——因此 "panicker" 不会误触 panic。另有一个刻意的例外:contains_error_indicator 走无词边界的子串匹配,因为它服务的是"是否包含 error 形态内容"的快速分诊调用点(如消息签名分类),保留 Python 时代的宽松语义;它使用的词集也不同于 score()(含 traceback、不含 timeout 等四个后补词)。
相对 Python 版本修复的两个 bug(fixed_in_3e1)
文件头注释记录了 2026-04-29 修复、并由 parity 夹具锁定的两处行为差异:
- Python 的
ERROR_KEYWORDS列了{abort, timeout, denied, rejected},但ERROR_PATTERN正则遗漏了这四个词——"Connection timeout" 因此从未被标记为 error。Rust 版把四词纳入自动机消费的 error 集; - Python 的
SECURITY_KEYWORDS包含token,会在input_tokens、tokens_saved等 LLM 度量行上产生大量误报。Rust 版将其从 security 集剔除。
对应的回归测试见 keyword_detector.rs 测试模块:timeout_now_classified_as_error_in_diff、rejected_now_classified_as_error、token_no_longer_flags_security_in_llm_proxy_context、auth_still_flags_security_in_diff、warning_fires_in_search_but_not_diff、word_boundary_excludes_substring_matches 等,分歧行都带 // fixed_in_3e1 标记;Python 侧的 parity 夹具为 tests/test_signals_keyword_parity.py。
检测器如何被 transforms 消费
信号最终以两种方式进入压缩决策:
- 按类别加权:search_compressor.rs 在
boost_errors开启时调用importance.score(..., ImportanceContext::Search),把Error映射为 +0.5、Warning+0.4、Importance+0.3(对应 Python 的PRIORITY_PATTERNS_SEARCH顺序),而Security与Markdown不加权(bump 为 0.0)——这体现了"类别到业务语义的映射发生在调用点,而不是检测器内部"。该压缩器还提供with_detector构造器,允许换入自定义LineImportanceDetector实现; - 按行计数:日志 offload 路径(log_offload.rs)持有
Box<dyn LineImportanceDetector>(默认KeywordDetector),对日志行逐行打分统计,再决定哪些行可被卸载。
两处调用点都是"在消费方把检测器接进 Tiered",检测器模块本身不感知其他层——这正是文档第 4 条接入约定的实例。
如何新增一个检测器(项目约定)
文档给出了五步清单,值得作为扩展规范逐条执行:
- 确认粒度。逐行分类器实现
LineImportanceDetector;整块分类器走新 trait。不要把跨粒度的工作硬塞进一个 trait; - 实现
score(&self, ...) -> ImportanceSignal。如实设置confidence:如果你是该输入的权威检测器,给 0.7+;如果希望分歧时让下一层覆盖,就给更低的值; - 禁止静默回退。没有信息时返回
ImportanceSignal::neutral()——绝不用低置信度"编造一个阳性答案"来 fail open; - 在消费方接入
Tiered,而不是在 signals 模块内。检测器自身不知道其他层的存在; - 补齐 parity 夹具。如果新检测器替换或增强了既有实现,为分歧行打上
// fixed_in_<phase>标记,保留审计线索。
规范的 ML 扩展路径:BGE 分类头
README 认为最可能成为下一层的,是在 relevance::EmbeddingScorer 已加载的 bge-small-en-v1.5 embedder 上加一个分类头。目标形态:
pub struct BgeClassifierDetector {
embedder: Arc<dyn Embedder>, // 与相关性打分共享
classifier: LogisticRegression, // 384 维 → 4 类 softmax
threshold: f32, // 在验证集上校准
}
impl LineImportanceDetector for BgeClassifierDetector { ... }
这被定位为最省路径,理由有三(见 README):
- 零新增资产:embedder 已经为 SmartCrusher 的相关性打分加载(relevance/embedding.rs 中默认模型即
BAAI/bge-small-en-v1.5,384 维,经 fastembed/ONNX Runtime 运行)。分类头仅约 1.5 KB 权重、每行约 1 ms 推理(可批处理),不需要新的 ONNX runtime、模型文件或下载; - 校准置信度天然契合 Tiered:高置信正样本可以短路
KeywordDetector,边缘样本则让位——而边缘样本恰恰是关键词自动机可靠的区域; - trait 形状零改动:
LineImportanceDetector对三条路径都直接兼容。
同时保留两个备选,以防 BGE 头欠拟合:
- 蒸馏 tinyBERT(ONNX)——精度更高,+10–20 MB 模型、+3 ms 延迟,引入新的
ort依赖; - 词法特征上的逻辑回归——输入是比率、行长度、结构标记、栈帧启发式,约 5 KB 模型、三者中最快,适合做 A/B 基线。
这三条路对上层完全透明:它们都只是 Tiered 栈里的新成员。
什么不属于 signals 模块
边界约定同样写在文档里,扩展时不要越界:
- 具体 transforms——归 crates/headroom-core/src/transforms/;
- 静态关键词数据表——是
KeywordDetector的配置而非检测逻辑,随消费方放在signals/keyword_detector.rs的KeywordRegistry; - 标签保护(
<headroom:keep>标记)——属于用户意图,不属于分类。
小结
signals 模块用最小的类型面(一个 trait + 一个信号结构体 + 一个组合器)承载了 Headroom Rust 核心的"保留哪些行"决策:ImportanceSignal 的三元组让类别、保留优先级与不确定度彼此解耦,Tiered 的 0.7 阈值把"谁说了算"变成一条可测试、可替换的栈规则,KeywordDetector 则示范了如何在修复 Python 版遗留 bug 的同时用 parity 夹具固化行为。对想扩展检测能力(尤其是小模型层)的开发者,五步接入约定和 BGE 分类头方案给出了从当前 0.7 置信度关键词层平滑演进到 ML 层、且不动任何调用点的完整路线。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00