用时间锚点管理故事世界状态:Nova Story 的架构设计
TL;DR — Nova Story 最核心的设计,不是“在写作工具旁边接了一个 LLM”,而是把小说项目本身建模成一个带世界状态的本地工作区:正文树负责承载创作结构,时间锚点负责切分故事状态,辅助资料按时间层叠加成快照,底层再用
bare Git repo + SQLite virtual workdir管理当前态和历史态。这样一来,分支、提交、语义化 diff、以及项目感知 AI,才真正建立在同一套数据模型之上。
项目地址:Nova Story
一、我为什么没有把小说项目继续建模成“一组文档”
很多写作工具的默认前提,其实都一样:
小说项目 = 若干文档 + 若干目录。
这个前提在短篇写作里通常没问题,但只要进入长篇、系列或者世界观比较重的作品,它就会越来越吃力。
因为作者真正维护的东西,从来都不只是文档。
作者维护的是一个不断变化的故事世界:
- 某个角色在前期还活着,后期已经死亡;
- 某个国家在前几章还是稳定秩序,后几章已经崩解;
- 某条设定在“大战前”成立,到“大战后”可能就已经失效;
- 某段正文依赖的是当时的世界状态,而不是项目里所有资料的全集。
如果这些变化没有被显式建模,写作过程就会越来越依赖作者的脑内记忆。人会混淆,AI 更会混淆。最后常见的问题就会出现:
- 把已经废弃的设定继续当成有效事实;
- 把战前资料带进战后章节;
- 把“当前正在写的部分”与“整个项目的历史信息”混成一团。
Nova Story 从一开始想解决的,就不是“再做一个 Markdown 编辑器”,而是这个更底层的问题:
能不能把小说创作过程建模成一个可分支、可提交、可按故事状态切换上下文的本地工作区?
而这套架构的中心,就是我最后引入的概念:时间锚点(timeline point)。
二、时间锚点到底是什么
时间锚点不是“章节编号”,也不是“剧情大纲目录”。
它更接近一个我在架构层刻意定义的概念:
故事世界状态发生重大变化的断面。
比如一部作品里,可能会有这样的时间锚点:
- 原点:故事开始前,世界仍处于初始状态;
- 锚点 A:王都陷落;
- 锚点 B:主角失忆;
- 锚点 C:新秩序建立。
这些锚点并不需要和章节一一对应。
一个锚点可以跨越多个章节、多个场景,因为它表达的不是“写到哪一章”,而是:
- 这个故事世界当前处于什么状态;
- 哪些设定仍然成立;
- 哪些辅助资料在这个时刻对作者和 AI 是可见的。
这件事看起来只是概念设计,实际上决定了整个系统后面的存储结构、diff 方式,甚至 AI 的上下文注入方式。
2.1 为什么我不想把时间轴做成“章节标签”
如果时间线只是章节标签,那么它最终只会变成另一种目录结构。
但我真正想表达的是:
- 一个世界状态可以对应多个章节;
- 一个章节也不一定意味着世界状态变化;
- 创作时需要切换的,不是“打开哪篇文档”,而是“当前处于哪个故事断面”。
所以在 Nova Story 里,时间锚点承担的是上下文切分机制,而不是排版辅助工具。
图 1:Nova Story 的核心领域模型
graph TD
T0[Origin 原点] --> T1[锚点 A 王都陷落]
T1 --> T2[锚点 B 主角失忆]
T2 --> T3[锚点 C 新秩序建立]
C1[正文节点 章节一] -.锚定.-> T1
C2[正文节点 章节二] -.锚定.-> T2
C3[正文节点 章节三] -.锚定.-> T3
A0[辅助资料 Origin] --> A1[辅助资料在 T1 的增量]
A1 --> A2[辅助资料在 T2 的增量]
A2 --> A3[辅助资料在 T3 的增量]
这张图最重要的不是“箭头怎么连”,而是三件事:
- 正文节点锚定到时间点,而不是直接挂在一个平铺文档列表里;
- 辅助资料不是一份静态全量树,而是沿着时间线不断叠加的快照;
- 同一个时间锚点可以服务多个正文节点,因为它表达的是世界状态,而不是章节编号。
三、我把创作工作区拆成了三套核心数据
围绕时间锚点,Nova Story 的工作区最终稳定成了三套核心数据结构。
3.1 正文树:创作结构的承载体
正文在系统里不是一篇大文档,而是一棵树。
每个节点都可以代表一个章节、场景或者片段,节点之间有明确的:
- 父子关系;
- 同级顺序;
- 标题;
- 正文内容;
- 锚定时间点。
这么做的原因很现实:
- 长篇创作天然是结构化的;
- 节点级移动、重排、拆分,比整篇文档编辑更接近真实写作过程;
- 只有把内容做成结构化节点,后面的语义化 diff 和 AI 操作才有基础。
3.2 时间线:故事状态的有序链
时间线本身不是一个简单数组,而是一条有顺序关系的链。
每个时间点记录:
- 唯一 ID;
- 标签;
- 描述;
- 前一个时间点。
也就是说,系统不仅知道“有哪些时间点”,还知道它们的相对顺序。这让“重排时间锚点”变成了一个一等能力,而不是需要全量重写数据的特殊操作。
3.3 辅助资料:随时间演化的参考快照
辅助资料包括人物设定、组织关系、世界观说明、地点资料、规则笔记等。
但在 Nova Story 里,它们并不是一份简单的文件树,而是:
在原点与各个时间锚点上逐层叠加出来的可见快照。
这意味着系统回答的问题不再是:
- “项目里有没有这份资料?”
而变成了:
- “在当前时间断面下,这份资料是否可见、是否仍然成立?”
这就是我为什么说,时间锚点在这里不是 UI 概念,而是数据模型的中心。
四、底层为什么我最终选了 Git,而不是再造一套数据库
如果只看业务需求,最直观的做法其实是:
- 用一套数据库存项目元数据;
- 用一套表结构存正文树;
- 再用一套表存时间线和辅助资料;
- 需要历史时自己做 version table。
但我最后没有这么做。
Nova Story 的底层选择是:
- 一个 bare Git repository 作为对象和历史内核;
- 一个 SQLite virtual workdir 作为当前分支的可编辑工作区。
4.1 我想要的,不只是“能保存”,而是天然带历史语义
小说创作天然需要这些能力:
- 分支:尝试不同剧情走向;
- 提交:记录阶段性版本;
- 历史:回看某个决策之前的状态;
- 对比:知道这次到底改了什么;
- 回滚:撤回错误修改。
如果这些能力本来就是系统核心,那最直接的方式其实就是:
不要把 Git 当成附属功能,而是把它当成存储内核。
4.2 当前态和历史态在这套架构里是分开的
在 Nova Story 里:
- Git 对象库负责保存历史树、提交、refs;
- VirtualWorkdir 负责保存当前分支的工作区状态;
- 分支再通过
branch-map.json映射到独立的 workdir key。
这比直接把一堆真实文件 checkout 到磁盘上更适合本地创作工作区,因为我真正需要的是:
- 可编辑的当前态;
- 可提交的快照;
- 可追溯的历史;
- 而不是一套必须和物理工作目录强绑定的文件布局。
4.3 连项目元数据和 AI chat,我也尽量放进同一存储模型里
Nova Story 里一些“看起来不像 Git”的数据,也没有额外再引入独立数据库,而是直接放进了自定义 refs:
- 项目元数据:
refs/novel-evolver/meta - AI 聊天记录:
refs/novel-evolver/chats
这样做的好处是,项目相关状态尽量被收敛到了同一类存储语义之下,而不是散落在多个相互独立的数据系统里。
4.4 先把这件事说清楚:Nova Story 不是“把 Git UI 化”
这里很容易产生一个误解:既然底层是 Git,那 Nova Story 是不是本质上只是一个给作者用的 Git 客户端皮肤?
我的答案是否定的。
更准确的说法应该是:
我不是把原生 Git 工作流直接暴露给作者,而是把 Git 当成内部状态机,再在上面建立一层面向创作的领域模型。
作者在界面里看到的是:
- 项目;
- 分支;
- workspace;
- 提交;
- 正文树;
- 时间锚点;
- 辅助资料快照。
而不是:
.git目录;- detached HEAD;
- staging area;
- rebase / stash / cherry-pick 这一类底层概念。
也就是说,Git 在这里承担的是历史对象存储与版本演化引擎,但 Nova Story 对作者暴露的是一套更贴近创作心智模型的控制面。
这也是我为什么坚持用 bare repo + virtual workdir 这套组合,而不是直接让系统操作一个真实 checkout 出来的工作目录。
图 2:Nova Story 的底层存储分层
graph TD
UI[React 工作区 / 项目工作台 / AI 面板] --> RPC[RPC 与 Chat API]
RPC --> DOMAIN[Workspace / Projects / AI Domain]
DOMAIN --> REPO[Bare Git Repository]
DOMAIN --> WD[SQLite VirtualWorkdir]
REPO --> META[refs/novel-evolver/meta]
REPO --> CHATS[refs/novel-evolver/chats]
REPO --> COMMITS[commits / trees / refs heads]
WD --> STATE[index.jsonl]
WD --> TL[timeline.jsonl]
WD --> MS[manuscript/*.md]
WD --> AUX[aux/origin + aux/timeline/*]
这张图里最值得注意的是中间那一层分工:
- Git 负责历史和对象持久化;
- VirtualWorkdir 负责当前可编辑状态;
- 领域层负责把这两者翻译成“正文树 / 时间线 / 辅助资料”这样的创作语义。
也正因为有这层分工,UI 和 AI 才不需要直接理解底层对象树,它们只需要理解领域模型。
为了把这个结构说得更具体一点,我现在的仓库外围组织大致长这样:
1 | <projectId>.git/ |
4.5 bare Git repo 里到底放了什么
先说最重要的一点:这里的仓库是 bare repo。
也就是说,它本身不直接提供一个给作者编辑的真实工作目录。它负责的是:
- 保存 commit / tree / blob;
- 维护
refs/heads/*这样的分支引用; - 保存项目级自定义 refs;
- 作为所有工作区的共同对象库。
在这套结构里:
refs/heads/<branch>指向真正的提交历史;refs/novel-evolver/meta用来保存项目元数据;refs/novel-evolver/chats用来保存 AI 对话相关文件;branch-map.json负责把分支名映射到一个不依赖分支名的workdirKey;workdir.db则是当前可编辑状态所在的 SQLite virtual workdir 存储。
这里还有一个我觉得挺有意思的工程细节:
meta 和 chats 这两个自定义 ref,并不是普通业务表,也不是另一套数据库,而是直接挂了一棵树,用来存像 project.json、chat-list.json、messages/<chatId>.jsonl 这样的文件。这让“项目附属状态”也尽量复用了 Git 的树对象语义。
4.6 真正可编辑的工作区,其实是 SQLite 里的一个虚拟文件系统
作者真正编辑的,不是 bare repo 本身,而是某个分支对应的 VirtualWorkdir。
从实现上看,这个 workdir 对外表现得像一个文件系统:
- 可以
readFile/writeFile; - 可以
mkdir/delete; - 可以列目录;
- 可以对当前状态做 diff;
- 可以把当前状态写成一棵 Git tree。
但这些操作的落点并不是磁盘上一堆散落的真实文件,而是 workdir.db 里的 SQLite 持久层。
我之所以选择这条路,而不是给每个分支都建一个真实目录,主要是因为它能同时满足几件事:
- 当前态是可编辑的;
- 历史态仍然由 Git 对象库统一管理;
- 分支切换和 checkout 不需要真的在磁盘上搬运大量文件;
- 多个分支可以共享同一个对象库,但各自维护独立的当前工作区。
4.7 工作区里的文件为什么长这样
写到这里,工作区内部的文件布局其实就能解释清楚了:
1 | index.jsonl |
这里我没有把全部状态粗暴地塞进一个大 JSON,而是拆成了几类更贴近语义的文件:
index.jsonl:正文树的结构索引,记录id / parentId / title / anchorTimelinePointId;manuscript/<nodeId>.md:每个正文节点自己的正文内容;timeline.jsonl:时间锚点链;aux/origin与aux/timeline/<pointId>:辅助资料的原点层和时间增量层。
这样做有几个好处:
- 正文结构变化和正文内容变化可以分开处理;
- 单节点正文修改不会把整棵树重写成一个巨大的 blob;
- diff 时更容易判断“这是结构变化,还是正文内容变化”;
- AI 读取某个正文节点或某个时间点时,也更容易只装配自己真正需要的那部分上下文。
4.8 一次 commit 和一次 checkout,内部到底发生了什么
如果把整个过程再说“工程一点”,一条分支上的一次编辑大概是这样流动的:
1 | 编辑正文/时间线/辅助资料 |
换成更接近实现的伪代码,大致就是:
1 | const treeHash = workdir.writeTree() |
而 checkout 到某个历史提交时,逻辑也不是在真实文件夹里执行 git checkout,而是:
- 找到目标 commit 的 tree;
- 把对应分支的 VirtualWorkdir 基线切到这棵 tree;
- 然后让领域层重新从 workdir 里读出正文树、时间线和辅助资料。
这件事的结果是:
Git 负责“历史上发生过什么”,VirtualWorkdir 负责“我现在正在编辑什么”。
这两个问题在 Nova Story 里是被明确拆开的,而不是混在一起的。
五、辅助资料为什么必须做成 overlay,而不是“当前唯一真相”
时间锚点真正发挥作用的地方,是辅助资料。
我没有把它设计成“一棵始终只有当前状态的文件树”,而是设计成了一个分层叠加模型:
aux/origin:故事开始前的初始设定;aux/timeline/<pointId>:某个时间点新增或修改的资料层。
当系统要读取某个时间点的辅助资料快照时,不是直接打开一个目录,而是会按顺序把这些层叠起来。
5.1 为什么这里我用的是 overlay 思路
因为我真正想表达的不是“文件覆盖”,而是:
- 哪份资料从什么时候开始成立;
- 哪份资料从什么时候开始失效;
- 同一路径在不同时间点下看到的内容能不能不同。
如果只是简单覆盖,就很难回答“历史上这份资料什么时候被改掉了”。
如果直接保留多份独立快照,又会让写入、diff 和回滚的成本迅速膨胀。
overlay 处在一个刚好合适的位置上:
- 存储的是增量;
- 读取时再构造快照;
- 时间断面的概念天然成立。
5.2 whiteout 是这套模型里很关键但容易被忽略的一环
删除在这套模型里不能只是“物理删掉文件”。
因为一旦真的物理删除,历史就会被破坏。某份资料也就失去了“它曾经存在过,但从某个时间点开始失效”的表达能力。
所以 Nova Story 在辅助资料层里用了 whiteout 语义。
它表达的是:
不是“这个文件从来不存在”,而是“从这个时间点开始,这个路径不再可见”。
这是一个非常重要的差别。
前者描述的是物理存储,后者描述的是故事世界状态。
而 Nova Story 想保存的,恰恰是后者。
5.3 这样一来,AI 读到的也不再是“资料全集”
当我把当前时间点切到某个锚点时,系统实际做的是:
1 | 目标时间点 = T2 |
这件事的价值在 AI 参与写作时会被进一步放大。
因为模型拿到的上下文不再是“项目里所有可能相关的设定”,而是当前这个故事断面下真正可见、真正成立的设定。
这比单纯做一个全量知识库检索要更贴近创作过程。对作者来说,这减少了信息污染;对 AI 来说,这减少了把错误世界状态带进当前章节的概率。
六、为什么我还专门做了语义化 diff 和 revert
如果系统底层已经是 Git,那一个很自然的问题就是:
直接展示 Git diff 不就够了吗?
答案是:对机器够,对作者不够。
Git 能非常准确地告诉我:
index.jsonl变了;timeline.jsonl变了;- 某个
manuscript/<id>.md变了; - 某个
aux/timeline/<pointId>/...路径变了。
但作者真正关心的不是这些底层文件名,而是:
- 哪个章节被移动了;
- 哪个章节的锚定时间点变了;
- 哪个时间锚点被重排了;
- 哪份辅助资料从哪个时间点开始被删除了。
所以在 Git 之上,我又做了一层领域语义翻译。
6.1 语义化 diff 不是替代 Git,而是把 Git 翻译回创作语言
在 Nova Story 里,工作区状态会被拆成三个 area:
| area | 底层变化 | 面向作者的语义 |
|---|---|---|
| content | index.jsonl / manuscript/*.md |
章节新增、删除、重排、改标题、改正文、改锚点 |
| timeline | timeline.jsonl |
时间点新增、删除、改名、改描述、改顺序 |
| aux | aux/origin / aux/timeline/* |
某份辅助资料在哪个时间点新增、修改、删除或失效 |
也就是说,Git 负责给我“真实差异”,领域层再把它解释成“创作差异”。
6.2 这样做之后,单项 revert 才真正成立
只有当系统知道“这是章节顺序变化”而不是“某个 JSONL 行改了”,它才能有意义地支持:
- 撤回单个正文节点的修改;
- 撤回某个时间点的改动;
- 撤回某条辅助资料路径在当前工作区里的变更。
这一步对我来说非常重要,因为我不想让作者面对的是一个抽象的版本控制系统,而是一个能用创作语言解释变化的工作区。
换句话说,Git 在底层是事实来源,但作者最终看到的必须是领域语义。
七、AI 在这套架构里不是外挂,而是运行时的一部分
很多“AI 写作工具”的典型形态,其实都差不多:
- 左边一个编辑器;
- 右边一个聊天框;
- 中间靠 prompt 拼接上下文。
Nova Story 不是这个思路。
在我的设计里,AI 不是拿到一大段字符串然后自由发挥,而是运行在这套项目模型之上。它能读取和操作的对象,天然就是:
- 正文树;
- 时间线;
- 当前时间点可见的辅助资料;
- 当前编辑器上下文。
7.1 上下文不是猜的,而是由编辑器状态显式注入的
AI 运行时会拿到当前编辑器的真实上下文,例如:
- 当前正在编辑的正文节点;
- 当前打开的辅助资料路径;
- 当前时间锚点;
- 当前时间锚点标签。
这意味着模型不再需要靠 prompt 去猜“我现在到底在写什么”,而是直接拿到当前工作区的事实状态。
7.2 AI 也不是直接写文件,而是通过工具操作领域对象
Nova Story 给 AI 注册的是一组项目工具,而不是一堆裸文件接口。它可以:
- 读取或修改正文节点;
- 创建、修改、移动时间锚点;
- 在当前时间断面下读写辅助资料;
- 在必须的时候向作者发起结构化追问。
这件事的意义在于,AI 修改的不是“某个路径下的某个文本文件”,而是正文树、时间线和辅助资料快照本身。
7.3 工具结果还会直接变成 UI 刷新事件
在实现上,AI 工具调用完成后,服务端不会只是返回一段文本,而是会根据工具结果发出工作区刷新事件,让前端知道:
- 应该刷新正文树;
- 还是刷新时间线;
- 还是刷新当前辅助资料;
- 是否需要自动切换到新的时间点或新创建的节点。
7.4 如果把 AI 上下文注入过程说得再工程一点
这部分如果只写成“系统会把编辑器上下文传给模型”,其实还不够具体。
Nova Story 真正做的事情,接近下面这条流水线:
1 | 当前可见聊天分支 |
其中最关键的是“当前编辑器快照”。它不是一段模糊描述,而是一份结构化状态,里面至少会包含:
- 当前 workspaceId;
- 当前激活的正文节点 ID 与标题;
- 当前激活的辅助资料路径;
- 当前时间锚点 ID;
- 当前时间锚点标签。
服务端在真正调用模型前,会把这份快照整理成一条额外的上下文消息。也就是说,模型最终收到的并不是“请根据当前章节继续写”,而更接近:
1 | 当前编辑器:正文节点 id=...;时间锚点 id=...,label=... |
如果用户在输入框里通过 @ 引用了全局 Prompt,这些引用也不会只保留成纯文本,而是会先固化成一份 snapshot,再一起注入到本轮模型消息里。这样做的目的,是避免 Prompt 在后续被改名或改内容后,影响已经发生过的对话语义。
7.5 还有一个很关键的细节:时间锚点切换可以在同一轮工具调用里生效
如果只是把“当前时间点”作为只读上下文传进去,那模型一旦调用 set_current_timeline,后续工具读取还是会看到旧状态。这显然不对。
所以在实现里,我给工具运行时维护了一份可更新的上下文快照。
这意味着同一轮推理里可以出现这样的链路:
1 | 先调用 set_current_timeline 切到某个时间点 |
这件事很小,但我觉得它特别能体现“AI 不是外挂聊天框,而是工作区运行时的一部分”。因为模型不是在回合之间被动等前端切状态,而是在同一轮工具执行里就真正改变了自己之后看到的项目上下文。
图 3:AI 与工作区的数据流闭环
sequenceDiagram
participant U as 作者
participant UI as 编辑器 / AI 面板
participant API as Chat API
participant M as 模型
participant T as Assistant Tools
participant W as Workspace Domain
U->>UI: 发送写作或修改请求
UI->>API: 携带当前编辑器上下文
API->>M: 注入系统提示词 + 项目上下文
M->>T: 调用正文/时间线/辅助资料工具
T->>W: 读写领域对象
W-->>T: 返回结构化结果
T-->>API: 工具输出 + 刷新事件
API-->>UI: 流式文本 + workspace refresh event
UI->>UI: 局部 refetch 并更新选中状态
这张图真正想说明的是:
- AI 不再是旁边悬着的聊天框;
- 它已经进入了工作区运行时;
- 它的写入结果会回到领域层,再驱动 UI 状态同步。
顺带一提,我连聊天本身都做了分叉路径支持。原因也很简单:创作探索本身就是分叉的,AI 对话也不例外。
八、为什么前端和服务层在这个项目里刻意保持得比较薄
Nova Story 的技术栈本身并不复杂:
- 服务端入口用
Bun.serve(); - 通过 RPC 暴露项目、工作区和配置能力;
- 单独提供 chat 接口承接流式 AI 输出;
- 前端用 React 组织项目列表、工作台、工作区和 AI 设置;
- 主编辑区统一用 CodeMirror;
- AI 输入区用 Lexical 支持
@promptmention。
这些技术选型当然重要,但在这个项目里,我刻意不让它们成为架构主角。
因为我越来越明确地觉得:
Nova Story 的决定性复杂度,不在前端框架,也不在 HTTP 层,而在领域模型是否成立。
如果“正文树 + 时间锚点 + 辅助资料 overlay”这套模型是对的,那么:
- UI 只是它的呈现层;
- RPC 只是它的分发层;
- AI 只是它的运行时扩展。
反过来,如果这套模型本身不成立,那么前端再漂亮、AI 再聪明,也只是在一套不稳定的数据基础上叠能力。
九、这套架构目前让我最满意的,和我最克制的地方
回头看现在这版 Nova Story,我最满意的地方其实不是“功能已经很多”,而是这几个关键前提已经被立住了:
- 小说项目不再只是文档集合,而是有世界状态的工作区;
- 时间锚点已经进入数据模型,而不是停留在 UI 概念;
- Git、工作区、语义化 diff 和 AI 已经被放进同一条架构主线上。
同时,我也很清楚这套系统现在仍然有一些刻意保守的地方。
比如:
- 当前历史视图仍然更偏单分支工作流,而不是完整的多泳道 Git 图谱;
- 一些更重的版本控制操作还没有全部开放到 UI;
- 更丰富的协作能力也还在后面。
但我现在反而觉得,这种克制是必要的。
因为在这种系统里,最不应该着急追求的,是“功能面铺满”。最应该先做对的,是数据模型和状态边界。
从工程验证上看,这条路线目前也已经具备了比较稳的基础:
bun test:361 个测试通过;bunx tsc --noEmit:通过;bun run lint:通过;bun run build:通过。
对一个状态层级比较多、涉及版本控制和 AI 写入的本地系统来说,这种可验证性不是锦上添花,而是能不能继续演进的前提。
十、最后总结一下:时间锚点不是附加功能,而是架构的中心
如果只用一句话总结这篇文章,我会这样描述 Nova Story:
它不是把小说写作当成“编辑一堆 Markdown 文件”,而是把它建模成“一个可分支、可提交、可按故事时间切换上下文的本地创作系统”。
在这套系统里,时间锚点承担的不是装饰性的标签功能,而是整个世界状态模型的中心:
- 正文节点通过它获得明确的世界状态坐标;
- 辅助资料通过它变成可按时间切换的参考快照;
- AI 通过它拿到正确的上下文边界;
- diff 和 revert 也因此可以提升到创作语义层。
对我来说,这也是 Nova Story 和“普通写作工具 + 聊天框”最本质的区别。
不是先做一个 AI,再想办法把小说塞进去;
而是先把小说项目建模成一个有世界状态的系统,再让 AI 在这套系统上工作。
如果后面我还要继续扩展它,我也大概率不会先从 UI 特效或者 prompt 技巧开始,而是继续沿着这条主线往下走:
- 更清晰的状态边界;
- 更强的版本控制表达;
- 更可靠的项目感知 AI。
因为对小说创作来说,真正重要的从来不是“我现在打开了哪篇文档”,而是:
我现在所写的这一段,处于哪个故事世界状态之中。