RTK discover 模块深入解析:命令重写管线与 LLM 会话历史 Token 分析实现
2026/9/7 17:29:57 网站建设 项目流程

RTK discover 模块深入解析:命令重写管线与 LLM 会话历史 Token 分析实现

【免费下载链接】rtkCLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies项目地址: https://gitcode.com/GitHub_Trending/rtk4/rtk

RTK(Reduce Token Keeper)是一个通过代理常见开发命令来降低 LLM Token 消耗的 CLI 工具,其核心枢纽正是src/discover/模块。本文基于模块文档 src/discover/README.md,结合 src/discover/registry.rs、src/discover/lexer.rs、src/discover/rules.rs、src/discover/provider.rs 的源码实现,系统讲解命令重写(rewrite)热路径的完整管线与rtk discover会话历史分析的聚合逻辑。读完本文,你将能够理解一条 shell 命令如何被逐段改写为rtk前缀命令、哪些守卫条件会阻止改写、规则表如何扩展,以及 Token 节省估算的算法依据。

完整的重写管线图参见 docs/contributing/TECHNICAL.md 的 "3.2 Hook Interception (Command Rewriting)" 一节;用户侧的使用说明见 docs/guide/analytics/discover.md。

discover 模块的两个核心职责

src/discover/模块承担两个看似独立、实则共享同一套分类逻辑的职责:

  1. 命令重写(热路径):每个 LLM Agent 的 hook 都会调用rtk rewrite "git status"。该模块决定是重写为rtk git status,还是原样放行。这是热路径——LLM 执行的每条命令都会经过这里。
  2. 历史分析(冷路径)rtk discover扫描过往的 LLM 会话,找出那些"本可以被重写却未被重写"的命令,估算其 Token 节省量。同一套分类逻辑,不同的消费者。

从源码结构看,这一设计在 src/discover/mod.rs 中体现得非常清晰:run()函数通过provider.extract_commands()提取历史命令后,对每个片段调用与热路径完全相同的registry::classify_command()registry::split_command_chain()。文档中的表述是准确的:"classification logic is shared between discover and rewrite — same patterns, same rules, different consumers"。

模块内部的文件分工:

文件职责
lexer.rs单遍 shell 分词状态机,产出类型化 Token
registry.rs分类器 + 重写引擎(classify_commandrewrite_command
rules.rs规则表RULES(正则模式、映射命令、节省率元数据)与忽略清单
provider.rsSessionProvidertrait 与 Claude Code JSONL 会话读取实现
report.rsdiscover 报告的文本/JSON 格式化

命令重写管线:从原始字符串到 rtk 命令

以文档给出的例子说明:当 hook 送来cargo fmt --all && cargo test 2>&1 | tail -20时,管线依次经过四个阶段,最终产出rtk cargo fmt --all && rtk cargo test 2>&1 | tail -20。Bash 在执行时才处理&&|——每个rtk调用都是独立进程。

调用链(源码级):

hook shell → src/hooks/rewrite_cmd.rs → registry::rewrite_command() → rewrite_compound() → rewrite_segment() / rewrite_segment_inner() → classify_command() → rules.rs::RULES

rewrite_command()的入口位于 src/discover/registry.rs,它先做三项预处理:折叠 bash 续行符(\<NL>归一为单个空格)、拒绝 heredoc(<<)与算术展开($(()、按引号感知的换行 Token 拆分行,多行块则走rewrite_multiline_block逐行独立重写。

阶段一:分词(Tokenization)

lexer.rs 中的tokenize()(入口在 L25)将原始字符串转换为类型化 Token。它是一个单遍状态机,理解 shell 引号、转义、重定向和操作符,产出的 Token 类型为:

pub enum TokenKind { Arg, // 普通参数(含引号内容) Operator, // &&、||、; Pipe(PipeKind), // |(Stdout)或 |&(StdoutAndStderr) Redirect, // 2>&1、>/dev/null、>> 等 Shellism, // $、*、?、(、) 等 shell 特殊构造 }

这一步是重写正确性的根基,因为朴素的字符串切分会在引号内容上出错,例如git commit -m "fix && update"中的&&绝不能被当作命令分隔符。文档给出的示例:

"cargo test 2>&1 && git status" → [Arg("cargo"), Arg("test"), Redirect("2>&1"), Operator("&&"), Arg("git"), Arg("status")]

从源码看,lexer 通过quote: Option<char>跟踪当前引号状态(单引号内反斜杠不转义,双引号内生效),遇到未加引号的&&|||;时冲刷当前参数并切换 Token 类型;引号内部的这些字符则保留在Arg中。此外,lexer 还提供shell_split()用于按空格安全切分参数(供search_uses_pattern_file()等守卫检查复用)。

阶段二:复合命令拆分与管道安全策略

重写引擎(rewrite_compound(),registry.rs L1035)遍历 Token 序列,在Operator&&||)与类型化的PipeToken(||&)处切分。对管道有一个关键的安全策略:

  • 普通管道:生产者与中间阶段保持原样(raw),只有被标记为pipeline_final_safe的、且"参数安全"的最终阶段才被重写;
  • 初始安全集:仅普通的greprg调用(见 rules.rs 中 grep/rg 规则,二者显式设置pipeline_final_safe: true);
  • 模式文件形式延迟grep -f pattern.txt/rg --file ...会被search_uses_pattern_file()(registry.rs L1197)检测出来——因为-f/--file可能把管道 stdin 当作配置消费,这类最终阶段不重写;
  • 错误流管道不重写:含|&PipeKind::StdoutAndStderr)的管道,analyze_pipeline()会将其标记为结构不支持,整条保持原样;
  • 不透明 shell 分组:管道与(){}分组 Token 同时出现时,rewrite_compound()直接返回None,整段放行。

只有当至少一个片段被实际改写时,rewrite_command()才返回Some(result);否则返回None,hook 侧将原命令交给 Agent 原生处理。这就是文档所说的 fallback 契约:"If any segment fails to match, it stays raw"。

阶段三:逐段重写(Per-segment rewriting)

每个片段进入rewrite_segment_inner()(registry.rs L1287),按文档列出的顺序执行四步:

1. 剥离尾部重定向。strip_trailing_redirects()(L495)通过 lexer Token(而非字符串匹配)从尾部回扫Redirect类型 Token,把2>&1>/dev/null等设置一旁,重写后再原样追加回去。例如git status 2>&1先按git status匹配,产出rtk git status 2>&1

2. 短路特殊情形。head/tail的行数形式不能走通用的前缀替换——否则head -20 file会被机械替换成rtk read -20 file(标志位置错误)。因此rewrite_line_range()(L1145)用专用正则处理,支持head -Nhead --lines=Ntail -Ntail -n Ntail --lines=Ntail --lines N六种形式:

head -20 file → rtk read file --max-lines 20 tail -n 5 file → rtk read file --tail-lines 5

注意正则要求单个文件参数(\S+$):多文件调用如head -3 a b c故意不匹配,交还原生二进制处理——原生head的多文件==> name <==分隔横幅是rtk read --max-lines无法复现的(源码注释引自 issue #1362)。

3. 分类命令。classify_command()(L107-L214)按固定顺序做归一化:

  • 剥离环境前缀(sudoenvVAR=val,正则见 ENV_PREFIX L63-L70);
  • 归一化绝对路径:/usr/bin/grepgrepstrip_absolute_path,issue #485);
  • 剥离 git 全局选项:git -C /tmp statusgit statusGIT_GLOBAL_OPT正则,L73-L75,支持-C-c--git-dir--work-tree等);
  • 归一化 PHP Composer 工具路径(vendor/bin/phpunit、自定义 bin-dir →phpunit);
  • 剥离 golangci-lint 的run前置全局选项;
  • 检查忽略清单:IGNORED_PREFIXEScdechormsedawkrtk等)与IGNORED_EXACT(见 rules.rs L989-L1041);
  • 最后用RegexSet对 60+ 条正则模式做快速匹配,取**最后一个(最特异的)**命中规则,再从捕获组中提取子命令以查询subcmd_savings/subcmd_status覆盖值。

4. 应用重写。找到匹配规则后,用strip_word_prefix()带词边界检查地替换rewrite_prefixes中的命令前缀为rtk <cmd>,再重新前置环境前缀、重新追加重定向后缀。由于rtk_cmd值在所有规则中唯一,rewrite_segment_inner可以直接按rtk_equivalent反查规则表。

此外,重写引擎还支持"透明包装前缀":内置的uv run(可路由回退)与 shell 关键字noglob/command/builtin/exec/nocorrect(不可回退),以及用户在config.toml中配置的[hooks].transparent_prefixes(如docker exec mycontainer)。剥离包装后对内部命令递归重写(深度上限MAX_PREFIX_DEPTH = 10),成功后再前置包装,例如docker exec mycontainer git statusdocker exec mycontainer rtk git status

重写守卫(Guards)

文档列出的五条守卫规则在源码中均可逐条对应:

守卫源码位置行为
环境前缀含RTK_DISABLED=1registry.rs L1303-L1313跳过重写,并在 stderr 打印提示(issue #345、#508)
gh--json/--jq/--templateregistry.rs L1436-L1444结构化输出会被 rtk 破坏,跳过(issue #196)
cat-n以外的标志registry.rs L1370-L1378-A-v-e等语义与rtk read不同,跳过
cat/head/tail>/>>重定向classify_command L144-L162这是写操作而非读操作,归为 Unsupported(issue #315)
命令命中hooks.exclude_commands配置compile_exclude_patterns()(L1221-L1250)模式编译为正则(自动加^...($|\s)锚定)或前缀匹配,命中即跳过

RTK_DISABLED的剥离函数strip_disabled_prefix()(L484)返回(env_prefix, actual_command)二元组,供热路径与 discover 聚合两条线复用。

环境前缀处理(ENV_PREFIX 正则)

ENV_PREFIX(registry.rs L63-L70)是一个组合正则:^(?:sudo\s+|env\s+|VAR=值\s+)*,其中"值"部分匹配三种形式——双引号("(?:[^"\\]|\\.)*",支持转义引号)、单引号('(?:[^'\\]|\\.)*')、无引号([^\s]*),且变量名限定为[A-Z_][A-Z0-9_]*。它覆盖文档列出的全部五种形态:

FOO=bar # 无引号 FOO="bar baz" # 双引号含空格 FOO='bar baz' # 单引号 FOO="he said \"hello\"" # 转义引号 A="x y" B=1 sudo git status # 链接式(含 sudo)

关键细节是前缀被剥离两次、目的不同(文档 "Env Prefix Handling" 一节):

  • classify_command()中剥离一次,仅为了让底层命令能命中规则(例如FOO=1 git status也要能分类为rtk git);
  • rewrite_segment_inner()中通过strip_disabled_prefix()再剥离一次,把前缀提取出来用于重前置——最终输出保留前缀:FOO=1 rtk git status

源码注释还点明了两个相关设计:RTK_DISABLED=检测发生在剥离后、分类前(热路径)与 discover 聚合中(mod.rs L100-L115),使RTK_DISABLED=1 cargo test会被单独统计为"被禁用的潜在节省",而不是简单丢弃。

规则系统:如何新增一条重写规则

新增规则只需在 src/discover/rules.rs 的RULES常量数组(L38 起)中添加一个RtkRule条目,无需改动其他文件。结构定义在 rules.rs L3-L13:

pub struct RtkRule { pub pattern: &'static str, // 匹配命令的正则(供 RegexSet 快速匹配) pub rtk_cmd: &'static str, // 映射到的 RTK 命令,如 "rtk cargo" pub pipeline_final_safe: bool, // 是否可作为管道最终阶段被重写 pub rewrite_prefixes: &'static [&'static str], // 待替换的命令前缀,如 &["cargo"] pub category: &'static str, // discover 报告的分类元数据 pub savings_pct: f64, // 默认节省率(%) pub subcmd_savings: &'static [(&'static str, f64)], // 按子命令覆盖节省率 pub subcmd_status: &'static [(&'static str, RtkStatus)], // 按子命令覆盖状态 }

仓库中现成的示例规则:

RtkRule { pattern: r"^(?:git|yadm)\s+(?:-[Cc]\s+\S+\s+)*(status|log|diff|show|add|commit|checkout|push|pull|branch|fetch|stash|worktree)", rtk_cmd: "rtk git", rewrite_prefixes: &["git", "yadm"], category: "Git", savings_pct: 70.0, subcmd_savings: &[("diff", 80.0), ("show", 80.0), ("add", 59.0), ("commit", 59.0)], ..RtkRule::DEFAULT }, RtkRule { pattern: r"^grep\s+", rtk_cmd: "rtk grep", pipeline_final_safe: true, // 允许出现在管道末位 rewrite_prefixes: &["grep"], category: "Files", savings_pct: 75.0, ..RtkRule::DEFAULT },

注意pattern中第一个捕获组(如(status|log|diff|...))是分类器的约定:classify_command()COMPILED[idx].captures()caps.get(1)作为子命令名,去查subcmd_savingssubcmd_status(registry.rs L170-L195)。因此需要按子命令细化元数据的规则务必把子命令放进第一捕获组。subcmd_status则标记特殊状态,例如 cargo 规则中("fmt", RtkStatus::Passthrough)表示cargo fmt走直通过滤而非压缩。

匹配性能方面,注册表通过LazyLock在首次使用时一次性编译所有模式:REGEX_SET: LazyLock<RegexSet>(一次扫描找出所有命中)与COMPILED: LazyLock<Vec<Regex>>(按序取捕获组),以及独立的ENV_PREFIXGIT_GLOBAL_OPTHEAD_N/TAIL_N等专用正则(registry.rs L54-L89)。

历史分析:rtk discover的实现

数据源:Claude Code JSONL 会话

rtk discover读取 Claude Code 的 JSONL 会话文件,每个文件包含 LLM 执行过的一切命令的tool_use/tool_result对。读取逻辑封装在SessionProvidertrait 与ClaudeProvider(provider.rs L33-L42)中,目前唯一实现是 Claude Code(trait 文档注明 Cursor Agent 的转录为纯文本格式,无法做结构化命令提取,需用rtk gain跟踪)。

extract_commands()(provider.rs L156-L271)逐行解析 JSONL:对type=assistant行提取message.contenttool_usename=Bash块的input.command,对type=user行提取tool_result的内容长度、前 1000 字符预览与is_error标志,再按tool_use_id配对。每行先做contains("\"Bash\"")廉价预过滤以跳过无关行,解析失败的行静默跳过。

一个容易踩坑的细节是项目目录名编码:Claude Code 将路径中的/._\:、空格、[]及非 ASCII 字符统一替换为-来生成~/.claude/projects/下的目录名。ClaudeProvider::encode_project_path()(provider.rs L128-L143)必须复现同一套编码,否则"零会话命中"(Windows 盘符冒号的历史 bug 即 issue #2919)。该函数有大量单元测试覆盖点号用户名、下划线、非 ASCII、Windows 路径等边界(provider.rs 测试段 L274 起)。

聚合与 Token 估算

discover::run()(mod.rs L43-L275)的流程与文档描述一一对应:

  1. ClaudeProvider定位会话文件(项目过滤为目录名子串匹配,since_days按文件 mtime 截断);
  2. 对每条提取的命令调用split_command_chain()拆分复合命令——内部先做引号感知的 heredoc 检测,再走 lexer 的split_on_operators(),与热路径同一套分词;
  3. 每个片段经classify_command()分类,落入三种桶:
    • Supported:按rtk_equivalent聚合到SupportedBucket,累计次数与 Token;
    • Unsupported:按基础命令(如docker build)聚合,保留示例命令;
    • Ignored:若片段以rtk开头则计入already_rtk(已采用率统计)。
  4. Token 估算采用双轨制(mod.rs L137-L152):
    • 真实值优先:有tool_result输出时,output_len / 4(输出字节数除以 4);
    • 类别均值兜底:无输出长度时用category_avg_tokens()的静态表(如 Git log/diff/show 200、Cargo test 500、Tests 800,见 registry.rs L32-L52)。
  5. 节省率是加权平均而非首见子命令的率:桶内累计total_output_tokens(节省量)与total_raw_output_tokens(原始量),报告时计算savings / raw,避免"按第一个子命令的节省率一刀切"的偏差(mod.rs L209-L216)。
  6. 报告条目按估算节省量降序排列;另单列RTK_DISABLED命中的 Top 5 高频命令(仅在底层命令确属受支持时计数,mod.rs L100-L115)。

需要明确口径的是(引自 docs/guide/analytics/discover.md):~N tokens估算的 bash 输出字节数除以 4,并非服务商实际计费的 Token 数——RTK 不携带真实 tokenizer,且 bash 输出只是输入 Token 的一个来源。应将其理解为"RTK 可压缩的输出体积"的量级参考。

使用方式与输出

rtk discover的 CLI 参数(src/main.rs L605-L619):

rtk discover # 分析当前项目历史 rtk discover --all # 所有项目 rtk discover --all --since 7 # 最近 7 天,所有项目
参数说明默认值
-p, --project <path>项目路径过滤(目录名子串匹配)当前工作目录(编码为 Claude slug)
-a, --all扫描所有项目
-s, --since <days>仅扫描最近 N 天的会话(按文件 mtime)30
-l, --limit <n>每节最多显示命令条数15
-f, --format <fmt>输出格式:text/jsontext

示例输出(样本数字,非典型结果):

Missed savings analysis (last 7 days) ──────────────────────────────────── Command Count Est. lost cargo test 12 ~48,000 tokens git log 8 ~12,000 tokens pnpm list 3 ~6,000 tokens ──────────────────────────────────── Total missed: 23 ~66,000 tokens Run `rtk init --global` to capture these automatically.

如果安装 RTK 后命令仍出现在 missed 列表中,通常意味着对应 Agent 的 hook 未激活。配套的rtk session子命令则展示各会话中 RTK 命令占比(coverage),低覆盖率通常指向RTK_DISABLED=1或某 subagent 的 hook 缺失(见 docs/guide/analytics/discover.md)。

小结:一条命令的两条命运

src/discover/的架构价值在于单套分类逻辑服务两个消费者:热路径的rewrite_command()追求"安全地改"——每一层守卫(引号感知分词、管道安全集、重写守卫、fallback 契约)都宁可放行也不愿破坏语义;冷路径的rtk discover追求"诚实地算"——复用同一分类器与同一拆分器,用真实输出长度优先、类别均值兜底、加权节省率的估算口径,量化 hook 未覆盖带来的 Token 损失。对维护者而言,扩展能力被收敛到单一入口:在 rules.rs 追加一条RtkRule,热路径重写与历史报告便同时生效,这正是文档 "Adding a New Rewrite Rule" 一节所承诺的"No other files need to change"。

【免费下载链接】rtkCLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies项目地址: https://gitcode.com/GitHub_Trending/rtk4/rtk

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询