Skip to content

AI 只能写初稿:ReadTao 的内容生成、人工审校与失败重试

约 3072 字大约 10 分钟

ReadTaoAIDramatiq内容工作流

2026-07-05

把古籍正文交给模型生成现代汉语、英文或拼音建议并不难,难的是模型超时、部分失败、原文后来修改时,系统还能不能保持状态可信。这篇记录 ReadTao 如何把生成任务限制在“机器初稿”,再通过输入版本、修订关系、人工审校和可重试任务形成可靠工作流。

上一篇里,一份古籍原文件已经经过来源登记、正文与注文分层、语义分段,最后形成稳定的 Segment

接下来才轮到 AI。

这也是我在 ReadTao 里有意坚持的顺序:先把来源和语义边界整理清楚,再考虑自动生成。否则模型只是在一份边界含混的文本上更快地产生更多内容。

本文对应的生成服务、Dramatiq Worker 和 Studio 任务界面都已放在 ReadTao GitHub 仓库 中。

这一篇的核心边界

模型调用成功,只能证明机器初稿已经产生;只有人工确认、审校通过并进入不可变发布快照,内容才会出现在 Reader。

先把不同内容层分开

一个 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,因此显示“内容需同步”

我没有再增加一个允许前端随意写入的“段落整体状态”字段。整体状态由规范原文和四个必需派生层的最新修订计算出来:

  1. 内容是否齐全;
  2. 是否基于最新原文;
  3. 是否存在驳回修订;
  4. 是否仍是草稿;
  5. 是否待审校;
  6. 是否全部批准。

只有所有最新修订都已经批准,并且四个派生层都指向最新规范原文,整个段落才是“已批准”。

这避免了两套事实来源:子内容明明还是草稿,段落状态却因为某个接口漏更新而显示已完成。

拼音为什么不是让模型直接输出一整行

拼音是我在实现中踩坑比较多的一层。

如果只存一整行拼音字符串,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、5

Studio 既提供单张任务卡的“重试”,也提供当前内容单元顶部的“重试失败项”。两种入口最终都调用同一套后端规则,不能把顶部按钮偷偷实现成“重新生成全部”。

我为什么把一次 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 的内容工作台把来源单元、语义段落、各内容层和任务状态放在同一屏里。

ReadTao Studio 典籍发布与内容完整度总览

任务卡固定按照“简体原文、现代汉语译文、英文译文、拼音”排列,不跟随接口返回顺序跳动。每张卡显示进度和逐段错误,当前页面发起的一组任务全部结束后,还会显示需要手工关闭的结果弹窗。

这里的目标不是制造更多状态,而是让编辑者离开一段时间再回来时,仍然能够回答:上次进行到哪里,哪些成功了,哪些需要继续处理。

AI 在这条链路中可以退出

ReadTao 的自动生成确实节省了重复劳动,但发布数据并不依赖 Provider 存活。

  • 简体可以确定性生成并人工修改;
  • 拼音有本地初稿和人工覆盖;
  • 翻译可以由模型起草,也可以完全人工录入;
  • 来源注文始终只来自可追溯来源;
  • 所有公开内容最终固定在不可变发布快照里。

所以我不把“接入了哪个模型”当作内容系统最核心的能力。更重要的是模型失败时不会污染已有内容,模型退出后编辑和阅读仍然成立。

下一篇进入整条链路的分界点:Studio 改了内容,Reader 为什么不会立即变化:ReadTao 的不可变发布设计