这套东西每天凌晨跑一次:抓 Reddit 和 HN 的帖子,去重,打分,让模型写成摘要,渲染成日报,邮件发出去。
代码从 2000 行长到 6000 行。长的部分几乎全在一个地方:数据源挂了怎么办。
这篇只讲降级链,因为它是改动最多、也最容易做错的部分。上线第一版到现在,我改了四次。
降级链长什么样
javascript
OAuth 官方 API → 公开 JSON 端点 → RSS 订阅源
每一层能拿到的东西不一样:
| 层 | 分数 | 评论数 | 正文 | 时间精度 |
|---|---|---|---|---|
| OAuth API | ✅ | ✅ | ✅ | 精确 |
| 公开 JSON | ✅ | ✅ | ✅ | 精确 |
| RSS | ❌ | ❌ | 部分 | 只能到 t=week |
第一版我写得很朴素:A 失败就试 B,B 失败就试 C,都失败就跳过。跑了三天就出问题了。
坑一:空结果不是失败
Reddit 有些板块当天确实没有新帖,返回的是空数组 []。
我第一版把"空数组"当成了"接口失败",于是触发降级------去抓 RSS,RSS 也空,再触发一次告警。结果每天凌晨给我发一堆"数据源异常",而实际上一切正常。
修正后的判断:
python
# 成功但空板块([])是合法结果,不回退------空板块走 403 回退只会制造噪音。
rdt_items = rdt_sub_posts(sub, sort, tf, limit)
if rdt_items is not None: # 注意:是 is not None,不是 if rdt_items
return rdt_items
if rdt_items 会把 [] 判成假,is not None 才是"请求成功且结果为空"。
这一行改动省掉了我每天早上删告警邮件的时间。
坑二:降级之后,下游不知道数据变差了
这是最隐蔽的一个。
RSS 只有标题和链接,没有分数、没有评论数。而我的评分模型里,分数占的权重不低------一条 800 分的帖子和一条 30 分的,排序完全不同。
降级到 RSS 之后,所有帖子的分数都是 0。评分模型不会报错,它只是把所有东西都排到最后,然后那一期的日报质量明显变差,但我不知道为什么。
改法是在数据里写一个标记:
python
if items:
print(f" [{sub}] Reddit API 不可用,已降级 RSS(无分数/评论)")
for it in items:
it["_degraded"] = "rss" # 下游看到这个字段就走降级评分
下游拿到 _degraded 就不再按分数排,改成按时间 + 关键词命中排。数据质量变差是不可避免的,但"不知道变差了"是可以避免的。
坑三:熔断要按"天"计数,不按"次"
LLM 那边也一样,我给它做了三层后端(本地网关 / 远程网关 / 兜底模型),失败就切下一个。
第一版熔断是按"连续失败次数"计的:失败 3 次就标记 degraded,跳过这层。
问题在于------这是个每天只跑一次的任务。
一次任务里可能有 8 个板块要摘要,网关挂了,8 次调用全失败,3 次就触顶。于是"今天网关抖了一下"被记成了"连续失败 3 次",第二天直接跳过网关,用更贵的兜底模型跑了一整天。
改成按天计:
python
def _record_gateway_failure():
"""同一自然日内多次失败只计一次(首次 +1,之后只刷新时间),
连续失败按"连续失败的天数"累计。"""
st = _load_gw_status()
today = health_state.today()
if st.get("last_failure_date") != today:
st["failed_days"] = int(st.get("failed_days") or 0) + 1
st["last_failure_date"] = today
st["degraded"] = st["failed_days"] >= _gateway_fail_threshold()
这样"一天挂一次"和"一天挂八次"是同一件事,而"连续挂三天"才值得熔断。
定时任务的熔断,单位应该是"运行次数",不是"调用次数"。 这一条我花了两次故障才想明白。
坑四:补跑不能走降级路径
后来加了个功能:某天漏跑了,可以补。
补跑有个要求------必须精确锁定那一天的数据 。而 RSS 的 t=week 只能给你"最近一周",没法精确到某一天。
所以补跑路径里我把降级全关了:
python
if HIST_WINDOW:
# 补跑只认官方 API:RSS 的 t=week 无法精确锁定历史某天
return _reddit_public_fallback(sub, sort, "week", limit)
宁可失败,不要拿到"大概是那几天"的数据。
因为补跑出来的东西会进历史库,一份日期错的数据比一份缺失的数据更麻烦------你不知道它错了,还会拿它算趋势。
现在的三条原则
跑了两个月,降级链最后收敛成三句话:
1. 空和失败要分开。 返回 None 是失败,返回 [] 是"今天没有"。混在一起,告警就废了。
2. 降级必须留痕。 每一层降级都在数据里写一个字段,下游据此改变策略。不写,下游会以为拿到了完整数据。
3. 越接近"事实"的路径,越不能降级。 抓取可以降(少点字段),补跑不能降(日期错一天,整条时间线都废)。
一句题外话
这套东西写完之后我有个感觉:AI 应用里最难的部分,往往不在模型那一侧。
模型调不通,换个后端就行;提示词不好,改两版就行。真正耗时间的是"数据从哪来、坏了怎么办、坏了之后下游知不知道"。
这部分没有 benchmark,没有论文,也没人写教程。但它决定了你的系统能不能连续跑 60 天不出事。