工具入口收敛之后,下一个问题不是继续加命令,而是让系统知道"现在处于什么状态"。如果 SKILL.md 没变、用例也没变,系统还每次都重新提炼规则、重新生成用例,本质上就是没有记忆。
MD5 缓存的价值不只是省时间。它让 SkillSentry 能根据 rules.cache.json、cases.cache.json 和当前文件哈希推断状态,从而判断该复用、该重跑,还是该提醒用户确认。
这篇文章的关键判断有两个:
- MD5 哈希缓存的主要价值不是省时间,而是给系统提供状态感知能力。缓存记录的是"最后一次执行时的 SKILL.md 内容",比较当前内容和缓存,就知道"规则变了没有"。
- 让系统"推断你需要什么"比让用户"说清楚要什么"的可用性高一个量级。前者需要用户了解工作流的含义和区别,后者只需要说「测评 xxx」。
- 一、问题:每次都全量跑,即使什么都没变
- 二、第一层缓存:规则缓存(rules.cache.json)
- 三、第二层缓存:用例缓存(cases.cache.json)
- 四、哈希比对如何工作
- 五、意外的副产品:状态感知
- [六、四种状态 → 四种推断](#六、四种状态 → 四种推断 "#%E5%85%AD%E5%9B%9B%E7%A7%8D%E7%8A%B6%E6%80%81%E5%9B%9B%E7%A7%8D%E6%8E%A8%E6%96%AD")
- 七、确认提示:推断不是强制,是建议
- 八、诚实面对:缓存会误导吗
一、问题:每次都全量跑,即使什么都没变
SkillSentry 的完整流程里,有两个最耗时的生成阶段:
-
规则提炼:读取 SKILL.md,提炼出一套结构化的规则列表(用于后续用例设计的依据)。这一步需要 LLM 仔细阅读并归纳 SKILL.md 的全部内容,耗时约 1-2 分钟。
-
用例设计:基于规则列表 + inputs/ 里的外部用例,设计覆盖各类场景的 evals.json。quick 模式约 5-10 分钟,standard 模式更长。
问题在于:这两个阶段的输入是固定的,只要输入没变,输出就不会变。
如果 SKILL.md 没有任何改动,规则列表就和上次完全相同,再提炼一遍只是在重复已知的结论。如果规则列表没有变,设计出来的用例集也和上次相同(给定相同规则 + 相同 inputs/,LLM 会设计出高度相似的覆盖场景)。
但原本的单体管道每次都全量跑,没有任何跳过的机制。改了一个工具调用参数名,确认主流程没崩,也要等完整的规则提炼 + 用例设计 + 执行,20-30 分钟。
这个问题不难解:内容没变,就用缓存。
二、第一层缓存:规则缓存(rules.cache.json)
缓存的内容:
json
{
"skill_hash": "a3f8b2c1",
"extracted_at": "2026-04-03T10:00:00Z",
"rules": [
"提交报销单前必须询问用户确认,用户拒绝时终止流程",
"金额字段只接受正数,为0或负数时提示用户重新输入",
"..."
]
}
使用逻辑:
ini
执行开始前:
计算当前 SKILL.md 的 MD5 → current_hash
读取 rules.cache.json 中的 skill_hash → cached_hash
if current_hash == cached_hash:
直接加载 rules 列表,跳过规则提炼(节省 1-2 分钟)
else:
重新提炼规则,完成后写入新的 rules.cache.json
这一步本身不复杂。MD5 的计算是确定性的------相同的文件内容总是产生相同的哈希值。哈希命中意味着文件没有改动,规则列表可以直接复用。
三、第二层缓存:用例缓存(cases.cache.json)
规则缓存解决的是"规则没变就不用重新提炼"。但还有第二层:规则没变 + 已经设计过用例,那用例也不用重设计。
json
{
"rules_hash": "a3f8b2c1",
"designed_at": "2026-04-03T10:05:00Z",
"mode": "quick",
"evals": [/* 完整的用例列表 */]
}
rules_hash 和 rules.cache.json 里的 skill_hash 是同一个值------都是生成这套用例时的 SKILL.md MD5。
使用逻辑:
ini
cases 步骤启动时:
计算当前 SKILL.md 的 MD5 → current_hash
读取 cases.cache.json 中的 rules_hash → cached_hash
if current_hash == cached_hash:
直接加载 evals 列表,跳过用例设计(节省 5-10 分钟)
else:
重新设计用例,完成后写入新的 cases.cache.json
两层缓存加在一起效果显著:规则缓存命中可跳过提炼(节省 1-2 分钟);用例缓存也命中时直接切换到 regression 工作流(5-7 分钟),跳过整个用例设计和 quick 双轮执行。节省的部分全在规则提炼和用例设计阶段。
四、哈希比对如何工作
实现上,MD5 计算是一行命令:
python
python3 -c "import hashlib,sys; print(hashlib.md5(open(sys.argv[1],'rb').read()).hexdigest())" /path/to/SKILL.md
输出是 32 位十六进制字符串(例如 a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5)。
在缓存文件里只存前 8 位就够了------用于展示时比较直观(a3f8b2c1),发生碰撞的概率可以忽略。
为什么 MD5 够用?
这里不需要密码学意义上的安全性,只需要「内容指纹」------能够可靠地检测文件是否发生了改变。MD5 在这个用途上完全足够,而且比 SHA-256 更短、更便于在 JSON 里展示和人工比对。
为什么不用文件修改时间?
修改时间(mtime)不可靠:复制文件、某些编辑器的保存行为、时区差异,都可能改变 mtime 而不改变内容。MD5 只看内容本身,跟文件系统元数据无关。
五、意外的副产品:状态感知
设计两层缓存时,我的目标是节省时间。但实现完之后发现了一个没有预期的副产品:
缓存文件是系统历史状态的快照。
rules.cache.json 存储的是「上次执行时 SKILL.md 是什么状态」,cases.cache.json 存储的是「上次设计用例时 SKILL.md 是什么状态」。把这两个文件和当前 SKILL.md 对比,可以得出四种不同的情况:
| 状态 | 含义 |
|---|---|
| rules.cache.json 不存在 | 从未测评过这个 Skill |
| 缓存存在,但 hash 不匹配 | SKILL.md 有改动(规则可能变了) |
| 缓存命中,cases.cache.json 也存在 | 规则和用例都没变,已有完整基准 |
| 缓存命中,cases.cache.json 不存在 | 规则没变,但还没设计过用例 |
这四种状态,对应四种截然不同的「此刻你需要什么测评」的答案。
六、四种状态 → 四种推断
状态感知的最大价值是:系统不再需要问你「要跑什么工作流」,它自己能推断出来。
状态 1:rules.cache.json 不存在
推断:quick(首次测评,需要完整流程)
原因:从未执行过,没有任何基准,需要设计用例再执行
bash
状态 2:hash 不匹配(SKILL.md 有变更)
推断:smoke(规则可能变了,先快速验证核心路径没崩)
原因:规则变了,之前的 Golden Set 可能不再反映当前逻辑。
smoke 比直接跑 regression 更谨慎------先用 4-5 个新用例快速验证,
确认没有断崖式崩溃,再考虑是否需要完整的 quick/standard
python
状态 3:hash 匹配 + cases.cache.json 存在
推断:regression(规则和用例都没变,直接跑已有 Golden Set)
原因:SKILL.md 没动,之前设计的用例仍然有效,
跑 regression 就能验证最近的改动没有破坏已有行为
bash
状态 4:hash 匹配 + cases.cache.json 不存在
推断:quick(规则没变,但还没有用例)
原因:规则可以直接加载,省去规则提炼,但还需要设计用例才能执行
这个推断逻辑是基于「我知道你做过什么」来判断「你现在需要什么」------不需要你解释背景,不需要你记住工作流名字,只需要说「测评 em-reimbursement-v3」。
时间影响(典型场景,quick 模式基准):
| 场景 | 无缓存 | 有缓存(规则命中) | 有缓存(规则+用例命中) |
|---|---|---|---|
| SKILL.md 未变,用例已有 | ~20min(quick) | - | 5-10min(regression) |
| SKILL.md 未变,无用例 | ~20min | ~12min | - |
| SKILL.md 有改动 | ~20min(quick) | - | 5-7min(smoke,快速验证) |
七、确认提示:推断不是强制,是建议
系统推断出工作流后,不会直接开跑。它会先输出一个状态确认:
javascript
✅ 已找到被测 Skill:em-reimbursement-v3
路径:~/.claude/skills/em-reimbursement-v3/SKILL.md
📊 状态检测:
规则缓存:命中(hash: a3f8b2c1,规则 14 条)
用例缓存:命中(共 10 个用例,设计于 2026-04-03)
→ 推荐工作流:regression
原因:SKILL.md 未变更,已有 Golden Set,直接验证现有用例即可
工具链:executor → grader → report
预计时间:5-10 分钟
直接回复「开始」或不回复则 30 秒后自动开始。
如需调整,说:「full」「quick」「smoke」「regression」「lint」
这个设计有两个考虑:
透明:让用户知道系统做了什么推断,依据是什么,避免黑盒。如果推断结果看起来不对(比如你明明改了 SKILL.md 但缓存显示「命中」),用户能立刻发现并纠正。
可覆盖:推断是建议,不是约束。说「full」立刻切换到完整流程,说「smoke」就跑冒烟。一个词就能覆盖推断结果,不需要解释原因。
30 秒超时自动开始是为了 OpenClaw 场景(飞书消息触发)------异步场景下用户不一定实时看消息,推断通过就自动执行,减少等待。
八、诚实面对:缓存会误导吗
MD5 碰撞:理论上存在两个不同内容产生相同 MD5 的可能,但概率极低(约 2⁻¹²⁸),对这个用途可以完全忽略。
缓存的内容是「上次提炼的规则」,不是「SKILL.md 的未来变更」
这是缓存最容易被误用的地方。缓存命中意味着「SKILL.md 内容和上次提炼时相同」,不意味着「提炼出来的规则是正确的」。如果上次规则提炼本身就有遗漏(比如 LLM 漏掉了某条隐含规则),那这个错误会被缓存固化下来,每次命中都会用到错误的规则列表。
处理方式:强制刷新。说「full 测评」或「重新提炼规则」,系统会忽略缓存,重新走提炼流程。
cases.cache.json 只在 quick 模式的用例下有意义
如果上次是 smoke 模式(只设计了 4-5 个用例),cases.cache.json 里只有 4-5 个用例。如果下次推断是 quick(需要 8-10 个用例)但命中了 smoke 的缓存,会用不足的用例集跑 quick------这是个潜在的误判。
目前的处理是在 cases.cache.json 里记录 mode 字段,命中时检查 mode 是否和当前需求匹配;不匹配则重新设计。
缓存文件的更新时机
rules.cache.json 在规则提炼完成后立即写入,cases.cache.json 在用例设计完成后立即写入。如果执行过程中途中断(规则提炼到一半崩了),不会写入不完整的缓存文件,下次重新触发时直接重跑。这是文件写入原子性带来的自然保护。
两层缓存带来的最大收益,不是节省了那几分钟规则提炼的时间,而是让系统从「问你要什么」变成了「我知道你需要什么」。这个变化让「测评 xxx」成为了一个真正无歧义的入口------不管你是第一次测评、迭代改了几行规则、还是准备正式提测,系统都能根据当前状态给出一个合理的默认选择,而你只需要确认或覆盖。