冻结扫描检测器:我造好了它,却没人消费它的告警

一条管道连续21天exit 0却在重复分析冻结的旧数据,揭示活动指标无法证明输出新鲜。
作者维护的本地选股分析任务连续21个工作日准时运行、日志全绿、退出码为0,但检测器发现其最新K线日期比今天落后15个工作日,管道一直在重复分析同一份冻结数据。文件计数、行数、运行时长等“活动指标”都能被冻结管道伪造,唯一无法伪造的是输出实质内容是否变化。作者因此对定义答案的字段做哈希指纹并排除所有时间戳,设计出panel_stale、trailing_repeat、verify_frozen三项检查。然而检测器上线后每天返回FROZEN却无人消费,因为未接入写入端,退化为装饰性仪表盘。核心结论是:裁决必须是写入的前置条件,失败模式应是拒绝写入,而非写入加警告。
一条检测命令打印出三个数字:15、15、3。这三个数字分别代表着一连串的沉默失败,也揭示了一个比"没有监控"更隐蔽的工程陷阱。

一个"成功"运行了21次的失效管道
作者维护着一个本地 Python 分析任务,它遍历 3,147 个个股日线 JSON 文件,把每只股票分类到六种技术形态之一,然后向 signal_history.jsonl 追加一行扫描结果,次日再对前一天的选股打分并写入验证日志。
这个任务连续 21 个工作日、每晚 15:35 准时运行,每次都以 exit 0 退出,日志全绿。直到作者运行了自己写的冻结检测器,真相才浮出水面:面板中最新一根 K 线的日期是 2026-09-28,比"今天"落后了 15 个工作日。管道并没有在分析当下,而是在反复分析同一份早已冻结的过去。
更耐人寻味的是,验证日志里出现了三次记录,target_date 在往前推进,但每只股票的收益数字却一字未变——因为价格文件的 mtime 从 2026-09-28 12:10 起就再没动过。系统在用新的日期包装旧的答案。
为什么文件计数救不了你
这个案例里有一个特别容易被忽视的细节:面板目录下确实有 3,147 个文件,但最新一根 K 线上只出现了 3,049 只标的,剩下约 100 个文件停在了更早的日期。没有任何环节对此报警。
文件数量不是新鲜度度量。行数、运行时长、退出码,这些"活动指标"在一条冻结的管道上全都可以被满足。作者由此提出一个关键判断:唯一无法被冻结管道伪造的读数,是"输出的实质内容有没有变"。
在 Unix 系统中,文件的 mtime(modification time)记录的是文件内容最后一次被修改的时间戳。对于数据管道,mtime 是一个廉价而直接的“数据是否更新”证据:如果价格文件每晚都被重新抓取写入,mtime 应当随之前进。作者正是靠 mtime 停留在 2026-09-28 12:10 这一事实,才确认价格文件根本没有被刷新。需要注意的是,mtime 也可能因为文件被重写为相同内容而变化,因此它适合作为辅助证据,而非内容级判据。
指纹答案,而不是指纹活动
解决方案是对定义"答案"的字段做哈希,刻意排除定义"本次运行"的字段。作者定义了 SNAP_KEYS = ('dist', 'picks', 'rebound', 'breaks'),序列化时使用 sort_keys=True 防止字典重排伪造变化,同时把 scan_date、verify_date、target_date、generated、run_at、ts 这些时间戳排除在外——日期推进不能伪造内容变化。
任何人拿到账本文件都能独立重算这些指纹,不需要访问作者的代码。
基于这个指纹,三项检查自然成立:
- panel_stale:比较最新 K 线日期与预期最后交易日,按工作日计算差距,超过阈值即触发。
- trailing_repeat:检查连续账本条目的指纹,尾部相同长度超过阈值即触发。
- verify_frozen:在验证账本中,如果
target_date推进而逐项明细完全不变,立即触发。
其中两项检查还顺带捕获了作者写代码前没想到的两种失败:管道可以持续读取、但数字停止移动;也可以持续改日期、但测量结果保持冻结。两种都长得像"产出在增长",实际都不是。
sort_keys=True 是 Python json.dumps 的一个参数,它强制字典在序列化时按 key 的字母序输出,而不是按插入顺序。这对指纹稳定性至关重要:同一份数据如果在不同运行中被构造成 key 顺序不同的字典,不做排序就会产生不同的 JSON 字符串和不同的哈希值,从而把“内容没变”误判为“内容变了”——这恰恰会掩盖冻结问题。反过来,固定排序让任何人在任何机器上重算都能得到相同指纹,使账本具备可审计性。
一周后:检测器是对的,但它是装饰
这篇文章最诚实、也最有价值的部分在这里。作者造好了检测器、发布了工具、写下了失败模式分析,然后——没有把它接到写入端。
自检测器上线以来,写入任务的 cron 又追加了三条字节级完全相同的记录。检测器每天返回退出码 2(FROZEN),但没有任何进程去消费这个退出码。一个没人调用的工具,它的告警等于不存在。
作者由此提炼出全文的核心洞见:一个裁决结果不是前置条件的检测器,只是一个仪表盘。 而一条显示绿色的仪表盘架在冻结的管道上,比没有仪表盘更糟——现在有两件东西在告诉你一切正常:写入端的 exit 0,以及检测器的存在本身。
修复只需要在写入端加一行:调用检测器,如果返回码是 2,直接 sys.exit('refusing to append a snapshot I cannot vouch for')。快照根本不该被写入。记录告警却照样写入过期条目,是同一个 bug 加上更多仪式感。
退出码(exit code)是进程向调用方传达结果的整数约定,通常 0 表示成功,非零表示某种失败。Shell 中的 cron、CI 系统和调度器都根据退出码决定是否告警或中断。但退出码只有被消费才有意义:如果调度器不检查它,或者后续步骤不依赖它,那么返回 2 与返回 0 在效果上完全相同。这正是作者所说“没人消费的裁决等于不存在”的技术根源——告警必须成为流程中的判断分支,而不是旁路日志。
适用于任何定时分析任务的六条规则
作者把自己的教训整理成可复用的检查清单:
- 绿色不是证据。"跑了、退出 0"只是在陈述运行器。要问:如果任务失败了,哪个读数会不同?如果答案是"没有",那你什么都没测量。
- 问自己:如果任务偷偷冻结了,输出会是什么样? 这个问题回答起来很便宜,而作者的可靠性 bug 全都住在这里。
- 指纹答案,排除时钟。 时间戳、运行 ID、行数、文件数,在什么都没变时都会变,在一切都变了时却都不变。
- "没有输入"需要自己的退出码。 检测器在没有任何条目可判断时返回
3 INSUFFICIENT,因为一个检查了零条记录的检查器绝不应该打印"健康"——正如一个打开了零个文件的文件扫描器绝不能打印"干净"。 - 连续两天相同是正常的,连续十五天不是。 把阈值显式化,用自己的命中率去调,记住单次重复不是信号。
- 闸门,不要报告。 没人消费的裁决是装饰。检查应该放在写入端的前置条件里,失败模式应该是"没有写入快照",而不是"写了快照加一条警告日志"。
工具的边界与未填的坑
scan_freshness_guard.py 仅依赖标准库,409 行,退出码分为 0 HEALTHY、2 FROZEN、3 INSUFFICIENT、1 error,附带一个基于合成 fixture 的 18 断言自测。仓库的 samples/ 目录公开了真实的冻结账本——包括那个含 10 条尾部重复的 16 条记录版本及其验证账本——所以 FROZEN 裁决可以在你自己的机器上复现,不需要作者的数据。代码以 MIT 协议发布。
作者主动声明了一个边界:这个工具检测的是冻结的输出,不是坏掉的抓取。一个每天都返回不同垃圾的数据源可以通过全部三项检查。新鲜是必要条件,不是充分条件。
他自己已知的下一个缺口是交易所节假日日历:目前 panel_stale 只认周末,把十月的交易所假期当作普通工作日处理——而这恰恰是 15 天的缺口被读成"普通周一漂移"的原因。
对于任何每天真正在跑的分析 agent——选股、定时扫描、任何"追加一份快照然后退出 0"的任务——这个检测器的价值不在于那 400 行代码,而在于它逼你回答的那个问题:你的绿色,到底测量了什么?
相关推荐

零成本搭建AI超级员工系统:智能体自动工作流全解析
深度拆解B站演示的AI超级员工系统:Agent智能体编排、数字人口播、地图与企业获客、24小时智能客服及法务模块全覆盖。分析零成本开户背后的商业逻辑与合规风险,帮助创业者判断这套AI数字员工系统的真实投产比。

Chatbot与AI Agent的本质区别:从对话到执行的跨越
Chatbot和AI Agent有什么区别?本文基于Learn Tech频道的MCP系列讲解,用订机票、笔记摘要等实例说明:Chatbot只给答案,Agent通过思考-行动-观察循环真正完成任务,并拆解模型、工具、记忆、目标四大部件及MCP的桥梁作用,同时讨论Agent的适用边界与权限风险。

免费GitHub仓库:342小时AI课程,从数学到MCP服务器
一个免费GitHub仓库提供342小时AI课程,包含523节课、20个阶段,覆盖数学基础到Agent构建和MCP服务器。支持目标驱动学习,每节课产出可留存的prompt、skill或agent,已有近10万人开始学习。