简介:这是一套基于Gemini大模型的超长篇网络小说生成工具,面向AI小说爱好者、独立创作者或需要高效产出长篇故事的用户,重点解决长期写作中世界观漂移、剧情逻辑断裂与角色状态不一致等问题。核心功能覆盖小说设定工坊、智能章节生成、状态追踪、基于向量的语义检索、知识库集成、自动审校以及可视化操作台,适合从大纲规划、逐章生成到修改复盘的全流程使用。资源包共30个文件,以20个Python源码文件为主,辅以配置文件、示例文件、依赖清单和Markdown说明文档,整体仅106KB,结构清晰易于扩展;目录按生成器、工具库、知识库、配置与输出等模块划分,便于快速定位与二次开发。目前已有139人学习参考,既可凭借GUI工作台快速搭建创作流水线,也可依据源代码理解多阶段生成与记忆管理机制,是一份思路完整、可落地的轻量级AI写作方案。
1. 这个压缩包的名字,其实是一道大模型长文本工程的综合题
“基于Gemini大模型的超长篇小说生成器!AI一键生成百万字网络小说!小白文终结者!.zip”,这个标题看起来是网文作者的“外挂”,但上手做过大模型应用的人都知道:整句话最难的不是“AI 生成”,而是“百万字”。Gemini 大模型的上下文窗口再大,也不是拿来一口气输出全书的。真正的工程量在大纲拆分、前文记忆、章节扩写、断点续跑,是一整套长文本工程。“小白文终结者”那五个字,做扎实了就是一个质量门禁模块。
这篇文章写给想用 Gemini 搭长文本生成器的新手,也写给调过 API、想知道参数边界和坑在哪的老手。下面从选型理由、模块划分、主循环代码、参数配置讲到避坑记录,最后一章给验证与进阶方案。
2. 为什么是 Gemini,为什么“百万字”不该一次生成
2.1 超长上下文在长篇小说里的正确用法
Gemini 之所以成为这个标题的主角,核心卖点是它的长上下文能力。Gemini 系列模型在宣传中把上下文窗口推到了一个很高的量级,这使得它可以在一次对话里放进大量文本。但很多人把这个能力误解成了“把一整本小说放进 prompt 里续写”。实际上,长上下文在小说生成器里的价值不是当“仓库”,而是当“工作台”:你把故事设定、人物状态表、最近几章的摘要和当前章细纲同时放在 prompt 里,让模型在一个足够大的信息空间里做决策,避免它因为看不到关键信息而写崩。
我在实践里的经验是,上下文里塞得越多,模型反而越容易捡了芝麻丢西瓜。Gemini 虽然窗口大,但输出质量并不会和输入长度成正比。对生成器来说,关键信息必须放在 prompt 最前面和最显眼的位置,背景设定要经过压缩后再进入上下文。这就是后面要用记忆模块而不是把全文堆进去的原因。超长上下文给了你设计空间,却逼着你学会做减法。
2.2 一百万字的真正成本:333 章、上千次调用和多小时运行
“百万字网络小说”在工程上是个具体的数字。网络小说一章通常 2000 到 4000 字,按平均 3000 字算,一百万字意味着大约 333 章。Gemini 一次调用最多只能输出几千到上万 token,换算成中文大概是几千字到一万多字,所以一章一次调用只够写草稿。为了保证质量,常见做法是每章生成后让它生成摘要,再把这个摘要压缩进全局记忆,跑完一定章节再做一致性检查。也就是说,每一章最少消耗 2 到 3 次 API 调用,整本书跑下来是上千次调用。
这还只是调用次数。如果每次调用平均耗时 3 到 8 秒,加上限流退避,生成一本书的耗时会从几十分钟拉到几小时甚至更久。这时候你会发现,最大的风险不是模型不会写,而是跑到一半进程挂掉、API 报错、网络闪断,或者账单超预算。因此任何一个合格的长篇小说生成器,都必须有 checkpoint 机制:每章生成完立刻落盘,记录当前进度,重启后从断点继续,而不是从头再跑一遍。否则所有关于“一键生成”的想象都会被一次 429 错误打回原形。
2.3 “小白文终结者”不是提示词,是质量门禁
“小白文”在网文圈是个说不清但大家都能感受到的概念,水字数、重复套路、逻辑崩坏、反派降智都在这个范畴里。要让 AI 终结小白文,光在 prompt 里写一句“不要写小白文”是不够的,因为模型对这类模糊的否定指令基本无感。我一般会把“小白文”拆成可检查的维度:重复率过高、高频套话、一段话车轱辘、前后设定冲突、角色动机断裂。这些维度一部分可以用规则脚本检测,比如 n-gram 重复率;另一部分要交给第二个模型来评审,让它以编辑的身份找出逻辑硬伤。
也就是说,“小白文终结者”在代码层面是一个夹在生成器和最终输出之间的质量门禁。生成器每写完一章,先跑一次规则检查,再决定是直接入库,还是把评审意见返回给生成器重写。这一条会在后面第 3 章的主循环和第 6 章的进阶方案里落地。先把结论放在这里:先定义什么是“小白文”,再谈怎么终结,否则你只是在押大模型的心情。
2.4 为什么优先选 Gemini 而不是先微调大模型
很多人拿到标题后的第一反应是“那我也微调一个大模型”,但长篇小说生成优先级应该先是 prompt 工程和上下文管理,其次才轮到微调。原因有三点:第一,微调需要一份“什么是好小说”的对齐数据,这个数据很难造,造几百章人工标注的成本比 API 调用还高;第二,微调改变的是模型的语言风格和指令偏好,但改不了它在长上下文里的遗忘问题,你不解决记忆模块,微调完照样写崩;第三,Gemini 本身已经见过足够多文本,缺的从来不是文笔,而是结构约束。所以在没有穷尽提示词和前置工程之前,不要急着考虑微调,等生成器跑通、积累了一批高质量的生成-改写语料再说。这个观点会在第 6 章重新展开。
3. 从 zip 到能跑的最小原型:模块划分、prompt 模板与主循环代码
3.1 把写作流水线拆成四个角色
一个可维护的长篇小说生成器,绝不会是“一个 prompt 写全书”的单一函数。按照大模型应用里常见的 agent 思路,我一般把生成器拆成四个角色:Planner 负责把小说拆成章;Writer 负责扩写正文;Memory 负责把“过去发生的事”压缩成记忆;Critic 负责挑毛病。每个角色本质上是一次带特定 prompt 和参数的 Gemini 调用,组合起来才变成一个稳定的循环。
这种多角色拆分的好处是,你可以单独调整某个环节而不影响整条链路。比如 Writer 写崩了,你只需改 Writer 的 prompt 和温度;大纲太老套,你只需改 Planner;章节前后矛盾,你优先查 Memory 的输出。如果所有逻辑挤在一个函数里,出了问题只能从头调试。新手最容易犯的错,是先写一个 500 行的单文件脚本,跑通后不断往里加 if else,最后自己都不敢改。模块化之后,每个角色的职责边界清楚,出问题时能快速定位,这也是 AI agent 类项目里最值得学的工程习惯。
3.2 prompt 模板怎么设计
prompt 不是越长越好。长篇小说生成器的 prompt 要做三件事:给角色身份、给输入槽位、给硬约束。下面的 Planner prompt 用来生成整本小说的结构化大纲。我习惯让它只输出 JSON,因为后续要按章节遍历,自由文本虽然好看但没法稳定程序化处理。
PLANNER_PROMPT = """你是网络小说主编,擅长写长线大纲。 请为「{genre}」题材生成一本小说的完整大纲。 总章节数:{total_chapters} 章,每章目标字数:{chapter_words} 字。 输出要求(严格遵循): 1. 只输出一个 JSON 对象,不要输出任何解释。 2. JSON 顶层包含 title、characters、volumes 三个字段。 3. characters 是人物表,每个元素必须包含 name、role、goal、constraints。 4. volumes 是若干卷,每卷包含卷名和 chapters 数组; 每个章节元素必须包含 title、viewpoint、goal、hook、foreshadowing。 5. 章与章之间要有明确的情节推进,禁止同一事件反复描述。 JSON 结构: { "title": "书名", "characters": [{"name": "主角名", "role": "主角", "goal": "目标", "constraints": "行为限制"}], "volumes": [{"name": "第一卷", "chapters": [{"title": "章节名", "viewpoint": "视角人物", "goal": "本章要推进的事", "hook": "章尾悬念", "foreshadowing": "伏笔"}]}] }"""逻辑说明:这段 prompt 把“主编”的角色和大纲的字段约束同时给定,模型只需要按槽位填内容。参数上,Planner 使用低温度,比如 0.3,让输出更稳定,避免大纲天马行空。total_chapters 和 chapter_words 通过 Python 字符串模板填进去,可以保证三百章的结构稳定。这里不对模型名做假设:生成函数里模型名用环境变量传入,你在自己账号上开通了哪一个就用哪一个。
然后是 Writer prompt:
CHAPTER_PROMPT = """你是网络小说写手,文风紧凑,拒绝注水。 当前故事状态: - 主线:{plot_state} - 人物:{character_state} - 全局摘要:{story_memory} 请写第 {chapter_index} 章《{chapter_title}》。 本章要求: - 以 {viewpoint} 的视角推进剧情。 - 必须完成:{goal}。 - 章尾埋下 {hook} 型悬念。 - 可以提及的伏笔:{foreshadowing}。 - 字数控制在 {chapter_words} 字左右。 - 禁止重复前文情节,禁止新增与全局摘要矛盾的设定。 直接输出章节正文,不要输出章节分析。"""逻辑说明:这个 prompt 把记忆模块的输出放在最前面,相当于给 Writer 一块“当前世界快照”。网络小说是高度序列化的内容,每章只需要知道“刚才发生了什么”和“这章要发生什么”,不需要读全文。把 story_memory 控制在一两千字内,能显著降低模型的记忆负担。
3.3 主循环代码:从大纲到逐章落盘
先写一个最小可用的 Gemini 调用封装,再做编排。
import json import os import time from pathlib import Path import google.generativeai as genai genai.configure(api_key=os.environ["GEMINI_API_KEY"]) def logits(prompt: str, *, temperature: float = 0.85, max_tokens: int = 4096, top_p: float = 0.9): # 模型名按你账号实际可用的填,常见是 gemini-1.5-pro 或其后续版本 model_name = os.environ.get("GEMINI_MODEL_NAME", "gemini-1.5-pro") model = genai.GenerativeModel(model_name) resp = model.generate_content( prompt, generation_config=genai.types.GenerationConfig( temperature=temperature, top_p=top_p, max_output_tokens=max_tokens, ), ) return resp.text参数说明:temperature控制随机性,写正文用 0.85 左右,保证文风灵活又不至于散;max_tokens是一次调用最多输出的 token 数,中文 3000 字大约需要预留 2500 到 4000 token,通常设 4096 起步,大纲等结构化输出可以设 8192。top_p和 temperature 是两套采样策略,这里用 0.9 即可。
然后是大纲解析。模型偶尔会用 markdown 代码块包住 JSON,或者输出没闭合,必须做兜底。
def parse_json(text: str) -> dict: """模型偶尔用 markdown 代码块包住 JSON,先剥掉再解析。""" text = text.strip() if text.startswith("```"): text = text[3:] if text.startswith("json"): text = text[4:] text = text.rstrip("`").strip() return json.loads(text) def plan_novel(genre: str, total_chapters: int, chapter_words: int) -> dict: prompt = PLANNER_PROMPT.format( genre=genre, total_chapters=total_chapters, chapter_words=chapter_words ) # 大纲是整本书的地基,失败后最多重试三次,避免坏数据流入主循环 for attempt in range(3): try: return parse_json(logits(prompt, temperature=0.3, max_tokens=8192)) except json.JSONDecodeError: print(f"大纲 JSON 解析失败,准备第 {attempt + 1} 次重试") time.sleep(5) raise RuntimeError("大纲连续三次生成非法 JSON")说明:连续三次失败直接终止比硬着头皮跑下去更划算,因为坏大纲会导致几百章全废。加了 5 秒等待,避免临时限流下立刻重试又被拒。
然后是单章生成。
def gen_chapter(chapter_plan: dict, memory: dict, chapter_index: int) -> str: prompt = CHAPTER_PROMPT.format( plot_state=memory["plot_state"], character_state=memory["character_state"], story_memory=memory["story_memory"], chapter_index=chapter_index, chapter_title=chapter_plan["title"], viewpoint=chapter_plan["viewpoint"], goal=chapter_plan["goal"], hook=chapter_plan["hook"], foreshadowing=chapter_plan["foreshadowing"], chapter_words=3000, ) return logits(prompt, temperature=0.85, max_tokens=8192)说明:memory是一个字典,包括plot_state、character_state和story_memory。它来自 Memory 模块,而不是随手上一个字符串。这种结构化设计能在 writer 每次调用前把最关键的人物和主线状态塞进 prompt 顶部,是避免“主角换人”的关键手段。
然后是 Memory 模块的摘要和压缩。
MEMORY_PROMPT = """你负责维护小说项目记录。 已有摘要:{old_memory} 新章节摘要:{chapter_summary} 请合并为一段不超过 {max_words} 字的故事记忆,必须保留: 1. 当前主线进度到哪一步。 2. 人物关系发生的最大变化。 3. 已埋下但未回收的伏笔。 4. 主角当前的实力/状态。 只输出合并后的记忆,不要输出分析。""" def summarize_chapter(chapter_text: str) -> str: prompt = f"请用不超过 300 字概括这一章的情节推进、人物变化和伏笔:\n{chapter_text}" return logits(prompt, temperature=0.2, max_tokens=800) def merge_memory(old_memory: str, chapter_summary: str) -> str: prompt = MEMORY_PROMPT.format( old_memory=old_memory, chapter_summary=chapter_summary, max_words=1200, ) return logits(prompt, temperature=0.2, max_tokens=1800)逻辑说明:摘要和合并都用低温,压缩记忆不是创作,要的是稳定。章节摘要控制在 300 字,合并后的全局记忆控制在 1200 字,这个量级放进上下文不会干扰正文生成。前文全部内容通过摘要层参与决策,而不是原文堆叠。
最后是主循环、落盘和断点续跑。
def load_done() -> set[int]: p = Path("progress.json") if p.exists(): return set(json.loads(p.read_text(encoding="utf-8"))) return set() def update_done(index: int) -> None: done = load_done() done.add(index) Path("progress.json").write_text( json.dumps(sorted(done), ensure_ascii=False), encoding="utf-8" ) def save_chapter(index: int, title: str, content: str) -> None: safe = "".join(c for c in title if c not in "\\/:*?\"<>|") path = Path("out") / f"{index:04d}_{safe}.md" path.write_text(content, encoding="utf-8") def run(novel_cfg: dict) -> None: outline = plan_novel(**novel_cfg) Path("out").mkdir(exist_ok=True) memory = { "plot_state": "故事尚未开始", "character_state": json.dumps(outline["characters"], ensure_ascii=False), "story_memory": "故事尚未开始", } done = load_done() chapter_index = 0 for volume in outline["volumes"]: for chapter_plan in volume["chapters"]: chapter_index += 1 if chapter_index in done: continue text = gen_chapter(chapter_plan, memory, chapter_index) save_chapter(chapter_index, chapter_plan["title"], text) summary = summarize_chapter(text) memory["story_memory"] = merge_memory(memory["story_memory"], summary) update_done(chapter_index) print(f"已完成第 {chapter_index} 章")说明:每次写完一章,做摘要、更新记忆,再立即标记完成。load_done和update_done读写progress.json,记录已完成章节编号。这样即使进程在 200 章时挂掉,重启后run会跳过已完成章节,只从进度继续。这个设计是长任务能不能跑过夜的命门。
3.4 断点续跑与数据目录设计
拿到标题里那个.zip压缩包时,第一个动作不是急着双击运行,而是解压后先确认有没有数据目录和进度文件的设计。没有进度机制的长篇生成器,原则上不能用。一个干净的数据目录大致是这样:
out/ 0001_第一章.txt 0002_第二章.txt ... progress.json章节正文用独立文件保存,进度用progress.json记录。我习惯把章节原始文本和后续改写版本分开存放,避免生成器回写时把原稿覆盖掉。所有文件统一用 UTF-8 编码,标题里的特殊字符要在写文件前过滤,否则 Windows 上会报路径错误。这些细节不复杂,但都能在长任务跑到一半时给你省下大把时间。
4. 关键参数与工程配置:让 Gemini 稳定输出不跑偏
4.1 温度调度表:规划低、写作高、记忆更低
主循环跑通之后,真正的调参才刚开始。长篇小说生成链路里,温度不是一次性设置。核心原则是:创作性任务用高温,结构性任务用低温。如果你的所有调用都用 0.7,会出现大纲松散、摘要啰嗦、正文平淡的现象。我一般按角色分开设置:Planner 温度取 0.2 到 0.3,保证大纲逻辑清晰;Writer 温度取 0.8 到 0.9,让文风有变化;Memory 模块的摘要和合并温度取 0.1 到 0.2,稳定压缩信息;Critic 校验温度直接用 0,要把不确定性降到最低。上面代码里已经体现了这个思路。
为什么不能统一用高温度?因为正文创作需要一些随机性,但大纲和摘要属于“从长文本里抽主干”的任务,随机性只会带来无关细节。反过来,如果你把 Writer 的温度压到 0.3,小说会写得像说明书,所有对话都端着,读者一眼看出是机器写的。这个平衡需要按输出用途单独调,而不是全项目一把梭。
4.2 top_p、max_output_tokens 与中文 token 估算
max_output_tokens是一次生成的硬上限。Gemini 的输出 token 默认值常常偏保守,如果你不调,可能一章只能生成几百字。中文 token 不是一字一 token,3000 字的中文在多数 tokenizer 里大概对应 2000 到 3000 token。简单估算时按 1.2 倍冗余去设,单章正文建议从 8192 起,再少就容易在章节末尾突然截断。top_p 和 temperature 可以配合,但不能同时把两者都推满。我常用 top_p 等于 0.9 到 0.95,在文本重复时可适当降到 0.85。要注意,top_p控制的是候选词集合大小,和 temperature 不是一回事,别只调一个就把所有问题都归咎于“玄学”。
另外还有个容易被忽略的点:max_output_tokens 设太大,不一定能真的输出那么多,Gemini 在长输出时也可能提前结束。不要依赖“输出 token 上限”来精确控制字数。更可靠的方式是在 prompt 里明确写“本章 3000 字左右”,并在生成后检查字数。如果总是偏短,可以拆成两段生成:先写上半章,再续写下半章,最后拼接。很多长章节省不了这种二次生成。
4.3 重试、限流与 429/403/500 的工程处理
长篇小说生成器的运行时间以小时计,网络和配额问题不能靠运气。代码里要套一层统一的重试封装。这套流程默认使用云端 Gemini API,如果你想换成本地大模型部署,只需要把logits函数替换成兼容 OpenAI 协议的客户端,主循环结构不用变。
def call_with_retry(prompt: str, *, retries: int = 6, base_delay: float = 2.0, **kwargs): for attempt in range(retries): try: return logits(prompt, **kwargs) except Exception as exc: text = str(exc) if "429" in text or "503" in text or "500" in text: # 指数退避加随机抖动,避免多个并发任务同时重试造成雪崩 time.sleep(base_delay * (2 ** attempt) + 0.5) continue if "403" in text: # 403 通常是账号权限/服务开通问题,重试不会解决,直接抛出 raise PermissionError("API 返回 403,请检查账号权限和接口开通状态") from exc raise raise RuntimeError("重试多次仍然失败,请检查配额")call_with_retry考虑两类错误:429、503、500 是临时性的,重试有意义;403 是权限问题,重试只会浪费额度。遇到 429 时,指数退避的间隔从 2 秒开始翻倍,最多 6 次;同时加一点随机抖动,防止多个 worker 同时请求时在退避结束后再次撞在一起。在长篇小说场景里,主线循环失败后的默认策略是“保留已落盘的章节,重启后从断点继续”,不要尝试在内存里维护全量状态。
4.4 成本预算:上千次调用的账单长什么样
长文本生成的费用比普通对话高不少,因为每次调用都要带记忆和剧情摘要。做预算时按 token 而不是按字数估算。一次单章扩写的输出可能在 4000 token,输入包含 memory 和章节 prompt,约 1500 到 2500 token;摘要调用输入是全文,输出 800 token;记忆合并输入是摘要,输出 1800 token。粗算下来,300 章的总调用量大约 1000 次,按 Gemini API 的 token 计费,一次完整跑完的账单会在几十到几百元人民币这个量级浮动。具体数字跟你的模型选择、输入输出长度以及是否命中免费额度有关,但原理上必须提前把账算清,否则“一键百万字”会升级成“一键烧掉一个月额度”。
| 环节 | 调用次数 | 输入规模 | 输出规模 |
|---|---|---|---|
| 大纲规划 | 1 | 小 | 大 |
| 单章扩写 | 300 次以上 | 中 | 大 |
| 章节摘要 | 300 次以上 | 大 | 小 |
| 记忆合并 | 300 次以上 | 中 | 小 |
所以我在项目里会先把max_output_tokens和每章目标字数绑死。目标 3000 字的章节,不会把输出上限拉到 16000,否则模型收不住,账单也收不住。成本控制不是最后看账单,而是从 prompt 设计和参数设置那一刻就开始的。
5. 避坑记录:五个让生成器翻车的现场与排查方法
5.1 403 或账号资格报错:先查权限,再改代码
现象:程序启动后第一次调用就返回 403,或在网页端 Gemini 登录后提示 “your account is not eligible for gemini code assist for individuals at this time”,API 调用同样失败。
原因:Gemini 的 API 服务和网页端产品的资格是分开的。API key 所在项目未启用 Generative Language API、key 设了 IP 限制、账号所属组织和地区不在服务范围内,都会让调用失败。
解决:先去 Google Cloud 控制台确认 API 已启用,再去确认 key 没有绑定限制,最后看账号类型是否支持。403 时重试没用,日志里看到 403 就直接终止任务,把错误抛出来,避免无意义重试。
5.2 第 10 章开始主角改名:人物状态没进记忆
现象:前 5 章主角叫“林动”,第 12 章变成“林冬”,后续章节随机乱跳,甚至性别都变了。
原因:每次扩写都是一次全新调用,之前的章节只作为摘要进入上下文。摘要里如果没有保留人物名和状态,模型就会按统计习惯重新起名。
解决:把人物状态表从摘要中独立出来,固定成结构化 JSON,在每次 Writer 调用时原文粘贴到 prompt 顶部。项目里我给 memory 增加了character_state字段,刚开始没加,结果所有性别、阵营、名字都在中途漂移。检查手段是在落盘后跑一个简单正则,把前后几章的人名出现次数拉出来对比,变化大的章节重点人工检查。
5.3 生成内容越写越重复:温度和上下文都在捣乱
现象:第 30 章开始,一段“他眼神一冷,体内灵力翻涌”反复出现,甚至同一章里出现两遍。
原因:模型在高 temperature 下容易在长文本中陷入高频循环片段;同时 prompt 里的旧摘要若有重复语料,也会加重回环。这不是模型“笨”,而是采样参数和历史输入的共同作用。
解决:先降 temperature,从 0.9 降到 0.8;再检查记忆摘要,如果摘要本身就反复出现同一句话,需要重写摘要 prompt;最后,在 Writer prompt 末尾加一句“禁止与本章前三段重复”。这句话看似低级,但对减少局部循环很有效。
5.4 大纲 JSON 反复解析失败:先剥代码块,再截断重试
现象:plan_novel 连续 3 次抛 JSON 解析异常,任务直接中断。
原因:模型输出常常带 markdown 代码块,或因为max_output_tokens太小被截断,导致 JSON 不闭合。
解决:parse_json里先自动剥离 ```json 包装,若文本末尾没有},则在重试前把max_tokens调大,同时让模型“直接输出 JSON,不要包裹代码块”。踩过的坑里,有六成问题出在输出 token 不够,使大纲被腰斩。最省事的做法是给 Planner 单独设 8192 以上输出上限,并限制单卷章节数量,避免大纲过长被截断。
5.5 进程中断后全书重来:增量落盘和断点续跑是底线
现象:程序在第 200 章时抛错,重启后从第 1 章重新生成,白白烧掉上百次 API 调用。
原因:没有做“每章完成立即保存”和“读进度文件跳过已完成章节”的设计,所有章节只存在于内存。
解决:主循环里每写完一章就save_chapter,并调用update_done把章节号写进 progress.json;下次运行时先load_done,遇到已完成的章节直接跳过。这个习惯养成后,长任务出问题的心情会好很多。还有一个相关坑是直接 append 到单个 txt 文件,写着写着文件损坏。要按章节独立成文件,每次写入用 UTF-8 编码,写完后立即 flush。
6. 进阶:用回归测试和 AI 编辑回路,把小白文拦在定稿前
6.1 重复率门禁:一个几分钟就能写好的检查脚本
质量门禁至少要做自动化,不能光靠人看。我用最简单的方式:统计相邻 5 个词的重复片段出现次数,超过阈值就标记为“疑似注水”,打回重写。
from collections import Counter def ngram_repetition(text: str, n: int = 5) -> float: words = [w for w in text if w.strip()] grams = ["".join(words[i:i + n]) for i in range(len(words) - n + 1)] if not grams: return 0.0 repeated = sum(1 for g, c in Counter(grams).items() if c > 1) return repeated / len(grams)这个脚本适合作为第一道门禁,在章节入库后跑一次,重复率高于 0.03 就触发重写。它不能代替人工阅读,但能快速抓住机器文本最常见的“车轱辘话”毛病。跑这个检查时要注意,标点符号不计入词序列,否则一句带感叹号的短句会被反复匹配。
6.2 让第二个模型当编辑:写作-评审-修改回路
进一步的做法是给生成器增加一个 Critic 角色:让一个独立调用把刚生成的章节读一遍,输出“逻辑硬伤、人物不一致、节奏问题”三类评审意见;把意见拼进 Writer prompt,让它重写。一个常见的参数配置是 Critic 温度设为 0,输出严格的问题清单,Writer 第二次调用温度保持在 0.8。这个回路会让每章 API 调用量翻倍,但它直接把“小白文终结者”从一个口号变成了可执行的流程。对成本敏感的项目,可以只对质量门禁判“有问题”的章节跑评审回路。
6.3 什么时候才需要微调
最后聊一下微调。当你的生成器已经跑通,并且积累了一批“生成后人工改过、质量稳定”的章节后,才值得考虑对 Gemini 做大模型微调或 LoRA。微调的目标不是教会模型写小说,而是把你在 prompt 里反复强调的“不要注水、不要重复”变成模型默认行为。在数据量不足几百章之前,微调带来的提升通常不如好好设计 memory 和评审回路。这个顺序反了,你会得到既贵又难排查的结果。
我自己的习惯是,大改 prompt 之前先留一组“坏样本”和“改后样本”,一步一动地比对输出长度、重复率和设定差错率,避免凭感觉调参。长文本生成翻车不是某一次调用的失误,而是流程在哪个口子没堵住。把质量门禁、断点续跑和角色拆分这三样做稳,Gemini 就能从“会写”变成“能稳定写完”。希望帮到你。
本文还有配套的精品资源,点击获取