时间口径错了,你的自动化就在系统性造假

我有一条每天自动跑的信息管道:定时抓一批公开渠道的公告,交给模型解读,产出一份当日简报。它跑得很好------准点启动、零报错、成本每天三厘钱、日志干干净净。

跑了一周之后,我才发现它每天都在撒谎。

不是模型幻觉,不是抓取失败,不是解析报错。所有环节都"正常工作"。它只是把一件事搞错了:它以为"我今天看到的"就是"今天发生的"。

这篇讲的就是这个坑。它不是我这条管道独有的,它是所有"定时抓取 + 自动分析"系统的结构性缺陷。而且它极其难被发现------因为系统不会报错,它只会让你越来越自信地读到假结论。

一、事故:一份开篇就假的简报

那天的简报开篇第一句是:

「今日集中发布......」

底下列了 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 年的";而一个脚本会毫不犹豫地把它们标成"今日",标一万次,一次都不带犹豫的。

所以对任何无人值守的系统,你要问的从来不是"它今天跑了吗",而是------

"如果它今天开始撒谎,我怎么会知道?"

如果这个问题你答不上来,那它可能已经撒了一周了。

相关推荐
TrisighT7 小时前
Skill 写成 2000 字说明书?Claude 根本不读——路由器思维与渐进式披露
aigc·ai编程·claude
Flynt7 小时前
MCP让Claude连上外部世界,我踩了5个坑才跑通
ai编程·claude·mcp
武子康7 小时前
Fable 5 的 7 月 19 日不是发布延期:模型可用性正在变成动态资源(九维可用性 + SLO 重构)
人工智能·chatgpt·claude
逐梦苍穹1 天前
Claude Code 迁移 Codex 实战复盘:基于 15 天本地日志分析使用量、信任成本与失败地图
prompt·claude·codex·claudecode
七牛开发者1 天前
从 Fable 案例看 Coding Agent 的未知项管理
ai·agent·claude·loop·codex
ServBay2 天前
同时用 Claude Code、Codex 和 Gemini CLI?一个本地网关就能统一管理
aigc·ai编程·claude
粤海科技君2 天前
WorkBuddy CLI 命令行实战:替代 Claude Code 的工作流
claude·workflow·腾讯
蓝田~2 天前
大模型本地部署与远程调用 — 从 API 到 Agent
java·人工智能·claude·claude code