首页
/ rtk 技术解析:LLM CLI 代理的端到端架构、命令重写流水线与过滤引擎实现

rtk 技术解析:LLM CLI 代理的端到端架构、命令重写流水线与过滤引擎实现

2026-09-06 19:24:59作者:裘晴惠Vivianne

本篇以 rtk 仓库中的官方技术文档为主线,完整拆解这个 Rust Token Killer 如何以「钩子拦截 + 命令重写 + 双轨过滤器 + 本地度量」的闭环,把 Claude Code、Copilot 等 LLM 编码代理的 bash 输出削减 60–90%。读完你将掌握:从 rtk init 安装钩子到一条 cargo test 被改写、执行、过滤、落库的完整调用链,以及 TOML DSL 过滤引擎的 8 级流水线与 ≥20% 削减率的测试验证方法。

1. 项目愿景:为什么要在 CLI 和 LLM 之间加一层代理

LLM 编码代理(Claude Code、Copilot、Cursor 等)会为处理的每一条 CLI 命令输出消耗 token。而绝大多数命令输出包含样板文本、进度条、ANSI 转义序列和冗长格式——它们烧钱但不提供可操作信息。

rtk 就坐在代理与 CLI 之间,过滤输出、只保留有价值的部分,从而降低费用并提高有效上下文窗口利用率。其工程约束是:单个 Rust 二进制、除二进制本身外无运行时依赖、每条命令增加的开销小于 10ms

文档特别强调一个度量口径问题:文中所有百分比测量的都是 bash 输出——它只是输入 token 的一个构成部分,而账单里还包含输出 token。rtk 并未内置 tokenizer(src/core/tracking.rs 使用 bytes / 4 估算),因此这些比例是可靠的,但绝对 token 数是近似值。这一点在 src/core/tracking.rs 中可以得到印证:模块头部注释明确写有「Storage: SQLite database (~/.local/share/rtk/tracking.db)」「Retention: 90-day automatic cleanup」。

2. 架构总览:三层过滤与四条设计原则

官方架构图中,一条命令的完整链路是:

User / LLM Agent
       |
       v
+--------------------------------------------------+
|  LLM Agent Hook                                  |
|  hooks/{claude,copilot,cursor,...}/              |
|  Intercepts: "git status" -> "rtk git status"   |
+-------------------------+------------------------+
                          |
                          v
+--------------------------------------------------+
|  RTK CLI (main.rs)                               |
|                                                  |
|  +-------------+    +-----------------+          |
|  | Clap Parser | -> | Command Routing |          |
|  | (Commands   |    | (match on enum) |          |
|  |  enum)      |    +--------+--------+          |
|  +-------------+             |                   |
|                    +---------+---------+         |
|                    v         v         v         |
|             +----------+ +--------+ +----------+|
|             |Rust Filter| |TOML DSL| |Passthru  ||
|             |(cmds/**)  | |Filter  | |(fallback)||
|             +-----+----+ +----+---+ +----+-----+|
|                   |           |           |      |
|                   +-----+-----+-----------+      |
|                         v                        |
|              +---------------------+             |
|              |   Token Tracking    |             |
|              |   (core/tracking)   |             |
|              |   SQLite DB         |             |
|              +---------------------+             |
+--------------------------------------------------+

三条输出路径构成「Rust 过滤器 → TOML DSL 过滤器 → 纯透传」的降级链,末端统一汇入 token 追踪。四条设计原则值得逐条对照源码理解:

  1. 单线程、无 async(启动 < 10ms)。入口 src/main.rs 中没有任何 tokio 痕迹,main() 只做 SIGPIPE 复位后同步调用 run_cli()std::process::exit(code)
  2. 优雅降级:过滤失败回退到原始输出。
  3. 退出码传播:RTK 从不吞掉非零退出码——对 CI/CD 可靠性和 pre-commit 钩子至关重要。
  4. 透明代理:未知命令原样透传。

3. 端到端流程:一条命令的完整生命周期

3.1 钩子安装(rtk init

用户执行 rtk init 为各自的 LLM 代理安装钩子,流程四步:

  1. 写入一个瘦 shell 钩子脚本(如 ~/.claude/hooks/rtk-rewrite.sh);
  2. 存储其 SHA-256 哈希用于完整性校验;
  3. 打补丁修改代理的配置文件(如 settings.json)注册钩子;
  4. 写入 RTK 感知指令(如 RTK.md)用于 prompt 层引导。

从源码结构看,src/main.rs 中的 AgentTarget 枚举实际列出了 12 个安装目标(Claude、Cursor、Windsurf、Cline、Kilocode、Antigravity、Kimi、Pi、Hermes、Droid、Vibe 等),而文档正文表述为「支持 7 种代理、各有独立安装模式」——安装模式数量随版本演进是增长的,以 src/main.rsAgentTarget 为准。钩子脚本内嵌于二进制中、安装时写出,安装模式、配置文件与卸载流程的完整说明见 src/hooks/README.md

3.2 钩子拦截(命令重写)

当 LLM 代理要执行 git status 时:

  1. 代理触发 PreToolUse 事件(或等价事件),以 JSON 携带命令;
  2. 钩子脚本读取 JSON、提取命令字符串;
  3. 钩子以子进程方式调用 rtk rewrite "git status"
  4. rtk rewrite 查询命令注册表,返回 rtk git status
  5. 钩子向代理返回「使用改写后命令」的响应;
  6. 任何环节失败(jq 缺失、rtk 未安装、无匹配)时钩子静默退出——原始命令照常执行。

全部重写逻辑都在 Rust 侧(src/discover/registry.rs),各代理的钩子只是处理各自 JSON 格式的瘦委托层。各代理 JSON 格式、复合命令处理与 RTK_DISABLED 覆盖项详见 hooks/README.md

重写流水线(Rewrite Pipeline)

调用链为:

hook shell → rewrite_cmd.rs → rewrite_command() → rewrite_compound() → rewrite_segment() → classify_command()

cargo fmt --all && cargo test 2>&1 | tail -20 为例逐步追踪:

LLM Agent: "cargo fmt --all && cargo test 2>&1 | tail -20"
  |
  |  Hook shell (hooks/claude/rtk-rewrite.sh)
  |  读取代理 JSON、提取命令、调用 `rtk rewrite "$CMD"`
  |  失败时(jq 缺失、rtk 缺失、旧版本):exit 0(透传)
  v
rewrite_cmd::run(cmd)                              [src/hooks/rewrite_cmd.rs]
  |  1. 加载配置 → hooks.exclude_commands
  |  2. check_command(cmd) → Deny → exit(2)
  |  3. registry::rewrite_command(cmd, excluded)
  |     → None → exit(1)          (无 RTK 等价物,透传)
  |     → Some + Allow → print, exit(0)
  |     → Some + Ask   → print, exit(3)
  v
rewrite_command(cmd, excluded)                     [src/discover/registry.rs]
  |  早退路径:
  |  - 空串 → None
  |  - 含 "<<" 或 "$(("(heredoc/算术)→ None
  |  - 简单 "rtk ..."(无操作符)→ 原样返回
  |  - 其他 → rewrite_compound(cmd, excluded)
  v
rewrite_compound(cmd, excluded)                    [src/discover/registry.rs]
  |  Step 1 — 词法分析(lexer.rs)
  |  tokenize() 产出带字节偏移的类型化 token:
  |    Arg("cargo") Arg("fmt") Arg("--all")
  |    Operator("&&")
  |    Arg("cargo") Arg("test") Redirect("2>&1")
  |    Pipe("|")
  |    Arg("tail") Arg("-20")
  |
  |  Step 2 — 按操作符切分,逐段改写
  |  Operator (&&, ||, ;) → 两侧都改写
  |  Pipe (|) → 生产者/中间段保持 raw,
  |             仅改写 pipeline-safe 的最终段
  |  Stderr pipe (|&) → 整条管道保持 raw
  |  Shellism (&) → 两侧都改写(后台)
  |
  |  对每段调用 rewrite_segment():
  |    segment 1: "cargo fmt --all"
  |    segment 2: "cargo test 2>&1"
  |    管道后: "tail -20" 保持 raw
  v
rewrite_segment(seg, excluded)                     [src/discover/registry.rs]
  |  Step 3 — 剥离尾部重定向
  |  strip_trailing_redirects() 重新分词:
  |    "cargo test 2>&1" → cmd_part="cargo test", redirect=" 2>&1"
  |  Step 4 — 已是 RTK → 原样返回
  |  Step 5 — 特判(分类前短路)
  |  head -N / --lines=N → rewrite_line_range() → "rtk read file --max-lines N"
  |  tail -N / -n N / --lines N → rewrite_line_range() → "rtk read file --tail-lines N"
  |  head/tail 带不支持的 flag(-c, -f)→ None(跳过)
  |  cat 带不兼容 flag(-A, -v, -e)→ None(跳过)
  |  Step 6 — classify_command(cmd_part)(见下)
  |  → Supported → 检查排除列表 → 继续
  |  → Unsupported/Ignored → None(跳过)
  |  Step 7 — 组装改写命令
  |  a. 从 rules.rs 找匹配规则
  |  b. 提取 env 前缀(ENV_PREFIX 正则,第二遍;第一遍在 classify 中)
  |  c. 守卫:前缀中 RTK_DISABLED=1 → None
  |  d. 守卫:gh 带 --json/--jq/--template → None
  |  e. 应用规则 rewrite_prefixes:"cargo fmt" → "rtk cargo fmt"
  |  f. 重组:env_prefix + rtk_cmd + args + redirect_suffix
  v
classify_command(cmd)                              [src/discover/registry.rs]
  |  1. 查 IGNORED_EXACT(cd, echo, fi, done, ...)
  |  2. 查 IGNORED_PREFIXES(rtk, mkdir, mv, ...)
  |  3. 用 ENV_PREFIX 正则剥离 env 前缀(仅用于模式匹配)
  |  4. 绝对路径归一化:/usr/bin/grep → grep
  |  5. 剥离 git 全局选项:git -C /tmp status → git status
  |  6. 守卫:cat/head/tail 带重定向(>, >>)→ Unsupported(这是写不是读)
  |  7. 匹配 REGEX_SET(来自 rules.rs 的 60+ 条编译后模式)
  |  8. 提取子命令 → 查自定义 savings/status 覆盖
  |  9. 返回 Classification::Supported { rtk_equivalent, category, savings, status }
  v
结果: "rtk cargo fmt --all && rtk cargo test 2>&1 | tail -20"
  |
  |  钩子把结果包进代理专属 JSON 返回给 LLM 代理
  v
LLM Agent 执行改写后的命令
(bash 处理 && 与 |,每个 rtk 调用是独立进程)

对照 src/discover/registry.rs 源码,文档描述的静态设施可以逐条对上:REGEX_SETrules.rsRULESLazyLock 一次性编译(registry.rs#L54-L62);ENV_PREFIX 正则显式兼容 sudo/env 前缀与带引号值(registry.rs#L63-L70);GIT_GLOBAL_OPT 覆盖 -C-c--git-dir--work-tree 等全局选项(registry.rs#L73-L75);HEAD_N/TAIL_N 等特判正则只接受单文件参数(\S+$),多文件调用如 head -3 a b c 故意不匹配、放给原生 head/tail 处理(registry.rs#L76-L89),与文档 Step 5 的「head/tail 特判」一一对应。

文档总结的五个关键设计决策:

  • 基于词法器的分词:单遍状态机(src/discover/lexer.rs)统一处理引号、转义、重定向、操作符,同时服务于复合命令切分与重定向剥离;
  • 段级改写:复合命令按操作符切开、每段独立改写,bash 在执行时重新组合;
  • 管道语义| 的生产者与中间段保持 raw,只有规则标记了 pipeline_final_safe 的参数安全最终段才可改写(目前限于普通 greprg 调用;-f/--file 模式文件形式会推迟,因其可能把管道 stdin 当配置消费);|& 单独识别、整条管道保持 raw;
  • 双重 env 前缀处理classify_command() 剥离前缀以匹配底层命令,rewrite_segment() 再单独提取同一前缀以便重新前置到改写命令上;
  • 回退契约:任何段匹配失败则该段保持 raw;仅当零段被改写时 rewrite_command() 才返回 None

3.3 CLI 解析与路由

改写后的命令到达 RTK 后,src/main.rsrun_cli()main.rs#L1621-L1646)严格按文档顺序执行五步:

  1. 遥测core::telemetry::maybe_ping() 发出非阻塞的每日用量 ping(fire-and-forget,1/天);
  2. Clap 解析Cli::try_parse_from(std::env::args_os()) 匹配 Commands 枚举;
  3. 钩子检查hooks::hook_check::maybe_warn() 在安装钩子过期时告警(限频 1/天;rtk gain 跳过,因它有自己内联的钩子提示);
  4. 完整性检查:对操作性命令执行 hooks::integrity::runtime_check(),校验钩子 SHA-256;元命令(init、gain、verify、config 等)不经过钩子管道,直接跳过;
  5. 路由match cli.command { ... } 分派到各专门过滤器模块。

若 Clap 解析失败(命令不在枚举中),则进入 run_fallback() 回退路径。

3.4 过滤器执行:两套过滤系统

RTK 有两套过滤体系:

Rust 过滤器src/cmds/ 下编译期模块,执行命令、解析输出并施加专门变换(正则、JSON、状态机),削减 60–95% 的 bash 输出。

TOML DSL 过滤器src/filters/ 下 60 余个声明式 .toml 配置,施加基于正则的行过滤、截断与区段抽取,在无 Rust 过滤器匹配时由 run_fallback() 应用。

每个 Rust 过滤器模块遵循同一模式:

  1. 启动计时器(TimedExecution::start());
  2. 执行底层命令(std::process::Command);
  3. 应用过滤(剥离样板、归组错误、截断);
  4. 过滤出错时回退到原始输出;
  5. 将 token 节省量写入 SQLite;
  6. 传播退出码。

通用模式、生态组织、跨命令依赖与「如何新增过滤器」见 src/cmds/README.md

3.5 回退路径(Fallback Path)

Clap 解析失败时的完整分支逻辑(src/main.rs):

Command received
  -> Clap parse succeeds?
     -> Yes: Route to Rust filter module
     -> No:  run_fallback()
              -> TOML filter match?
                 -> Yes: Capture stdout, apply filter, track savings
                 -> No:  Passthrough (inherit stdio, track 0% reduction)

源码中可以看到文档所述每一步的实现细节:

  • 元命令守卫args[0] 命中 RTK_META_COMMANDS(gain、discover、learn、init、config、proxy、run、hook、verify、trust、session、rewrite 等,定义于 src/core/constants.rs#L10-L31)时直接显示 Clap 错误,绝不尝试去 $PATH 里裸执行——防止 rtk gain --typo 误跑系统二进制;
  • TOML 匹配core::toml_filter::find_matching_filter(&lookup_cmd) 查表,且用 args[0] 的 basename 做匹配键,使 /usr/bin/make 这类绝对路径也能命中 ^make\bmain.rs#L1347-L1357);RTK_NO_TOML=1 环境变量可整体禁用该层(src/core/toml_filter.rs#L401-L403);
  • TOML 命中:捕获 stdout(若 filter 声明 filter_stderr 则连 stderr 一起捕获合并,用于剥离 liquibase 之类工具的横幅);过滤后按 Lossiness 枚举区分无损/截断/整体丢弃,截断场景写入 tee 并附提示;命令未找到时打印 [rtk: ...] 并返回 127;
  • 无匹配Stdio::inherit 纯透传流式执行,timer.track_passthrough() 按 0% 削减记账,退出码原样传出。

TOML 过滤引擎细节、信任门控(trust-gated)项目过滤器见 src/core/README.md

3.6 Token 追踪(SQLite 记账)

每条命令执行都会向 SQLite(~/.local/share/rtk/tracking.db)写入一条记录:

  • 输入 token(原始输出大小)与输出 token(过滤后大小);
  • 节省百分比、执行耗时、项目路径;
  • 90 天自动保留清理;
  • token 估算:ceil(chars / 4.0) 近似。

分析命令(rtk gainrtk cc-economicsrtk session)查询该库生成仪表盘与 ROI 报告。src/core/tracking.rs 的模块注释与文档完全一致(90 天保留、SQLite 存储、input/output token 与 savings % 度量),且数据库路径按平台区分:Linux 为 ~/.local/share/rtk/tracking.db,macOS 为 ~/Library/Application Support/rtk/tracking.db,Windows 为 %APPDATA%\rtk\tracking.db。分析模块见 src/analytics/README.md,追踪库 schema 见 src/core/README.md

3.7 Tee 恢复:失败命令的原始输出保险

命令失败(非零退出码)时:

  1. 未过滤的原始输出保存到 ~/.local/share/rtk/tee/{epoch}_{slug}.log
  2. 打印提示行:[full output: ~/.../tee/1234_cargo_test.log]
  3. LLM 代理可直接重读该文件,而不是重跑失败命令。

Tee 可配置(启用/禁用、最小体积、最大文件数、单文件最大体积),且永不影响命令输出或退出码。实现位于 src/core/tee.rs,回退路径中的调用点可见于 src/main.rs#L1401-L1416:命令失败走 tee_and_hint(),TOML 过滤产生 Lossiness::Tail/Lossiness::Whole 截断时走 force_tee_tail_hint()/force_tee_hint(),tee 配置与轮转策略见 src/core/README.md

4. 目录地图:按 README 下钻

src/ — Rust 源码

目录 职责 其 README 中的内容
src/main.rs CLI 入口、Commands 枚举、路由 match (无 README — 直接读文件)
src/core/ 共享基础设施 追踪库 schema、配置系统、tee 恢复、TOML 过滤引擎、工具函数
src/hooks/ 钩子系统 安装流程(rtk init)、完整性校验、rewrite 命令、信任模型
src/analytics/ token 节省分析 rtk gain 仪表盘、Claude Code 经济学、ccusage 解析
src/cmds/ 命令过滤器(9 大生态) 通用过滤模式、跨命令路由、token 节省表、链接到每个生态
src/discover/ 历史分析 + 重写注册表 重写模式、会话提供者、复合命令切分
src/learn/ CLI 纠错检测 错误分类、纠错对检测、规则生成
src/parser/ 解析器基础设施 规范类型(TestResult、LintResult 等)、3 级格式模式、迁移指南
src/filters/ TOML 过滤器配置 TOML DSL 语法、8 级流水线、内联测试、命名约定

hooks/ — 部署的钩子产物(根目录)

目录 代理 README 内容
hooks/ (父级) 全部 JSON 格式、重写注册表概览、退出码契约、覆盖控制
hooks/claude/ Claude Code shell 钩子机制、PreToolUse JSON、测试脚本
hooks/copilot/ GitHub Copilot Rust 二进制钩子,VS Code Chat 与 Copilot CLI 共享单一 PreToolUse schema
hooks/cursor/ Cursor IDE shell 钩子、空 JSON 响应要求
hooks/cline/ Cline / Roo Code 规则文件(prompt 层,无程序化钩子)
hooks/windsurf/ Windsurf / Cascade 规则文件(工作区作用域)
hooks/codex/ OpenAI Codex CLI 感知文档、AGENTS.md 集成
hooks/opencode/ OpenCode TypeScript 插件、zx 库、原地变更

5. 钩子系统:各代理的集成方式

代理 钩子类型 机制 可修改命令?
Claude Code Shell 钩子 settings.json 中的 PreToolUse 是(updatedInput
GitHub Copilot (VS Code) Rust 二进制 rtk hook copilot 读 JSON 是(updatedInput
GitHub Copilot CLI Rust 二进制 rtk hook copilot 读 JSON 是(updatedInput
Cursor Rust 二进制 rtk hook cursor 读 JSON 是(updated_input
Gemini CLI Rust 二进制 rtk hook gemini 读 JSON 是(hookSpecificOutput
Cline/Roo Code 规则文件 prompt 层引导 否(prompt)
Windsurf 规则文件 prompt 层引导 否(prompt)
Codex CLI 感知文档 AGENTS.md 集成 否(prompt)
OpenCode TS 插件 tool.execute.before 事件 是(原地变更)

每个代理的完整 JSON schema 见 hooks/README.md;安装流程、完整性校验与 rewrite 命令见 src/hooks/README.md

6. 过滤流水线:Rust 与 TOML DSL 双轨

Rust 过滤器(src/cmds/:面向复杂变换的编译期模块,削减 60–95% 的 bash 输出。各生态子目录 README 有逐文件说明。

TOML DSL 过滤器(src/filters/ 下 60 余个 *.toml:声明式 8 级流水线。src/core/toml_filter.rs#L487-L495 的函数注释与文档逐级吻合:

1. strip_ansi       — 移除 ANSI 转义
2. replace          — 逐行可链式正则替换
3. match_output     — 整体匹配则短路
4. strip/keep_lines — 按正则过滤行
5. truncate_lines_at— 每行截断到 N 字符
6. head/tail_lines  — 保留前/后 N 行
7. max_lines        — 绝对行数上限
8. on_empty         — 结果为空时的消息

过滤器从三个层级加载:内建(编译进二进制)、全局(~/.config/rtk/filters/)、项目本地(.rtk/filters/,信任门控)。源码中 collect_match_patterns()src/core/toml_filter.rs#L439-L459)清楚体现了信任门控:项目/全局层路径先经 hooks::trust::check_trust_with_content() 校验,仅 TrustedEnvOverride 状态的过滤器模式才进入 RegexSet,内建 TOML 则无条件并入。

7. 性能约束

指标 目标 验证方式
启动时间 < 10ms hyperfine 'rtk git status' 'git status'
内存占用 < 5MB 常驻 /usr/bin/time -v rtk git status
二进制体积 < 5MB stripped ls -lh target/release/rtk
bash 输出削减 每过滤器 ≥20%(下限) 快照 + token 计数测试

实现手段:零 async 开销(单线程、无 tokio)、LazyLock 惰性正则编译(src/discover/registry.rs#L54-L70src/core/toml_filter.rs#L399-L422 中的 REGISTRY/MATCH_SET 均为 LazyLock 单例,一次编译终身复用)、最小分配(借用优于克隆)、启动时零配置文件 I/O(按需加载)。

8. 测试规范:与模块同居的测试与真实 fixture

测试放在模块文件内部#[cfg(test)] mod tests 块中(例如 src/cmds/cloud/container.rs 的测试就在该文件底部)。

1. 从真实命令输出创建 fixture(不用合成数据):

kubectl get pods > tests/fixtures/kubectl_pods_raw.txt

2. 在同一模块文件内写测试

#[test]
fn test_my_filter() {
    let input = include_str!("../tests/fixtures/my_cmd_raw.txt");
    let output = filter_my_cmd(input);
    assert!(output.contains("expected content"));
    assert!(!output.contains("noise line"));
}

3. 验证输出削减量(要求 ≥20% 的 bash 输出削减;测试中的 count_tokensrtk gain 背后的 bytes / 4 估算器都是近似值,作为比例可靠):

#[test]
fn test_my_filter_savings() {
    let input = include_str!("../tests/fixtures/my_cmd_raw.txt");
    let output = filter_my_cmd(input);
    let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(input) as f64 * 100.0);
    assert!(savings >= 20.0, "Expected >=20% savings, got {:.1}%", savings);
}

测试组织约定:

tests/
├── fixtures/           # 真实命令输出(绝不用合成数据)
│   ├── git_log_raw.txt
│   ├── cargo_test_raw.txt
│   └── dotnet/         # 生态专属 fixture
└── integration_test.rs # 集成测试(#[ignore])

完整的测试要求、pre-commit 门禁与 PR 清单见 CONTRIBUTING.md,深度参考(过滤分类学、性能基准、架构决策)见 ARCHITECTURE.md

9. 已规划的未来改进

  • 抽出 cli.rs:把 Commands 枚举、13 个子枚举(GitCommandsCargoCommands 等)与 AgentTarget 从 main.rs 迁到专门的 cli.rs 模块,可将 main.rs 从约 2600 行减到约 1500 行(当前 src/main.rs 实际已近 3900 行,枚举与路由 match 仍集中在其中);
  • 拆分路由:把 match cli.command { ... } 块抽成独立路由模块;
  • 流式过滤器:对长时命令逐行过滤流式输出,而非整块缓冲。

10. 小结

rtk 的工程答案可以浓缩为一句话:用词法器做段级重写的命令注册表(src/discover/),用 Rust 过滤器与信任门控的 TOML DSL 做双轨降级(src/cmds/src/filters/),用 SQLite 做可审计的节省记账(src/core/tracking.rs),用 tee 保留失败现场(src/core/tee.rs——全程单线程、无依赖、失败即透传。这套设计使得「代理拦截 → 改写 → 过滤 → 记账」的每一步都可被单测 fixture 独立验证,也让新增一个命令过滤器成为有模板可循的常规工程动作。

登录后查看全文
热门项目推荐
相关项目推荐