原创

Node.js AI 总结管线设计:抓取、总结分离与失败重跑

基于本站生产任务的真实 429 故障,讲解如何把内容抓取和 AI 总结拆成可独立执行的阶段,保存原始快照、显示实际模型、校验结构化输出,并在限流或解析失败后安全重跑。

AI 内容工程专题 · 第 1/11 篇查看专题目录 →

一句话结论

内容抓取和 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 围栏或字段缺失。建议先在内存中完成四层验证:

  1. 传输成功:HTTP 和 choices 正常;
  2. 解析成功:正文可以解析为 JSON;
  3. Schema 成功:summary、highlights、columns 类型正确;
  4. 业务成功:链接来自抓取数据,条数和分类满足配置。

全部通过后才能替换已发布的 ai 结果。如果验证失败,保留上一版成功内容,并把错误写入单独的运行状态,而不是把公开日报变成空白页。

文件写入也需要可靠性

JSON 文件适合当前规模,但写入时应先写临时文件,再原子重命名,避免进程中断留下半个 JSON。多实例部署不能同时直接写同一个文件,应迁移到 SQLite 或 PostgreSQL,并用任务 ID 保证幂等。

同一天重复执行抓取或总结时,应更新同一条日期记录,而不是追加重复日报。归档可以只保留最近 30 天,但成功结果和运行日志要分开保存。

后台需要显示哪些状态

  • 最近一次抓取时间、来源数和总条数;
  • 各来源成功或失败状态;
  • 最近一次总结时间及实际模型;
  • 延迟、结束原因和 Token 用量;
  • 当前任务是否运行中;
  • 可操作按钮:仅抓取、仅总结、完整执行;
  • 失败后明确提示下一步,而不是只显示“生成失败”。

上线前测试清单

  1. 让一个数据源超时,确认其他来源仍能落盘;
  2. 模拟全部来源为空,确认不会调用模型;
  3. 模拟模型 429,确认原始 sections 保留;
  4. 模拟 HTTP 200 但 JSON 非法,确认旧日报不被覆盖;
  5. 对同一天重复总结,确认只更新一条记录;
  6. 重启容器后再次总结,确认仍能读取抓取快照;
  7. 检查后台不返回明文 API Key。

最终建议

AI 内容管线的可靠性来自阶段边界,而不是“换一个更强模型”。先把抓取结果变成可复用快照,再让总结成为可重跑的独立任务;同时记录实际模型、错误码和结构校验结果。这样即使模型限流,内容资产也不会丢失,管理员可以在几秒内判断故障位置并安全恢复。

常见问题

AI 总结失败后需要重新抓取吗?

如果抓取快照已经保存,就不需要。应直接读取当天 sections,单独重跑总结,避免重复访问来源和改变输入。

抓取与总结分开会不会让数据不一致?

应通过日期和任务 ID 绑定快照,并记录 generatedAt 与 summarizedAt。总结始终读取明确的快照版本,就能保持可追踪。

模型失败时应该清空当天日报吗?

不应该。保留上一版成功结果或原始数据,把错误写入运行状态;只有新结果通过解析和业务校验后再替换公开内容。

延伸阅读:在总结前处理重复内容

抓取结果进入总结前,还应规范 URL、识别相似标题并保存内容指纹。具体实现见 AI 内容去重:URL 规范化、标题相似度与内容指纹。

实测与内容说明

实测记录

  • 2026-08-12 生产任务:3 个来源共抓取 26 条内容
  • GLM-4.7-Flash 总结返回 HTTP 429 / code 1305
  • 抓取快照已保存,可不重新抓取而单独重跑总结

参考资料

内容版本 1.2000000000000002 · 审核:推荐智能手记 · 计划复审:2026-09-13