[{"data":1,"prerenderedAt":1561},["ShallowReactive",2],{"\u002F2026-03-08":3,"\u002F2026-03-08-rel":185},{"id":4,"title":5,"body":6,"column":167,"date":168,"description":12,"extension":169,"hero_image":170,"meta":171,"navigation":172,"path":173,"seo":174,"series_id":170,"severity":170,"stem":175,"summary":176,"tags":177,"__hash__":184},"posts\u002F2026-03-08-为什么聊天式续写写不出长篇.md","你写一句，AI 补一句——写不出长篇",{"type":7,"value":8,"toc":158},"minimark",[9,13,18,21,24,27,30,33,37,40,43,46,49,53,56,59,62,65,68,71,74,77,83,86,89,94,97,100,103,108,111,114,130,133,135,138,141,144,147,150,153,155],[10,11,12],"p",{},"聊天式续写是最直观的 AI 辅写方案：打开对话框，输入一句提示词，模型回你一段正文，满意就保存，不满意就重试。这套流程在短篇创作上表现尚可，但一旦进入长篇领域（十万字以上），就会陷入无法解决的困境。问题不在模型的生成质量本身，而在结构。",[14,15,17],"h2",{"id":16},"缺陷一上下文窗口的遗忘曲线","缺陷一：上下文窗口的遗忘曲线",[10,19,20],{},"聊天式续写的工作方式很简单：每次写作时，把用户已有的全部上文（或者说上文的最近 N 个 token）和新的提示词一起送给模型。理论上讲，模型看到了完整的前文，应该能保持一致性。",[10,22,23],{},"但实际问题在于两个地方。",[10,25,26],{},"其一是窗口容量本身。一部长篇小说可能有 30 万字以上的内容。即便用最新一代模型，上下文窗口也往往只有 10 万到 20 万 token 的实用规模（留出空间给生成和提示词本身）。换句话说，越写到后面，模型能看到的\"完整前文\"就越来越残缺——它最多看到最近几十万字，之前的设定、伏笔、角色铺垫就开始消失在视野外。",[10,28,29],{},"其二是遗忘的渐进性。即便窗口足够大，模型对位置较远的信息的关注度也会衰减。一句在第 1 章出现的设定，到了第 10 章时，模型的 attention 权重已经显著下降。结果就是：前期精心铺设的世界规则、人物设定、暗线伏笔，在中后期写作时越来越容易被忽略。",[10,31,32],{},"因此，聊天式续写只要进入\"中段\"（通常是总体篇幅的 30%-60% 位置），就开始显现一个普遍现象：前面设定的某条规则突然被违背了，之前出现过的配角突然改了性格，世界背景的某个细节被自相矛盾地改写了。用户要么手动修正，要么放弃这个版本重来——但这套流程本质上没有解决这个问题，只是在打补丁。",[14,34,36],{"id":35},"缺陷二缺乏全局结构导致的情节崩坏","缺陷二：缺乏全局结构导致的情节崩坏",[10,38,39],{},"更深层的问题在于，聊天式续写是完全\"局部最优\"驱动的。模型看到当前的情节状态和用户的续写提示，就生成看起来最合理的下一段——这个\"最合理\"完全是基于局部上下文的计算，跟整本书的宏观规划无关。",[10,41,42],{},"想象一个场景。设定中主角应该在第 10 章经历某个关键的认知转变。但因为聊天式续写没有全局蓝图，模型在写到第 5 章时，可能就把这个转变在某个对话里「提前透支了」——也许是通过某个配角无意中透露的线索，也许是主角在某个场景的突然顿悟。等真的写到第 10 章，用户设想的那个转变场景就显得多余或突兀了。",[10,44,45],{},"类似的崩坏层出不穷：主线推进的节奏被打乱（有的卷写得很快，有的卷反复打转），角色的成长弧线被割裂（某些关键的人物转折变成了无根据的 180 度掉头），冲突的堆积与释放失去均衡（后期要么冲突堆积无处释放，要么所有矛盾突然在一两章内全部坍塌）。",[10,47,48],{},"更棘手的是，这些问题往往到了中后期才会被察觉——因为短期内看起来每一段都是通顺的，只有当读者或作者从整体视角回顾时，才会发现故事的骨架扭曲了。而这时候修复的成本已经极高：可能需要推翻已写的大量内容，重新规划后续。",[14,50,52],{"id":51},"缺陷三设定无法约束导致的规则漂移","缺陷三：设定无法约束导致的规则漂移",[10,54,55],{},"更底层的问题是：聊天式续写没有\"设定约束层\"。模型每次生成时，面对的是一个完全开放的选择空间——用户提交的最新提示词，加上看得到的部分前文，就是全部的约束。",[10,57,58],{},"这意味着什么？意味着即便你在开篇时精心设计了世界观（某个势力如何组织、什么力量受法律保护、什么信息不可能被大众所知），到了中期，这些设定在实际写作中就会悄悄漂移。",[10,60,61],{},"一个典型的例子：设定里\"超自然现象不可能被公众验证\"，但写到某个场景时，模型生成的内容里，主角突然在众目睽睽之下暴露了异常能力。这看起来是个一次性的错误，但更深层反映的是：模型没有对这条规则的持久的、可检索的认知。它只是在前文里看到了一次\"不可能被验证\"这样的提法，而到了新的场景，这条规则就从 attention 视野里消失了。",[10,63,64],{},"人物性格也一样会漂移。一个被设定为\"冷静内向、不会主动表露情感\"的角色，因为连续几章里剧情需要他说一些温情的话，模型就开始习惯性地让他说更多内心独白、做更多主动表白。几十章后，这个角色已经变成了\"偶尔有冷静时刻的热血少年\"——跟原设定判若两人，但这个漂移是不知不觉发生的。",[10,66,67],{},"对策是什么？不是让用户在每一次续写时重复提示\"记住这个角色设定\"，那样会导致提示词爆炸。而是需要一个独立的、可持久保存、可实时检索的\"设定库\"——世界观、规则、角色档案都不再嵌在对话文本里，而是作为独立资产存在，每次生成时被动态调用。",[69,70],"hr",{},[14,72,73],{"id":73},"对应的系统化改造",[10,75,76],{},"上面这三个缺陷指向的不是\"用更好的模型\"或\"写更详细的提示词\"，而是结构性的架构问题。改造方向明确：",[10,78,79],{},[80,81,82],"strong",{},"第一层：世界管理与设定库",[10,84,85],{},"把世界观、规则、势力、地点、异常要素都独立存储成结构化资产。这不只是\"把设定写成文档\"，而是做成可检索、可版本控制、可进行相关性匹配的知识库。",[10,87,88],{},"关键是\"可检索\"。写到某一章时，系统能自动理解当前剧情的背景与参与角色，从全量的世界设定里抽取\"本章需要遵守的那部分规则\"。某个势力的细节不需要每次都喂给模型，但一旦这个势力在当前章节出现，相关的规则和背景就应该被自动调用。",[10,90,91],{},[80,92,93],{},"第二层：结构先于文本",[10,95,96],{},"长篇小说应该先完成宏观规划，再进行逐章生成。这个规划层不是给用户看的装饰性文档，而是真正的结构化输入，用来约束后续写作。",[10,98,99],{},"故事宏观规划应该包括：故事的核心冲突是什么，主角的成长路径分几个阶段，每个阶段的关键转折点在哪里，中间该如何铺垫、如何兑现。基于这个规划，再往下拆卷战略（这本书分几卷，每卷的 narrative goal 是什么），再往下拆章节目录（第几到第几章解决什么情节）。",[10,101,102],{},"每一层的拆解都是可见的、可调整的，而不是\"先写一阵子再看哪里崩坏了\"。",[10,104,105],{},[80,106,107],{},"第三层：生成时的双重约束",[10,109,110],{},"写作过程不再是\"提示词 + 前文 → 正文\"的单向过程，而是\"设定库 + 结构方案 + 前文 + 提示词 → 正文\"的多源约束模型。",[10,112,113],{},"当生成某一章的正文时：",[115,116,117,121,124,127],"ul",{},[118,119,120],"li",{},"世界设定库被查询，相关的规则和背景被检索进来",[118,122,123],{},"当前章节在结构中的位置被明确（这是第几章，在第几卷，对应的情节目标是什么）",[118,125,126],{},"这一章前面已发生的硬事实（前几章里明确发生过的交易、死亡、承诺）被提取出来，作为不可违背的约束",[118,128,129],{},"然后才是生成",[10,131,132],{},"这样的架构下，模型就不再是\"凭空补全\"，而是\"在已经被裁剪好的舞台里推进故事\"。",[69,134],{},[14,136,137],{"id":137},"从聊天式到生产系统的转变",[10,139,140],{},"本质上，这是从\"对话工具\"向\"创作生产系统\"的转变。",[10,142,143],{},"聊天式续写的隐含假设是：模型和用户在对话中共同完成创作，每一步都是交互式的决策。这在短篇、或者用户有丰富创作经验时还能接受。但在长篇小说这样\"周期长、决策多、前后牵连复杂\"的场景，这个假设就破裂了。",[10,145,146],{},"一个更现实的假设是：创作本身有清晰的阶段和流程。开书定盘（明确想写什么、风格基调、目标读者）→ 世界设定（这个故事的舞台是什么样的）→ 宏观规划（故事从何开始、如何推进、在哪里高潮、怎样结尾）→ 章节拆解（把规划落实成具体章节、每章的目标、每章的主要内容）→ 逐章生成（按照章节目标和前文，生成这一章的内容）→ 质量控制（检查是否有设定冲突、逻辑断裂、风格不一致）。",[10,148,149],{},"这个流程中，每一步的输入和输出都是明确的。设定库是独立资产，结构方案也是独立资产，章节执行时既受结构约束，也受设定约束。出现问题时，可以在对应层修复，而不是\"推翻重来\"。",[10,151,152],{},"这也解释了为什么\"管理知识与设定（RAG）、控制写作风格与叙事一致性、最终生成完整章节甚至整本小说\"这样的能力，不能用聊天框搭出来——它们需要一个真正的生产系统来协调。",[69,154],{},[10,156,157],{},"对于新手创作者来说，聊天式续写的诱惑很大：看起来立刻能写，不用先花时间规划。但长篇的现实是残酷的——越是\"先写后想\"，后期的推倒重来成本就越高。而系统化的流程虽然前期看起来更复杂，反而是在为后续的顺畅推进铺路。这不是工程师思维强加给创作的约束，而是长篇故事本身的内在需求。",{"title":159,"searchDepth":160,"depth":160,"links":161},"",2,[162,163,164,165,166],{"id":16,"depth":160,"text":17},{"id":35,"depth":160,"text":36},{"id":51,"depth":160,"text":52},{"id":73,"depth":160,"text":73},{"id":137,"depth":160,"text":137},"内容流水线","2026-03-08","md",null,{},true,"\u002F2026-03-08",{"title":5,"description":12},"2026-03-08-为什么聊天式续写写不出长篇","聊天式续写在长篇小说上的三个结构性失效原因，与对应的系统化改造方向。",[178,179,180,181,182,183],"AI 创作","LLM 应用","系统架构","长篇生成","知识管理","结构化规划","3gqAytNbAf3h3SEeppiy9mJE8Ef_HAeyqESaq_ajUhE",[186,908,1225],{"id":187,"title":188,"body":189,"column":167,"date":895,"description":193,"extension":169,"hero_image":170,"meta":896,"navigation":172,"path":897,"seo":898,"series_id":170,"severity":170,"stem":899,"summary":900,"tags":901,"__hash__":907},"posts\u002F2026-07-08-视频包装与数字人配音.md","配音时长不可控——那就让画面跟着它走",{"type":7,"value":190,"toc":886},[191,194,198,205,208,211,247,250,264,267,270,277,459,472,475,486,489,492,499,510,553,560,563,574,582,591,594,605,608,611,617,652,658,690,696,713,716,722,759,765,784,789,818,821,824,827,830,844,847,850,853,872,875,882],[10,192,193],{},"数字人口播的完整链路已能生成对口型视频原片，但从音频出发反向约束画面节奏、再分层合成包装成可发布的成片，这一环涉及三个工程难点，都不是生成问题，而是一致性与路径确定性问题。",[14,195,197],{"id":196},"tts-时长驱动的反向工作流","TTS 时长驱动的反向工作流",[10,199,200,201,204],{},"MiMo TTS 客户端同步调用返回 base64 音频与实际音长（单位秒），这个时长是后续所有操作的源头。关键约束在于",[80,202,203],{},"时长由上游决定，不能人为指定","。",[10,206,207],{},"拿一个具体例子：用户输入 60 字的口播文案，送给 MiMo preset 模式（冰糖音色、wav 格式）。MiMo 返回的不是\"这段话应该播 30 秒\"，而是\"我合成出来的音频实际是 28.5 秒\"。TTS 生成的时长由语速、音色、模型版本等因素共同决定，变量太多了。",[10,209,210],{},"在这个约束下，整个包装流程必须反向适配：",[212,213,214,225,238],"ol",{},[118,215,216,219,220,224],{},[80,217,218],{},"TTS 合成音频"," → 得到 ",[221,222,223],"code",{},"audioDurationSec","（来自 ffprobe 或 MiMo 返回）",[118,226,227,230,231,233,234,237],{},[80,228,229],{},"飞天对口型"," → 输入 ",[221,232,223],{},"，输出\"这个长度的人物口播视频\"（飞天返回 ",[221,235,236],{},"duration","）",[118,239,240,243,244,246],{},[80,241,242],{},"HyperFrames 包装"," → 模板中的时间线也以这个 ",[221,245,236],{}," 为基准构建字幕、转场、片头片尾的时间点",[10,248,249],{},"如果反过来做——先定好画面框架是 60 秒，再想办法让音频往里塞——就陷入了\"剪音频、变速播放、或者音视不同步\"的泥潭。",[10,251,252,253,256,257,260,261,263],{},"实现上，MiMo 音频落地后先上传我方 S3，用 ffprobe 测长作为权威值（",[221,254,255],{},"probeVideoDurationSec"," 对音频也适用），这个测值再被传给飞天 ",[221,258,259],{},"create_by_audio"," 接口。飞天不是按输入时长生成固定时长的视频，而是真正根据音轨长度对口型——返回的 ",[221,262,236],{}," 几乎就等于输入的音频时长（考虑 AAC 编码器的 priming delay，实测偏差 0.64 帧即 21.33ms，可忽略）。",[14,265,266],{"id":266},"数字人作为独立图层而非重新生成",[10,268,269],{},"一个常见的工程误区：既然最终成片是\"数字人 + 包装\"，能不能让 HyperFrames 直接去生成数字人？不能。飞天数字人是外部供应商接口，响应慢（异步任务，等待 2-5 分钟常态），接口也不开放生成能力给外部工具（只提供对口型 API）。再者，用户可能想用同一份配音试多个数字人形象、或试多个包装模板，重新生成整条视频的成本太高。",[10,271,272,273,276],{},"设计决策是把飞天原片（已完成对口型的 MP4 文件）当作 HyperFrames composition 里的一个 ",[221,274,275],{},"\u003Cvideo>"," 图层。模板里声明这样的结构：",[278,279,283],"pre",{"className":280,"code":281,"language":282,"meta":159,"style":159},"language-html shiki shiki-themes github-light github-dark","\u003Cdiv data-track-index=\"0\">\n  \u003Cvideo data-start=\"0s\" data-duration=\"$MAIN_VIDEO_DURATION\" \n          data-has-audio=\"true\" \n          src=\"$MAIN_VIDEO_URL\">\n  \u003C\u002Fvideo>\n\u003C\u002Fdiv>\n\n\u003Cdiv data-track-index=\"1\">\n  \u003C!-- 字幕、花字、角标等包装层 -->\n\u003C\u002Fdiv>\n\n\u003Cdiv data-track-index=\"2\">\n  \u003C!-- 片头片尾、转场 -->\n\u003C\u002Fdiv>\n","html",[221,284,285,312,339,352,365,375,385,391,407,414,423,428,444,450],{"__ignoreMap":159},[286,287,290,294,298,302,305,309],"span",{"class":288,"line":289},"line",1,[286,291,293],{"class":292},"sVt8B","\u003C",[286,295,297],{"class":296},"s9eBZ","div",[286,299,301],{"class":300},"sScJk"," data-track-index",[286,303,304],{"class":292},"=",[286,306,308],{"class":307},"sZZnC","\"0\"",[286,310,311],{"class":292},">\n",[286,313,314,317,320,323,325,328,331,333,336],{"class":288,"line":160},[286,315,316],{"class":292},"  \u003C",[286,318,319],{"class":296},"video",[286,321,322],{"class":300}," data-start",[286,324,304],{"class":292},[286,326,327],{"class":307},"\"0s\"",[286,329,330],{"class":300}," data-duration",[286,332,304],{"class":292},[286,334,335],{"class":307},"\"$MAIN_VIDEO_DURATION\"",[286,337,338],{"class":292}," \n",[286,340,342,345,347,350],{"class":288,"line":341},3,[286,343,344],{"class":300},"          data-has-audio",[286,346,304],{"class":292},[286,348,349],{"class":307},"\"true\"",[286,351,338],{"class":292},[286,353,355,358,360,363],{"class":288,"line":354},4,[286,356,357],{"class":300},"          src",[286,359,304],{"class":292},[286,361,362],{"class":307},"\"$MAIN_VIDEO_URL\"",[286,364,311],{"class":292},[286,366,368,371,373],{"class":288,"line":367},5,[286,369,370],{"class":292},"  \u003C\u002F",[286,372,319],{"class":296},[286,374,311],{"class":292},[286,376,378,381,383],{"class":288,"line":377},6,[286,379,380],{"class":292},"\u003C\u002F",[286,382,297],{"class":296},[286,384,311],{"class":292},[286,386,388],{"class":288,"line":387},7,[286,389,390],{"emptyLinePlaceholder":172},"\n",[286,392,394,396,398,400,402,405],{"class":288,"line":393},8,[286,395,293],{"class":292},[286,397,297],{"class":296},[286,399,301],{"class":300},[286,401,304],{"class":292},[286,403,404],{"class":307},"\"1\"",[286,406,311],{"class":292},[286,408,410],{"class":288,"line":409},9,[286,411,413],{"class":412},"sJ8bj","  \u003C!-- 字幕、花字、角标等包装层 -->\n",[286,415,417,419,421],{"class":288,"line":416},10,[286,418,380],{"class":292},[286,420,297],{"class":296},[286,422,311],{"class":292},[286,424,426],{"class":288,"line":425},11,[286,427,390],{"emptyLinePlaceholder":172},[286,429,431,433,435,437,439,442],{"class":288,"line":430},12,[286,432,293],{"class":292},[286,434,297],{"class":296},[286,436,301],{"class":300},[286,438,304],{"class":292},[286,440,441],{"class":307},"\"2\"",[286,443,311],{"class":292},[286,445,447],{"class":288,"line":446},13,[286,448,449],{"class":412},"  \u003C!-- 片头片尾、转场 -->\n",[286,451,453,455,457],{"class":288,"line":452},14,[286,454,380],{"class":292},[286,456,297],{"class":296},[286,458,311],{"class":292},[10,460,461,464,465,467,468,471],{},[221,462,463],{},"data-has-audio=\"true\""," 这一行是命门——没有它，渲染器会给 ",[221,466,275],{}," 默认加 ",[221,469,470],{},"muted","，成片就没声音。",[10,473,474],{},"这样做的好处显而易见：",[115,476,477,480,483],{},[118,478,479],{},"数字人原片一旦生成就不动，支持单独重做配音（只需重新调用 TTS 和飞天，不影响包装渲染）",[118,481,482],{},"同一个配音可套多个模板，不需要重新对口型",[118,484,485],{},"包装层的 HTML\u002FCSS 改动不会触发数字人重生成，迭代快",[10,487,488],{},"缺点是需要镜像内内置 Chromium 和 FFmpeg（官方渲染镜像实测 3.72GB），与 API 镜像分离部署。但这换来的是确定性输出和可控的环境——生产机、开发机、CI 跑同一个 composition，出片应该帧级一致（除了字体、系统库这类版本差异导致的微调，都锁版本了）。",[14,490,491],{"id":491},"成片路径的幂等性与状态机",[10,493,494,495,498],{},"用户选定模板、确认方案后，系统提交一个 ",[221,496,497],{},"VideoRenderJob","：输入模板 ID、原片 key、字幕 cues、音频 URL 等，输出成片 objectKey。同一份输入如果重试或重新提交，必须得到同样的输出（或者快速失败）。",[10,500,501,502,505,506,509],{},"状态流转是 ",[221,503,504],{},"queued → preparing → rendering → uploading → completed"," 或 ",[221,507,508],{},"failed","：",[115,511,512,522,528,542,548],{},[118,513,514,517,518,521],{},[80,515,516],{},"queued","：任务入队，等待 worker 消费。这一步是防并发上限的信号量检查——飞天有并发限制（错误码 1001），Redis 信号量 ",[221,519,520],{},"yunclaude:dub:sky:sem"," 兜底（触顶不直接失败，改为入队等待）。",[118,523,524,527],{},[80,525,526],{},"preparing","：下载原片、组装模板（注入数据、渲染占位符）。这一步的幂等性来自 objectKey 的内容寻址——同一个原片 key、同一个模板版本，组装出的 composition.html 比特级相同。",[118,529,530,533,534,537,538,541],{},[80,531,532],{},"rendering","：Chromium 逐帧捕获、FFmpeg 合成。这是确定性的关键——环境锁定（Node 24、Chromium 版本、FFmpeg 版本、字体版本都在镜像里写死），同一个 composition 渲染多次输出帧级一致。M1 POC 中用 seek 点验证：",[221,535,536],{},"t=4.5s→第 135 帧","、",[221,539,540],{},"t=30.0s→第 900 帧","（读原片内置计数器，nb_frames=1800），长时间点零漂移。",[118,543,544,547],{},[80,545,546],{},"uploading","：上传 OSS，记录 objectKey。返回给前端时用现签（每次读时重新签，不存短时 URL）。",[118,549,550,552],{},[80,551,508],{},"：任何一步异常，立即 rollback。计费侧已扣的视频点全额退款（resource operationId 幂等）。",[10,554,555,556,559],{},"失败的兜底是 reaper（BullMQ 的死信队列处理），轮询 ",[221,557,558],{},"status=running"," 超期（>10 分钟）的任务，标记为失败并补退款。",[14,561,562],{"id":562},"音画同步与字幕对齐",[10,564,565,566,569,570,573],{},"成片里字幕何时出现、何时消失，这些时间点由分析步生成的 ",[221,567,568],{},"subtitleCues"," 定义（格式 WebVTT）。一个 cue 的结构是 ",[221,571,572],{},"start → end"," + 文本，例如：",[278,575,580],{"className":576,"code":578,"language":579},[577],"language-text","00:05.000 --> 00:08.500\n这是一段口播文案\n","text",[221,581,578],{"__ignoreMap":159},[10,583,584,585,587,588,590],{},"HyperFrames 的字幕图层根据这些 cues 生成动画：start 时刻淡入，end 时刻淡出。整个成片的时间线参考都来自主视频（",[221,586,275],{}," 元素），而主视频的时长就是飞天返回的 ",[221,589,236],{},"——由音频长度决定。",[10,592,593],{},"一个细节：AAC-LC 编码器有 priming delay（约 1024 samples@48kHz = 21.33ms），所以成片里音频起点和视频起点存在一个已知的 21ms 偏移。但这是编码器层的常数，不是渲染问题，接受即可。",[10,595,596,597,600,601,604],{},"关键词高亮（比如把卖点词着色为黄色）需要在分析时做标记，例如 HTML 标签：",[221,598,599],{},"\u003Cspan class=\"highlight\">关键词\u003C\u002Fspan>","，然后 CSS 定义颜色。模板编写规约里明确禁止动画 ",[221,602,603],{},"letterSpacing"," 等布局属性（会在逐帧捕获时 snap 到整数像素产生抖动），只允许 transform（x\u002Fy\u002Fscale\u002Fopacity）。",[14,606,607],{"id":607},"成片路径的端到端烟测",[10,609,610],{},"发版前的验收分六个阶段：",[10,612,613,616],{},[80,614,615],{},"A. 发版前置","（缺一项线上就炸）",[115,618,619,630,636,646,649],{},[118,620,621,622,625,626,629],{},"后台已配渲染单价（resourceKey ",[221,623,624],{},"video_render_sec","），且 ",[221,627,628],{},"enabled"," 勾选",[118,631,632,633,237],{},"灰度名单已配置（环境变量 ",[221,634,635],{},"VIDEO_RENDER_HTML_USER_IDS",[118,637,638,639,642,643,237],{},"发版机 ",[221,640,641],{},"release.sh"," 已改（支持第 6 个镜像 ",[221,644,645],{},"yc-video-render",[118,647,648],{},"K8s 集群配额已提升（requestQuota 新增 2C\u002F2Gi、limitsQuota 新增 4C\u002F4Gi）",[118,650,651],{},"构建机磁盘充足（≥10GB 空闲）",[10,653,654,657],{},[80,655,656],{},"B. 发版后基础设施","（不通过立即 rollout undo）",[115,659,660,666,673,680,687],{},[118,661,662,663,665],{},"迁移已执行（",[221,664,497],{}," 表已建）",[118,667,668,669,672],{},"渲染 worker 已起（",[221,670,671],{},"READY 1\u002F1","，镜像 tag 与本次发版一致）",[118,674,675,676,679],{},"容器内 hyperframes CLI 可用（",[221,677,678],{},"hyperframes --version"," 返回 0.7.70）",[118,681,682,683,686],{},"容器内中文字体已装（",[221,684,685],{},"fc-list :lang=zh"," 非空）",[118,688,689],{},"worker 连上 Redis 队列（日志无 crash loop）",[10,691,692,695],{},[80,693,694],{},"C. 回归防线：未放量用户零感知","（最高优先级）",[115,697,698,701,704,707],{},[118,699,700],{},"不在灰度名单的用户进增强步看不到\"包装模板\"卡",[118,702,703],{},"旧链路（ffmpeg）完整出片，字幕\u002F转场\u002FBGM 都在",[118,705,706],{},"旧链路仍生成 AI 插片，计费时间线出现 seedance 扣费",[118,708,709,710,712],{},"未放量时 ",[221,711,497],{}," 表没有新行",[10,714,715],{},"这段最重要是因为旧链路服务着所有现存数字人用户。一个新功能开关不应该波及灰度外的用户。",[10,717,718,721],{},[80,719,720],{},"D. 灰度用户正向流程","（核心价值）",[115,723,724,727,734,741,744,747,750,753,756],{},[118,725,726],{},"模板列表可见：增强步看到\"不加包装\"+\"美食探店\"两张卡",[118,728,729,730,733],{},"选模板后出片最终 ",[221,731,732],{},"completed","，可播放",[118,735,736,737,740],{},"成片有口播声音（这是 ",[221,738,739],{},"data-has-audio"," 命门检验）",[118,742,743],{},"片头\u002F片尾\u002F角标都在：0-3s 品牌片头、右上角全程角标、最后 3s CTA",[118,745,746],{},"中文不乱码：片头标题与字幕汉字正常",[118,748,749],{},"字幕关键词高亮：关键词黄色，其余白色，标签没被打碎",[118,751,752],{},"进度实时可见：出片过程中进度条推进，不死在一个数字",[118,754,755],{},"成片链接可下载且长期有效：15 分钟后刷新页面仍能播放",[118,757,758],{},"渲染时长符合预期：60s 成片≤5 分钟（M1 POC 实测 39.5s）",[10,760,761,764],{},[80,762,763],{},"E. 资金正确性","（看后台账单，不能只看页面）",[115,766,767,772,775,778,781],{},[118,768,769,770,237],{},"扣的是视频点不是算力点（计费时间线条目 title 首段是 ",[221,771,319],{},[118,773,774],{},"不再扣 seedance 插片钱（灰度用户的时间线里没有插片扣费）",[118,776,777],{},"结算按实际秒数（units ≈ 成片时长整秒向上取整）",[118,779,780],{},"余额不足时不产生任务、不扣费",[118,782,783],{},"重复提交不重复扣费（operationId 幂等）",[10,785,786],{},[80,787,788],{},"F. 异常路径",[115,790,791,797,803,806,809,812],{},[118,792,793,794,796],{},"渲染失败会退款：任务置 ",[221,795,508],{},"，计费时间线出现退款条目",[118,798,799,800,802],{},"失败同步反映到增强任务：增强任务也变 ",[221,801,508],{},"，前端不会永远停在\"渲染中\"",[118,804,805],{},"排队中可取消并全额退：返回成功，退款到账",[118,807,808],{},"已在渲染中不可取消：返回 409，不退款（CPU 已烧）",[118,810,811],{},"worker 重启不丢任务：任务被重新捞起或置失败退款",[118,813,814,815,817],{},"超时判失败：>10 分钟置 ",[221,816,508],{}," 并退款",[10,819,820],{},"每一条都写了具体的检查命令和判据。比如验证成片有声音，就是直接播放、耳朵听；验证字幕关键词高亮，就是肉眼看颜色；验证退款，就是对比出片前后的计费时间线。",[14,822,823],{"id":823},"设计的权衡",[10,825,826],{},"这套方案的成本是什么？",[10,828,829],{},"首先是镜像大小：官方渲染镜像装了 280 多个 apt 包、node_modules、Chromium 和 FFmpeg，构建出的镜像实测 3.72GB，单独占用一个 K8s node pool，构建时间约 10 分钟，推拉镜像耗时显著上升。但这是\"要么装进 API 镜像肥到 6GB+、拖累三个部署，要么单独镜像\"的取舍——选了单独。",[10,831,832,833,835,836,839,840,843],{},"其次是模板编写规约的学习成本。一个看起来\"普通的网页动画\"可能在逐帧 seek 渲染时炸：GSAP 退场动画必须挂在 clip 内层并补 hard kill，禁止 ",[221,834,603],{}," 等布局属性，中文字体必须显式 ",[221,837,838],{},"@font-face","（容器内用 Noto Sans CJK）。",[221,841,842],{},"hyperframes check"," 作为模板上架的强制 gate，能一次性拦截这类问题。",[10,845,846],{},"获得的是什么？確定性输出。同一个模板、同一份配音，渲染 100 次得到 100 个帧级一致的成片（前提是输入稳定，不变数字人形象、不变字体库版本）。这对 CI 自动回归、成片质量审核都很有意义。还有灵活性：配音、数字人、包装模板可以独立迭代，不互相阻塞。",[14,848,849],{"id":849},"线上部署的最后两步",[10,851,852],{},"发版后的两个关键项，缺一项都会导致灰度用户无法下单：",[212,854,855,864],{},[118,856,857,509,860,863],{},[80,858,859],{},"后台配价",[221,861,862],{},"resourceKey=video_render_sec"," 的单价必须填（单位元\u002F秒），且勾选 enabled。没配的话 chargeResource 会返回 404，用户点开始出片就直接报错。",[118,865,866,509,869,871],{},[80,867,868],{},"灰度名单",[221,870,635],{}," 环境变量决定了谁能看到模板卡。留空 = 全员走旧 ffmpeg 链路（可用于紧急回滚），指定用户 ID = 该用户进入新链路。发版初期应只填 1-2 个测试账号。",[10,873,874],{},"这两个配置是代码之外的硬依赖，容易遗漏。烟测清单里放在最前面，作为\"不做后面全走不通\"的前置项。",[10,876,877,878,881],{},"整个方案的核心原则是",[80,879,880],{},"分层隔离与路径确定性","：TTS 决定时长、飞天决定人像、HyperFrames 决定包装，各层独立演进，成片路径从输入到输出一条流水线，无分支、无条件、无随机。这对一个产生可发布物料的流水线来说，是底线。",[883,884,885],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":159,"searchDepth":160,"depth":160,"links":887},[888,889,890,891,892,893,894],{"id":196,"depth":160,"text":197},{"id":266,"depth":160,"text":266},{"id":491,"depth":160,"text":491},{"id":562,"depth":160,"text":562},{"id":607,"depth":160,"text":607},{"id":823,"depth":160,"text":823},{"id":849,"depth":160,"text":849},"2026-07-08",{},"\u002F2026-07-08",{"title":188,"description":193},"2026-07-08-视频包装与数字人配音","数字人口播成片的最后一环：配音时长不可控，如何反向驱动画面？原片如何作为独立图层无损合成？成片路径如何保证确定性？",[902,903,904,905,906],"TTS","数字人","HyperFrames","音视同步","流水线设计","ISg-n2hvjyCKUW_WMTk0Ais8ji7YhOK5rXPmNyUBWQk",{"id":909,"title":910,"body":911,"column":167,"date":1211,"description":915,"extension":169,"hero_image":170,"meta":1212,"navigation":172,"path":1213,"seo":1214,"series_id":170,"severity":170,"stem":1215,"summary":1216,"tags":1217,"__hash__":1224},"posts\u002F2026-07-02-工作流迁移与复用.md","搬一条生产线，难的不是业务逻辑",{"type":7,"value":912,"toc":1200},[913,916,919,922,926,933,936,942,945,948,955,970,973,976,993,996,1015,1018,1021,1034,1037,1043,1069,1080,1083,1086,1089,1096,1099,1102,1105,1108,1115,1118,1147,1162,1165,1168,1194,1197],[10,914,915],{},"旧平台上已经跑通了漫剧和小说两条完整的创作工作流。新平台已具备图片生成、异步任务、用户鉴权和资源计费的基础设施。现在需要把这两条链路搬过来，但不是照搬代码，而是复用领域逻辑重新适配。",[10,917,918],{},"迁移看似是一个代码挪动的问题，实际上是一个架构解耦的问题。两个平台在三个维度上产生了紧密耦合，直接挪动代码会导致新平台继承旧平台的设计包袱。",[14,920,921],{"id":921},"三处不兼容的耦合",[923,924,925],"h3",{"id":925},"任务队列与调度模型",[10,927,928,929,932],{},"旧平台的工作流采用进程内任务 Map 和命令式 API 网关。漫剧工作流从剧本拆分镜头、生成资产图、生成视频、拼接整集——这一系列操作都是通过 ",[221,930,931],{},"__api\u002Fcomic_*"," 这样的命令端点来驱动的，任务状态存在内存 Map 里，重启进程就丢了。",[10,934,935],{},"新平台设计了一套资源计费底座，任务需要经历预留（reserve）→ 执行 → 结算（settle）→ 失败时退款（refund）的流程。以小说工作流为例，每个阶段（设定、世界观、角色、分卷、大纲、章节）都是独立任务，生成前要冻结估算的算力点，生成后按实际输出字符数结算，多退少补。这套计费流程嵌入到新平台的 Billing 服务里，不能绕过。",[10,937,938,939,204],{},"如果我直接把旧平台的任务调度逻辑搬过来，新平台的任务会跳过 reserve 这一步，造成\"已生成但余额不足无法扣费\"的问题。同样，旧平台的视频任务如果失败只是标记 failed，没有退款逻辑，新平台则需要原子地调用 ",[221,940,941],{},"RefundCharge",[10,943,944],{},"两边的任务模型在原语层面就不兼容。",[923,946,947],{"id":947},"存储路径与对象约定",[10,949,950,951,954],{},"旧平台用本地 JSON 文件存储小说项目数据。小说作品、设定、世界观、角色这些结构化阶段的原始输出被序列化到磁盘上，依赖一套约定好的目录结构。漫剧项目也类似，源项目中通过 ",[221,952,953],{},"featureDir(userId, 'comic')"," 这样的函数来确定资产文件的存储根路径。",[10,956,957,958,961,962,965,966,969],{},"新平台统一使用 S3 \u002F OOS 对象存储加 Prisma 数据库。项目的元数据（作品标题、阶段状态、版本历史）存 Prisma 表，生成的内容（图片、视频、结构化文本）存对象存储并记录 key。例如小说的 ",[221,959,960],{},"NovelSection"," 表存储化后的 JSON 结构和展示文本，章节正文存在 ",[221,963,964],{},"NovelChapterVersion"," 表的 ",[221,967,968],{},"content"," 字段。",[10,971,972],{},"如果我从旧平台直接挪过来一套\"读本地 JSON\"的逻辑，新平台就要维护两套存储系统。更重要的是，新平台的计费系统依赖持久化的结构化数据——只有把规范化后的展示文本存进表里，后台才能追溯某次扣费对应的具体内容。",[923,974,975],{"id":975},"计费模型与资源定价",[10,977,978,979,537,982,985,986,505,989,992],{},"旧平台对小说的计费可能是按生成 token 次数、按模型调用、或者干脆不计费。新平台设计了一套通用资源计费框架：每个资源有一个 key（例如 ",[221,980,981],{},"image_generation",[221,983,984],{},"novel_text_output","），配套一个定价模型（",[221,987,988],{},"PER_CALL",[221,990,991],{},"PER_UNIT","），后台可以随时调价。",[10,994,995],{},"对小说工作流，新平台的选择是按可见字符数计费，不按 token。这意味着：",[115,997,998,1001,1008],{},[118,999,1000],{},"用户最终看到的文本去掉空白后的字符数才会被计入。",[118,1002,1003,1004,1007],{},"模型原始输出如果是 JSON 格式，那么 ",[221,1005,1006],{},"{}"," 括号、字段名、引号、逗号、缩进都不算——只计算最终展示的内容。",[118,1009,1010,1011,1014],{},"每个阶段的计费单位是 ",[221,1012,1013],{},"算力点 \u002F 千字","，后台可配置，用户无感知。",[10,1016,1017],{},"从旧平台的某种计费方式切换到这套新模型，需要重新梳理每个环节的计费触发点。如果只是把旧的生成函数搬过来调用一遍，新平台的 Billing 根本没有机会介入。",[14,1019,1020],{"id":1020},"迁移方案的抽象层次",[10,1022,1023,1024,537,1027,537,1030,1033],{},"面对这三处耦合，如果采取\"完全抽象\"的思路——把工作流定义成一个通用的步骤编排引擎，每个平台只需要实现 ",[221,1025,1026],{},"execute_step",[221,1028,1029],{},"store_result",[221,1031,1032],{},"charge_resource"," 这样的抽象接口——理论上很优雅，但代价是引入了一层不必要的复杂性。通用引擎要支持各种平台的差异，势必要留下很多可配置项，导致理解和维护难度上升。",[10,1035,1036],{},"实际采取的方案是有选择的复用：",[10,1038,1039,1042],{},[80,1040,1041],{},"复用稳定部分","：工作流的步骤编排和领域逻辑。小说工作流的七个阶段顺序（设定 → 宏观 → 世界观 → 角色 → 分卷 → 拆章 → 正文）是领域知识，与平台无关。这套提示词组装、JSON 解析、规范化、展示文本提取的逻辑，从旧平台完整迁移到新平台，TypeScript 化但不改核心算法。漫剧工作流的四阶段（剧本 → 资产 → 分镜 → 成片）也是如此。",[10,1044,1045,1048,1049,537,1052,537,1054,537,1057,1060,1061,1064,1065,1068],{},[80,1046,1047],{},"重新实现易变部分","：任务调度、存储、计费。小说模块新增 ",[221,1050,1051],{},"NovelProject",[221,1053,960],{},[221,1055,1056],{},"NovelChapter",[221,1058,1059],{},"NovelTask"," 等 Prisma 模型，完全按新平台的设计来。生成任务在开始前调用 ",[221,1062,1063],{},"billing.reserveResource()","，成功后提取展示文本、调用 ",[221,1066,1067],{},"billing.settleResource()","，失败时退款。这套接口与 Image Generation 任务的流程完全一致，在平台已有的基础上建造。",[10,1070,1071,1072,1075,1076,1079],{},"具体的模型调用、LLM 的参数、生成的超时策略这些\"具体模型调用\"的细节，留在各自的适配层。例如小说模块的 ",[221,1073,1074],{},"novel-generation.ts"," 只负责组装提示词、调用 LLM、解析结果，不涉及数据库操作；数据库操作交给 ",[221,1077,1078],{},"novel-service.ts","。这样领域逻辑与平台逻辑的边界清晰，后续不同平台可以并行维护。",[14,1081,1082],{"id":1082},"关键的取舍决定",[10,1084,1085],{},"完全抽象的诱惑在于\"一套代码支持多平台\"的承诺。但这需要引入足够的可配置性和接口设计，而代价是灵活性反而下降——当某个平台需要特殊处理某个步骤时，通用引擎不得不打补丁。这在两个平台差异较大的情况下尤其成立。",[10,1087,1088],{},"选择有针对性的复用，意味着承认重复：数据模型要各写一份，任务调度逻辑要各写一份。但这个重复是可控的，因为它们在各自平台内部是一致的。小说模块的任务预留\u002F结算逻辑完全复用 Image Generation 已有的那套，不需要新增抽象层。",[10,1090,1091,1092,1095],{},"另一个关键的取舍是用户模型的配置。旧平台可能让用户选择小说生成用哪个模型，新平台则完全隐藏模型选择，由后台配置或环境变量决定。这简化了前端和 API 的设计——用户请求体里根本不接受 ",[221,1093,1094],{},"model"," 字段，Billing 也不需要按模型定价。代价是失去了\"用户自行选择成本与质量的平衡\"的灵活性，但换来了计费模型的清晰和后台的可控。",[10,1097,1098],{},"这类决定需要在迁移前明确：哪些能力是目标平台\"必须有\"的，哪些是\"很有但可以先不做\"的。漫剧工作流迁移时明确排除了白模（3D 白模式）视频相关的所有功能，不复制 Blender 依赖、不暴露白模提示词输入、成片阶段只处理普通视频。这个决定减少了迁移的复杂性，也避免了在新平台上重新部署 Blender 和白模渲染的基础设施。",[14,1100,1101],{"id":1101},"稳定部分与易变部分的边界",[10,1103,1104],{},"这个分法的关键在于，稳定部分要确实稳定。领域逻辑（各阶段的提示词、章节的上下文组装、长篇的记忆管理）在两个平台上是一样的，因为它反映的是小说创作或漫剧创作的规律。但是，一旦涉及\"这个阶段的输入从哪里读、输出存到哪里、失败后怎么处理\"，就已经是平台相关的。",[10,1106,1107],{},"以小说的世界观生成为例：",[10,1109,1110,1111,1114],{},"稳定部分是 ",[221,1112,1113],{},"worldPrompt(previousSections, projectSettings)"," 这个函数，它组装 LLM 提示词，描述\"根据设定和前序阶段的输出，生成世界观\"。这个函数与平台无关。",[10,1116,1117],{},"易变部分是：",[115,1119,1120,1127,1130,1140],{},[118,1121,1122,1123,1126],{},"生成前如何预留算力点（新平台调 ",[221,1124,1125],{},"billing.reserveResource","，旧平台可能不调）",[118,1128,1129],{},"生成结果是一个 JSON 对象，如何从中提取展示文本（两个平台的数据模型可能不同，但提取逻辑应该是一样的——属于稳定部分）",[118,1131,1132,1133,1136,1137,237],{},"提取后的展示文本存到哪里（新平台的 ",[221,1134,1135],{},"NovelSection.displayText"," 字段，旧平台可能是本地文件 ",[221,1138,1139],{},"sections\u002Fworld.json",[118,1141,1142,1143,1146],{},"失败时如何处理（新平台调 ",[221,1144,1145],{},"billing.refundResource","，旧平台可能是清除临时文件）",[10,1148,1149,1150,537,1153,1155,1156,537,1158,1161],{},"这样划分后，新平台只需要在适配层（",[221,1151,1152],{},"novel-routes.ts",[221,1154,1078],{},"）重新实现存储和计费部分，核心生成逻辑（",[221,1157,1074],{},[221,1159,1160],{},"novel-prompts.ts","）从旧平台迁移过来。",[14,1163,1164],{"id":1164},"实际的迁移清单",[10,1166,1167],{},"从这个分析，迁移的具体工作清单变成：",[212,1169,1170,1176,1182,1188],{},[118,1171,1172,1175],{},[80,1173,1174],{},"评估可复用的代码"," — 在旧平台找出真正与平台无关的部分。对小说工作流，这包括提示词、JSON 解析、规范化逻辑。对漫剧工作流，这包括分镜拆分的逻辑、资产提取的规则。",[118,1177,1178,1181],{},[80,1179,1180],{},"定义新平台的合同"," — 每个工作流步骤的输入输出是什么，资源消耗如何计量。小说阶段的输出是规范化 JSON 加展示文本，消耗的资源是可见字符数。漫剧镜头的输出是镜头配置和首帧图片，消耗的资源是视频秒数（按模型和分辨率估算）。",[118,1183,1184,1187],{},[80,1185,1186],{},"实现平台适配层"," — 数据模型（Prisma 表）、任务调度（与 Billing 集成）、错误处理和重试。这部分代码是新平台特有的，不能也不应该复用。",[118,1189,1190,1193],{},[80,1191,1192],{},"测试边界"," — 在适配层写测试，验证计费逻辑、任务状态机、错误恢复。在稳定部分写测试，验证生成质量、规范化正确性。",[10,1195,1196],{},"不采用这样的分层，而是直接把旧平台的 Next.js 路由、本地文件操作、命令式 API 端点搬到新平台，结果是新平台沦为旧平台代码的容器。后续需要升级某个依赖、调整计费模型、或对接新的生成模型时，都会发现改一个地方影响到另一个地方。",[10,1198,1199],{},"设置清晰的边界，允许有选择的重复，是避免这个问题的方法。",{"title":159,"searchDepth":160,"depth":160,"links":1201},[1202,1207,1208,1209,1210],{"id":921,"depth":160,"text":921,"children":1203},[1204,1205,1206],{"id":925,"depth":341,"text":925},{"id":947,"depth":341,"text":947},{"id":975,"depth":341,"text":975},{"id":1020,"depth":160,"text":1020},{"id":1082,"depth":160,"text":1082},{"id":1101,"depth":160,"text":1101},{"id":1164,"depth":160,"text":1164},"2026-07-02",{},"\u002F2026-07-02",{"title":910,"description":915},"2026-07-02-工作流迁移与复用","两条生产工作流从旧平台迁移到新平台，难点不在业务逻辑重写，而在解耦三处平台依赖——任务调度、存储约定、计费接口。",[1218,1219,1220,1221,1222,1223],"架构","工作流","微服务","平台化","任务队列","计费系统","zEJUEqZgsQkHJOxHWOoc4WL1iYD_NaAli4VZYCye-ZY",{"id":1226,"title":1227,"body":1228,"column":167,"date":1547,"description":1232,"extension":169,"hero_image":170,"meta":1548,"navigation":172,"path":1549,"seo":1550,"series_id":170,"severity":170,"stem":1551,"summary":1552,"tags":1553,"__hash__":1560},"posts\u002F2026-06-25-长文写作的记忆分层.md","三十万字之后，AI 该记住什么？",{"type":7,"value":1229,"toc":1538},[1230,1233,1236,1239,1242,1245,1248,1251,1255,1258,1264,1272,1278,1283,1289,1294,1300,1305,1316,1319,1323,1326,1332,1337,1343,1348,1353,1358,1364,1369,1375,1380,1383,1386,1390,1393,1396,1399,1402,1405,1412,1450,1461,1472,1475,1478,1487,1496,1509,1522,1525,1532,1535],[10,1231,1232],{},"长篇小说写到三十万字后，\"记住前面写了什么\"变成最大的瓶颈。角色在第五章确立的设定，到第二十章突然对不上；伏笔埋了二十章没回收；世界观里的时间线出现逻辑洞；前面说过的势力关系后面又改了。全量注入上一章或全书到上下文里根本不可能，纯向量检索又容易因为相似度不够而漏掉关键的硬约束。",[10,1234,1235],{},"这个问题需要分层的记忆系统。根据信息的稳定性和用途把记忆分成三层：设定层、事实层、文本层。每层的存储方式和检索策略完全不同。",[14,1237,1238],{"id":1238},"为什么不能统一用向量库",[10,1240,1241],{},"一个很自然的想法是\"把所有信息存到向量库里，需要时向量检索\"。但设定信息不行。",[10,1243,1244],{},"世界规则、人物档案这类信息是硬约束，一旦因为向量相似度不够高而没被召回，就会产生直接的设定冲突。模型可能生成\"主角这章掌握了某个禁忌知识\"，但系统没召回\"世界规则里明确说这个知识是自杀性的\"，结果后续剧情完全跑偏。向量检索是概率性的，概率性检索不能承载硬约束。",[10,1246,1247],{},"事实信息（已发生的事件、角色状态变化、伏笔）可以用向量检索，因为\"不完美的匹配\"还能通过更多文本从模型推理出来。如果系统检索到\"角色在某章失去了一条胳膊\"这个事实后又检索到\"这个事件在剧情中的含义是……\"，模型有足够的上下文修复漏掉的细节。",[10,1249,1250],{},"原文（正文片段）是纯量级的数据，全量注入的成本太高，用摘要替代是更经济的做法。",[14,1252,1254],{"id":1253},"设定层结构化存储全量或定向注入","设定层：结构化存储、全量或定向注入",[10,1256,1257],{},"设定层装的是世界级的硬规则和静态档案，包括：",[10,1259,1260,1263],{},[80,1261,1262],{},"世界规则","（约 600 字压缩）",[115,1265,1266,1269],{},[118,1267,1268],{},"现实是否稳定、超常能力是否公开、死亡是否可逆、信息获取是否受限",[118,1270,1271],{},"这个世界\"允许什么、不允许什么\"",[10,1273,1274,1277],{},[80,1275,1276],{},"系统性规则","（通常 4-6 条）",[115,1279,1280],{},[118,1281,1282],{},"力量体系的上限、交易与代价的原理、禁忌知识的危害方式",[10,1284,1285,1288],{},[80,1286,1287],{},"主要势力","（通常 3-5 个）",[115,1290,1291],{},[118,1292,1293],{},"每个势力的名称、目标、常用手段、与其他势力的关系",[10,1295,1296,1299],{},[80,1297,1298],{},"核心人物档案","（通常 4-8 个）",[115,1301,1302],{},[118,1303,1304],{},"每个人物的身份、核心目标、掌握的信息、心理底线",[10,1306,1307,1308,1311,1312,1315],{},"这些东西在整部小说中是不变的（或变化极缓），写作的任何环节都需要遵循它们。设定层必须在调用 AI 生成章节时",[80,1309,1310],{},"全量注入","或",[80,1313,1314],{},"按需定向注入","。全量注入适合小说世界相对简洁的情况；如果世界复杂设定众多，可以做定向注入——比如这一章涉及势力 A，就只注入 A 的档案和相关规则。",[10,1317,1318],{},"关键是：设定层的记忆片段永远不能因为上下文窗口压力而被省略。这是整个系统的天花板。",[14,1320,1322],{"id":1321},"事实层向量检索-时间过滤","事实层：向量检索 + 时间过滤",[10,1324,1325],{},"事实层装的是已经发生的、会影响后续剧情的信息。",[10,1327,1328,1331],{},[80,1329,1330],{},"章节摘要","（每章一句话）",[115,1333,1334],{},[118,1335,1336],{},"\"主角从势力 B 得到了关键物品 X，但同时被势力 A 注意到了\"",[10,1338,1339,1342],{},[80,1340,1341],{},"角色动态状态","（追加式、非覆盖）",[115,1344,1345],{},[118,1346,1347],{},"某角色掌握了什么新信息、失去了什么能力、和某人的关系发生了什么变化",[10,1349,1350],{},[80,1351,1352],{},"伏笔记录",[115,1354,1355],{},[118,1356,1357],{},"埋下的伏笔及其状态（待回收 \u002F 已回收）、涉及的章号",[10,1359,1360,1363],{},[80,1361,1362],{},"时间线","（关键事件及其时间距离）",[115,1365,1366],{},[118,1367,1368],{},"\"第五章后的第三天，X 事件发生\"",[10,1370,1371,1374],{},[80,1372,1373],{},"世界新设定","（剧情中确立、原设定没有的）",[115,1376,1377],{},[118,1378,1379],{},"\"这个世界原本不知道 X，但第十二章中 Y 揭露了 X\"",[10,1381,1382],{},"这一层用向量检索的原因是数据量大（几十万字的小说可能有几百条事实）且不需要完美精确。模型在知道\"五章前主角失去了左腿\"和\"十章前主角加入了某组织\"的基础上，能够合理推理出后续事件。即使系统漏掉了某条非关键事实，模型也不太会产生硬冲突。",[10,1384,1385],{},"时间过滤很重要。检索结果应该默认优先最近的事件，因为离当前章节越近的事件通常影响力越大。\"三章前角色的转折\"比\"五十章前的背景\"更应该被注入。",[14,1387,1389],{"id":1388},"文本层只取最近段落","文本层：只取最近段落",[10,1391,1392],{},"文本层就是原始的正文片段，用于衔接语气和细节。一个几十万字的小说，全部原文根本放不进上下文。",[10,1394,1395],{},"解决方案是简单的：只注入前一章的结尾（约 800 字），这足以让模型维持住文笔连贯性和情感线的延续。对于需要回顾很久之前的情节细节的场景，用事实层的摘要替代——\"第五章中，X 因为 Y 而死亡\"这一条摘要比翻出整个第五章的原文高效得多。",[10,1397,1398],{},"当然也存在\"某章需要直接引用或高度呼应某个很久以前的场景细节\"的情况。这时候文本层可以临时扩大范围，但这是特例，不是常态。",[14,1400,1401],{"id":1401},"具体的实现策略",[10,1403,1404],{},"设定层在每次生成章节前直接注入——要么全部、要么按这章涉及的范围选择。不需要任何检索逻辑，就是结构化的数据块。",[10,1406,1407,1408,1411],{},"事实层维护一个 ",[221,1409,1410],{},"memory.json"," 文件，存储：",[115,1413,1414,1420,1426,1432,1438,1444],{},[118,1415,1416,1419],{},[221,1417,1418],{},"synopsis","：全书概要，每章更新后重写，封顶 1000 字",[118,1421,1422,1425],{},[221,1423,1424],{},"chapterDigests","：逐章一句话摘要",[118,1427,1428,1431],{},[221,1429,1430],{},"characters","：角色的动态状态（与静态档案分开）",[118,1433,1434,1437],{},[221,1435,1436],{},"foreshadow","：伏笔列表，标记待回收或已回收",[118,1439,1440,1443],{},[221,1441,1442],{},"timeline","：关键事件及章号和相对时间",[118,1445,1446,1449],{},[221,1447,1448],{},"worldFacts","：剧情中新确立的设定事实",[10,1451,1452,1453,1456,1457,1460],{},"每章生成完成后，系统调用一次 AI，输入\"旧记忆 + 本章正文\"，要求输出",[80,1454,1455],{},"增量","——这一章新增了什么伏笔、角色状态如何变化、有没有新设定。然后用纯函数 ",[221,1458,1459],{},"normalizeMemory()"," 合并增量到旧记忆里：新伏笔追加、已回收伏笔置状态、角色按 name upsert（同 ID 的覆盖）、synopsis 重写、timeline 追加。",[10,1462,1463,1464,1467,1468,1471],{},"生成下一章时，",[221,1465,1466],{},"buildChapterMessages()"," 的流程变成：注入设定块 → 注入记忆块（由 ",[221,1469,1470],{},"buildMemoryContext()"," 生成） → 注入上一章结尾 → 生成新章。记忆块里包含：最近 6 章摘要、全部角色现状、最后 8 条未回收伏笔、最后 5 条时间线、最后 10 条世界新设定。这些都是硬上限，防止记忆块因为章数增多而无限膨胀。",[14,1473,1474],{"id":1474},"一致性与失败处理",[10,1476,1477],{},"这套流水线的核心难点不是单点生成质量，而是一致性和失败恢复。",[10,1479,1480,1483,1484,1486],{},[80,1481,1482],{},"一致性来自增量而不是合并","。每章记忆更新要求 AI 只输出增量，而不是整个记忆的重写。这样设计有两个好处：一是防止 AI 每次都改掉前面的内容（一种无意义的膨胀），二是增量小得多，解析 JSON 时出错的概率降低。合并逻辑交给纯函数 ",[221,1485,1459],{},"，这个函数可以单测，保证逻辑稳定。",[10,1488,1489,1492,1493,1495],{},[80,1490,1491],{},"同一章重建幂等","。由于 ",[221,1494,1424],{}," 按章号存储，相同章号的摘要会覆盖而不是追加，所以即使记忆重建时对同一章调用多次，也不会出现重复记录。角色状态的 upsert 机制也是同理。",[10,1497,1498,1501,1502,1504,1505,1508],{},[80,1499,1500],{},"记忆更新失败不影响正文","。每章正文先落盘、记忆更新在后。如果记忆更新失败（比如 AI 返回格式错误、JSON 解析异常），降级处理：用这章的 title 或 summary 作为摘要追加到 ",[221,1503,1424],{},"，",[221,1506,1507],{},"lastChapterNo"," 照常推进，其余记忆保持不变。这样下一章的生成可以继续进行，用户不会察觉到记忆层的故障。",[10,1510,1511,1514,1515,1518,1519,1521],{},[80,1512,1513],{},"重建任务的断点续跑","。存量书如果想补全记忆，后台可以跑 ",[221,1516,1517],{},"runMemoryRebuild()"," 任务，按章序遍历，跳过 ",[221,1520,1507],{}," 以内的章（已纳入过的）。这个任务可停、可继续，防止重复扣费。",[14,1523,1524],{"id":1524},"记忆分层的边界",[10,1526,1527,1528,1531],{},"这个设计对应的问题空间是：",[80,1529,1530],{},"长篇写作中保持内容一致性和连贯性","。它解决的是系统层面的记忆管理，不覆盖人工的内容审校和设定调整。",[10,1533,1534],{},"如果作者在某个时刻决定\"我要改前面某角色的设定\"，那是人为修改设定层、然后手动落盘的过程。系统的记忆不会自动同步这种改动，因为改动涉及主观判断。类似的，如果某章生成出来的内容和记忆产生冲突（比如模型莫名其妙加了个新势力），那也需要人工介入编辑，而不是靠记忆系统自动修正。",[10,1536,1537],{},"记忆系统的职责是：提供足够的上下文约束，让模型生成时减少无意义的冲突；一旦冲突出现，系统快速降级，不中断工作流。",{"title":159,"searchDepth":160,"depth":160,"links":1539},[1540,1541,1542,1543,1544,1545,1546],{"id":1238,"depth":160,"text":1238},{"id":1253,"depth":160,"text":1254},{"id":1321,"depth":160,"text":1322},{"id":1388,"depth":160,"text":1389},{"id":1401,"depth":160,"text":1401},{"id":1474,"depth":160,"text":1474},{"id":1524,"depth":160,"text":1524},"2026-06-25",{},"\u002F2026-06-25",{"title":1227,"description":1232},"2026-06-25-长文写作的记忆分层","长篇到几十万字后，用分层记忆解决剧情断裂和设定崩坏——设定层硬约束、事实层概率检索、文本层按需取用，层级的存储与检索策略完全不同。",[1554,1555,1556,1557,1558,1559],"长篇写作","记忆管理","内容生成","AI辅助创作","向量检索","一致性维护","IK_-YIn1bPi-9X-l1KlQn3a-D0cTe45drq3MhS7m-AA",1785406912444]