上一篇把确定性逻辑从 Prompt 中拆出来,解决的是"什么该由代码负责"。本文继续看工具边界:当主流程已经足够清楚之后,哪些能力应该保持为可组合工具,而不是永远绑在一条从头到尾的管道里。
这篇不是讲"我们怎么拆工具",而是讲一个更通用的问题:什么时候应该把一条固定流程拆成可组合工具箱。
SkillSentry 原来的单体管道只有一种使用方式:从头跑到尾。这个设计能保证完整性,但也带来一个问题:日常开发里最常见的需求并不是"每次全量重测",而是"我只改了一条规则,能不能只验证受影响的用例"。
工具箱化之后,最大的收获不是速度,而是自然浮现出一个原本不可能存在的工作流:regression。它跳过用例设计,直接复用已有 Golden Set 和历史失败用例,正好对应日常修复后的验证需求。
本文的核心方法是:把管道拆成五个原子能力(sentry-static / cases / executor / grader-report / sentry-report),用 JSON 文件而不是函数调用传递状态;薄编排器只负责调度顺序,不承载业务逻辑。
核心结论有两个:
text
1. 工具箱化最大的收获不是速度,而是让 regression 这种按需工作流变得可能。
2. 在 LLM orchestration 里,工具间通过文件而不是函数调用传递状态,反而更利于恢复、替换和检查。
- 一、先说清楚问题:不是「慢」,是「绑死了」
- 二、拆分的前提:先定清楚每个工具的边界
- 三、原子工具:为什么这样拆
- 四、架构涌现:一个我没设计过的工作流
- [五、JSON 文件是合约:LLM 工具链的接口设计](#五、JSON 文件是合约:LLM 工具链的接口设计 "#%E4%BA%94json-%E6%96%87%E4%BB%B6%E6%98%AF%E5%90%88%E7%BA%A6llm-%E5%B7%A5%E5%85%B7%E9%93%BE%E7%9A%84%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1")
- 六、编排器越薄越好
- 七、改造前后对比
- 八、诚实面对:工具箱化解决不了什么
- 九、工具箱化之后的新问题:可分发性
本文最重要的发现不是"测评变快了",而是:把单体管道拆分成原子工具后,自然涌现出了一个我们从来没有专门设计过的工作流------它不可能在单体管道时代存在,却在工具组合之后自然出现了。这个发现比所有性能数据都更值得记录。
一、先说清楚问题:不是「慢」,是「绑死了」
SkillSentry 最初是作为一个完整测评管道设计的:
css
规则提炼 → 触发率评估 → 用例设计 → [L1 执行] → [L2 Grader] → [L3 Comparator] → 报告
很长一段时间里,我以为主要问题是「太慢了」,于是做了一轮性能优化:MD5 哈希缓存(跳过规则重提炼)、mega-batch 并行、Grader 批量调用、上下文压缩......优化完,quick 模式从 30 分钟降到了 8-10 分钟(mcp_based)/ 15-20 分钟(其他)。
但用了一段时间之后,我发现更本质的问题不是速度,而是整个系统只有一种使用方式。
三个高频场景,都被这个单一流程卡住了:
场景一(最常见):改了报销单提交逻辑里的一个字段映射,只想确认「主流程没有崩」。 系统的回答是:重新提炼规则 → 重新设计 10 个用例 → 每个用例跑 2 次 × 双侧 → Grader → 报告。20 分钟。 我需要的是:跑上次已经验证过的 5 个核心用例,5 分钟出结论。
场景二:刚写完一个新 Skill 的 description,想先确认触发率够不够,再决定要不要写功能规则。 系统没有这个能力,只能触发全量流程,等 20 分钟换来一个我根本不需要的完整报告。
场景三:Skill 上线前想做一次静态检查,确认 HiL 节点没有漏、规则不冗余。 同样不行。
这三个场景的共同症结不是「流程太慢」,而是:「工具」和「流程」被死死绑定在一起了------有没有 MCP 连接、是不是要跑完整 Grader、要不要出 HTML 报告,这些事情在用例设计那一步就已经被固定下来,用户没有任何选择空间。
工具箱化要解决的是这个问题,不只是速度。
二、拆分的前提:先定清楚每个工具的边界
「工具箱化」不是把一个大文件切成几个小文件。在动手之前,我强迫自己回答一个问题:
每个工具的输入是什么、输出是什么、它对外部状态有什么依赖?
这一步花了比写代码更多的时间,因为不想清楚这件事,拆出来的工具会悄悄依赖彼此的内部状态,表面上独立,实际上还是一条串联管道。
整理之后,得到了这张表:
| 当前工具 | 核心职责 | 输入 | 输出 | 能否独立运行 |
|---|---|---|---|---|
| sentry-static | 静态检查 + 触发检查 | SKILL.md / description | static-check / trigger 结果 | ✅ 完全独立 |
| cases | 用例设计 | rules + inputs/ | evals.json | ✅ 独立 |
| executor | 并行执行用例 | evals.json | transcript + timing | ✅ 依赖 evals.json |
| grader-report | 断言评审 + 汇总报告 + 发布建议 | transcripts / grading 输入 | grading-summary + report | ✅ 依赖执行结果 |
| sentry-report | 已有 grading 后重新出报告 | grading-summary / session | report.html | ✅ 特殊场景独立使用 |
这张表有一个关键列:能否独立运行 。每个工具的依赖必须是文件(evals.json、grading.json),而不是内存里的某个对象或另一个工具的运行时状态。这个约束决定了后来工具间数据接口的设计方向。
三、原子工具:为什么这样拆
拆分之后,每个工具都是一个独立的 本地 Skill,有自己的 description(用于触发检测)。
早期版本讨论的是"五个原子工具",稳定实现已经把其中一部分合并为更清晰的入口。保留下来的设计原则是:工具边界必须按输入/输出文件切,而不是按"看起来像一个步骤"的口头描述切。
少拆(比如把 cases + executor 合并)的问题:合并之后「只设计用例、不执行」这个场景就消失了------用例设计完了先 review、再决定要不要跑,这个中间状态就没了。
多拆(比如把评分和报告长期拆成两个常规步骤)的问题:报告强依赖评分结果,分开会增加一次 subagent 调度和一套接口维护开销。当前版本的主流程因此收敛为 grader-report;独立 sentry-report 只保留给"已有 grading,重新出报告"的特殊场景。
当前工具,按「是否需要真实执行」分成两类:
不需要执行任何工具的(纯分析/生成):
sentry-static:静态读 SKILL.md,覆盖 description、HiL、复杂度、冗余规则、规则可测试性和触发检查,30 秒到 2 分钟cases:读规则 + inputs/,双源合流设计用例,标注断言强度分级,输出 evals.json,5-10 分钟
需要执行的(启动 subagent):
executor:读 evals.json,并行启动 with_skill / without_skill(按当前 baseline 规则决定是否需要 without_skill),输出 transcript + timing,10-20 分钟grader-report:读执行结果,完成断言评审、汇总指标、生成报告和发布建议sentry-report:读已有 grading/session,重新生成报告,不作为主 pipeline 的常规评分步骤
特别说明 sentry-static:这个工具的最大价值是它不需要任何 MCP 连接。Skill 刚写完、还没有配置任何工具调用的情况下,30 秒就能发现 HiL 漏洞。最典型的漏洞:写了「提交前询问用户确认」,但没写「用户拒绝时怎么处理」------这是 HiL 检查项,静态分析就能发现,比跑测试用例快很多。
四、架构涌现:一个我没设计过的工作流
这是全文最重要的一节,也是我在动手拆之前没有预料到的。
拆分完成后,我在梳理「各种工具可以组成哪些工作流」时,发现了一个之前根本不存在的组合:
跳过 cases 设计,直接用 inputs/ 里已有的用例,跑 executor → grader-report。
这就是 regression 工作流。
为什么说它是「涌现」的,而不是「设计」的?
在单体架构里,这个工作流物理上不可能存在。SkillSentry 的主流程是:规则提炼 → 用例设计 → 执行 → 评审 → 报告,这五步是一条强制串联的流水线。没有任何地方可以插入「我已经有用例了,跳过设计这步」的选项。即使想到了这个需求,也无从实现。
拆成工具箱之后,工具是独立的,流程是编排器决定的,「跳过某个工具」变成了一个正常的选项。regression 工作流是架构本身给出的,不是我主动设计的。
它的价值在哪里?
看一下这张频率分布表:
| 场景 | 触发频率 | 改造前能用的工作流 | 改造后 |
|---|---|---|---|
| 修了一条规则,验证没有崩 | 最高频(每天多次) | quick(20min,超杀) | regression(5-10min) |
| 迭代完成,准备提测 | 中频(每周数次) | quick(合适) | quick(不变) |
| 正式发布前全量验证 | 低频(每次发布) | full(合适) | full(不变) |
regression 工作流命中的是最高频的使用场景,而这个场景在单体架构里一直是被 quick 工作流(过度设计的流程)强行兜着的。
这个发现让我意识到:工具箱化的真正收益,不是给现有工作流提速,而是让之前被压制的工作流重新浮出来。单体架构把「测评」变成了一个不可分割的动作,工具箱化把它还原成了一组可以独立使用的能力。
五、JSON 文件是合约:LLM 工具链的接口设计
在设计工具间的数据传递方式时,我自然而然地选择了文件------工具 A 把结果写到 evals.json,工具 B 读取 evals.json。
做完之后才意识到,这个选择和传统软件工程的直觉是相反的。
传统软件工程的建议是:优先用函数调用(参数类型安全、无文件 I/O 开销),文件耦合是反模式(两个模块通过临时文件传递状态,耦合不可见、难以测试)。
但对于 LLM 工具链,这个直觉在三个关键点上失效:
① LLM subagent 之间没有共享内存
传统函数调用之所以好用,是因为调用方和被调用方在同一个进程里,可以传递内存引用。LLM subagent 不是这样的------每个 subagent 是一个独立的 API 调用,没有共享堆,「把对象引用传给另一个 subagent」这个操作根本不存在。你能传递的只有可序列化的数据。选择「函数调用」还是「文件」,在 LLM orchestration 里,本质上是在选择「直接 inline 所有数据」还是「用文件路径作为引用」。文件路径作为引用,反而是更轻量的方式。
② 文件让失败可恢复
一次 executor 批次中途崩了(网络超时、MCP server 不稳定),已完成的 eval 的 transcript.md 还在磁盘上。重新触发时,检查文件是否存在,存在则跳过,只跑未完成的部分。
用函数调用 + 内存传递,这个能力根本实现不了------上一次的运行结果在 subagent 退出时已经消失。
③ 文件让工具可替换
想换一个更快的 Grader 实现?只需要保证它输出的 grading.json 格式不变,其他工具完全不需要修改。
想加一个新工具(比如专门做「regression 对比」的工具)?只需要它能读 grading.json,往 workspace 目录写一个新 JSON 文件,就可以接入工作流。
这是「接口稳定性」在 LLM orchestration 里的具体形态:工具间的接口是文件格式,而不是函数签名。改变函数签名需要同时修改调用方;改变文件格式,只要旧格式的读者和新格式的写者都更新,过渡是可控的。
④ 文件让状态可检查
测评跑到一半,我怎么知道哪些 eval 已经完成了?哪些断言 failed?直接 cat sessions/xxx/eval-3/grading.json 就知道了,不需要 LLM 告诉我。
这是 LLM 工具链里一个经常被忽视的可观测性问题:LLM 的上下文不是永久存储,对话结束了状态就消失了。用文件持久化中间状态,是 LLM 工具链里「调试友好」的必要条件。
工具间的文件接口如下,每个文件的格式就是工具间的合约------写的一方不能随意改字段,读的一方也知道能依赖哪些字段:
sql
rules.cache.json ← SkillSentry 写,cases 步骤读
cases.cache.json ← cases 步骤写,复用时 executor 间接受益
evals.json ← cases 步骤写,executor 读
timing_with.json ← executor 写,grader-report 读
timing_without.json ← executor 写,grader-report 读
grading.json ← grader-report 写,sentry-report 可在重出报告时读取
trigger_eval.json ← sentry-static 写,grader-report 读(full 模式)
comparison.json ← Comparator/Analyzer 写,grader-report 汇总读取(standard/full 模式)
eval_environment.json ← executor 写(并行率审计)
这张图有一个值得注意的地方:没有任何一个工具既是某个文件的读者又是另一个文件的写者(除了 SkillSentry 编排器本身)。每个工具的依赖链是单向的,没有循环。这不是意外,是在定义工具边界时强制保证的。
一句话总结 :在 LLM 工具链中,文件接口比函数调用更可靠,因为它解决了三个函数调用解决不了的问题:可恢复 (系统崩溃后可从文件继续)、可替换 (任何工具都能读写同一格式的文件)、可检查(文件内容随时可人工核验)。
六、编排器越薄越好
改造后,SkillSentry 主 SKILL.md 从 222 行精简到 154 行,execution-phases.md 从 600 行精简到 140 行。
被删掉的内容,全部迁移到了各工具自己的 SKILL.md 里------subagent steps 上限、transcript 双分离格式、Grader 调用规则、without_skill 早退指令......这些执行细节,和编排器没有任何关系。
编排器剩下的是:
markdown
1. 识别用户意图 → 映射到工作流
2. 初始化工作目录 + 规则缓存检查
3. 按顺序触发工具,传入 workspace_dir
4. 工具通过文件传递状态(编排器不关心文件内容)
这里有一个反直觉的地方:编排器越薄,工具箱反而越好用。
道理是这样的:如果编排器里包含了工具的执行细节(比如「Grader 每次必须处理 ≥2 个用例」),那每次 Grader 的规则有变化,都要改编排器。但 Grader 的用法只和 Grader 自己有关,应该封装在 agents/grader.md 里,不应该泄漏到编排器里。
当编排器只知道「调用 Grader」而不知道「Grader 具体怎么工作」,工具的内部实现就可以独立演进,不会每次改工具都要同时改编排器。
这和微服务设计里「API Gateway 不应该包含业务逻辑」是同一个原则,只是在 LLM orchestration 的语境下重新表述了一遍。
七、改造前后对比
时间变化(典型场景)
| 场景 | 改造前 | 改造后 | 减少 |
|---|---|---|---|
| 修了一条规则,验证主流程 | 20-30 分钟(quick,杀鸡用牛刀) | 5-10 分钟(regression) | ~70% |
| 开发迭代冒烟验证 | 20-30 分钟(只有 quick) | 5-7 分钟(smoke) | ~75% |
| 检查 SKILL.md 写法质量 | 不可能单独触发 | 30 秒(check 静态模式) | 全新能力 |
| 验证 description 触发准确性 | 不可能单独触发 | 2 分钟(check 触发率模式) | 全新能力 |
| 用例设计完先 review 再决定 | 不可能中途停 | cases 步骤单独运行 | 全新能力 |
| 正式发布前全量验证 | 45 分钟+ | 45 分钟+(无变化) | --- |
full 测评的时间没有变化------工具箱化优化的是「不必要的全量流程」,不是全量流程本身的速度。
文件体积变化
| 文件 | 改造前 | 改造后 |
|---|---|---|
SkillSentry/SKILL.md |
222 行 | 154 行(-31%) |
execution-phases.md |
600+ 行 | 140 行(-77%) |
| 各 sentry-* 工具 | 不存在 | 5 个,共约 500 行 |
execution-phases.md 变化最大。原来它承担了「Phase 1 触发率规范 + Phase 3 用例设计规范 + Phase 4 执行规范 + Phase 5 报告规范」四件事,600 行里任何一块的改动都需要理解整个文件的上下文。现在它只做一件事:定义工具间的数据接口,140 行,改任何一个 JSON 字段,影响范围一目了然。
八、诚实面对:工具箱化解决不了什么
工具间的接口变更是新的风险
用文件做接口,好处在前文已经说了。代价是:接口变更不会在编译期报错。如果 executor 步骤修改了 timing_with.json 的字段名,grader-report 在读取时会静默地得到 null,不会有任何报错,直到出了报告才发现数据缺失。
这个问题在传统软件里由类型系统和编译器解决,在 LLM 工具链里没有对应的机制,只能靠文档(execution-phases.md 里的接口定义)和人工纪律。
单独调用工具时,前置条件需要用户自己知道
executor 步骤需要先有 evals.json,grader-report 需要先有执行结果,sentry-report 需要先有已有 grading/session。如果用户不清楚这些前置条件,会得到令人困惑的「文件不存在」错误,而不是「你需要先运行 cases 步骤」这样的友好提示。目前靠 description 字段做说明,但没有自动的前置条件检查。
regression 工作流的正确性依赖 Golden Set 的质量
regression 工作流跳过了用例设计,假设 inputs/ 里的 Golden Set 是足够好的。如果 Golden Set 本身有盲区(比如缺少某类边界用例),regression 通过了并不意味着 Skill 质量没有问题,只意味着 Golden Set 里的用例都通过了。Golden Set 的维护成为新的质量保证环节,这个责任从工具本身转移到了使用者身上。
LLM 的串行本能没有被根本解决
工具箱化改变了架构,没有改变 LLM 的执行倾向。executor 步骤里要求「with_skill 和 without_skill 必须在同一消息中并行发出」,这条规则依赖 LLM 自觉遵守。并行度审计(每批完成后计算 start_gap)让违规可见,但不能主动阻止串行。真正可靠的并行需要平台层的原生并发支持。
工具箱化给 SkillSentry 带来的最大价值,不是速度提升,而是让测评系统从「一个动作」变成了「一组能力」。这个变化让之前被压制的 regression 工作流有了存在的空间------而它恰好是日常开发中最需要的那一个。
九、工具箱化之后的新问题:可分发性
完成工具箱化之后我意识到,架构问题只解决了一半------工具能不能被别人用,是第二个关键问题。
工具箱化完成后,我系统地梳理了一次「普通人拿到这个工具,第一次使用的完整开销」。这个梳理过程本身就发现了几个问题------它们和架构无关,但对实际可用性的影响不小。
问题一:安装需要克隆 6 个仓库
工具箱化之后,独立 Skill 是独立目录。用户要装 SkillSentry,需要分别克隆 6 个目录到正确位置。这在开发者自己用的时候不是问题,但如果要分享给别人,门槛就出来了。
解法是把 sentry-* 的 SKILL.md 全部放进 SkillSentry 主仓库的 tools/ 子目录,再写一个安装脚本(install.sh + install.ps1)负责把各工具部署到正确位置。用户现在的安装步骤变成:
bash
获取 SkillSentry 发布包后进入目录
cd SkillSentry && bash install.sh
脚本会自动检测系统里装了 本地编码助手 还是 OpenCode(或两者都有),部署到对应路径,最后输出验证结果。一次克隆,覆盖两个平台。
这个改动的工程量很小,但对「能不能传播出去」的影响是质变。工具再好,安装需要 6 步,大多数人会在第 2 步就放弃。
问题二:mcp_based Skill 测评可能出假阴性,但用户不知道
executor 步骤执行用例时会直接调用被测 Skill 依赖的 MCP 工具。如果 MCP server 没有启动,工具调用会报错,transcript 里一堆 error,Grader 会判 FAIL。
这个 FAIL 不是 Skill 的问题,是环境的问题。但对用户来说,收到的报告里写着「通过率 30%,建议修复」------他们会去改 Skill,而不是去查环境配置。这是测评系统最危险的一种失效模式:结论看起来可信,但结论的来源是污染的。
修复是在执行工作流前加一步 MCP 可用性预检:列出被测 Skill 引用的工具名,对照当前可用工具,不匹配时明确告警并让用户选择继续还是中止。这一步把「静默失败」变成了「可见失败」,代价是多一次工具调用,收益是结论的可信度。
问题三:使用开销不透明
用户说「测评 xxx」之前,不知道这次会消耗多少 token。quick 模式 10 个用例 × 2 次运行 × 双侧 = 20 个 subagent,单次消耗约 5-10 万 token。如果用的是按量统计的运行环境,这是真实的资源,应该在开始前告知。
修复很直接:在推断工作流的确认提示里加一行 Token 预估。信息本来就有(工作流决定了用例数和运行次数),只是之前没有输出给用户看。
这三个问题有一个共同的特征:它们只有在真实分发、真实使用的场景下才会浮现。 自己用的时候,知道 MCP 该怎么配,知道 6 个目录怎么放,知道这次大概跑多久------这些背景知识是隐含在脑子里的,不会感觉到有任何门槛。
工具设计从「我能用」到「别人也能用」,需要的不是更多功能,而是把这些隐含的前提条件一条一条显式化。
后续演进:工具箱化之后还要收敛契约
工具箱化解决了"能不能按需组合"的问题,但它不是终点。后续架构继续往两个方向收敛:
| 工具箱化阶段解决的问题 | 后续继续补强的方向 |
|---|---|
| 原子工具可以按需组合 | Pipeline 状态机保证步骤不可跳过 |
| 文件作为步骤间契约 | active-pipeline.json 支持断点续跑 |
| 静态检查、触发率、综合建议逐步合并 | 稳定入口收敛为 sentry-static |
| Grader 作为独立评审者 | 主流程收敛为 grader-report,评分和报告一次完成 |
| CI 接入停留在概念层 | sentry_ci.py 读取结构化 JSON 产物做门禁 |
核心思路没变:工具箱化、按需组合、文件作为步骤间契约。变化的是执行保障机制------从"依赖编排器记忆"升级为"状态机强制执行",解决长对话中 AI 跳步的结构性问题。