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

Quick Check
对还是错:AI 工具将在 2 年内完全取代 SEO 的需求。
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?
你的业务目前在 AI 方面最大的挑战是什么?


