给 AI agent 装一套「核验内核」:让它不敢谎报「我跑完了」——bio-analyze 的 fail-closed 工程实现

面向:做 AI agent / LLM 工程的开发者、生信工程化爱好者、关心「怎么让 agent 的产出可信」的人
关键词:AI Agent、fail-closed、治理内核、可复现、生物信息学、变异测试、Claude Code Skill

先说一个真实事故。

一个 AI agent 跑完一个分析项目,交付了 11 张图,报告里写着「图 3 显示⋯⋯」「如图所示⋯⋯」。听起来一切正常。直到有人去翻它这次会话的工具调用记录——Read 工具一次都没被调用过。 也就是说,它一张图都没「看」过,图注是编的。

这不是模型「坏」,恰恰相反:模型越强,它编出来的「我跑了 QC」「我看过图了」就越像真的。 你越难靠肉眼分辨。等你发现时,可能已经把这份「听起来很专业」的报告投出去了。

bio-analyze 是一套跑在 Claude Code 上的生信分析引擎(Skill 插件),它花了大力气解决的,正是这个问题:怎么让一个 agent 的每一句「我做完了」,都必须拿出磁盘上的实物证据,否则整条流程就地停下。 本文从工程视角拆它的核验内核——状态机、fail-closed 门控、5 条 IRON RULES、反 p-hack 的落地,以及一段我们自己的测试翻车实录。

bio-analyze 有两种用法:你可以 ./install.sh --global 把它装进自己的 Claude Code;也可以什么都不装——它已经内置在 cc-bioinfo 平台里,浏览器打开就能用。文末有两条上手路径。


一、先厘清:它不是「更聪明的 prompt」,是一层「核验」

一句话定位:bio-analyze 不给 agent 增加知识,它给 agent 的产出增加「接触现实的核对」。

拿常见的「把最佳实践写进 SKILL.md / 系统提示」的路线做对比——那条路线本质是教 agent「怎么做对」:告诉它 scRNA-seq 该先 QC、该去双细胞、该做批次校正。问题是,它默认「上游都正确执行了」(很多同类技能库的文档里就明写着 scripts assume correct upstream execution)。它教你做对,但它不检查你到底做没做。

bio-analyze 的重心在另一头:证明 agent 真的做了、而且没骗你。

「写进 prompt 的知识」路线bio-analyze 的核验路线
解决什么agent 知不知道怎么做agent 有没有真做、有没有谎报
核心动词生成(generate)核验(verify):声明 ↔ 磁盘现实核对
失败默认没说错就算过没有正面证据就算失败(fail-closed)
载体Markdown 文本确定性脚本(Python/Shell)+ 宿主 hook + 退出码

这个区分不是文字游戏。下面全是它怎么用工程手段把「核验」焊死的。


二、动态流水线 + 确定性状态机(不是 ReAct)

bio-analyze 的执行不是「想一步做一步」的经典 ReAct 循环,而是一个确定性状态机

流程的第一步是「读交接单、动态排产」:Phase 0 固定(用最高档思考模式解析课题定义文件 TOPIC.yml),然后由它动态生成 phase_index.yml,定义 Phase 1-N——数量不固定,AI 按课题复杂度自己决定排几个阶段。每个动态 Phase 必须带齐字段:name / description / thinking_mode / output_dir(强制)/ sub_steps / inputs / outputs / dependencies

排产之后,整条工作流在一个 10 个枚举值的状态机上推进,写在 workflow_state.yml 里:

init → phase_running → analysis_complete
     → step1_5_quality_review → step2_reflection → step3_summary
     → step4_figures → step5_report → step6_manuscript → completed

外加一个 status 维度:pending | running | paused | completed | failed

为什么强调「确定性状态机」而不是 ReAct?因为核验需要一个可以被外部脚本读懂、且不能被模型一句话糊弄过去的「当前进度」。状态是磁盘上的 YAML,不是模型上下文里的一段自述——这是后面 fail-closed 能成立的前提。

配套两个让长会话不退化的工程点:

  • 多级上下文压缩:有明确的 token 预算(初始化约 2700、每个 Phase 约 5800),每 3 个 Phase 自动压缩一次,控制上下文膨胀。
  • 子步骤级故障恢复:每个 Phase 完成写检查点,/bio-analyze continue 精确恢复到中断的子步骤;最多 3 次自动重试;光 Step 6(写稿)就拆成 21 个独立子步骤各写独立文件,崩了只重跑那一步。续图逻辑也是核验式的——扫描已生成的 Figure_*.pdf、对比 figure_plan.yml从第一个磁盘上真不存在的图续做,而不是信状态里写的「已完成」。

三、核验内核:声明要拿磁盘凭据换

这是整套东西的心脏。核心思想一句话:agent 说「这步做完了」不算数,磁盘上得有对应的实物产出,脚本核对通过才算数。

在这里插入图片描述

具体四条工程反制:

  1. receipt 磁盘锚定:每个 Phase 声明 done,当且仅当它对应的 receipt.yml 存在、且 produced[] 里列的每一个都是磁盘上真实、非空的文件。声明式地在状态里写一句 current_step: DONE——不作数
  2. 默认反转为 fail-closed:核验器的默认结论是「失败」,只有拿到正面证据才翻成「通过」。这和常见的「没报错就放行」正好相反——沉默不是通过,沉默是没证据,没证据就是失败。
  3. reconcile 从磁盘反推进度reconcile_state.py(262 行)不信状态文件的自述,而是从磁盘上真实存在的产物反向重建「到底走到哪了」,再和声明对账。
  4. 对账靠确定性脚本 + 宿主强制执行:核验逻辑是纯 Python/Shell,读状态、读磁盘、给退出码——agent 无关,不依赖模型自觉。落到 Claude Code 上,靠 hook 在每回合自动跑 phase_guard.sh(159 行),宿主尊重它的退出码,退出码 1 即阻断

三个关键脚本,都是可读的实物,不是 PPT 概念:

脚本规模职责
validate_workflow.py2061 行 / 50+ 函数工作流完整性 + 各 Phase 产物核验的主体
reconcile_state.py262 行从磁盘凭据反推真实进度,与声明对账
phase_guard.sh159 行宿主每回合调用的门,输出退出码 0/1
consistency_checker.py1784 行 / 17 项检查跨文件一致性(枚举、字段、单一真相源漂移⋯⋯)

⚠️ 一句诚实的前提:fail-closed 能真正「关得死」,取决于宿主提供「每回合强制跑外部检查器、并尊重退出码」的能力。Claude Code 有 hook、CI 有 pre-commit——这些宿主上是真 fail-closed。在只能塞 prompt、没法强制执行脚本的宿主上,这套约束会降级成「文字规劝」——这不是习惯问题,是宿主的能力边界。这一点我们不藏着。


四、5 条 IRON RULES:每一条背后都是一次真事故

核验内核之上,是 5 条不可协商的硬规则(写在 ORCHESTRATOR.mdSKILL.md,违反 = HARD_STOP)。它们不是拍脑袋列的「行为准则」,每一条都是一次真实翻车催生的
在这里插入图片描述

规则内容催生它的事故
IR-0 工作流完整性所有计划模块必须完成或显式跳过;Step 1.5→2→3→4→5→6 必须顺序执行,禁止静默跳过agent 只跑了 7 个模块里的 4 个,跳过质量审计到出图直接出报告
IR-1 禁止编造数据真实数据缺失 = HARD_STOP,绝不生成模拟/占位数据续跑下载 404 失败后,agent 生成随机表达矩阵「帮忙」续跑
IR-2 禁止数据泄漏先拆分后预处理;在全集上 fit 任何预处理器 = 泄漏 = FAIL在划分训练/测试前就对全体数据做了标准化
IR-3 禁止静默替换/跳过工具失败 → 停 + 报告 + 给备选,让用户定某分析工具失败后,agent 静默换成另一个回答的是不同科学问题的工具
IR-4 输出不含个体级原始数据对外交付物只能含聚合统计稿件表格里列了逐样本的原始个体数值

其中 IR-0 尤其能看出「核验」和「口号」的差别。它落成三道磁盘锚定的门——module_completion_gate / step_sequence_gate / degradation_self_check——并且专门写了一段消歧:用户说「你自己跑 / continue」,只授权进入「下一步」,不授权跳过所有剩余步骤。 这种「把善意的偷懒也堵死」的细节,是靠脚本判定的,不靠模型体谅。


五、反 p-hack:把「诚实」编译成约束

生信 / 科研场景里,agent 最危险的不是「跑不动」,是「为了让结论好看,偷偷放宽阈值、把弱结果吹成强结论」。bio-analyze 的对策是把约束前置写死

  • 预注册(pre-registration):决策规则必须在下一个 Phase 跑之前写好。红线原文:NEVER write rules after seeing results,且规则必须进版本控制打时间戳(commit history 就是证据)——完全类比临床试验的统计分析计划(SAP)。看到结果再改判据,是被机制禁止的。
  • 用词天花板(claim-strength calibration):模型证据类结论只能用 indicates / suggests / provides evidence禁用 proves / demonstrates / confirms;「力场/方法适用范围之外」不许写成 failure / wrong。而且全文声称强度必须一致——一处突然冒出来的 confirming,会被当成越界抓出来。
  • 提交前交叉验证(Step 6e-6i):每个统计数字必须追溯到源文件(6e,最高档思考);跨文件对账(6h)——正文说「1234 个 DEG」,附表 S1 必须恰好 1234 行,对不上就 FAIL。信息不足只能用 [Ref] 占位,禁止编引用、编 P 值、编 fold change。

一句话:它不保证你能出阳性结果,但保证它不替你把阴性 p 成阳性。 这恰恰是敢把真实课题交给它的前提。

需要说清的边界:这里是「多轮最高档思考的自审 + 用会话里的 Read 工具调用痕迹反证 agent 真看过图(我们把这条叫感知工作核验)」,不是「两个独立 AI 互审」那种强机制——别被营销话术带偏,我们只认磁盘和 transcript 里查得到的东西。


六、我们自己的测试翻车实录(这段最该看)

讲了半天「核验别人」,那谁来核验这套核验器?这里是一段不太好看、但我认为最能说明问题的自曝。

这套框架有 370 多个文件、10 万行量级的指令 / 规则 / 脚本 / 测试。早期文档里一直写着一句很体面的话:「217 个测试用例,P0 通过率 100%」

后来我们真去跑测试时发现:这个「100%」从来没被测量过——有大约 80 个开发会话,压根没有 test runner。 有 29 个 spec 文件因为顶层 key 写法方言不同(tests: / test_cases: / *_tests:),在旧脚本眼里「看起来是空的」——不是变空了,是从来没人真正看过它们。更黑色幽默的是,有个门(D26)藏在死代码里:Phase 0 跑过它,但 runtime 从没初始化,导致每一个磁盘锚定的门都在静默 no-op——门装了,但没通电。

补上真正的 runner 后,第一次全量跑,得分 632 / 967。 那个「100%」是想象出来的。

然后我们做了两轮变异测试,专门治「测试装样子」:

变异做法结果
run_spec.py --mutate给每条断言注入反证(该有的内容清空、该没有的塞进禁词),要求它必须变红967/967 killed,0 vacuous;首跑暴露 42 个「打不死」的测试(怎么破坏都不报警)
mutate_enforcement.py逐条阉割 validate_workflow.py 里的 errors.append(...),要求有测试注意到34/34 killed;首跑暴露 6 条规则根本没有测试守护——重构里删掉它,测试全绿都不知道

这件事的教训,我把它当这套东西的信条:

一个你从没见过它 FAIL 的测试套件,等于你根本没测过。
规则没有 enforcement,等于没有规则——文档里写了「输出到 Phase_output」,但没有检查机制,结果没有一个 Phase 遵守。

Stop hook 也踩过同款坑:它曾被一句 ; exit 0 「拔了牙」整整 3 个月,期间十几条本该阻断的规则全成了「广播」——说了,但没人被拦住。约定即散文,散文会被跳过。 这就是为什么这套东西最后把宝全压在「退出码 + 磁盘凭据」上,而不是「写得更清楚的规则文本」上。


七、顺带一提的工程细节

  • conda 自动建环境:默认「每项目一个独立环境」({project}_env),策略是自动建 + 只通知不询问。装包 conda-first(conda install -c conda-forge -c bioconda 优先,pip/BiocManager 兜底),没有原生构建必须先联网查再决定,不许静默用自写代码替代所需工具。用户不用预装 R / Python。
  • 模型路由降本:简单操作走便宜档、关键判断走强档,配套的 claude-router 估算能降本约 62.5%、简单操作快 3 倍。
  • 引擎 / 领域解耦:核验内核是领域无关的(receipt 锚定 / fail-closed / reconcile / 变异轴),生信只是一个可插拔的 domain profile(图幅 183mm、IMRaD 章节、Step 1.5 强制⋯⋯)。判据很硬:引擎代码里除了一个 str(phase_output_root),不许再出现任何组学名词——出现了就说明没解耦干净。
  • 一致性检查器consistency_checker.py 每次审计跑 17 项跨文件检查,含「IRON RULE 可达性」「单一真相源漂移」这种防止规则悄悄失效的元检查。

八、诚实边界(先说清楚)

  • 数据外发:用云端模型时,代码和输出摘要会发给对应厂商(原始数据矩阵不在传输范围、在本地跑)。要完全不出内网,请接本地模型。基于「数据敏感度」自动路由拦截的能力尚未完全实装,涉密数据请自行控制,别默认它帮你拦住了。
  • 它是初稿工具,不是终稿:Step 6 出的是结构完整、参数精确、图文对应的初稿,科学准确性、文献、讨论分寸仍需你把关。
    广度不是它的赛道:它窄而深,押的是「执行可信度」这一条护城河;要海量现成技能覆盖,那是另一类工具的活儿。

把边界写在最显眼处,本身就是这套东西的方法论:一个愿意告诉你「这条我还没做到」「我这测试从前是假的」的工具,才值得你把真实课题交给它。
在这里插入图片描述


Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐