模型调用成功,只能证明机器初稿已经产生;只有人工确认、审校通过并进入不可变发布快照,内容才会出现在 Reader。
AI 只能写初稿:ReadTao 的内容生成、人工审校与失败重试
把古籍正文交给模型生成现代汉语、英文或拼音建议并不难,难的是模型超时、部分失败、原文后来修改时,系统还能不能保持状态可信。这篇记录 ReadTao 如何把生成任务限制在“机器初稿”,再通过输入版本、修订关系、人工审校和可重试任务形成可靠工作流。
在上一篇里,一份古籍原文件已经经过来源登记、正文与注文分层、语义分段,最后形成稳定的 Segment。
接下来才轮到 AI。
这也是我在 ReadTao 里有意坚持的顺序:先把来源和语义边界整理清楚,再考虑自动生成。否则模型只是在一份边界含混的文本上更快地产生更多内容。
本文对应的生成服务、Dramatiq Worker 和 Studio 任务界面都已放在 ReadTao GitHub 仓库 中。
先把不同内容层分开
一个 ReadTao 语义段落下面,不是只有“原文”和“翻译”两个字符串。
| 内容层 | 产生方式 | 是否允许 AI | 发布前要求 |
|---|---|---|---|
| 规范原文 | 来源导入、人工修订 | 不作为生成内容 | 必须批准 |
| 简体原文 | 确定性简繁转换、人工例外 | 不需要模型 | 必须批准并基于最新原文 |
| 拼音 | 本地词典生成、模型风险复核 | 只复核风险 | 风险必须逐项确认 |
| 现代汉语 | 模型初稿、人工修改 | 可以生成初稿 | 必须批准并基于最新原文 |
| English | 模型初稿、人工修改 | 可以生成初稿 | 必须批准并基于最新原文 |
| 来源注文 | 来源自带、人工校订 | 禁止生成 | 独立批准并固定到发布快照 |
来源注文尤其需要单独说明。
王弼注、河上公注或原始来源里的“疏”“音义”,不是 ReadTao 根据正文生成的解释。它们有自己的来源片段、稳定注文块和修订记录,也不会进入正文翻译、拼音复核或 RAG 的模型请求。
把来源注文排除在模型链路之外,不只是 Prompt 里写一句“请忽略注释”,而是数据模型和 Worker 根本不提供这种任务类型。
为什么生成必须异步
一个内容单元可能包含几十甚至上百个语义段落,每段又可能需要简体、拼音、现代汉语和英文四类任务。
如果让 HTTP 请求一直等待全部模型调用完成,会遇到:
- 浏览器或代理超时;
- 中间某一段失败导致整批结果难以判断;
- 用户刷新页面后不知道任务是否还在运行;
- 已经成功的段落被重复调用和重复计费;
- Worker 重启后任务状态无法恢复。
ReadTao 使用 Redis 与 Dramatiq,把创建任务和执行任务分开。
Studio 不需要维持一个很长的上传请求。页面关闭后重新进入内容单元,也能从数据库恢复任务状态。
Job 和 JobRun 分别记录什么
我把“一次批量生成请求”和“其中某一个段落的一次尝试”分开保存。
Job 记录:
- 任务类型;
- 当前状态;
- 请求人;
- 输入段落与原文修订映射;
- 总数、成功数和失败数;
- 开始、结束时间和总体错误。
JobRun 记录:
- 具体段落和内容层;
- 第几次尝试;
- Provider、模型和 Prompt 版本;
- 输入校验和;
- token 用量和 Provider 响应 ID;
- 这一段的错误信息。
这种拆分让 Studio 不只是显示一个模糊的“任务失败”。它可以告诉我:英文任务共 104 段,103 段成功,第 57 段因为结构校验失败,需要单独处理。
输入修订是任务的一部分
创建生成任务时,Backend 会把每个 segment_id 对应的最新原文修订 ID 保存到 input_revisions。
可以把它理解成这次任务的输入快照:
{
"segment-a": "segment-revision-v3",
"segment-b": "segment-revision-v1"
}Worker 真正处理前还会再次读取最新原文。如果 segment-a 已经从 v3 修改到 v4,旧任务结果就不能附着到新原文上。
if latest_revision_id != expected_revision_id:
raise StaleInputError("原文已更新,请基于最新修订重新生成")没有这道检查,会出现一种表面上很难察觉的错位:页面显示的是最新原文,译文其实来自前一版。
派生内容为什么保存 based_on 关系
每份 RenditionRevision 都保存 based_on_segment_revision_id。
SegmentRevision v1
├── 简体 v1
├── 拼音 v1
├── 现代汉语 v1
└── English v1
SegmentRevision v2
└── 旧派生内容仍然指向 v1,因此显示“内容需同步”我没有再增加一个允许前端随意写入的“段落整体状态”字段。整体状态由规范原文和四个必需派生层的最新修订计算出来:
- 内容是否齐全;
- 是否基于最新原文;
- 是否存在驳回修订;
- 是否仍是草稿;
- 是否待审校;
- 是否全部批准。
只有所有最新修订都已经批准,并且四个派生层都指向最新规范原文,整个段落才是“已批准”。
这避免了两套事实来源:子内容明明还是草稿,段落状态却因为某个接口漏更新而显示已完成。
拼音为什么不是让模型直接输出一整行
拼音是我在实现中踩坑比较多的一层。
如果只存一整行拼音字符串,Reader 很难知道哪个音节对应哪个汉字,也无法只修正一个多音字。
当前做法是先用本地 pypinyin 产生确定性词元:
{
"token_index": 0,
"surface": "道",
"pinyin": "dào",
"start_offset": 0,
"end_offset": 1,
"is_manual_override": false
}模型只负责指出可能存在的多音字、古义或专名风险,并给出建议,不直接重写全部词元。
模型复核超时也不会丢失已经生成的本地拼音。系统会保留初稿,增加“模型复核待完成”风险,等待人工处理。
这比让整段任务直接失败更符合实际:确定性结果依然有价值,但我不能把“模型没来得及检查”伪装成“已经确认没有风险”。
失败不应该让整个内容单元重跑
任务状态至少包括:
queued → running → succeeded
↘ partial_failed
↘ failed
↘ cancelled批次允许部分失败。已经成功的段落会保留,重试时只创建未完成段落的新任务。
Backend 会从原任务的 input_revisions 中排除已有成功 JobRun 的段落:
原任务:1、2、3、4、5
成功: 1、2、4
失败: 3、5
重试: 只提交 3、5Studio 既提供单张任务卡的“重试”,也提供当前内容单元顶部的“重试失败项”。两种入口最终都调用同一套后端规则,不能把顶部按钮偷偷实现成“重新生成全部”。
我为什么把一次 Worker 消息限制为两个段落
最早按整章处理时,一条 Dramatiq 消息会连续调用很多次模型。即使单段大多数时候都很快,少数慢请求累积以后仍可能撞上 Actor 的硬超时。
更麻烦的是,Python 的 TimeLimitExceeded 不一定像普通业务异常一样被原有捕获逻辑处理,任务可能停留在 running,Studio 看见的进度长期不变。
现在 Worker 每次只处理两个段落:
GENERATION_JOB_CHUNK_SIZE = 2
has_more = execute_generation_job(
job_id,
max_items=GENERATION_JOB_CHUNK_SIZE,
)
if has_more:
process_generation_job.send(job_id)这样一条消息的最长执行时间被限制在更小范围,完成后再续投剩余工作。发生硬超时时,Actor 还会显式把任务收敛到可见的失败或部分失败状态,提示重试未完成段落。
这次调整让我更明确了一点:异步不等于“扔进队列就不用管了”。任务必须可观察、会结束,而且失败后知道从哪里继续。
人工审校不是一个布尔值
生成成功后,内容首先进入 draft,完整状态流转是:
同一个管理员可以同时拥有编辑、审校和发布角色,但操作仍然要显式发生。这样即使是个人项目,我也能回头区分:
- 哪一份是机器初稿;
- 哪一份经过人工修改;
- 谁在什么时间批准;
- 驳回原因是什么;
- 当前发布快照固定的是哪一份修订。
被驳回的修订不会原地改回草稿。返工时会创建新的 draft 修订,原驳回记录继续保留。已经批准的内容再次编辑也一样:新修改从新草稿开始,旧批准修订和线上发布快照不变。
Studio 怎样把状态呈现出来
Studio 的内容工作台把来源单元、语义段落、各内容层和任务状态放在同一屏里。

任务卡固定按照“简体原文、现代汉语译文、英文译文、拼音”排列,不跟随接口返回顺序跳动。每张卡显示进度和逐段错误,当前页面发起的一组任务全部结束后,还会显示需要手工关闭的结果弹窗。
这里的目标不是制造更多状态,而是让编辑者离开一段时间再回来时,仍然能够回答:上次进行到哪里,哪些成功了,哪些需要继续处理。
AI 在这条链路中可以退出
ReadTao 的自动生成确实节省了重复劳动,但发布数据并不依赖 Provider 存活。
- 简体可以确定性生成并人工修改;
- 拼音有本地初稿和人工覆盖;
- 翻译可以由模型起草,也可以完全人工录入;
- 来源注文始终只来自可追溯来源;
- 所有公开内容最终固定在不可变发布快照里。
所以我不把“接入了哪个模型”当作内容系统最核心的能力。更重要的是模型失败时不会污染已有内容,模型退出后编辑和阅读仍然成立。
下一篇进入整条链路的分界点:Studio 改了内容,Reader 为什么不会立即变化:ReadTao 的不可变发布设计。