Don't validate the output. Validate that you were allowed to generate it.
Don’t validate the output. Validate that you were allowed to generate it.
不要验证输出内容,要验证你是否有权生成它。
Introduction
I run a site that collects overseas viewer comments about individual anime episodes, translates them, and publishes them. It updates automatically every day. There is one failure mode that matters more than all the others: creating a page for an episode that has not aired yet. If the page exists before the broadcast, there are no comments to put on it. But the heading “Episode 8 — overseas reactions” is already live. An empty page is recoverable. What is not recoverable is a pipeline that decides an empty page looks bad and fills it with something plausible. At that point invented sentences are wearing the face of real people. This post is about the check that prevents that, and about the day it actually fired.
简介
我运营着一个网站,专门收集海外观众对各集动画的评论,将其翻译并发布。网站每天自动更新。在所有可能的故障模式中,有一种最为严重:为尚未播出的剧集创建页面。如果页面在播出前就存在,上面将没有任何评论。但标题“第 8 集——海外反应”却已经上线了。空白页面是可以补救的,但如果自动化流水线认为空白页面“看起来很糟糕”并用看似合理的内容填充它,那就无法挽回了。到那时,编造的句子就会披上真实人物的外衣。本文将介绍防止这种情况发生的检查机制,以及它在某天真正触发时的情景。
The overall shape
The obvious implementation is arithmetic on dates: Take the air date of episode 1, assume weekly broadcast, count the weeks elapsed until today, and treat every episode up to that number as aired. That works for producing candidates, but it is not evidence that anything aired. A skipped week makes the real count lower. So does a recap episode. Calendar arithmetic never observes the broadcast; it only restates an assumption. So I split the pipeline in two:
- Candidate generation — weekly arithmetic, guessing which episodes are missing. Guessing is fine here.
- Existence check — does an observation from outside my system exist for this episode? No guessing allowed here.
整体架构
最直观的实现方式是日期计算:以第 1 集的播出日期为基准,假设每周播出,计算到今天为止经过的周数,并将该数字之前的所有剧集视为已播出。这对于生成候选剧集很有效,但并不能证明剧集真的播出了。停播一周会使实际集数减少,总集篇也会产生同样的影响。日历计算从不观察实际播出情况,它只是重申了一个假设。因此,我将流水线分为两部分:
- 候选生成 —— 每周计算,猜测哪些剧集缺失。在这里,猜测是可以接受的。
- 存在性检查 —— 对于这一集,我的系统之外是否存在观察结果?这里绝对不允许猜测。
For the existence check I use the per-episode discussion threads on MyAnimeList (a large anime database; MAL from here on). One thread is created per episode after it airs, and the timestamp of the first post in that thread is readable. Viewers post after watching. So that timestamp is external evidence that the broadcast happened. Better still, it lives in exactly the same place I fetch the comments from, so verification costs no additional data source.
为了进行存在性检查,我使用了 MyAnimeList(一个大型动画数据库,以下简称 MAL)上的分集讨论帖。每集播出后都会创建一个讨论帖,且该帖首条评论的时间戳是可读取的。观众在观看后才会发帖,因此该时间戳就是播出已发生的外部证据。更棒的是,它正好位于我抓取评论的同一个地方,所以验证过程不需要额外的额外数据源。
The core of the implementation
The check is a subtraction between what I claim and what the outside world recorded.
核心实现
该检查本质上是我所声称的日期与外界记录的日期之间的减法运算。
/**
* @param {string} expectedAired the air date my data claims (YYYY-MM-DD)
* @param {string} firstPostedAt first post in MAL's thread for that episode (ISO 8601, UTC)
*/
function checkAirDate(expectedAired, firstPostedAt, limitDays = 5) {
const expected = Date.parse(expectedAired + 'T00:00:00Z');
const observed = Date.parse(firstPostedAt);
const gapDays = Math.round((observed - expected) / 86400000);
return {
expectedAired,
firstPostedAt,
gapDays,
limitDays,
status: Math.abs(gapDays) <= limitDays ? 'ok' : 'mismatch',
};
}
The arithmetic is trivial. What does the work is the asymmetry: the left operand is my claim, the right operand is an outside observation. Comparing two values from the same side of that line verifies nothing. The five-day tolerance absorbs time zones and streaming delays. Japanese late-night slots shift the local date, and overseas releases can lag by days. Demanding an exact match would reject correct episodes. Finally the per-episode results are aggregated:
计算过程很简单。起作用的是这种不对称性:左操作数是我声称的日期,右操作数是外部观察结果。比较同一侧的两个值无法进行任何验证。五天的容差吸收了时区和流媒体延迟带来的误差。日本深夜档会改变本地日期,而海外发布可能会滞后几天。要求完全匹配会误删正确的剧集。最后,将各集的结果进行汇总:
const conclusive = episodes.every((ep) => ep.airDateCheck.status === 'ok');
if (!conclusive) return; // don't write any fetched comments
Every episode must pass, or nothing is written. Partial acceptance is wrong here: if some episodes disagree, the air-date data was produced incorrectly, which means the ones that appear to agree may only be agreeing by accident.
每一集都必须通过检查,否则不写入任何内容。这里不能接受部分通过:如果某些剧集不一致,说明播出日期数据生成有误,这意味着那些看起来一致的剧集可能只是巧合。
Where it bit me
The gate fired today, on my own code. Episodes 4 through 7 were missing, so I created four page stubs and went to fetch comments. All four stubs claimed the same air date: today. The stub generator was defaulting the air date to the current date instead of leaving it unset. This is the part worth keeping. That bug is invisible by inspection. 2026-08-23 is a well-formed date and a perfectly possible value; nothing about the record looks wrong. It only becomes wrong next to an outside observation, and then it becomes wrong with a number attached: minus twenty-five days.
我踩过的坑
今天,这个“闸门”在我的代码上触发了。第 4 到第 7 集缺失,所以我创建了四个页面存根并去抓取评论。所有四个存根都声称播出日期是今天。存根生成器将播出日期默认为了当前日期,而不是留空。这正是值得保留的部分。那个 Bug 在检查代码时是不可见的。2026-08-23 是一个格式正确的日期,也是一个完全可能的值;记录本身看起来没有任何问题。只有在与外部观察结果对比时,它才显现出错误,并附带了一个具体的数字:负二十五天。
After filling in the real air dates and re-running: “conclusive”: true, “mismatched”: [], “totals”: { “titles”: 1, “episodes”: 4, “reactions”: 32 } Thirty-two comments from the same threads, against eight while the run was failing — a failed run stops fetching early. One more thing. The same check also prevents under-collection. The weekly arithmetic proposed episode 8 as a candidate, but MAL has no thread for episode 8 yet; the last observable one is episode 7. Arithmetic said 8, observation said 7. Observation wins.
在填入真实的播出日期并重新运行后: “conclusive”: true, “mismatched”: [], “totals”: { “titles”: 1, “episodes”: 4, “reactions”: 32 } 从同一讨论帖中抓取到了 32 条评论,而之前失败时只有 8 条——因为失败的运行会提前停止抓取。还有一点,同样的检查也防止了抓取不足。每周计算建议将第 8 集作为候选,但 MAL 还没有第 8 集的讨论帖;最后可观察到的是第 7 集。计算结果说是 8,观察结果说是 7。观察结果胜出。
The result
Here is the site this runs on: https://anime.autoarticles.net. Each episode page carries the poster’s handle, a per-post URL, and the original English text alongside the translation. A translation on its own gives the reader no way to confirm that the quote is real. Making it confirmable is part of the same design decision.
结果
这是运行该系统的网站:https://anime.autoarticles.net。每个剧集页面都带有发帖人的 ID、单条评论的 URL,以及原文与译文对照。单纯的翻译无法让读者确认引用的真实性。使其可验证,也是这一设计决策的一部分。
Conclusion
Automated pipelines don’t emit convincing falsehoods because the generator is too clever. They emit them because nothing in the system is able to say that a precondition stopped holding. The stub generator that defaulted the air date to today raised no error at all. The value was present and well-formed. What stopped it was a single component comparing that value against a record from outside the system. So put the gate before generation, not after it. Auditing generated prose for plausibility is a losing game, because plausible prose is cheap to produce. Verifying that you were allowed to generate — against a fact you did not author — is cheap and decisive.
结论
自动化流水线之所以会输出令人信服的谎言,并不是因为生成器太聪明,而是因为系统中没有任何组件能指出“前提条件已不再成立”。那个将播出日期默认设为今天的存根生成器没有报错,因为该值存在且格式正确。真正阻止它的是一个将该值与系统外部记录进行对比的组件。因此,请将“闸门”放在生成之前,而不是之后。审计生成内容的合理性是一场注定会输的游戏,因为生成看似合理的内容成本太低了。而验证你是否有权生成——通过对比一个非你所作的事实——既廉价又具有决定性。