用大模型构建AI小说创作流水线:从一句话到结构化短篇

发布时间:2026/9/7 2:45:26
用大模型构建AI小说创作流水线:从一句话到结构化短篇 看到这个标题第一反应是这是一条情绪浓度很高的都市情感小说文案。但换一个视角它其实是一道非常适合用来打磨大模型创作能力的输入题——只凭一句包含人物身份、生理状态、关系冲突和通话语境的开场白如何让模型产出一部结构完整、逻辑自洽的短篇故事我在实际接触 AI 写作工具时观察到一种普遍现象让大模型写一个惊艳的开头很容易但让它把开头变成故事却很难。很多初次尝试的人会直接丢给模型一句“帮我写一部小说”结果前几百字还有点张力越往后越像作文模板人物关系说变就变矛盾像被一键解除。问题不在模型能力而在生成流程缺少结构约束。这篇博客会以“旧爱荒芜我怀孕了肚子疼得厉害。电话接通时我声音都在抖。”这一句话作为输入样例从提示词工程、结构化输出、角色一致性校验和章节迭代四个维度演示如何搭建一条可复用的 AI 小说创作流水线。读完它你能跑通这样一条链路输入一句话开头自动拆解出人物关系和核心矛盾生成三幕式故事大纲再输出第一章正文并执行一次最小化的设定一致性检查。这里先给出全文的核心判断大模型写故事真正缺的不是文采而是约束。你给它清晰的人物表、动机链条和叙事结构它就能把一句高冲突台词扩展成一部有起承转合的短篇你只给它一句“随便写”它就只能制造一堆正确的废话。下面从问题拆解开始。1. 这一句话到底需要被解决什么问题很多接触过大模型的人都经历过类似的场景拿到一个很有戏剧性的故事开头让模型续写结果续出来的内容又平又假。不是模型的词汇量不够而是我们缺少一套把故事拆解成可管理单元的工程思维。以“旧爱荒芜我怀孕了肚子疼得厉害。电话接通时我声音都在抖。”为例这句话天然包含了以下需要后续处理的信息叙述者是一位孕妇正处于身体危机的状态电话接通的对象是某种意义上的旧爱说明两人之间存在未了结的情感关系叙述者选择在身体最脆弱的时候打这通电话说明这次联系一定有强动机可能是求助、告别或者摊牌。这些都是模型在生成后续章节时必须持续遵守的约束。如果直接把这句话丢给模型让它“继续写”模型大概率会写出一个模糊的医院场景然后让旧爱突然出现最后以和解收场。这种走向不是模型不理解人物而是没有被告知故事的核心矛盾是什么、人物各自的动机是什么、结局应该朝哪个方向收敛。AI 写作工具和普通聊天框的本质区别就在于前者把创作过程工程化了。我们需要先把一句话里的叙事元素抽出来变成可以验证、可以复用的结构化数据。这篇文章适合三类读者一类是想基于大模型做 AI 写作产品的开发者他们需要的是可控的输出而不是随机文本一类是做内容自动化的技术运营希望批量生成可用的短篇文案还有一类是纯粹被“模型生成长篇故事”这件事吸引想搞清楚背后实现的逻辑。无论哪一类最应该认识到的一点是即便模型拥有再强的语言能力生成流程如果没有结构和校验依旧无法支撑一篇中长篇故事。2. 高冲突开场白背后的叙事单元拆解一句话之所以能成为爆款开头是因为它的信息密度极高。写作者用几个短句就完成了“人物、关系、危机、悬念”四类叙事要素的铺垫。对我们做工程的人来说这四类要素就相当于一条数据记录里的核心字段。先用表格把这句话拆开看叙事单元原文线索在后续生成中的作用人物我、旧爱确定叙述视角和核心关系生理状态怀孕、肚子疼得厉害制造紧急氛围约束场景选择情感标签旧爱荒芜预示关系已经破裂复合不是唯一出口事件动作打电话声音发抖推动情节从一个即时动作展开悬念为什么打这通电话后续情节必须解释的动机拆解之后能看出来模型续写时真正需要追踪的不是“写了多少字”而是这些信息是否保持一致。它不能在下文里突然写“我”轻轻松松地独立产检那与开头的强烈身体不适冲突也不能让旧爱表现得形同陌路那会让整个联系的动机失去支撑。这个道理其实和做结构化数据建模很像先定义实体再定义关系最后定义状态变化。实体是人物关系是“旧爱”背后的复杂情感状态变化是怀孕、疼痛、通话带来的剧情推动。很多 AI 写作提示词写不好的原因就在这一步直接把整段描述塞给模型要求它一口气完成所有事。模型在短文本内还能勉强维持一旦生成超过一两千字早期设定就会被慢慢遗忘。必须把这句话先解析成人物卡、冲突卡和场景卡再让模型基于这些卡片去生成。这样每次生成都相当于从数据库读取设定而不是让模型去记忆一段很长的上下文。我建议你以后拿到任何一句高冲突文案都先用表格把它拆成上面的叙事单元。这不是多余的功夫而是整条流水线的地基。3. 为什么“帮我写一部小说”这类提示词没有用很多人在第一次尝试大模型写作时都会写出类似这样的提示词帮我根据“旧爱荒芜我怀孕了肚子疼得厉害。电话接通时我声音都在抖。”续写一部小说要求文笔好有感染力。这类提示词的问题在于它把所有自由度和所有责任都交给了模型。模型确实会回应但它没有稳定的人物设定可参考没有清晰的矛盾起点不知道冲突应该在哪个阶段爆发。于是生成结果往往出现下面几种问题第一人物关系摇摆不定。开头可能是打电话向旧爱求助写了一段之后旧爱突然变成深情守护者矛盾被快速消解再过几章两人的立场又可能反着来。这属于典型的一致性缺失。第二情节缺乏推进逻辑。没有大纲约束时模型倾向于在一个情绪场景里反复打转用大量环境描写和心理独白填充字数但故事始终没有往前走。它甚至会因为上下文窗口里的情绪信息过多进入一种“情绪循环”状态。第三结尾自动鸡汤化。模型在没有任何收束指令时很喜欢把结局写成大彻大悟、原谅一切、岁月静好。这种结尾从单章看是流畅的放在一个高冲突故事里却会让读者非常泄气。从技术上解释这就是纯自由生成模式的局限。模型只能在给定上下文中做逐词预测它并没有一个“故事全局变量”来存储谁对谁做过什么、哪个伏笔还没回收。想要解决必须借助外部约束。你可以用 JSON 定义人物卡用列表保存大纲用检查脚本比对生成文本中的人物是否一致也可以用记忆模块在每章生成后更新故事状态。无论哪种方案思路都一样把创作变成一次一次有状态、有校验的调用而不是一次性的文本预测。4. 环境准备与工具选型下面进入可执行的环节。这一节的所有代码以 Python 为例使用 OpenAI 兼容接口调用大模型并用 pydantic 做结构化输出校验。这套方案同样适用于接入了 OpenAI 兼容接口的各类国产模型或本地部署服务版本细节请以实际项目官方文档为准。准备一个干净的 Python 3.9 以上环境建议使用虚拟环境隔离依赖。项目里新建一个requirements.txtopenai1.0.0 pydantic2.0.0 python-dotenv1.0.0安装依赖pip install -r requirements.txt然后创建.env文件保存 API 密钥和模型配置。需要注意密钥文件绝不能提交到 Git 仓库建议把.env写入.gitignore。# 文件路径.env LLM_API_KEY你的密钥 LLM_BASE_URLhttps://api.openai.com/v1 LLM_MODELgpt-4o-mini如果你的模型服务商提供了兼容地址把LLM_BASE_URL改成对应地址即可。这里的gpt-4o-mini是示例模型名实际使用时按你自己可用的模型名填写。代码入口文件命名为novel_generator.py。之所以选择 OpenAI 兼容接口是因为它能屏蔽不同模型服务商的差异你的代码只需要写一套客户端逻辑换服务商时只需要修改环境变量。在生产项目中这套方式也更容易做多模型切换和降级容错。5. 从一句话到结构化故事生成完整示例这一节是全文的重点我会分四步演示如何实现一个最小可用的 AI 小说生成器。整个流程是定义结构化输出模型、生成故事设定、生成第一章正文、执行一致性检查。5.1 定义故事设定卡用 pydantic 定义输出结构让模型返回的内容必须包含固定字段。# 文件路径novel_generator.py from typing import List from pydantic import BaseModel, Field class Actor(BaseModel): 故事角色。 name: str Field(description角色姓名或称呼) role: str Field(description角色在故事中的功能例如女主角、旧爱) goal: str Field(description角色在当前故事中的核心愿望) conflict: str Field(description角色面临的核心矛盾) class StorySetting(BaseModel): 故事设定卡。 title: str Field(description故事标题) premise: str Field(description一句话故事前提) actors: List[Actor] Field(description主要人物列表) acts: List[str] Field(description三幕结构依次为第一幕、第二幕、第三幕) ending: str Field(description结局走向)这组类的价值在于约束输出格式。当你要求模型“只输出 JSON”并且 JSON 必须能转换成StorySetting时模型就难以偷懒它必须给出完整的角色表和情节推进方向。这一步是从“自由创作”走向“工程化创作”的关键。5.2 构建提示词并生成设定接下来写一个函数把一句话开头解析成设定卡。# 文件路径novel_generator.py import json import os from dotenv import load_dotenv from openai import OpenAI load_dotenv() client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL) or https://api.openai.com/v1, ) def build_setting_prompt(opening_line: str) - str: return f 你是一名资深小说编辑。请根据用户给出的开场白设计一部短篇故事的完整设定。 开场白{opening_line} 要求 1. 抽取人物、关系、场景、冲突四类信息。 2. 为每个主要人物写出一句核心目标和一句核心矛盾。 3. 设计三幕结构第一幕是起因第二幕是冲突升级第三幕是结局。 4. 结局不要强行和解要符合人物动机。 只输出 JSON不要输出任何解释。格式如下 {{ title: 故事标题, premise: 一句话故事前提, actors: [ {{name: 人物称呼, role: 人物功能, goal: 核心目标, conflict: 核心矛盾}} ], acts: [第一幕概述, 第二幕概述, 第三幕概述], ending: 结局走向 }} def generate_setting(opening_line: str) - StorySetting: response client.chat.completions.create( modelos.getenv(LLM_MODEL, gpt-4o-mini), messages[ {role: system, content: 你擅长将小说创意转化为结构化设定。}, {role: user, content: build_setting_prompt(opening_line)}, ], temperature0.7, response_format{type: json_object}, # 部分服务商不支持时可删除该参数 ) content response.choices[0].message.content data json.loads(content) return StorySetting(**data)这里真正核心的部分是response_format{type: json_object}。它能让支持该参数的模型直接输出合法 JSON减少解析报错。如果你的模型服务商不支持这个参数删除它然后在提示词里继续强调“只输出 JSON”即可。5.3 根据设定卡生成第一章正文我们已经有了一份设定卡现在要让模型基于设定写第一章。# 文件路径novel_generator.py def build_chapter_prompt(setting: StorySetting) - str: actors_text \n.join( f- {actor.name}{actor.role}目标{actor.goal}矛盾{actor.conflict} for actor in setting.actors ) return f 请根据以下设定写一部 3000 字左右小说的第一章。 故事标题{setting.title} 故事前提{setting.premise} 主要人物 {actors_text} 三幕结构 {setting.acts[0]} {setting.acts[1]} {setting.acts[2]} 第一章必须从这句开场白开始 “旧爱荒芜我怀孕了肚子疼得厉害。电话接通时我声音都在抖。” 写作要求 1. 以第一人称视角展开。 2. 保持上述人物关系一致不要凭空增加关键人物。 3. 延续高冲突氛围避免过度抒情。 4. 结尾停留在新的悬念上为第二章做铺垫。 def generate_first_chapter(setting: StorySetting) - str: response client.chat.completions.create( modelos.getenv(LLM_MODEL, gpt-4o-mini), messages[ {role: system, content: 你是擅长都市情感题材的小说写作者。}, {role: user, content: build_chapter_prompt(setting)}, ], temperature0.8, ) return response.choices[0].message.content注意这里没有把第一章正文接在上一次对话之后而是每次重新发送完整的设定卡。这是维持长文一致的常用方法与其让模型在超长上下文中自己回忆设定不如每次显式注入设定。代价是多消耗一些 token但换来的是稳定性和可解释性。5.4 最小化一致性校验设定卡生成之后还需要检查模型生成的文本有没有偏离设定。这里做一个非常轻量的校验函数检查关键人物是否出现以及标题是否出现在正文中。如果有的服务商返回内容缺失会在控制台打印警告。# 文件路径novel_generator.py def check_consistency(setting: StorySetting, chapter_text: str) - list: issues [] if 旧爱荒芜 not in chapter_text: issues.append(正文开头没有包含原开场白标题“旧爱荒芜”。) for actor in setting.actors: if actor.name in [我, 她, 他, 你]: continue if actor.name and actor.name not in chapter_text: issues.append(f设定中的人物“{actor.name}”没有出现在第一章请确认是否为关键伏笔。) if len(chapter_text) 800: issues.append(第一章字数过少可能没有完整展开冲突。) return issues这一步看似简单实则是整个流程中性价比最高的环节。模型输出是概率性的它可能在某一次生成里突然忘记旧爱应该保持距离感如果没有校验这种错误会直接进入下一篇。把校验函数接入 CI 或构建流程是目前比较稳妥的工程做法。5.5 主流程串联最后用主函数把它们串起来。# 文件路径novel_generator.py def main(): opening_line 旧爱荒芜我怀孕了肚子疼得厉害。电话接通时我声音都在抖。 print( 正在生成故事设定...) setting generate_setting(opening_line) print(json.dumps(setting.model_dump(), ensure_asciiFalse, indent2)) print(\n 正在生成第一章正文...) chapter generate_first_chapter(setting) issues check_consistency(setting, chapter) if issues: print(\n 一致性检查发现以下问题) for issue in issues: print(-, issue) else: print(\n 一致性检查通过。) with open(chapter1.md, w, encodingutf-8) as f: f.write(f# {setting.title}\n\n{chapter}\n) print( 第一章已保存到 chapter1.md) if __name__ __main__: main()运行命令很简单python novel_generator.py这组代码完整地演示了“一句话 - 设定卡 - 大纲 - 正文 - 一致性检查”的链路。它不是一篇只能跑一次的脚本而是一个可以不断扩展骨架。后续加角色记忆、伏笔管理、多章生成都是在它上面叠加逻辑。6. 运行结果与效果验证因为大模型输出具有随机性这里不展示某一次具体生成的固定结果而是说明验证方式。正常运行时你会在控制台先看到一份 JSON 格式的故事设定结构类似下面这样{ title: 旧爱荒芜, premise: 怀孕的女主角在最无助的时刻拨通了旧爱的电话却发现自己从未真正了解当年的分手真相。, actors: [ { name: 林知遥, role: 女主角, goal: 弄清楚旧爱隐瞒的真相并保住孩子, conflict: 身体濒临崩溃却无法信任任何人 }, { name: 顾沉, role: 旧爱, goal: 弥补当年的过失却不愿再次靠近, conflict: 越是想保护对方越暴露出当年分手的秘密 } ], acts: [ 第一幕电话接通女主角求他送自己去医院。, 第二幕在医院重逢旧爱的冷漠表象被意外揭开。, 第三幕两人共同面对一个被隐瞒多年的危机抉择是否重建信任。 ], ending: 结局不强行和解两人在真相面前各自做出选择。 }这不是固定输出只是一个结构示意。判断生成是否成功要看这样几点设定卡字段是否完整尤其是actors中每一名角色是否都有goal和conflict。第一章是否是从给定的开场白开始而不是换了另一种叙述。生成文本中是否出现设定卡里没有提到的新关键人物。第一章结尾是否留有悬念而不是直接给出感情定论。一致性检查结果是否通过有没有触发“人物未出现”或“字数过少”的警告。如果你生成的设定卡里出现“两个人物目标完全雷同”的情况通常不代表失败但意味着模型并没有仔细区分人物。更稳妥的判断方式是人工阅读一下acts的三条概述看看每一幕是否形成了冲突推进关系起因、升级、收束。三条都在同一个情绪平面上打转就需要调整提示词或温度参数。7. 常见问题与排查思路问题现象可能原因排查方式解决方案模型返回的内容无法用json.loads解析部分服务商不支持response_format模型输出了多余说明文字打印原始返回内容检查前后是否有 Markdown 代码块删除response_format在提示词末尾再次强调“只输出 JSON”第一章里旧爱的态度和设定卡不一致生成正文时没有显式注入设定信息检查build_chapter_prompt是否完整包含人物卡每次生成都重新发送人物卡和三幕结构不要依赖模型记忆生成内容陷入抒情循环情节没有推进温度设置过高或提示词里缺少“冲突升级”要求观察三幕结构是否被写入上下文降低temperature到 0.6 到 0.7强调每段必须有行动或新信息模型擅自引入关键新角色提示词没有明确禁止新增角色在写作要求中加入“不要增加设定之外的推动剧情的角色”增加一致性校验检测设定之外的频繁出现人名强依赖固定网络模型担心数据安全密钥或对话内容发往第三方服务查看服务商数据处理协议换用私有化部署模型或接入具备数据隔离的兼容服务这里最常被低估的是第一个问题。很多模型服务商的 OpenAI 兼容层实现了接口格式但没实现JSON mode。遇到解析失败时先不要怀疑是自己的代码写错打印原始content看一眼通常就能定位。稳妥做法是在解析前把内容里的 Markdown 代码块标记去掉并截取第一个{到最后一个}之间的片段。8. 最佳实践与工程建议如果这只是一个演示脚本那么到此为止已经够了。但如果你真的想把它做成一个内容产品或者用在日常批量创作中还有几件事值得考虑。第一件事是不要把整本小说塞进一次请求。即使长上下文模型已经普及把几万字一次生成出来仍然会导致设定漂移和风格不稳定。更推荐的做法是“章节窗口”每章独立生成生成前注入该章需要用到的设定卡和前三章摘要。摘要可以由模型自动生成也可以由上一章结尾的悬念直接延续。第二件事是把故事知识放到外部存储里。人物卡、时间线、伏笔清单、章节版本都应该持久化。最简单的做法是每个故事一个目录目录下存setting.json、outline.json、chapter_01.md、summary.json。稍微复杂一点可以接入 SQLite 或向量数据库但不要一开始就引入重型依赖。第三件事是建立风格负面清单。很多模型生成文本会露出明显的“AI腔”比如高频使用“不禁”“瞬间”“仿佛”“心中涌起”这类词。你可以在提示词里加一句“避免使用以下表达不禁、瞬间、仿佛、心中涌起、泪流满面”。也可以生成后跑一个关键词统计脚本把高频词汇打进日志。第四件事是设置内容安全护栏。这类以孕产妇为背景的创作素材可能涉及就医场景、心理危机情节。生成内容只能作为虚构故事使用不能替代专业医疗建议或心理干预。产品层面应在显著位置提醒用户涉及健康决策时务必咨询执业医师。如果自动化批量生成必须保留人工审核环节不能把模型输出直接发布。第五件事是版本管理。你一定会调整提示词而提示词改动可能让生成结果完全变样。建议把每条提示词也纳入版本管理并记录使用的模型名、温度、seed 等参数。这样当你从一批结果中发现某一次生成特别好时可以反推出当时的配置。9. 总结从一句“旧爱荒芜我怀孕了肚子疼得厉害。电话接通时我声音都在抖。”到一套完整的 AI 小说生成流程核心变化不是模型选择而是方式的切换把创作从不可控的自由文本转变成“拆解信息、结构化设定、按大纲生成、逐章校验”的工程链路。真正让 AI 写作工具跑起来的不是哪一版模型突然开窍而是你愿不愿意在调用模型之前多写几个类、多定义几层结构、多跑一次一致性检查。人物卡和冲突卡从哪来从开头这句话里拆出来。章节间为什么不会忘设定因为每次调用都重新注入设定。长篇为什么能保持张力因为三幕结构在最前面就定好了方向。如果你打算动手实践建议先别做复杂的产品设计就从这个脚本开始把生成第一章跑通再往里面加角色记忆、伏笔管理和章节总结。等你积累了几十个故事的生成记录之后你会比刚开始时更清楚哪些字段更重要、哪些提示词纯属冗余。这也是这项技术现阶段最有意思的地方模型负责语言我们负责结构。别指望一个 Prompt 解决所有问题真正可靠的 AI 写作工具本质上是一个不断迭代的工程系统。