AI 整理全书为什么不能只靠一个大 Prompt

续章的全书整理功能曾把作品设定、章节、事实和记忆一次性发给模型,两次调用跑了八分多钟。后来我把它改成按需查资料的有限 Agent 循环,没用框架,也没再被单次超长上下文拖死。

我第一次拿《西游记》测续章时,它把火焰山后面直接接到了灵山受封。

祭赛国、金光寺、碧波潭、九头虫,这一串本来能写得很热闹的东西,被压得只剩几句交代。我让 AI 往回补,它又开始重复旧章节,像写到半路才想起少交了一段作业。

续章是我做的 AI 小说应用,英文名是 Novevia,仓库和早期代码里还保留着 Chapterly 这个名字。它会先生成作品设定、故事路线和章节列表,再一章一章往下写。前几章好看不算太难,难的是写到几十章后,它还记得这本书要去哪里。

当时我以为问题在模型,后来发现,更大的问题是我给模型安排了一种很笨的工作方式。

写作者被整本书的章节卡片和资料淹没,故事路线从火山骤然跳到远方宫殿

English version: I Stopped Sending the Whole Book to the Model

概念篇:从一次模型调用到一个 Agent

“全书”两个字把我带偏了

最早的实现很符合直觉:既然叫“整理全书”,就把和全书有关的材料都给它。

作品设定、故事路线、全部章节卡片、事实库、近期记忆和用户要求,服务端先尽量压缩,再拼成一个大 prompt。模型做完诊断,如果服务端检查出危险修改,还要把这批材料连同草案再发一遍,让它自查。

短作品上,这办法能用。书一长,它的问题就不再是“prompt 写得好不好”,而是一个请求到底能有多胖。

一次线上任务里,两次模型调用加起来超过了 8 分钟。前端把它改成异步任务,可以避开 HTTP 连接超时,却不能让这 8 分钟消失。页面上的黄条一直在转,我也不知道它是在认真看书,还是已经读晕了。

更别扭的是,用户有时只问“第 13 章这个标题为什么接不上”,系统仍然把整本书的材料都搬过去。模型看完后,往往给出一句没法反驳、也没什么用的结论:整体节奏偏快,建议加强人物成长。

材料并不少,缺的是选择。该看哪几章,要不要查故事路线,事实库和记忆对这个问题有没有用,这些判断都被我省掉了。我先把仓库门打开,再叫 AI 自己进去翻,至于要查什么,系统并没有说清楚。

先给一张地图

后来我把第一步改了。AI 不再先看全部材料,只看一张 manifest,也就是作品清单。

这张清单里有书名、题材、章节数、已写和未写章节的数量,也会告诉模型故事路线是空的、过薄还是可用,事实库和记忆各有多少条。它不带正文,也不展开整份大纲。

从庞大的资料库中先看索引,再按问题取出少量相关档案

模型看完清单,才选择工具。它可以查某一段章节卡片,读路线摘要,取某一章的摘要、开头或结尾,也可以按关键词查事实库和章节记忆。最近的调整记录也做成了工具,免得它隔几天又提一遍同样的建议。

我没有用 provider 自带的 function calling,只约定了一个 JSON 格式:

{
  "type": "tool_call",
  "tool": "list_chapter_cards",
  "args": { "fromChapterNo": 9, "toChapterNo": 15 },
  "reason": "需要确认祭赛国前后的章节承接"
}

服务端收到 tool_call 后执行工具,截断过长字段,再把结果放回对话。模型可以继续查,也可以认为证据已经足够,输出 final 方案。

这样一来,“第 13 章标题不对”只需查相邻几章和路线;“祭赛国写得太快”才需要进一步搜索金光寺、碧波潭和九头虫。整本书的长度不再直接决定第一个请求的长度,问题本身开始决定该读多少材料。

这个循环算不算 ReAct

代码写完后,我才回头想这个问题。

ReAct 是 Reasoning and Acting 的缩写。模型不做一次性回答,它在推理、行动和观察之间往返:决定查什么,调用工具,看到结果,再决定下一步。

续章的实现符合这个核心。模型根据上一次 observation 选择下一个工具,代码没有预先写死顺序。它也没有复制论文里的完整 Thought 格式,不保存模型的长思维链,只记一句简短的 reason。所以我更愿意把它叫做有限的 ReAct-style loop。

不过,ReAct 并不是这轮改造里最重要的那个词。它只是一个后来对得上的名字。对产品更有用的变化,是上下文从“服务端事先打包好的全部材料”,变成了“模型围绕当前问题建立的工作集”。

在这个功能里,这也是 Agent 和普通模型调用的分界线。模型是其中的决策部件,Agent 还得有工具、状态、循环、停止条件和权限边界。少了后面这些东西,只是一个会输出 JSON 的模型。

Agent 不能直接改书

一旦让模型自己选工具,下一个问题就是:它可以走多远。

我给的答案很保守。不同模型档位只允许 3 到 5 个决策步骤,工具也只能调用 3 到 5 次。每次请求有 24,000 到 64,000 字符的上限,超出后保留系统指令、manifest 和最近的工具结果。这不是精确的 token 预算,但能防止对话随着工具调用一路变胖。

有限的资料查询循环经过步数限制、只读档案、方案校验和人工确认

更重要的限制是,所有查资料的工具都只读。模型不能写数据库,只能提交一份结构化方案。validate_draft_plan 会检查它是不是在覆盖已写正文,是不是把新章节插到了不安全的位置,或者一口气补了过多章节。检查不通过,模型最多修正一次。

已经有正文的章节只会收到建议,未写且为 0 字的计划章才能进入自动调整列表。用户看完方案后还要点一次确认,应用后也可以回滚。让模型直接改库确实能少写几段代码,只是这几段代码迟早会从事故里补回来。

运行方式也从一次 HTTP 请求变成了持久化任务。页面可以关,任务可以取消;后台进程中断后,worker 会把超时的 running 任务重新排队,最多恢复两次。每个 tool_calledtool_resultplan_validated 都会记到任务事件里,前端会显示最近一步,刷新后也能继续看。

Pi 最后没有接

早期方案里,我考虑过接 @earendil-works/pi-ai。等到真正开始写全书整理,这个依赖并没有进项目。

pi-ai 更靠近模型接入层:统一 provider、消息格式、流式输出、tool call 和 usage。如果连 Agent runtime 也用 Pi 的实现,还能少写一部分消息循环、状态和工具执行代码。但章节怎么查,事实库怎么搜,哪些内容绝对不能自动改,这些仍然是续章自己的事。

项目当时已经有 NovelAiGatewayService,provider 选择、错误归一、usage 和成本记录都在这里。我没有为了再做一遍这些事而换掉它,只在上面加了 NovelBookDoctorRuntime,再用 NovelBookDoctorToolRegistry 收拢工具。任务的排队、恢复、应用和回滚,继续放在 NovelBookAdjustmentService 里。

所以,这个 Agent 的编排层算是手写的,底下的模型调用和任务基础设施不是。我不反对 Agent 框架,只是这个循环最多 5 步,工具数量也不多,还没有复杂到值得替换既有调用链。等多个 Agent 开始共用工具、记忆、trace 和重试策略时,账可能会另算。

超时没了,账还没算完

改完以后,我最想解决的问题已经解决了:整理全书没有再因为单次提交超长上下文而超时。

超时仍然可能发生。provider 的响应会变慢,网络会出问题,整个任务也有时限。现在消失的是一个明确的故障模式:书越长,第一个请求越长,最后把自己拖死。

总 token 也不一定比原来少。ReAct 会把一次大调用拆成几次小调用,如果模型连续查了路线、章节、事实和记忆,加起来可能更贵。现在能确定的是单次请求受控,不能直接推出“token 大幅下降”。

质量也一样。目前的测试能证明 manifest 不带完整内容,章节卡片不泄漏正文,单章工具只返回请求的片段,危险草案会被拦下。这些测试能保住边界,不能证明 AI 已经有了资深编辑的判断力。真要比质量,还得固定一批作品和问题,记录方案采纳率、误修率和实际工具路径。

现在再点一次“整理全书”,页面上的黄条还是会转。不同的是,任务记录里能看到它刚查了哪几章,为什么又去翻事实库,方案如果被拦,又是卡在哪条规则上。我不用再盯着一个八分钟没动静的请求,猜它究竟在忙,还是已经被我塞晕了。

至于 ReAct,这个名字放在代码说明里很合适,放在这轮实践里倒没那么神秘。模型没有突然读懂《西游记》,我也没找到一个无所不能的 Agent 框架。它只是照着目录,缺什么拿什么;拿够了,停下来交方案。

黄条照样会转,好在现在能转到头了。

正在加载讨论...