ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

LLM虚构写作工作流实践:从大纲生成到一致性检查

LLM虚构写作工作流实践:从大纲生成到一致性检查 把 LLM 接入虚构写作最初看起来只是把一个“帮我写小说”的提示词发给模型然后等它输出几千字。真正跑过几条长篇之后会发现写作不是单次生成而是前提设定、大纲规划、场景扩展、角色一致性、语言打磨等多个环节的连续配合。LLM 真正释放的不是“自动写作”的能力而是把创作者的注意力从“别把角色眼睛颜色记混”“这几章时间线到底过了几天”这类机械劳动中腾出来。下面这条实践路径可以反复使用从一句话前提开始搭一个能生成大纲、扩写场景、做一致性检查和保留创作记录的小型虚构写作辅助系统。1. 先理解虚构写作到底需要 LLM 做什么1.1 虚构写作不是一次生成而是多轮组合很多人第一次用 LLM 写小说会直接发送一个“帮我写一章”的提示词。结果是模型确实生成了文字但往往无法直接使用开头漂亮中段开始重复人物行动和设定冲突时间线混乱。问题不在于模型能力而在于提问方式。虚构写作天然是分阶段的先有故事前提再拆成结构大纲每个节点还要考虑人物动机、场景目标、世界规则和这一场的情绪变化。这个流程里每一阶段的输出都是下一阶段的输入。把整个过程压进同一个对话窗口会让上下文越来越长早期设定被后续内容稀释模型只能靠猜测补全信息。因此一个可靠的虚构写作 LLM 应用核心不是“怎么让模型写得好”而是“怎么把写作流程拆成可组合的步骤并让每个步骤只做一件事”。这种思路和普通业务系统里“拆模块、定接口、控数据流”是一致的。1.2 LLM 在创作流程中的四个角色虚构写作辅助系统里LLM 通常承担四个不同角色。每个角色对采样参数的要求不一样不能用一个 temperature 走到底。角色主要工作典型输入典型输出推荐 temperature头脑风暴者发散故事前提、人物钩子和冲突点一句话前提十个左右的故事方向1.0 到 1.2结构规划者生成章节大纲、场景目标和节奏前提、人物动机编号大纲0.6 到 0.8场景草稿者把大纲展开成可阅读的场景正文场景大纲、角色卡、世界设定草稿正文0.8 到 1.0一致性检查者找出草稿与设定矛盾的细节草稿、事实规则表矛盾清单或“无冲突”0.2 到 0.4同一个模型可以同时承担多个角色但提示词和参数必须分开。如果让一个写草稿的请求兼职做检查模型很容易为了“维持叙事流畅”而忽略矛盾细节导致检查形同虚设。1.3 从“对话生成”转向“可复用的写作工作流”对话式 LLM 的价值是即时反馈但缺陷是状态不可控。今天写了一段明天想继续只能把昨天的输出复制粘贴回来。而工作流式应用把大纲、角色卡、场景草稿、修订记录都看成结构化数据模型只是数据流中的处理函数。一个最小可复用流程可以设计成输入一句话前提。调用模型生成大纲。从大纲中取出第一个场景标题。读取对应角色卡和世界设定。调用模型扩写场景草稿。调用一致性检查器验证草稿与设定是否冲突。将大纲、草稿、检查结果保存到本地文件。后面所有章节都围绕这条主线展开。这样设计的好处是单步失败不会拖垮整条流程角色卡更新后可以重新生成场景草稿版本也能被 Git 追踪。2. 环境与工具链选型先跑通最小闭环2.1 本地推理引擎还是云端 API虚构写作需要频繁修改和反复生成如果每次都走云端 API成本和延迟会很快暴露。更合适的做法是本地开发时使用开源权重模型和推理引擎快速验证流程需要更高质量输出时再切换到 OpenAI 兼容接口的云端模型。先说本地方案。在 Mac 上比较常见的推理引擎有 Ollama、llama.cpp、LM Studio它们都提供 OpenAI 兼容接口。以 Ollama 为例启动后默认监听http://localhost:11434/v1可以直接用openaiPython SDK 连接不需要额外改装。方案典型工具优点需要注意本地推理Ollama、llama.cpp、LM Studio无接口费用数据不出设备支持离线需要显存或内存足够的设备模型切换要手动拉取云端 APIOpenAI 兼容模型服务模型质量更高参数上下文更大有费用需要管理密钥、限流和日志混合模式本地草稿 云端精修兼顾成本和质量需要两套调用配置注意返回格式差异如果团队以 Java 为主可以关注 Spring AI 生态配合 MCP 接入外部工具结合 RAG 和 Agent 完成更完整的写作工作流。Python 侧则更轻量下面示例先用 Python 跑通。2.2 与精度相关的配置不能只看名字本地推理时经常听到 fp16、bf16、fp32这些精度决定模型权重在内存或显存里的占用大小也影响推理速度和生成质量。对虚构写作这类长文本任务模型文件太大跑不起来精度压得太狠又会出现语言流畅度下降。精度内存占用主要特点适用场景fp32最高数值稳定但模型文件大调试、对比基准fp16中等半精度训练和推理都常用多数推理引擎默认支持bf16中等数值范围和 fp32 接近不容易出现上溢大模型训练和较新设备推理int8 / int4较低进一步压缩体积速度更快本地大模型常见量化方案实际使用中大多数情况不需要手动处理精度因为 Ollama 等工具拉取模型时已经确定了量化方案。但如果要转换权重或使用 llama.cpp 手动量化就要明确目标设备支持哪种格式并在压缩后做一次生成质量对比不能只看能跑通就上线。注意精度越低模型体积越小但不代表质量一定可接受。项目里最好用同一段创作测试文本分别跑原始版和量化版对比人物称呼、设定细节和语言通顺度再决定。2.3 依赖和项目结构创建一个虚拟环境并安装依赖python -m venv .venv source .venv/bin/activate pip install -r requirements.txtrequirements.txt示例openai1.30.0 python-dotenv1.0.0 numpy1.26.0 pyyaml6.0项目目录这里参考下面的结构fiction_writer/ ├── .env ├── requirements.txt ├── config/ │ ├── settings.yaml │ ├── world_setting.md │ └── character_cards/ │ └── lin_che.json ├── fiction_writer/ │ ├── __init__.py │ ├── llm.py │ ├── outline.py │ ├── scene.py │ └── consistency.py ├── output/ │ └── drafts/ └── main.py.env里保存本地推理引擎的连接信息和采样参数LLM_BASE_URLhttp://localhost:11434/v1 LLM_API_KEYollama LLM_MODELqwen2.5:7b LLM_EMBEDDING_MODELnomic-embed-text TEMPERATURE_OUTLINE0.7 TEMPERATURE_SCENE0.9 MAX_TOKENS_SCENE1200这里模型名只是示例实际要看本地已经拉取了哪个模型。Ollama 拉取模型的命令是ollama pull qwen2.5:7b模型授权和版本以官方仓库为准。3. 搭一个虚构写作助手从一句话前提到场景草稿3.1 用 JSON 管理角色卡和世界设定不要把角色设定写在一大段自然语言里。角色卡应该是一个结构化 JSON方便程序按字段读取也方便后续做一致性检查。config/character_cards/lin_che.json示例{ id: lin_che, name: 林彻, age: 28, appearance: { hair: 黑色短发, eyes: 深灰, mark: 左眉有一道旧疤 }, background: 旧港灯塔看守人连续七年没有请假, speech_habit: 安静少用形容词情绪激动时会称呼自己“我”而不是“本人”, goal: 找回七年前在月下航线失踪的妹妹, fear: 变成岛上居民那样失去记忆 }世界设定可以单独写成world_setting.md生成场景时和角色卡一起拼进提示词。这样比把全部信息堆在对话窗口里更可控也方便后续做向量检索。3.2 场景扩写的提示词模板写场景草稿时提示词里必须有三个部分场景大纲、世界设定、角色卡。缺少任何一个模型都会靠想象补全导致设定漂移。import json SCENE_SYSTEM_PROMPT 你是一个虚构写作辅助模型。 你需要根据世界设定、角色卡和场景大纲写出 600 字左右的场景草稿。 要求 1. 角色外观、语言习惯和动机必须与角色卡一致。 2. 不得违反世界设定。 3. 场景中要有目标、阻碍和变化不要只做环境描写。 def build_scene_prompt(scene_heading: str, role_card: dict, world_setting: str) - str: return f{SCENE_SYSTEM_PROMPT} 【场景大纲】 {scene_heading} 【世界设定】 {world_setting} 【角色卡】 {json.dumps(role_card, ensure_asciiFalse, indent2)} 这个函数把几部分内容拼成最终提示词结构清晰。实际项目里可以在 system prompt 中直接放固定要求把场景大纲、世界设定、角色卡放到 user 消息中这样不同消息之间的边界更明确。注意不要把 JSON 角色卡直接塞进自然语言段落里而不做任何说明。模型分不清哪些是设定、哪些是正文要求需要靠“角色卡”这样的标签和明确指令来保持一致。3.3 核心代码大纲生成、场景扩展、一致性检查先封装一个统一的 LLM 调用函数。这里使用 OpenAI 兼容接口本地也可以是 Ollama。fiction_writer/llm.py示例import os from openai import OpenAI def get_client() - OpenAI: return OpenAI( base_urlos.getenv(LLM_BASE_URL, http://localhost:11434/v1), api_keyos.getenv(LLM_API_KEY, ollama), ) def chat(prompt: str, *, temperature: float 0.8, max_tokens: int 1200) - str: client get_client() response client.chat.completions.create( modelos.getenv(LLM_MODEL, qwen2.5:7b), messages[{role: user, content: prompt}], temperaturetemperature, max_tokensmax_tokens, ) return response.choices[0].message.contentmain.py示例跑通从前提到大纲再到第一段场景的最小流程import argparse import json from fiction_writer.llm import chat def generate_outline(premise: str) - str: return chat( f请为一个虚构短篇生成 4 段式大纲前提是“{premise}”。 每段用一句话说明情节输出编号列表。, temperature0.7, max_tokens800, ) def expand_scene(scene_heading: str, role_card: dict) - str: prompt f你是一个虚构写作辅助模型。 场景大纲{scene_heading} 角色卡{json.dumps(role_card, ensure_asciiFalse, indent2)} 请写出 600 字左右的场景草稿必须保持角色卡中的外形、语言习惯和动机。 确保场景里有目标、阻碍和变化。 return chat(prompt, temperature0.9, max_tokens1400) if __name__ __main__: parser argparse.ArgumentParser(description虚构写作助手演示) parser.add_argument(--premise, requiredTrue) args parser.parse_args() outline generate_outline(args.premise) print([大纲]) print(outline) first_scene outline.splitlines()[0] with open(config/character_cards/lin_che.json, encodingutf-8) as f: role_card json.load(f) draft expand_scene(first_scene, role_card) print(\n[第一段场景草稿]) print(draft)这个示例没有引入编排框架但已经体现了“数据流驱动生成”的思路。后续加入 RAG、Agent 时只需要替换chat函数内部实现或把generate_outline、expand_scene改造成可观测的节点。3.4 为什么要把“检查”设计成独立步骤写草稿时模型会优先保证文本通顺很容易牺牲小的设定细节。所以一致性检查必须是一个独立请求使用低 temperature让模型只做“找矛盾”这一件事而不是“边写边检查”。一致性检查分为两层规则检查把角色外观、世界规则等事实写成一个列表让模型逐条比对。上下文检查结合向量检索出的相关设定检查草稿中是否存在冲突。规则检查的代码骨架如下import json from fiction_writer.llm import chat FACT_RULES [ 林彻是黑色短发左眉有旧疤, 月下航线只在夜晚出现, 岛上居民会丢失记忆主角目前没有, ] def check_with_llm(draft: str) - str: prompt ( 你是小说编辑器。请找出草稿中与规则冲突的地方。\n f规则{json.dumps(FACT_RULES, ensure_asciiFalse)}\n f草稿{draft}\n 只输出冲突列表没有冲突就输出空列表 []。不要解释。 ) result chat(prompt, temperature0.2, max_tokens500) return result生产环境建议在返回后尝试解析 JSON。如果解析失败应把这次检查标记为失败并重试不能因为没有输出就默认通过。4. 让写作助手记住设定RAG 和向量检索的轻量用法4.1 简单上下文拼装什么时候会失效如果把整本世界设定都塞进 prompt会带来两个问题一是 token 消耗巨大二是模型在长上下文中会“忽略”关键信息尤其是与当前场景无关的设定占多数时。RAG 在这里的作用不是“联网查资料”而是“在需要时只取出当前场景相关的设定片段”。写作助手生成一场戏时只需要当前地点的规则、出场角色、时间线状态不需要把几十个人物和全部世界观都读一遍。4.2 一个轻量向量检索的代码骨架本地起步阶段用 NumPy 保存向量并做余弦相似度计算就足够。代码重点在于接口要清晰后续换成 FAISS、Milvus、pgvector 时不需要改上层逻辑。import numpy as np class VectorMemory: def __init__(self): self.texts [] self._vectors None def add(self, text: str, vector: list[float]): self.texts.append(text) vec np.array(vector, dtypenp.float32) if self._vectors is None: self._vectors vec else: self._vectors np.vstack([self._vectors, vec]) def retrieve(self, query_vector: list[float], top_k: int 3): q np.array(query_vector, dtypenp.float32) scores self._vectors q top np.argsort(scores)[-top_k:][::-1] return [(self.texts[i], float(scores[i])) for i in top]使用时先把世界设定和角色卡切块生成向量存入索引生成场景前用当前场景标题或角色动机作为 query取出与它最相关的几个片段再拼进提示词。4.3 文本向量 API 未配置时怎么排查一个常见故障是对话生成正常但向量检索返回的结果始终为空。很多人会以为是索引问题实际只是文本向量 API 没有正确配置。由于很多客户端库在向量请求失败时会默认返回空列表不会直接抛异常所以现象往往比原因明显得多。检查项操作预期结果环境变量打印LLM_EMBEDDING_MODEL非空且与索引构建时一致模型名确认本地或远端存在该向量模型能正常返回向量维度打印索引向量 shape维度一致否则检索结果全为 0索引文件检查索引路径和构建时间索引不是空文件日志查看请求日志和错误码没有隐藏的 401、404 或超时错误注意向量维度不一致时矩阵乘法结果会是 0相似度排序没有意义。索引重建后必须重新校验维度。5. 运行验证不能只看程序能启动5.1 端到端运行示例本地推理服务启动后执行python main.py --premise 灯塔看守人林彻发现每晚靠岸的船不属于这个世界一个正常输出示例[大纲] 1. 风暴夜里林彻在旧港灯塔值夜看见一艘载着不存在的港口乘客靠岸。 2. 大副说出林彻小时候的昵称并警告他不要继续记录航线。 3. 林彻决定登船寻找失踪七年的妹妹却在甲板上发现所有船员都没有脸。 4. 叶遥以港口商人的身份出现警告林彻不要追问月下航线的真相。 [第一段场景草稿] 林彻把望远镜放在窗台上雨点已经盖过栏杆。桅杆灯火在雾里出现时他第一反应是海图上没有这条船。船身吃水很深但看不到装卸货物的动静。他举起灯在头顶划了两圈对面却没有回应。 按旧港规定夜间进港必须有对应信标。可这艘船像被潮水推着走一样缓缓停进了三号泊位。林彻拉下防水帽走出灯火室时才发现甲板上站着一排人每个人都维持
返回列表