我有一条每天自动跑的信息管道:定时抓一批公开渠道的公告,交给模型解读,产出一份当日简报。它跑得很好------准点启动、零报错、成本每天三厘钱、日志干干净净。
跑了一周之后,我才发现它每天都在撒谎。
不是模型幻觉,不是抓取失败,不是解析报错。所有环节都"正常工作"。它只是把一件事搞错了:它以为"我今天看到的"就是"今天发生的"。
这篇讲的就是这个坑。它不是我这条管道独有的,它是所有"定时抓取 + 自动分析"系统的结构性缺陷。而且它极其难被发现------因为系统不会报错,它只会让你越来越自信地读到假结论。
一、事故:一份开篇就假的简报
那天的简报开篇第一句是:
「今日集中发布......」
底下列了 40 条。看起来是个大新闻------一天发 40 条,确实像"集中发布"。
我去核了一下。40 条里,19 条不是当天发布的。最老的一条来自 2024 年 5 月,距离"今日"整整 14 个月。
再往下核,简报里有一条重点提示,说某项"1000 亿专项资金"是重大利好。这条确有其事------但它是去年的政策。原文里写的是"今年",而那份文件本身是去年发的,"今年"指的是去年。模型忠实地把"今年"抄了过来,我忠实地把它读成了"今天的今年"。
一句话总结当时的状态:管道的每个零件都在正常工作,整体输出的却是系统性的假货。
二、根因:两个时间,我用错了那个
代码里长这样(简化):
python
# 抓取时给每条记录盖一个时间戳
record = {
"title": title,
"url": url,
"date": parse_publish_date(html), # 公告自己写的发布日期
"fetched_at": datetime.now().isoformat(), # 我把它抓下来的时刻
}
然后判断"今天有哪些新增"的时候,我用的是:
python
today = date.today().isoformat()
new_items = [r for r in records if r["fetched_at"].startswith(today)]
看出来了吗?fetched_at 是我的时间 ,date 才是世界的时间。
用 fetched_at 筛"今日新增",筛出来的是"我今天第一次看到的 ",不是"今天发布的"。这两个集合在稳态下高度重叠,所以平时看不出问题------直到发生下面任何一件事:
- 新增了一个数据源(它的全部历史存量会在同一天涌进来,全被标成"今日")
- 某个源的分页变了,翻出了几页老内容
- 上游补发了一批旧文件
- 你的解析器修好了,以前抓漏的老记录突然全进来了
这些恰恰都是自动化系统的日常。 换句话说:这个 bug 不是"如果"会触发,是"什么时候"触发。而我的那次触发,正是因为我前一天刚补了两个源、又回填了历史------我自己修好了一个东西,从而放大了另一个 bug。
三、为什么它这么难发现
这个 bug 有三个恶劣属性,值得单独拎出来说:
1. 它不报错。 没有异常、没有告警、没有失败重试。日志里全是绿的。你的监控如果只看"任务是否成功",它永远是成功。
2. 它的输出看起来更好,不是更差。 40 条比 21 条"信息量大"。一个每天给你 40 条的管道,比每天给你 3 条的,感觉上更有价值。错误的方向恰好是讨好你的方向------这是最危险的一种 bug。
3. 下游会把它洗白。 模型拿到 40 条被标成"今日"的东西,它不会质疑标签,它会顺着标签写出"今日集中发布"这种通顺的话。LLM 是个极好的谎言抛光机:你喂给它一个错误前提,它还给你一段自信、流畅、看起来经过分析的结论。错误经过它一过,反而更难被识破了。
第 3 点值得所有做 LLM 管道的人抄在墙上:模型不会替你校验前提,它只会把你的前提说得更好听。
四、修法:三层,缺一不可
我最后没有只改那一行筛选条件。只改一行是治不好的------因为问题的本质不是"筛错了",而是系统里没有任何一个地方对"时间"这件事负责。
第一层:让每条数据自己说出它是什么时候的
不要在简报里只写标题。每一条都强制带上出生日期和身份标签:
【新发布】2026-07-15 某某通知
【历史文件·417天前】2024-05-24 某某方案
【日期未知】------ 某某公告
关键是第三类:"日期未知"必须是一个显式的状态,而不是悄悄当成今天。
这里有个我后来才想明白的点:一开始我看到"日期未知"就想给它个默认值(比如取抓取日)。这是错的。"我不知道"和"我知道是今天"是两件完全不同的事,把前者静默转成后者,就是在制造假数据。 宁可让它显眼地缺着。
第二层:把纪律写进给模型的 prompt,而且放在最前面
模型不会自己去质疑标签,那就明确命令它质疑:
arduino
★时效纪律(最高优先级):
- 每条证据都带【新发布】/【历史文件·N天前】/【日期未知】标签,
这个标签是你判断"新旧"的唯一依据,不许自行推断。
- 严禁把【历史文件】写成"今日"、"近日"、"刚刚"。
- 文件里出现的"今年""近期""下一步",一律相对于该文件的发布日期理解,
不是相对于今天。
- 【日期未知】的材料不得用于任何时效性结论。
最后一条是那次事故的直接教训:"今年"这个词在一份去年的文件里,指的是去年。模型不会自动做这个时间平移,你必须显式告诉它。
第三层:排序也得按发布日
我原来按抓取顺序截断(prompt 有长度上限,超了就砍尾巴)。结果是:一堆历史文件排在前面占位,当天真正的新政策被挤出了 prompt。
改成按发布日倒序再截断。这条很容易被忽略------你以为你在"控制成本",其实你在"随机丢弃最重要的证据"。
五、修完之后:一个更难受但真实的数字
修好第二天,新简报的开篇变成了:
「新旧分明:15 条新发布 / 25 条历史仅作基准」
再往后我又加了一层内容分类(区分"政策原文"和"解读稿/公示名单"),当天 57 条过筛之后,真正新发布的原文只有 3 条。
从"今日集中发布 40 条"到"真正的新信号 3 条"。
这个数字难看多了。但它是真的。而一个每天诚实地告诉你"今天只有 3 条值得看"的管道,比一个每天热情地给你 40 条的管道,有价值得多------后者你迟早会不再相信它,然后整条管道就废了。
六、可迁移的检查清单
如果你也在做定时抓取 + 自动分析,照着这几条自查,大概率能捞出同类问题:
- 你的记录里,"我看到它的时间"和"它发生的时间"是两个字段吗?还是只有一个?
- 你判断"新增"用的是哪一个?(用错的那个不会报错)
- 新接一个数据源时,它的历史存量会不会被当成"今日新增"?
- 时间解析失败时,你是显式标记未知,还是悄悄给了个默认值?
- 材料里的"今年/近期/下一步",你的分析层知道该相对于哪个日期理解吗?
- 需要截断时,你是按重要性 截,还是按碰巧的顺序截?
- 你的监控能发现"任务成功了但结论是假的"吗?还是只能发现"任务失败了"?
七、真正的教训
这件事让我改的不只是代码。
我原来的验收标准是"它跑起来了吗"------启动成功、无报错、有输出。这个标准让我一周都没发现问题,因为按这个标准它天天满分。
正确的标准是"它说的是真的吗"。
自动化的可怕之处在于:它会把你的错误,用完美的纪律性,每天准时地重复一遍。 一个手工做事的人,做到第三天大概会嘀咕一句"怎么老是这几条 2024 年的";而一个脚本会毫不犹豫地把它们标成"今日",标一万次,一次都不带犹豫的。
所以对任何无人值守的系统,你要问的从来不是"它今天跑了吗",而是------
"如果它今天开始撒谎,我怎么会知道?"
如果这个问题你答不上来,那它可能已经撒了一周了。