一句话结论
内容抓取和 AI 总结必须拆成两个可独立执行、可重复运行的阶段。本站 2026 年 8 月 12 日的真实任务中,抓取阶段成功保存了 3 个来源共 26 条内容,随后 GLM-4.7-Flash 返回 HTTP 429;由于原始数据已经先落盘,故障只影响总结,可以稍后单独重跑,不需要重新访问所有来源。
如果你正在处理模型接入和结构化输出,可先阅读 GLM-4.7-Flash API 配置与实测 和 GLM-4.7-Flash JSON 输出实测。
为什么一个“立即生成日报”按钮不够
最简单的日报程序会顺序执行抓取、调用模型、写入结果。只要模型超时、限流或 JSON 解析失败,整个任务就被标记为失败。管理员无法快速判断是数据源故障还是模型故障,也无法只重试失败环节。
更稳妥的数据流是:
数据源 → 抓取并校验 → 保存原始 sections
↓
选择模型并总结
↓
校验结构化结果
↓
发布日报
抓取成功是一个独立检查点。后面的模型调用失败时,不能删除或覆盖已经保存的原始数据。
本站生产故障记录
8 月 12 日任务保存了以下数据:
| 来源 | 条数 |
|---|---|
| Hacker News(AI) | 10 |
| GitHub Trending | 8 |
| GitHub AI 项目 | 8 |
| 合计 | 26 |
总结阶段使用 glm-4.7-flash,接口返回 HTTP 429,业务错误码为 1305,含义是模型当前访问量较大。这个错误不应导致重新抓取,因为 26 条输入已经完整保存在当天记录中。正确操作是等待、切换健康模型或稍后点击“仅 AI 总结”。
推荐的数据结构
每天只保留一个记录,并允许抓取和总结分别更新自己的字段:
{
"date": "2026-08-12",
"generatedAt": "2026-08-12T00:00:00.000Z",
"sections": [
{ "source": "Hacker News (AI)", "items": [] }
],
"ai": {
"model": "glm-4.7-flash",
"summarizedAt": "2026-08-12T00:00:06.000Z",
"error": "AI HTTP 429"
}
}
重新抓取时只替换 sections,保留当天已有总结直到新总结成功;重新总结时只读取已保存的 sections,不访问外部数据源。
抓取阶段的 Node.js 实现
async function fetchDailyData(date) {
const sections = [];
for (const source of enabledSources) {
const result = await fetchSource(source);
if (result.ok && result.items.length) {
sections.push({ source: source.name, items: result.items });
} else {
log({ step: 'fetch', source: source.id, error: result.error });
}
}
const totalItems = sections.reduce(
(total, section) => total + section.items.length,
0,
);
if (!totalItems) throw new Error('所有数据源均未抓取到内容');
const archive = loadArchive();
const previous = archive.days.find(day => day.date === date) ?? {};
const report = {
...previous,
date,
generatedAt: new Date().toISOString(),
sections,
};
upsertDay(archive, report);
saveArchiveAtomically(archive);
return { date, totalItems, sections: sections.length };
}
如果部分来源失败但仍有足够内容,可以继续保存并在后台显示失败来源;只有全部来源都为空时才终止。
总结阶段只读取快照
async function summarizeDaily(date) {
const archive = loadArchive();
const report = archive.days.find(day => day.date === date);
if (!report?.sections?.length) {
throw new Error('没有可总结的数据,请先执行抓取');
}
const model = selectHealthyModel();
try {
const ai = await summarizeWithAI(model, report.sections);
report.ai = {
...validateSummary(ai),
model: model.model,
modelId: model.id,
summarizedAt: new Date().toISOString(),
};
saveArchiveAtomically(archive);
return report.ai;
} catch (error) {
report.ai = {
error: sanitizeError(error),
model: model.model,
summarizedAt: new Date().toISOString(),
};
saveArchiveAtomically(archive);
throw error;
}
}
后台应该明确显示本次使用的模型、执行时间、失败原因和抓取条数,避免把“AI 总结”误解成只选一条内容。
429 应该怎么处理
429 不只有一种原因。智谱错误码可能表示访问量过大、并发超额、余额不足或账户状态问题。处理前应同时记录 HTTP 状态和响应体业务码。
- 临时访问量过大:带随机抖动等待后重试一次;
- 并发超额:降低并发,进入任务队列;
- 余额或账户问题:立即停止重试并告警;
- 多模型已配置:切到健康备用模型;
- 所有模型失败:保留原始数据,允许人工重跑。
不要无限重试,也不要在一个 HTTP 请求中等待几分钟。后台接口可以立即返回“任务已开始”,再通过状态接口显示进度。
防止失败覆盖上一版日报
模型返回 HTTP 200 后仍可能出现空 content、Markdown 围栏或字段缺失。建议先在内存中完成四层验证:
- 传输成功:HTTP 和 choices 正常;
- 解析成功:正文可以解析为 JSON;
- Schema 成功:summary、highlights、columns 类型正确;
- 业务成功:链接来自抓取数据,条数和分类满足配置。
全部通过后才能替换已发布的 ai 结果。如果验证失败,保留上一版成功内容,并把错误写入单独的运行状态,而不是把公开日报变成空白页。
文件写入也需要可靠性
JSON 文件适合当前规模,但写入时应先写临时文件,再原子重命名,避免进程中断留下半个 JSON。多实例部署不能同时直接写同一个文件,应迁移到 SQLite 或 PostgreSQL,并用任务 ID 保证幂等。
同一天重复执行抓取或总结时,应更新同一条日期记录,而不是追加重复日报。归档可以只保留最近 30 天,但成功结果和运行日志要分开保存。
后台需要显示哪些状态
- 最近一次抓取时间、来源数和总条数;
- 各来源成功或失败状态;
- 最近一次总结时间及实际模型;
- 延迟、结束原因和 Token 用量;
- 当前任务是否运行中;
- 可操作按钮:仅抓取、仅总结、完整执行;
- 失败后明确提示下一步,而不是只显示“生成失败”。
上线前测试清单
- 让一个数据源超时,确认其他来源仍能落盘;
- 模拟全部来源为空,确认不会调用模型;
- 模拟模型 429,确认原始 sections 保留;
- 模拟 HTTP 200 但 JSON 非法,确认旧日报不被覆盖;
- 对同一天重复总结,确认只更新一条记录;
- 重启容器后再次总结,确认仍能读取抓取快照;
- 检查后台不返回明文 API Key。
最终建议
AI 内容管线的可靠性来自阶段边界,而不是“换一个更强模型”。先把抓取结果变成可复用快照,再让总结成为可重跑的独立任务;同时记录实际模型、错误码和结构校验结果。这样即使模型限流,内容资产也不会丢失,管理员可以在几秒内判断故障位置并安全恢复。
常见问题
AI 总结失败后需要重新抓取吗?
如果抓取快照已经保存,就不需要。应直接读取当天 sections,单独重跑总结,避免重复访问来源和改变输入。
抓取与总结分开会不会让数据不一致?
应通过日期和任务 ID 绑定快照,并记录 generatedAt 与 summarizedAt。总结始终读取明确的快照版本,就能保持可追踪。
模型失败时应该清空当天日报吗?
不应该。保留上一版成功结果或原始数据,把错误写入运行状态;只有新结果通过解析和业务校验后再替换公开内容。
延伸阅读:在总结前处理重复内容
抓取结果进入总结前,还应规范 URL、识别相似标题并保存内容指纹。具体实现见 AI 内容去重:URL 规范化、标题相似度与内容指纹。