首页
/ caveman-compress 技能源码级解析:用"洞穴人语法"压缩项目记忆文件、为每轮会话削减 46% 输入 Token

caveman-compress 技能源码级解析:用"洞穴人语法"压缩项目记忆文件、为每轮会话削减 46% 输入 Token

2026-09-06 18:11:02作者:庞队千Virginia

CLAUDE.md、todo 清单、偏好配置这类"项目记忆文件"会在每次会话启动时被 Claude Code 重新读入,体量一大就成了逐轮重复消耗的输入成本。caveman-compress 是 caveman 工具集内置的一项技能,用"洞穴人语法"(短词、碎片句式、删冗余)改写这些自然语言文件,并配有可读备份与严格的自动校验。读完本文你将掌握它的触发方式、压缩规则、命令级工作流,以及它在代码层是如何用"零 Token 的本地 Python 校验 + 定向修复重试"保证代码块、URL、路径、标题一字不改的。

一、它解决什么问题

Claude Code 会在每个会话启动时加载 CLAUDE.md 等项目记忆文件。一个 1,000 token 的"项目记忆",每打开一次项目就新增 1,000 个输入 token;打开 100 次就是 100,000 token。文件越大,这类"每会话固定成本"越可观。

caveman-compress 的思路不是改造运行时,而是改造文件本身:把自然语言文件压缩成"洞穴人语"——用短词替换长词、允许碎片句式、删掉寒暄与连接词——从而在源头降低每一轮的输入体量。技能本体位于 skills/caveman-compress/SKILL.md,它隶属于 caveman 项目:"caveman 让 Claude 用更短的话作答,caveman-compress 则用来缩短受支持的项目记忆文件,且带备份与校验"(见 skills/caveman-compress/README.md)。

需要强调的是,该技能只作用于自然语言文件,代码、配置文件一律跳过;且压缩产物会经结构校验后覆盖原文件,原始可读版本则保存在源码目录之外的数据目录中(理由详见"五、备份为什么放到仓库外")。

二、触发方式与文件布局

在 Claude Code 中使用斜杠命令触发:

/caveman-compress <filepath>

也可以直接用自然语言提出压缩某个记忆文件的请求。常用示例:

/caveman-compress CLAUDE.md
/caveman-compress docs/preferences.md
/caveman-compress todos.md

技能要求 Python 3.10 及以上版本,作为 caveman 插件的一部分内置分发:安装 caveman 一次即可使用 /caveman-compress。技能目录结构如下:

skills/caveman-compress/
├── SKILL.md              # 技能定义(frontmatter + 压缩规则)
├── README.md             # 使用说明与基准数据
├── SECURITY.md           # 安全模型说明(Snyk 高危评级的解释)
└── scripts/
    ├── __main__.py       # 模块入口:python3 -m scripts
    ├── __init__.py
    ├── cli.py            # CLI 参数解析与文件级前置检查
    ├── compress.py       # 编排核心:锁、压缩、校验、修复、原子写
    ├── detect.py         # 文件类型判定(零 Token)
    ├── validate.py       # 结构校验器(零 Token)
    └── benchmark.py      # Token 计量与基准表输出

SKILL.md 的 frontmatter 还声明了触发语义:"Compress a memory file such as CLAUDE.md or a todo list into caveman format to save input tokens, keeping a readable backup. Trigger: /caveman-compress"。

三、运行流程:从命令到落盘

3.1 命令行调用

从 SKILL.md 所在目录(skills/caveman-compress/)直接以模块方式运行:

python3 -m scripts <absolute_filepath>

这里 scripts/__main__.py 会把控制权交给 scripts/cli.pymain()cli.py 依次完成"参数个数校验 → 文件存在且为普通文件 → resolve 绝对路径 → 探测文件类型 → 判定是否可压缩 → 调用 compress_file()",整个 CLI 全程零 Token。

3.2 完整流水线

README 给出了整条调用链,每一步都有源码对应:

/caveman-compress CLAUDE.md
        ↓
basic checks: 文件存在、小于 500KB、非敏感文件名
        ↓
获取该文件的跨会话锁(最长等 15 分钟,超时则报错)
        ↓
探测文件类型        (零 Token)
        ↓
Claude 执行压缩     (Token:一次调用)
        ↓
校验输出            (零 Token)
  检查项:标题、代码块、URL、文件路径、列表
        ↓
有错时:Claude 仅定向修复问题点   (Token:定向修复)
  不重新压缩,只修补损坏的部分
        ↓
最多重试 2 次
        ↓
写压缩结果 → CLAUDE.md
写原始内容 → CLAUDE.original.md(存于数据目录)

整条链路里只有两处消耗 Token:首次压缩 + 校验失败后的定向修复,其余全部由本地 Python 完成。

具体到 compress.pycompress_file(),会先做三类"不需要互斥"的拒绝检查再取锁,避免被拒绝的输入在共享状态里留下永久锁文件:

  1. 文件不存在 → FileNotFoundError
  2. 超过 MAX_FILE_SIZE = 500_000(500KB)→ 拒绝(读取前就拦截,见下"安全"一节);
  3. 命中敏感路径启发式名单 → 拒绝。

随后在 file_lock() 上下文中执行 _compress_file_locked() 主体:先再次 should_compress 判定,再读取原文、准备备份路径、剥离 YAML frontmatter、对正文做代码块遮罩、调用 Claude 压缩、恢复代码块、组装输出、原子写入、进入"校验+修复+重试"循环。

四、压缩规则全解

SKILL.md 把规则分成四组,是提示词与校验器共同遵守的"宪法"。

4.1 删除类内容

类别 例子
冠词 a、an、the
填充词 just、really、basically、actually、simply、essentially、generally
客套语 "sure"、"certainly"、"of course"、"happy to"、"I'd recommend"
委婉语 "it might be worth"、"you could consider"、"it would be good to"
冗余表达 "in order to"→"to"、"make sure to"→"ensure"、"the reason is because"→"because"
连接性废话 however、furthermore、additionally、in addition

4.2 逐字保留(绝不修改)

  • 围栏代码块与缩进代码块(详见 4.4);
  • 行内代码(反引号内容);
  • URL 与链接(完整 URL、Markdown 链接);
  • 文件路径(/src/components/..../config.yaml);
  • 命令(npm installgit commitdocker build);
  • 技术名词(库名、API 名、协议、算法);
  • 专有名词(项目名、人名、公司名);
  • 日期、版本号、数值;
  • 环境变量($HOMENODE_ENV)。

4.3 保留结构

  • 全部 Markdown 标题(标题文本原样保留,只压缩标题下方正文);
  • 无序列表层级(保留嵌套深度);
  • 有序列表(保留编号);
  • 表格(压缩单元格文字、保留表结构);
  • Markdown 文件开头的 frontmatter/YAML 头。

4.4 压缩正文的"语感"约定

  • 用短同义词:"big" 而非 "extensive","fix" 而非 "implement a solution for","use" 而非 "utilize";
  • 允许碎片句:"Run tests before commit",而不是 "You should always run tests before committing";
  • 直接陈述动作,丢弃 "you should / make sure to / remember to" 这类劝告包装;
  • 合并语义重复的条目;
  • 多个示例表达同一模式时只保留一个。

4.5 SKILL.md 给出的对照范例

原句:

You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.

压缩后:

Run tests before push to main. Catch bugs early, prevent broken prod deploys.

另一例:

The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.

压缩后:

Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.

注意第二条演示了"名词术语必须保留"与"技术句允许缩减为碎片主谓结构"的组合拳。

4.6 关于代码的"绝对红线"

SKILL.md 用整段大写强调:``` 围栏内的一切必须逐字节复制,不删注释、不动空行、不重排行、不缩短命令、不做任何简化;反引号行内代码同理;文件含代码块时,将代码块视为只读区域,仅压缩其外文本,且不得围绕代码合并相邻小节。

五、备份为什么放到仓库外

压缩会原地覆盖原文件,因此先写备份。备份文件名统一为 <原文件名>.original.md,但不放在源文件旁边,而是放在源码目录之外的数据目录:

  • Linux/macOS:$XDG_DATA_HOME/caveman-compress/backups/<父目录名>/
  • Windows:%LOCALAPPDATA%\caveman-compress\backups\<父目录名>\

对应的平台感知实现是 compress.py_state_base_dir()backup_dir_for():前者在 Windows 上取 LOCALAPPDATA(未设置时回退 AppData/Local),其他平台取 XDG_DATA_HOME(未设置时回退 ~/.local/share),再统一拼上 caveman-compress/backups;后者用源文件父目录名做二级键,降低不同项目同名文件互相覆盖备份的风险。

把备份放到树外的根本原因:防止技能的自动加载器把 .original.md 当作活跃文件重复读入。也就是说,备份既服务于"人可读、可回滚",也服务于"不让压缩产生的第二份大文件反过来吃掉你刚省的 token"。

也因此形成一条可持续的维护闭环:直接编辑数据目录里的 .original.md(人可读版),再跑一次技能即可基于最新内容重新压缩。反过来,工具也会拒绝压缩任何 *.original.mddetect.pyshould_compress() 显式跳过此类文件),防止备份被二次改写。

六、适用范围边界(Boundaries)

  • 只压缩自然语言文件:.md.txt.typ.typst.tex 及无扩展名的自然语言文件(README 的表格还补充了 .markdown.rstdetect.pyCOMPRESSIBLE_EXTENSIONS 与之一致);
  • 永不修改.py.js.ts.json.yaml.yml.toml.env.lock.css.html.xml.sql.sh 等(detect.pySKIP_EXTENSIONS 还额外列出 .tsx/.jsx/.scss/.bash/.zsh/.go/.rs/.java/.c/.cpp/.rb/.php/.swift/.kt/.lua/.csv/.ini/.cfg 以及 DockerfileMakefileGemfileCMakeLists.txt 等常见无后缀或误导性后缀的代码文件);
  • 混合内容文件:只压缩散文段,代码段视为只读区;
  • 无法判断是代码还是散文的内容,保持原样
  • 每次覆盖前在树外数据目录生成 .original.md 备份,并跳过 *.original.md 本身。

值得留意的是 detect_file_type()detect.py)的分类策略:有扩展名走白/黑名单;无扩展名时靠内容推断——先看 shebang(#! 即脚本)、JSON/YAML 内容启发式,再统计前 50 行中匹配 CODE_PATTERNS(import/def/class/function、if(/for(/while(、装饰器 @、JSON 键值对、赋值字面量等)的代码行占比,超过 40% 判为代码。这些判断全部发生在调用 API 之前,不花一分 Token。

七、零 Token 结构校验:数据不会静默损坏

在压缩结果写回之前,validate.py 会把备份(原始)与压缩文件并排做六项校验,全部为本地 Python 实现:

校验器 判定规则 失败后果
validate_headings 标题数量一致,且文本/顺序逐条相等 文本变化为 error;仅层级变化降级为 warning
validate_code_blocks 围栏块 + 4 空格缩进块逐一相等 error
validate_urls URL 集合一致 error
validate_paths 路径集合一致,硬性路径(./..//、盘符或带点文件名)丢失 硬性丢失为 error,模糊项为 warning
validate_bullets 列表项数量变化不超过 15% 仅 warning
validate_inline_codes 反引号行内代码多重集一致 丢失为 error,新增为 warning

几个实现细节值得注意:

  • 标题文本是否逐字保留被判定为 error 而非 warning(validate.py),因为文档内锚点链接都指向标题生成的 slug,改标题即悄悄改坏所有站内锚点。
  • 文件路径校验曾经"从未真正报错",导致被丢掉的 src/hooks/caveman-config.js 一路绿灯通过;现在用 DEFINITE_PATH_REGEX 区分"确定的路径"与"普通成对出现的词"(如 prose 里的 "pros/cons"),前者丢失即 error(见 validate.py 的说明)。
  • 代码块提取同时覆盖围栏块与 4 空格缩进块:CommonMark 规定 4 空格缩进构成代码块,缩进代码里的一行命令若被当成散文重写(把 kubectl delete pod --all -n prod 改成 -n dev),将是"校验通过但文件被破坏"的最坏情况,因此缩进块被单独提取并纳入逐字比对(validate.py)。
  • 行内代码提取前会先剥离围栏块,并把残留的围栏标记行清空,防止展示性反引号错配造成"文件永远无法通过校验"的假失败(validate.py 注释引用了 issue #820 的教训)。

校验失败即进入 定向修复:CLI 会基于原文件与压缩结果 + 具体错误清单构建 build_fix_prompt()compress.py),只修补被点名的坏点("DO NOT recompress or rephrase the file;ONLY fix the listed errors"),绝不整篇重压;每轮修复还会做一次"正文不得混入前言、输出首行必须匹配原文结构锚点(frontmatter ---# 标题)"的守卫(compress.py),防止模型把解释文字夹带到正文里。最多重试 2 次,仍失败则用原始字节还原文件、删除备份并报错,原文件始终可回滚。

八、代码块的"遮罩-恢复"机制

既然要求代码逐字保留,为何还要把整个文件交给 Claude?compress.py 给出了工程答案:先遮罩、后恢复

  • mask_code_blocks() 把围栏块与 4 空格缩进块整体替换成一行不透明标记:@@CAVEMAN_PRESERVED_CODE_<序号>_<sha256 前 16 位>@@。模型只见散文和占位标记,物理上没有机会改动代码。
  • call_claude() 返回后由 restore_code_blocks() 按标记逐个还原:某个标记出现次数不为 1、或输出中残留未知标记,都直接抛错拒绝落盘(fail closed)。
  • 同理,文件首部的 YAML frontmatter 会被 split_frontmatter() 在压缩前剥离、压缩后原样拼回(compress.py),因为 LLM 有"即使规则要求保留也爱改写 frontmatter"的坏习惯。
  • strip_llm_wrapper()compress.py)负责剥掉模型偶尔给整个输出套上的外层 markdown 围栏——它只在该围栏确实是"首尾同一块完整围栏、中间无同类围栏"时才剥除,避免把普通文档里两段独立代码块误判为一个外层围栏后熔毁结构。

这意味着送给模型的是散文加标记、模型返回后标记被原子还原,代码块从输入到输出全程没有真正接触过模型的可改写文本流。

九、落盘可靠性:原子写、字节级备份、行尾保留

覆盖写有风险,compress.py 用三重机制兜底:

  • 原子写write_bytes_atomic() 先在目标同目录写临时文件并 fsync,再 os.replace() 覆盖,并继承原文件权限位。它规避了 Path.write_text()"先截断后编码"——中途抛错会留下 0 字节空文件把旧内容毁掉的问题;
  • 字节级备份与读回校验:备份写入后立刻 read_bytes() 与内存原始字节比对,不一致即删除坏备份并中止,绝不带着"坏备份 + 新压缩主文件"收场(compress.py);
  • 行尾与编码:读取时严格 UTF-8 解码并探测 CRLF/LF,写回时按探测结果还原行尾,保证 CRLF 文档压缩后仍是 CRLF;遇非 UTF-8 文件则直接拒绝(提示先转码),避免无法解码的字节在往返中被静默丢弃(compress.py)。

备份防覆盖同样有闸门:如果目标备份已存在,工具会中止并提示用户自行处理,防止覆盖更早的重要内容(compress.py)。

十、跨会话锁:并发压缩不会互相踩踏

两个会话同时压缩同一文件会彼此覆盖。file_lock()compress.py)实现基于 OS 原生文件锁的跨会话互斥:POSIX 用 fcntl.flock,Windows 用 msvcrt.locking;进程被 kill 或崩溃时内核自动释放锁,无需维护"锁是否过期"的标记文件状态。

锁路径由备份路径做 SHA-256 取前 16 位哈希得到(compress.py),既保证同一源文件必然序列化,又避免把超长原始路径明文写进文件系统。等待上限 LOCK_WAIT_SECONDS = 900 秒,轮询间隔 1 秒;等待超过 900 秒抛 LockTimeoutError 提示重试。设计上有意让"单次 Claude 调用超时"(CLAUDE_CALL_TIMEOUT_SECONDS = 900 // (2+1) = 300 秒)短于总锁等待窗口,防止一次卡死的 API 调用把整把锁占成死锁。实现还考虑了两类异常边界:不实现文件锁的文件系统(部分 NFS/SMB/FUSE)降级为"无协调继续运行"并显式警告;对锁目录与锁文件会拒绝穿越预置符号链接(_O_NOFOLLOW + 目录 0o700)。

十一、能耗账:它到底能省多少

README 在五个真实项目文件夹具上给出了实测基准(夹具位于 tests/caveman-compress/,压缩/原始成对存放,例如 claude-md-preferences.original.mdclaude-md-preferences.md):

文件 原始 Token 压缩后 Token 节省
claude-md-preferences.md 706 285 59.6%
project-notes.md 1145 535 53.3%
claude-md-project.md 1122 636 43.3%
todo-list.md 627 388 38.1%
mixed-with-code.md 888 560 36.9%
平均 898 481 46%

这些数值由 benchmark.py 计量:优先用 tiktoken 的 o200k_base 编码计数,缺失时退化为分词数;支持 python3 benchmark.py 原始.md 压缩.md 的配对模式与自动扫描 tests/caveman-compress/*.original.md 的全量模式,并逐对跑校验,输出含 Valid 列的 Markdown 表格。

README 同时给出了严谨的免责声明:所有夹具均通过"标题、代码块、URL、路径"的结构校验,但这只能证明结构无损,并不构成对任意文件、任意模型下语义等价或任务质量等价的一般性证明;结论里的 46% 是"计数的 token 缩减",不代表端到端成本恒等。换言之,数字可复现、机制可验证,但请把它当作压缩效果的抽样参考而非保证。

同一文档还给了另一个直观锚点:1,000 token 的记忆文件 × 100 个会话 = 100,000 token 的累计输入;省 46% 就意味着这 100,000 token 里约有 4.6 万不再产生。省下来的不是一次性的,而是每次会话都会复利的固定成本。

十二、安全模型:哪些数据会越过进程边界

把文件内容交给第三方 API 之前,这个技能设了多层闸门(SECURITY.md 系统解释了为何会被 Snyk 静态分析判为 High Risk,以及这些操作的真实边界):

  1. 500KB 硬上限:超过即拒绝,API 调用前拦截;
  2. 敏感路径拒绝名单:文件名匹配 credentials*secrets*passwords*.env*.netrcid_rsaauthorized_keys、各类密钥证书扩展名,或路径包含 .ssh/.aws/.gnupg/.kube/.dockersecret/credential/password/apikey/token 等归一化片段时直接拒读(compress.pyis_sensitive_path()),并提示用户改名以覆盖误报;
  3. 只访问用户显式指定的文件:读取目标文件、把备份写入树外数据目录,除此之外不触碰任何文件;
  4. 子进程无 shell 注入:回退走 claude --print CLI 时使用固定参数列表、经 stdin 传内容,不使用 shell=True,也不做字符串插值(compress.py);设置了 ANTHROPIC_API_KEY 时则优先走 Anthropic Python SDK 直连,模型名可用 CAVEMAN_MODEL 环境变量覆盖(默认 claude-sonnet-4-5),两种情况都不产生子进程;
  5. 不执行文件内容、不做目标文件之外的网络请求。

一句话概括安全取舍:压缩会把原文送往 Anthropic API,这是无法回避的第三方数据边界;所以它在源头用"扩展名 + 敏感名单 + 大小上限"把可能泄密的文件挡在读取之前,而不是寄希望于模型自觉。

十三、错误处理与失败语义

整个流程遵循"宁可不动、不可损坏"的失败语义,关键的失败分支都显式打印"原文件未被动过 / 已还原":

  • 文件不存在、非文件、超 500KB、敏感名 → 报错退出,不创建备份与锁残留;
  • 非自然语言 → 打印 Skipping: file is not natural language (code/config) 正常退出(退出码 0);
  • 校验失败且 2 次修复仍不通过 → 用原始字节还原主文件并删除备份,compress_file() 返回失败(CLI 退出码 2);
  • 输出与输入完全一致 → 判定模型拒绝压缩或文件本已是洞穴人语,中止且不产生备份;
  • 模型改动了代码保留标记 → fail closed,拒绝写入;
  • 非 UTF-8、空文件、frontmatter 剥离后正文为空 → 全部拒绝。

CLI(cli.py)还强制在 Windows 上将 stdout/stderr 重配为 UTF-8,避免 cp1252 控制台因无法编码错误分支里的 字形而崩溃——那样会把真实错误掩盖成"半压缩文件"。

十四、源码导览与实验建议

想亲手验证这套机制,推荐从这些入口读起:

可以做的低风险实验:先备份你自己的记忆文件,再运行 /caveman-compress 观察它在数据目录里产出的 .original.md;或直接用 python3 -m scripts <filepath> 跑 CLI,用 benchmark.py 复算自己文件的节省率,再检查校验器输出的 Error/Warning 列表。若误压缩,编辑数据目录中的 .original.md 重新压缩即可。需要留意的是,本文所有描述以当前仓库代码为准;不同版本的触发方式、默认模型与阈值可能随 skills/caveman-compress/SKILL.md 的更新而变化。

结语

caveman-compress 把"省 token"拆成了一个可验证的工程闭环:用短词与碎片句式改写散文(省输入),用树外备份保住可读原稿(可回滚),用"遮罩-恢复 + 六项零 Token 校验 + 定向修复重试"保证代码块、URL、路径、标题在压缩前后逐字等价(不损坏),再用跨会话锁与原子写保证并发与落盘安全(不踩踏)。对高频加载的项目记忆文件而言,这是一笔按会话次数复利累积的成本优化——而在它身后,skills/caveman-compress/scripts/validate.py 的每一行注释都在提醒:压缩工具最危险的不是省得少,而是"看起来通过、实际上悄悄改坏了你的文件"。

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