Braipen 迭代复盘:从文本生成到可控创作工作流
正文生成完,是否就能成为下一章的依据?这次 Braipen 迭代把正文、用户确认、后台摘要与长期知识分开管理,并补上版本校验、前章锁定、结构化输出验证和可观测性。本文记录这些设计的取舍、失败边界与测试依据。
Braipen 是一个基于 React、FastAPI 和模型 API 的小说创作项目。本文对应源码版本 fb9d55a,重点是应用如何管理生成结果。模型负责写出内容,应用负责决定哪一版可以保存、确认、复用,以及出错后怎样继续。
1. 先把正文交给用户,再更新摘要
最初的链路把“完成一章”理解为一组连续动作:生成正文、生成摘要、保存结果。实际使用时,用户最想先看到的是正文;而刚生成的正文,又未必是准备保留的版本。先对初稿生成摘要,不仅增加等待,还可能在用户改稿后留下过期的上下文。
因此,单章模式现在分成两个阶段:
生成正文 → 保存草稿 → 用户阅读、修改、确认
↓
后台摘要与一致性检查
↓
摘要就绪,允许推进后章
|
确认接口返回 HTTP 202,后续工作交给 FastAPI 的 BackgroundTasks。用户修改过正文时,先保存新版本,再基于新正文生成摘要并检查疑似冲突。没有修改时,则采用普通摘要和规则检查。
这个改动缩短了用户获得正文之前的等待链路,也避免对尚未定稿的内容提前做摘要。它没有提高模型输出正文的速度,后台工作仍然需要时间。
拆开链路之后,真正需要解决的问题是:旧任务晚返回时怎么办?
我为章节建立了 revision,由文件名和正文内容共同计算 SHA-256,并给每次后台工作分配 job_id。模型调用在项目锁之外运行;提交结果时重新进入锁,同时检查版本、任务 ID 和确认状态。下面是这一原则的简化示意:
result = call_model(confirmed_text) # 不持有项目锁等待模型
with project_lock:
if current_revision != revision or current_job_id != job_id:
return # 旧结果不能写入新状态
if chapter_status != "confirmed":
return
save_summary_and_update_index(result)
|
实际实现把检查放在写摘要文件和章节索引之前。即使正文被外部编辑,或者文本相同却保存成了另一个文件版本,旧摘要也不能继续冒充当前版本的记忆。
对应实现:章节确认与后台工作。对应测试:过期版本与任务不得回写。
2. 连续生成需要定义依赖、停止和失败
连续生成可以写成一个循环,但循环本身不解决章节之间的依赖:后一章需要前一章的有效摘要,前文被引用后又不能随意改写。
当前批量模式一次最多生成 10 章,按“正文 → 自动确认 → 摘要 → 下一章”的顺序推进。开始下一章之前,后端检查前章状态,并持久化锁定标记。锁定检查覆盖编辑、续写保存和重新生成等写入入口,前端按钮禁用只是其中的界面反馈。
| 情况 | 当前处理 |
|---|---|
| 前章尚未确认,或摘要未就绪 | 阻止后章启动;历史未跟踪章节有兼容处理 |
| 用户点击停止 | 完成当前章及摘要后停止,不再开始下一章 |
| 摘要失败 | 停止后续生成,保留已保存正文并显示失败 |
| 后章正文生成失败 | 已经被本次生成引用的前章继续锁定 |
| 批量结束 | 最后一章仍可修改,后续再次引用后才锁定 |
| 服务重启 | 未完成任务显示中断,保留正文,由用户检查后重试 |
持续锁定是一个保守选择。如果允许修改已经被后文引用的章节,就需要追踪哪些摘要、知识和后文受影响,并定义失效及重算流程。当前项目尚未实现这套依赖重算,所以先明确修改边界。
这里使用的是单进程锁与后台任务。元数据会落盘,但没有引入持久化任务队列;服务重启不会自动接续中断的模型调用。这是当前规模下的实现边界,扩展多进程时必须重新处理共享锁和任务调度。
对应实现:连续生成服务。对应测试:停止、失败与重启行为。
3. 长期知识要有写入权限和来源
与直接在聊天窗口生成小说相比,项目里另一条值得保留的链路是:正文中出现的内容,先成为候选事实,再由作者决定是否进入正式叙事知识。
用户发起章节分析 → Story Delta → 待审核知识候选
↓ 作者接受或修改后接受
写入叙事图谱
↓
为后续章节组装上下文
|
这条分析链路由用户在资料库发起,并非每章生成后自动运行。接受候选之前创建安全快照,写入时保留来源章节、草稿和候选 ID。目前审核合并支持创建节点和关系,不是任意图谱修改。
文件存储下,图谱与审核记录也不是一个数据库事务。如果图谱已写入,而审核状态保存失败,重试需要根据来源标识识别已经插入的实体,避免重复创建。来源字段因此既服务于解释,也参与失败恢复。
图谱如何进入模型上下文,同样需要说清楚。当前采用可解释的规则打分,综合章节目标匹配、重要度、未解伏笔及一跳邻居挑选记录,保留入选理由,并设置节点、关系数量上限。已确认且重要度高的事实与一般背景分别放入不同提示词层级。
这不是新型检索算法,也不能保证所有事实始终进入提示词。单章流程可以预览并选择使用组装结果;批量流程自动构建,但当前没有传入章节目标,主要依赖重要度、伏笔等信号。图谱可视化便于查看和编辑,生成质量仍取决于知识本身、选择规则及模型对约束的遵守程度。
章节任务与场景计划也采用类似的生效边界:草稿不参与正式约束,批准后才可被解析使用;场景计划绑定具体任务版本。它们是作者维护的生成输入,目前不属于模型自主规划。
4. JSON 格式、字段合法与事实可信是三层问题
Story Delta 等结果需要继续交给程序处理,因此对应调用显式启用 response_format={"type":"json_object"}。但请求 JSON 对象,并不能替代应用的结构校验,更不能证明内容真实。
当前处理分成三层:
- 格式约束:只在需要结构化结果的调用中启用 JSON 模式;小说正文和普通摘要保持文本输出。空响应和被截断的响应仍然拒绝。
- 结构校验:Story Delta 在兼容性归一化之前检查必需字段、字段类型、操作枚举与数值范围。失败时反馈具体错误,最多修复一次;再次失败就显式结束,不保存伪造的成功结果。
- 内容审核:结构合法的知识仍是候选,等待作者审核;正文检查产生的警告仍是疑似问题,等待用户核对。
“解析 → 校验 → 反馈错误 → 一次修复”可以称为一个有上限的反馈闭环。它的价值在于可终止、可追踪,不需要因此把整个应用描述成自主 Agent。
修改正文后的逻辑提示还增加了证据门槛:模型必须给出 evidence,服务端验证它是当前正文里的连续原文,阅读器再把用户带到对应位置。没有有效原文证据或字段类型错误,检查会显示未完成,不能把空列表包装成“没有发现问题”。
逐字证据只能证明模型引用了存在的文字,不能证明它对矛盾的判断正确。因此界面保留“疑似”“可能”的表述,合理改写也不会仅因不同于初稿就被判错。这里尚未测量误报率或召回率。
5. 优化之前,先记录用户究竟等在哪里
“生成慢”可以指首次看见正文慢,也可以指整章完成慢,或正文结束后仍在等待摘要。将这些时间混成一个总耗时,很难判断下一步该改什么。
这次先调整提示词排列:共享规则、项目设定与参考材料在前,章节号、任务和场景约束等变化内容靠后,让相邻章节具有可复用的输入前缀。同时保留原有上下文裁剪预算,每次读取最新材料,不为提高命中机会而复用旧事实。材料出现的位置也不等同于指令优先级,硬连续性约束和批准任务的地位仍需明确表达。
正文生成日志则拆开记录:
| 指标 | 含义 |
|---|---|
first_content_ms |
流式调用中首个非空白正文片段的等待时间,排除 reasoning 事件 |
body_complete_ms |
正文接收完成时间 |
total_ms |
当前生成任务最终耗时,包含保存阶段;不包含之后独立确认摘要的调用 |
| cache hit / miss tokens | 模型服务返回的缓存统计,缺失时保持未知 |
| 模板版本 | 区分提示词排列规则,避免把不同版本的结果混在一起 |
完整请求的哈希另存于 AI Run 的提示词档案,用于追踪输入变化,不属于这份正文耗时日志的字段。同步调用无法观察首个正文片段到达的时刻,因此该项为 null;服务商没有返回缓存数据时,也不能记成 0。流式结束时不带 choices 的 usage 数据仍需读取。指标日志写入失败,不应把已经保存成功的正文变成生成失败。
目前只能说明“已经准备好衡量优化”,还不能声称缓存命中率、成本或延迟改善了多少。下一步应使用固定样本与一致参数对比真实调用,再决定瓶颈位于输入处理、输出生成还是应用流程。
对应实现:章节提示词、正文指标。对应测试:公共前缀与材料更新、指标边界。
6. 阅读体验也有状态一致性问题
阅读器支持字体、字号和底色,但调大字号会重新换行,沿用旧的像素滚动位置就可能跳离正在读的句子。当前实现尽量记录视口内的字符锚点,排版变化后将同一字符恢复到原来的可见位置;不支持相关浏览器能力时回退到相对进度。
跨页面的阅读进度则按项目、章节和内容版本保存。正文已经更改时,不把旧进度直接套到新内容上。默认 18px 宋体、暖纸或随系统底色只是起点,更关键的是调整排版时仍能接着读,点击逻辑提示后能找到原文,也能返回提示。
界面偏好保存在浏览器本地存储,正文和凭据不进入这份偏好记录。存储损坏或不可用时回到默认值,不阻断阅读。
对应实现:阅读器的字符锚点与证据定位、阅读位置恢复。
7. 回归测试教会我的两个小问题
第一次是摘要任务认领:如果先把 job 加入内存中的运行集合,再写入磁盘上的 running 状态,写盘失败后就可能留下一个无法正常重试的“正在运行”任务。修复方式是先成功持久化,再更新内存占用;同时用故障注入测试验证失败后的清理与重试。
第二次是生产环境默认 API 地址。开发时前后端都在本机,127.0.0.1 看起来没有问题;到了线上,它却指向访问者自己的电脑。生产构建默认使用同源请求,再通过实际构建产物和线上浏览器验证,才能确认界面访问的是服务器后端。
2026 年 9 月 21 日发布时,同一源码提交在本地和服务器均通过 233 项后端测试、8 项前端测试及生产构建;隔离环境中还验证了确认、后台提示、连续生成、前章锁定和章边界停止。线上检查覆盖静态资源、健康接口及已有章节阅读。
这些结果证明的是已覆盖的工程行为。测试使用替身或隔离数据,没有据此得出真实模型生成质量、缓存收益或逻辑检查准确率的结论。候选审核的完整故障恢复与上下文检索也仍有补充专项测试的空间。
这次迭代留下的三个判断
- 记忆更新首先是版本与权限问题。 哪段内容由谁确认、对应哪个版本、何时失效,决定后续上下文是否可信。
- 异步流程首先需要明确失败语义。 旧任务回写、重复提交、写盘失败和服务重启,比给生成按钮增加一个 loading 更值得提前设计。
- 优化需要可比较的证据。 把首段等待、正文完成和后台工作拆开,把未知统计保留为未知,才能据此安排后续投入。
当前 Braipen 仍是一套有明确边界的个人创作工具。后续最值得继续验证的,是这些边界在真实长篇创作中是否减少了重复确认、过期上下文和人工排错,而不只是生成了更多文字。