07:30,日报邮件准时进了邮箱。正文第一行写着「昨日成交 0 单」,附件表格里是 1843 行明细。两个数字来自同一条链路,相差 1843。这不是模型算错,是上游 SQL 的时间窗口比下游晚了一天——链路跑通了,但没人校验过它跑对了。
这是我把日报做成定时任务后第一个月的真实事故。后来我把它拆成四段,每段单独设检查点,才敢让它无人值守。下面写下四段的失败模式、幂等策略和几个必须避开的坑。
段 | 职责 | 常见失败 | 幂等策略 | 实测耗时 |
|---|---|---|---|---|
1 采集 | 拉业务库增量 + 第三方 API | 上游延迟写入,拉到半天的数据 | 按业务日期分区覆盖写 | 40–90 s |
2 清洗 | 去重、时区归一、字段对齐 | 时区混用导致跨天错位 | 落地后做行数与金额双校验 | 10–20 s |
3 生成 | 模型把结构化数据写成叙述 | 数字与表格不一致(幻觉) | 抽取正文数字回表比对,不一致则回填 | 25–60 s |
4 投递 | 邮件 + 群机器人 | 超时、重复推送 | 以 (日期, 版本号) 做幂等键 | 5–15 s |
第三段是最脆的一环,因为它把「可校验的表格」变成了「不可直接校验的自然语言」。前两段出错会报异常,第四段出错用户会投诉,只有第三段出错是静默的:数字看着合理,其实是编的。
日报最常见的错误是把「今天」当成「昨天」。我第一次写窗口时用了 created_at >= CURDATE(),跑出来永远是 0 单,因为凌晨跑任务时当天还没有数据。改成按业务日期取前一日,并显式声明时区:
-- 增量窗口:按业务日期取前一日,显式绑定时区,避免容器 UTC 与业务 UTC+8 错位
SELECT
DATE(CONVERT_TZ(o.created_at, '+00:00', '+08:00')) AS biz_date,
COUNT(*) AS order_cnt,
SUM(o.pay_amount) / 100.0 AS amount_yuan
FROM orders o
WHERE o.created_at >= CONVERT_TZ(:biz_date, '+08:00', '+00:00')
AND o.created_at < CONVERT_TZ(DATE_ADD(:biz_date, INTERVAL 1 DAY), '+08:00', '+00:00')
AND o.status IN ('PAID', 'SHIPPED', 'DONE')
GROUP BY biz_date;关键在 CONVERT_TZ 的两个方向:入库是 UTC,统计是 UTC+8,写反了就会前后各偏 8 小时,表现为「每天固定少 8 小时的订单」。这个 bug 我盯了两天才发现,因为它只在 00:00–08:00 的订单上体现。
采集段还要处理「补单」——支付成功但回调延迟落库的订单,会晚几小时才出现。我的做法是每天重跑前 7 天的窗口,覆盖写入而不是追加,这样补单自然被吸收,代价是每天多跑 7 次查询(约 15 秒,可接受)。
把 1843 行明细直接丢给模型是不可行的:token 成本高,且模型对长表格的聚合几乎必错。我的做法是先用 SQL 算出 12 个指标(单量、GMV、客单价、退款率、Top5 品类、环比),再把这张 12 行的小表交给模型,只让它写解释性段落。
即便如此,仍有数字漂移。我统计了 60 次生成结果:正文里出现数字 41 个,与源表不一致的有 6 个,错误率约 15%,集中在「百分比取整」和「环比方向」上——它会把 +3.2% 写成「下降 3.2%」。
所以第三段必须加一道回填校验:正则抽出正文里所有数字,逐个到指标字典里查,查不到或对不上就用模板句替换。
import re
NUM = re.compile(r"-?\d+(?:\.\d+)?%?")
def verify_and_fill(text, metrics):
"""正文数字必须能在指标字典里找到,否则该句降级为模板句。"""
bad = []
for m in NUM.finditer(text):
token = m.group()
if token.rstrip("%") not in metrics["tokens"]:
bad.append(token)
if bad:
return metrics["fallback_text"], bad # 交回模板,记录告警
return text, []这段代码很土,但它把「静默出错」变成了「显式降级」。跑了两周,命中 4 次,其中 3 次是模型把 1843 写成 1834——肉眼几乎发现不了。
第四段看似简单,实际最容易造成信任崩塌:重复推送三次日报,用户就不再看了。我用 (biz_date, version) 做幂等键,同一版本只允许成功投递一次;重试时 version 递增,正文里会带「第 2 版」标记,读者能看出这是修正。
这里踩过一个坑:邮件发送超时不等于没发出去。第一次重试导致同一封日报发了两遍,因为 SMTP 在 30 秒后超时,但服务端其实已经接收。后来改成发送前先查投递日志表,超时后延迟 90 秒再查一次状态,仍未确认才重发。
最大的失误不是技术,是范围。第一版我让它连「异常归因」一起写——为什么单量掉了 20%。模型给出的原因看起来头头是道(天气、竞品活动、投放衰减),但没有一条有数据支撑,等于把猜测包装成了结论。现在我把它砍掉了,日报只描述事实和变化,归因留给人在会上判断。
这套链路现在跑在 PRIVO 的定时任务里,四段各自独立成一次执行,失败只重跑单段,不会把整条链拖垮;生成段的回填校验也是我后来补进去的,最初那版直接让模型自由发挥,才有了开头那个 0 单的日报。
如果只能记一条经验:把不可校验的环节夹在两个可校验的环节之间,并且给每个环节一个能对上的数字。链路能跑通不算什么,跑通之后数字能对上,才算能用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。