跳转到内容

设计原则

agent 读到的每一个 token 都是要计费的输入。CtxCtl 的存在是因为两大上下文污染源——整文件 倾倒和原始命令日志——都可以避免。本页解释这个工具背后的设计决策:压缩究竟保留了什么, 以及节省在哪里会自然趋缓。

上下文按 token 计费,体量就是预算

Section titled “上下文按 token 计费,体量就是预算”

编程 agent 会话的 token 大多花在重复阅读上:为找一个函数读整个文件,为找一条错误翻完整份 构建日志。基准测试 背后的脚本化 agent 实验中,仅使用内置 read + bash 工具的 base 组,每个会话平均烧掉 26,284 个未缓存输入 token——而且这还是在 provider 前缀缓存 命中率已经高达 81.7% 的基础上。

经济账毫不留情:

  • 输入 token 在它出现的每一轮都被计费,而不是只算一次。
  • 大上下文还会拉低回答质量——干草堆越大,针越难找。
  • provider 的提示词缓存只折扣重复且稳定的前缀,前提是前缀保持不变。

CtxCtl 直接打击前两点,并为第三点做了工程设计。

ctxctl 读取文件的这一半是一个 tree-sitter 符号引擎,铁律只有一条:用 AST 定位,从原始 源码字节切片。

这条流水线刻意保持简单且无状态:

  1. 用 tree-sitter 把文件解析成 AST(解析后即丢弃,不缓存任何东西)。
  2. 抽取符号——名称、种类、行区间、规范化签名——形成大纲(outline)。
  3. 把请求的符号解析到它的字节区间。
  4. 返回该区间的逐字节原始源码。

没有任何改写、转述或摘要。切片原样保留注释、格式和缩进,因此它仍然是合法源码:你的 agent 可以直接引用它、重新解析它,或与后续编辑做 diff,中间不需要任何翻译步骤。

Terminal window
ctxctl outline src/server.rs
ctxctl symbol src/server.rs --name handle_request
# src/server.rs [12 symbols, ~2.1 KB -> ~410 tokens, saved ~80%]
fn handle_request L:42-58 pub async fn handle_request(&self, id: u64)
struct RequestHandler L:12-40 pub struct RequestHandler {
const MAX_RETRIES L:60 const MAX_RETRIES: u32 = 3;
# handle_request src/server.rs:42-58 (58 tokens, saved ~85%)
pub async fn handle_request(&self, id: u64) -> Result<String, Error> {
let row = self.db.get(id).await?;
Ok(row.to_string())
}

大纲给出地图(此例中比原文件小约 80%);切片只给你点名要的那块地盘。当连完整函数体都嫌多时, --compact 返回 AST 剪枝视图——签名加一条 // ... [N lines omitted] 折叠标记—— --signature 则只返回声明行。工作流见探索陌生代码

命令输出是另一条消防水管。ctxctl exec 包裹一条命令,把它的输出送进一条固定的压缩流水线:

  1. 保留关键行:匹配配置模式的行——默认 errorwarningfailedpanicfatal (大小写不敏感)。诊断位置行(--> src/foo.rs:12:34,rustc/cargo 风格)被隐式保留, 因此被保留的错误头信息始终带着 file:line 上下文。
  2. 保留头部与尾部:开头和结尾各若干行(默认各 5 行),保住命令回显和最终结论/摘要。
  3. 折叠中段:压成一行 ... [N lines omitted] 标记,但只在超过折叠阈值后触发 (默认 20 行)。
  4. 压缩失效时警告:如果折叠执行了却只省下不到 10%——通常是 --keep 模式过宽—— 会追加一条确定性警告行。
Terminal window
ctxctl exec "cargo build"
$ cargo build
error[E0308]: mismatched types --> src/main.rs:12
... [34 lines omitted]
warning: unused variable: `x` --> src/server.rs:88
= note: 2 warnings emitted
Saved ~70% (1,240 -> 372 tokens)

压缩随子进程产出增量流式进行,内存始终以头尾窗口加保留匹配为上界——输出几个 GB 的命令也 撑不爆内存。被包裹命令的退出码原样透传。调优细节见压缩命令输出

省一次 token 只是及格线。CtxCtl 的核心契约是输出字节稳定:正文绝不包含时间戳、计数器、 随机值、机器相关路径或 PID。相同输入 + 相同配置 → 字节级一致的输出。

这个性质与 provider 提示词缓存相乘——Anthropic 缓存命中约有 ~90% 折扣,OpenAI 约 ~50%。 确定性的工具意味着相同的运行产生相同的前缀,昨天的缓存 token 今天依然有效。带时间戳、进度 计数器这类易变内容的输出会让缓存失效,悄悄把省下的钱还回去。

连节省统计本身也遵守这条规则:saved N% 是由 cl100k_base BPE 分词器确定性计算的函数—— 从不依赖外部测量。

没有索引、没有缓存、没有守护进程、没有会话状态。每次调用都是解析后丢弃。代价是一些重复 解析,换来的是:

  • 零安装、零过期——会话中途被编辑的文件,下一次调用就是新鲜解析。
  • 没有任何东西会损坏、需要迁移或在 worktree 与分支间清理。
  • 配置文件只保存默认行为偏好,在任何地方都以同一套优先级解析。

诚实面对边界比一个漂亮的头条数字更重要。节省幅度与你的 provider 已有的缓存质量成反比:

  • 对 provider 缓存激进的模型——deepseek-v4-flash 在基准中相对内置工具削减了 −33% 会话成本——相对成本收益反而缩水,因为基线本身已被折扣打过折;尽管未缓存(全额计费) 输入仍然下降。
  • 弱缓存模型的头条赢面更大:solar-pro4 节省了 47% 会话成本

两种情况下 ctxctl 都减少了被当作新输入计费的部分;这份缩减有多少转化为美元,取决于你 provider 的缓存质量——见基准测试的注意事项。另外两条诚实的边界:

  • 基准中工具调用密集的会话大约花了 2× 墙钟时间;你用秒数换美元和缓存压力。
  • 微小输入会让账反过来算:小文件上 JSON 信封可能超过文件本身,此时 saved% 合理地为 0。