LLM长文本生成一致性:从误差累积到Agent编排与RAG 📅 发布时间:2026/8/28 11:24:38 👁 浏览次数: 打开一本号称“AI 一口气写出来的小说”你大概率会在第三章左右停下来——不是因为它写得不好而是因为它“哪里都好但哪都不连着”。如果你也经历过这种体验那本文要讨论的就不是“AI 小说到底算不算文学”这种审美问题而是一个更值得技术人深挖的工程问题为什么当前由 LLM 直接生成的长篇小说普遍处于“单段还行、整篇垮掉”的状态真正拖后腿的是模型智力还是应用架构我的判断是这主要不是模型能力的问题而是三个非常具体的技术瓶颈在起作用——自回归生成过程中的误差累积、上下文窗口的有效容量限制以及模型对虚构世界缺乏持久记忆。理解了这三个瓶颈你会顺带理解很多 LLM 应用开发的共性问题为什么长文档生成会前后矛盾、为什么 Agent 需要编排、为什么 RAG 会被反复提及、为什么推理精度在本地部署时如此重要。这篇文章会从“为什么 LLM 小说读不下去”切入拆解这些机制并给出可落地的 Agent 编排、RAG、精度配置与评测方案。1. “我不读 LLM 小说”这不是审美偏见是技术判断先说一个容易被误会的事实。很多人以为“AI 小说读不下去”是因为文笔差。但如果你真的去翻那些生成文本会发现单看任何一个自然段它们的流畅度已经高到和人类写手很难区分。真正让人放弃的是段与段之间、章与章之间缺少一种“咬合感”上一章主角还在城市边缘调查下一章突然拥有了权限极高的后台账号前文反复铺垫的伏笔到后文彻底消失。这种断裂不是文笔造成的而是生成机制造成的。所以我这里说的“我不读 LLM 小说”准确的表达应该是“我不读没有经过工程化编排的 LLM 长文本输出”。把“写一篇小说”直接丢给模型期待它一次性输出几万字的完整故事本质上是在让一个只擅长局部预测的系统去完成一件强依赖全局规划的事。模型每一次生成都只关注“下一个 token 接什么最合理”这决定了它在短文本上可以表现很好但一旦文本变长它就会逐渐失去对“整本书”的把控。这类问题不仅在小说场景里存在。写一个 20 页的项目报告、生成一个 1000 行的代码文件、维护一套多模块的 API 文档只要涉及“长文本、多步骤、前后一致性”你都会撞到同一堵墙。所以“AI 小说读不下去”不是一个边缘娱乐话题而是一个典型的长文本 LLM 应用压力测试。把这个问题研究透了你在做 Agent、RAG、长文档生成时都会更清醒。2. 为什么 LLM 写不出能“读下去”的长篇机制层面的三个瓶颈要真正理解“读不下去”必须回到 LLM 的底层机制。这里不展开复杂的理论只讲三个和长文本生成强相关的核心瓶颈。2.1 自回归生成与误差累积LLM 本质上是一个“下一个 token 预测器”。它每生成一个 token都会把前面生成的所有内容重新作为条件再预测下一个 token。你可以把它想象成一位抄写员他每次动笔前只记得上一句话而不是整本书。当句子很短时这种机制不会出问题但当一个文本长达数千甚至数万字时早期生成内容的微小偏差会在整个生成链条上逐步累积。这就是为什么 LLM 生成的短篇故事经常像模像样长篇却容易后半段跑偏。不是模型“突然变笨”而是它没有机会反复回看前文、修正早期设定。一旦生成路径已经走偏后续内容再流畅也只是在一个错误的地基上堆砌漂亮的句子。2.2 上下文窗口的“物理极限”你可能会说“现在的模型不是动辄支持 128K、200K 上下文吗把整本小说塞进去不就行了”理论上可以但工程上非常不划算。第一token 数量直接对应成本几万字的小说全部放入 context一次生成的费用普通人很难接受。第二注意力机制在超长序列上的有效性和稳定性是递减的上下文越长模型对每一部分的“关注”可能越均匀反而丢失了重点。因此实际的长文本生成很少采用“一次到位”的方式而是分块处理先写大纲再按章节生成最后拼接。问题在于模型每次只看到当前这一块它并不知道前面章节里埋了哪些伏笔、下了哪些判断长程依赖只能靠外部机制去补。这是长篇 LLM 生成持续崩坏的根本原因之一。2.3 一致性、幻觉与缺乏持久记忆在 RAG 应用里我们经常讨论“幻觉”问题模型会一本正经地编造不存在的文档内容。小说创作场景同样存在幻觉只是它表现为“虚构世界内部的设定漂移”。一个正常的作者在写作时会有一个“设定书”主角的性格、能力边界、时间线、配角关系。但模型本身没有数据库也没有记忆。每次生成都是独立调用如果不把相关设定写进 prompt模型就会“合理地”补编一个自认为合理的细节。这个细节单独看没问题放在全书里就是灾难。本质上长篇小说的一致性维护已经超出了“模型聪明不聪明”的范畴变成了一个“模型之外的信息管理问题”。2.4 这不是小说问题是所有 LLM 长文本应用的共性问题把小说换成长文档、代码工程、产品需求书你会发现底层问题一模一样模型每次调用之间没有记忆长上下文成本高生成过程缺少全局状态。所以“AI 小说读不下去”最值得开发者在意的启示是不要让模型去做它不擅长的大跨度任务而应该把任务拆成可验证的小块再用工程手段去维护一致性。下面要讲的 Agent 编排就是在做这件事。3. 方案一Agent 编排把“写小说”变成可监控的工程流水线3.1 为什么要用 Agent 而不是一次 Prompt“Agent”这个概念在最近两年被讨论得非常多。简单说Agent 是一个能够自主拆解任务、调用工具、按步骤执行并观察结果后继续调整的系统。长篇创作天然适合 Agent 化因为它本身就是“计划 → 执行 → 反思 → 再执行”的过程。你不需要一个无所不能的模型而需要一组分工明确的小模型调用Planner负责全书大纲、角色表、时间线规划。Writer负责按大纲写出每一章的正文。Reflector/Editor负责检查章节之间是否有设定矛盾、节奏问题并给出修改建议。这种拆分的收益很直接错误被隔离在单次调用里不会被无限放大全局状态由外部代码维护不再完全依赖模型记忆每一步都可以日志化、可回滚、可人工干预。3.2 示例计划-执行-反思 Agent 骨架下面是一个简化但可运行的 Python 示例。它演示了“计划 → 执行 → 反思”三段式 Agent 的基本骨架llm_complete函数在示例里返回模拟文本确保你直接运行也能看到流程。真实项目中只需要替换这一个函数的实现接入 OpenAI、Ollama、本地模型或 MCP 客户端即可。# 文件路径novel_writer_agent.py # 运行方式python novel_writer_agent.py # 说明这是一个不依赖第三方 SDK 的 Agent 骨架聚焦流程设计。 from dataclasses import dataclass dataclass class Chapter: title: str outline: str content: str def llm_complete(prompt: str) - str: 模拟一次模型调用。 生产环境中替换为真实调用例如 - OpenAI SDK / Anthropic SDK - Ollama 本地模型 - 自建 HTTP 推理服务 # 打印 prompt 长度方便观察每次调用携带的信息量 print(f[LLM 调用] 输入 prompt 长度{len(prompt)} 字符) return f这是模型基于 prompt 生成的内容prompt 起始部分{prompt[:40]} def plan_novel(idea: str) - list[str]: 第一层规划。 真实项目中模型会返回完整的大纲文本再用代码解析成章节列表。 这里为了示例可运行直接返回固定大纲。 prompt f请把小说创意拆成章节大纲创意{idea} raw_outlines llm_complete(prompt) print(raw_outlines) # 生产环境解析 raw_outlines这里作为演示返回人工大纲 outlines [ 第1章主角发现城市导航系统出现不明异常。, 第2章调查过程中遇到关键人物 Dr. 叶。, 第3章系统权限冲突迫使主角做出关键选择。, ] return outlines def execute_chapter(outline: str, setting: str) - Chapter: 第二层执行。 把当前章节的大纲和全局设定传给模型生成正文。 title_prompt f为章节起标题{outline} title llm_complete(title_prompt) content_prompt ( f全局设定{setting}\n f本章大纲{outline}\n 请写出一章约 800 字的正文。 ) content llm_complete(content_prompt) return Chapter(titletitle.strip(), outlineoutline, contentcontent) def reflect(chapters: list[Chapter]) - str: 第三层反思。 把已生成章节的大纲汇总给模型检查设定冲突和节奏问题。 book_summary \n.join(f- {c.outline} for c in chapters) prompt ( 请检查以下小说章节之间存在哪些设定冲突并给出修改方向\n f{book_summary} ) return llm_complete(prompt) if __name__ __main__: # 全局设定在 Agent 外部维护不依赖模型记忆 idea 一位程序员的数字分身接管了城市导航系统 setting 主角林澈28 岁后端工程师时间2042 年城市导航网络是基础设施。 outline_list plan_novel(idea) chapter_list [execute_chapter(o, setting) for o in outline_list] for idx, ch in enumerate(chapter_list, 1): print(f 第 {idx} 章{ch.title} ) print(ch.content[:60]) print() print( 反思阶段 ) issues reflect(chapter_list) print(issues)3.3 代码关键逻辑说明这个示例虽然简单但已经包含了 Agent 编排最核心的三件事第一状态外置。setting和outlines是由 Python 代码维护的模型不需要“记住”上一章内容而是每次调用时由外层代码把需要的上下文注入 prompt。这是 Agent 与“多次直接调用模型”的本质区别。第二任务拆解。规划、写作、反思分别由独立的llm_complete调用完成这意味着每一阶段都可以单独替换模型、单独加日志、单独做人工校对。第三可监控。每个环节都有明确的输入输出出现问题可以直接定位是“大纲错了”还是“正文生成错了”而不是面对一个几万字的大黑盒不知所措。运行这个脚本你会看到模型调用被按顺序触发每个阶段都有输出。真实项目中你只需要把llm_complete替换为真实模型调用并做好大纲解析、token 计数和错误重试即可。4. 方案二用 RAG 维护虚构世界的“记忆”4.1 RAG 解决什么问题RAGRetrieval-Augmented Generation检索增强生成最初是为了解决模型知识过时、容易幻觉的问题。它的核心思路是不把答案直接“背”出来而是先从外部知识库检索相关片段再把检索结果作为上下文交给模型让它基于这些上下文生成回答。在长篇创作里RAG 承担的更像是“设定书管理员”的职责。前面说过模型最大的问题是每次调用都没有记忆。RAG 可以做一个外部记忆库把所有固定的世界观设定、角色档案、时间线、前文摘要都结构化后存入库中每次写新章节之前先根据本章关键词检索出相关的设定片段把它们拼进 prompt。这样模型虽然还是没有记忆但它每次都能“查资料”。4.2 示例最小设定检索器为了不引入额外依赖下面用一个基于关键词匹配的最小示例来演示 RAG 的完整流程。生产环境中你可以把retrieve函数替换为向量检索比如使用sentence-transformers生成文本向量的 embedding再用faiss或主流的向量数据库完成相似度召回。# 文件路径setting_rag.py # 运行方式python setting_rag.py # 说明用关键词匹配演示 RAG 的“检索-注入”流程。 from dataclasses import dataclass dataclass class SettingEntry: subject: str # 设定主题 keywords: list # 关键词用于这篇演示的简易召回 content: str # 设定详情 # 模拟“设定书”生产环境通常来自数据库或向量库 SETTING_BOOK [ SettingEntry( subject主角能力, keywords[林澈, 能力, 导航权限], content林澈拥有城市导航网络的只读权限不能修改任务优先级。, ), SettingEntry( subject反派, keywords[Dr. 叶, 反派, 意图], contentDr. 叶是导航网络的前任架构师希望用导航数据控制城市出行。, ), SettingEntry( subject时间线, keywords[2042, 时间线, 事件顺序], content2042 年 4 月导航网络出现夜间异常2042 年 5 月林澈开始调查。, ), ] def retrieve(scene_keywords: list[str]) - list[SettingEntry]: 检索根据场景关键词找出相关设定。 results [] for entry in SETTING_BOOK: if any(keyword in entry.keywords for keyword in scene_keywords): results.append(entry) return results def build_prompt_with_setting(user_prompt: str, scene_keywords: list[str]) - str: 生成把检索结果拼进 prompt再交给模型。 hits retrieve(scene_keywords) setting_context \n.join( f[{entry.subject}] {entry.content} for entry in hits ) return ( 以下是当前场景可能相关的设定\n f{setting_context}\n\n f请基于以上设定写作{user_prompt} ) if __name__ __main__: prompt build_prompt_with_setting( 写一段林澈在 2042 年 4 月发现导航系统异常的情节。, [林澈, 时间线, 2042], ) print(prompt)这个例子体现了 RAG 的两个关键动作先检索再注入。retrieve把“林澈能力”“时间线”等设定找出来build_prompt_with_setting把它们拼进用户 prompt从而让模型在生成前就掌握与当前场景相关的设定。真实环境里你需要把SETTING_BOOK换成向量库scene_keywords换成用户 prompt 的 embedding 查询但整体流程是完全一致的。4.3 从检索到写入设定更新流程RAG 用于长篇创作时有一个容易被忽略的重点设定库不能只读还要不断写入新状态。写完第三章后主角可能从“只读权限”变成了“临时管理员权限”反派 Dr. 叶可能已经出场并暴露了部分动机。这些动态信息如果不及时更新到库里后续章节检索到的设定就是过期的。所以工程上更推荐的做法是设定库分为两层。第一层是“固定设定”比如世界观、人物初始状态基本不变化第二层是“动态状态”由写作 Agent 每完成一章后自动提取并更新例如时间线推进、角色状态变化。这样既能在生成前精确召回相关信息也能避免 RAG 结果与当前剧情脱节。5. 方案三推理精度配置与本地部署值得关注但不决定一切5.1 fp16 / fp32 / bf16 是什么在聊到本地部署 LLM 的时候fp16、fp32、bf16 这几个词几乎绕不开。它们代表浮点数的不同精度表示方式。精度越高数值越接近原始模型但内存占用也越大精度降低显存占用减小但会引入一定的数值误差。精度数据位宽内存占用常见场景说明fp3232 位高训练基准、调试精度最高但推理时显存开销大fp1616 位中GPU 加速推理速度较快但存在数值溢出风险bf1616 位中大模型训练与推理动态范围接近 fp32更常用需要说明的是这里给出的是一般性特征不针对某个具体模型。在生产项目里具体使用哪种精度要结合 GPU 型号、显存大小和推理框架的支持情况来确定。5.2 精度对长文本生成的实际影响回到本文的主题推理精度是不是决定“LLM 小说读不读得下去”的关键因素答案是否定的。精度影响的主要是资源边界和局部文本稳定性而不是跨章节的一致性。哪怕你用 fp32 精度把整个设定文本都塞进 prompt该出现的角色动机矛盾、时间线跳转依然会出现因为那是架构层面的问题不是数值精度的问题。这一点很重要尤其是在本地部署时很多人会花大量时间去纠结“用 fp16 还是 bf16”而忽略了更大的质量瓶颈。更明智的顺序是先把上下文管理、设定检索和 Agent 编排做好再回到精度问题上做资源优化。精度优化解决的是“跑不跑得动”架构优化解决的是“生成得好不好”。5.3 本地部署与量化建议如果你只是调用 API完全不需要关心精度参数模型服务端会处理。但如果你想在本地跑一个开源模型就要考虑显存。常见做法是优先使用 bf16因为它在动态范围上更接近 fp32比较稳定如果显存仍然不够再考虑 4bit 或 8bit 量化。在量化或调整精度后建议做一次内容对比测试用同一段小说设定和同一个 prompt分别在不同精度下生成检查输出在“流畅度”和“设定一致性”上是否有明显差异。如果没有明显差异说明当前项目的瓶颈不在精度你不需要在精度上继续投入资源。如果出现明显的语义崩坏再考虑回调精度或增大上下文长度。6. 评测什么算“可读”长文本生成系统的验证方法6.1 为什么 BLEU / ROUGE 不够很多刚接触生成任务的人会下意识想到 BLEU、ROUGE 这类指标。它们确实在机器翻译、文本摘要等场景内有一定参考意义因为它们衡量的是“n-gram 重叠度”。但对于长篇小说这种开放式创作根本没有标准答案谈何重叠度哪怕有参考文本一个能完全复用原文 n-gram 的系统也无法说明它生成的故事是连贯的、有因果的。所以长篇生成需要更贴近内容的评测方法。6.2 LLM-as-Judge 与状态追踪目前在工程里比较实用的是两种方法。第一种是 LLM-as-Judge让另一个能力更强的模型作为裁判按维度给生成结果打分。你可以自定义评分维度例如“角色一致性”“时间线合理性”“伏笔回收情况”。裁判模型不参与写作不容易被当前生成路径带偏。第二种是状态追踪把章节内容转换成结构化状态再对状态做规则检查。下面这个示例展示的是“状态追踪”的雏形让模型抽取每章的时间、角色和关键事件然后通过代码检查时间线是否倒流。# 文件路径consistency_checker.py # 运行方式python consistency_checker.py # 说明演示用结构化状态检查章节间的一致性问题。 def generate_chapter_summary(chapter_text: str) - dict: 真实项目中调用 LLM从章节文本中抽取 JSON 结构。 这里直接返回演示数据。 prompt ( 请从章节文本中抽取结构化信息输出 JSON {time: 章节发生时间, characters: [], key_events: []}\n f章节文本{chapter_text