May 18, 2026

我如何在自己的 AI 流水线里抓到一次跨客户的博客质量漂移(并在一个下午内堵上)

By Frank Yao
我如何在自己的 AI 流水线里抓到一次跨客户的博客质量漂移(并在一个下午内堵上)
Frank Yao

Quick Check

True or false: AI tools will replace the need for SEO entirely within 2 years.

TL;DR

一位客户给我发来了一张他们每周 SEO 健康报告的截图。上面写着"36 天没有自动发文"。这是错的——我的自动发布定时任务这段时间一直在每周触发两次,并且一直在发送成功通知邮件。于是我开始追查这个度量上的 bug。结果我找到的不是一个问题,而是两个:

1. **一个度量 bug。** 审计读的是错误的表。修复花了 15 分钟,外加从 Sanity 回填 79 行数据。 2. **一次真实的内容质量漂移。** 在修 #1 的过程中,我把流水线过去 30 天发出的每一篇博客都审计了一遍。**6 个客户、48 篇博文中有 35 篇没通过质量底线。** 字数单薄、缺主图、零内部链接、没有 TL;DR 或 FAQ 小标题。

修复当天下午就上线了:在发布路径上加一道质量闸门,只有当文章具备主图、1500+ 字、TL;DR 小标题、FAQ 小标题、以及至少 3 条内部链接时才放行。我还写了一个跨客户审计脚本,每晚扫描所有客户的 Sanity 项目,把失败清单邮件发给我。

这篇文章就是这次复盘。如果你也在为多个客户跑内容流水线,同样的漂移多半正在你这里发生。下面是我如何抓到它的,以及我改了什么。

我当时跑的是什么

先交代下背景——我跑着一条 AI 驱动的内容流水线,向 6 个客户站点发布长篇 SEO 博文。每个客户有自己的 Sanity CMS 项目。文章由定时任务每周发布两次,流程是:

1. 从 Search Console 拉取该客户的"临门一脚"关键词(排名 5 到 20、有真实曝光量的那些)。 2. 用 Claude 按特定的文案家声音(Halbert、Hopkins、Wiebe、Ogilvy 等)写一篇 2500 字的文章。 3. 跑一遍 E-E-A-T 评分和违禁词过滤。如果分数低于 10 分中的 7 分就退回重写,最多重试 2 次。 4. 拉一张主图和 2 张内嵌图,打水印,上传到 Sanity。 5. 把文档发布到 Sanity。客户的 Next.js 站点会在下一次页面请求时拉到。 6. 向 Facebook、Instagram、Threads、LinkedIn 推送社交预告。

这条流水线我已经跑了大约 4 个月。过去 45 天里大概有 90 篇文章是从它走出去的。整套东西在 Windows 任务计划程序里无人值守地运行。每次运行后我会收到一封汇总邮件。背后的框架细节在服务页面有更多说明。

第一个投诉

客户首先点出来的,是周一的 SEO 健康报告。报告头号数字写着"36 天没有自动发文"——但他们不到一周前还收到过我的成功邮件。他们的潜台词就是:这两个谁在撒谎?

当两份证据互相打架时,正确的动作是找到它们共同读取的那张表,看看里面到底有什么。于是我顺着报告代码追下去:

const pubRes = await pool.query(` SELECT domain, MAX(completed_at) AS last_pub FROM content_tasks WHERE status = 'published' AND completed_at IS NOT NULL GROUP BY domain `);

它读的是 `content_tasks`。然后我又顺着自动发布定时任务追。自动发布定时任务写的是 `seo_alerts` 和 `distribution_tasks`。它**根本不**写 `content_tasks`。成功邮件是真的。审计读的是定时任务从未碰过的一张表。

那个 36 天的缺口之所以是 36 天,是因为那是最后一次有人通过仪表盘 UI 发文的时间——仪表盘走的是另一条路径,而那条路径**会**写 `content_tasks`。仪表盘那条路径已经沉默了一个多月,因为所有的发布都转到定时任务上了。

**修复 #1:** 我在自动发布脚本的成功分支里加了一行 `INSERT INTO content_tasks (...)`,这样以后的运行都能被记录下来。然后我写了个一次性脚本,从每个客户的 Sanity 项目里回填最近 45 天的数据。落了 79 行。审计指标终于和现实对上了。

整个过程大概花了 25 分钟和三条 SQL。

更深一层的问题

我正一边查 Sanity 一边搭回填脚本时,客户又发来了第二条消息。他们点开了一篇最近的博文,内容很单薄——不到 500 字、没有主图、没有内部链接、没有 TL;DR、没有 FAQ。他们说:"这就是我一直觉得不同客户之间在漂移的质量。为什么会这样?"

到这里有两件事很清楚了:

  • 这篇文章确实存在于 Sanity 里。
  • 自动发布定时任务的质量闸门本该把它挡下来。每一道闸门都能抓到它:字数不到 2500、没图、没内部链接、没 FAQ 小标题。

那它是怎么发出去的?

我直接查询 Sanity,看文档 ID。自动发布定时任务用的是一个确定性前缀:`blog-autopub-{timestamp}`。那篇单薄的文章前缀不一样:`blog-{domain-with-dashes}-{slug}`。这种命名模式来自另一个文件——仪表盘库里的 `sanity-publish.ts`,是仪表盘"发布到 Sanity"按钮用的。

**有两条并行的发布路径通向同一批 Sanity 项目**,而只有其中一条强制执行了质量闸门:

路径 文档 ID 前缀 E-E-A-T 图片 字数下限 TL;DR + FAQ

------ --------------- --------- ------- ------------ -------------

定时任务 `blog-autopublish.cjs` `blog-autopub-...` 有 有 有(2500) 有

仪表盘 `sanity-publish.ts` `blog-{domain}-{slug}` 无 无 无 无

仪表盘那条路径是早于定时任务的旧内容工作流留下的产物,它依然能用。任何人按下仪表盘里"发布到 Sanity"按钮,都可以把一段 200 字的草稿直接送上线,没有任何检查。同样的架构盲区多半也存在于绝大多数多客户配置里——我在AI 自动化服务页面里讲过我整体上是怎么思考流水线闸门的。

让我意外的那次审计

在打补丁之前,我想知道这次漂移波及了多大范围。于是我写了一个 200 行的审计脚本,它会查询每个客户的 Sanity 项目,把过去 30 天每一篇文章按质量底线打分,然后写出一份 markdown 报告。第一次跑就拉出了 6 个客户共 48 篇文章。

**其中 35 篇没过底线。** 不只是那些走旁路、显眼的漏网之鱼。拆分如下:

  • **3 篇旁路路径的文章** — 单薄、没图、没标题。正是客户点出的那种模式。
  • **32 篇定时任务路径的文章** — 失败大多是"内部链接少于 3 条"和"没有 TL;DR 小标题"。还有一些是"没有主图"(尤其是 CinCin 的文章——连续 30 天每一篇都没有)。

定时任务在它的 markdown 输出里其实写了 TL;DR 章节,但 markdown 到 Sanity portable text 的转换把标题样式给抹掉了。内部链接也是一样——它们在 markdown 里,但转换过程把 Sanity 渲染真正链接所需要的 `markDef` 条目给丢了。还有一家客户在其 `blogPost` schema 上专门用的是 `heroImage` 字段(其他客户用的是 `post` schema 上的 `mainImage`)——图片上传环节静默失败时,发布照样推进,结果落地的文章里完全没有图片。

所以定时任务里的质量闸门评的是源 markdown,但最终落到 Sanity 的文档并不总是把 markdown 的质量带过去。闸门说"好内容";发出去的文档却"不够好"。

我改了什么

三件事,按顺序来。

1. 在共享库里加一道质量闸门

我在仪表盘的 `sanity-publish.ts` 库里加了一个 `validateContentMeetsProtocol()` 函数。它在任何 Sanity mutate POST 之前都会跑一遍。如果内容没有主图、字数不到 1500、内部链接少于 3 条、没有 TL;DR 小标题或没有 FAQ 小标题,发布调用就会返回 `success: false, error: 'blocked_quality_gate: ...'`,数据库那一行被标为 `status = 'blocked_quality_gate'`。文章根本不会到达 Sanity。

这个底线特意定得比我定时任务里 2500 字的目标低,因为手工精心打磨的内容有时合情合理地就在 1600 到 2000 字之间。目的是抓住单薄的垃圾,不是惩罚边缘情况。

确实存在一个用环境变量的旁路,留给真正的紧急热修——但每一次旁路都会被记录。对于我为客户跑的更大范围的流水线设计,同样的闸门哲学贯穿我们交付的每一项服务

2. 一份跨客户的每晚审计

我写了 `blog-quality-audit-all-clients.cjs`。它每晚扫描每个客户的 Sanity 项目,按同一份协议给每篇文章打分,写出一份 markdown 报告,只要有任何客户出现失败,就给我发邮件汇总。如果全部通过,不发邮件。默认沉默。

脚本还会给每个失败的来源路径打上标签(`AUTOPUB`、`BYPASS` 或 `OTHER`),这样我一眼就知道是哪条流水线在出问题。今天第一次跑就抓到了那 32 篇定时任务路径的失败——否则我永远不会注意到。更大的AI 自动化系统的细节在这里

3. 定时任务端的修复积压清单

我给定时任务本身列了一份小小的待办:修好 markdown 到 portable text 的转换,保留链接 `markDefs` 和标题样式;让 `heroImage` 字段在图片上传失败时直接致命,而不是静默地发出一篇没图的文章;在没有任何在线源返回图片时,强制回退到一张默认的品牌图片。这些会在接下来几天陆续上线,审计脚本会告诉我每个客户的失败计数什么时候归零。

如果你也在跑 AI 内容流水线,这件事为什么对你重要

如果你在跨多个站点批量发布 AI 生成的内容,以下这些是我用代价学到的:

1. **每条发布路径都需要同一道闸门。** 如果你既有定时任务、又有仪表盘按钮、还有 CLI 脚本,三者都需要同样的最低质量校验。否则垃圾会从那条没看守的路径漏出去。把闸门集中放在一个共享库里,让三者都调用。 2. **对源做评分,不等于对目的地做评分。** 如果你的闸门评的是 markdown,但你的转换到 CMS 的步骤静悄悄地丢内容,那你的闸门就是装饰品。也要给目的地文档打分——这正是审计脚本替我做的事。 3. **审计遥测必须是跨客户的。** 单客户的仪表盘会把横切的回归藏起来。"过去 30 天每一篇都没图"这种规律,在我同时查询所有客户之前是看不见的。 4. **度量 bug 和质量 bug 从外面看起来一模一样。** "仪表盘说零篇文章"和"文章都是烂的"看上去都像"流水线坏了"。要分清楚是哪种,唯一的办法是去读仪表盘实际读的那张表,然后采样流水线实际产出的那些文档。 5. **在你相信闸门之前,先把审计建起来。** 闸门会失败式开放。审计能捕捉闸门漏掉的。两者都要有。

整套东西——闸门、审计、回填、复盘——大概花了 4 小时的专注工作。大部分时间是在读代码,不是写代码。下一次漂移,审计脚本会在 24 小时内抓到,而不是等到 6 周以后。

FAQ

如何防止 AI 生成的博客内容随时间出现质量漂移?

跑两层独立的防护。第一层是每条发布路径上的闸门——一个函数,文章不达明确的质量底线(字数、主图、内部链接、FAQ 小标题)就不放行。第二层是跨客户审计,每天扫描你的线上 CMS,只要有文章不过底线就给你发邮件。闸门会静悄悄地失败;审计能注意到。

内容流水线里的"度量 bug"和"质量漂移"有什么区别?

度量 bug 指的是你的仪表盘或审计在读错误的来源,所以它要么报告了不存在的失败,要么把真实的失败藏了起来。质量漂移指的是内容本身正在退化——即便你的仪表盘说一切正常。两者从外面看是一样的("流水线好像不对劲")。要分清楚,只能采样流水线实际产出的那些工件,把它们和仪表盘的说法做对比。

怎么判断我的 AI 内容流水线是不是有多条发布路径?

去看你 CMS 里的文档 ID。绝大多数自动化脚本用的都是确定性前缀(时间戳、slug、文档类型标签)。如果你在同一种内容类型下看到了多种明显不同的前缀模式,那你多半就有多条发布路径,而其中至少有一条没有执行和其它路径一样的闸门。

AI 生成的博客内容,我最少该设多高的质量底线?

对于 SEO 内容,我的底线是:最少 1500 字、有主图、有 TL;DR 或 Key Takeaways 小标题、有 FAQ 小标题、至少 3 条指向同域名其它页面的内部链接。这是底线——不是目标。我定时任务的目标是 2500 字。底线的作用是挡住最严重的漂移,而不是定义什么叫做好。

多久审计一次线上 AI 生成的内容合适?

多客户配置下,每晚就是合适的节奏。对 6 个项目跑一次 Sanity 查询,成本基本等于零,而一周内漏掉的发布,第二天发现远比第三十天发现便宜得多。如果只有一个站点,一周一次也行。如果你有超过 6 个客户,那你大概率需要一个真正的监控系统,而不是一个 shell 脚本。

审计邮件里应该写什么?

刚够你判断要不要采取行动就行。我每个客户放一张表,显示总文章数、通过数、失败数,外加几条失败的 slug 样本。如果全部通过,脚本根本不发邮件。沉默就是成功信号。每篇文章的完整细节会进入一份 markdown 报告文件,只在汇总告诉我该开的时候我才打开它。

已经掉到质量底线之下的线上文章应该怎么补救?

不要删——一删就丢了数据,也丢了链接权重。把文档在 CMS 里的 slug 改成 `archived-{原 slug}-{日期}` 这种前缀,这样文章不再在线上 URL 上服务,但仍可恢复。URL 会变成 404(或者,如果有一篇过闸门的同 slug 双胞胎,那篇会无争议地接管)。对于有可观流量或外链的文章,加一条 301 指向相关枢纽页或合规版本。今天下午我用这种方法归档了 6 篇线上文章,一个一次性脚本对每个项目调用 Sanity mutate API。总耗时大约 5 分钟。

下一步

如果你想把这个审计脚本用在自己的流水线上,通过联系页面给我留个言,我会把源码分享给你。大概 200 行 Node.js,除了 Sanity REST API 和一个可选的 Resend 邮件 key 以外没有任何依赖。

更大的教训——也是我这周会在每个客户那里都拧紧的那条——是:自动化给了你触达,但成本是可见性。如果你看不到你的流水线昨夜发了什么出去,你就不会注意到漂移,直到客户指着一篇单薄的文章问你为什么。审计脚本现在替我看着了。

Where Are You Right Now?

What's your biggest challenge with AI and your business right now?

Related Articles

Ready to put this into action?

Let's talk about how AI automation and smart digital strategy can drive real results for your business.