版本KR · 한국어EN · EnglishJA · 日本語ID · Bahasa IndonesiaTH · ไทยZH · 简体中文ZH · 繁體中文
📖 您正在免费阅读全书。如果它对您有帮助,可以通过购买精校出版的纸质版·电子书来支持作者 → BOOKK 纸质版(韩语) ₩61,600  ·  BOOKK PDF(韩语) ₩15,000  ·  uPaper EPUB(韩语) ₩15,000  ·  WikiDocs 免费阅读(韩语)  ·  作者 LinkedIn
GAME DESIGN × AI WORKFLOW 游戏策划实务即用的 AI · Claude Code 活用法 没有一个数字是编造的 六个月的 AI 工作流 —— 提示词·代码·验证 全部公开 304 atoms 48 skills 85 chapters 李旼洙 DESIGN DIRECTOR · 24Y · 2026

游戏策划实务即用的 AI·Claude Code 活用法

没有一个数字是编造的 —— 运行了六个月的 AI 工作流,连同提示词、代码、验证全部公开

24年资历的总监在中规模(10\~50人)团队中实际运营的304条规则·48个工具·自动化系统,原样呈现

著 李旼洙 · 2026

本书正文的全部内容均以2026年上半年为基准。AI 工具的费用、模型与功能变化迅速,具体数值与安装方法请在各官方页面确认最新信息。


本书的发布与版权

本书希望被广泛阅读,因而免费公开。但无论是韩语原版,还是英语、日语译本,本书的原作者是李旼洙(Minsoo Lee)这一事实,在任何地方都必须保持不变。

您可以自由地这样做。 个人学习、非商业性的共享与引用、非商业性的翻译、用于公司内部学习小组 —— 但请同时标注原作者和正本链接,并在修改内容或转译为其他语言时如实说明这一点。二次作品也请以相同条件共享。

请先另行取得许可。 商业性的出版与销售、用作付费课程的教材、纳入公司的商品与服务,以及抹去原作者署名后的再分发。

本书所承载的知识在一册之中即告完结,其本身是免费的。倘若它对您有所帮助、您愿意支持作者,以正式电子书或付费工具包来表达心意,我将不胜感激。


序言 —— 写了三遍的书

这本书写了三遍。

第一遍并不是书,而是一份公司内部手册。在公司运行 AI 工作流的六个月里,为了让团队不必把同一条规则问上两遍,我把决策、工具和流程以文档的形式固定了下来。它不是为了出版而做的,而是为了减少每天的重复而积累起来的运营文档。本书的具体性正来源于此 —— 它的依据不是为写书而编造的案例,而是一份真正在运行的手册。

第二遍是把那份手册改写成书的第一稿。然而它变成了一篇煞有介事地罗列"用 AI 能做这些事"的泛泛之谈。表格很多,展示效果的数字也很多。那些数字大多用小字标注着"加工数值"。重读时,我发现那正是它最大的缺陷。一边谈 AI 的运用,却始终没有展示真实的画面;一边谈效果,举出的却是编造的数字。把手册的具体性搬进书里,反倒把它弄丢了。

于是第三遍,我把全部重新写了一遍。就是您此刻手中的这份正文。我让公司内部手册的具体性重新复活,同时把本书的原则定得简单。

第一,每一章都会把真实的会话从头到尾展示给您。 我敲下的提示词全文、AI 吐出的原始输出、在那份输出中我拒绝了什么、又如何重新指派 —— 这些都收录在内。我不会用"AI 帮你搞定"这样一句话来结束一章。

第二,数字只有三种之一。 任何人都能核对的公开标准(模型 token 单价、无障碍指南)、实际输入在我系统代码中的常量,或是明确标注"这是我的估算"的值。编造的节省金额表一个也没有。我把诚实当作了差异化所在。

第三,实务中运行了六个月的真实系统,原样引用。 304张决策卡(atom)、48个工具(skill)、每次输入时自动调取相关记忆的钩子(hook)、让一个人运营四个人份协作语境的记忆结构 —— 全都在内。不是抽象的"某种工具",而是把文件名、代码和分数原样写了下来。在正文案例中,我只是遮去了公司·项目名和团队成员的名字,工作流的具体性并未抹除。(允许本书出版的公司,我在致谢中以实名写明 —— 因为我已取得对方的谅解。)

我是一名有24年资历的游戏策划。我以单机游戏的 QA·评审入行,此后从在数十个国家上线的 MMORPG 的总监,到200人规模 AAA MMORPG 的早期开发,再到全球手游 MMORPG 的运营(LiveOps)—— 一路做着 RPG 与 MMORPG 及其变种走了过来。在《仙境传说》(Ragnarok Online)、Bless Online、传奇(MIR)系列这样的项目中,我以总监、主策划、系统策划,有时还以 PM 等多种职务参与,也曾创办过一家小小的手游公司。

老实说,我太早就当上了总监。之后,作为系统策划亲手摆弄配置表与战斗数值,作为内容策划一行一行地量产任务与 NPC,策划活动,量产新内容 —— 这样的团队成员时期持续了很久。本书中出现的工作流,有相当一部分正是在那个位置 —— 不是管理别人,而是亲自把手弄脏的位置 —— 怀着"无论如何都想减少这种重复"的心情做出来的。如今我在一线作为一款 MMORPG 的策划总监带领着中规模(10\~50人)团队,但本书的这些工具并非总监的管理工具,而是从实务者的手中诞生的。正因如此,本书的系统不是理论,而是每天都在运转的工作环境。在家里独自做的一款小小的解谜游戏的案例,也以同样的方式处理 —— 那款游戏的 git 提交和真实代码,我原样引用了。

AI 无法取代游戏策划的工作。它只是让你的手从杂活中解放出来。用那双手做什么,依然是人的分内之事。但愿本书能成为那场转变的实务指南。


本书的阅读方法

本书无需从头到尾按顺序读。挑一条适合自己处境的路就好。若您是第一次接触终端·安装,无论如何请先翻开 1.0「开始之前」 —— 这一章会先替您减轻对黑色屏幕的恐惧。

路径 路线 适合的读者
导入之路 1.0(安装)→ 第1部分(导入)→ 第2部分(信息架构)→ 自己领域中的1个 刚开始用 AI 工具的策划
全程之路 第1·2部分 → 分领域(第3\~15部分)→ 流程(第16\~19部分)→ 运营(第20\~24部分) 设计团队级导入的负责人
独立·单人之路 1.0(安装)→ 第1·2部分 → 第23部分(个人游戏开发)→ 各章「单人精简版」 没有团队、独自·业余制作的开发者
通用职务之路 第1·2部分 → 第17部分(会议记录)→ 第16部分(协作)→ 第18部分(决策)→ 第21·22部分(自我改进·治理) 游戏之外的策划·PM·普通上班族
问题解决之路 附录索引 → 反向跳到对应章节 眼下有问题要解决的读者

每章末尾都有「动手试试」。目标不是读完就合上的章节,而是让您今天在自己的环境里至少动手迈出一步。

对在游戏之外工作的读者,再多说一句。本书的工作流有相当一部分 —— 把会议记录变成决策、追踪决策的波及、验证关卡(本书对一道由人或检查器把关验证的环节的称呼,类似质量门禁 quality gate)、成本管理、版权·伦理 —— 与游戏无关也照样运转。您完全可以把"游戏策划"替换成自己的职务来读。 各章的「游戏之外的应用」框就是那座桥;若时间紧张,只跟着90分钟超浓缩课程(17.1 → 16.2 → 22.1 → 21.1)走一遍,也能用手体会到核心骨架。

这里我先做一个区分。本书中的"单人"有两种含义。一种是单人总监 —— 独自扛起好几个人份协作语境的负责人 —— 另一种是独自做游戏的个人·业余开发者。各章末尾的「单人精简版」是为后者准备的,写明了在没有团队、没有公司文件夹的情况下,只把那一章的核心带走的路径。

本书既可作为一册通读,也可分成两支 —— 第1\~15部分"基础·领域"与第16\~24部分"流程·运营" —— 从需要的一边读起。而且本书正文的代码大多无需外部依赖,仅用 Python 标准库即可原样运行。只有关系图之类的少数工具需要标准库之外的包(networkx、PyYAML),在那些地方我把安装的一行(pip install …)一并写在了代码旁边。除这种情况外,无需另行下载,您可以复制代码块直接运行确认。

遇到术语卡住时,别在那里停下,先往后翻。黑色终端之所以陌生,不是工具的缺陷,而是熟悉度的问题,那份距离感会在 1.0 和第1部分中与您一同缩小。


最简单的活用法

最后,我向您透露一个本书最快的活用法。那就是把这本书本身整本喂给像 Claude Code 这样的 AI 工具。

本书并不是只为人阅读而写的。各章的提示词全文·代码·验证流程,都以 AI 能够直接理解并复现的形式写就。所以,您可以在自己的项目文件夹里把本书交给 AI —— 无论是 PDF 还是文本 —— 像"读一读这本书的一致性检查模式,做一个适配我们配置表的检查工具"这样去拜托它。于是 AI 会把对应章节的工作流照着您的环境搭建起来。人一章一章地亲手照做的路,和把整本书交给 AI 一同搭建的路 —— 两条都敞开着。

不过有一点不会改变。采用什么、拒绝什么,那最后的决定 —— 正如本书从头到尾反复申说的那样 —— 依然是您的分内之事。即便让 AI 读了书、装好了系统,审核那套系统吐出的候选项的位置,仍由人来把守。哪怕是最简单的活用法,也是在这一原则之上运转的。


一个约定

本书的任何一张表里,都没有"为说服读者而注水的数字"。我不夸大效果,而是把产生效果的结构展示给您。把同样的结构搬到您自己的项目里,您自己的数字就由您自己去测量。这就是本书所能给予的、最诚实的帮助。


"重现"这个词的约定

本书的正文中,实操记录(worked transcript,完整保留的真实操作过程记录)的输出常常带有"重现(reconstruction)"这一标注 —— 例如「步骤3 —— Claude 的输出(重现)」。这个词意味着保存了什么、又对什么动了手,我只精确地约定一次。因为对一本把诚实写在封面上的书来说,这是最不该含糊的地方。

"重现"并不是编造,而是对真实会话的编辑。 界线如下。

原样保存的部分 编辑过的部分
我敲下的输入提示词全文 —— 复制即可直接使用的形态 公司·项目·NPC·团队成员的专有名 → 书籍用的匿名(IP 保护)
AI 吐出的输出的结构与失败 —— 偏离的候选项、悄悄违反规则的部分、我拒绝并重新指派的往返 长度 —— 进不了正文的枝节缩为「摘录」
代码·常量·验证值 —— 为可通过外部运行复现而原样保留 换行·留白等为版面所做的排版

换句话说,在重现的输出里,我没有添加注水的数字,也没有添加并不存在的成功。 只是做了匿名化、摘录,并为版面稍作整理而已。没有任何地方把失败的输出改写成了成功 —— 恰恰相反,我特意保留了失败。因为那正是展示人会拒绝什么的地方。(与此相对,凡是把代码运行结果或系统日志标注为"实测""原样引用"的地方,都是未经编辑搬过来的。)

译注: 本版的简体中文译文以 AI 机器翻译为初稿、再经人工编校而成。代码块内的提示词与输出也为便于阅读译为了中文;本书承诺"不加修饰"的一手资料——韩文原文——已在韩文版中原样保留。代码的语法、标识符、数值与验证值均未改动。汇率换算为约数,以 2026 年中按约 1 元人民币兑 200 韩元为准。


1.0 开始之前 —— 安装·账号·费用·终端生存包

1.1 是"第一次见面"。是坐在闪烁的光标前、敲点什么试试的环节。但要坐到那个位置上,先得准备好一些东西。工具已经装好,登录已经完成,大致知道费用是怎么产生的,在黑屏前会敲上几个字。本章比 1.1 还要靠前一步。

很多入门书会跳过这一步。只写一行"打开终端"就略过去了。可入门者恰恰就卡在那一行。终端在哪里、要装什么、装的过程中冒出红字该怎么办——在第一行就停下的人,根本到不了 1.1。本章的目标只有一个,就是让你不要卡在第一行。

本章分为五个部分。安装、账号·登录、费用方案的概念、终端生存包,以及"5 分钟首次运行"清单。按顺序跟下来,坐到 1.1 那个位置上的准备就完成了。

flowchart LR A["1.0 准备阶段<br/>(本章)"] --> B["1.1 第一次见面<br/>(坐到光标前)"] A1["① 安装"] --> A2["② 账号·登录"] A2 --> A3["③ 费用方案概念"] A3 --> A4["④ 终端生存包"] A4 --> A5["⑤ 5 分钟首次运行"] A5 --> B classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class A1,A2,A3,A4,A5 human class B pass

1.0.1 安装 —— 各操作系统一行命令

安装的原则是遵循官方指引。工具经常变化,通过非官方渠道下载的安装文件有风险。所以本书不附下载链接,而是教你如何找到官方渠道。在搜索框里输入"Claude Code 官方文档"或"Claude Code install",Anthropic 的官方文档页面会排在最前面。安装命令直接照搬那个页面上的,是最安全的做法。

大方向最好先了解一下。Claude Code(本书统一用英文写法)是一个在终端里运行的工具,通常用一行命令安装。各操作系统的流程略有不同。

操作系统 准备物 安装流程(概念)
Windows PowerShell(系统自带) 把官方文档里的安装命令一行粘贴到 PowerShell
macOS 终端(系统自带) 把官方文档里的安装命令一行粘贴到终端
Linux 终端 把官方文档里的安装命令一行粘贴到终端

三个操作系统的流程都一样。"打开终端 → 粘贴官方文档里的那一行 → 回车"。命令不需要背。从官方文档里复制粘贴才是正道。

安装过程中即便冒出红字(报错)也不必慌张。入门者遇到的安装错误大多是两种之一。要么是权限问题,要么是缺少前置工具(例如 Node.js 这类运行时)。如果出现了红字,把整句话原样复制去搜索或者问 AI,十有八九能解决。报错信息不是敌人,而是线索。

确认是否安装成功的方法:在终端里敲 claude --version 并回车。如果出现一行版本号,就是安装成功了。如果出现"找不到命令"之类的提示,说明要么还没装好,要么需要重新打开终端。把终端彻底关掉再重新打开,然后再确认一次吧。


1.0.2 账号·登录

装完不等于马上就能用。Claude Code 是一个借用 Anthropic 的 AI 模型来工作的工具,所以需要一个确认"是谁在用"的登录环节。

流程很简单。在终端里第一次运行 claude,就会出现登录指引。通常网页浏览器会自动打开,在那里用 Anthropic 账号登录即可(如果没有账号,可以在那个界面新建)。登录完成后,浏览器会显示"现在可以回到终端了"之类的提示,终端那边也会出现完成标志。

入门者在这里经常卡住的地方有两处。

第一,浏览器没有自动打开的情况。这时终端里会显示一行很长的网址(URL)。把那个网址复制到浏览器地址栏里打开就行。这不是卡住了,只是要手动多做一步而已。

第二,弄不清账号类型的情况。在网页聊天(Claude.ai)里用的账号,和 Claude Code 的账号·费用是怎么关联的,这一点的政策可能因时期而异。遵循登录界面的指引和官方文档是最准确的。按照首次运行界面的提示一步步走,大多都能顺利登录。

登录一次之后,在那台 PC 上就会一直保持。不需要每次都重新登录。


1.0.3 费用方案概念 —— 包月订阅 vs API 按量

入门者最不安的部分就是"会花多少钱?"。总有一种模糊的担忧:是不是每敲一个字就会产生费用。先把大方向搞清楚,这种不安就会减轻。费用方式大致分两条路。

方式 计费形态 类比 适合谁
包月订阅 每月固定金额 通信包月套餐 入门者·日常使用
API 按量 用多少算多少(按 token) 电表 大量·自动化·开发集成

包月订阅是按月支付一笔固定金额、在额度内随意使用的方式。和手机包月套餐类似。每月花费相同,容易预测,不需要在意"每敲一行多少钱"。所以入门者通常以包月订阅起步,心里会更踏实(作者推测——具体的方案构成与额度因时期而变,请在官方费用页面确认)。超过额度后,要么等到下个周期,要么升到更高的方案。

API 按量是按实际使用量(token)成比例计费的方式。像电表一样,用多少就计费多少。适合大量处理、自动化流水线,或者与其他程序集成的情况。用得精细就高效,但在入门阶段,在对用量没有感觉之前,成本可能难以预测。

token 是什么、为什么用它来计费,会在 1.2(AI 模型·token·驱动框架)里详细讲。这里只需记住一点。入门者通常以包月订阅起步。因为每月金额固定,可以在没有"用着用着会不会被天价账单砸中"这种担忧的情况下练习。方案名称·价格·额度经常变化,所以本书不附具体数字。本书内容以 2026 年中为基准撰写,费用方案·模型·功能在那之后仍会持续变化。当前的数值,在官方费用页面确认最准确。

一句话总结:对"是不是每用一次就要花钱"的担忧 → 包月订阅就是每月固定。入门以包月起步,心里会踏实。


1.0.4 终端生存包 —— 减少对黑屏的恐惧

现在来到最大的那堵墙,黑屏。1.1 之所以以"在闪烁的光标前迟疑"开场,原因就在这里。对一双用 GUI 工作了 24 年的手来说,终端是陌生的。但要做到不卡在第一行,需要的命令并不多。下面这六个就够了。

命令 读法 作用 类比
pwd pee-double-u-dee 显示我现在在哪个文件夹 "这里是哪儿?"
ls el-es 列出当前文件夹里有什么 打开文件夹窗口看看
cd 文件夹名 cee-dee 进入那个文件夹 双击文件夹
cd .. cee-dee 点点 退回上一级文件夹 后退
Enter 回车 执行敲下的命令 确认按钮
Ctrl + C control-cee 中断正在运行的东西 停止按钮

(Windows PowerShell 里 ls·cd·pwd 也照样能用。macOS·Linux 也一样。所以这六个不挑操作系统。)

用这六个命令做的事,画成图就是这样。在终端里的移动归根结底就是在文件夹内外进进出出,和在 GUI 里双击文件夹或者后退是一样的动作。

flowchart TD Q["pwd<br/>这里是哪儿?"] --> L["ls<br/>这里有什么?"] L --> D{"看得到<br/>要进的文件夹吗?"} D -- "是" --> IN["cd 文件夹名<br/>进入"] D -- "否,往上" --> UP["cd ..<br/>退出"] IN --> L UP --> L RUN["敲完命令后"] --> ENT["Enter<br/>执行"] STUCK["好像卡住了的时候"] --> STOP["Ctrl + C<br/>中断并把光标退回来"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; class Q,L,IN,UP,ENT,STOP code class D human

黑屏可怕的真正原因,是"敲错了好像会弄坏什么"的那种感觉。但上面这六个里没有哪个命令会弄坏东西。pwd·ls·cd 只是查看或移动,不会删除或修改文件。Enter 只是执行,Ctrl + C 只是中断。所以这六个命令随时都可以放心敲。

有时屏幕看起来像是卡住了。敲了命令却半天没反应,或者光标在另一行闪烁、好像在等什么的时候。这种时候按一下 Ctrl + C,大多就会回到原来的光标。光是知道有这个"停止按钮",黑屏就会变得没那么可怕。卡住了就用 Ctrl + C 退出来,重新开始就行。

最后,当敲过的字堆了一大堆、看着乱糟糟的时候,可以把屏幕清空。Windows PowerShell·macOS·Linux 都用 clear 命令清屏。清空了也不会让做过的事消失,只是把看得到的字整理一下而已。


1.0.5 "5 分钟首次运行"清单

走到这里,准备就完成了。如果能在 5 分钟内通过下面这五格,就有资格坐到 1.1 那个位置上了。哪怕只卡在一格,回到对应的那一节(1.0.1\~1.0.4)就行。

flowchart LR C1["① 终端<br/>能打开"] --> C2["② claude --version<br/>出现版本号"] C2 --> C3["③ 运行 claude →<br/>登录完成"] C3 --> C4["④ 用 pwd·ls<br/>能看到我的文件夹"] C4 --> C5["⑤ 能用 Ctrl+C<br/>退出来"] C5 --> OK["✅ 进入 1.1"] classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class C1,C2,C3,C4,C5 human class OK pass

五格都填满了的话,黑屏就不再是一堵未知的墙了。工具装好了,登录完成了,知道了费用方式的大方向,也会在屏幕里移动和停下。1.1 就在这个准备之上开始。坐到闪烁的光标前,第一次敲下"帮我总结一下这个文件夹里有什么",到那个位置上就行了。


1.0.6 Python·pip —— 要运行工具的话(只在需要时)

本书前半部分(第 1·2 部分)只用自然语言提示词就能跟下来。不过第 4 部分之后的部分章节会直接运行小的 Python 脚本(例如 pip install pyyamlpip install pyvis)。即使第一次接触 Python 也没关系。有两条路。

第一,自己安装的路。Python 从 python.org 下载安装(安装界面里一定要勾选"Add to PATH"),在终端里用 python --version 确认。pip 是随 Python 一起装上的包安装工具,像 pip install pyyaml 这样,一行就能装上需要的包。

第二,交给 AI 的路(推荐)。更简单的路是把环境搭建本身交给 AI。在终端里这样请求就行。

确认一下是否装了 Python,如果没有,告诉我适合我操作系统的安装方法。
然后给我一行命令,用来安装本章需要的 pyyaml 包。

AI 会检查你的环境并帮你生成安装命令。卡住了就当场把报错信息原样贴上,问"这个错误怎么解决?"就行。每个要运行工具的章节,有这一个模式就够了。在 Python·pip 让你觉得有负担的环节,那一章的「单人精简版」会给出不写代码、走得更轻松的路。


下一章预告


动手试试

setup 1. 打开你正在用的操作系统的终端(Windows:PowerShell,macOS:终端)。 2. 搜索"Claude Code 官方文档",把官方安装指引页面打开放着。 3. 把计时器设为 5 分钟——目标是通过 1.0.5 清单的五格。

prompt(一行一行,按顺序敲敲看。这是命令,不是自然语言提问)

① claude --version      # 出现版本就是安装成功
② pwd                   # 现在我在哪个文件夹
③ ls                    # 这个文件夹里有什么
④ cd ..                 # 退回上一级(然后再 ls 一次)
⑤ claude                # 运行 Claude Code(出现登录指引就跟着走)

verify - 在 ① 出现一行版本号,就说明安装完成了。如果出现"找不到命令",关掉终端再重新打开,然后再试一次。 - 用 ②·③·④ 查看和移动文件夹的过程中,亲自确认一下什么都不会被弄坏。这三个是只查看·只移动的安全命令。 - ⑤ 运行过程中好像卡住了,就用 Ctrl + C 退出来。能退出来,就说明你已经亲身确认了"有个停止按钮"。

单人精简版

如果你是既没有团队也没有公司文件夹的个人,那就先把安装(①)和用 Ctrl + C 退出来这两件事学会。用 claude --version 确认"工具装好了",用 Ctrl + C 确认"卡住了也能退出来",对黑屏的恐惧有一半,一个人也能在 5 分钟内整理清楚。费用先以包月订阅起步,就能不用担心成本、尽情练习。

1.1 游戏策划与 Claude Code 的第一次相遇

黑色的画面亮了起来。光标在闪烁。一位有着 24 年资历的游戏策划坐在它面前。用 PPT 和 Excel、Wiki 和 Figma 工作了 24 年的手,在键盘上停顿了片刻。那有点像从前还有电脑培训班的年代里的 DOS。在那之后,终端这个东西只在程序员的桌面上才见得到。不知道该敲什么,而且仿佛一敲错就会弄坏什么。这一份犹疑,正是本书的出发点。

大多数人会在这里关掉窗口。然后在会议上一遍遍重复"我们也得做点什么才行"。本章要做的,是不关掉那扇窗口,陪你一起坐在熬过头 30 分钟的那个位置上。目标不是什么宏大的导入战略,而是把"在闪烁的光标面前敲点什么、距离感就会消解"这件事,实打实地放到你手里。


游戏策划第一次坐到 Claude Code 这类 AI 编程工具面前时,新奇与不适会在同一个位置同时涌起。这两种情绪相互冲突这件事本身,就是导入的第一条线索。

新奇的理由很明确。原本要花上半天的数据表一致性检查,几分钟就完成了;拖沓冗长的会议记录被汇总成决策事项的表格;一年前被埋没的策划案,用一句自然语言就能重新调取出来。

不适的理由也同样明确。黑色的画面、闪烁的光标、英文命令,跟日常的工作景象太不一样了。游戏策划的一天是在 GUI 之上流动的,而往黑色终端里敲字这件事,跟职业身份很难贴合。不过这份不适并不是工具的缺陷,而是习惯了 GUI 的人所需付出的适应成本。光是承认这一点,距离感就已经消解了一半。

本书就是一本想要缩短这份距离感的书。1.1 陪你一起坐在第一次相遇的位置上,梳理该看什么、该尝试什么、又有什么可以先放一放。


1.1.1 为什么是现在,游戏策划必须用 AI

游戏策划比其他职能更晚才汇入 AI 的浪潮。处理代码的人先进去,设计师、美术接着进去。策划则常常陷入一遍遍重复"我们也得做点什么才行"、却一拖再拖的模式。

拖延的理由是合理的。策划的产出不像代码那样定型化,而是文本、表格、图示、会议、口头共识混杂在一起。AI 输出的可信度看起来偏低,煞有介事的谎言又很危险,而且 AI 是否真的理解游戏系统也令人怀疑。

然而在 2024\~2026 年之间,有三件事发生了变化。

第一,AI 模型的推理能力越过了临界点。它不再停留在简单的句子生成,而是能处理复杂的系统设计、一致性验证、影响分析。最新的 Claude 系列能辅助游戏策划工作流的相当一部分。不过这并不意味着可以把全部都交给它。验证与责任仍然在人这一边。(辅助的幅度因工作类型和团队成熟度而大相径庭——作者推测,未经验证。)

第二,驱动框架(harness)成熟了。Claude Code 这类工具不是单纯的聊天。它会直接读写文件、执行命令,再把结果作为输入接回来。这跟人工作的方式很像。

第三,记忆、atom、skill 这类运营技法已经落地。AI 不再是用一次就完事,而是有了一套方法论:把团队的知识累积起来,让它随着时间推移越来越聪明。本书后半部分要讲的核心,正是这种累积。

这三件事一旦汇合,对游戏策划来说,导入 AI 也就成了一个合理的时间点。趁还没更晚之前开始,是划算的。


1.1.2 Claude Code 是什么——与其他工具的区别

策划常接触的 AI 工具有两类。一类是往聊天框里抛问题的聊天机器人型(ChatGPT、Claude 网页应用),另一类是在代码编辑器里做自动补全的编辑器结合型(Cursor、Copilot)。

Claude Code 属于第三类。它在 CLI(终端)里运行,能访问人的整个工作环境。三类工具在哪里分道扬镳,用一张图来看是这样的。

聊天机器人型 编辑器结合型 Claude Code

输入位置 网页聊天框 代码编辑器 终端 文件访问 需要上传 打开的文件 整个项目 命令执行 不可 部分可行 自由(权限内) 输出形态 文本 代码建议 文件变更·执行结果 策划适配度 比喻 前台咨询台 自动补全的笔 邻座的同事

游戏策划的工作不是代码,而是文档、表格、关系。Claude Code 的强项在于:它能看到、理解并操作人工作的整个文件夹。不必像聊天机器人那样每次都把资料复制粘贴进去。

用办公室来打比方,聊天机器人型是前台咨询台。问一次答一次,资料每次都得重新拿出来。Claude Code 则更接近邻座的同事。它知道资料在哪里,会用自己的手打开文件,把结果整理好再放回桌上。同样是 Claude,让它坐到哪张桌子上,擅长的事情就会随之不同。


1.1.3 头 30 分钟——该看什么、该尝试什么

安装和配置在 1.0 里讲。1.1 专注于在头 30 分钟里体验什么能减少距离感。头 30 分钟分为四个区段,每个区段都会让一部分距离感分阶段地消失。

flowchart LR A["查看(5 分钟)<br/>帮我概括一下这个文件夹<br/>→ '它听得懂自然语言'"] B["搜索(5 分钟)<br/>查找关键词·最近文件<br/>→ '它替我干掉每天的杂活'"] C["转换(10 分钟)<br/>会议记录→表格,Excel→Markdown<br/>→ '它帮我腾出时间'"] D["验证(10 分钟)<br/>质疑刚刚的输出<br/>→ '这个工具就该被质疑'"] A --> B --> C --> D D --> E["距离感消解 80%"] classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class A,B,C ai class D human class E pass

头 5 分钟——查看

打开终端、运行 Claude Code,黑色画面亮起,光标闪烁。很多人会停在这个位置,但一停下,就错过了消解距离感的第一个机会。输入一句自然语言。

帮我概括一下这个文件夹里有什么

既不是代码也不是命令,就是一句话。Claude Code 会扫描文件夹结构,整理出有哪些文件、看起来是处理什么的项目,然后作答。在装着策划文档的文件夹里敲下这一句,大致会得到这样的回应。



帮我概括一下这个文件夹里有什么

我扫描了这个文件夹。看起来是游戏数据·策划文档的工作文件夹。 文件类型 数量 内容推测 ───────────── ──── ─────────────────────────── .xlsm 12 数据表(道具·任务·NPC 等) .md 34 策划案·会议记录草稿 .png 88 UI 原型·参考图 子文件夹 6 /data /docs /ref /meeting /export /tmp 观察: - /meeting 文件夹里堆积了最近一周的 5 份会议记录。 - item_table.xlsm 与 quest_table.xlsm 似乎共享同一个 ID 列 (FK 候选)。 - /tmp 文件夹里有多个临时 export 文件——可能是待清理对象。 要先看哪一个?

从这一次回应里,"这个工具听得懂的不是命令,而是自然语言"这一事实会切身地进入你的身体。不用亲自打开文件夹,就能拿到一张梳理好的图——什么在哪里——这种体验,会当场削掉一半的距离感。

接下来 5 分钟——搜索

让它做一点小搜索。比如"告诉我最近一周内修改过的 5 个 Markdown 文件",或者"含有'奖励'这个词的文件在哪里?"这类请求。策划每天都要打开"在文件中查找"、敲入关键词、滚动结果的活儿,Claude Code 用自然语言接过来,整理好再还给你。这个工具替你干掉每天的杂活——这种感觉会在第二个 5 分钟里站稳脚跟。

接下来 10 分钟——转换

让它做一点小转换。比如"从这份会议记录里只提取决策事项,做成表格",或者"把这张 Excel 表的第 1\~10 行转成 Markdown 表格"这类请求。原本用手要花 5\~10 分钟的转换,在很短的时间里就被处理掉——这种体验会带来一种感觉:工具不止于替你搜索,更是在帮你腾出时间。

最后 10 分钟——验证

止步于此之所以危险,是因为 AI 会说煞有介事的谎话。让它自己去质疑刚刚做出来的输出。

帮我再确认一下刚才做的表格和原文是否一致。有没有漏掉的条目?

于是 Claude Code 会重新读一遍原文,把它跟自己的输出逐行对照,报告出不一致之处。实际上在这个区段里,经常会出现这样的画面。



帮我再确认一下刚才做的表格和原文是否一致。有没有漏掉的条目?

我把原文(meeting_0603.md)和刚才做的表格重新对照了一遍。 找到了 3 处不一致。 ⚠ 遗漏 "教程奖励下调 50%" —— 在原文第 12 行,但表格里漏掉了 ⚠ 笔误 负责人 "金某某" → 表格里误写成了 "李某某" ⚠ 模糊 "奖励再议" —— 是决策还是搁置,原文表述不清。 我擅自把它归入了'决策',但需要确认。 要我重新做一张修正后的表格吗?模糊的那条要怎么处理, 告诉我后我会照着改。

工具懂得质疑自己的输出,并且这份质疑是和人一起来做的——这是最后 10 分钟的核心。像第三条那样,反过来问"我是擅自判断的,请你确认一下"的这种态度,正是把验证留在人手里的安全阀。

30 分钟过去,距离感的 80% 已经消失了。剩下的 20% 会在接下来的各章里慢慢减少。


1.1.4 在公司里看到的景象——中等规模团队的 6 个月

作者作为设计总监运营的某个 MMORPG 项目(以下称"项目A"),已与策划团队(4\~5 人)用以 Claude Code 为中心的工作流运营了约 6 个月(项目A 的整个开发团队为中等规模,10\~50 人)。这里转述几个景象。

只把最抓得住手的一件拿出来,按实测来算。这是横跨 30 多张数据表的 FK(外键)一致性检查。它是用人眼去追踪"一张表的 ID 在另一张表里是否被正确引用"的工作,表越多,组合就以几何级数膨胀。

从半天到 5 分钟。这一条我不会去做一般化。其他工作的节省幅度更小,或者会新添出审阅时间。同样这 6 个月里看到的其他景象,只用方向和比例来记。

每个工具一旦做成,6 个月里累积下来节省的时间,按作者的体感不是以人-周(person-week)计,而是以人-月(person-month)计(精确的合计未测量,为估算值)。靠那些时间,得以专注于更深的策划。

这类工具一旦做成,就能长久地干活。不过'长久'并不等于'无人'。要运营它的人和验证结构一起配齐,才能长久;只留下工具而人走了,两个季度内就会腐坏。本书后面的各部分,会讲上述每个工具是如何被做出来、又如何运营的。


1.1.5 恐惧与期待——坦诚地面对

来坦诚地点一点游戏策划在 AI 工具面前常有的恐惧。不回避而是去面对,是导入的第一步。

"AI 会取代我的工作"这份恐惧,对了一半,也错了一半。单纯的杂活(一致性检查、文档转换、搜索)会被 AI 取代,但决策、优先级、玩家情绪的设计,它取代不了。反倒是善用 AI 的策划,从杂活中解放出来,专注于本质。不妨自问:"我的工作里,杂活和本质的比例是多少?"如果杂活占 70%,那本质的 30% 原封不动还是你自己的,而那 30% 变得更重要,才是关键。

"AI 错了,责任谁来担"这个问题也经常出现。对策划的决策所负的责任,永远是策划自己的。不经验证就照搬 AI 的输出,那是策划的失误,而不是 AI 的失误。把验证流程一并设计进去,是导入的一部分。1.1.3 最后 10 分钟里看到的'让它质疑自己的输出',就是这套流程里最小的那粒种子。

"不太懂代码所以用不了"这份恐惧很快就会解开。Claude Code 用自然语言运行,不懂代码就以不懂的样子起步即可。因为会和 AI 一起读它写出来的脚本,几个月下来,简单的脚本就能读懂并修改了。学习会自动地跟上来。

"工具变得太快"也是常见的担忧。要把模型、功能、趋势全都追上,人会累垮。只把对自己工作流有帮助的 1\~2 个功能深入学透,其余的等需要时再看。

先打个预防针。即便 1.1.3 的回应画面看起来很光滑,实际的头 30 分钟里,会混着答偏的回答、张冠李戴的文件概括、卡顿的输出。那是正常的。本书讲的不是光滑的成功故事,而是更多地讲:当输出答偏了,要怎么重新请求把它纠正过来。


1.1.6 本书的使用方法

本书由 24 个部分构成。不必从头到尾按顺序读。从下面三种模式里挑一个适合你处境的就行。

模式 路径 耗时
导入模式 Part 1(导入) → Part 2(信息架构) → 你自己的领域 1 个 1\~2 个月
全量模式 Part 1\~2 → 各领域(3\~15) → 流程(16\~19) → 运营(20\~24) 6 个月\~1 年,适合以团队为单位
问题解决模式 附录索引 → 反向回到对应章 约 1 周,手头有问题时

如果拿不准选哪条路,按你更接近哪一边来分就行。如果你是游戏之外的策划、PM 或普通职场人,可以不选上面三种模式,而走「通用职能之路」(第 1·2 部 → 第 17 部会议记录 → 第 16 部协作 → 第 18 部决策 → 第 21·22 部自我改进·治理)——即便跳过游戏领域的章节,核心骨架照样立得起来,而每一章的「游戏之外的应用」方框,就是把内容搬到你自己职能上去读的那座桥(索引见附录 F.5)。如果没时间,只跟着 17.1 → 16.2 → 22.1 → 21.1 这四章走也可以。如果你是刚开始接触 AI 工具的非专业读者,就用'导入模式',只抓住你自己的领域(或最接近的领域)一个、一路读到底,而深度领域的部(4·8·11 等)只取导入部分'给非专业者的一行话',需要时再下沉到正文。

本书的所有章节都不走到学术深度,而是停在可运营的层面。把中等规模团队里实际跑了 6 个月的技法原样搬过来,陪你一起走那条从小处起步、再做大的路,就是目标。


1.1.7 与下一章的衔接

1.1 是一章缩短距离感的章节。1.2 会往里再走一步,用游戏策划友好的语言解释这个工具的基本机制。让你不再害怕模型、token、上下文、驱动框架这些词,就是 1.2 的目标。正式的配置(记忆·权限·settings.json)在 1.3 里讲。


本章要点

下一章预告


动手试试

setup 1. 打开终端(Windows 用 PowerShell,macOS 用终端)。 2. 移动到汇集了策划文档的文件夹,然后运行 Claude Code(安装见 1.0)。 3. 把计时器设成 30 分钟——5 分钟(查看)·5 分钟(搜索)·10 分钟(转换)·10 分钟(验证)。

prompt(每个区段敲一行,按顺序敲下去)

① 帮我概括一下这个文件夹里有什么
② 含有'奖励'这个词的文件在哪里?
③ 从这份会议记录里只挑出决策事项做成表格
④ 帮我再确认一下刚才做的表格和原文是否一致。有没有漏掉的条目?

verify - 在 ① 中,用眼睛对照一下文件夹结构的概括是否和实际文件夹相符。 - ④ 的不一致报告只要出来哪怕一条就算成功。这意味着你亲眼看到了 AI 质疑自己输出的那一幕。 - 就算出来答偏的回答也不算失败。像"刚才那个答错了,只重看这个文件"这样重新请求,直到这一步,才是头 30 分钟的练习。

单人精简版

如果既没有团队也没有公司文件夹,作为个人,就在你自己 PC 上随便一个工作文件夹(例如下载文件夹、笔记文件夹)里,只敲上面的 prompt ① 和 ④。用 ① 确认"它听得懂自然语言",用 ④ 确认"可以质疑它的输出",那么本章的两个核心,一个人在 5 分钟内也能切身体会到。

1.2 模型·token·驱动框架 —— 一次任务中 token 流动的路径

这是某次做完一件任务、查看用量时的事。这周的五份会议记录堆在文件夹里,我得在周一上午站会之前把"已确定的内容"整理成一页。我在 Claude Code 窗口里敲下一行字:"从这个文件夹的会议记录里,只把决策事项挑出来做成表格。"按下回车后约 0.4 秒,画面下方闪出一行小小的灰色字。

Reading meeting-2026-05-25.md ... (1,840 tokens)
Reading meeting-2026-05-27.md ... (2,310 tokens)

这行灰色字就是本章的主题。你抛出一句中文,工具就把它切成 token,把文件读成 token 喂给模型,再接收模型的回答写进文件。这一来一回每转一圈,就计一次费,资料也在模型的"视野"里不断累积。本章用游戏策划的语言,拆解这行灰色字背后发生的事。模型·token·上下文·驱动框架,这四个词就够了。

术语备注 - 模型(model):生成答案的大脑。有 Opus·Sonnet·Haiku 这样大小和性格各异的种类。 - token:把文字切碎后的片段。计费·速度·视野都以这个单位来数。 - 上下文窗口(context window):模型一次能装进脑中的 token 最大量。 - 驱动框架(harness):让模型干活的车体。Claude Code 就是一例。


1.2.1 驱动框架循环 —— 灰色字的真面目

上面那行灰色字不是随机日志,而是一个固定循环中的一格。驱动框架(harness)所做的事归根结底就是快速地转同一个圈:把文件读出来喂给模型,模型说"执行这条命令"就去执行,再把结果重新喂给模型。这个圈一直转到任务结束为止。

flowchart TD Start([人:一行指令]) --> Read[驱动框架:读取文件·资料<br/>转换为 token] Read --> Inject[驱动框架:注入上下文<br/>+ 累计 token 求和] Inject --> Model{模型:决定下一步动作} Model -->|需要执行命令| Exec[驱动框架:执行 shell 命令·脚本] Exec --> Result[把执行结果<br/>重新输入为 token] Result --> Inject Model -->|答案已就绪| Write[驱动框架:写入文件·输出] Write --> Verify{验证通过?} Verify -->|失败| Inject Verify -->|通过| Done([保存结果]) classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class Start,Verify human class Read,Inject,Exec,Write code class Model ai class Result data class Done pass

在这张图里,人动手的格子只有最上面(指令)和最下面(确认验证结果)两处,中间的圈由驱动框架自主运转。读五份会议记录时灰色字闪了五次,就是把 Read → Inject 这格转了五圈。换成网页聊天,你得亲自打开五个文件复制·粘贴。驱动框架替你干掉这份劳动——这正是把聊天机器人和 CLI 型驱动框架分成两种不同工具的决定性差别。

循环每转一圈,就在 Inject 格里把累计 token 求和。所以不先理解 token,这个循环的成本和上限都看不见。先从 token 看起。


1.2.2 token —— 一次任务花掉的真实货币

token 不是字,而是模型把文字切开后的片段。按经验法则,英文大约 4 个字符算 1 个 token,中文(及韩文)大约 2 个字符接近 1 个 token(这不是官方换算,而是运营用的粗估——实际值由模型的分词器决定,且因句而异)。含空格 20 个字符,大致就是 10 个 token 上下。

把那次会议记录任务用 token 跟一遍(下面的数字是同一任务的单次测量。会随会议记录篇幅·摘要长度变化,所以请把它当作量级和比例来读,而非绝对值)。

步骤 做什么 token(输入) token(输出)
指令 "只挑决策事项做成表格"一行 \~25
读会议记录 ×5 5 个 md 文件正文 \~10,400
注入分类规则 会议类别 atom 1 条(JIT) \~480
模型推理·制表 把 12 条决策做成表 \~1,600
验证再输入 对 Linter 揪出的 1 处遗漏再追问 \~320 \~210
累计 \~11,225 \~1,810

有两点扎眼。第一,我敲的指令是 25 个 token,而整个任务光输入就超过 1 万 1 千 token。成本几乎全部不来自我的句子,而来自工具读进来的资料。第二,输出(1,810)大约是输入(11,225)的六分之一。大多数策划自动化都是这样读得多、写得少。所以要降本,与其打磨输出,不如治理输入资料的量,后者效果大得多。

任务结束后敲 /context,就能看到这个会话占了多少上下文。不留意 token 时,它就像打印纸一样无意识地流走;一旦可视化,姿态就变了。这份可视化正是节约的起点。

治理 token 的工具不是抽象的节约精神,而是把输入资料细致处理的具体技法。

  1. JIT 注入 —— 不预先把全部资料都装上,只在需要时用关键词匹配拉取。上表的"注入分类规则 480 token"就是一例。进来的不是会议分类规则的整篇文档(数千 token),而仅是匹配到的一条 atom。
  2. 摘要缓存 —— 长文档在供人看的原件之外,另备一份供 AI 看的摘要本。AI 读摘要本。
  3. atom 拆分 —— 一个文件只装一条决策(2.2 详述),就能精确拉取所需片段,从而节约 token。
  4. 整理上下文 —— 会话变长就压缩。Claude Code 支持自动压缩。
  5. 模型选择 —— 简单转换若用大模型,同样的 token 成本也会更贵。这是下一节的主题。

其中第 1 项 JIT 注入,是本书工作环境里实际运转的装置。一旦输入一行字,inject_memory.py 钩子就按分数高低匹配内存 atom,只挑出排名前几条来注入,即便失败也不阻断工作流(实现细节在 1.3 详述)。"只取需要的资料、只取前几条、即便失败也安静"这条 token 节约原则,原样写进了一个代码文件里。


1.2.3 模型 —— 同一车体,不同引擎

模型就像汽车引擎,可以在 Claude Code 这同一车体上换装 Opus·Sonnet·Haiku 这些不同的引擎。换了引擎,任务的性格就变了。

模型匹配 —— 深度 vs 速度·成本

→ 速度·低成本 推理深度 ↑

Opus 大型引擎 设计评审·GDD 合成

Sonnet 中型引擎 会议记录·日常 80%

Haiku 紧凑型 配置表简单转换

对照策划工作来看,大致这样划分。像系统设计评审、整合多份资料合成 GDD(Game Design Document,游戏设计文档,即详细规格书)初稿这类需要深度推理和一致性的活,交给 Opus;像会议记录决策抽取、每日摘要这类大多数日常工作,交给 Sonnet;像数据表的简单格式转换这类几乎不需要判断的活,交给 Haiku。前一节的会议记录任务跑在 Sonnet 上,也是按这个标准——挑出决策搬进表格,比起深度推理,更讲究均衡与速度。

引入初期人人都会掉进一个陷阱:想把所有任务都跑在最好的引擎,也就是 Opus 上的冲动。顺着这股冲动走,成本·速度的负担很快会转成运营负担,而"让模型去匹配任务"的手感也立不起来。运营真正的本事不是每次都在脑子里挑模型,而是在模式成型后用自动化把它固化下来。

这类固化,就把模型明确写进 settings.json 或斜杠命令里(1.3 详述)。一旦固化,每次挑选的工夫就没了。

模型大约每半年出一个新版本,即便名字相同,4.5 和 4.6 也不一样。新版本出来时,只拿工作流中最核心的五项任务、用同样的输入做对比。要全部测试会累垮。光看这五项的结果差异,就足以判断要不要切换。


1.2.4 上下文窗口 —— 循环逐渐填满的上限

前面说过,循环每转一圈,就在 Inject 格里累积 token。那份累积撞上的天花板就是上下文窗口,也就是模型一次能处理的 token 最大量。拿人来比,就是工作记忆(working memory)。

前面那个会议记录任务,累计输入是 1 万 1 千 token 级,相对 200K 天花板只占 6% 出头,很宽裕。但若不切换任务、在同一窗口里把会话拖得很长,就会逼近天花板。一旦装满,旧内容就被截掉,模型开始丢失前半部分的"记忆",自动压缩随之触发,先前的对话被替换成摘要本。

治理这个天花板的习惯有四条。

模式 何时
会话分离 转向别的主题时,新开一个会话
显式压缩 一件任务结束后,只留核心再压缩
内存外置 常用资料拆成 atom,用 JIT 随时注入
上下文可视化 /context 用眼睛确认当前用量

游戏策划常碰到的吃重场景,是会议资料·策划案·数据表同时需要的任务,这时 1M 选项就有用。不过 1M 伴随成本·速度负担,所以平时 200K 就够,只有资料包真的很大时才动用。


1.2.5 驱动框架真正过滤虚假之处 —— 验证格

回到循环图最下面的 验证通过? 格。没有这一格,模型那套煞有介事的谎话就会原样存进文件。模型有时会自信满满地把错误答案当成正确答案抛出来(幻觉,hallucination),这种频率随代际上升而下降,但不会归零。所以把验证设为常设的一格。

策划中危险的幻觉很具体:引用一个根本不存在的数据表列,用错误的公式算数值,或者把会议上没敲定的事当成已确定的来摘要。会议记录任务里最可怕的是第三种——"只讨论、暂时搁置的事项"悄悄爬上了决策表。

验证有五种模式。

  1. 原文对照 —— 把 AI 输出与原始资料再比一遍("帮我确认这个是不是真在那份资料里")。
  2. 双向转换 —— A→B 转换后,再把 B→A 反向转换,看是否一致。
  3. 抽样评审 —— 从输出里随机挑 3\~5 条由人亲自确认。
  4. Linter 自动化 —— 自动检查输出是否违反既定的格式·范围·规则。
  5. 两模型交叉验证 —— 让 Opus 评审 Sonnet 的输出。

不必每次五个全做,而是按任务的风险度挑 1\~3 个。会议记录任务里,把第 4 项 Linter("决策是否齐全包含主体·内容·期限")和第 3 项抽样评审做一次捆在一起。前面 token 表最后一行"验证再输入 320 token",正是 Linter 揪出遗漏、向模型回问的那一来一回,换成循环图就是 验证失败 → Inject 又多转了一圈。

每次都靠人全检,引入效果就减半,所以验证本身也是自动化对象。会议记录决策抽取由 Linter 检查格式遗漏,数据表转换检查行数·合计·外键一致性,GDD 自动生成检查核心章节遗漏。通过的人就不用看,只看没通过的。这就像在塞满柜子的文件里,只把贴了红标的文件夹拿到手中的画面。让人的视线只落在危险的地方——这正是验证自动化的目的。


1.2.6 四个词被串进一次任务的所在

现在把那次会议记录任务从头到尾捋一遍,看四个词如何被串进一行。

干什么 对应哪个概念
1 在会议记录文件夹里运行 Claude Code 驱动框架
2 任务是分析会议记录,故选 Sonnet 模型
3 合计约 11K token,在 200K 窗口内 —— OK token·上下文
4 把会议分类规则 atom 用 JIT 自动注入 token(节约)
5 模型把 12 条决策输出为表 模型·驱动框架循环
6 Linter 查出 1 处格式遗漏 → 回问补全 验证(循环多 1 次)
7 weekly-decisions-2026-W21.md 保存·提交 驱动框架

如果用手做,打开五份会议记录读一遍、只挑出决策搬抄、再对齐格式,要花 30 分钟。一旦自动化就缩到 5 分钟,而这 5 分钟里人手只做扫一遍验证抽样这一件事。人的时间只落在真正需要的地方(看搁置事项有没有被错当成决策爬上去)。关键不是省下的 25 分钟,而是那道视线落点变了。


1.2.7 常见误解

"Opus 总是更好"最常见。若无视成本·速度确实如此,但对简单任务而言 Opus 是浪费。按任务匹配才是答案。

"1M 上下文总是必需的"也常出现。大多数 200K 就够,1M 伴随负担,所以只用在资料包真的很大时。

"验证是人来做的"只对一半。可自动验证的部分占多数,人则专注剩下的。

"token 不用操心"在个人工作里某种程度上行得通,但多人一起用时,累计成本会迅速变大。从一开始就把可视化·节约模式固定下来更稳妥。

"驱动框架没有差别"也意外地多。即便是同一个模型,是聊天机器人还是 CLI,也会分成不同的工具。前面看到的有没有复制·粘贴劳动,就是那个差别。


1.2.8 动手试试

用一件小任务,亲手把本章的四个词跑一遍。

setup

prompt

从这个文件夹的笔记里,只挑出"已确定的内容",
做成主体·内容·期限 3 列的表格。
搁置·讨论中的事项剔除,并为表中每一行
标上它来自哪个文件的文件名。

verify

单人精简版

如果你刚开始用工具,上面只需抓住两件事。第一,把笔记整个文件夹交出去,别亲自复制·粘贴(交给驱动框架循环)。第二,输出表一律连同文件名一起拿到,只对可疑的行打开原文看。模型选择或 token 可视化,等熟练了再加也不迟。不用手搬资料、把输出与原文对照,光这两个习惯,引入就立稳了一半。


本章要点

下一章预告

1.3 记忆·权限·配置基础设施

我打开一个新会话,输入"来看一下技能冷却时间的数值平衡吧"。在按下回车之前,屏幕下方有一行灰色小字一闪而过:[memory injected: 2 atoms, 1,842 chars]。我并没有打开任何文件,这意味着上周固化下来的冷却时间规则文档,已经被附加到了模型输入的前面。这就是搭好基础设施的工作环境发出的第一个信号。打开工具的那一刻,工具已经记得我。

要让这个画面成立,需要三样东西提前各就各位。AI 记住什么(记忆)、AI 在没有人工批准的情况下能做什么(权限),以及开关这两者的中央开关(settings.json)。第一次安装最多花一个小时,而这一个小时会化作此后 6 个月里每天省下的时间回到你身边。这是一笔几乎能全额收回的投资。

本章是一段实地走查(walkthrough):依次展开作者在个人 PC 上实际运行的那一行 settings.json、它所调用的 inject_memory.py,以及那个文件读取的 _jit_manifest.json,一步步跟着走。读到最后,你就能亲手指出"记忆被自动注入"这句话,到底发生在哪个文件的哪一行。


1.3.1 settings.json —— 一切的起点,只有一行

先从结论看起。在作者的个人 PC 上,开启记忆自动注入的,就是 settings.json 里仅有的一个代码块。

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python ~/.claude/hooks/inject_memory.py"
          }
        ]
      }
    ]
  }
}

把这个代码块说的话翻译成大白话就是:"每当用户提交提示词的事件(UserPromptSubmit)发生时,就执行一次名为 inject_memory.py 的 Python 脚本。"就这么简单。不是 AI 聪明到会自动记住,而是每当有输入进来时,人事先登记好的脚本就插进来执行一次的结构。

settings.json 是控制 Claude Code 所有行为的中央文件,分为两层。

两者会合并后生效。所以作者把团队需要共享的 hook、权限放在 settings.json,把仅在这台家用 PC 上使用的绝对路径或个人工具路径分开放在 settings.local.json。这样的分离既能在协作时避免 git 冲突,也能防止个人配置泄漏到团队仓库里。

除了 hook 之外,还有几个经常会用到的条目。

这里有一个最重要的运营习惯。settings.json 哪怕只有一个小小的笔误,都会让工具本身起不来。JSON 里少一个逗号,解析就会崩。所以修改前备份是必须的。作者的 PC 上实际就留有这样的备份文件。

settings.json.bak_2026-05
settings.local.json.bak_2026-05

用日期后缀备一份,回滚只要 1 秒。用 git 管理就更好了。就像抽屉里的一把旧钥匙,平时用不上,但在锁着的门前,总会有非用一次不可的那一刻。


1.3.2 inject_memory.py —— hook 内部实际发生的事

现在进入 settings.json 调用的脚本内部。这是走查的脊柱。代码只有一百来行,但核心是五个动作。

flowchart TD A["会话:用户提交提示词\n例:'来看技能冷却时间的数值平衡'"] --> B["UserPromptSubmit hook 触发\nsettings.json 执行 inject_memory.py"] B --> C["加载 _jit_manifest.json\natom 17 个的元数据"] C --> D["按 score 降序排列\n→ 对每个 atom 尝试 regex 匹配"] D --> E{"是否有\n匹配的 atom?"} E -->|没有| Z["什么都不注入\nexit 0"] E -->|有| F["只取前 max_matches(3) 个"] F --> G["合计超过 6,000 字时\ntruncate"] G --> H["在用户输入前\n附加 atom 正文"] H --> I["模型同时接收 atom + 提示词\n→ 作出响应"] D -.->|任何异常| Z2["无条件吞掉异常\nexit 0(不阻断流程)"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; class A human class B,C,D,E,F,G,H code class I ai

把这五个动作展开来说就是这样。

1)读取 manifest。 脚本首先打开 ~/.claude/projects/C--Users-user/memory/_jit_manifest.json。这个文件里整理着 atom 的元数据(名称·路径·匹配 regex·分数)。作者的个人 PC 上目前登记了 17 个 atom。

2)按 score 降序排列。 每个 atom 都有一个 score 值。分数越高的 atom,越先被尝试匹配。当同一个关键词命中多个 atom 时,这个分数决定谁拥有优先权。

3)用 regex 匹配。 把用户输入的字符串与每个 atom 的 regex 模式逐一比对。输入里只要有"冷却时间",带有 쿨다운|cooldown|GCD 模式的 atom 就会被命中。比对时不区分大小写。

4)最多只截取 3 个。 无论匹配出多少,只要超过 max_matches(作者环境是 3)就只保留前 3 个。如果被选中的 atom 正文合计长度超过 6,000 字,就 truncate。用双重上限来防止输入膨胀,是一道安全装置。

5)无论出什么异常,都以 exit 0 结束。 这是设计的核心。无论 manifest 损坏、文件消失,还是 regex 写错,脚本都会悄悄吞掉异常,以退出码 0 结束。因为 hook 一旦以非 0 的码退出,用户的提示词本身可能就被阻断。"哪怕记忆注入失败,也绝不阻断用户的工作流程"这条原则,被记录在代码最外层的 try/except 里。

重心落在第 4 条和第 5 条上。第 4 条(上限)防止记忆把 token 撑爆,第 5 条(吞掉异常)防止基础设施妨碍工作。两者都是同一套哲学的两副面孔——"自动化不让人觉得碍手碍脚"。


1.3.3 _jit_manifest.json —— 唤醒 atom 的关键词字典

1.2 里承诺过"只取需要的资料、只取前几个、失败也悄悄略过"这条节省 token 的原则,并把实现细节留到了本章。那个细节就住在 inject_memory.py 读取的 manifest 里。它是 JIT(Just-In-Time,仅在需要时才载入资料的方式)的心脏,一个 atom 的条目长这个样子。

{
  "atoms": [
    {
      "name": "combat_cooldown_rule_v2",
      "path": "atoms/combat/combat_cooldown_rule_v2.md",
      "regex": "쿨다운|cooldown|GCD",
      "score": 80
    },
    {
      "name": "user_health",
      "path": "memory/user_health.md",
      "regex": "건강|복약|컨디션|약물",
      "score": 95
    }
  ],
  "config": {
    "max_matches": 3,
    "case_insensitive": true
  }
}

四个字段定义一个 atom。

config 块里的 max_matches: 3,就是 1.3.2 中看到的"最多 3 个"上限的出处。用手改 manifest,行为立刻就变。

这里点一下规模感。作者的个人 PC 用 17 个 atom、一份 manifest 就轻量地跑起来了。相比之下,公司实务环境(项目A)截至 2026 年 5 月的备份里,登记着团队 atom 304 个、skill 48 个。有一个 hot atom 的 score 高达 356.53(属于处理文件名规则的 view_html_filename_convention 系列),它并非一开始就高,而是在反复调用·验证中累积下来的痕迹。

个人 PC 的 17 个与公司的 304 个之间的差距说明:哪怕是同一套 JIT 机制,资料堆积的速度和规模也与项目密度成正比。没必要一开始就造出 304 个。从 5 个核心 atom 起步,每周固化一两个,不知不觉 manifest 就厚起来了。

作者推测(未经验证):关于 score 随匹配·验证次数累积的说法,是基于运营模式的一种解读。分数的计算公式本身会随各环境的 manifest 设计而不同,因此上面 356.53 这样的绝对值只是作者环境的实测快照,并非通用标准。

记忆分两层来放的原则,这里再点一遍。

区分 位置 何时加载 用途
全局 ~/.claude/memory/ 所有会话 本人身份·协作规则·语言设置
项目 ~/.claude/projects/<项目>/memory/ 对应项目的会话 各项目的 atom·规则·资料

全局保持轻量更安全。一旦全局变重,那份重量就会以 token 成本累积到每一个会话上。拿办公室打比方,全局就是桌上的名片夹(越轻每天越好用),项目记忆则是旁边柜子里的文件夹(按项目单位变厚也不会增加平时的负担)。所以自动加载的全局里只放核心,丰富的资料堆到项目记忆里,再用 JIT 在需要时唤醒。


1.3.4 权限 —— 沉淀工作痕迹的白名单

接下来是基础设施的第三根支柱,权限。Claude Code 可以删文件、执行命令、调用外部 API。强大与危险结伴而来。权限系统管理着这份危险。

权限分两类。无需人工批准就自动执行的,和每次都要拿到批准的。哪一类放什么,由 settings.jsonpermissions 块定义。

{
  "permissions": {
    "allow": [
      "Bash(ls:*)",
      "Bash(git status:*)",
      "Bash(git diff:*)",
      "Read(*)",
      "Grep(*)"
    ],
    "deny": [
      "Bash(rm -rf:*)",
      "Bash(git push --force:*)"
    ]
  }
}

这里需要一次视角的转换。这份 allow 列表不是单纯的配置值,而是工作痕迹的沉淀。一开始只把读取·检索这类放进自动允许,几乎是空的。可是同样的工作反复做上一两个月,就会冒出"这个命令每次都要按批准好烦"这样的模式,于是把它们一个个挪进 allow。变长的列表,正是"我用这个工具反复做了什么"的指纹。

作者的公司环境(项目A)有大约 80 个自动允许的模式。从 20 个起步,历经 6 个月又加上了 60 个,把那 60 个倒着读,过去半年反复做了哪些工作便一目了然。数据表提取、关系图生成、模式(schema)文档化——常用的工具,正是常常被允许的权限。

权限运营中会沉淀下四种模式。

每次都弹出批准弹窗,人会累。也有减轻疲劳的装置。可以用 fewer-permission-prompts 之类的斜杠命令把频繁出现的模式批量登记,或在一个会话内给出临时允许,或仅在个人工作中使用全权自动允许模式。不过最后这个选项,在团队环境里不推荐。

疲劳感与安全之间的平衡,由本人来调。太严格工作转不动,太宽松会出事故。哪怕宽松地起步,只要备好季度清理的周期,平衡自然会找到位置。


1.3.5 会话开始时 —— 记忆与权限一同加载的全景

到目前为止看到的三根支柱(settings·记忆·权限)在一个会话里如何同时运作,以一行输入为基准展开来看。下面是输入"来看技能冷却时间的数值平衡"时实际发生之事的一个剖面。

settings.json 记忆 (JIT) 权限 (permissions)

UserPromptSubmit hook 触发 inject_memory.py 执行(保证 exit 0)

manifest 17 atom score 排序·regex 前 3 个·6000 字 应用上限 cooldown atom 附加到输入前

allow / deny 每次工具调用检查 读取自动 · 删除需批准

三条泳道在同一次输入上交会。settings.json 唤醒 hook,hook 挑出记忆附到输入上,由此生成的响应在调用工具时,权限作为最后一道关卡发挥作用。用户明明只敲了"来看冷却时间"这一行,三套基础设施却在看不见的地方依次干活。这就是工具让人感觉成了"我的工具"那一刻的内部结构。


1.3.6 首次配置指南 —— 一小时内进入可运营状态

理论看过了,就动手。第一次装好 Claude Code 之后,一个小时就能把上面那整张图铺到你自己的 PC 上。分成五个区间。

0\~10 分钟,确认安装·运行。 安装后在终端里启动 Claude Code。在某个文件夹里问一句"这个文件夹里有什么?",确认能得到响应。先看工具是不是活着。

10\~25 分钟,写好三个全局记忆。 自动加载的全局,三个文件就够了。

照着作者的样例原样抄来起步也好。在运营中慢慢打磨即可。

25\~40 分钟,settings.json 基本配置。effortLevel 设为 high,放入权限起始集(读取·检索自动,写入·删除需批准),再备一份备份(settings.json.bak_<日期>)。备份是这一区间里最重要的一行。

40\~55 分钟,第一批项目 atom 五个。 把本人每次都会忘的决定、经常要查的信息挑五个做成 atom。文件夹是 ~/.claude/projects/<项目>/memory/。格式参考第 5 章。有这五个,即便暂时不做 JIT manifest,仅靠全局自动加载也能见效。

55\~60 分钟,测试一次。 打开一个新会话,抛出一个本人领域的问题。确认全局记忆有没有自动加载,响应的语气是否遵循本人的协作规则。

到这里就是一个小时。JIT manifest 与 hook,等 atom 超过 50 个、自动加载开始变重的时候再引入也不迟。到那个时候,把 1.3.2 的 inject_memory.py 铺上去就行。


1.3.7 常见错误与规避法

引入初期反复出现的错误归为五类,而每一类都站在同一个事故成因之上。

错误 事故成因 规避法
往全局塞太多 所有会话都变重、浪费 token 全局控制在 5KB 以内,细节转移到项目记忆
把所有权限设为自动允许 便利遮蔽了危险的第一现场 只把读取·检索设为自动,写入·删除需批准(含季度清理)
不备份就改 settings 损坏的 settings 让工具本身起不来 改动前自动保存 settings.json.bak_<日期>
把 atom 无限堆进记忆文件夹 自动加载逼近 token 上限 约从 50 个起引入 JIT manifest
团队·个人配置混在一个文件里 git 冲突·个人配置泄漏 团队用 settings.json,本人用 settings.local.json

这五条没必要从第一天起就全部规避。全局膨胀和漏备份,最好在头一个小时内就把规避模式立住;其余三条,则在运营一个月左右、于本人最可能出事故的位置上装上规避装置,会更自然。


1.3.8 第 1 部分收尾

1.1 是缩短面对工具时那份距离感的场,1.2 是抓住该工具最小运作机制的场,1.3 则是用记忆·权限·settings 铺好第一套基础设施的场。这三章构成本书的导入部分。到这里收住,让工具不间断运营的基本骨架就立住了。

关键在于:这套骨架不是静态的配置。manifest 的 atom 每周都在增,allow 列表沿着工作痕迹变长,score 经过验证不断累积。基础设施不是铺下去那一刻就完成,而是在铺好的地基之上与使用者一同生长。个人 PC 的 17 个拉开到公司 304 个的差距,正是这份生长的距离。

从 Part 2 开始,正式进入信息架构。第 4 章 YAML 前置数据(frontmatter)、第 5 章 Atom、第 6 章 Layer、第 7 章本体论(ontology),依次铺开。在 1.3 里只作为 manifest 的一个条目出现过的 atom,到 2.2 会成为一整章的主角。脊柱立住之后,各分领域的章节才能在同一套坐标上找到自己的位置。


本章要点

下一章预告


动手试试

setup 1. 打开 ~/.claude/settings.json,在改动前以 settings.json.bak_<今天日期> 备份一份。 2. 在 permissions.allow 里放入 Read(*)Grep(*)Bash(ls:*)Bash(git status:*),在 permissions.deny 里写入 Bash(rm -rf:*)Bash(git push --force:*)。 3. (atom 在 50 个以上时)在 hooks.UserPromptSubmit 里登记 python ~/.claude/hooks/inject_memory.py,并在 _jit_manifest.json 里写好 atom 条目(name·path·regex·score)和 config.max_matches: 3

prompt - 在新会话里,抛出一个有意包含 manifest 中某个 atom 关键词的问题。例如:"以冷却时间规则为基准,来看技能数值平衡吧"。

verify - 确认输入之后是否出现 [memory injected: N atoms] 之类的信号。 - 看看预期的 atom 有没有反映到响应里。 - 故意往 manifest 里塞一段损坏的 JSON 试试,确认提示词是否依旧不被阻断、照常运作(保证 exit 0),然后再还原回去。

单人精简版 - 不要 hook,也不要 manifest,就这么起步。在全局 MEMORY.md 一个文件里只写身份 3 行 + 协作规则 3 行,权限只把 Read(*)·Grep(*) 设为自动允许。等 atom 用顺手、接近 50 个的时候,再把 1.3.2 的 hook 叠上去。基础设施是从小起步、沿着痕迹养大的,而不是一开始就备齐 304 个。

2.1 YAML 前置元数据 —— 让所有文档成为数据

里程碑构建前一天的晚上,系统策划团队成员 A 通过即时通讯工具问我:"这周改过奖励曲线的文档有几个?评审到哪一步了?"我答不上来。文档散落在某个文件夹里,谁最后改过它、它属于哪个里程碑,这些信息分散在各自的记忆和文件名约定里。那天晚上我们做的事,就是定下一条约定——在文档第一行写上六行字。正是那六行字,让从下一个里程碑起,团队成员 A 的问题不必有人打开文件夹就能回答。

文档最上方 --- 之间写的几行 YAML,就叫前置元数据(frontmatter)。这条约定让人和机器无需读正文一个字,就同时知道"这份文档是什么"。本章会用一段真实运行的脚本,跟踪这一行字如何成为整个信息架构的入口坐标。

先只交代一个术语。本书把策划文档分成五个 Layer(第 6 章正式展开):L0=世界观·概念,L1=系统规则,L2=内容,L3=数据,L4=实现坐标。自上而下依赖是正常方向。下文出现的 layer: 2,就是"这份文档属于内容 Layer"的坐标声明。


2.1.1 为什么不是"文档"而是"作为数据的文档"

传统的策划文档一直活在 Word、PPT、Google Docs 上。正文是为人阅读而优化的。可是文档的类型、责任、状态、位置这类元信息,要么融在正文里,要么依赖文件夹结构和文件名约定。于是要知道"这份文档属于哪个里程碑、谁是责任人、上次评审是什么时候",就得打开正文看。

这里叠加了两重局限。第一,文档不会自己说明身份。身份藏在人的记忆和文件夹约定里,而那套约定随时间腐化。第二,AI 没有线索去推断上下文。你对 Claude Code 说"帮我评审这份文档",它会把正文从头读到尾、浪费 token,也不知道责任边界到哪里。

YAML 前置元数据一次解决这两点。在文档第一行明确地写入元数据,人和机器都不必打开正文就能识别文档。这就像柜子抽屉正面贴了标签,不拉开抽屉也知道里面装什么。而且这枚标签不止是分类工具。后文会看到,仅 layer 这一个字段,就成了程序化生成与自动验收的入口坐标。


2.1.2 真实的前置元数据 —— 一份文档的头 14 行

不用抽象示例,我们直接看项目A的奖励曲线文档头上实际顶着的前置元数据(仅对 ID·实名做化名处理,结构与线上运营完全一致)。

---
title: "主线任务第12章奖励曲线"
layer: 2
status: review
owner: teammate_a
created: 2026-04-15
updated: 2026-05-20
related:
  - quest_main_chapter12
  - reward_curve_milestone_2
affects:
  - L3_BalanceSheet_v2
ip_check: passed
---

# 主线任务第12章奖励曲线

(正文开始)

关键是 --- 上方与下方的分离。上方是解析器读取的数据,下方是人阅读的正文。Markdown 渲染器通常会隐藏前置元数据,阅读时不受干扰。一个文件同时承载数据(frontmatter)和内容(正文),成为单一真实来源。

要特别留意 layer: 2affects: [L3_BalanceSheet_v2] 这两行。它声明的是"这份内容(L2)文档会影响数据 Layer(L3)的配置表"。仅凭这一点,工具就能不读正文、把 L2→L3 的依赖关系画成图。反过来,如果一份 L3 数据文档用 depends_on 去引用 L1 系统规则(自下而上的反向依赖),那就是设计上的坏味道。工具会自动检出这种反向引用。

YAML 比 JSON 更易于手写,原因很简单:用缩进表达结构,几乎不需要引号,还能写 # 注释。适合策划亲自填写。


2.1.3 标准住在哪里 —— _NAMING_FRONTMATTER_STANDARD

字段可以无限增加。越加,撰写负担越重,标准越容易崩。所以项目A分两层来运营:所有文档共通的最小核心字段,以及按领域分的扩展字段。

共通的最小核心字段有六个。

字段 格式 用途
title 字符串 人可读标题。可与文件名不同
layer 0\~4 第 6 章 Layer 坐标
status draft / review / approved / archived 文档状态
owner 用户名 责任人(1 人)
created YYYY-MM-DD 创建日
updated YYYY-MM-DD 最后修改日

仅这六个,就能即刻知道文档的新鲜度、责任与位置。头一个月要忍住继续添加的冲动。运营一阵后,哪个字段真正必要,会自然浮现。

按领域的扩展字段因领域而异。系统策划爱用 depends_on·affects,战斗策划爱用 combat_phase·anim_target,叙事爱用 world_region·chapter,数值策划爱用 data_sheet·formula_id。这些扩展字段不能各自随意发散,所以由唯一一份标准文档钉死它们的正式名称、允许值与示例。那份文档就是 _NAMING_FRONTMATTER_STANDARD.md。要新增字段,必须经过这份文档。而且这份标准文档本身被注册为 atom,与那条强制在文档名前加 Layer 编号的规则(docs_layer_numeric_prefix_naming atom)归在同一序列里管理。

这里发生了一个重要的转变。如果标准只是一份给人读的文档,人就会违反它。而把标准做成机器读取的数据,机器就会强制执行它。下一节就是这一转变的真实代码。


2.1.4 实操记录(worked transcript)—— 用代码强制标准,以及一个 datetime bug 带来的教训

实操记录(worked transcript):完整保留的真实操作过程记录。

现在我让 Claude Code 做一个"检查项目A所有 Markdown 文档是否遵守前置元数据标准的 Linter"。核心要求有两点:抓出检查项(必填字段缺失、status 非标准值、layer 0\~4 违例、状态为 review 却 90 天以上没动过的文档),并且不要把允许值硬编码进代码,而要从标准文档里读取。这一分离是关键。改了标准,不改代码,检查基准也随之改变。(脚本全文与直接运行步骤放在本章末尾的「动手试试」。)

这里出过一件事。Claude 最初给出的代码在 STALE 检查里用 today - fm["updated"] 计算日期差,并在注释里写道"像 updated: 2026-05-20 这样写的话,PyYAML 会自动解析为 datetime.date"。这话只对了一半。在真实文档上一跑,部分文件抛出了 traceback。

TypeError: unsupported operand type(s) for -: 'datetime.date' and 'str'

原因出在人手上。有的作者写 updated: 2026-05-20(被解析为 date),有的作者写 updated: "2026-05-20"、加了引号(被解析为字符串)。在标准没有钉死日期格式的地方,人手出现了分歧,而 Claude 只假设了其中一种。我拒绝了这段代码,重新要求"把两种写法都安全地规范化为 date,updated 缺失的情况也要过滤出来"。Claude 插入了一个检查输入类型、把两者都规范化为 datetime.date 的辅助函数(修正后的代码块也见「动手试试」)。

真正的教训不是代码 bug。而是标准没有钉死日期写法的地方,人手出现了分歧。于是我在 _NAMING_FRONTMATTER_STANDARD.md 里加了一行 updated: YYYY-MM-DD (不加引号)。Linter 在检查代码的过程中,反倒暴露了检查对象——标准本身——的漏洞。

修正后脚本的第一次输出并不干净。下面把真实跑出来的脏结果原样保留。

[NO-FM]   manuscript/legacy/old_combat_notes.md
[MISSING] manuscript/system/quest_flag_table.md: layer
[STATUS]  manuscript/content/town_intro.md: WIP
[LAYER]   manuscript/balance/dps_v2.md: None
[STALE]   manuscript/system/inventory_rules.md: 134d

这五行就是导入初期团队的真实状态。旧文档压根没有前置元数据(NO-FM),某份文档漏了 layer,有人写了 status: WIP 这样的非标准值,某份数值文档把 layer 留空成了 None,某份系统规则文档已经在 review 状态里睡了 134 天。标准从一开始就不会被遵守。Linter 只是每天早晨把这个事实摆出来而已。


2.1.5 从 frontmatter 到脚本 —— 流程

把上面的实操记录压缩成一张流程图,就是下面这样。它展示了人写下的一行字,如何一路流向机器的检查关卡。

flowchart TD A["作者:新建文档<br/>模板自动插入前置元数据 6 字段"] --> B["只填空值<br/>title / layer / status / owner ..."] B --> C["已保存的 .md 文件<br/>--- frontmatter --- + 正文"] C --> D["运行 Linter 脚本<br/>rglob('*.md')"] D --> E["_NAMING_FRONTMATTER_STANDARD.md<br/>读取允许值"] E --> F{"检查<br/>必填字段 / status / layer / stale"} F -->|"通过"| G["关系图输入<br/>related·affects → L 坐标依赖度"] F -->|"违例"| H["每日报告<br/>自动通知责任人 owner"] H --> B G --> I["可向 AI 提问<br/>'把 Layer2 review 文档汇总给我' → 即答"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class A,B human class D,F code class C,E,G data class H fail class I ai

关键有两点。第一,标准(E)与脚本(D)是分离的。改了标准,不改代码,检查基准也随之改变。第二,违例(H)不是死路,而是回到撰写阶段(B)的循环。这不是责怪人,而是把本人文档退回去让本人自己改。


2.1.6 运营案例 —— 某中等规模团队的 6 个月

作者作为总监运营的项目A,约在 6 个月前把前置元数据引入了整个策划团队(4\~5 人)。引入不是一蹴而就,而是经过了四个节点。

引入第 1 周最大的抵触是"这玩意儿每次都要手写?"。每份新文档都要背着写六行,确实麻烦。解法是模板自动插入。VSCode 代码片段、Obsidian 模板、策划门户的"新建文档"按钮,会自动塞进一个空的 YAML 块。作者只填空值。抵触在一周内消失了。

第 1 个月爆发了标准冲突。多人各自随意添加字段,owner·responsible·author 同时出现。明明是同一个概念,却有三种写法,搜索和自动化都崩了。解法是用 _NAMING_FRONTMATTER_STANDARD.md 一份文档整理所有字段的正式名称、允许值与示例,并把新字段的添加规则化为必经此文档。标准在一个月内稳定下来。

第 3 个月,2.1.4 里那个 Linter 进来了。哪怕有标准,人也会违反。所以让每天早晨自动生成一份一致性报告,落到团队即时通讯工具的公共频道里。责任人只看自己的文档就行。自动化之后,标准违例明显减少(作者估测,非精确测量值——体感大致降到一半以下)。

第 6 个月,与 AI 的结合大放异彩。标准稳定后,下面这类提问都能即答返回。

最终,前置元数据成了人与 AI 之间的公共词汇。人写,AI 理解;AI 写,人验证。两边看同一批 key。只不过第 1 周的抵触、第 1 个月的冲突、第 3 个月的 Linter、第 6 个月的结合——这是累积 6 个月才形成的结果,不是一蹴而就的。


2.1.7 常见错误与规避法

导入初期反复出现的错误,可归为五类。它们都站在同一个根上——"把标准只交给人的意志的那个地方"。

错误 事故原因 规避法
一开始就定义太多字段 作者填空值填到累,质量下降 从核心 6 个起步,1\~2 个月后只补常用的
字段名不停改(tagtagscategory) 旧名残留在累积文档里,搜索·自动化崩坏 改名时同步迁移脚本。发现旧名时自动转换或告警
每次都人手写 拼写错误·字段缺失·日期写法分歧(2.1.4 那个 bug)成了家常便饭 优先模板·代码片段·"新建文档"自动化。人手只用在有意义的值上
只立标准、不验证就放任 哪怕有标准也不知道谁违反了,自然腐化 用 Linter + 每日自动报告,让违规者本人去改
忘了 layer 字段 没有 Layer 坐标,跨领域可见性与验收关卡都建不起来 layer 强制为必填字段。Linter 检出缺失

五个错误不必从第一天就全堵上。第 1·3 项在导入第 1 周就把规避模式立好,第 2·4·5 项则边运营、边从本团队最常撞上的地方开始依次嵌入,这样更自然。


2.1.8 小处起步 —— 在 3 周内落地

前置元数据的导入,意外地是项轻活。3 周就能在一个团队里站稳。

第一周定义核心六字段、做好模板,只对新文档应用,把撰写负担降到最低。第二周对常看的前 20 份文档手动应用,在真实使用中检查哪些字段不够用。第三周启动 Linter 和每日报告,从那时起,标准就不再靠人的意志、而是靠工具之力来维持。

不要把现有文档全部一次性迁移。从常看的开始,从新文档开始。过了 6 个月左右,几乎所有文档都会带上前置元数据。但 100% 并不是目标。为了把一次都没打开过的旧文档也迁移而花时间,是浪费。


动手试试

以最小单位,亲手跑一遍完整的一个循环。

setup - 在工作文件夹里放 2\~3 份待检查的 .md 文档。其中一部分故意去掉 layer,或填入 status: WIP 这样的非标准值。 - 在同一文件夹放一份标准文档,写上几行。 status: allowed = ["draft", "review", "approved", "archived"] updated: YYYY-MM-DD (不加引号)

prompt(输入给 Claude Code)

给我写一个 Python 脚本,检查这个文件夹下所有 .md 的 YAML 前置元数据。抓出必填字段 title·layer·status·owner 缺失、status 违反允许值(从标准文档里读取)、layer 非 0\~4 整数、status 为 review 但 updated 超过 90 天。updated 无论以字符串还是 date 形式传入,都要安全处理,并按文件逐个输出违例。

verify - 跑一遍脚本,确认故意埋的违例是否全被抓到。 - 在标准文档的 allowed 列表里加上 WIP 后再跑一遍,确认在没改一行代码的情况下,status: WIP 是否变成通过。这就是标准与代码已分离的证据。 - 把加引号的文档和不加引号的文档都放进去,确认不会出现 2.1.4 里看到的 TypeError

参考:Linter 脚本全文

这是 2.1.4 里 Claude 最初给出的代码。STALE 检查那一行(age = (today - fm["updated"]).days)里原封不动地带着 datetime bug。

import sys, datetime, pathlib, re
import yaml  # PyYAML

ROOT = pathlib.Path("manuscript")
STANDARD = pathlib.Path("_NAMING_FRONTMATTER_STANDARD.md")
REQUIRED = ["title", "layer", "status", "owner"]

def load_allowed_status(standard_path):
    # 从标准文档中提取 `status` 的允许值
    text = standard_path.read_text(encoding="utf-8")
    m = re.search(r"status:\s*allowed\s*=\s*\[(.*?)\]", text)
    if not m:
        return ["draft", "review", "approved", "archived"]
    return [s.strip().strip('"').strip("'") for s in m.group(1).split(",")]

def parse_frontmatter(md_path):
    text = md_path.read_text(encoding="utf-8")
    if not text.startswith("---"):
        return None
    end = text.find("---", 3)
    block = text[3:end]
    return yaml.safe_load(block)

def main():
    allowed = load_allowed_status(STANDARD)
    today = datetime.date.today()
    violations = 0
    for md in ROOT.rglob("*.md"):
        fm = parse_frontmatter(md)
        if fm is None:
            print(f"[NO-FM]   {md}")
            violations += 1
            continue
        for field in REQUIRED:
            if field not in fm:
                print(f"[MISSING] {md}: {field}")
                violations += 1
        if fm.get("status") not in allowed:
            print(f"[STATUS]  {md}: {fm.get('status')}")
            violations += 1
        if not isinstance(fm.get("layer"), int) or not (0 <= fm.get("layer") <= 4):
            print(f"[LAYER]   {md}: {fm.get('layer')}")
            violations += 1
        if fm.get("status") == "review":
            age = (today - fm["updated"]).days   # ← 这里会崩
            if age > 90:
                print(f"[STALE]   {md}: {age}d")
                violations += 1
    sys.exit(violations)

重新要求后修正过来的核心代码块。updated 无论以字符串还是 date 形式传入,都安全地规范化。

def as_date(v):
    if isinstance(v, datetime.date):
        return v
    if isinstance(v, str):
        return datetime.date.fromisoformat(v.strip())
    return None

# main() 中 STALE 检查的替换部分
if fm.get("status") == "review":
    upd = as_date(fm.get("updated"))
    if upd is None:
        print(f"[MISSING] {md}: updated")
        violations += 1
    elif (today - upd).days > 90:
        print(f"[STALE]   {md}: {(today - upd).days}d")
        violations += 1

单人精简版

没有团队也行。在自己用的笔记文件夹里,把核心字段缩到 title·status·updated 三个,Linter 只抓"status 为 review 但 updated 超过 30 天的文档"。仅凭这一点,"我评审到一半就忘掉的文档"每周就会浮上水面一次。标准-模板-检查的三角形,在单人规模下也照样运转。


本章要点

2.2 按页 Atom —— 单文档单决策的解剖

新人入职的第一周,他在聊天里问我:"战斗冷却时间是 0.6 秒,对吗?写在哪份文档里?"我答:"在技能系统 GDD(Game Design Document,详细规格文档)里。"他又问:"那 GDD 的哪一节?从职业设计到伤害曲线,再到 UI 显示方式,一共 220 行。"我打开文件,亲自帮他找。在第 137 行。他最后问:"可为什么是 0.6 秒?0.5 不行吗?"这个答案任何文档里都没有。我记得是六个月前的一次会议上定的,但理由埋在某份会议记录的某个角落里。

这场五分钟的对话里,包含了 220 行整合文档的全部三种失败:找不到位置(检索失败)、没有理由(脉络丢失)、每次都得有人居中转述(无法自动化)。把同样的问题抛给 AI,情况更糟。AI 会把 220 行全部读完,然后连和冷却无关的伤害曲线也一并掺进答案里。

本章的处方很简单。一份文档只装一个决策。按这个原则切得很细的决策单元文档,就叫 atom。把 220 行的 GDD 拆开,"冷却时间是 0.6 秒"就成了一个 atom,而这个 atom 里,位置、内容、理由、例外、关系都汇聚在一处。本章不讲抽象理论,而是把一个真实的 atom 从头解剖到尾:它如何命名、写入哪些 frontmatter、如何标明关系,以及最终 AI 如何只精准地拎出这一个 atom。


2.2.1 取一份检材 —— combat_cooldown_rule_v2

要解剖的检材,是项目A中实际运行的一个 atom。它的名字是 combat_cooldown_rule_v2。文件全文如下。不长,因为只装了一个决策。

---
name: combat_cooldown_rule_v2
title: "战斗冷却规则 —— v2"
type: rule
layer: 1
status: approved
owner: 李旼洙
created: 2026-03-10
updated: 2026-05-12
applies_to: [skill_system, item_system]
---

# 战斗冷却规则 v2

Why(为什么):限制可同时使用的技能数量,以减轻瞬时决策负担,
并保留连招输入的意义。

Rule(规则):所有主动技能都具有全局冷却 0.6 秒 + 单独
冷却(各技能自定义)。全局冷却进行期间,任何
主动技能都无法施法。

How to apply(适用):
- 定义新技能时必须明确标注单独冷却
- L3_SkillSheet 的 cooldown 列若为 0,则违反本规则
- 构建阶段的一致性检查会自动检出违规

Exceptions(例外):
- 被动技能不适用本规则
- 终极技能采用单独的能量条系统(See: [[ultimate_gauge_system]])

Relations(关系):
- affects: [[combat_dps_calculation_v3]], [[balance_curve_v3]]
- derives_from: [[principle_decision_load_reduction]]
- conflicts_with: [[skill_cancel_rule_legacy_v1]]
- requires: [[combat_input_buffer_system]], [[skill_system_v2]]
- is_a: rule
- part_of: combat_system_master

把这一页文件分成五个部位来看:命名、frontmatter、单一决策、关系、可追溯性。五个部位都齐备,AI 才会把这个 atom 读作"独自也说得通的单元"。


2.2.2 部位 ① 命名 —— 名字本身就是坐标

文件名是 combat_cooldown_rule_v2。这不是随手起的名字,而是有三段式结构。

combat_         cooldown_rule          _v2
└ prefix        └ 决策正文            └ 版本
  (哪个领域)     (关于什么的决策)       (第几次修订)

prefix combat_ 是"这是战斗领域的决策"这一坐标。项目A的规则 atom 以 prefix 区分领域:quest_(任务)、data_(数据运营)、docs_(文档运营)、meeting_(会议记录)、portal_(策划查看器)。光看 prefix,就能抓住这个决策属于谁的责任范围、会从哪里受到影响。

命名一旦动摇,一切都跟着动摇。同一个决策若以 skill-cooldown.mdcooldown_skill_v2.md 两次出现,检索会崩,后文要讲的 JIT 匹配也会崩。所以项目A先把命名规则本身固化成了一个 atom。那就是 atom_naming_convention_v1,它强制要求 snake_case、必带 prefix、版本 suffix。而且这条规则不靠人的自觉,而是由 Linter 来守。没有 prefix 的文件名一旦被提交,就会在构建阶段被拦下。

命名背后,埋着贯穿全书的更大设计。frontmatter 里的 layer: 1 就是第二个坐标。如果说 prefix 指明"哪个领域",那么 Layer 指明"哪个抽象层级"。两个坐标结合,atom 的位置才被确定为平面上的一个点。这里 Layer 只是坐标(0\~4 层级定义的细节见 2.3)。冷却规则是"控制生成的输入规则",所以坐落于 Layer 1。把这个 Layer 坐标以数字 prefix 强制写在文档名前的规则也另有一条 —— docs_layer_numeric_prefix_naming。一个名字里,等于明示了两条坐标轴。

这套设计的本质不是整理癖。我对团队反复说过一句话。"当初分 Layer,就是为了做程序化生成。"只要每个 atom 都明示了领域坐标(prefix)与层级坐标(Layer),日后 AI 就能做到"把 Layer 1 的全部 combat 规则作为输入,自动生成 Layer 2 的内容"。名字,就是那套自动化的寻址体系。


2.2.3 部位 ② frontmatter —— 机器读取的标签

正文上方 --- 之间的 YAML 块,就是 frontmatter。它是把 2.1 讲过的标准原样应用到 atom 上,是给机器(构建脚本、JIT hook、关系图生成器)而非给人读的标签。

字段 机器用它来做的事
name combat_cooldown_rule_v2 成为其他 atom link 目标的唯一 ID
type rule 按类别统计、筛选(rule / concept / decision ……)
layer 1 按 Layer 着色、排序,反向引用检出的基准轴
status approved draft、approved、archived 中只有 approved 进入构建
applies_to [skill_system, item_system] 影响范围 —— 这条规则触及的系统
created/updated 2026-03-10 / 2026-05-12 变更追踪,陈旧 atom 排查的基准日

这些标签写好了,自动检查就成为可能。例如,被声明为 layer: 1 的系统规则,若在正文里直接引用 [[L3_SkillSheet_row_0042]] 这样的数据 atom(Layer 3),那就是上层被绑死在下层具体值上的反向引用(L3→L1)。项目A在构建阶段自动检出这种模式。因为规则应该引用数据的格式,而不是数据的某一行。frontmatter 里没有 layer 这一行,这项检查本身就无从成立。

status: archived 的处理也是 frontmatter 的活儿。决策变了,atom 不被删除,而是获得 status: archived + archived_at 日期。构建与 JIT 会排除 archived 的 atom。记录留下,但退出现役。在项目A六个月的运营中,废弃率约为 15%(作者实测)。如果这个比例接近 0%,就读作废弃工作流没有运转的信号。


2.2.4 部位 ③ 单一决策 —— 能否用一句话概括

atom 解剖的核心,是确认正文是否只装了一个决策。检查法很简单。试着把这个 atom 的决策用一句话概括。

"所有主动技能都具有全局冷却 0.6 秒。"

一句话就结束了。合格。如果概括变成"冷却是 0.6 秒,连招中缩短 50%"这样的两句,那就是两个决策。要拆成 combat_cooldown_rule_v2(基础冷却)和 combat_combo_cooldown_reduction_v1(连招缩短)。

判断单一性还有两个辅助检查。

独立废弃检查。只废弃这一个 atom,系统会不会垮?废弃冷却规则,战斗平衡会动摇,但系统照转。单元是对的。反过来,如果废弃它会连带另外五个一起垮,那这五个其实是一个决策的五块碎片。该合并成更大的 atom。

单一引用检查。别处只挂 [[combat_cooldown_rule_v2]] 这一个 link,意思是否通?通,单元就对。如果为了引用这一行,得把正文好几处都读一遍,那就是还没拆够。

通过这些检查的正文,自然会对齐成五个小节 —— Why、Rule、How、Exceptions、Relations。尤其是别删掉 Why。前面引子里,新人最后问的"为什么是 0.6 秒?",答案就在这儿 —— "为减轻瞬时决策负担、保留连招输入的意义。"六个月后,有谁提议"减到 0.5 秒"时,这一行就成了讨论的起点。失去 Why 的 atom,会变成谁也不敢动的化石。


2.2.5 部位 ④ 关系 —— 箭头制造出影响分析

atom 最下方的 Relations 小节,把这份检材从一张孤立的便签,变成图中的一个节点。关键不在于只写"相关文档",而在于明示关系的种类

flowchart TD P["原则:决策负担减轻"] -->|derives_from| C["combat_cooldown_rule_v2"] C -->|affects| A1["combat_dps_calculation_v3"] C -->|affects| A2["balance_curve_v3"] C -->|requires| R1["combat_input_buffer"] C -->|requires| R2["skill_system_v2"] C -.->|conflicts_with| X["skill_cancel_legacy_v1<br/>(待废弃的冲突)"] classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class P,C,A1,A2,R1,R2 data class X fail

六种关系各自做着不同的事。

若只是简单的 "Related: [文档A]、[文档B]" 链接,就得人去逐一推敲。关系类型一旦以 enum 写入,机器就能推敲。"把改动这个 atom 会受影响的全部列出来",就成了沿 affects 追溯的自动查询;"找出现在互相矛盾的所有规则",就成了扫描 conflicts_with 的自动检查。这六个 enum 正式的本体论(ontology)设计放在 2.4 讲,2.2 只点明:atom 标准是预先套用了那套 enum 的形态。

关系箭头同时也是关系图生成工具的输入。项目A的 gen_relation_map.py 会读取所有 atom 的 frontmatter layer 与 Relations 小节,自动绘出按 Layer 着色的交互式关系图 HTML。正因为每一个 atom 都带着坐标(Layer)与箭头(Relations),这才成为可能。


2.2.6 部位 ⑤ 可追溯性 —— 一个 atom 挡下的 30 分钟

五个部位都齐备的 atom,是可追溯的。谁、何时、为何作出这个决策,把什么判为违规,全都在一处。可追溯性的价值,在用真实挡下的事件来呈现时,才最为鲜明,而不是靠统计。

项目A的 meeting_image_caption_standard atom,是一条规则:会议记录里附的图片,必须以图注明确标注"是哪个画面、为什么附上、是什么决策"。没有这个 atom 的年代,一张截图没带图注就贴进了某份会议记录,一周后看到它的同事为了向作者确认"这是什么画面?",花了 30 分钟。有了这个 atom 之后,同样的遗漏再次发生时,构建阶段的 Linter 自动逮住了没图注的图片。改到完成只用 5 分钟。30 分钟变成了 5 分钟。

另一份检材 skill_listing_budget_wrapper_only_policy,是这样一条规则:把全局斜杠命令槽位限制为 12 个,本体技能另置于单独目录,但在全局只暴露 12 个 wrapper。固化之前,全局斜杠命令一度膨胀到将近 40 个,每次会话开始都在啃食 token 预算。定义了这个 atom 之后,自动整理工具会在每次会话开始时清理超额部分。规则靠工具来执行,而不是靠人的记忆。

这样的 atom,在项目A里累积了约 304 个(作者实测,六个月运营时点)。只看分布的大类:防止复发的规则(rule)占比最大,其次是一次性决策的固化(decision)、领域概念(concept)、协作校正(feedback)。一个 atom 挡下的时间以分钟计,但 304 个累积起来,节省的总量就跨进了以天计。这就是把 atom 称作"资产"而非"整理"的理由。


2.2.7 把解剖变成自动注入 —— JIT 的实际运作

至此,我们对一个 atom 作了静态解剖。现在来看它活动起来的瞬间。1.3 的 JIT(Just-In-Time)hook,只挑出与输入关键词匹配的 atom,当场注入上下文。JIT manifest 是一份把匹配关键词与分数映射到各 atom 的 JSON。

{
  "name": "combat_cooldown_rule_v2",
  "path": "atoms/combat/combat_cooldown_rule_v2.md",
  "regex": "쿨다운|cooldown|글로벌 쿨다운|GCD",
  "score": 75
}

实际注入是这样流转的。

flowchart TD A["用户输入:<br/>把技能冷却减到 0.5 秒会怎样?"] --> B["JIT hook:<br/>扫描 manifest 的 regex"] B --> C{"冷却匹配?"} C -->|"是 score=75"| D["combat_cooldown_rule_v2<br/>全文注入"] C -->|"否"| E["不注入"] D --> F["AI 读完 Why、Rule、Exception<br/>后再作答"] F --> G["回答:0.6 秒以决策负担<br/>减轻为依据。减到 0.5 秒则需<br/>重新审视 affects 对象 DPS、<br/>平衡曲线"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class A human class B,C code class D data class F,G ai

关键在最后一格。AI 不会只答"曾经是 0.6 秒"。它读了 atom 的 Why,就给得出依据;读了 Relations 的 affects,就连改动时会动摇的对象(DPS 计算、平衡曲线)也提前点到。切细、写明理由、标注关系的五个部位,全都在回答里活了起来。

到这里,单一决策原则是自动化前提这一点就显露了。假如这个 atom 是 220 行的整合 GDD,"冷却"一词被匹配的那一刻,职业设计、伤害曲线、UI 就会被整块注入,token 预算被削减,AI 也会在五个决策中迷失该答哪一个的焦点。atom 越小越清晰,JIT 准确度越高。切得细,不是整理的美德,而是自动注入的前提条件。

score 是守住上下文预算的装置。一次输入匹配到多个 atom 时,只注入 score 排名前 N 个(默认 3 个)。打分标准在运营中定。


2.2.8 个人 atom 与团队共享 atom —— 两层的分离

解剖过的检材 combat_cooldown_rule_v2,是拿到 status: approved 的团队共享 atom。并非所有 atom 一开始就坐到这个位置。项目A把 atom 分为两层。

分层的理由是心理上的。个人 atom 自由,才能毫无负担地写下验证前的假设,并在一周后废弃。如果一开始就对团队公开,就会因"这要是错了怎么办"而干脆不写。反过来,团队共享 atom 严格,全员才会信任并引用。

combat_cooldown_rule_v2 起初恐怕也只是个人 atom 里"冷却 0.6 秒,试试看吧"的一行备忘。在 Alpha 版本中验证过后,以变更请求的形式晋升为团队共享,再经另一位策划评审,成了 approved。这套个人→团队的晋升流程本身,就是 atom 系统随时间变聪明的 self-improving 循环的一条主轴。


2.2.9 五种常见错误

atom 运营初期反复出现的错误,可归纳为五种。它们都出自同一个根:"把 atom 当作一次性备忘,而非资产。"

错误 什么被破坏 规避法
头一周造太多 未验证的 atom 堆积,运营垮掉 从验证过的一两个起步,交给自然增长
不做废弃 陈旧 atom 持续被 JIT 匹配,生成错误答案 季度排查,status: archived + archived_at
太抽象/太具体 "做出好设计"无法验证,一行杂感毫无意义 做到"攻击距离只有 0.5/1.5/3.0/5.0"的程度
命名不一致 检索、JIT 匹配整块崩坏 先做命名规则 atom,再用 Linter 强制
不写 Why 时间一久,变成谁也不敢碰的化石 强制 Why、Rule、How、Exception、Relations 五个小节

不必从第一个月就完美避开这五种。第 1 和第 4 种,用一个命名规则 atom 就一起解决了;第 2、3、5 种,在运营第三个月时跑一次季度排查,自然就会对齐。


2.2.10 通往下一章

本章把一个 atom 分成五个部位看了一遍:名字(坐标)、frontmatter(机器标签)、单一决策(一句话检查)、关系(影响分析)、可追溯性(挡下的 30 分钟)。并确认了这五个部位在 JIT 自动注入中如何整块活起来。

名字里明示的两条坐标,其中之一 layer: 1,2.2 只是一带而过。2.3 会正面处理那个 Layer。只要给每个 atom 赋予 Layer 坐标,即便分属不同领域,也开始看得见彼此的产出物坐落在哪里。而 2.4 会把本章只借用了 enum 名字的六种关系(affects、derives_from、conflicts_with、requires、is_a、part_of)正式形式化为本体论。在 YAML(2.1)→ Atom(2.2)→ Layer(2.3)→ Ontology(2.4)一路延伸的信息架构骨架中,本章是其第二个关节。


本章要点


动手试试 —— 造一个 atom 并用 JIT 注入

setup. 在工作文件夹里建一个 atoms/ 目录,最先写命名规则 atom(atom_naming_convention_v1)。哪怕只写 snake_case、必带 prefix、版本 suffix 三行也行。如果用 JIT,就放一个 _jit_manifest.json 空数组。

prompt. 挑一个你每次都忘的决策,用下面的提示词拿到 atom 初稿。

"把下面这个决策做成 atom 标准格式。决策:'主动技能具有全局冷却 0.6 秒。'小节为 Why、Rule、How to apply、Exceptions、Relations 五个。frontmatter 里放 name(snake_case+prefix)、type、layer、status: draft、owner、created。最后再确认决策能否用一句话概括。"

verify. 用三点检查拿到的 atom。① 决策能否用一句话概括(不能就拆开)。② Why 是否非空。③ 在 manifest 里加上 {"name", "path", "regex", "score"} 一行,把那个 regex 关键词作为真实输入抛出去,atom 是否被注入。三点都通过,第一个 atom 就完成了。


单人精简版

如果你是没有团队、没有 Linter、没有构建流水线的单人开发者,可以把本章整章压缩成一个笔记应用的文件夹。

核心不是工具,而是五个部位的习惯。最初的 10 条笔记最难,熬过那个坎,接下来的 100 条,手会自己造出来。

2.3 Layer 设计 —— 游戏系统抽象化

那是分工从三个增长到八个的时段。战斗策划把技能攻击距离定为 8m。同一周,关卡设计师把副本通道宽度锁定为 6m。两者在各自分工内都是完全合理的决定。问题在三周后的版本里暴露出来。范围技能穿过通道墙壁打出去,敌人在玩家根本看不见的地方被打死。这不是任何人的失误。只是两个人没有一扇窗户能看见彼此的决定而已。

本章讲的就是造出这扇窗户的故事。让每个分工保留自己的房间,同时只凭一个坐标就能知道隔壁房间里在发生什么。这个坐标系,就叫作 Layer。


2.3.1 孤岛化 —— 每次都会再次遭遇的敌人

游戏策划的分工被切得很细。系统、战斗、叙事、内容、关卡、数值、UX、QA。每个分工都有自己的工具、产出物、会议。规模越大,各自越是深入自己的领域,陷入不知道别的分工在做什么的状态。这就叫作孤岛(silo)化。

孤岛化的代价要等时间过去才会显现。

原因不是能力不足。各自在自己的分工里做了合理的决定,只是没有一条通道能让人察觉别的分工的决定而已。用会议来填补,会议就会暴增;用群聊来填补,信号就会被噪声淹没。这并不是说会议和群聊毫无价值,关键在于把"能填补的部分"和"不能填补的部分"之间的边界划清楚。

解决办法是:既不收窄各自的领域(保持分工分化),又能让彼此看见对方的流程(统一可见性)。这两个看似冲突的诉求,只要对齐到同一个坐标系上,就能同时达成。那个坐标系就是 Layer。拿办公室来打比方,就相当于每个人都有自己的桌子,同时看着同一面挂钟和日历。


2.3.2 Layer 的定义 —— 5 层抽象

本书使用的 Layer 是 0\~4 的 5 层抽象。越往上越抽象、越少变更;越往下越具体、变更越频繁。

抽象 · 不变 具体 · 变动

L0 愿景·核心价值 程序化生成角色:上下文锚点 —— 不变,每次调用都注入的基准点

L1 系统·世界骨架 程序化生成角色:生成输入规则 —— 规则手册·关系·标签(生成器遵循的约束)

L2 内容·流程 程序化生成角色:生成正文堆积的地方 —— 任务·进度·关卡曲线

L3 实现·配置表 程序化生成角色:数值·ID·关系 —— 模拟的输入值

L4 版本·QA 产出物 程序化生成角色:验证关卡 —— 版本结果·缺陷·试玩捕获

五个层各自在程序化生成·自动化管线中所承担的角色,写在上图右侧的标签里。这个映射是本章的脊柱。如果只把 Layer 看成"整理得很好的文件夹",那只看到了一半。每个层都精确对应生成管线的某一个阶段(锚点 → 规则 → 正文 → 数值 → 关卡)。

Layer 装什么 变更频率
Layer 0 游戏想给玩家的核心体验。可压缩为一句话 极低(贯穿项目整个生命周期)
Layer 1 游戏系统的大结构与世界观骨架 低(以里程碑为单位)
Layer 2 游玩流程、任务线、进度阶段、关卡曲线 中(以冲刺为单位)
Layer 3 实际数据值、参数、公式、变量 高(以日为单位)
Layer 4 在版本中确认到的结果、缺陷报告、试玩视频 极高(实时)

这 5 层并非游戏专用概念。可以把同一条脊柱原样搬到一般 IT 产品开发上。没有做过游戏的读者,请用下面的职务翻译表把每一层对应到自己的产出物上(左边是游戏策划的 Layer,右边是 SaaS·App·内部系统等中放在相同位置上的产出物)。

Layer 游戏策划 一般 IT 产品 相同的问题
L0 核心体验 想给玩家的核心体验(一句话) 产品愿景 —— 为谁、解决什么问题、怎么解决 "为什么要做这个"
L1 系统规则 系统结构·世界观骨架 业务·功能规则 —— 领域规则、权限模型、核心工作流 "什么应该如何运作"
L2 内容 任务线·进度阶段·关卡曲线 发布·路线图 —— 功能打包、上线顺序、里程碑 "在什么时候放出什么"
L3 数据 数据值·参数·公式 规格表 —— API 规格、字段定义、配置值、阈值 "准确的值和定义是什么"
L4 版本·QA 版本结果·缺陷·试玩视频 部署·QA —— 部署产出物、缺陷报告、监控日志 "实际放出去的东西跑得对不对"

读法和游戏完全一样。越往上变更越少(产品愿景每季度变一次),越往下越频繁(配置值每天都变)。前面看到的孤岛事故 —— 攻击距离和通道宽度冲突的那一幕 —— 与一般 IT 中"后端字段定义(L3)和前端画面规则(L1)对不上,在上线前夕爆掉"的事,是完全相同的结构。只是分工的名字不同,脊柱是同一条。

这 5 层并非绝对。根据规模和领域,4 层可能就够,也可能需要 6 层。关键不在于数字是不是 5,而在于"明确定义层级"这一行为本身。

一个产出物也可能横跨两个 Layer。"技能系统 GDD(Game Design Document,详细规格书)"同时装着系统设计(Layer 1)和具体数据(Layer 3)。这时要么把文档拆开,要么把主 Layer 定为 1、把数据部分分离成单独的表格,但无论哪种方式,都要明确标出每个部分住在哪个 Layer。


2.3.3 元原则 —— 同时实现分化与整合

分工横向铺开,Layer 纵向堆叠。一个分工的工作横跨多个 Layer。下面的矩阵用单元格的颜色深浅,表现 11 个分工(横轴)× Layer 0\~4(纵轴)的分布重心。深色格就是那个分工的重心 Layer。

系统 战斗 叙事 内容 关卡 数值 UX/UI QA 角色 美术 运营

L0 愿景 L1 系统 L2 内容 L3 数据 L4 版本·QA

重心 次要分布 微弱·无

纵向读,能看出一个分工横跨哪些 Layer;横向读,能看出一个 Layer 聚集了哪些分工。L0(愿景)这一行,叙事和美术指导最深 —— 这是离愿景最近的两个分工。L3(数据)这一行,系统·战斗·关卡·数值·角色聚得很深 —— 这是它们在配置表里彼此碰撞的信号。

只要明确地拥有这份分布,别的分工就能立刻知道"得去看战斗的 Layer 2"的位置。这不是孤岛的墙被推倒,而是在墙上凿出了一扇窗。

把整个矩阵压成一句话就是这样:纵轴 Layer 是为了把生成自动化而分,横轴分工是为了发挥专业性而分。两者在网格的某一格里相遇。


2.3.4 运营案例 —— 某 MMORPG 项目的实测

笔者作为设计总监运营的 MMORPG 项目A,与策划团队(4\~5 人)一起把 Layer 系统运营了约 6 个月(整个开发团队属于中等规模,10\~50 人)。看看具体案例。

先看叙事 5 层。叙事策划文件夹本身就按 Layer 做了分割。

NarrativeDocs/ Layer0_Vision/ 世界的核心信息,1.1~1.2 Layer1_World/ 地区·势力·时代设定 Layer2_StoryLine/ 主线任务流程 Layer3_DialogueSheet/ 实际台词·名称数据 Layer4_BuildVO/ 已进入版本的配音

叙事作者在 Layer 2 改动主线故事的一个分支,就会影响到 Layer 3 的台词表,对已经录好的 Layer 4 配音则可能产生不可逆的影响。正因为明确标了 Layer,才能立刻追溯影响范围。

关系图自动生成工具 gen_relation_map.py 也一并在运营。它分析配置表之间的外键关系,生成交互式 HTML 关系图,并用节点颜色表现 Layer(红=L1 系统,黄=L2 内容,绿=L3 数据)。从哪个 Layer 向哪个 Layer 流动依赖,一目了然。如果依赖倒着流 —— L3 朝 L1 射出箭头 —— 几乎总是设计缺陷。

程序化关卡生成的主文档,把 Layer 坐标明确写在 frontmatter 里。

---
title: 程序化关卡设计主文档 v0.1
layer_inputs: [L1.World, L2.StoryLine]
layer_outputs: [L3.LevelData, L4.PlayCapture]
---

这两行就声明了"这条管线以 Layer 1·2 为输入,生成 Layer 3·4",并且在变更时影响范围的计算被自动化。L0 愿景即使不写也始终是输入 —— 因为任何生成,愿景锚点每次都会跟着。

还有一条强制在文档名里加 Layer prefix 的 atom 规则。团队共享的 atom 之一是这样:

docs_layer_numeric_prefix_naming:配置表文件名必须带 Layer 编号 prefix(L1_L2_L3_)。没有 prefix 的表会在一致性检查中告警。

规则越简单越强大。只要按名称排序就会按 Layer 聚到一起,AI 工具也仅凭文件名就知道 Layer。人忘了,一致性检查也会抓住。


2.3.5 反向引用检出 —— 实操记录

前一节(2.3.4)说过"L3 朝 L1 射出箭头几乎总是设计缺陷"。如果把这种检出交给 AI 而不是人眼,会怎么样。把实际跑过的一段过程不加修饰地原样照搬过来(仅对公司可识别信息做匿名化)。

[提示词全文]

看一下附上的两个 frontmatter,Layer 依赖方向正不正常。
引用只有从高编号→低编号流动才算正常。倒过来就告警。

--- 文档 A (L1_combat_system.md) ---
title: 战斗系统骨架
layer: L1
refs: [L3_skill_damage_sheet, L2_combat_flow]

--- 文档 B (L3_skill_damage_sheet.md) ---
title: 技能伤害表
layer: L3
refs: [L1_combat_system]

[Claude 原始输出 —— 不加摘要,原样照搬]

我检查了两个文档。

文档 B (L3_skill_damage_sheet, L3):
  refs → [L1_combat_system]
  方向:L3 → L1 (具体 → 抽象)
  判定:正常。配置表引用自己所遵循的系统骨架,
        是自然的方向。

文档 A (L1_combat_system, L1):
  refs → [L3_skill_damage_sheet, L2_combat_flow]
  - L1 → L2 :正常 (设计指向下层流程)
  - L1 → L3 :⚠ 反方向告警
        L1 系统骨架直接引用了 L3 配置表。
        这意味着"设计文档从属于特定的数据值",
        会造成数据每次变更都得动设计文档的
        耦合。

建议:从文档 A 的 refs 中移除 L3_skill_damage_sheet。
      如果系统骨架确实需要知道具体数值,就把那个数值
      作为抽象规则上移到 L1(例如:"基础伤害为
      职业系数 × 等级"),或经由 L2 流程文档中转。

摘要:检出反向引用 1 处 (A: L1 → L3)。

[人工验证 / 否决]

AI 的判定是对的。只是"经由 L2 流程文档中转"的建议在这个案例里过头了。L1 需要知道的是伤害公式的形态,而不是表里的某个特定单元格。所以只采纳了第二条建议(上移为抽象规则),否决了中转的提议。

[再次请求]

去掉中转的提议,只按"把伤害公式的形态作为抽象规则
上移到 L1"这个方向,给出 L1_combat_system.md 的修改 diff。
也把 refs 整理一下。

这一个循环就是反向引用检出的大本营。AI 抓住方向违规(自动),人削掉建议的适当尺度(评审),只把收窄后的工作再交回去(再次请求)。在项目A中,gen_relation_map.py 以图为单位、portal_layer_change_impact_check atom 在变更被检测到的时点触发,强制做影响范围检查。

如果这种比对由人来亲自做,光是打开两个文档对齐 refs、判定方向就要花好几分钟。当文档增加到数百个,实际上就不可能了。反向引用总是一两个一两个地悄悄混进来,过了很久才在版本里爆掉。


2.3.6 Layer 分解 = 程序化生成·自动化的前提

Layer 整合的表面目的是消解孤岛、统一协作语言(2.3.1\~2.3.5)。本质目的还要再深一层。当 Layer 分解扎下根来,程序化生成·自动化的前提条件就齐备了。

前两节的运营案例,处于人来决定、AI 协助验证·注入的阶段。再下一步,就进入到分工本身的量产由 AI 生成候选、由人采纳的阶段。这一步的前提之所以是 Layer 分解,有三个理由。① AI 生成候选,必须能明确"要生成哪个 Layer 的什么"。② 自动一致性检查,要在 Layer 间依赖方向标准化之后才能运作(2.3.5 的反向引用检出)。③ 变更影响的自动计算,要有"变更发生在哪个 Layer"的坐标才有可能。三者都汇聚到"没有 Layer 分解,自动化本身就被堵死"。把坐标分开的那双手的尽头,从一开始就摆着程序化生成。

在还没看到各分工部分的阶段无需深入,所以只把应用的两个阶段勾出轮廓。保守应用是人来决定,AI 自动协助一致性检查·变更影响计算·JIT 注入 —— 2.3.4·2.3.5 的运营案例就在这里。工具成本小,累积效果到运营第 6 个月左右才显现,大多数中等规模(10\~50 人)团队都能达到。进取应用则更进一步,分工的量产本身由 AI 生成候选(叙事 Persona、PCG 规则手册、程序化关卡、数值变更候选、美术资产等),人只决定"采纳哪个候选"。各分工的具体形态和工具成熟度,在相应分工的部分里展开。

进取应用各分工共通需要的 3 要素是:① Layer 分离·标注基础设施(frontmatter·atom·文件名 prefix),② 候选生成·评估循环(AI 候选 N 个 → 自动评估 → 排名·依据报告),③ 人工评审关卡(只有被采纳的结果才进入下一个 Layer)。不过,在任何时点,确定性核心(模拟·物理·法律约束)都由人·确定性代码负责,所有评审都在进入不可逆阶段(录音·选角·上线曝光等)之前的可逆阶段就了结 —— 这条可逆/不可逆的边界是各分工共通的原则。

最后说一个时点。保守应用在 2010 年代也部分可行,但进取应用被三个限制卡着:AI 候选生成的表现力、自动评估的自然语言解读、人工评审负担。LLM 发展之后,三者都进入实用领域,进取应用从纸面上的愿景下降到了实务阶段。AI 的发展抬高了程序化生成·自动化的可实现性 —— 贯穿本书全书的这条元信息,就在这里。


2.3.7 分工坐标 —— 本书各分工部分所住的地方

本书的各分工部分,会在导入处明确各分工主要分布在哪个 Layer,在章节内部也频繁使用 Layer 坐标。先整理在此(这是把 2.3.3 矩阵的重心搬成了表格)。

分工 主 Layer 备注
系统策划 L1\~L3 从设计骨架到配置表,涵盖面广
战斗策划 L1\~L3,L4 一部分 连招骨架\~伤害表,版本测量
叙事策划 L0\~L4 用文件夹运营 5 层结构
内容策划 以 L2 为中心 进度流程·任务线
关卡设计 L2\~L3 含程序化生成管线
数值策划 以 L3 为中心,L4 测量 数据值·曲线·验证测量
UX/UI 设计 L1\~L3 交互骨架\~画面数据
QA 设计 以 L4 为中心,L0\~L3 验证 验证所有 Layer 是否都反映进版本
角色·宠物·坐骑 L1\~L3 系统·世界·数据
美术指导 L0\~L1 + L4 产出物 愿景·世界指南 + 版本评审
运营 L2\~L4 运营循环·实时数据

每个分工也会触及别的 Layer,但只要知道重心,协作通道就看得见。数值(L3)和运营(L2\~L4)在 L3 相遇,所以始终要紧密协作;离愿景(L0)最近的两个分工是叙事和美术指导。这些相邻关系在坐标系上自然地显现出来。


2.3.8 从小处开始,往大处培养

要想一开始就完美地引入 Layer 系统,就连开始都做不到。渐进地引入才是正解。

flowchart LR S1["第1阶段<br/>单一分工引入<br/>(推荐:叙事)"] --> S2["第2阶段<br/>相邻分工扩展<br/>(叙事+内容)"] S2 --> S3["第3阶段<br/>全分工标准化<br/>(文件名 prefix·一致性检查)"] S3 --> S4["第4阶段<br/>AI 工具整合<br/>(JIT·变更影响·复盘分类)"] S1 -.->|"小规模(~10人)团队"| E1["到这里就够"] S3 -.->|"中规模(10~50人)团队"| E2["建议到这里"] S4 -.->|"大规模(100+)团队"| E3["要走到这里才有效"] classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class S1,S2,S3,S4 human class E1,E2,E3 pass

每个阶段至少一个月,长则以季度为单位。硬推,人就会累。把运营负担控制在不超过引入价值的范围内、调节速度,这是总监的工作。

小规模(\~10 人)走 1\~2 阶段,中规模(10\~50 人)走第 3 阶段,大规模(100+)要走到第 4 阶段才会出效果。这并不是说小团队用不了。只是深度不同,核心价值在第 1 阶段就已经开始了。


2.3.9 结论 —— 全书的脊柱

Layer 不是单纯的文件夹整理技巧。它是把分化的游戏策划捆进一个坐标系、让 AI 能够推理的元原则,进而是各分工程序化生成·自动化的共通前提条件。

本书其余所有部分都以本章为前提。各分工部分会在导入处明确各分工在 Layer 中占据的坐标,流程部分讲横跨 Layer 的运营系统,运营部分讲 Layer 系统本身的 self-improving 循环。

下一章(游戏本体论与知识图谱)在 Layer 之上叠加语义关系。如果说 Layer 是坐标,那么本体论就是坐标之上的语义箭头。两者合在一起,AI 才终于能自主推理"这个文档会影响那个文档"。

有一点要说清楚。本章的任何自动化都没有代替决定。在反向引用检出中,机器只是把违规候选铺开,选择接受什么、接受到哪一步的,是人的手。Layer 是帮助人更快做出更好决定的坐标系,不是把决定甩出去的装置。


本章要点

本章核心 atom(参考)

下一章预告


动手试试

setup —— 挑一个分工(推荐:叙事)的文件夹,把下级文件夹从 Layer0_Vision/Layer4_BuildVO/ 分成 5 个。把现有文件移动到对应的 Layer。名字含糊的文件,以"这个文档变化的频率"为标准来安放(变得越勤就放越下面的 Layer)。

prompt —— 挑两个配置表的 frontmatter,把 2.3.5 的提示词全文原样粘进去,让它判定 Layer 依赖方向。只要准确给出一条核心规则就行。"引用只能从具体→抽象(高编号→低编号)流动才算正常。"

verify —— AI 抓出反方向引用时,不要原样接受它的建议,要亲自削掉适当尺度(2.3.5 的"人工验证/否决")。只把采纳的方向作为 diff 再次请求。按名称排序时,如果 Layer 看上去从上到下聚在一起,就说明 prefix 规则站稳了。

单人精简版

一个人作业,Layer 也照样起作用。没有团队,就没有"分工之间的孤岛",但有"时点之间的孤岛"。三周前的我和今天的我会忘记彼此的决定。只要把文件夹仅按 Layer 0\~4 分开 —— 愿景一张,系统骨架几张,进度流程,配置表,版本备忘 —— 就能立刻找到过去的我把什么放在了哪一格。只要对 AI 加一行"现在在做 L2 工作",它就不会拽来无关 Layer 的资料。减到 4 层·3 层也行。核心不是数字,而是"明确标出层级"这一行为。

2.4 本体与 wikilink 图谱 —— 验证语义箭头

周一上午,一条变更请求被提了上来。战斗团队的成员 A 在团队即时通讯工具里写下一行字:"我想把全局冷却从 0.5 秒改成 0.3 秒。有谁受影响吗?"换作平时,从这里开始就是一场半小时的会议。负责伤害计算公式的人举手,负责连招取消规则的人插话,还有人问"Boss 的招式会不会也受影响"。没有人把整张图都装在脑子里,于是会议被翻找记忆这件事填满。

可这一次不一样。请求提上来 1 秒后,机器人自动加了一条评论:"修改这个 atom 会影响 4 个 atom。skill_dps_calculationcombat_combo_cancel_v3refgame_boss_pattern_phase2balance_curve_v3。负责人:成员 B、成员 A、成员 C。"会议没有召开。4 个人各自只确认了自己的 atom 就结束了。(这个机器人我们会在后面亲手做出来 —— 见 2.4.3。)

这条评论不是魔法。在 2.3 里我们给所有 atom 赋予了 Layer 坐标,在此之上,本章又加上了语义箭头 —— 哪个决策影响哪个决策。坐标只说到"这里有什么"。"这个影响那个""那个必须先存在才成立""这两个不能同时开启"之类的关系,是画在坐标之上的箭头。本章讲的就是如何标注这些箭头,以及如何自动揪出断掉的箭头。

术语备注 - 本体(ontology):把概念及其之间的关系明确定义出来的体系。本书采用简化为 6\~12 种关系的轻量版本。 - wikilink:[[atom_name]] 形式的文档间链接。借用了 Obsidian、Roam 等工具中使用的写法。 - 反向引用(backlink):"指向这个 atom 的那些 atom"的列表。正向引用的反方向。 - 孤立节点(orphan):任何地方都没有引用的 atom。废弃候选的信号。 - 断链(broken link):指向不存在的 atom 的 wikilink。错别字、改名留下的痕迹。


2.4.1 关系就是箭头 —— 仅凭 wikilink 为何不够

在 2.1 里,我们用 YAML 前言(frontmatter)附上了元数据,又在 atom 正文里撒下了 wikilink。仅凭这些,文档就已经像网一样连接起来了。问题在于,这些连接没有写明它意味着什么

这个决策成立于 [[skill_cooldown_rule_v2]] 之上。

这一行只说到"提及了 skill_cooldown_rule_v2"。为什么提及?是这个决策需要那条规则(requires),是从那条规则派生而来(derives_from),还是与那条规则冲突(conflicts_with)?人读句子就明白,机器却不知道。就算问 AI"开启这个决策会不会有什么东西被破坏",仅凭没有语义的链接也答不出来。

所以要给 wikilink 套上关系类型。游戏策划中实际用到的关系,出乎意料地少。下面这六种就覆盖了 90% 以上。

affects 施加影响

derives_from 派生自~

requires 须先存在

conflicts_with 不可同时启用

is_a 特例

part_of ~的一部分

例:combat_combo_cancel_v3 —[affects]→ skill_dps_calculation combat_combo_cancel_v3 —[derives_from]→ vision_taste_focused_combat combat_combo_cancel_v3 —[requires]→ combat_input_buffer_system

把这六种用 enum 固定下来的 atom 就是 ontology_relation_enum_v1。要新增关系类型,必须经过变更请求评审。即便增加,10\~12 个也是适当的上限,起步时只用 affects、derives_from、requires 三个也足够。写关系的位置是 atom 的 YAML 前言。

---
name: combat_combo_cancel_v3
layer: 1
affects: [skill_dps_calculation, refgame_boss_pattern_phase2]
derives_from: [vision_taste_focused_combat]
requires: [combat_input_buffer_system, skill_cooldown_rule_v2]
conflicts_with: [skill_cancel_rule_legacy_v1]
---

人只写正向的那一行。反向("谁在 affects 我")由工具扫描全部来计算。如果在两边都写 affects,同步负担就会翻倍,而一旦只改一边、忘了另一边,图谱就开始说谎。一个方向靠人手,反方向靠工具,这是贯穿本章始终的原则。


2.4.2 实操记录(worked transcript) —— 一次性验证 wikilink 反向引用与图谱完整性

从这里开始,我把实际发生的一次会话整段照搬过来。"实操记录"是本书的专有词,指完整保留的真实操作过程记录。情形是这样的:把连招取消规则从 v2 推翻重做为 v3 时,决定废弃 skill_cancel_rule_legacy_v1。但在废弃之前,必须确认有没有别的 atom 指向这个 atom(反向引用),以及废弃之后会不会留下断链或冲突(图谱完整性)。靠手动 grep 会漏掉。交给 Claude。

提示词(全文)

我附上的 atom 文件夹(decisions/)里有 atom,前言里应该写着关系
(affects/derives_from/requires/conflicts_with/is_a/part_of)。
正文里也有 wikilink。现在我想废弃 skill_cancel_rule_legacy_v1,
废弃前请帮我检查 3 项:1) 反向引用 2) 断链 3) 冲突。
不要猜测,实际读取文件再回答。找不到就说找不到。
// 反向引用=YAML 关系+正文 wikilink 两边 / 断链=因废弃而 stale、变为不存在的链接
// 冲突=conflicts_with 对象中是否还有 status: approved 存活

Claude 的输出(原始 —— 不做摘要、不做整理)

我读取了 decisions/ 文件夹中的 47 个文件。以下是针对 skill_cancel_rule_legacy_v1 的 3 项检查结果。

1. 反向引用(指向这个 atom 的 atom)

通过 YAML 关系指向的 atom: - combat_combo_cancel_v3 —— conflicts_with: [skill_cancel_rule_legacy_v1] - skill_cancel_rule_v2 —— derives_from: [skill_cancel_rule_legacy_v1]

通过正文 wikilink 指向的 atom: - combat_combo_cancel_v3 正文:"解决了旧版 [[skill_cancel_rule_legacy_v1]] 的输入缓冲问题" - qa_combat_regression_checklist 正文:"回归项第 3 条是按 [[skill_cancel_rule_legacy_v1]] 编写的"

也就是说有 4 个 atom 引用这个 atom。(YAML 2 个 + 正文 wikilink 2 个,但 combat_combo_cancel_v3 两边都有,所以实际 atom 数为 3 个)

2. 断链(废弃时会断掉的地方)

仅靠废弃本身,wikilink 并不会"断掉" —— 文件仍然保留,只是 status 变成 deprecated。不过: - qa_combat_regression_checklist 是以这个被废弃的 atom 为基准定义回归项的,因此内容上失效。这不算断链,但属于 stale link。需要复查。 - 如果你连文件都删除,那么上述 4 处引用就全部变成断链。

⚠️ 有一处含糊的地方:balance_curve_v3 正文里有一个 [[skill_cancel_rule_legacy]](末尾没有 _v1)的链接。它是错别字、本想指向 _v1,还是有意指向另一个 atom,仅凭文件无法确定。名为 skill_cancel_rule_legacy 的文件在文件夹里不存在。也就是说,这很可能已经是一条断链。需要确认。

3. 冲突残留(conflicts_with 对象是否存活)

skill_cancel_rule_legacy_v1conflicts_with 绑定的 atom:只有 combat_combo_cancel_v3 一个。 - combat_combo_cancel_v3 的 status:approved(存活)

→ 与废弃对象冲突的新规则处于正常批准状态。这是正常的。这是废弃旧版、新版存活下来的自然替换。不是冲突残留警告。


总结:废弃是安全的,但请先处理两件事。 (a) qa_combat_regression_checklist 的 stale link —— 更新回归项第 3 条。 (b) balance_curve_v3[[skill_cancel_rule_legacy]] —— 疑似错别字。可能已经是断链。

人的验证与否决

在这里,我没有照单全收这份输出。(a)是对的 —— 回归检查清单得由我来更新。但我在(b)处停了下来。Claude 说 balance_curve_v3[[skill_cancel_rule_legacy]] "疑似错别字",可它究竟是不是断链,我得亲自去看文件夹。AI 说"无法确定",这是诚实的信号,不是结论。

打开文件夹一看,名为 skill_cancel_rule_legacy 的文件确实不存在。可查看 git log 才发现,6 个月前有一次把 skill_cancel_rule_legacy 只改了名skill_cancel_rule_legacy_v1 的提交。那时 balance_curve_v3 的正文链接没能更新就被放过去了。也就是说,这不是错别字,而是改名后变成孤儿的断链。Claude 的"疑似错别字"这一猜测只对了一半。原因不是错别字,而是 rename 遗漏。这点区别如果我没抓住,就会只改回归检查清单,断链则被原样埋掉。

重新请求(反映已确认的事实)

确认过了。[[skill_cancel_rule_legacy]] 不是错别字,而是 6 个月前
rename(_legacy → _legacy_v1)时没能改正文链接而留下的断链。

那就趁这个机会,把整个文件夹里"指向不存在的 atom 的 wikilink"
全部找出来。以正文 wikilink 为准,凡是目标文件不在文件夹里的,全部列出。
用表格列出文件名和其中的断链。

我收集了 47 个文件中所有 [[...]] 正文 wikilink,并与文件夹的文件清单做了比对。目标文件不存在的断链共 3 条

含链接的文件 断掉的 wikilink 推测原因
balance_curve_v3 [[skill_cancel_rule_legacy]] rename 遗漏(→ _v1)
quest_reward_table_v2 [[concept_reward_currency]] 目标 atom 未编写(只有计划,没有文件)
refgame_boss_pattern_phase2 [[skill_global_cooldown]] rename 遗漏(→ skill_global_cooldown_rule)

3 条中有 2 条(rename 遗漏)只要把目标 atom 名改成 _v1 / _rule 即可。1 条(concept_reward_currency)由于所指向的 atom 本身不存在,需要你去编写,或删除该链接。我没有自动修改 —— 请在确认你想要哪一种后再进行。

三条全都属实。如果靠手动 grep,quest_reward_table_v2 的断链几乎肯定会被漏掉。那条链接是"预先指向一个尚未创建的 atom"的、有意为之的未来引用,可 6 个月里没人去创建那个 atom,它实际上已经成了一句作废的承诺。

这次会话所展现的事很简单。反向引用检出与断链检出,AI 擅长读取并比对整个文件夹;而原因判定与意图确认,则由人来做。 AI 做到"这里断了"为止,人则做到"为什么断、怎么修"为止。


2.4.3 画成图谱才看得见的东西 —— 把验证搬到视觉上

前一节的检查也可以每次都用提示词来跑,但把同样的检查用代码固化下来,就能在图谱上一目了然。项目A 里有一个由 2.3 中介绍的 gen_relation_map.py 扩展而来的图谱工具在做 R&D。核心是读取文件夹里的 atom,用 networkx 构建有向图之后,再叠上四个检查函数。

import networkx as nx

# build_graph(folder): 读取 atom 文件夹,以节点(=atom)和
#   YAML 关系边构建 DiGraph。(全文见「动手试试」)

def find_cycles(G):                      # 循环依赖
    return list(nx.simple_cycles(G))

def find_orphans(G):                     # 入度为 0 = 孤立候选
    return [n for n in G.nodes if G.in_degree(n) == 0]

核心就是两行。simple_cycles 抓出循环依赖(A requires B requires C requires A),in_degree(n) == 0 抓出孤立节点 —— 不需要亲手写 DFS。其余两个函数也是同等分量的一行式。find_broken_wikilinks 用正则收集正文 [[...]],挑出不在节点清单里的;反向引用则把图谱倒着遍历一遍就出来(全文见「动手试试」)。可视化时把节点颜色按 Layer 涂,边的颜色按关系类型涂,被引用很多的节点(入边多的节点)画大一些,让枢纽显露出来。就像在文件柜上贴好彩色标签、整理过的文件夹那样,模式会先在视野中浮现。

下面是把 2.4.2 会话中涉及的那些 atom 的实际关系搬过来画成的图谱。箭头方向意味着"起点 atom 朝着终点 atom 建立关系"。

graph LR combo[combat_combo_cancel_v3<br/>L1·approved] legacy[skill_cancel_rule_legacy_v1<br/>L1·deprecated] v2[skill_cancel_rule_v2<br/>L1] dps[skill_dps_calculation<br/>L3] vision[vision_taste_focused_combat<br/>L0] buffer[combat_input_buffer_system<br/>L1] boss[refgame_boss_pattern_phase2<br/>L2] qa[qa_combat_regression_checklist<br/>L4] broken[skill_cancel_rule_legacy<br/>不存在] combo -->|affects| dps combo -->|affects| boss combo -->|derives_from| vision combo -->|requires| buffer combo -->|conflicts_with| legacy v2 -->|derives_from| legacy qa -.stale.-> legacy balance[balance_curve_v3] -.broken.-> broken classDef dep fill:#eee,stroke:#999,stroke-dasharray:4 classDef miss fill:#fff,stroke:#cc2222,stroke-dasharray:4 class legacy dep class broken miss

用虚线画出的两条边,正是 2.4.2 里靠人工验证抓到的问题。qa → legacy 是以废弃 atom 为基准的 stale link,balance_curve_v3 → skill_cancel_rule_legacy 是指向不存在节点的断链。画成图谱后,这两条虚线就在实线之间凸显出来。要是只用文本来运营,它们就会埋没在 47 个文件的某处,永远看不见。

在验证关卡(verification gate,由人或检查器把关验证的环节,类似质量门禁 quality gate)(Layer 4)上自动运行的规则有四条。

这四条规则一旦用代码固化,就不必像 2.4.2 那样每次都编写提示词。变更请求一提上来,机器人就重新构建图谱,把受影响的 atom 清单与断链、循环、冲突自动写成评论。章首那条"有 4 个 atom 受影响"的评论,正是这个 —— 这个机器人就是前面预告过的那个机器人。


2.4.4 为什么是 Layer —— 为了程序化生成而划分的坐标

这里得说清楚 2.3 与 2.4 为什么是一个整体。Layer 坐标与关系箭头不是分头引入的,而是同一目的的两个面。

表面上,Layer 统一了协作语言 —— 叫它"这是 L1 系统决策""那是 L3 数据",哪怕领域不同也共享同一套坐标。但本质目的在别处。Layer 是为了程序化生成而划分的坐标。

L0 愿景是上下文锚点 —— 它不变,且每次都注入给 AI。L1 系统是生成的输入规则 —— 规则手册、关系、标签都住在这里。L2 内容是生成出来的正文堆积的地方,L3 数据以数值、ID、关系充当模拟的输入,L4 构建·QA 是验证关卡。关系箭头在这套坐标之上作为生成的约束条件运转。AI 生成新内容时,requires 箭头成为"这个必须先存在"的前提,conflicts_with 箭头成为"这个不能一起开启"的禁令。

领域会分化(战斗、任务、经济各自拥有专长),但所有产出物都带有 Layer 坐标,因而彼此认知。分化与整合在同一套坐标系上同时成立。等这张图谱长得足够大,AI 在生成候选时就会把关系箭头当作自动约束来读,人只需在评审关卡上确认有没有违反即可。2.4.2 的实操记录就是它的缩小版 —— AI 读取图谱、找出约束违反(断链、冲突),人在关卡上判定。


2.4.5 领域概念也是节点 —— 一部小型词汇词典

成为关系箭头起点、终点的节点,不只有决策 atom。把"技能""任务""奖励"这类领域概念定义出来的 atom,同样是图谱的一等公民。concept_skill_definition_v1 长这样。

# 技能 (Skill)
Definition: 角色在战斗中发动的单位动作。包含输入、冷却时间、资源、效果。
Required Properties: input / cooldown / cost / effects
Subtypes: active_skill (is_a) / passive_skill (is_a) / ultimate_skill (is_a)
Not a skill: 自动攻击 [[concept_auto_attack]] / 变身 [[concept_transformation]]

只要写上 boss_skill is_a skill,上位概念的规则就会自动继承。项目A 里这类概念定义 atom 大约有 19 个,所有决策 atom 都引用它们。词汇一旦统一为一本词典,会议、文档、代码之间的翻译负担就消失了。等于是不在每张桌子上各放一本不同的词典,而是共享同一本词典。前面实操记录中被当作断链抓到的 concept_reward_currency,正是"约好要收进词典、却还没创建的空条目"。


2.4.6 保持轻量 —— 学术本体的陷阱

读到这里,会有人问"这不就是 OWL/RDF 那样的正式本体吗"。不是。而且我是故意让它不是的。

学术本体(OWL、RDF、SKOS)很强大,但关系类型有几十到几百种,需要专用推理引擎,要运营就得有本体专家驻场。在搜索引擎、医疗、法律这类精密推理生死攸关的领域,它是必需的。可游戏策划中实际需要的推理只有"变更影响范围""先决依赖""冲突检出"三种,而且全都用沿图谱逐格遍历的简单搜索(BFS、DFS)就够了。前一节用 networkx 一行抓出循环就是证据。正式推理引擎是过剩配置。

标准只有一条。策划能否靠手动运营。 YAML 6 个 enum、networkx 处理、用 pyvis 或 D3.js 可视化。一旦越过这条线,工具就不再是工具,而成了又一份负担。轻量不是妥协,而是设计意图。

要避开这个陷阱,有五个反复出现的错误。它们都从"把本体当成强制标准来对待"这同一个根上长出来。

错误 规避法
一开始就定义太多关系类型 从三个(affects、derives_from、requires)起步,只在需要时再加
强制每个决策都带关系 没有关系的 atom 也认作正常 —— 空关系会弄脏图谱
把 affects 写成双向 只手写一个方向,反方向靠工具自动计算
执着于 OWL、RDF 停留在可运营的水平(YAML + enum)
没有可视化、只用文本运营 哪怕是简单的 HTML 视图也从一开始就提供 —— 就像 2.4.3 的虚线,看不见就不会去修

第 1、2、3 个在引入后 1 个月内就能摸到规律,第 4、5 个在第 3 个月的复盘中排查,自然就会对齐。


2.4.7 小处起步与下一章

头一个月是亏本的。只是关系书写的负担在累积,看得见的效果却没有。所以要从小处起步。第一周只用 affects、derives_from、requires 三种关系,第 2\~4 周对 20 个核心 atom 应用,看着图谱长起来。在第 1 个月做一个前一节那种水平的 HTML 图谱视图,从第二个月起可视化就开始体现价值,到第三个月,自动验证(循环、冲突、孤立、断链)就直接缩短了会议时间。能否熬过这亏本的一个月,是引入成败的分水岭。

下一个第 8 章 Wikilink 会从运营层面深挖本章用箭头处理过的 [[...]] 写法 —— 反向引用面板每天怎么用,改名时如何一次性更新链接(从源头杜绝 2.4.2 的 rename 遗漏),如何把 Obsidian 这类工具的图谱视图融入实务。YAML(第 4 章)→ Atom(第 5 章)→ Layer(第 6 章)→ Ontology(第 7 章)→ Wikilink(第 8 章)就是信息架构完整的五边形。


动手试试

setup. 定下一个汇集 atom 的文件夹(例如 decisions/),为每个 atom 的 YAML 做好写关系键的准备。一开始只用 affectsderives_fromrequires 三个。正文链接统一为 [[atom_name]] 形式。

prompt. 在废弃、改名之前抛出下面这段。

读取这个文件夹里的 Markdown atom,我想废弃/变更 [目标_atom],请检查:
(1) 反向引用:用 YAML 关系和正文 wikilink 指向这个 atom 的全部 atom
(2) 断链:变更/删除时会断掉或变 stale 的链接
(3) 冲突残留:conflicts_with 对象中 status: approved 的那些
不要猜测,实际读取文件,找不到就说找不到。

verify. AI 标注"疑似错别字""无法确定"的地方,要由人亲自打开。断链的原因(是错别字、rename 遗漏还是未编写)由人借助 git log 和文件夹实物来判定。不要让它自动修改,确认意图后亲手修。

图谱工具全文(2.4.3 节选)

正文(2.4.3)中只展示了核心的两个函数。把整个文件夹构建成图谱并挂上四项检查的全文如下。

import networkx as nx
import re, yaml, glob, os

REL_TYPES = ["affects", "derives_from", "requires",
             "conflicts_with", "is_a", "part_of"]
WIKILINK = re.compile(r"\[\[([a-zA-Z0-9_]+)\]\]")

def build_graph(folder):
    G = nx.DiGraph()
    files = {}
    for path in glob.glob(os.path.join(folder, "*.md")):
        name = os.path.splitext(os.path.basename(path))[0]
        text = open(path, encoding="utf-8").read()
        fm = yaml.safe_load(text.split("---")[1]) or {}
        files[name] = fm
        G.add_node(name, layer=fm.get("layer"), status=fm.get("status"))
    # YAML 关系边
    for name, fm in files.items():
        for rel in REL_TYPES:
            for tgt in (fm.get(rel) or []):
                G.add_edge(name, tgt, type=rel)
    return G, files

def find_broken_wikilinks(folder, known_nodes):
    broken = []
    for path in glob.glob(os.path.join(folder, "*.md")):
        text = open(path, encoding="utf-8").read()
        for m in WIKILINK.findall(text):
            if m not in known_nodes:
                broken.append((os.path.basename(path), m))
    return broken

def find_orphans(G):
    # 入度为 0 且没有 part_of/is_a 父节点的节点
    return [n for n in G.nodes if G.in_degree(n) == 0]

def find_cycles(G):
    return list(nx.simple_cycles(G))

单人精简版

没有工具也行。一个 atom 文件夹、一个 Claude 就够了。写新决策时只在 YAML 里加 requiresaffects 两行,每当发生废弃、改名时就把上面的 prompt 跑一遍。图谱可视化是以后的事 —— 在变更前把断链和冲突过一遍的习惯,这一次,就挡住了单人运营中最大的亏损。


本章要点

3.1 系统策划的日常与 Layer 坐标

周四下午 4 点 50 分。数值策划填好的技能表刚刚上传。技能 312 个。每个技能都要在 effect_id 这一栏里填上效果编号,而那个编号指向另一张效果表里的某一行。两者对上了,游戏才能跑起来。对不上,客户端就会调用一个空效果,或者悄无声息地崩溃。

从前的我是用手一行行核对的。技能表点一格,跳到效果表,确认编号,再跳回来。312 遍。再快也要两个小时。在眼睛发花的最后 50 个里,总会漏掉一两个,而正是这一两个,在 QA 版本里炸了。

本章要讲的,是那两个小时去了哪里,以及那道一致性检查在系统策划的工作地图上究竟落在哪个坐标。坐标不先定好,你就只能凭感觉去判断该把 AI 嵌在哪里,而且永远只能凭感觉。


3.1.1 系统策划要做四样东西

系统策划是在抽象度与具体度之间游走范围最广的人。他接过名为愿景的那团雾,一直把它拽到数据表最后一格那个坚硬的数字为止。在这段旅程中产出的成果,共有四类。

(1) 把愿景翻译成结构。 当总监说"打击感十足的动作战斗"时,系统策划就把它转化成技能、连招、取消、顿帧这套骨架。"成长的自主决定权"就变成职业、技能树、装备系统。这是雾凝结成构筑物的第一瞬间。

(2) 为系统之间的接口编写规格。 战斗、移动、背包、商店、任务、公会同时在跑。战斗中打开背包会不会附加无敌?强化途中收到 PvP 申请怎么办?这些情形的答案汇聚起来,造就了所谓"做得好"的那种手感。每一处缺了答案的地方,玩家都会感到烦躁。

(3) 对数据表及其结构(schema)负责。 312 个技能的系数,数百件道具的效果,数十种怪物的行为。数值要么自己填,要么交给数值策划或内容策划。但表的列定义(结构,schema)这一块,得攥在系统策划手里。这是替别人做好一只只贴了标签的抽屉的活儿。抽屉做得潦草,每个人填法就各不相同,一致性也就破了。

(4) 设计行为逻辑。 角色、怪物的 AI 会以状态机(FSM,Finite State Machine,有限状态机)、行为树(Behavior Tree,以下简称 BT)、决策表、程序化规则等形态产出。这些资料交到程序员手中,变成代码。

四样东西全都汇到同一个人的桌上,这一点是关键。所以"今天把时间花在什么上"就成了系统策划最大的运营决策。


3.1.2 系统产出有它的 Layer 坐标

在 2.3 里,我们把整个游戏制作物从 L0(愿景)到 L4(版本)放上了一根坐标轴。现在,就把 3.1.1 的四样产出原样标在那根轴上。像系统策划这样,一个领域的产出物横跨多个 Layer、铺得这么开的情形,是很罕见的。

下面这张图,把产出物落在 Layer 上的哪个位置、又在每个坐标上与谁相遇,都画在了一张纸上。

L0 L1 L2 L3 L4

愿景 —— 系统策划只负责接收 系统骨架:职业、战斗、背包、公会的定义 接口:系统间交互规则、优先级 结构 + 数据:表的列定义,部分数值 版本 —— QA 验证意图是否落地

↔ 总监、叙事 ↔ 美术指导 ↔ 其他系统策划 ↔ 数值、内容 ↔ QA

系统策划亲手制作的区段(L1→L3)

这张图说明的有两点。第一,系统策划负责的是从 L0接过来、一直送达到 L4 的这段长路。第二,亲手制作的区段是 L1\~L3,而这三格里每一格协作对象都不一样。每换一格,协作语言就变,所以不把坐标放在心上,会议就会一次次空转。

不过这并不是说一个人要把 L1\~L3 全都包了。团队大,L1\~L2 负责人和 L3 负责人就会分开;团队小,一个人全看。坐标是分工的地图,不是把活儿全推给一个人的命令。


3.1.3 坐标定下来,就能看清把 AI 嵌在哪里

图画好了,现在该上色了。哪个坐标引入 AI 的效果大?不是闷头"全部自动化",而是看坐标的性质来挑。

flowchart TD L1["L1 系统骨架<br/>(职业数量、战斗模型)"] L2["L2 接口<br/>(交互规则)"] L3["L3 结构 + 数据<br/>(312 行的表)"] L1 -->|"直接关乎游戏特质<br/>由人决定"| H1["AI = 辅助人做决策的变形/验证"] L2 -->|"情形爆炸<br/>影响范围大"| H2["AI = 自动提取变更的影响范围"] L3 -->|"定式、重复<br/>一致性检查"| H3["AI = 专责生成、验证、转换"] H1 --> R["人做核心决策,<br/>AI 管细节、一致性、重复"] H2 --> R H3 --> R style L3 fill:#fff8e1,stroke:#f9a825 style H3 fill:#e8f5e9,stroke:#2e7d32 style R fill:#e3f2fd,stroke:#1565c0

关键在于坐标越往下走,AI 专责的比重越大。L1 的"职业要做几个"关乎游戏特质,得攥在人手里。反过来,L3 的"312 行外键是不是都对得上"是定式、重复,该整个交给 AI。L2 在中间——决策由人来做,但"改了这条规则会牵连到哪里"这样的影响范围提取,由 AI 来托着。

这张图解释了为什么 3.1.4 之后的所有实操都从 L3 附近起步:因为那是效果最大、风险最小的位置。结构工具就算出故障也不会闯祸,关系图只是画画图而已,一致性检查则可以由人否决。


3.1.4 实操记录(worked transcript):在坐标 L3 上把一致性检查交给 AI

理论到此为止。现在回到 3.1 开头的那个周四。技能表里 312 个 effect_id 是否与效果表对得上,让 AI 来试一试。不做删减,把实际往来原样照搬。(实操记录,worked transcript:完整保留的真实操作过程记录。)

数据录入遵循 schema-first 原则。也就是说,人先定义说明书表,从那里产出 Enum 和 .proto(由 VBA(Excel 宏语言) Export),最后才填入 csv 数据。所以验证提示词也以"先读结构"开头。要钉死一点:正本不是结构文档,而是实际 export 出来的 JSON(json_over_schema_doc_as_source_of_truth)。因为文档会撒谎,export 出来的 JSON 撒不了谎。

提示词(全文)

你是我们项目的数据一致性检查器。 输入是两个 export JSON。 1) skill.json —— 每个技能对象都有 skill_idnameeffect_id 字段。 2) effect.json —— 每个效果对象都有 effect_ideffect_type

请检查以下各项。 (A) skill.json 里所有 effect_id 是否都存在于 effect.json 中(外键一致性)。 (B) 存在于 effect.json、却没有任何技能引用的孤立(orphan)效果。 (C) skill.jsoneffect_id 为空或为 0 的行。

规则:不要猜测。JSON 中没有的字段,就报告"字段缺失"。 输出分为 (A)(B)(C) 三个小节,每条违规用一行写出 skill_id(或 effect_id)和原因。 若没有违规,就在对应小节里只写"无违规"。

Claude 的原始输出(未加工、未美化)

没有收到两个输入 JSON。当前消息中没有附带文件内容。 请用以下任意一种方式重新发来。 - skill.jsoneffect.json 的实际内容(粘贴或附件) - 或者两个文件的路径

收到内容后,我会按 (A)(B)(C) 的顺序检查。没有文件,我不会编造结果。

在这里停下,反倒是值得信赖的信号。如果输入是空的,却回来一份像模像样的报告,那份干净利落本身才是危险信号。不凭空造出检查结果,而是反问"先把文件给我"的那一方,才靠得住。我把两个 JSON 实际粘了进去,重新发起请求(因篇幅所限,正文中只摘录一部分)。

// skill.json (摘录)
[
  {"skill_id": 1001, "name": "强击",     "effect_id": 5001},
  {"skill_id": 1002, "name": "回旋斩", "effect_id": 5002},
  {"skill_id": 1003, "name": "举盾","effect_id": 0},
  {"skill_id": 1004, "name": "涂毒", "effect_id": 5099}
]
// effect.json (摘录)
[
  {"effect_id": 5001, "effect_type": "DAMAGE"},
  {"effect_id": 5002, "effect_type": "DAMAGE"},
  {"effect_id": 5003, "effect_type": "BUFF"}
]

Claude 的第二次输出(基于实际数据)

(A) 外键一致性 —— 违规 1 件 - skill_id: 1004effect_id: 5099effect.json 中不存在(dangling reference)

(B) 孤立效果 —— 违规 1 件 - effect_id: 5003 (BUFF) → 没有任何技能引用

(C) 空/0 effect_id —— 违规 1 件 - skill_id: 1003effect_id: 0。可能是有意为之的"无效果",请在结构中确认 0 是否为有效值。

人的验证与否决

三件里有两件是明摆着的 bug。5099 是数值策划还没在效果表里加上那一行,就先把它填进技能里了——典型的工序错位的情形。抓到了。

而 (C) 我否决了。在我们项目里,effect_id: 0 表示"被动槽位,无效果",是个有效的约定值。AI 不知道这一点,于是把它当作"请确认"提了上来——这个判断没错,但不该算成违规。于是我转向下一步——把这个约定固化成 atom,让下一次检查起,AI 就把 0 认作有效值。

再次请求(注入领域知识)

追加规则:effect_id == 0 表示"被动,无效果",是我们项目的有效约定值。 因此在 (C) 中,effect_id == 0 不算违规。只把空字符串、null、负数作为 (C) 违规报告。

(C) 空/0 effect_id —— 无违规 (effect_id: 0 按有效约定值处理。无空值/null/负数。)

这一行约定,正是领域知识的累积。系统策划的决策相对长久,一旦固化下来,一年后同样的检查也会按同样的标准来跑。柜子里又叠上了一张决策卡。

在这段记录里,人做的事只有三件——(1) 指定"先读结构"的输入顺序,(2) 确认 5099 是真 bug,(3) 知道 0 是有效值,从而否决并纠正 AI 的判断。其余 312 行那种一格一格跳着看的两个小时,消失了。被自动化掉的是"跳转加比对"这种劳动,而剩下的那三行判断,才是核心。


3.1.5 不断累积的资产:把坐标固化为代码

上面那段记录里的检查,固然可以每次都用手去让它跑,但 L3 上反复出现的活儿,用工具固化下来才是系统策划的正道。这里引用笔者在用的两样东西——不是抽象的"项目A的工具",而是真真切切在桌上跑着的东西。

gen_relation_map.py 会分析表的列名、数值,自动检测外键关系,并生成可交互的 HTML 关系图。如果说 3.1.4 里 skill.effect_id → effect.effect_id 这道箭头是人在脑子里画出来的,那么这个脚本就把那道箭头对整张表画成了图。依赖逆行的地方(L3 数据反过来引用 L1 骨架的风险),在图里会立刻跳出来。

schema-doc 技能会解析 xlsm 的 $结构表,自动生成 Markdown 结构文档。3.1.4 (C) 里冒出的那个"请在结构中确认 0 是否为有效值"的疑问,问的就是这份结构——它让人不必翻别的文件,就能直接读到最新的结构。表一变,文档跟着变,文档与实际数据对不上的那个老毛病也就少了。

把这两样工具的位置再用坐标说一遍,就是这样。schema-doc 守的是 L3 的列定义,gen_relation_map.py 守的是 L2\~L3 之间的关系。AI 辅助提示词(像 3.1.4 那样的验证)则在它们之上运转。三者不是各跑各的,而是分管同一根坐标轴的不同高度。

这些工具的实际用法,会在 3.2、3.3、3.4 里动手照着做。3.1 是定下"嵌在哪里"的地图,接下来三章是动手嵌进去的活儿。


3.1.6 渐进引入:从风险小的坐标开始

三样工具一次性全开,运营负担会比效果先到。按笔者的经验,安全的顺序是从风险小的坐标(下面那头)开始。

时点(建议) 引入 坐标 出错时会发生什么
1 个月 结构优先(3.2) L3 不过是文档少更新了一次
2\~3 个月 关系图可视化(3.3) L2\~L3 不过是图不准确
3\~6 个月 AI 辅助提示词(3.4) L1\~L3 要经过验证,人可以否决

时间不是绝对标准。视团队规模、既有基础设施而定,可能要花两倍,也可能减半就完事(笔者推测,未经验证)。不变的是顺序。把风险大的决策辅助放在最后,等前两样工具已让团队养成验证习惯之后,再去碰最敏感的那个位置。


3.1.7 测量 —— 诚实地

不美化数字,如实记下。下面是笔者作为总监运营的 MMORPG 项目(以下简称"项目A")的策划团队(4\~5 人,整个开发团队为中等规模 10\~50 人,运营约 6 个月)里观察到的情况。这不是精确的自动计量,而是基于工作日志和复盘记录的笔者的观察,建议只当作方向和大致的比例来读。

关键在于:省下来的时间,不是不做游戏的时间。那段时间会回流到像 L1 骨架那样、没法交给 AI 的深层决策上。减掉劳动,用在判断上——这就是本章想给的一句话。


动手试试:在坐标 L3 上做一次一致性检查

setup. 把两张表(例如技能、效果)export 成 csv。手头方便的话,转成 JSON 备着(正本是 export 结果而非文档的原则)。在两张表之间挑一对外键(例如 skill.effect_id → effect.effect_id)。

prompt. 把 3.1.4 的提示词全文原样用上。三句核心别落下——(1)"先读结构/构造",(2)"不要猜测,没有的就报告没有",(3)"没违规就只写无违规"。

verify. AI 提上来的违规清单,由人一行行看。真 bug 就修,因为领域约定值(例如 0 = 无效果)产生的误报就否决,并把那个约定加进提示词(或 atom)。下次检查起同样的误报消失了,就是又叠上了一张资产。

单人精简版

如果你是没团队也没表的单人开发者,谷歌表格开两个标签页就够了。一个标签页是"技能",另一个是"效果"。用 effect_id 这一列把两者连起来。把标签页下载成 csv,粘进 3.1.4 的提示词,那么哪怕不是 312 行、而是 30 行的表,同样能抓出 dangling reference 和孤立效果。只是规模不同,坐标是一样的。从 L3 起步,上手之后再用关系图和影响范围一格一格往上爬就行。


本章要点

3.2 模式优先 —— $模式比数据更先行

周一上午,新来的策划填好了技能表的 120 行,用 csv 构建后,客户端日志里跳出了 28 条红色报错。class_id 引用了 47 号,但职业表里根本没有 47 号。element 列里,有人写成了 Fire,又有人写成了 fire,还有一行用韩文写着 화염。为了逐行手动排查这 28 条红线,半个下午就这么没了。

这次事故的原因,并不是数据填错了。而是在生成数据之前,没有把这些数据必须遵循的规则写明白。规则只留在脑子里,人一换,规则也跟着变。本章讲的就是把规则——模式(schema)——比数据更先建立起来的工作流。而且,让这套规则不靠人的手、而靠工具以文档的形式强制执行。


术语备注 - 模式(schema):数据表的列定义。名称、类型、范围、外键、说明。 - $模式:放在 Excel 数据表(xlsm)内、专门用于列定义的工作表。它装的不是数据行,而只是列的规则。 - FK(外键):引用其他表 PK(主键)的列。比如 class_id 指向 Class 表的某一行。 - proto:Protocol Buffers 定义(.proto)。客户端与服务器共享的数据结构、Enum 契约。 - 单一事实来源(single source of truth):同一信息只在一处管理,让所有人都看向那一处的运营原则。


3.2.1 输入顺序本身就是模式

如果把模式优先只理解为"提前定义好列",那只抓住了一半。核心在于先输入什么的顺序。填数据的手按什么顺序移动,决定了一致性是被守住还是被打破。

本书推荐的输入顺序,是一条四格的管线。

flowchart LR A["$模式表<br/>(列规则定义)"] --> B["Enum / *.proto<br/>(用 VBA Export 生成代码契约)"] B --> C["csv 数据<br/>(在规则内填行)"] A -.->|schema-doc| D["模式文档<br/>(.md 自动生成)"] C -.->|gen_relation_map.py| E["FK 关系图<br/>(HTML 自动生成)"] D -.-> F(("AI / 人<br/>读取同一份定义")) E -.-> F classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class A,B,C,D,E data

从左向右流动的实线就是强制的输入顺序。先定义 $模式,再从那里用 VBA(Excel 宏语言)Export 抽出 Enum 与 proto,然后只在那份契约之内填 csv 数据。虚线是从这份输入中自动派生出来的产物——模式文档(schema-doc)与 FK 关系图(gen_relation_map.py)——人和 AI 通过这些派生物看到同一份定义。

只要这个顺序被强制执行,本章开头看到的那 28 条红线,大部分都会在填数据之前就被关掉。如果 element 只能是 fire/ice/lightning/none 四者之一这件事,被 proto 的 Enum 固定下来,那么 Fire 也好、화염 也好,都会在输入阶段被拦下。如果 class_id 引用 Class 表 PK 这件事,被写明在 $模式 里,那么 47 号的缺失,就不是在构建时、而是在检查时更早被抓住。

一旦把顺序颠倒——先填数据、再回过头来整理模式——模式就变成了事后打扫。在已经堆了 1000 行的地方去修列规则,规则就会反过来跟着数据走,那一刻,事实来源就立反了。


3.2.2 实操记录(worked transcript,完整保留的真实操作过程记录)—— 从 $模式 到 csv 一次走通

与其用嘴解释,不如真的把一张表从头到尾走一遍。假设要新建一张技能表。下面是带着 AI 辅助进行的全程记录。不做删减,把出错的地方和人否决的地方原样留下。

第 1 步 —— 人先用手写 $模式

工具和 AI 都还不叫。列规则由人亲自定义。唯独这一步不外包。

# Skill 表 $模式 (由人编写)
列              类型       范围/约束              FK                  说明
skill_id       int        1000~9999            (PK)                技能唯一 ID
name           string     1~30字                -                  显示名称
class_id       int(FK)    -                    Class.class_id      可用职业
cooldown       float      0.0~120.0            -                   冷却时间(秒)
damage_coef    float      0.0~10.0             -                   攻击力乘数系数
element        enum       fire/ice/lightning/none  -              属性
is_passive     bool       true/false           -                  是否被动

第 2 步 —— 让 AI 起草 proto Enum(提示词全文)

$模式element enum 和 FK 结构搬进 proto 契约。这是格式转换,是可以交给 AI 的活儿。

提示词(全文): 下面是一张游戏数据表的列规格。请把这份规格转换成 Protocol Buffers(proto3)的 message 和 Enum。规则:(1) enum 类型的列要单独定义为 enum,首个值必须以 _UNSPECIFIED = 0 开头。(2) FK 列把引用对象写进注释。(3) 范围约束在 proto 里无法表达,只写进注释。规格里没有的列不要新增。

skill_id int PK / name string / class_id int FK→Class.class_id /
cooldown float 0~120 / damage_coef float 0~10 /
element enum{fire,ice,lightning,none} / is_passive bool

第 3 步 —— Claude 的原始输出(原样,未作改动)

syntax = "proto3";

// Skill 表数据契约
// 注意: 范围约束(cooldown 0~120, damage_coef 0~10)
//       proto 无法强制。请在 Linter 中检查。

enum SkillElement {
  SKILL_ELEMENT_UNSPECIFIED = 0;
  SKILL_ELEMENT_FIRE = 1;
  SKILL_ELEMENT_ICE = 2;
  SKILL_ELEMENT_LIGHTNING = 3;
  SKILL_ELEMENT_NONE = 4;   // ← 人工评审标记
}

message Skill {
  int32 skill_id = 1;       // PK
  string name = 2;
  int32 class_id = 3;       // FK → Class.class_id
  float cooldown = 4;
  float damage_coef = 5;
  SkillElement element = 6;
  bool is_passive = 7;
}

第 4 步 —— 人的验证与否决

到这里,人否决了一处。AI 把 none 放成了 SKILL_ELEMENT_NONE = 4,置于 enum 的末尾,可在 $模式 里,none 是"无属性"这一默认状态。争议点在 proto 的 0 号值。在 proto3 中,enum 的 0 号是"当值没被填时自动落入的位置",所以 0 号到底是空着留给 _UNSPECIFIED(未填值),还是用 none(有意为之的无属性)这类语义值填上,就会分道扬镳。一旦把两者合在同一个位置,把列留空的行(失误)特意选了无属性的行(有意)就都以 0 落进来,从此再也分不清。AI 虽然把 _UNSPECIFIED = 0none 分开放了,却把 none 挪到了 4 号末位,让最常见的默认状态在检索、调试时离得更远了。

人做出的决定: - _UNSPECIFIED = 0 保留(用于检出缺失)。 - none 照旧保留,但在 csv 编写规则里追加一条:"无属性必须显式写 none,禁止留空"。空白=0=UNSPECIFIED 一律视为输入错误。 - 把这个决定在 $模式element 行说明里追加一行:"无属性也要显式写明(none)。空白即错误。"

没有照搬 AI 的输出。格式接受了,语义的边界由人来划。

第 5 步 —— 用 VBA Export 按钮从表里抽出 Enum/proto

验证过的 proto 定义,由 Excel 的 Export 宏($模式 表上的按钮)一致地输出。人不在 IDE 里重新打字。表才是事实来源,所以 proto 由表里产出。这个顺序,是 3.3 将要讲的"输出即正本"(json_over_schema_doc_as_source_of_truth)原则的延伸——不是文档去解释代码,而是表去产出代码。

第 6 步 —— 这才开始填 csv 数据(AI 辅助,含再请求)

提示词(全文): 请生成 5 个满足上述 Skill proto 与 $模式 的 csv 行。约束:class_id 取 [1,2,3] 之一(当前 Class 表中存在的 PK),damage_coef 在被动(is_passive=true)时为 0.0,element 原样写 fire/ice/lightning/none 字符串,禁止留空。

Claude 的首次输出里,有一行出了岔。

skill_id,name,class_id,cooldown,damage_coef,element,is_passive
1001,烈焰斩,1,3.5,2.4,fire,false
1002,冰封之手,2,8.0,3.1,ice,false
1003,生命恢复,1,0.0,1.2,none,true     ← 否决: 明明是被动 damage_coef 却≠0
1004,雷枪,3,5.0,2.8,lightning,false
1005,防御姿态,2,0.0,0.0,none,true

第 1003 行违反了规则(is_passive=truedamage_coef 应为 0.0)。人否决并再次请求。

再请求(全文): 第 1003 行违反规则。is_passive=true,但 damage_coef 是 1.2。被动应为 0.0。只改第 1003 行重新给我。

Claude 再输出: 1003,生命恢复,1,0.0,0.0,none,true

AI 第一次没能全部答对,不是缺陷,而就是会发生的事。要紧的是,正因为铺好了模式,那一行出岔的数据才能用眼睛点出来、用一行就退回去。要是没有模式,第 1003 行就会在构建后、于游戏里以"被动技能却造成伤害"的 bug 形式被发现。

这整段记录的教训很简单。只要输入顺序被固定为 $模式 → proto → csv,AI 就能快速把格式填好,人只检查语义和违规。一旦顺序崩了,人就得从格式到语义全部一肩扛起。


3.2.3 schema-doc —— 不让人去誊抄模式

$模式 放在 Excel 里,对策划来说方便,但对 AI、git 和外部工具而言,那是一处封闭的位置。所以我们运营一个把 $模式 自动转换成 Markdown 的工具。斜杠技能 schema-doc 做的就是这件事。

它的动作分四步。

  1. 解析 Excel(xlsm)的 $模式 表(python-calamine,Rust 加速)
  2. 抽取列定义的 5 要素
  3. 转换为 Markdown 表
  4. 在同一文件夹生成 <表名>_schema.md

核心是人不把模式写两遍。在 Excel 里定义一次,Markdown 就由工具来生成。两者不可能对不上。3.3 将要讲的"把模式文档当正本,就会与实际输出对不上"这一陷阱,在这里被翻转成"Excel 是正本,文档是派生"来规避。

schema-doc 生成的结果(以前面记录中的 Skill 表为准):

# Skill 表模式  (自动生成 —— 禁止直接修改)

| 列 | 类型 | 范围/约束 | FK | 说明 |
|---|---|---|---|---|
| skill_id | int | 1000~9999 | (PK) | 技能唯一 ID |
| name | string | 1~30字 | - | 显示名称 |
| class_id | int(FK) | - | Class.class_id | 可用职业 |
| cooldown | float | 0.0~120.0 | - | 冷却时间(秒) |
| damage_coef | float | 0.0~10.0 | - | 攻击力乘数系数 |
| element | enum | fire/ice/lightning/none | - | 属性。无属性也要显式写明(none),空白即错误 |
| is_passive | bool | true/false | - | 是否被动。为 true 则 damage_coef=0 |

_source: Skill.xlsm / generated by schema-doc_

请看 elementis_passive 说明列里,3.2.2 第 4、6 步中人划下的边界原样跟了进来。人在 $模式 里写了一行,文档、proto、验证就都共享了同一条规则。这就是单一事实来源真正运转起来的样子。

落到 Markdown 上的模式,会立刻被三处直接用上。


3.2.4 gen_relation_map.py —— FK 是否还活着,用图来看

如果说模式是表内部的规则,那么 FK 就是表与表之间的规则。class_id 引用 Class 表这一定义写在 $模式 里,但这条引用此时此刻是否真的还活着,需要另作检查。

gen_relation_map.py 自动侦测各数据表的 FK 关系,绘制成交互式 HTML 关系图。当 Skill 的 class_id→Class、Item 的 set_id→ItemSet 这样的箭头汇聚到一个画面上,"引用对象已消失的 FK"就会以断开的箭头显眼地呈现。本章开头那种 47 号缺失的事故,便不是以构建日志里的红线、而是以关系图里断掉的线,在填数据的途中就被看见。

这个工具的实操使用与可视化,将在 3.3 正式展开。本章要记住的只有一点。$模式 若不写明 FK,关系图也好、一致性检查也好,都没有可画的图。写明 FK 不是可选项,而是模式优先的前提。


3.2.5 模式优先五步工作流

把 3.2.2 的记录一般化,就成了五个步骤。把每一步的主体与产出分开来看,什么由人攥着、什么交给工具,就一目了然了。

模式优先五步 —— 主体 × 产出

步骤 主体 产出

1. 模式设计 $模式 5 要素·FK 定义 2. 自动文档化 schema-doc 模式 .md 3. 契约抽取 VBA Export Enum / *.proto 4. 数据初稿 AI + 人 csv 行 (违规否决·再请求) 5. 一致性·影响 Linter / 关系图 违规报告·FK 图 蓝=人的决定 / 绿=工具自动 / 黄=AI 初稿+人工评审

五个步骤不必在头一个月就全部备齐。哪怕只跑第 1、2 步(模式设计 + 自动文档化),也能抓住一半的价值。第 3\~5 步等运营熟练之后再逐步接上。一开始就强推五步,编写者的负担会在落地之前就把运营拖停。


3.2.6 在项目A中测到的东西

笔者作为总监运营的某 MMORPG 项目(以下称"项目A")里,把这套工作流跑了约 6 个月。下面的数字中,数据表列一致性、新表初稿时间是从工具日志和工作记录里汇总的实测,FK 断裂频次则是从构建失败的 issue 反推得来的笔者估算(未验证)

项目 引入前 引入后 依据
列名一致性 约 60% 约 95% schema-doc 比对实测
FK 断裂频次 每周 2\~3 起 每月 1 起以下 构建 issue 反推(笔者估算)
新表初稿时间 4\~8 小时 1\~2 小时 工作记录实测
新策划理解表 开会 3 次 文档 1 次 + 开会 1 次 入职案例(仅方向)

引入成本是工具初期开发约 3 天 + 运营落地约 1 个月。引入成本相对 6 个月的累计效果而言很小,这是运营得出的结论。不过上述比例只是一个团队、一个项目的单一案例,并不保证能原样搬到别的团队。


3.2.7 AI 与模式的协同,以及边界

铺好模式,AI 的数据生成可靠度会飞跃式上升。原因在于,模式会预先把那种成为幻觉温床的模糊输入范围关掉。面对"给我做 20 个技能"这样的请求,若没有模式,AI 就会发明出貌似合理的列,填进与本表不兼容的值。有了模式,同样的请求就会以遵守了所定义的 7 个列、各项约束、FK 的行返回。即便像 3.2.2 第 1003 行那样冒出违规,点出一行再请求一次就完事。

代价是,边界很分明。数值不交给 AI。damage_coef 若让 AI"随手"定,就会与游戏的意图冲突。把格式正确的候选快速铺出来,到这里为止是 AI 的份内事,而"这个技能的系数取 2.4 对不对",由人来回答。话虽如此,并不是说 AI 对数值毫无用处——曲线是否平滑、离群值、范围统计,AI 能迅速抓出来。测量数字交给工具,而那个数字对不对则由人来甄别。


3.2.8 常见错误与规避

错误 规避
堆了 1000 行模式之后才引入 新表一律先写 $模式
$模式 与 csv 的同步崩了 用 schema-doc 自动化把两者绑到同一来源
不写明 FK 不写明 FK 则关系图、一致性检查都无意义
proto Enum 的 0 号用了语义值 0 是 _UNSPECIFIED(检出缺失),语义值从 1 起
模式文档只有人在读 用 Markdown 表 + 元信息统一,让 AI 也能读

动手试试

setup 1. 选一张你所在领域里最核心的表(技能、道具、怪物中选一个)。 2. 在那个 Excel 文件里追加一张名为 $模式 的表,给每一列写上 5 要素(名称、类型、范围、FK、说明)各一行。这一步由人亲自来做。

prompt(只在 proto/csv 初稿上用 AI)

请把下面的 $模式 转换成 proto3 的 message 与 Enum。enum 首个值为 _UNSPECIFIED = 0。FK 把引用对象写进注释。范围约束只写进注释。规格里没有的列禁止新增。 (把你自己的 $模式 粘到这里)

接着:

请生成 5 个满足上述 proto 与 $模式 的 csv 行。不要生成违反约束的行。is_passive=true 则 damage_coef=0。

verify 1. 把 AI 给的 5 行逐行与模式比对。若有违规行,就以"第 N 行违规,只改那一行给我"再请求(否决与再请求是正常过程)。 2. 用 schema-doc(或同级的简单 Python 脚本)把 $模式 抽成 .md,确认 Excel 定义与文档是否一致。 3. 若有 FK,就把引用对象的 PK 是否真实存在比对一遍。


单人精简版

如果没有工具也没有团队,一个人起步,那么一个 Excel 文件、一个文本编辑器就够了。

  1. 在表的第一个标签页建 $模式,把列规则按 5 要素写好(15 分钟)。
  2. 把那份规格原样复制,向 AI 请求"proto Enum + csv 5 行"(10 分钟)。
  3. 把拿到的 csv 与模式用眼睛比对,把违规的一行用再请求改好(10 分钟)。
  4. $模式 文本以 skill_schema.md 存进记事本。这就是你自己的第一个单一事实来源。

转到下一张表时,重复同样的 4 步。当一个季度内有 5\~10 张核心表按同样的顺序排齐,那时才真正值得接上 schema-doc 这类自动化。


本章要点

下一章预告

3.3 关系图可视化 —— 用眼睛看清依赖关系

一位新来的策划入职第一周走到我的座位前。"我想改一下任务奖励表,可这东西一动,会把哪里弄崩呢?"我正要指着显示器作答,却停了下来。我脑子里有一幅图。RewardTable 咬着 ItemTable,ItemTable 咬着 ItemEffectTable,在它们之上 QuestTable 又引用着奖励……可一旦把那幅图用话说出口,听者脑中的形体就崩塌了。我在白板上画了七个方框。箭头开始缠成一团。30 分钟后,他点点头回到了座位,第二天又带着同一个问题回来了。

正是这一幕让我写下了这一章。系统策划的脑子里有一张依赖关系图。问题在于,它只存在于脑子里。人一换,图也就消失了。我需要一件把这幅图外化出来的工具,于是做出了 gen_relation_map.py

数据表只有 5\~10 张时,靠脑子就够了。一旦超过 30 张,人的工作记忆就应付不来了。一个项目的表格文件夹,通常很早就越过了那条线。把哪里依赖哪里用文字写成的表,即便读了也画不出图来。这一章会从头到尾跟着走一遍:把外键关系自动生成为可交互的 HTML 关系图的完整操作过程(worked transcript,完整保留的真实操作过程记录)。


3.3.1 关系图能解决的四个问题

在做工具之前,先理清没有关系图时实际上会卡在哪里。有四个场景反复出现。

新策划入职引导。 新策划为了熟悉系统结构而约了会。就是上面那一幕。用话传达的依赖关系,在听者脑中撑不了几天。如果一起点开一张关系图,第一次会议就能画出一半以上。它与白板上的手绘图有一个决定性的不同:图不会被擦掉,而是留在原处。

变更影响范围讨论。 系统变更请求提了上来。"这个会影响到哪里?"约了会,讨论了半天,还是漏掉了一两个区域。如果有关系图,只要点击要变更的节点、沿着入边(inbound edge)追下去,影响范围就一目了然。讨论只需确定"这个影响是不是真的成立"以及优先级即可。

检出依赖逆行。 L3 数据表引用 L1 系统文档是正常的。反方向(上层 Layer 直接引用下层数据表)则几乎总是设计缺陷。在用文字罗列的 FK 清单里,人是抓不住这种逆行的。在图里,它会立刻以一根 Layer 颜色错乱的箭头显现出来。

发现孤立的表。 偶尔会发现一张哪里都没有引用的表。要么是旧策划留下的残迹,要么是决定废弃却只剩文件没删的情况。这就像办公室角落里滚着一个没贴标签的箱子。要有图,才能发现那座孤岛。

这四个问题的共同点是:都属于"必须用眼睛看清结构才能解决"的范畴。靠文字和表格是行不通的。


3.3.2 实操记录:从数据表到关系图

现在真正跟着走一遍。输入是一个装着数据表的文件夹,输出是在浏览器中打开的一张可交互 HTML。我会把这中间 AI 做了什么、人在哪里做了验证/否决,毫无遗漏地记下来。

3.3.2.1 整体流程

flowchart TD A[数据表文件夹<br/>多个 xlsm/xlsx] --> B[1. 扫描:收集表·列头] B --> C[2. 提取 FK 候选<br/>*_id / *Id / 规格书 FK 标记] C --> D[3. 匹配引用对象<br/>列名 → 目标表] D --> E{人工验证} E -->|否决误报| C E -->|通过| F[4. 构建图<br/>节点=表,边=FK] F --> G[5. 赋予 Layer 元数据<br/>参照 schema-doc 输出] G --> H[6. 用 pyvis 渲染 HTML] H --> I[relation_map.html<br/>浏览器可交互] I --> J{人工诊断} J -->|发现逆行·孤立·循环| K[提出设计修改请求] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class A,I data class B,C,D,F,G,H code class E,J,K human

核心是第 3 步和第 5 步之间的人工验证回路。FK 候选提取由机器铺好初稿,人在其中剔除误报。一旦省掉这个回路,关系图看上去煞有介事,却是一幅错的图。

3.3.2.2 FK 从哪里来 —— 输入顺序

这件工具的准确度,取决于你从哪里把输入拉过来。3.2 中定下的 schema-first 原则原样适用。FK 信息的正本顺序如下。

  1. $스키마 —— 每张数据表的第一份正本。按列明确标注了类型、Enum、FK 目标。这里若写明了 FK,那它就是第一优先。
  2. *.proto / Enum 定义 —— 由 VBA(Excel 宏语言)Export 导出的 schema。当规格书为空时,用它补全类型。
  3. 实际 csv 输出 —— 表格导出的实际数据。规格书里没有的关系,也会在数据中以模式显现(例如:若 npc_id 列的值全部落在 NPCTable 的键范围之内,那它实际上就是 FK)。

这里要明确一条原则。正本不是 schema 文档,而是实际的 JSON/csv 输出。 即便规格书里写着 reward_id 是 FK,只要实际数据中该列为空或指向了无关的值,那就是规格书错了。工具在两者不一致时信任数据这一侧,并把不一致本身记进报告。这就是不把 schema-doc 当正本的理由。

3.3.2.3 第 1 步 —— 文件夹扫描与 FK 候选提取

工具的第一个动作,是打开文件夹内所有 xlsm/xlsx,收集表与列头。这一步本身是机械的,没什么意思。有意思的是提取 FK 候选的规则。一开始很简单:列名若以 *_id*Id 结尾,就视为 FK 候选。可单凭这一条规则,误报和漏报都出现了。

这里我接入了 AI。把整份列头清单丢过去,让它推断 FK 目标表。下面是我实际抛出的提示词全文与原始输出。

提示词: 下面是若干游戏数据表的表名与列头清单。请推断每一列是否为外键(FK),若为 FK,则它引用了哪张表的哪一列。列名规则通常为 <目标表单数形式>_id。但若没有把握,请标为"不确定"并写明理由。不要凭猜测填充。

QuestTable: quest_id, title, npc_id, reward_id, prev_quest_id, region_id
NPCTable: npc_id, name, region_id, faction_id
RewardTable: reward_id, item_id, count, currency_id
ItemTable: item_id, name, item_effect_id, grade
ItemEffectTable: item_effect_id, effect_type, value
RegionTable: region_id, name, parent_region_id

Claude 原始输出(PK 行省略,仅摘录 FK 判断):

QuestTable - npc_id → NPCTable.npc_id(推测为发布任务的 NPC) - reward_id → RewardTable.reward_id - prev_quest_id → QuestTable.quest_id(前置任务,自引用) - region_id → RegionTable.region_id

NPCTable - region_id → RegionTable.region_id - faction_id不确定:输入清单中没有 FactionTable。按列名规则看,应引用 FactionTable.faction_id,但由于未提供该表,无法确定目标。

RewardTable - item_id → ItemTable.item_id - currency_id不确定:清单中没有 CurrencyTable。货币种类也有可能以 Enum 管理,因此可能并非 FK。需要核对规格书。

ItemTable - item_effect_id → ItemEffectTable.item_effect_id - grade → 非 FK(推测为 Enum:等级值)

ItemEffectTable - effect_type → 非 FK(推测为 Enum)

RegionTable - parent_region_id → RegionTable.region_id(自引用,地区层级结构)

注意:以上推断基于列名规则,并未核对实际数据值。特别是 countvalue 这类不以 _id 结尾的列,我已从 FK 检查范围中排除;若存在隐藏引用,则会被漏掉。

在这份输出上,人做的事有三件。

这里得到的教训很明确。AI 最有用的地方,不是快速推断,而是把不知道的位置留作"不确定"的那份克制。要是它硬把空格填满,faction_id 就会被连到无关的表上,而那个误报会作为一根假箭头留在关系图里,把新策划引向歧途。

3.3.2.4 第 2 步 —— 构建图与赋予 Layer

得到经过验证的 FK 清单后,gen_relation_map.py 就来构建图。表是节点,FK 是有向边。数它的入边数(有多少张别的表引用了我),据此决定节点大小。被引用得越多,节点越大,也就是系统的枢纽。

Layer 元数据从 schema-doc 技能生成的 Markdown schema 文档里拉取。3.1 中定义的 Layer 坐标(L0\~L4)以标签的形式附在每张表上,工具读取它来给节点上色。这个衔接很重要。关系图若不知道 Layer,就只是一堆方框和箭头;只有知道 Layer,才能用颜色来诊断"逆行"。

把工具内部结构以代码骨架的形式呈现,大致如下(仅摘录核心流程)。

# gen_relation_map.py (仅摘录核心流程)
from pyvis.network import Network

LAYER_COLORS = {          # Layer 调色板 —— 用 1 个 atom 标准化
    "L0": "#2c3e50",      # 元/公用
    "L1": "#2980b9",      # 系统
    "L2": "#27ae60",      # 内容
    "L3": "#f39c12",      # 数据实例
    "L4": "#c0392b",      # 派生/缓存
}

def build_graph(fk_list, layer_map):
    net = Network(directed=True, height="900px")
    inbound = count_inbound(fk_list)          # 入边统计
    for sheet in all_sheets(fk_list):
        layer = layer_map.get(sheet, "L0")
        size = 10 + inbound[sheet] * 3        # 越是枢纽节点越大
        net.add_node(sheet, color=LAYER_COLORS[layer],
                     size=size, title=sheet_tooltip(sheet))
    for src, dst, col in fk_list:
        # Layer 逆行检测:上层 Layer 引用下层时用警示色
        edge_color = "#e74c3c" if is_reverse(src, dst, layer_map) else "#888"
        net.add_edge(src, dst, title=col, color=edge_color)
    return net

is_reverse 是这件工具的小核心。若一条边的出发表比到达表处于更上层(例如 L1 → L3),就判为逆行并把边涂成红色。当人打开图、看到红色箭头时,那几乎总是该动手处理的地方。

3.3.2.5 第 3 步 —— HTML 渲染与结果结构

最后一步是 pyvis 吐出可交互 HTML。点击节点时,该表的列、Layer、入边数会以工具提示(tooltip)弹出,还能在搜索框里按表名过滤。之所以必须是 HTML 而非静态 PNG,原因就在这里 —— 一旦节点数超过几十个,静态图里箭头就会缠成一团,什么都看不见。要用鼠标拖开铺展,再点击把关注区域收窄,才抓得住模式。

把用上述示例数据生成的关系图结构转成 SVG,大致如下。颜色代表 Layer,红色箭头表示(本例中没有)逆行的位置。

RegionTable (L1) QuestTable (L2) NPCTable (L2) RewardTable (L3) ItemTable (L3) ItemEffectTable (L3)

parent_region_id (自引用)

看节点大小就知道,RegionTable 被引用得最多(Quest·NPC 都指向它)。这就是枢纽。ItemEffectTable 是叶子节点,所以小。新策划问"要理解这个系统该从哪儿看起",答案已按节点大小的顺序写在图里了。


3.3.3 图造就的诊断 —— 与 Layer 结合

3.1 中定义了 Layer 坐标。当这一章的关系图把那套坐标提升到视觉层面时,用文字或表格无法实现的四种诊断,就能在同一屏内完成。

不过这并不意味着图能抓住所有问题。图抓的是结构性缺陷。这个 FK 在语义上是否真的是对的关系(例如 npc_id 究竟是"发布任务的 NPC"还是"任务中出现的 NPC"),图是解不开的。那是人的领域判断之责。工具只不过是为人的判断铺好施展的舞台。


3.3.4 没有自动更新就会腐烂

关系图不是做一次就完事的。表每周都在新增、变更。交给手动更新的关系图,一两个月就会与实际结构错位,而错位的地图会指错路,那还不如没有。被一张错图坑过的团队成员,从此就不再看图了 —— 这是最昂贵的失败。

所以,把更新挂到自动触发上。

生成的 HTML 会自动部署到内部静态托管(策划门户)上。无需另装工具,只要有浏览器,人人都能看到同一张地图。这就像桌边总是摊开着的那张地图。无论谁来问,都指着同一幅图一起作答。


3.3.5 常见错误与规避法

错误 为何发生 规避法
节点超过 100 个、图缠成一团 把所有领域硬塞进一屏 按领域过滤、按分组拆分视图
Layer 颜色每件工具都不同 调色板在每段代码里重新定义 用 1 个 atom 把调色板标准化(LAYER_COLORS)
FK 检出只抓 *_id,导致漏报·误报 依赖一行正则 规格书 FK 标注 + 实际数据值验证并行
图是做了,可没人看 没接入工作流 强制在变更请求·会议中附上图
做了却不更新而腐烂 依赖手动更新 必须自动触发,手动一个月就失效

gen_relation_map.py 的运营里,最常被坑的是第三行。只信 *_id 规则,就会漏掉 countvalue 这类隐藏引用(3.3.2.3 中的 AI 也自行警示了这一局限),并把 Enum 的 grade 误判为 FK。规格书和实际数据两者都看的验证回路,才是这一行的答案。


3.3.6 先用单人精简版试一试

想一口气处理整个公司的数据表,既沉重,又会在展示出价值之前就把人累垮。从自己分管的一个文件夹起步,小规模开始。

动手试试

setup. 1. 选一个装着你自己负责的 5\~10 张数据表的文件夹。 2. 用 pip install pyvis openpyxl 装好依赖(读 Excel 用 excel-reader 技能或 openpyxl)。 3. 先确认各表的 $스키마 表里是否标注了 FK。没有的话,就只收集列头。

prompt. 把列头清单汇总起来,原样抛出 3.3.2.3 的提示词。关键在最后一行 —— "若没有把握就标为不确定,不要凭猜测填充"。这句话挡住了假箭头。

verify. 1. 把 AI 抛出的 FK 候选一行一行地看。对标为"不确定"的行,用规格书/实际数据来确定。 2. 疑似 Enum 的列(像 gradeeffect_type 这种没有 _id 却看着像 FK 的)要从 FK 中剔除。 3. 确认自引用(prev_*_idparent_*_id)是否抓对了。 4. 用验证过的清单画图,在浏览器中打开,用眼睛找出红色箭头(逆行)和孤岛(孤立)。

单人精简版

如果没时间做工具,第一周用一张手绘的 mermaid 起步也行。把 5 张表的 FK 按 3.3.2.3 的格式直接写进 mermaid。带着这一张去开会,展示"这就是我们系统的依赖关系",价值就在那一刻当场得到证明。价值一旦显现,自动化工具自然会随后跟上。"必须一开始就拿出能跑的工具"这份负担,你大可放下。

扩展会按这个顺序自然流动 —— 第 1 周自己表格的 mermaid 手绘图 → 第 2 周加上 Layer 颜色和点击 → 1 个月自动更新(git hook 或夜间批处理)→ 3 个月部署到内部门户 → 6 个月全部表格的整合关系图。


3.3.7 与下一章的衔接

3.2 讲了表的内侧(schema),3.3 讲了表的外侧(关系)。3.4 会在这之上叠加 AI 辅助提示词模式。在 schema 与关系都已就位的系统之上,AI 如何辅助一致性检查与影响范围提取,接下来会以一系列实用模式展开。


本章要点

下一章预告

3.4 AI 辅助系统设计的提示词模式

那是 Alpha 版本即将到来的一周。我在技能表里新增了一个职业的一行并保存。可那一行所引用的 buff ID,其实是前一天有人删掉的那一行——这一点我直到第二天早上构建崩了之后才发现。为了顺着崩掉的构建回溯找出原因,花了我两个小时。要是在删掉那一行之前能先问一句"这个删了没关系吗?",这两个小时本可以不必花。

本章讲的就是如何让 AI 替你问出那个问题。重点不在于把提示词写得多漂亮,而在于把同一个问题固化下来,不必每次都从 0 重写一遍。在 3.2 里铺好了模式(schema),在 3.3 里铺好了关系图。两者都是数据的骨架。本章要在这副骨架之上,把人抛给 AI 的问题本身固化为资产。

先钉死一点。AI 造出来的不是答案,而是候选。本章出现的所有模式里,最终拍板的那只手始终留在人这一侧。


3.4.1 即兴提示词漏在两处

刚开始用 AI 时,每次都用自然语言即兴敲。大致是这样:

帮我看下技能表。确认有没有外键断掉的,
有奇怪的就告诉我。哦对,还有冷却时间为负的。

这条提示词在两处漏掉东西。

第一,检查项每次都不一样。今天想起了"冷却时间为负",明天就忘了。昨天筛过一次的"重复 PK(Primary Key,主键)",今天的提示词里没了。依赖人记忆的检查,会随人的状态而漏。

第二,结果格式每次都不一样。同一个意图,写成"帮我确认""检查下""扫一遍"这些不同说法,AI 有些天用表格回,有些天用大段文字回。格式参差不齐,结果就没法再拿去自动处理。

解法是把提示词从手上拿下来,放进抽屉。把每次手写的便条,换成贴了标签的卡片,从同一个抽屉里取。那张卡片就是本书所说的斜杠命令(skill)和 atom。


3.4.2 固化的三种形态与取舍标准

固化有三种容器。把什么装进哪一个,取决于调用频率与稳定性。

调用频率 高 ↑ 低 ↓ 定义稳定性 →

斜杠命令(skill) 高频·稳定 → /check-sheet 一词调用,结果格式固定

atom 自动注入(JIT) 高频·核心约束 → 关键词触发 无需记忆,嵌入自然语言中

模板文件(.md) 偶尔·大型作业 → 调用文件 用眼看、易于修改

偶尔·定义不稳定 → 先别固化(保持即兴)

高频且定义已定型的作业用斜杠命令。高频但"不能忘的约束"用 atom JIT 自动注入。偶尔做但分量大的作业用模板文件。而定义仍在摇摆的作业,先不固化,留作即兴。三者不必一开始就备齐。从一两个斜杠命令起步,看到价值再增加。


3.4.3 模式 ① 一致性检查 —— 实操记录(worked transcript)

与其用嘴解释,不如把一个模式从头到尾走一遍。这是一个在删掉一个空行之前,自动先问一句"这个删了没关系吗?"的模式。它的名字叫 /check-sheet。里面固化着检查项与输出格式。

作为依据的资产,藏在本书各处嵌入的实测作业记录里。数据录入遵循 schema-first 原则(atom data_entry_schema_first)。录入顺序是 $스키마 表 → Enum/*.proto(VBA(Excel 宏语言)Export)→ csv。而正本不是模式文档,而是实际的 JSON 输出(atom json_over_schema_doc_as_source_of_truth)。一致性检查就是把这两条原则原样搬成检查规则。

setup —— 固化命令的内部

展开 /check-sheet,里面装着这样一段提示词正文。这就是不必每次手敲的那部分。

角色:你是游戏数据表的一致性检查器。

要检查的表:{{sheet_name}}
可参考的模式:$스키마 表(各列的类型·范围·FK 对象)
可参考的正本:同一张表的 export JSON(优先于模式文档)

检查项(按此顺序):
1. 外键有效性 —— FK 列的值是否实际存在于被引用的表中
2. 范围越界 —— 超出 $스키마 中定义的 min/max 的值
3. 必填列缺失 —— required=true 的列为空的行
4. 重复 PK —— 同一 PK 出现两次以上

输出格式(务必照此表):
| # | 行 | 列 | 违规类型 | 当前值 | 修正候选 | 置信度 |

规则:
- 修正候选只提建议。绝不直接改表。
- 置信度为高/中/低三者之一。若属猜测则为低。
- 没有违规时,只输出"违规 0 件"并结束。

这段正文里要紧的是最后三行规则。"不直接改"、"标注置信度"、"猜测要坦白为低"。这三行把 AI 拴在推候选的位子上,而不是给答案的位子上。

prompt —— 人实际敲的一行

/check-sheet skill_table

结束了。只递一个表名。四个检查项也好、输出表格式也好,都不再重写。这就是固化的价值。

Claude 的原始输出

下面是实际跑了一次后返回的输出。不做摘要,把含糊之处与 AI 犹豫的地方都原样附上。

skill_table 一致性检查结果如下。共发现 4 件违规。

| # | 行  | 列          | 违规类型     | 当前值 | 修正候选                | 置信度 |
|---|-----|------------|------------|--------|----------------------|------|
| 1 | 142 | buff_id    | 外键违规     | 9007   | 9070(疑似笔误,差 1 位) | 中   |
| 2 | 142 | buff_id    | 外键违规     | 9007   | 9012(名称"猛毒"相近)   | 低   |
| 3 | 88  | cooldown   | 范围越界     | -1     | 0(不可为负)           | 高   |
| 4 | 215 | combo_next | 必填缺失     | (空白) | 暂不判断               | 低   |

补充几点注意事项。

- 第 1 条与第 2 条是针对同一行(142)同一列的两个不同候选。
  9007 这个 ID 在 buff 表中并不存在。9070 与之只差一位数字,
  有可能是笔误;9012 的名称"猛毒"相近,因此也列为候选。
  两者置信度都低。需要由人来选。

- 第 4 条(215 行 combo_next 空白)很含糊。$스키마 中标为 required=true,
  但查看 export JSON 后发现,此列为空的行还有 12 个。
  可能是说明书未更新,也可能存在"连招收招技能的 combo_next 留空"这条
  规则,只是说明书未予反映。我无法断定,
  因此置为"暂不判断"。

- 若想在 cooldown 之外再跑更多范围检查,需要确认 $스키마 其他列的
  min/max,但我所收到的信息中 range_check 列有一部分为空,
  因此 cast_time 列未能检查。

这份输出之所以是好输出,不在于它干净,而在于它诚实。它没有把第 4 条断定为"既然必填就填上",而是流露出"说明书可能未更新"的自我怀疑。它也坦白了没能看到 cast_time 这一限制。倘若 AI 自信满满地说第 4 条"请填 0",那很可能是会毁掉连招收招技能的错答。

verify —— 人的验证与否决

接着由人逐行裁定。验证自动伴随,意味着这张表会自动生成,并不意味着人不看。

最后第 4 条是关键。在原样相信 AI 输出就会毁掉完好数据的位置上,人的否决起了作用。同时,第 4 条也暴露了检查规则本身的缺陷。说明书的 required=true 是错的。

再请求 —— 否决会修正规则

到这里并不收手。否决是让下一次检查更聪明的输入。人接着敲:

第 4 条不是违规。combo_finisher=true 的技能,combo_next 留空
才正常。把这条例外规则加进检查项里,今后 finisher 行的
combo_next 空白不要再当违规抓。还有,说明书的 required 定义
看起来错了的项,单独拆到"说明书疑点"小节里告诉我。
明白了。我会把例外反映进 /check-sheet 的检查规则。

新增规则:
- 进行 combo_next 必填检查时,先确认同一行的 combo_finisher 值。
  若 combo_finisher=true,则 combo_next 空白按正常处理(排除违规)。

新设小节:
- "说明书疑点" —— 当 export JSON 的实际分布与 $스키마 定义相悖时,
  把需要改的对象分离为说明书(而非数据)候选并报告。
  (依据"正本为 JSON"的原则,若多数数据违背说明书,
   则怀疑说明书一侧。)

要把这条变更记录到 /check-sheet 命令定义的 atom 里吗?

最后那一行才是这个模式的完成。一次否决没有止于一次性的修改,而是被记录进命令定义的 atom,传播给下一个人、下一次检查。这就是固化的复利。同一个失误不会被筛第二次。


3.4.4 模式 ② 变更影响范围提取 —— 实操记录(worked transcript)

第二个模式,是要消掉开头那个场景里的那两个小时。在改一行、改一个决定之前,先问"这会波及到哪里"。它的名字叫 /impact。3.3 里做的关系图(gen_relation_map.py 输出的 FK 关系 HTML)就是这个模式的燃料。

setup —— 固化的探查路径

/impact 里固化着"该翻查哪里才能不遗漏地找全影响",分四条线。

角色:你是变更影响范围提取器。

变更对象:{{target}}  (表名·列·atom_id·系统决定,四者之一)

探查路径(四条都要走):
1. 关系图入站 —— 在 gen_relation_map.py 输出中,以 FK 引用
   {{target}} 的那些表(指向这张表的箭头)
2. 正文提及 —— 在 sources/manuscript 中,{{target}} 的 ID·名称
   直接出现的文档
3. atom affects 关系 —— 与 {{target}} 经 affects 相连的 atom
4. 反向引用 —— 以 [[wikilink]] 指向 {{target}} 的笔记

输出格式:
| 类型 | 名称 | 关联依据 | 影响强度 | 负责人 |

规则:
- 影响强度为直接/间接/弱三者。直接 FK 为"直接",正文提及为"间接",推定为"弱"。
- 若担心遗漏,哪怕标为弱也要列上。漏掉比列上更糟。
- 不做优先级或工作指派。只生成清单。

prompt

/impact buff_id=9012

用一行问出"想把猛毒 buff(9012)换成别的效果,哪里会动摇?"。

Claude 的原始输出

buff_id=9012(猛毒)的变更影响范围如下。已探查全部 4 条路径。

| 类型    | 名称                | 关联依据              | 影响强度 | 负责人    |
|--------|--------------------|----------------------|---------|----------|
| 表      | skill_table         | buff_id FK 直接引用    | 直接     | teammate_a |
| 表      | monster_drop        | reward_buff FK 引用    | 直接     | teammate_b |
| 表      | set_item_effect     | proc_buff FK 引用      | 直接     | teammate_a |
| 文档    | 독계열_밸런스_노트.md  | 正文直接提及"9012"     | 间接     | (作者)   |
| atom    | poison_stack_rule   | affects: 9012          | 间接     | (作者)   |
| 笔记    | 상태이상_설계.md       | [[맹독]] 反向引用       | 弱       | (作者)   |

补充几点。

- skill_table 中引用 9012 的行有 3 个(刺客职业技能)。
  替换后这 3 个技能的实际效果会变。这是最强的影响。

- monster_drop 的 reward_buff,我把握得稍弱。列名虽是
  reward_buff,但部分行里似乎混进了指向 item 而非 buff 的
  值。建议这张表你直接打开确认一次。

- "독계열_밸런스_노트.md"只是以文本提及 9012,并非 FK,
  因此置为间接。不过它是写有平衡意图的文档,实际影响可能
  很强。请留意:强度(间接)与重要度可能并不一致。

- set_item_effect 中有 4 种套装道具以 proc_buff 使用 9012。
  抽掉猛毒,这些套装的概念就会变。

verify —— 人的验证与否决

这里 AI 的诚实同样帮了验证。要是把 monster_drop 断定为直接,就会夸大影响、约出一场不必要的协调会。多亏 AI 说"建议你确认一下"、把球交回给人,强度才得以纠正。

再请求 —— 从提取到共享

清单一经验证,下一步的手就是把它传达给人。

按直接受影响的负责人分组,给 teammate_a / teammate_b 各拟一份
要发的简短共享备忘草稿。格式为"正在考虑把 9012 猛毒换成 X,
你的表里有 N 行受影响"。决定还没定,所以用征求意见的语气、
而非通知的语气。
按负责人分的共享备忘草稿如下。用的是征求意见而非通知的语气。

[致 teammate_a]
正在考虑把猛毒 buff(9012)换成别的效果的方案。尚未确定,
想先听听意见。影响范围:
- skill_table:刺客技能有 3 行引用 9012(直接影响)
- set_item_effect:4 种套装道具以 proc_buff 使用 9012
替换后这些技能·套装的概念会变。若有反对或补充意见,请在
本周内告知。

[致 teammate_b]
想请你确认 monster_drop 的 reward_buff。正在考虑替换猛毒
buff(9012),看上去受影响的行只有 1 个(reward_buff 列里
混进了 item ID,最初看上去更多)。能否帮我确认实际只有这 1 行
是对的吗?

两份备忘都置为"正在考虑·征求意见",未放入确定通知的措辞。
发送前,你需要先填上替换候选效果(X 处)。

开头那两个小时在这里消失了。在变更之前先问影响、提前共享给负责人,于是在第二天构建崩掉之前,协调就已结束。而 AI 造出来的始终只是备忘草稿,按下发送的手指是人的。


3.4.5 两个模式共有的骨架

上面两份记录遵循同一副骨架。其余所有模式也都搭在这副骨架上。

flowchart LR A[固化的命令<br/>检查项·输出格式·禁忌] --> B[人:一行调用<br/>只递对象] B --> C[Claude:原始输出<br/>候选+置信度+自我怀疑] C --> D{人工验证} D -->|采纳| E[反映] D -->|修正| F[强度·值修正] D -->|否决| G[发现规则缺陷] G --> H[更新命令定义 atom] H -.反映到下次调用.-> A classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class A,H data class B,D,F human class C ai class E pass class G fail

关键是右下角的虚线。否决不是终点,而是作为修正命令本身的输入折返回来。一致性检查里被否决的第 4 条成了 finisher 例外规则,那条规则被记录进 atom,传播给下一次检查。没有这个反馈,同一个错答每周都会被重新筛一遍。

三处留着人的手。调用(选对象)、验证(采纳·修正·否决)、规则改进(否决的回流)。AI 只在这之间做推候选的活儿。


3.4.6 其余模式 —— 同一骨架的变奏

把同一副骨架搬到别的作业上,模式就会增多。不附记录,只点位置。它们都原样遵循 3.4.5 的骨架,因此制作时关键在于不漏掉"固化的检查项"与"人工验证位"。

模式 一行调用 AI 推的候选 人握的决定
GDD 初稿合成 /gdd-new <系统> 标准 9 小节初稿,未定处标 [TBD] 愿景·优先级·删减
状态机/BT 转换 /diagram-state 自然语言 → mermaid + 可达性验证 状态定义·转移条件
接口冲突检查 /check-interface <GDD> 输入输出·时间窗口冲突用例 优先级规则
数值计算 /balance-calc <表> <公式atom> 曲线计算值 + 与现有的 diff 公式·游戏意图
复盘作业分类 /retro-classify <周期> Layer×领域分布 + 异常信号 分类校正·解读

数值计算里只钉死一点。曲线在数值上即便平滑地下落,那份平滑是否契合游戏的意图,是另一回事。本想把 Boss 前的区段故意留得陡峭,AI 却以"离群值"为由把它削平的事是有的。所以数值计算即便自动附带了曲线验证,最后一行也要在人将其与意图对照之后才闭合。


3.4.7 运营的五项原则与收敛点

模式一多,就需要运营纪律。下面五项原则不是要背的规则,而是要嵌进工具本身的设计原则。

原则 为什么
一命令 = 一作业 越小越易于复用·调试。不把检查·修改·共享都塞进 /check
命令自动伴随验证 在输出表本身加上置信度·依据列,减轻人工验证的负担
命令定义化为 atom 像 3.4.3 的否决→规则回流那样,把缘由·示例·变更历史留在 atom 里
度量使用频率 月调用不足 1 次的命令是淘汰候选。用数据来砍
人的手只在决定上 命令只到生成候选为止。禁止自动决定

最后一个收敛点。在作者运营过的某个 MMORPG 项目里,能在系统策划中稳定留存的斜杠命令,随时间推移收敛到 12 个上下。这不是公开标准,而是一个项目的观察值(作者经验,未经验证)。但方向是明确的。命令不是无限地增,而是每月加减 1~2 个,停在脑子里装得下的数目上。贴了 100 个标签的抽屉,和没贴标签的抽屉一样。

引入不必一次到位。第一个月,把每周重复的一件作业固化为斜杠命令就够了。那一件显出价值,下个月自然就蔓延到两件、三件。


3.4.8 Part 3 收尾

在 3.1 铺好系统策划的 Layer 坐标,在 3.2 铺好模式,在 3.3 铺好关系图,再在 3.4 于其上叠加 AI 辅助提示词。走过四章的系统策划,一周会这样改变。

flowchart LR M[周一:GDD 初稿合成<br/>4h → 1h] --> T[周二:一致性检查<br/>半天 → 5 分钟] T --> W[周三:新人引导,一张关系图<br/>5 次会议 → 1 张图] W --> Th[周四:影响范围提取<br/>1h 会议 → 10 分钟] Th --> F[周五:复盘自动分类<br/>手动 → 基于数据] F --> R[把省下的时间<br/>再投入设计·评审·玩家体验] classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class M,T,W,Th,F ai class R pass

杂活时间减少,那些时间又回到深入的设计与对玩家体验的思考上。不把省下的时间重新填满杂活——这才是引入工具的真正理由。

下一个 Part 4 是战斗策划。它是系统策划最近的兄弟,3.1~3.4 的工具与模式可原样过渡过去。


本章要点


动手试试

setup. 挑一件每周重复的检查作业(例如表的一致性)。把该作业的 4 个检查项与输出表格式写下来,固化为一个斜杠命令。务必把三行规则("不直接改 / 标注置信度 / 猜测要坦白")放进命令正文。

prompt. 只把对象用一行递过去来调用。

/check-sheet skill_table

verify. 把返回的表逐行以采纳·修正·否决裁定。一旦出现否决,那不是运气,而是规则的缺陷。把那条例外追加进命令定义的那一行再发一次,让下一次调用不会把同一个失误筛第二次。

单人精简版

如果既没有团队也没有 atom 系统,就用备忘录应用里的一个文本块代替斜杠命令。标题写"表检查提示词"。内容是上面 setup 的 4 个检查项 + 3 行规则。每次检查时复制这个文本块,只改表名再粘给 AI。一旦碰上要否决的事,就在那个备忘块里直接加一行例外。工具无论是斜杠命令还是一张备忘,循环(固化 → 调用 → 验证·否决 → 规则更新)都照样转。

4.1 战斗策划与 Layer —— 打击感落在哪一格

本章学习目标(难度 🟡 实务 · 前置:四则运算·表格计算):学会把"打击感"这类抽象形容词拆解为可测量的信号,并用坐标指定战斗策划的五项产出各自落在 Layer 的哪一格。

构建评审会议室。程序员把刚接入的新技能投到显示器上。角色挥剑,敌人被向后击退。五个人在看。有人开口。

"嗯……总觉得打击感有点弱。"

旁边的人点头。"对,有点平。"

程序员问:"该改哪里、怎么改呢?"

沉默。会议室里的五个人没有一个能用数字回答这个问题。"打击感弱"是五个人都感受到的,但没有人能说出"把顿帧从 3 帧改到 5 帧"。会议花了 40 分钟来回抛"再厚重一点""冲击力不够"之类的形容词,最后以"先放到下个构建里再看吧"收场。

这一幕浓缩了战斗策划的全部问题。这是玩家最直接感受到的领域,可一旦要把这种感受落成语言,剩下的就只有形容词。形容词无法测量,无法测量就无法调整。战斗策划的第一项工作,就是把这些形容词拉下来变成数字。

本章要定的,是这些数字落在哪一格。战斗策划做的五项产出各自落在 Layer 的何处,以及这套坐标为何成为自动化的前提条件。4.2、4.3、4.4 的实战工具都在这套坐标之上运转。

给非本专业读者的一句话。 在本部分,你不必记住战斗数值或帧单位。你只需带走这一条 —— "以形容词来回传递的请求,既测不了也调不了。" 把"再厚重一点"拉成"把什么改成几"的那一刻,协作才能转起来 —— 这个思路对游戏之外任何岗位的模糊反馈都同样适用。4.1.1 的五项产出可以轻轻扫过,你只把这一条握在手里再往下走也行。


4.1.1 战斗策划桌上的五样东西

把战斗策划负责的产出用一句话概括,就是"玩家的输入被转换为屏幕上动作的全过程"。把它切成五块。

第一,战斗 Look & Feel 规格书。 把打击感、反馈性、重量感这类抽象,翻译成可测量数值的文档。这是本领域最难的产出,同时也是评判其余四样的基准。

Look & Feel 又可拆为四个信号。

没有这份规格书,会议室那一幕就会重演。有了规格书,就能给出"顿帧 3→5 帧,镜头震动振幅 +20%"这样的调整指令。

第二,技能·连招·取消系统。 这是输入被转换为动作的规则。

第三,角色·怪物 AI。 NPC 行为逻辑 —— 行为树(Behavior Tree,以下简称 BT)、状态机(FSM(Finite State Machine,有限状态机)/HFSM)、决策表。怪物行为模式、Boss 阶段切换、同伴 NPC 协同、集群模拟都归在这里。

第四,伤害·资源·冷却时间公式。 这是玩家的选择被转换为结果的数学。伤害系数·防御减免·暴击·属性修正,资源(MP/气力/耐力)的消耗·恢复曲线,冷却时间分布。

第五,动画控制规格书。 这是规定策划意图在实际构建中如何呈现的图纸 —— 动画图(graph)·BT·IK 连接。这通常是与程序员·动画师的协作,但如果策划不提供意图的规格书,意图就会在构建里走样。只丢材料不给图纸,盖出来的会是另一栋房子。

这里的关键是:五样东西在同一张桌子上相遇。连招规则(第二)一变,伤害公式(第四)的 DPS 就变,那又反过来改变 Look & Feel(第一)的体感重量。哪样产出是哪样产出的输入,如果没有写明,一次改动就会牵动五处。所以需要坐标。


4.1.2 五项产出落在 Layer 的哪一格

在 2.3 立下的 L0\~L4 坐标之上,把五项战斗产出摆上去。这套映射是本章的脊椎。

flowchart TD L0["L0 · 愿景<br/>'打击感鲜活的动作战斗'"] L1["L1 · 系统骨架<br/>连招·取消结构 / Look&Feel 规格书 / 职业骨架"] L2["L2 · 内容流程<br/>各章节敌人群落曲线 / 技能解锁顺序"] L3["L3 · 数据表<br/>伤害系数·冷却时间值·资源消耗"] L4["L4 · 构建实测<br/>实测 DPS / 真实连招路径 / 玩家反馈"] L0 -->|"接收"| L1 L1 -->|"骨架规定流程"| L2 L1 -->|"规格书是数值的基准"| L3 L2 --> L3 L3 -->|"落入构建"| L4 L4 -.->|"测量 → 修正规格书的反馈"| L1 L4 -.->|"离群值 → 调整数据表"| L3 classDef vision fill:#2d3748,stroke:#1a202c,color:#fff classDef build fill:#c05621,stroke:#7b341e,color:#fff class L0 vision class L4 build

用表格再整理一遍是这样。

Layer 战斗策划的产出 变更频率
L0 (接收 —— 愿景:"打击感鲜活的动作战斗") 几乎固定
L1 连招·取消结构 / Look & Feel 规格书 / 职业骨架
L2 各章节敌人群落推进曲线 / 技能解锁流程 中等
L3 技能伤害系数表、冷却时间值、资源消耗
L4 构建实测 DPS、真实可行连招路径、玩家反馈 每个构建

战斗策划的特点是:L4 的比重比其他领域都大。剧情策划的 L1 规格书几乎就等于最终成品,而战斗不同。"打击感好"属于必须在构建里亲手打、看着画面才能知道的领域。规格书里写了"顿帧 5 帧",它实际是否体感厚重,只能在 L4 里确认。所以仿真与自动测量工具在本领域创造的价值最大(4.4)。

不过,L4 大并不意味着 L1 不重要。看那些虚线箭头。L4 的测量值会反馈回 L1 规格书。没有规格书,测量值就失去了"比对的基准"。要有 5 帧的规格书,才会得出"实测 4 帧,少了 1 帧"这样的诊断。规格书 → 构建 → 测量 → 修正规格书的循环,贯穿全部五个 Layer。战斗策划把手按在整个循环上,是同一张桌子上同时转动着最多 Layer 的位置。


4.1.3 把形容词变成数字 —— 一段真实工作的脊椎

抽象论到此为止。现在回到会议室那一幕,用实操记录(worked transcript,完整保留的真实操作过程记录)来看"打击感弱"实际被拉成数字的过程。工具只有一个 LLM。输出不加修饰,连人否决掉的部分也原样保留。

笔者作为总监运营的某 MMORPG 项目(以下称"项目A")的战斗 TF 中,将某个新近战技能的 Look & Feel 规格化的真实流程,在此还原。

提示词(全文)

把这个技能的 Look & Feel 不要用形容词,改成 ms·帧数值。
命中时序、顿帧、镜头震动、特效同步、后摇,共五项。
没把握的就写"需在本游戏中验证"。

技能:大剑纵劈 (heavy_overhead_slash)
意图:"沉重地劈下,敌人踉跄的感觉"
帧率:60fps

Claude 原始输出(不做摘要·整理)

大剑纵劈的"沉重感"主要由顿帧与后摇的长度,以及镜头震动的衰减塑造。按项目逐条给出。

1) 命中时序。 武器越重,从输入到触发的前摇越长,重量感才立得起来。从输入到第一次视觉反馈(角色开始举剑)建议 50\~80ms(3\~5 帧)。不过,第一次视觉反馈和第一次命中反馈是两回事。命中落实取决于动作长度,这里无法断言 —— 需在本游戏的动作长度下验证

2) 顿帧。 是厚重一击的核心变量。如果轻攻击是 1\~2 帧,那么大剑纵劈建议 4\~6 帧(约 67\~100ms)。超过 6 帧,"厚重"就会滑向"卡顿",反馈性变差。

3) 镜头震动。 振幅约为屏幕高度的 1\~2%,持续 0.2\~0.3 秒,衰减比起线性更应该是 ease-out(快速减弱)。厚重一击适合首次振动强、随后快速平息的曲线。

4) 特效同步。 VFX(命中特效)·SFX(命中音)·顿帧必须在命中帧上完全一致地触发。哪怕只错开 1\~2 帧,也会被体感为"发平"。UI 反馈(伤害数字)晚 1\~2 帧无妨。

5) 后摇。 厚重的代价是后摇长。命中后到可被击中状态的恢复建议 20\~30 帧(333\~500ms)。但这个值会随游戏整体战斗节奏(快节奏动作 vs 沉稳的魂类)而大幅变化 —— 需在本游戏的基准节奏下验证

综合来看,"厚重感 = 长前摇 + 4\~6 帧顿帧 + ease-out 震动 + 同帧 VFX/SFX/顿帧 + 长后摇"。不过以上数字是一般建议范围,真正的重量感必须在构建里亲手打、再调整才能定下来。

人的验证·否决·重新请求

到这里,策划不会整份照单全收。他在两处停下。

否决 1 —— 镜头震动振幅"屏幕高度 1\~2%"。 这款游戏移动优先。在小屏幕上 1\~2% 几乎看不见。还有移动端晕动问题。策划否决了这个建议值,改用"移动端不靠震动,而以强化顿帧来表现重量"这一自有原则。LLM 只给了一般论,它并不知道这款游戏的平台约束。

保留 2 —— 顿帧"4\~6 帧"。 这不是否决而是保留。范围上是对的,但确切的值要在构建里靠手感定。规格书里写"以 4 帧为默认值放进构建,并做出 5、6 帧的变体,把三者亲手比较"。

重新请求是这样发出的。

这是移动优先项目。把镜头震动最小化,把重量感
改用顿帧·后摇·SFX 来表现的方向,重写规格书。
顿帧把 4/5/6 帧三种变体做成构建比较用的表格。

在这第二次输出里,LLM 做出了反映移动端约束的规格表。那张表进了构建,在下一次构建会议上,策划不再用形容词,而是说"4 帧变体太轻,采用 5 帧"。40 分钟的会议缩短为 5 分钟的决定。

这段实操记录展示了什么

三点。第一,LLM 很擅长把形容词拉成数字范围的初稿 —— 这能打破会议室里的沉默。第二,LLM 不知道这款游戏的约束(移动端·节奏·动作长度) —— 所以它只能给一般建议值,否决·调整是人的份内事。第三,LLM 自己两次咬定"必须在构建里亲手打才能定下来" —— 重量感的最终判断在 L4 的人手上,这一点连工具都知道。


4.1.4 AI 收回引入价值的四个位置

上面那段实操记录只展示了一个位置(规格化)。在整个战斗策划中,AI 创造价值的位置有四处。

1) 仿真 —— 价值最大。 把 DPS(Damage Per Second,每秒伤害)曲线·连招路径·资源消耗,在不出构建的情况下预先计算。比做出构建再亲手测量要快得多。4.4 会用 simulate_dps 仿真器直接处理。

2) 状态机·BT 自动生成。 把"这个 Boss 在血量 50% 以下狂暴化,狂暴期间使用三连击模式"这样的自然语言描述,转换为 BT/FSM 图。准确度高 —— 规则结构是 LLM 擅长处理的领域。把脑中的逻辑移到图上的时间被省下来。

3) 构建捕获自动分析。 从游戏画面中自动提取命中时序·连招成功率·伤害分布。不过,这是实现难度最高的位置(下面会诚实地掂量)。

4) 数值调整候选建议。 分析数据表的每一行,检出离群值·曲线不平滑,提出调整候选。人只做选择。

这四个位置里,构建捕获自动分析(3)在"做得到"和"轻松做到"之间的距离最远。书里常写"AI 从视频里自动全提取出来",可实际上没那么简单。视频像素级的计算机视觉、现成的视觉 API、游戏内 telemetry 日志 —— 这三种捕获方法在准确度·实现负担上的比较,以 4.4 为正本,请参照那边。这里只点出结论。

最现实的路是游戏内 telemetry 日志。让引擎直接打出"第 1204 帧 skill_overhead 命中,伤害 340,连招计数 3"这样的事件。这是源数据,所以准确,而且插入一次日志代码即告完成。LLM 则用来读这些日志,摘要成自然语言报告("到 3 连为止资源效率不错,但从 4 连开始骤降")。视频则留作辅助,只让人用眼确认可疑的个例。

也就是说,"AI 自动分析视频"这一愿景的现实形态是 telemetry 日志 + LLM 摘要,而非像素视觉。这一诚实的区分,是 4.4 工具选择的出发点。

而四个位置里都不变的一件事是:"打击感好"的最终判断,AI 做不了。 那属于玩家情感的领域,对那份情感的责任由人扛起。AI 只是快速地为那份情感判断做出依据材料。仿真数值、BT 图、telemetry 报告 —— 全是供人凭手感做决定的素材。


4.1.5 划分坐标的真正理由 —— 自动化的前提条件

到这里为止,都是"把产出按 Layer 划分,协作时话能说通"这种表面理由。把连招规则放在 L1、伤害表放在 L3,解释过是因为变更频率不同。这话没错,但不是全部。

划分坐标的本质理由是:自动化只在它之上才运作。Layer 分解是程序化生成·自动化的前提,这一一般命题在 2.3 已经讲过,这里只聚焦于这个前提在战斗领域的三种自动化里如何分流。

第一,仿真要"分清什么是输入、什么可改"才转得起来。 确定性内核(物理·攻击判定框 —— L1 骨架)与可变更的规格(伤害值·冷却时间 —— L3 数据表)如果混在一起,仿真器就无法定义"可变更候选空间"。内核固定、数据表可变 —— 有了这一分离,simulate_dps 才能做"把伤害系数从 280 到 340 每次加 20、画出 DPS 曲线"这样的搜索。

第二,构建捕获自动分析要动作 atom 已被标注才有意义。 在规格层标注了"这一帧区间是 skill_overhead 的 hit 阶段"这样的 atom,才能把 telemetry 日志中提取的信号与规格自动比对。没有标注,日志就是"第 1204 帧某物命中"这种无意义点位的罗列。

第三,LLM 连招序列生成要取消规则·输入队列被分离为外部文档才能运作。 "在这个角色的 7 对可取消组合和输入队列 200ms 之内,提出 10 条 5 连招序列"这样的限定请求,只有在取消规则没有固化在代码里、而是落成文档时才可能。

这三点说的是同一句话。确定性内核与规格混在一起,自动化就被堵死;分离开,自动化就被打开。 Layer 分解,表面目的是统一协作语言,本质目的是为自动仿真·捕获分析·LLM 序列搜索铺好前提条件。

从保守应用到进阶应用

铺好这个前提后,战斗运营会分两步进化。

保守应用 —— 人来设计,自动来验证。 当下大多数动作·MMORPG 的战斗运营都在这里。人亲手写连招·取消规格,自动则仿真 DPS·资源、用 telemetry 捕获,产出"规格 vs 测量"的比较报告。人解读其中的差异,决定修正规格,再回到写规格,循环转动。设计是人,仿真·捕获·比较是自动。

进阶应用 —— AI 发起候选,人只做采纳。 这是下一步。AI 在可取消组合和输入队列之内自动枚举 10\~30 条序列,自动并行仿真每条序列的 DPS·资源,LLM 给出"资源效率第 1,输入难度中"这样的排名·解读。留在人手上的决定只有"候选中采纳哪条序列作为标志性"这一个,以及总监对构建落地·动作捕获的决定。从零做出序列,与从 30 条中挑一条,工作负担是两个量级。

进阶应用要立住,得具备三样。(1) 不出构建、在 1 秒内算出 DPS·资源·生存时间的确定性仿真基础设施,(2) 连招·取消·输入队列被分离为外部文档并标注的动作 atom,(3) 基于 telemetry 的捕获自动分析。三样都是上面说的 Layer 分解的直接产物。

动作捕获是不可逆步骤 —— 决策门

最后是可逆性。战斗策划的验收循环里,混着可回退和不可回退的步骤,知道这道边界很重要。

可逆 ──────────────────▶ 决策门 ──────▶ 不可逆

连招·取消 规格修正

仿真执行·报告 (结果可自由丢弃)

数据表 数值调整

落入构建 (开发) 部分可逆

决策门

动作捕获 (标志性动作) 捕获工作室·演员·重拍成本

落入构建 (线上) 热修复成本·用户认知变化

动作捕获是战斗中最厚的不可逆步骤。捕获工作室的档期、演员的邀约、重拍成本都很大。所以标志性动作的动作捕获,只在仿真·捕获自动分析充分运转、序列已定下来之后才进行。无论保守还是进阶,都把动作捕获和线上构建之前设为决策门。战斗策划的所有验收,都要在这道门左侧的可逆步骤内结束,才安全。


4.1.6 公司里的景象 —— 减少了什么

这是项目A的战斗 TF 把上述坐标与工具运营 6 个月所测得的变化。下表数字取自 TF 运营记录里的大致平均值,并非精密测量值,把它当作体感变化的方向来读才准确。

项目 引入前 引入后
Look & Feel 会议时间 平均 2 小时(主观讨论) 平均 30 分钟(以测量值为准)
连招图绘制 1\~2 小时/技能组 10 分钟/技能组
DPS 曲线验证 出构建后手动测量(≈1 天) 仿真(≈10 分钟)
新技能数值调整 3\~4 轮构建周期 1\~2 轮构建周期

比数字本身更关键的是方向。四个项目全都从"主观讨论·手动测量·构建反复"挪向了"测量值·仿真·图自动化"。会议室里形容词少了,数字多了。这就是本章想说的那一句话 —— 战斗策划的工作,是在主观(打击感·趣味)与客观(数值·仿真)之间架一座桥,而 AI 是快速铺起那座桥的工具。桥的尽头,决定"厚重"的那只手,依旧是人的。


本章要点


动手试试 —— 把形容词拉成数字

setup. 有一个 LLM 就够。挑出手边一个技能(新的旧的都行)。用一行形容词写下这个技能的意图 —— "厚重地""轻捷地""沉钝地"之类。

prompt. 在下面的骨架里填进技能信息。

你是战斗策划助手。把下面技能的 Look & Feel 转换为"可测量的
数值规格"。不要形容词,要 ms·帧·% 单位。
没把握的项目明确写"需在本游戏中验证"。

技能:[名称]
意图:"[一行形容词]"
帧率:[如 60fps]
项目:1)命中时序 2)顿帧 3)镜头震动 4)特效同步 5)后摇

verify. 对输出里的每一个数字抛出两个问题。(1) 在这款游戏的约束(平台·节奏·动作长度)下,这个值对吗?→ 不对就把约束告知它并重新请求。(2) 这个值需要在构建里靠手定吗?→ 若是,就别写单一值,把 2\~3 个变体写进规格,在构建里比较。LLM 咬定"需验证"的项目,绝不要原样照单全收。

4.1.7 单人精简版

如果是独自做的游戏,五项产出·五个 Layer 不必全备。最少做两样。一,Look & Feel 规格书一页 —— 对核心动作 3\~5 个,只把顿帧·后摇·同步用数字写下来。用形容词写的备注,半年后连你自己都认不出。二,把连招·取消从代码里分离成一个文件 —— 把可取消组合抽成数据,日后就能让 LLM"用这些组合提出 5 条连招"。这两样,是独自开发也能为自动化敞开门的最小坐标。

4.2 战斗 Look & Feel —— 把手感转化为数值的环节

会议室的显示器前围着五个人。同一个版本、同一个技能、同一段 30 秒的视频正在屏幕上第三次循环播放。客户端程序员先开口:"我觉得还行。"美术抱着胳膊说:"太弱了。感觉缺了点什么。"旁边的策划插话:"特效是不错,可就是不'跟手'。"总监盯着看了好一会儿,做出决定:"嗯……再稍微做得'厚重'一点吧。"

然后会议就结束了。"再稍微厚重一点"到底是多少 ms、多少帧,没有一个人记下来。下一个版本里,程序员实现的是他自己理解的"厚重",美术叠加的是他自己理解的"厚重"。然后下一周,在同一个会议室里看着同一段视频,同样的对话又重复一遍。

打击感、手感、Look & Feel。这是战斗策划中用得最频繁、却又最缺乏定义的词。所有人都自以为懂,可脑子里的定义各不相同,于是讨论结束后什么也没留下。本章要做的,就是把那种"感觉"分解成可测量的数值。这是把手感从抽象拉回数据的环节。


4.2.1 战斗 = Look & Feel,系统 = 行为

先把边界划清楚。战斗策划大致分为两条线。

本章只讲后者。伤害是 100 还是 120,与手感没有直接关系。那 100 点伤害"打进去的那一瞬间"玩家如何体感,才是手感。即便是同一个伤害公式,只要命中时机与顿帧不同,就会让人感觉像是完全不同的游戏。

先得老实指出一点:打击感不是仅凭三个数值就能完成的。攻击动作(动画)的加速·减速曲线、被击方的反应(受击反馈·硬直)、80—90 年代日本动作游戏惯用的夸张变形(deformation,即命中瞬间把角色拉伸、压扁的残影·糊化表现),这些全凑齐了,"打中了"这一整块的感觉才立得起来。本章集中、并能拉回到可测量数值的,是其中的三条轴。动作·受击反应·夸张变形是动画师·美术师着手更多的领域,留到后面几章和美术部分讲;这里把重心放在策划可以用规格固定、并能在版本中验证的三条轴上。这三条轴可以这样拆分。

可测量的三条轴(并非手感的全部)

命中时机 输入 → 反应 何时响应 单位: ms "反应快不快"

顿帧 命中瞬间 让时间停顿的长度 单位: frame "厚重不厚重"

特效同步 VFX·SFX·UI· 摄像机·震动 单位: frame offset "是否像一个事件那样爆发"

+ + 这三者是测量对象 —— 动作·受击反应·夸张变形属美术·动画领域(另述)

当会议室里有人说"弱"时,那种弱来自三者之一。是反应慢(时机)?是没有命中感(顿帧)?还是各自为政(同步)?用这三条轴分解去追问,"弱"才终于变成一句可以修正的话。

不过"弱"的成因并不总是只在这三条轴上。把构成手感的要素一个不漏地列出来,划清本章负责到哪里。

Look & Feel 构成要素 是什么 本章中
命中时机 输入 → 首次反应的 ms。最先被怀疑的要素 测量·规格(轴 1)
顿帧 命中瞬间让时间停顿、赋予重量的长度 测量·规格(轴 2)
摄像机抖动 配合打击、画面晃动的反馈 测量·规格(含于轴 3)
VFX·SFX 时机 特效与声音是否同步到命中帧 测量·规格(含于轴 3)
攻击动作(动画) 挥击的加速·减速,预备动作与后续动作的曲线 提及(美术·动画领域)
受击反应·硬直 被击方一震并陷入硬直的反应 提及(下一章·美术部分)
夸张变形 命中瞬间拉伸、压扁的夸张(残影·糊化) 提及(美术部分)
手柄震动 传到手上的物理反馈 测量·规格(含于轴 3)

上面四项归入本章的三条轴,成为测量·规格的对象;中间三项(动作·反应·夸张变形)不可或缺,但属于策划一人难以用数值收口的美术·动画领域,因此只明确"它们存在"这一点。要是动作僵硬、或者被击方稳稳站着不动,那么三条轴全对了打击感也立不起来。


4.2.2 命中时机 —— 从输入到反应

三条轴里最先讲时机是有原因的。玩家怀疑手感时,最先卡住的是"反应慢"这种感觉;别的东西再华丽,只要输入迟钝,那一瞬间一切都会垮掉。所以从时机抓起。

手感的第一条轴是时间。从按下按钮的那一瞬间(0ms)到画面首次反应的瞬间,要花多少 ms。人对这种延迟敏感得惊人。60ms 和 120ms 的差别,"嘴上说不清楚,手却知道"。

一次攻击不是单纯的一个点,而是铺展在时间上的多个事件。把一次普攻放到时间轴上看,长这个样子。

普攻第 1 段 —— 时间轴 (warrior / skill_id 1001)

0ms 100 150 250 350

输入(0)

施法动作 0~100

攻击判定框 100~150

视觉特效 100~250 (延迟 50ms 后淡出)

伤害结算 110 (比视觉晚 10ms → 实际上同时被感知)

后摇 150~350 (直到可接收下一次输入)

这张图里最重要的数字是"攻击判定框首次开启的 100ms"。意思是按下按钮 100ms 之后,攻击判定开始。这个值决定了手感的体感速度。

推荐区间因类型·角色而异,但大致的基准线是有的。

种类 推荐 输入→反应 备注
即时反应(轻攻击) 60\~120ms "跟手"感的核心区间
厚重反应(大型技能) 200\~400ms 为厚重感而刻意设置的前摇
蓄力(长时间充能) 500\~2000ms 刻意的等待,另行处理

这个区间不是绝对标准。这是作者估算(未经验证):休闲手游往输入宽松的一侧浮动约 ±50ms,格斗主机端则倾向于收得更严。比数字本身更关键的,是让全队共享一条基准线——"我们游戏的轻攻击约定为 90ms"。有了基准线,才能看着版本说出"对/错"。

但这里有一个陷阱。人眼分不清 90ms 和 110ms。在 60fps 下 1 帧约为 16.67ms,而这 20ms 的差距不过一帧出头。会议室里"好像有点慢?"这句话到底对不对,光靠眼睛终究判不出来。所以才需要测量。


4.2.3 如何从版本中提取时机 —— 诚实的对照

规格里写了"攻击判定框 100ms"。如何确认版本中实际是在 100ms 开启的?自动化的路分三条(视频分析·现成 vision 工具·游戏内 telemetry),三种方式的精度·难度对比在 4.4 中作正式论述。这里只点出结论。实务中最先要铺设的是游戏内 telemetry。原因很简单。与其从视频里推断 VFX"出现在画面上的那一帧",不如在代码触发 OnHit 事件的那一帧直接打一行 [HITLOG],后者准确得多、也便宜得多,完全不在一个量级。视频分析只用于没有输入叠层的外部视频(例如竞品分析),我们自己的版本则从 telemetry 铺起。

telemetry 日志长这个样子。

[HITLOG] frame=6  t_ms=100  evt=hitbox_on    skill=1001 char=warrior
[HITLOG] frame=6  t_ms=100  evt=vfx_trigger  skill=1001
[HITLOG] frame=6  t_ms=100  evt=sfx_trigger  skill=1001
[HITLOG] frame=7  t_ms=117  evt=damage_apply skill=1001 dmg=124
[HITLOG] frame=7  t_ms=117  evt=ui_dmgnum    skill=1001
[HITLOG] frame=6  t_ms=100  evt=cam_shake    skill=1001 amp=0.4

策划要做的,是把这份日志与规格逐行比对。其中大多是数值换算与机械对照,人靠眼睛反复做会疲惫、会出错,而 LLM 不会疲惫。下一节实际让它做一遍。


4.2.4 实操记录 (worked transcript) —— 让 AI 把 telemetry 日志与规格对照

把规格 yaml 和版本 telemetry 日志两份都贴进去,让 AI 对照两者、找出对不上的地方。下面是提示词全文、Claude 的原始输出,以及人工验证的全过程。不做摘要,原样照搬。(实操记录指完整保留的真实操作过程记录。)

第 1 次提示词

下面是一个战斗技能的"命中时机规格",以及从实际版本中提取的
telemetry 日志。请把版本相对规格对不上的项目整理成表。

[规格]
character: warrior
skill_id: 1001
fps: 60
events:
  hitbox_on:    {expect_ms: 100, tol_frame: 0}
  vfx_trigger:  {expect_ms: 100, tol_frame: 0}
  sfx_trigger:  {expect_ms: 100, tol_frame: 1}
  damage_apply: {expect_ms: 110, tol_frame: 1}
  ui_dmgnum:    {expect_ms: 100, tol_frame: 2}
  cam_shake:    {expect_ms: 100, tol_frame: 1}

[telemetry 日志]
[HITLOG] frame=6  t_ms=100  evt=hitbox_on    skill=1001
[HITLOG] frame=6  t_ms=100  evt=vfx_trigger  skill=1001
[HITLOG] frame=8  t_ms=133  evt=sfx_trigger  skill=1001
[HITLOG] frame=7  t_ms=117  evt=damage_apply skill=1001
[HITLOG] frame=9  t_ms=150  evt=ui_dmgnum    skill=1001
[HITLOG] frame=6  t_ms=100  evt=cam_shake    skill=1001

60fps(1 帧=16.67ms)。请把 expect_ms 换算为帧,与日志的 frame
比对,差值超过 tol_frame 的标记为 FAIL。

Claude 原始输出(原样)

先把规格的 expect_ms 换算为帧。在 60fps 下,frame = round(ms / 16.67)。

与日志中实际 frame 比对的结果如下。

事件 期望 frame 实际 frame 差值(frame) 容差 判定
hitbox_on 6 6 0 0 PASS
vfx_trigger 6 6 0 0 PASS
sfx_trigger 6 8 +2 1 FAIL
damage_apply 7 7 0 1 PASS
ui_dmgnum 6 9 +3 2 FAIL
cam_shake 6 6 0 1 PASS

有两个项目超出了容差。 - sfx_trigger:声音比视觉命中晚 2 帧(约 33ms)。超出容差 1 帧。玩家很可能感觉"声音稍微有点对不上"。 - ui_dmgnum:伤害数字晚 3 帧(约 50ms)显示。超出容差 2 帧。可能导致"明明打中了,数字却晚出"的别扭感。

damage_apply 为 frame 7,与规格上的 frame 7(110ms 换算)一致,因此正常。不过 damage_apply 比 hitbox_on(frame 6)晚 1 帧这一点,需要确认是否为有意设计。

人工验证 —— 否决了一格

收到输出后并没有立刻全信。验证点有两个。

验证 1(四舍五入边界 —— 通过)。 damage_apply 的 110ms 换算让人起疑。110 / 16.67 = 6.598…。四舍五入得 7,没错。可如果规格本意是"6 帧 = 与 100ms 同一帧"呢?直接确认后发现,规格里确实写着 damage_apply: expect_ms 110,刻意与 vfx(100)拉开 10ms 差距。所以 frame 7 的换算是准确的。AI 通过。

验证 2(最后一句 —— 否决)。 AI 在结尾补的那句"damage_apply 比 hitbox_on 晚 1 帧这一点,需要确认是否为有意设计"是噪声。规格里已经用 expect_ms 110 明示了意图,AI 却把它当成"需要确认"反问回来。既然给了规格,就该在规格之内把答案收口,它却制造了不必要的疑问。这句话没有采纳。

带着否决理由重新发起请求。

第 2 次提示词(重新请求)

damage_apply 在规格里已用 110 钉死意图,所以去掉"需要确认"的注释。
// 意思是:不要去反问规格之内已收口的项目
只保留两条 FAIL(sfx、ui_dmgnum),不要断定成因,
改成"待确认的代码/资源位置"检查清单。

Claude 第 2 次输出(原样)

针对两条 FAIL 的确认检查清单如下。不断定成因,只整理确认点。

sfx_trigger(+2 帧延迟) - [ ] 声音触发是绑定在与 hitbox_on 相同的事件上,还是挂在单独的动画通知(notify)上 - [ ] 音频资源本身是否有前导静音(leading silence)——文件开头的空白段可能被当作延迟 - [ ] 声音播放调用是否在同一帧进入,却因输出延迟被推后(平台音频缓冲)

ui_dmgnum(+3 帧延迟) - [ ] 伤害数字 UI 是否在 damage_apply(frame 7)之后于单独的 tick 中刷新 - [ ] UI 刷新是否被集中到下一帧末尾批量处理(延迟渲染队列) - [ ] 数字弹出动画的起始帧是否被加入了额外的延迟值

可以断定的:两个项目都超出了规格容差,且比视觉命中(frame 6)晚。无法断定的:以上成因中究竟是哪一个,在确认代码/资源之前无从得知。

这第二次输出原样采纳了。不去编造成因、而是改成"待确认之处",正是想要的形态。我把这份检查清单原样交给了声音负责人和 UI 负责人。声音那边的元凶是音频资源前导静音 33ms(检查清单第 2 项)。UI 那边是下一帧刷新的结构(第 1 项)。

到这里,分工的界线就清晰了。AI 机械地对照规格与日志、抓出 FAIL;人则(a)否决 AI 制造出来的不必要反问,(b)在代码里确定 FAIL 的真正成因。 让 AI 去断定成因,它会编出听起来煞有介事的假话,因此只让它做到"待确认之处"为止才安全。


4.2.5 顿帧 —— 打击的重量

第二条轴是停顿。命中那一瞬间把游戏时间极短地停住、或放慢的效果。它决定了"打中了"这种感觉的强度。这是格斗游戏与动作 RPG 中最强力的手感工具。太长会闷,太短则没有重量。

推荐区间(以 60fps 为准)如下。这些数值是动作游戏中通行的大致惯例,绝对值因游戏而异、需各自调整。

种类 推荐帧数 换算
轻击中 1\~2 帧 16\~33ms
中等击中 3\~5 帧 50\~83ms
重击中(必杀技) 6\~12 帧 100\~200ms
暴击·命中弱点 上述值 + 2\~3 帧 ——

要按角色·技能区别给值。要是全给一样,重量差就出不来,最终所有攻击都收敛到同一种调子。这一点连接到 4.2 三条核心讯息之一。

"由谁来停顿"也是一项设计选择。

选项 效果 适合
只停攻击方 攻击方一侧有重量感,被击方继续后退·倒地 动作
只停被击方 被击方暂时定住,攻击方自由移动 连招友好
两边都停 最强的重量感 格斗游戏传统

规格这样输入。

character: warrior
skill_id: 1001
hit_stop:
  attacker: 2          # frames
  victim: 4
  critical_multiplier: 1.5   # 暴击时 1.5 倍(四舍五入)

顿帧是仅凭规格数值无法对"对/错"收口、手感验证最棘手的一条轴。telemetry 能抓到"实际是否停了 4 帧",但"4 帧是否合适"必须由人亲手摸版本来判定。AI 保证规格一致,人则看那个规格值本身是否合适。


4.2.6 特效同步 —— 是否所有东西都在同一帧爆发

第三条轴是同时性。VFX(视觉特效)·SFX(声音)·UI(伤害数字)·摄像机(抖动)·震动(手柄)若在同一帧开始,玩家的大脑就会把它们捆成"一个事件"。哪怕只错开 1\~2 帧,也会出现"别扭"的反应;错开 3\~5 帧,就会出现"像是 bug"的反应。前面实操记录里 sfx 晚 2 帧、ui 晚 3 帧而 FAIL,正是这条轴的问题。

同步对象共 5 种及其容差。

要素 触发时点 容差
VFX(视觉特效) 命中帧 ±0(必须同时)
SFX(声音) 命中帧 ±1 帧(16ms)
UI 伤害数字 命中帧 ±2 帧
摄像机抖动 命中帧 ±1 帧
手柄震动 命中帧 ±2 帧

关键在于这 5 种全部都要写进规格。常见的错误是只把 VFX 写进规格,其余四种当作"它们会自己对齐吧"放任不管。规格里没有,版本验证就没有基准;就算 telemetry 抓到了,也没有可比对的对象。5 种都进了规格,自动比对才能收口。

自动比对照搬上一节的实操记录即可。只要 telemetry 日志里 5 种的触发帧都打全了,AI 就会与规格对照、只报告超差的项目。人不必每个版本都用眼睛去确认 100 个技能。不过,AI 抓出规格对不上,与人去抓"规格全对、可手感还是不立"的领域,这两件事仍然是分开的。


4.2.7 从规格到下一个版本 —— 整个循环

把至此为止的碎片接成一条流程。这个循环一旦转起来,会议室里的"再稍微厚重一点"就被翻译成"顿帧 victim 4→6 帧"。

flowchart TD A["编写规格<br/>(策划: yaml)"] --> B["版本实现<br/>(程序员·美术师)"] B --> C["运行版本 + telemetry 日志<br/>[HITLOG] 自动输出"] C --> D["AI 自动对照<br/>规格 yaml vs telemetry"] D --> E{"是否有超差<br/>FAIL?"} E -->|有| F["FAIL 项目 + 确认检查清单<br/>(AI, 禁止断定成因)"] F --> G["负责人在代码·资源中<br/>确定真正成因 (人)"] G --> B E -->|无| H["策划: '手感'评审<br/>规格虽对、手感立不立 (人)"] H -->|需调整| A H -->|OK| I["作为下次里程碑复盘的输入留存"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class C code; class D,F ai; class A,G,H human; class I pass;

在这个循环里,AI 负责的格(D、F)和人负责的格(A、G、H)清晰地分开。AI 擅长机械对照与生成检查清单,人擅长设定基准线·确定成因·最终手感判定。自动化不是要把人剔出去,而是让人从每次都要花半天的"用眼睛数帧"中解脱出来,只专注于"手感"。

循环的最后一格(I)很重要。这些测量数据不是用一次就丢,而是再次作为下次里程碑复盘的输入进来。一旦"上个季度的手感 FAIL 集中在 sfx 同步上"这类规律以数据留存,下个季度就先动音频流水线。


4.2.8 测量减少了讨论的环节 —— 运营观察

在作者以总监身份参与的某个手游 MMORPG 项目(以下称"项目A")中,把上面的循环跑了约 6 个月,观察到的变化如下。下面的数值不是精密计量,而是基于会议记录的时间戳与版本验证记录的作者运营观察(含估算),请按方向与比例来读。不要把绝对值当作可引用的基准来用。

项目 引入前 引入后 性质
单次 Look & Feel 会议耗时 拖得很长 缩短到一半以下 以会议记录为准,体感
版本验证(多个技能) 几乎一整天 大幅缩短 telemetry 自动对照的效果
"打击感弱"反馈的消解 多个版本周期 1\~2 个周期 样本少,只看方向
规格 vs 版本 一致率 一半上下 大多一致 引入 telemetry 后变得可测量

比数字本身更本质的是定性变化。会议室里一冒出"好弱啊",马上就会反问"哪条轴?时机?顿帧?同步?";答不上来就一起把 telemetry 调出来。可测量的客观为讨论造出了终点,这是 6 个月里最大的变化。"再厚重一点"走出会议室之外的情况变少了。


4.2.9 常见错误与规避

错误 规避
没有规格就先验证版本 规格优先。没有基准,"对/错"就无从谈起
想先铺视频分析 我们自己的版本从 telemetry 起。视频分析只用于外部视频
让 AI 断定 FAIL 成因 只做到"待确认之处"检查清单。成因由人在代码里确定
只写 VFX 规格,漏掉其余 4 种 5 种(VFX·SFX·UI·摄像机·震动)全部写进规格
把一个角色的规格复制给全部角色 按角色·技能差异化。相同则手感收敛为一种调子
在所有击中上滥用顿帧 只用在有意义的击中上。滥用则发闷
原样接受 AI 输出的反问 对规格之内已收口的项目,其"需要确认"注释予以否决

动手试试

在自己的项目里以最小规模铺一遍这个循环的步骤。

setup 1. 选 1 个要验证的技能(推荐普攻)。 2. 在代码的 6 处触发点(hitbox_on、vfx、sfx、damage_apply、ui_dmgnum、cam_shake)各埋一行日志:[HITLOG] frame=X t_ms=Y evt=... skill=...。 3. 编写规格 yaml(在 events 块里写 expect_ms + tol_frame)。把本章的第 1 次提示词示例当模板用。

prompt 4. 把版本跑一遍,收集 telemetry 日志。 5. 把规格 yaml + telemetry 日志一起贴给 AI,这样吩咐:"把版本相对规格对不上的项目,按 60fps 帧换算来对照,只把 FAIL 给成表。不要断定成因,改成'待确认之处'检查清单。"(本章第 2 次提示词的形态)

verify 6. 亲手抽验一格 AI 输出的帧换算(ms / 16.67 四舍五入)。哪怕只有一格错,就要怀疑全部。 7. 如果 AI 去反问规格之内已收口的项目、或断定成因,就予以否决并重新请求。 8. 把 FAIL 检查清单交给负责人,在代码·资源里确定真正的成因。

单人精简版

如果是既没团队也没 telemetry 基础设施的单人开发者,就这样精简。把版本以 60fps 录屏,并打开按键叠层让输入瞬间可见。录下要验证的技能一次之后,在视频编辑器里亲手数"按下按钮的帧"和"画面首次变化的帧"。把这两个帧号和规格期望值给 AI,吩咐"按 60fps 换算为 ms 与规格比较",即便没有 telemetry,也能验证核心的一条轴(命中时机)。要做到 5 种同步是难的,但哪怕只抓住"输入→反应"这一条轴,手感讨论的一半也已经搬到客观这边来了。


下一章从一次击中转入击中的连续。连招·取消·输入队列——讲那些让一次击中自然衔接到下一次击中的规则。


本章要点

下一章预告

4.3 连招·取消·输入队列 —— 枚举路径并加以验证

战斗设计师组员 B 站在会议室白板前,用马克笔画着方框。基础1、基础2、基础3,还有向旁边岔出去的重击分支。当箭头增加到七条左右时,有人问道:"那么重击挑空之后用闪避取消,还能再回到基础1吗?"组员 B 停下了笔。白板上的图里并没有画出那条路径。是能画却没画,还是规则上根本不可能,他自己也无法当场回答。

这就是连招设计真正的难题。连招在脑海里看上去像"1-2-3 连起来再分支到重击"这样简单的一条主干。可一旦掺入取消和输入队列,主干就变成了图。在六个节点上只要再加几条取消边,实际能踩到的路径就会膨胀到几十条。人无法在脑海里把这几十条全部展开。于是,"这条路径太强了"这样的数值问题,往往要等它进了构建版本之后才被发现。

本章的目标只有一个:建立一套不靠手画、而是自动枚举出全部连招路径,并对每条路径加以验证的工作流。把用自然语言写下的规则转成规格,从规格中枚举路径,再把枚举出的路径送进模拟。在这个过程中,AI 能帮到哪一步、又会在哪里撒谎,我会原原本本地展示出来。


4.3.1 连招不是表格,而是图

把连招写成表格,会是这样:"基础1 之后是基础2,基础2 之后是基础3。"行与列一目了然。可这张表会撒谎。因为表格假定了一条直线。在真实战斗中,玩家会从基础2 岔向重击,把重击用闪避取消,闪避刚结束又按下基础1。这些分支和循环都藏在了表格的行与行之间。

所以连招真正的形态是有向图。动作是节点,连接是边。每条边上挂着输入窗口(何时接受输入)和输入键。节点上挂着持续帧数,部分节点上还挂着奖励条件(必须经过特定节点才会附加伤害倍率)。

把战士角色的一套基础连招画成图,如下所示。六个节点,含取消分支。

基础1 (21f) 基础2 (24f) 基础3 (30f) 终结技 ×1.5

重击 (33f) 挑空 (28f) 闪避 (18f)

10~21f 12~24f 14~30f

重击 6~24f 闪避取消

闪避后重新进入基础1

与白板有两处决定性的不同。第一,每条边上都标明了输入窗口的帧数范围。"重击 6\~24f"的意思是:从基础2 开始之后的第 6 帧到第 24 帧之间接受重击输入。第二,有一条用虚线画出的闪避→基础1 重新进入的边。这正是组员 B 在会议室里无法当场回答的那条路径。用图明确标出后,"有/没有"就一清二楚了。

这张图若由人手画,六个节点加七八条边。如果有二十个角色、每个角色又有三四套连招,图就会变成几百张。手是跟不过来的。所以要把图写成文本规格,再从中自动生成图示与验证。


4.3.2 规格供人阅读,也供机器解析

把上面的图转写成 YAML 规格。核心是节点(nodes)、边(edges)、奖励(bonuses)三个块。取消规则也视为边的一种 —— 因为打断后跳到另一个节点,归根结底也是一条边。

# warrior_basic_chain.yaml
character: warrior
combo_id: basic_chain

nodes:
  - { id: basic_1,  name: 基础1,   duration_frames: 21 }
  - { id: basic_2,  name: 基础2,   duration_frames: 24 }
  - { id: basic_3,  name: 基础3,   duration_frames: 30 }
  - { id: heavy,    name: 重击,    duration_frames: 33 }
  - { id: launch,   name: 挑空,    duration_frames: 28 }
  - { id: dodge,    name: 闪避,    duration_frames: 18, cancels_recovery: true }

edges:
  - { from: basic_1, to: basic_2, input: light, window: [10, 21] }
  - { from: basic_2, to: basic_3, input: light, window: [12, 24] }
  - { from: basic_2, to: heavy,   input: heavy, window: [6, 24] }
  - { from: heavy,   to: launch,  input: heavy, window: [10, 33] }
  - { from: heavy,   to: dodge,   input: dodge, window: [0, 33], type: cancel }
  - { from: basic_3, to: dodge,   input: dodge, window: [0, 30], type: cancel }
  - { from: dodge,   to: basic_1, input: light, window: [8, 18] }   # 重新进入

bonuses:
  - { on: basic_3, requires_path: [basic_1, basic_2], damage_multiplier: 1.5 }

这份规格同时满足两类读者。人读到 window: [6, 24],会明白"重击是从基础2 中段开始接受的";机器解析同一行,用于生成图示和枚举路径。一个来源同时产出人的理解和机器的验证。

上面的帧数(2124[6, 24])不是实测值,而是为了本章说明、由作者构造的示例值(未经验证)。在真实项目中,这些值来自动画师制作的蒙太奇(montage)长度和构建版本里的通知(notify)时机。第一次写规格时填入设计师的意图值,等构建版本出来后再捕获、用实测值加以校正 —— 这条校正循环在 4.4 中讲解。


4.3.3 实操记录(worked transcript)—— 从自然语言到规格

把组员 B 画在白板上的规则用自然语言交出去,让 AI 转成规格 YAML。这里不做概括,而是把提示词全文、Claude 的原始输出、人的验证/否决都原样照搬。"实操记录"指的是完整保留的真实操作过程记录。

提示词(全文)

下面是战士角色的连招规则。把它转成连招规格 YAML。

规则:
- 基础攻击连成 1、2、3 三段。用轻攻击键。
- 在基础2 进行途中按下重击键,可以岔向重击。
- 重击之后再按一次重击,会把敌人挑空(挑空)。
- 重击随时可以用闪避取消。
- 基础3 也可以用闪避取消。
- 闪避刚结束后,可以用轻攻击重新开始基础1。
- 基础3 只有在基础1 和基础2 都经过之后,伤害才是 1.5 倍。

规格格式:
- nodes(id, name, duration_frames)、edges(from, to, input, window, type)、
  bonuses(on, requires_path, damage_multiplier)
- 取消边用 type: cancel 标记。
- 帧数值,我没给的你不要自己定,留成 TODO 空着。

最后一行是关键。明确要求:没给的帧数不准编造。这是在提示词层面强制执行 v3 标准第 3 条(数值要诚实)。

Claude 原始输出(未经编辑)

已转换为规格。帧数值未提供,故留为 TODO。

```yaml
character: warrior
combo_id: basic_chain
nodes:
  - { id: basic_1, name: 基础1, duration_frames: TODO }
  - { id: basic_2, name: 基础2, duration_frames: TODO }
  - { id: basic_3, name: 基础3, duration_frames: TODO }
  - { id: heavy,   name: 重击, duration_frames: TODO }
  - { id: launch,  name: 挑空, duration_frames: TODO }
  - { id: dodge,   name: 闪避,   duration_frames: TODO }
edges:
  - { from: basic_1, to: basic_2, input: light, window: TODO }
  - { from: basic_2, to: basic_3, input: light, window: TODO }
  - { from: basic_2, to: heavy,   input: heavy, window: TODO }
  - { from: heavy,   to: launch,  input: heavy, window: TODO }
  - { from: heavy,   to: dodge,   input: dodge, window: TODO, type: cancel }
  - { from: basic_3, to: dodge,   input: dodge, window: TODO, type: cancel }
  - { from: dodge,   to: basic_1, input: light, window: TODO }
bonuses:
  - { on: basic_3, requires_path: [basic_1, basic_2], damage_multiplier: 1.5 }
```

需要确认的地方:
1. "闪避刚结束后重新开始基础1"我放进了 dodge -> basic_1 这条边,但
   这究竟是在闪避的后摇期间接受输入,还是在闪避完全结束之后,并不明确。
2. 规则里没有提到挑空(launch)之后是否衔接空中连招。
   需要确认挑空是否就是连招的终点。
3. 闪避到底放成节点,还是只放成边(取消动作),这点模糊。暂且放成了节点
   —— 我判断闪避本身是一个具有持续时间的动作。

在这里做评估。优点:没有编造帧数,全部留成了 TODO。遵守了提示词的约束。而且末尾仅有的三个问题,都是真正模糊的地方 —— 尤其是第 1 个(重新进入的输入时机)正是组员 B 在会议室里没能当场回答的那个漏洞。AI 比人更早找到了漏洞。

模糊之处:第 3 个里把闪避放成节点,这是见仁见智的判断。闪避既是"取消动作",同时又是"具有持续时间的动作",两边都说得通。AI 选了一边,又如实报告说它模糊,这是诚实的;但这属于设计决策,得由人来定。

人的验证/否决

回答三个问题,并否决其中一部分。

另有一处要否决。AI 没有给 dodge → basic_1 边加 type: cancel,而这条边是打断闪避的后摇再进入的,具备取消性质,这点没错。但这里把它视为"闪避后的正常进入",当作普通边处理 —— 因为对这个角色而言,后摇取消与正常衔接在游戏手感上没有差别。这是人凭借领域判断覆盖 AI 分类的一个例子。

再次请求

好。把下面这些反映进去,重新给出最终规格:
- 给 dodge 追加 cancels_recovery: true。
- dodge -> basic_1 边的 window 取 [8, 18]。
- 其余帧数我还是没给,保持 TODO。不过上面那张图的示例值
  (basic_1=21、basic_2=24、basic_3=30、heavy=33、launch=28、dodge=18)
  我会用,所以就用这些值填进去。要用注释标明这是未经验证的示例值。

这次再请求得出的结果,就是 4.3.2 的那份 YAML。它不是一次成形的。提示词 → 原始输出 → 验证/否决 → 再请求。正是这个循环造就了规格的可信度。AI 标出模糊之处,人凭领域知识来决断 —— 只靠其中一方都行不通。


4.3.4 自动枚举路径

既然规格是图,那么连招路径的枚举就变成了图搜索问题。从起点节点出发,寻找通往终点节点(或终结技)的所有路径,采用深度优先搜索(DFS)。人在脑海里做不到,而代码瞬间就能完成。

作者团队的隔离工作区 95_BattleTF 里有一个负责这项枚举的小脚本。它读取规格 YAML,抽出所有路径,并验证每条路径在规则上是否成立(边是否存在)。只看核心逻辑的话,如下所示。

# 95_BattleTF/enumerate_paths.py (节选)
import yaml

def load_graph(path):
    spec = yaml.safe_load(open(path, encoding="utf-8"))
    adj = {}
    for e in spec["edges"]:
        adj.setdefault(e["from"], []).append(e)
    return spec, adj

def enumerate_paths(adj, start, max_depth=8):
    results = []
    def dfs(node, path, edges):
        # 终点节点(无出边)或达到深度上限则确定路径
        outs = adj.get(node, [])
        if not outs or len(path) >= max_depth:
            results.append((list(path), list(edges)))
            return
        for e in outs:
            if e["to"] in path:        # 防止循环:一条路径中同一节点只走 1 次
                results.append((list(path), list(edges)))
                continue
            dfs(e["to"], path + [e["to"]], edges + [e])
    dfs(start, [start], [])
    return results

basic_1 开始跑,会涌出一大批用手绝对展不全的路径。只看其中一部分,如下所示。

# 路径 备注
1 基础1 → 基础2 → 基础3 标准三段,满足终结技奖励
2 基础1 → 基础2 → 重击 → 挑空 分支连招
3 基础1 → 基础2 → 重击 → 闪避 → 基础1 → … 循环进入
4 基础1 → 基础2 → 基础3 → 闪避 → 基础1 → … 终结技后重置

第 3 和第 4 很重要。由于有闪避重新进入的边,连招发生了循环。人在白板上没看到的,正是这种循环路径。如果不在 DFS 里加上防循环(同一节点一条路径只走 1 次)的护栏,枚举就会陷入无限循环 —— 这是第一次跑代码时真的卡了一下才发现的陷阱。只要图里存在循环,枚举器就一定需要护栏。

枚举阶段的产物有两类。第一,规则上成立的所有路径列表。第二,规则矛盾检测 —— 如果规格里有 dodge → basic_1 这条边,而 dodge 节点的定义却缺失,枚举器就会把它揪出来,标为"指向未定义节点的边"。手写规格时最常犯的错误,就是这种悬空(dangling)引用。


4.3.5 把枚举出的路径送进模拟

光有路径列表,还不知道"哪条路径太强"。得把每条路径放进 DPS 模拟器。作者团队的 simulate_dps 担当这个角色 —— 它接收路径(节点序列)、每个节点的伤害与帧数、奖励规则,计算总伤害和总耗费帧数,再算出每秒伤害(DPS)。

# 95_BattleTF/simulate_dps.py (节选,假定 60fps)
def simulate(path_nodes, node_dmg, node_frames, bonuses):
    total_dmg = 0
    total_frames = 0
    visited = []
    for nid in path_nodes:
        dmg = node_dmg.get(nid, 0)
        # 奖励:若 requires_path 全部经过,则套用倍率
        for b in bonuses:
            if b["on"] == nid and all(r in visited for r in b["requires_path"]):
                dmg *= b["damage_multiplier"]
        total_dmg += dmg
        total_frames += node_frames[nid]
        visited.append(nid)
    seconds = total_frames / 60.0
    return {"dmg": total_dmg, "frames": total_frames,
            "dps": round(total_dmg / seconds, 1) if seconds else 0}

把 4.3.4 的枚举结果整批灌进来,每条路径的 DPS 就会落成一张表。下面是把节点伤害用示例值(基础打击 100、重击 180、挑空 140 —— 均为未经验证的加工值)填入后跑出的结果。

路径 总伤害 总帧数 DPS
基础1→基础2→基础3 (终结技 ×1.5) 100+100+150 = 350 75 280.0
基础1→基础2→重击→挑空 100+100+180+140 = 520 106 294.3
基础1→基础2→基础3→闪避→基础1 350+0+100 = 450 144 187.5

这张表改变了讨论。"重击分支看上去比标准三段更强?"这样的直觉,变成了"重击路径 DPS 294 对标准 280,领先 5%"这样的数字。如果这 5% 的领先是有意为之,就通过;否则就拉长重击帧数,把 DPS 压下去。在构建版本出来之前,就在规格阶段做出这个判断。

把整条工作流压成一张图,如下所示。

flowchart LR A["自然语言规则<br/>(teammate_b 白板)"] --> B["提示词 → AI"] B --> C{"原始规格<br/>含 TODO·疑问"} C -->|人工验证/否决| D["确定规格 YAML<br/>warrior_basic_chain.yaml"] D --> E["enumerate_paths.py<br/>DFS 枚举所有路径"] E --> F["规则矛盾检测<br/>悬空边·无限循环"] E --> G["simulate_dps.py<br/>逐路径计算 DPS"] G --> H["数值判断<br/>路径间 DPS 差距"] F --> D H -->|帧数调整| D classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class E,F,G code; class B,C ai; class H human; class A,D data;

规格(D)处在中心,枚举(E)·验证(F)·模拟(G)从那里岔出。一旦检出矛盾或数值失衡,就回到规格去修。白板上没有这条循环 —— 所以白板上的连招,要等进了构建版本之后才知道它错了。


4.3.6 取消与输入队列 —— 拉长和收窄路径的两个旋钮

到目前为止,我们看了连招图和路径枚举。取消和输入队列,是调节这张图的两个旋钮,方向恰好相反。

取消会拉长边。 每追加一条取消规则,图上就多一条边,枚举出的路径数会以乘法暴增。所以取消并不是"越宽松越好"。把取消放得越开,路径就越爆炸,其中混入非预期的强力路径(就像上一节那种循环路径)的概率也随之上升。格斗游戏传统上把取消管得很严,动作 RPG 则放得很宽,原因正在这里 —— 是类型(genre)决定了"允许多少条路径"。并不存在绝对正确的窗口值。

处理取消时,务必分门别类地标明。如果笼统地放成"什么都能取消",枚举器就会在所有节点之间造出取消边,路径将失控膨胀。

取消类型 规格表示 对路径的影响
动作取消 特定节点 → 特定节点,type: cancel 仅追加选择性分支
闪避取消 多个节点 → dodge,window: [0, dur] 几乎在所有节点都有逃生口
防御取消 多个节点 → guard 进入防御,通常限于后摇
不可取消 没有出向的 cancel 边 发动后一直到底(霸体)

输入队列不是收窄路径,而是让路径真的踩得到。 没有队列时,玩家必须以帧为单位精准对上每条边的输入窗口(例如 [12, 24])。靠人的反应几乎不可能。队列会把在窗口之前按下的输入存进缓冲区,等窗口打开的那一刻自动发动。也就是说,队列不改变图的路径,而是给图上行走的人穿上鞋,让人能在图上走起来。

input_queue:
  window_start_ratio: 0.5   # 从动作进度 50% 起缓冲下一个输入
  expire_frames: 10         # 缓冲输入的有效期
  priority: latest          # 同时多重输入时以最后一个为优先

三个参数之间的平衡是关键。window_start_ratio 太小,连动作前段的输入都会被缓冲,蹦出非预期的后续动作。expire_frames 太短,队列就失去意义,又要回头去要求精准;太长,则很久以前按下的输入会姗姗发动,引发"怎么突然动了"的事故。推荐的起始值是 expire_frames 5\~15、window_start_ratio 0.5 上下 —— 不过这只是要随类型和角色分量去调整的起跑线,而非标准答案。

运营上还有一点。不要把输入队列参数给每个角色都设得各不相同。设一个全局默认值,只对分量特别不同的角色(如巨型 Boss 型等)做 override。如果对二十个角色的队列值各管各的,就分不清哪个是有意的差异、哪个是失误。

最后还有一点要点明。到目前为止,我们只把边当作"有/没有"来处理,但即便边存在,在那个节点上究竟如何切换到下一个节点,又是另一个决策。连招之间的连接方式有三个核心分支,虽然超出本书的深度、不用代码展开,但若不提及,规格就只画了一半。

这三者面对同一条边,却能把游戏手感做成截然相反。在规格阶段只定边的存在与窗口,连接方式(混合 vs 跳帧)通常留到构建版本时,与动画师一起定。不过若在规格里预先留一行 transition: blend / transition: skip 这样的字段,就能在构建阶段省去再问一遍"这条边当初是定的怎么切来着"。连接方式是连招图里那条隐藏的第三根轴。


4.3.7 构建版本验证是另一回事(4.4 预告)

到这里为止的所有验证,都发生在规格之上。路径枚举、DPS 模拟、矛盾检测,统统以 YAML 为对象。可规格里的帧数值是设计师的意图值,不是构建版本的实测值。动画师做出的蒙太奇的实际长度、构建版本里通知实际触发的帧、输入队列在引擎里实际生效的窗口 —— 这些得捕获构建版本来测量。

要从构建版本的视频里自动抽取 5 个信号(打击发生帧、后摇、取消窗口、输入队列、顿帧)实现难度很高,现实中最可信的是游戏内遥测(telemetry)—— 让构建版本内部打出"这个动作在这一帧接受了这个输入"这样的日志,再把日志与规格对照(捕获方法的比较参见 4.4)。这条对照循环就是 4.4 的主题。如果规格上是 [12, 24] 的窗口,在构建版本里被测成 [14, 26],那就是用构建实测来校正规格。


4.3.8 常见错误与回避法

错误 为何危险 回避法
把连招写成直线表格 分支·循环藏在行间被遗漏 用图(节点+边)做规格,不手画
把取消笼统成"什么都行" 枚举路径暴增,混入强力路径 分别明示动作·闪避·防御取消
枚举器没有防循环护栏 闪避重新进入处陷入无限循环 每条路径同一节点只走 1 次的护栏
误把规格帧数当实测 意图值与构建值不一致 标注意图值,用构建捕获校正(4.4)
把输入队列按角色各管各的 无法区分有意的差异/失误 全局默认 + 仅对部分做 override
原样相信 AI 填的帧数 编造的数值被写入规格 用"没给的值留 TODO"提示词强制

本章要点


动手试试 —— 连招路径枚举迷你流水线

这是一套能动手跟着跑的最小步骤。只要有 Python 和 pyyaml 就够了。

setup. 建一个工作文件夹,在里面放规格文件和两个脚本。

combo-mini/
  warrior_basic_chain.yaml   # 4.3.2 的规格
  enumerate_paths.py         # 4.3.4 的 DFS 枚举器
  simulate_dps.py            # 4.3.5 的模拟器

执行 pip install pyyaml 后,把 4.3.2 的 YAML 原样粘进规格文件。

prompt. 把自然语言规则转成规格这一步交给 AI。原样使用 4.3.3 的提示词,但务必包含最后那条约束。

帧数值,我没给的你不要自己定,留成 TODO 空着。
取消边用 type: cancel 标记,模糊的部分单独提成问题。

这两行能阻止 AI 编造数值和擅自判断。在得出的规格里,由人来填 TODO 和问题清单。

verify. 规格完成后,做两次验证。

python enumerate_paths.py warrior_basic_chain.yaml   # 所有路径 + 矛盾输出
python simulate_dps.py    warrior_basic_chain.yaml   # 逐路径 DPS 表

在枚举输出里确认:(1) 循环路径是否没有无限增长,(2) 是否没有指向未定义节点的悬空边。在 DPS 输出里看路径间的差距是否在意图范围之内。差距大就改规格里的帧数/伤害,再跑一遍。

单人精简版

如果没时间另外造工具,就把规格编写和路径枚举放进一次 AI 对话里收尾。给出自然语言规则,并装进一条提示词:"转成连招规格 YAML 之后,从起点节点起,把所有可能路径用深度优先全部列出,循环只走一次就掐断。如果有指向未定义节点的边,就标出来。"AI 会把规格化、枚举、矛盾检测一次性做完。DPS 模拟则把节点伤害以表格一并给出,用"计算每条路径的总伤害与帧数,做成表给我"这样的后续请求来收。精度会差一些,但比白板看得远多了。核心始终不变 —— 连招别在脑海里展开,让它枚举出来看。

4.4 AI 辅助战斗模拟与验证

战斗 TF 的构建 #234 刚刚提交。这是第一次接触新技能 skill_thunder 的构建。规格书上写着命中时机为 150ms。我输入指令。指尖的感觉告诉我:慢了,分明慢了。我叫来邻座的团队成员 A:「这个是不是有点延迟?」成员 A 试着打了两下:「嗯……好像确实有点。」两个人都没把握。规格说是 150,可手感却咬定是 200 左右。谁对?指尖与纸面的较量。下一个构建里,又会有人说「体感上还行吧」,而就因为这一句话,又一个构建悄悄流过去了。

本章的目标就是终结这场较量。如果指尖说是 200,那就用数字证明它到底是不是真的 200。而且,要在构建提交之前,光看规格就能预先知道「这个技能的 DPS 比目标高出 30%」。在带领 200 人投入的 3A 级 MMORPG、从早期就开始打磨战斗的那段日子里,感觉与数字相左时的那份无措感也是一模一样的。改变的只有一点:如今手里有了能用数字给这种偏差收尾的工具。

如果说 4.2、4.3 讲的是如何书写战斗的规格,那么 4.4 讲的就是这份规格是否按意图运作。验证有两条轴线。一条是不依赖构建、仅靠计算来验证的模拟,另一条是从实际构建的录像中提取测量值的捕获分析。当两条轴线扣成一个循环,战斗策划的迭代次数就会从以天计缩短到以小时计。

先把结论说在前面:本章的核心,就是把规格书上写的数字(150ms)和构建中测得的数字(220ms)并排放在一起,读出两者的差异(4.4.5)。前面几节(模拟器、连招枚举)可以读作让这种对照成为可能的准备步骤。


4.4.1 等待构建的代价

战斗策划要验证一个新技能,会发生什么事?

策划写规格。程序员录入数据,美术接上动作与特效,构建跑一遍,QA 过一轮,这才轮到策划亲手上手试。快则两天,通常三四天。如果在循环末尾才发现「DPS 太高了」,这个发现就成了一道「回到起点重来」的命令。三四天再来一次。

模拟就是在这个循环的第一步给出答案的工具。仅凭规格就算一遍。答案不好就改规格,再算一遍。在进入「构建」这个昂贵的阶段之前,规格本身先被过滤一次。这就像先把模型车放进风洞里试一试。在把真车开上道路之前,可疑的设计在桌面上就被淘汰了。

当然,风洞并不能 100% 预测道路。所以才需要第二条轴线——捕获分析。如果说模拟给出的是理想的答案,那么捕获给出的就是构建里实际发生的答案。把两者并排放在一起、读出差异——这就是本章的全部。


4.4.2 simulate_dps —— 可执行的模拟器

抽象的伪代码什么也验证不了。所以从一开始就写能跑的代码。下面是笔者在战斗 TF 中所用 simulate_dps.py 的核心骨架,为了载入本书而剥离了公司数据并加以重构。它不依赖任何外部库,仅靠 Python 标准库即可运行(完整文件参见「动手试试」)。

输入很简单。一个技能是带有 damagecast_sec(施法占用时间)、cooldown_secresource_cost 的 dataclass;一个角色则是带有资源总量、每秒恢复量、技能列表、优先级循环序列的 dataclass。运行在其上的主体,只有一条简单的贪心(greedy)规则:「在每一刻可用的技能中,选优先级最高的那个来释放」。它既不比真实玩家聪明,也不比真实玩家笨,目的只是抓住一个理想上限。只摘出充当脊柱的那部分,就是这样:

# 以 0.05 秒为一个 tick 构建时间轴。若不在施法中,则按优先级顺序选取第一个可用技能释放。
while t < duration_sec:
    resource = min(char.max_resource, resource + char.resource_regen * tick)
    for name in cooldowns:
        cooldowns[name] = max(0.0, cooldowns[name] - tick)
    if t >= busy_until:                     # 施法动作未结束则等待
        for name in char.rotation:          # 按优先级顺序
            s = skill_by_name[name]
            if cooldowns[name] <= 0 and resource >= s.resource_cost:
                total_damage += s.damage
                resource -= s.resource_cost
                cooldowns[name] = s.cooldown_sec
                busy_until = t + s.cast_sec  # 在此时刻之前无法使用下一个技能
                break
    t += tick
# …(dataclass 定义、warrior 输入、输出循环参见「动手试试」的完整代码)

skill_thunder(伤害 420、施法 0.9s、冷却 6s)设为 warrior 的第一优先级,skill_dashbasic_1 排在其后,跑 20 秒的结果(python simulate_dps.py):

平均 DPS: 261.0
  t=  0.0s  skill_thunder  资源=60
  t=  0.9s  skill_dash     资源=47
  t=  1.3s  basic_1        资源=50
  t=  1.6s  basic_1        资源=53
  t=  1.9s  basic_1        资源=55
  ...

这个值有什么意义?意义在于,不依赖构建、在 1 秒之内,就知道了 warrior 的理想 DPS 上限约为 261 这一事实。如果目标 DPS 是 180,那么这份规格就高出了 +45%,过头了。无需等待构建,现在就可以去调 damagecooldown_sec

局限也要诚实地写明。这个模拟器不反映玩家的输入失误、因移动与闪避产生的空档、敌人的干扰。所以测量值总是比实际构建偏高。这不是 bug,而是「上限线」这一模拟器定义本身。与实测之间的差距,在 4.4.5 中用捕获来补上。

AI 运用笔记。 上面的骨架是笔者亲手写的,但要接入新的资源模型(例如怒气槽在受到伤害时累积的结构)时,要像「请给这个 simulate_dps 加一条受击时怒气 +5 的规则。在 tick 循环内,作为独立于现有资源恢复的变量」这样引用现有代码来提出请求。如果让它从白纸生成整个模拟器,只会得到无法验证的代码。脊柱由人来定,AI 来添枝。


4.4.3 连招路径的自动枚举

仅凭 DPS 一个数字还不够。要验证「哪条连招是设计意图中的主连招」,就得把所有可能的路径展开来看。用手画树,到了七八个节点脑子就已经爆了。把路径不重不漏地展开,这件事机器压倒性地比人做得好——只是,得让它以人能够回溯检查的形式来产出。

下面是一段接收连招图、枚举所有路径并按 DPS 排序的代码。combo_graph 用邻接表记录「某个动作之后可以取消接入哪个动作」,它直接从 4.3 的状态机规格中提取而来。核心是递归展开直到死路尽头的 all_paths 生成器。

# enumerate_combos.py —— 展开连招图的所有路径并按 DPS 排序
combo_graph = {"start": ["A"], "A": ["B", "D"], "B": ["C", "E"], "D": ["C"], "C": [], "E": []}
action_stats = {  # (伤害, 耗时秒)
    "A": (300, 0.8), "B": (450, 1.0), "C": (450, 1.2), "D": (600, 1.4), "E": (200, 0.6),
}

def all_paths(node="start", path=None):
    path = (path or [])
    nexts = combo_graph.get(node, [])
    if not nexts:                       # 死路尽头 = 完成的连招
        yield [n for n in path if n in action_stats]
        return
    for nxt in nexts:
        yield from all_paths(nxt, path + [nxt])

results = []
for p in all_paths():
    dmg = sum(action_stats[a][0] for a in p)
    dur = sum(action_stats[a][1] for a in p)
    results.append((p, dmg, round(dur, 1), round(dmg / dur, 1)))

for p, dmg, dur, dps in sorted(results, key=lambda r: -r[3]):
    print(f"{' → '.join(p):<18} {dmg:>5} dmg  {dur:>4}s  DPS {dps}")

执行结果:

A → D → C            1350    3.4s  DPS 397.1
A → B → C            1200    3.0s  DPS 400.0
A → B → E             950    2.4s  DPS 395.8

这里策划该读出的信号不是简单的第一名。三条路径的 DPS 在 396\~400 之间几乎贴在一起——这是一个信号:「无论用哪条连招效率都差不多,所以主连招没有自己的身份」。如果意图是「A→D→C 应当是高风险高回报的主连招」,那就该提升 D 的伤害或缩短其耗时,把 DPS 拉高一档。该回到规格了。

在这种自动枚举替代手算的地方,即使连招节点增加到 20 个,人也只需读那张排好序的表。


4.4.4 构建捕获分析 —— 现实的方法是 telemetry

现在轮到第二条轴线。这是测量构建中实际发生了什么的阶段。人们常会联想到「AI 看构建录像自动分析」,但这里要诚实地划分支路。获取测量值的路径有三条,而三者的成本与精度差别很大。

A. 录像直接分析 从屏幕像素中 提取输入·动作·VFX 精度:低~中 实现难度:非常高 帧误差 ±1~2 研究·演示用

B. 现成视觉 API 向外部 vision 服务 发送捕获帧 精度:中 实现难度:中 IP 泄露风险 需检查公司内部政策

C. 游戏内 telemetry 引擎将事件 连同时间戳一起记录 精度:高 实现难度:低~中 直接使用引擎时刻 现实的选择

A 方案(录像像素分析)听起来很有吸引力。从屏幕上自动提取输入显示、角色动作变化、特效首帧、声音波形、伤害数字 UI——这五个信号,这是它描绘的图景。但实际做出来就会发现,由于帧压缩噪点、UI 遮挡、动态模糊,±1\~2 帧的误差是常态。在 60fps 下 1 帧约为 16.7ms。在以 ms 为单位计较命中时机的验证里,±33ms 的噪点是致命的。实现难度非常高,而精度配不上这份努力。

所以现实的答案是 C 方案,即游戏内 telemetry 日志。引擎其实早已在内部精确知晓输入时刻、动画通知(notify)的发生时刻、VFX 生成时刻、伤害应用时刻。与其从像素中推断这些时刻,不如让它用一行日志记下来即可。与其从像素中还原出 100ms,不如原样接收并记下引擎已知的那 100ms。

// 在战斗动作处理代码中加一行(UE C++ 伪示例)
// 在输入接收 / 伤害应用时点调用同一个 logger
CombatTelemetry::Log("input",  SkillName, GetWorld()->GetTimeSeconds());
CombatTelemetry::Log("hit",    SkillName, GetWorld()->GetTimeSeconds());

logger 以 JSON Lines 格式逐行输出。

{"event":"input","skill":"skill_thunder","t":12.340}
{"event":"hit",  "skill":"skill_thunder","t":12.560}
{"event":"input","skill":"basic_3","t":14.100}
{"event":"hit",  "skill":"basic_3","t":14.166}

inputhit 的时刻差,就是测得的命中时机。12.560 − 12.340 = 0.220 秒 = 220ms。这不是像素分析的 ±33ms,而是引擎时刻原样的值。把这份日志取出来与规格对照,就是下一节的内容。


4.4.5 实操记录:规格 150ms vs 测量 220ms

现在把两条轴线汇到一处。规格承诺了 150ms,telemetry 测得了 220ms。+70ms。指尖是对的。把缩小这一差距成因的过程,与 AI 一起从头跟到尾。不做概括,连 AI 一开始踩空、判断失误的场面也原样载入。

提示词

规格说命中时机是 150ms,但测量是 220ms。贴上下面的日志,以人实际会打字的长度来提问。比起冗长的客套,意图明确的简短请求更好——让它连验证方法都写出来的那一行,加上禁止武断的那一行,就是这段提示词的脊柱。

skill_thunder 的命中时机规格为 150ms,但测量为 220ms,超出 +70ms。请用下面的 telemetry 划分可能的原因,并写明各自用哪条日志来确定/排除。不要武断,不确定就标「未确认」。 // 「连验证方法都写出来」「禁止武断」这两点是核心意图。其余客套省略也不影响结果。

{"event":"input","skill":"skill_thunder","t":12.340}
{"event":"anim_start","skill":"skill_thunder","clip":"cast_thunder","t":12.340}
{"event":"anim_notify","skill":"skill_thunder","notify":"HitWindow","t":12.560}
{"event":"hit","skill":"skill_thunder","t":12.560}

按规格,cast_thunder 的 HitWindow 通知应在 0.150s 位置。

Claude 原始输出(第一次 —— 验证前,含踩空)

对于 +70ms 超出,我推测以下四种原因。

  1. 网络延迟。 输入往返服务器时可能多出了 70ms。如果是多人构建,请确认 RTT。
  2. 帧率下降。 如果构建以 30fps 运行,1 帧为 33ms,通知时机会被四舍五入到帧边界,延迟随之累积。
  3. 施法动作时长变更。 cast_thunder 片段可能比规格变长,导致 HitWindow 通知被往后推。
  4. 输入缓冲延迟。 输入队列可能晚一个 tick 才被处理。

可能性最高的原因是第 1 项网络延迟。70ms 是典型的 RTT 范围。

人工验证与驳回

在这里停下,重新看日志。AI 的第一推测(网络)在这份日志里已被排除inputanim_start 恰好打在同一时刻 12.340。这意味着输入进来的瞬间动作立即开始,没有网络往返插足的余地。第 1 项错了。

第 2 项(帧率)也站不住脚。若是 30fps,本该看到 33ms 单位的参差不齐,可 input→hit 恰好是干净的 0.220。这不是帧边界四舍五入,而是片段内固定位置的气味。

决定性的线索另有所在。anim_notify 的时刻在 anim_start 基准上 +0.220。规格说 HitWindow 应在片段开始后 0.150,而实际片段里却固定在 0.220 位置。也就是说,要么片段本身就被做得与规格不同,要么通知位置从 0.150 被挪到了 0.220。原因既不是网络也不是帧率,而是第 3 项,动作/通知数据。AI 把正确答案的候选包括进了四项之内,但把名次排反了。

再次请求的提示词

把排除依据由人手填好后再扔回去。无需长篇解释,只把排除掉的和剩下的事实简短地钉死。

排除网络、帧率。(依据:input=anim_start 时刻相同,故输入→动作无延迟 / input→hit 为 0.220,无帧噪点且稳定。)剩下的事实:anim_notify 在 anim_start +0.220,规格为 +0.150。仅凭这一事实缩小原因,并以行动为单位写明该向程序员、动画师请求什么。 // 核心是「以行动为单位」——不是推测,而是以「向谁请求什么」收尾。

Claude 原始输出(第二次)

根据所给依据,原因可缩小为动画片段 cast_thunder 的 HitWindow 通知被放在了 0.220s 而非 0.150s。剩下两条分支。

区分两条分支的确认方法: 1. 向动画师:请求 cast_thunder 的 HitWindow 通知帧号、片段总帧数,以及与规格撰写时点数值相比的变更记录。如果标记被挪动,则为(乙)。 2. 向程序员:确认 skill_thunder 的 montage play rate 是否为 1.0,角色攻击速度属性是否会乘到这个片段上。如果 play rate 为 0.68(=0.150/0.220),则为(甲)。 3. 在其他角色攻击速度下再测一次 anim_start→anim_notify 的间隔。若间隔随攻击速度变化则为(甲),不变则为(乙)。

建议处置:若确认为(乙),把通知改回 0.150,或把规格更新为 0.220(取意图所在的一边)。若确认为(甲),检查 play rate 应用逻辑。

这次的输出值得采纳。它没有把推测当作定论,而是分成了(甲)/(乙)两条分支,并把用数据区分每条分支的方法该向谁请求什么以行动为单位写了出来。尤其是第 3 项确认(改变攻击速度后重新测量)是人容易遗漏的决定性分歧点。把这份报告原样带进会议,会议就不再是争论「原因是什么」的场合,而成了「在 30 分钟内确定是(甲)还是(乙)并选定处置」的场合。

这份实操记录展现的核心,是 AI 并不会一开始就给出正确答案这一事实。第一次输出是把网络列为首位的踩空。只有当读日志、排除候选的人工验证介入,分析才终于收敛到正确答案。AI 把候选铺得很宽,人来收窄。这一分工就是整个 4.4 的方法论。


4.4.6 把两条轴线合成一个循环 —— 验证循环

模拟(4.4.2\~4.4.3)与捕获分析(4.4.4\~4.4.5)若各跑各的,只值一半。当合为一体时,就形成了下面的循环。

flowchart TD A["撰写规格<br/>(策划,4.2·4.3 格式)"] --> B["模拟<br/>simulate_dps · 连招枚举<br/>(自动,1 秒)"] B -->|"DPS·连招异常"| A B -->|"规格通过"| C["构建<br/>(程序员·美术,数日)"] C --> D["收集 telemetry 日志<br/>(input·hit·anim_notify)"] D --> E["规格 vs 测量自动对照<br/>(检出如 150ms vs 220ms 之差)"] E -->|"超出阈值的项"| F["AI 原因推测报告<br/>(铺开候选 → 由人收窄)"] F --> G["策划检查 → 决定处置<br/>(更新规格 vs 修改构建)"] G --> A E -->|"全部项一致"| H["进入下一个技能"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class B,E code; class F ai; class A,G human; class D data; class H pass;

由于模拟在构建之前给出答案,进入构建的规格已经被过滤过一次。所以构建之后发现的问题,性质就从「规格错了」收窄为「规格与实现相左」。4.4.5 的 +70ms 正是后者——规格 150 是合理的,只是实现偏成了 220。这一区分消除了会议上的责任归属之争。

不过这个循环并不能覆盖所有战斗内容。像主 Boss 的招牌演出这种感觉即内容的领域,无法被还原成 DPS 数字。模拟擅长基于数值的内容,而演出依然得靠人眼的录像评审来定夺。循环在数值之河里打转,演出之河则另自流淌。


4.4.7 在六个月运营中看到的

这是笔者运营过的某款 MMORPG(以 refgame 系列操作手感为目标的项目A)战斗 TF 六个月的测量。下列数值是从 TF 内部记录中摘出的实测值,并说明:构建数、时间是以循环为单位四舍五入后的值(不是精确到分钟,而是以循环、半天为单位的测量)。

项目 引入前 引入后
新技能验证循环 平均 3\~4 个构建 平均 1\~2 个构建
100 个技能的构建验证 半天(手动) 30 分钟(telemetry 自动对照)
数值会议 2 小时(主观讨论) 30 分钟(基于数据)
构建前夕发现的缺陷 平均 5\~8 件/构建 平均 1\~2 件/构建

比数字更重要的变化,是会议的性质。引入前,会议有一半是「这个技能太强了」对「不,正合适」。指尖对指尖的较量。引入后,那个位置变成了「测量 DPS 比目标高 +12%,测量命中时机比规格高 +70ms。是缩短动作,还是把伤害降 10%?」从争论问题是什么的时间,转变为决定怎么修的时间。

这一转变的代价也诚实地写明。把 telemetry logger 植入战斗代码全局的初期工作约 1\~2 周,把规格 schema 统一到全部角色与技能上又额外耗了一个季度。第一个季度里,我认为模拟与 telemetry 两者只要有一个跑起来就够了。两者扣成一个循环,是从第二个季度才开始的。


4.4.8 常见错误与规避

有五种反复出现。

第一,绝对信任模拟值。simulate_dps 的 261 是上限,不是实测。不与 telemetry 比较的话,就总会高估。

第二,不做测量。只对规格做模拟、却不用 telemetry 看构建,4.4.5 那样的 +70ms 就会悄悄累积。telemetry 日志记录不是选项,而是战斗代码的基础设施。

第三,不把报告带进会议。即使自动报告已经摆在那儿,只要它不在会议议程上,就没人会看。把「本次构建 telemetry 对照表」作为议程的固定项纳入。

第四,不经验证就采纳 AI 的推测。正如 4.4.5 所见,AI 的第一次推测是踩空的。AI 是拓宽候选的工具,不是下结论的工具。绝不跳过「用日志排除候选」这道由人来做的步骤。

第五,每个技能的规格结构都不一样。如果某个技能把 cast_sec 写成 cast_time、另一个写成 castMs,那模拟器也好、对照脚本也好,每次都会崩。把一份共同的规格 schema 强制到所有技能上——这是 4.4 整套工具得以运转的前提。

第一个季度不必把五个全都搞定。哪怕只立住一两个,循环也会明显变短。其余的会在循环转动中自然补上。


4.4.9 Part 4 收尾

4.1\~4.4 依次讲了战斗策划的坐标、Look & Feel、连招、模拟。4.1 讲战斗策划把什么视作可测量的对象,4.2 讲如何测量与调整命中时机、顿帧、特效同步,4.3 讲如何把连招、取消、输入队列写成状态机,而 4.4 讲这一切规格是否按意图运作、如何在不依赖构建/在构建中加以验证。

读完 Part 4 的战斗策划,一周会这样改变。周一,新技能规格上挂载 simulate_dps 自动验证。周二,用连招路径自动枚举检查主连招的身份。周三,构建提交后 telemetry 对照表自动弹出。周四,基于数据的讨论 30 分钟。周五,修订下个循环的规格。构建循环从 3\~4 次减到 1\~2 次,会议缩短到一半以下。打击感这一抽象之物,转移到了可测量的 220ms。

而构建 #234 的那一幕——对于「这个是不是有点延迟?」这个问题,如今由 telemetry 日志替我们回答说「是 220ms」。指尖与纸面的较量,结束了。

下一个 Part 5 是叙事策划。它将进入 2.3 中介绍的 NarrativeDocs Layer 0\~4 结构的正式应用案例。


动手试试

setup. 1. 请把下面的 simulate_dps.py 整段原样写出来。无依赖,用 python simulate_dps.py 即可立即运行,并复现 4.4.2 的「平均 DPS: 261.0」。

# simulate_dps.py —— 不依赖构建、仅凭规格计算 DPS
from dataclasses import dataclass

@dataclass
class Skill:
    name: str
    damage: float          # 单次命中伤害
    cast_sec: float        # 施法(动作占用)时间(秒)
    cooldown_sec: float    # 再次使用的冷却时间(秒)
    resource_cost: float   # 资源消耗(MP/气力)

@dataclass
class Character:
    name: str
    max_resource: float
    resource_regen: float  # 每秒资源恢复
    skills: list           # list[Skill]
    rotation: list         # 优先级顺序(技能名)

def simulate_dps(char: Character, duration_sec: float, tick=0.05):
    cooldowns = {s.name: 0.0 for s in char.skills}   # 剩余冷却
    skill_by_name = {s.name: s for s in char.skills}
    resource = char.max_resource
    total_damage = 0.0
    busy_until = 0.0          # 施法动作结束的时刻
    log = []
    t = 0.0
    while t < duration_sec:
        resource = min(char.max_resource, resource + char.resource_regen * tick)
        for name in cooldowns:
            cooldowns[name] = max(0.0, cooldowns[name] - tick)
        if t >= busy_until:   # 若不在施法中则选取下一个技能
            for name in char.rotation:          # 按优先级顺序
                s = skill_by_name[name]
                if cooldowns[name] <= 0 and resource >= s.resource_cost:
                    total_damage += s.damage
                    resource -= s.resource_cost
                    cooldowns[name] = s.cooldown_sec
                    busy_until = t + s.cast_sec
                    log.append((round(t, 2), name, resource))
                    break
        t += tick
    return total_damage / duration_sec, log

if __name__ == "__main__":
    warrior = Character(
        name="warrior", max_resource=100, resource_regen=8,
        skills=[
            Skill("skill_thunder", damage=420, cast_sec=0.9, cooldown_sec=6, resource_cost=40),
            Skill("skill_dash",    damage=180, cast_sec=0.4, cooldown_sec=3, resource_cost=20),
            Skill("basic_1",       damage=60,  cast_sec=0.3, cooldown_sec=0, resource_cost=0),
        ],
        rotation=["skill_thunder", "skill_dash", "basic_1"],
    )
    dps, log = simulate_dps(warrior, duration_sec=20)
    print(f"平均 DPS: {dps:.1f}")
    for t, name, res in log[:8]:
        print(f"  t={t:>5}s  {name:<14} 资源={res:.0f}")
  1. 请在战斗引擎代码的输入接收点、伤害应用点各加一行 telemetry 日志(4.4.4)。把引擎时刻(GetTimeSeconds 等)原样记下来是关键。

prompt. 请从构建 telemetry 中挑一个与规格相左的项,按 4.4.5 的格式向 AI 提问——贴上日志摘录,并务必包含「不要把推测当定论,要写明各原因的验证方法,不确定就标为未确认」。

verify. 不要原样采纳 AI 的第一次输出。请亲自读日志,用手抹掉可排除的候选(像 4.4.5 排除网络、帧率那样),附上依据后再次请求。当最终报告以「原因推测 + 向谁请求什么」的行动为单位收尾时,再把它带进会议。

单人精简版。 如果全局安装 telemetry logger 有负担,就只在你要验证的那一个技能上打 input、hit 两行。simulate_dps 也只看那一个技能的 DPS。不要铺开整套工具,先用最可疑的一个技能把循环转一圈,再扩展。


本章要点

5.1 NarrativeDocs Layer 0\~4 结构

走进会议室,白板上有一个角色名字被红笔圈了起来。是个叫金某的 NPC。在一位策划的支线任务里,他是"在主角小时候收养并把他养大的养父";而在另一位策划的主线任务第 3 章里,他却是"背叛主角后离去的旧日同伴"。两份文档都在一个月前通过了审批,两者都已进入了构建。连配音录制的报价都拿到了。

谁都没有错。两位策划都读过世界观文档,也都参考了角色设定。问题在于,同一个角色的设定散落在三个不同的文件里,而其中哪一个才是"真的",谁也无法断言。世界观文档是一整块 70 页的 Word 文件,一搜索,金某出现在十一个地方。哪一行是决定、哪一行是备注,没有任何区分。

那天会议结束后定下来的事情,就是把 NarrativeDocs 拆成五层。本章讲的就是这五层的故事。


5.1.1 为什么从最抽象的领域开始

把按领域的 Layer 分解先从叙事开始,是有理由的。

叙事最抽象。世界观、情感、基调这类东西落不到数字上。它不像美术或系统那样有"精灵图数量""伤害系数"这种明确的单位。如果连这样抽象的领域都能被干净地分解为 Layer,那么更具体的其他领域自然会用同样的模式解开。这相当于先看难的地方能不能走通。

再者,叙事的接口最多。角色与美术相接,任务与内容、关卡相接,台词与 UX、本地化相接,奖励与系统相接。它处在横跨领域最多的位置,所以 Layer 统合的价值会立刻显现出来。

最后,自然语言产出物的占比最高。因此它也是 AI 辅助作用最大的领域。不过并非所有游戏都以叙事为中心。如果是休闲、街机类型,本章的深度可能会显得过头。即便如此,"把一整块文档拆成 Layer、收窄接口"这一骨架本身,可以原封不动地搬到任何领域。


5.1.2 五格抽屉

把 NarrativeDocs 分解为五层后的样子如下。愿景(L0)在上,构建·QA(L4)在下,连接各层之间的通道有意收得很窄。


L0 愿景 —— "这个世界是什么" world_premise · narrative_pillar · tone_manifesto (不变 · 约 4.5 页) 总监·主策

pillar · tone (变更时 L1 重新审查)

L1 系统 —— "如何运作" faction_system · reputation_model · dialogue_branching_rule · lore_consistency_rule 资深叙事

规则手册·分支策略 (受影响任务自动列表化)

L2 内容 —— "发生了什么" main_quest · side_quest · character_bible · lore_codex (最厚的一层) 多名策划

quest_id · npc_id · dialogue_id (仅一列)

L3 数据 —— "机器读取的形态" quest_table · npc_table · dialogue_id_table · reward_table (自然语言 0 行) 叙事+数据

表格变更时自动触发 lint

L4 构建·QA —— "能否上线" narrative_qa_checklist · voice_review_log · localization_status 叙事+QA

L0 每次都作为上下文注入 (生成·验收的锚点)

落到文件夹上就是下面这个形态。每个文件名都不是书中抽象出来的名字,而是实际存在于那个文件夹中的文件名。

NarrativeDocs/
├── Layer0_Vision/
│   ├── world_premise.md          (世界观前提 —— 不变)
│   ├── narrative_pillar.md       (情感支柱 3 根)
│   └── tone_manifesto.md         (基调·禁忌用语清单)
├── Layer1_System/
│   ├── faction_system.md
│   ├── reputation_model.md
│   ├── dialogue_branching_rule.md
│   └── lore_consistency_rule.md
├── Layer2_Content/
│   ├── main_quest/               (以章为单位)
│   ├── side_quest/
│   ├── character_bible/
│   └── lore_codex/
├── Layer3_Data/
│   ├── quest_table.xlsx
│   ├── npc_table.xlsx
│   ├── dialogue_id_table.xlsx
│   └── reward_table.xlsx
└── Layer4_Build_QA/
    ├── narrative_qa_checklist.md
    ├── voice_review_log.md
    └── localization_status.md

重要的一点是,这五层不由一个人负责。每一层的主负责人各不相同,只把相邻两层之间的通道标准化。上图中的红色箭头就是那条通道。尤其是 L2 与 L3 之间的通道,故意画得最窄(粗红箭头),其中的理由后面再看。

用抽屉来比喻,就是一个五格抽屉。第一格里放绝不挪动的世界观一行,第二格里放规则手册,第三格里放正文,第四格里放表格,第五格里放验收日志。格与格之间的通道很窄,通道之上挂着变更通知的铃铛。


5.1.3 L0 —— 为了让它不变而写得尽量小

L0 不会变。它一变,游戏的身份认同就变了。所以篇幅必须小。小才不会变。

文档 篇幅
world_premise.md A4 1.5 页
narrative_pillar.md A4 1 页 (情感 3 个)
tone_manifesto.md A4 2 页 (基调 + 禁忌用语清单)

合起来约 4.5 页。这就是 L0 的分量。一旦变重,人们就会害怕变更;一害怕,其他层就开始绕过 L0。绕行一旦开始,L0 就成了一份死文档。

narrative_pillar.md 的实际骨架是下面这个样子(内容已抽象化)。

---
title: 叙事情感支柱
layer: L0
status: locked
last_updated: 2026-05-18
---

## 1. 对失去之物的怀念
- 玩家在每一章结尾都会失去一样东西。
- 失去的东西不会再回来 (只能通过回想)。

## 2. 义务与自由的冲突
- 所有主要 NPC 都背负着两种义务。
- 玩家的选择只能保全其中一种义务。

## 3. 微小举动的分量
- 比起宏大的英雄行为,微小的善意会带来更大的结果。

这三行支柱,决定了它下面数百页的方向。status: locked 不只是一个普通标签。L4 的自动检查之一会读取这个标签,使得 locked 文档在 PR 中被修改时,若没有主叙事的批准就无法合并。


5.1.4 L1 —— 与代码最接近的叙事

L1 可以变更,但成本很大。因为它是规则手册。规则手册改一行,所有遵循该规则的内容都会受到影响。

faction_system.md 的骨架如下。

---
title: 势力系统
layer: L1
atoms:
  - faction_relation_matrix
  - faction_membership_rule
  - faction_quest_eligibility
---

## 1. 势力概念
N 个势力。每个势力由 (理念、资源、领土) 定义。

## 2. 势力之间的关系
- relation_matrix.json (-3 敌对 ~ +3 同盟)
- 关系变化触发条件: 主线任务决定、声望临界

## 3. 玩家归属规则
- 同时归属最多 2 个 (敌对关系不可同时归属)
- 退出惩罚: 声望 -2,同盟势力 -1

请留意 frontmatter 的 atoms: 列表。这三个 atom 名称不是普通的备注,而是第 7 部分本体论与第 11 部分关系图所追踪的标识符。当某个任务引用了 faction_quest_eligibility,这条规则一旦变更,该任务就会自动出现在受影响列表里。L1 是与游戏代码最接近的叙事产出物,所以要与系统策划结对作业。


5.1.5 L2 —— 最厚的一层,正文居住的地方

L2 最厚。主线任务、支线任务、角色圣经、传说辞典全都住在这里。先前在会议室里冲突过的"养父 vs 背叛的同伴"金某的真正设定,如今也只以 character_bible/ 中的一个文件存在。那个文件是单一真相来源,任务只引用它。

主线任务文件夹是下面这个形态。

main_quest/
├── chapter_01_awakening/
│   ├── 00_chapter_overview.md
│   ├── 01_quest_a_call_to_arms.md
│   ├── 02_quest_b_first_choice.md
│   └── ...
├── chapter_02_road/
│   └── ...
└── _TEMPLATES/
    └── quest_template.md

每个任务文件都遵循 atom 标准格式。

---
title: 拿起武器时
layer: L2
type: main_quest
atoms:
  - quest_chapter_01_awakening_a
related:
  affects: [reputation_model, faction_relation_matrix]
  derives_from: [narrative_pillar, world_premise]
  requires: [character_kim, faction_alpha]
  part_of: chapter_01_awakening
---

## 进行阶段
1. ...

## 分支
- 选择 A 方案时: ...
- 选择 B 方案时: ...

## 奖励 (参见 L3)
- reward_table.xlsx → quest_001 行

核心是 related: 块。在写下 requires: [character_kim] 的那一刻,这个任务就声明了它从 character_bible 取用金某的设定,不再在自己的文件里重新定义金某。叙事正文放在 L2,数值奖励放在 L3 表格。两者若放在同一个文件里,每改一行表格都得动到正文,而那样一来翻译键就会错位。


5.1.6 L3 —— 没有一行自然语言的层

L3 是表格和 ID。一行自然语言句子都进不来。

quest_table.xlsx
| quest_id | chapter | type | unlock_level | reward_xp | reward_gold | dialogue_set_id |
|----------|---------|------|--------------|-----------|-------------|-----------------|
| q_001    | ch01    | main | 1            | 500       | 100         | ds_001          |
| q_002    | ch01    | main | 2            | 800       | 150         | ds_002          |

连台词也只用 ID 来引用。正文另放在 dialogue_id_table.xlsx 里,与翻译键 1:1 映射。连接 L2 与 L3 的通道,只需 quest_id 一列就够了。前图中唯独这条通道是粗红箭头,原因正在于此。把接口收窄到一列,是 Layer 分离的核心。通道一宽,两侧就会对彼此知道得太多,改动一侧时另一侧就会跟着崩坏。


5.1.7 L4 —— 出货关卡

L4 是验收与出货。每当新内容进来,自动·人工检查就会启动。自动检查由脚本来跑。下面这四个是实际挂在 CI 上的 lint。

检查 工具
所有 dialogue_id 映射存在 dialogue_lint.py
所有 quest_id 都属于某个 chapter quest_lint.py
奖励合计在各章曲线范围之内 reward_curve_check.py
禁忌用语是否出现 tone_lint.py (基于 L0 tone_manifesto)

请注意 tone_lint.py 直接读取 L0 的 tone_manifesto.md 这一点。最上层(不变的愿景)与最下层(出货关卡)通过自动化直接连接在了一起。愿景里写下的禁忌用语,若在上线前夕从正文中被检出,构建就会被拦下。

自动抓不到的东西,由人来看。

检查 负责
与 L0 情感支柱契合 主叙事
角色配音一致性 角色负责人 + 叙事
本地化可行性 本地化人员

自动与人工的边界一旦清晰,验收时间就会变得可预测。"这一章的验收要几天?"也就能答得上来了。


5.1.8 让 AI 来做 Layer —— 实操记录

到这里为止是结构。然而这个结构真正发挥作用的地方,是在制作新内容时。在一个已经铺好五层的项目里,接到一条支线任务的一句话想法,把它撒进各 Layer 的工作,我让 Claude 做了一遍,这里原样搬出那次真实的会话(实操记录,worked transcript —— 完整保留的真实操作过程记录)。不做摘要,连偏掉的部分和被拒绝的部分都一并收录。

发出的提示词(全文):

绝对不要违反附上的 L0/L1 上下文,按我们的 Layer 结构来分解。

[narrative_pillar.md 全文 —— 情感支柱 3 个]
[tone_manifesto.md 节选 —— 禁忌用语: "命运"、"预言"、"被选中的人"]
[character_bible/character_kim.md 节选 —— 金: 收养主角的养父,
 寡言,话尾含糊,不直接说出情感]

想法: "金把亡妻的遗物交给玩家的一段简短支线任务。"

L2 任务文件(含 related 块,正文 200 字左右)、L3 quest_table 行和
dialogue_id,还有 tone 违规的自我检查。先只写 L2 然后停下。

Claude 的原始输出(L2):

---
title: 褪色的手帕
layer: L2
type: side_quest
atoms:
  - quest_side_kim_keepsake
related:
  affects: [reputation_model]
  derives_from: [narrative_pillar]
  requires: [character_kim]
  part_of: chapter_02_road
---

## 进行阶段
1. 金悄悄把玩家叫过来。不像平常那样直接说出来意。
2. 递出一块旧手帕。"这个……还是你拿着比较好。"
3. 玩家若问那是什么,金不作答,转头望向窗外。
4. (选择) 接下 / 婉拒。

## 分支
- 接下: 金的声望 +1。之后的章节中解锁 1 次与手帕相关的回想。
- 婉拒: 金点点头,把手帕重新收起。声望无变化。
  手帕永远不再出现。

## 奖励 (参见 L3)
- reward_table.xlsx → quest_side_kim_keepsake 行

到这里 Claude 停了下来,并补上了一行。

"为了体现 L0 支柱第 1 条(失去的东西不会再回来),我让选择'婉拒'时手帕永远消失。不过有一点需要确认。亡妻的设定在 character_kim.md 节选里并不存在。这是我擅自加进去的,可能会与角色圣经冲突。"

人的验证与拒绝:

这段输出的基调很准。金的寡言、话尾含糊、不直接说出情感的习惯,都活了下来。也没有禁忌用语。然而最后那条自我检查恰恰点中了真正的问题。金有一位亡妻的设定,哪里都没有。AI 从"遗物"这个词自然地推断出了已故的配偶,但这是在角色圣经里偷偷加入新事实的行为。这正是本章开头会议室里那起事故的种子。

所以我拒绝了。发回了重新请求。

重新请求:

基调不错。但"亡妻"是圣经里没有的新设定,不能用。
金的过去只确定到"离开故乡时留下了点东西"为止。不要指定
遗物的具体身份,只含糊地改成"很久以前留下的东西"之类。
手帕也是圣经里没有的,所以降到"一块布片"之类。

重新请求后的输出(节选):

2. 递出一块旧布片。"很久以前……我留下了一样东西。
   这个就当是它的一角吧。"
3. 玩家若问那是什么,金不作答,转头望向窗外。

这次就接受了。既没有把金的过去重新钉死,又触及了情感支柱第 1 条(失去)。失去的究竟是什么,直到最后都含糊地留着。

这一次会话呈现出来的有两点。第一,只要把 Layer 上下文(L0 支柱 + L1 基调 + L2 圣经)准确注入,AI 就能把基调对得惊人地好。第二,AI 一遇到空白就会煞有介事地填满。当那个填补是圣经里没有的新事实时,抓住它的依然是人。Layer 结构的 requires: [character_kim] 告诉了我们"该去看哪里",所以验证者立刻就知道该和哪个文件对照。要是没有结构,就得把 70 页重新翻一遍。


5.1.9 窄通道之上的通知铃

把五层分开的真正理由,是为了收窄接口。而每一处窄接口都贴上变更检测的自动化。

接口 流动的是什么
L0 → L1 pillar、tone (变更时触发 L1 规则手册重新审查)
L1 → L2 规则手册·分支策略 (变更时受影响任务自动列表化)
L2 → L3 quest_id、npc_id、dialogue_id (仅一列)
L3 → L4 表格变更时自动触发 lint

例如在 L1 的 faction_system.md 中,把"同时归属最多 2 个"改成"最多 1 个"的 PR 提上去时,关系图会扫过引用了 faction_quest_eligibility 的 L2 任务,生成一份受影响列表,并把这份列表自动附加到 PR 评论里。改规则的人不必"约两三次会去查谁受影响",看着评论里附的列表,开个 30 分钟的会就结束了。

核心是这一点。如果只分 Layer 而接口含糊,那就只是隔板多了几块而已。比起 Layer 分离本身,接口的自动化才是本质。


5.1.10 运营 6 个月,改变了什么

迁移到五层、运行 6 个月之后的测量结果。下面的数值基于作者团队的运营记录,但绝对值只以方向·比率来呈现(作者估算·未经验证)。分离前是凭记忆,分离后是实测,因此并非把同一份资料量了两次,这是一个局限。

项目 Layer 分离前 Layer 分离后
新策划入职上手 3 周 1 周
规则手册变更影响范围掌握 开会 2\~3 次 自动评论 + 会议 30 分钟
制作一章新主线任务 4 周 2.5 周
上线前一章验收 5 天 2 天
本地化遗漏事故 每季度 3\~5 件 每季度 0\~1 件

减少得最明显的是本地化遗漏。随着 dialogue_id 在 L3 与翻译键 1:1 绑定,dialogue_lint.py 拦下映射遗漏,"未翻译的台词进入构建"的事故几乎消失了。上手变短也很关键。对新策划可以说"只背 L0 的 4.5 页,你的任务就照 L2 模板填",而不是"把 70 页 Word 全读一遍"。

老实补一句,这个效果并非一蹴而就。第一个季度只贴了一项自动评论,其余都是手工。接口自动化是每个季度增加一项。比起分 Layer,贴自动化花的时间更长。


5.1.11 更深的理由 —— 通往程序化生成的路

到这里为止是表面的理由。"统一各领域之间的协作语言。"然而还有一个更本质的理由。Layer 分解是使程序化生成成为可能的前提。五层各自对应程序化生成中的一个角色(L0 锚点 → L1 规则手册 → L2 正文 → L3 数值 → L4 关卡),而一旦混成一团,生成器就无法确定从哪里读、写到哪里去而崩溃——这一普遍命题在 §6.6 已整体讨论过。这里只看这个前提在叙事五层之上实际是如何运作的。

在一整块 70 页文档之上,生成算法无法确定从哪里读、写到哪里去。叙事五层正好成了生成流水线的五个阶段——L0 锚点、L1 输入规则、L2 正文堆叠的位置、L3 模拟输入、L4 验证关卡。

前面的实操记录其实已经是这五个阶段的精简版了。把 L0 支柱和 L1 基调作为上下文注入(锚点),在 L2 生成了正文,L3 行随之产出,tone 违规的自我检查模仿了验证关卡。把人一次只让做一个任务的事情,在同样的结构上换成让 generator 量产支线任务,就成了程序化生成。

往更远走,就会这样流动——玩家行为累积 → 世界 BT 节点状态变化(Squad 上层) → NPC 数值变化(声望·关注点·优先级) → NPC 标签+数值即为触发条件 → 在任务云中匹配的任务被触发。任务不是预先全写好,而是带着标签像"悬在空中的云"一样放着,玩家的行为一旦改变 NPC 数值,与触发条件相符的任务就会落下来出现。这个进阶模型会在 5.3 再详细讨论(含流程图)。这里要强调的一点是,所有这些流动都只在五层分解之上才能运作。

反过来,Layer 混在一起的团队走不到程序化生成。一尝试,就会因一致性事故而崩溃。想象一下金某既是养父又是叛徒的那起事故,通过生成器被自动量产出来的样子就明白了。

不过这并不意味着一开始就要完美备齐五格抽屉。第一个季度只分离 L0 一行和 L1 规则手册一本也就够了。分离要循序渐进,接口要收窄。

最后关于时点的一点。Layer 分解本身,是从确定性 PCG(Procedural Content Generation,程序化内容生成)时代就有的分离。新出现的是 LLM 在那个分离之上,把自然语言正文、人设、叙事分支也处理了起来——上面的记录里 AI 配合金的基调写出台词这件事,用 5 年前的规则表是做不到的。这套时点论会在 5.3 再展开。

下一章(5.2)将看到,在这五层之上,lore_consistency_rule 如何自动验证世界观→角色→任务的一致性,也就是从结构上拦下金某事故的检查器。


动手试试 —— 第一次 Layer 分解

假设你从已经有一整块世界观文档的状态开始。

setup. 在 NarrativeDocs 文件夹下建立五个空文件夹。从 Layer0_VisionLayer4_Build_QA。把现有的一整块文档原样留着,从中只挑出"绝对不变的那一行",移到 Layer0_Vision/narrative_pillar.md。在 frontmatter 里输入 status: locked。让这一个文件不超过 4.5 页。

prompt. 制作新内容时,按这个顺序把上下文给 AI。

[narrative_pillar.md 全文]
[tone_manifesto.md 禁忌用语]
[相关 character_bible 文件节选]

想法: "<一句话想法>"

不要违反这个上下文,先从 L2 任务文件写起。填好 related 块
(requires/derives_from/affects),圣经里没有的新设定
不要加,需要的话就停下来询问。

verify. 在输出中要看两点。(1) 实际打开 requires: 里写的文件,对照 AI 有没有凭空造出那里没有的事实。(2) 在正文中搜索禁忌用语(有 tone_lint.py 就自动,没有就用眼睛)。两者只要有一项被命中就拒绝,并明确指出错在哪里后重新请求。上面记录里拒绝"亡妻"的,正是 (1)。


5.1.12 单人精简版

如果你是没有团队、独自制作的独立开发者,五层可能看起来过头。两格就够了。

放一张 vision.md(L0+L1 合并)、一个 content/ 文件夹(L2),以及一张电子表格(L3)。QA 不另设一层,而是用在 content 文件 frontmatter 里只写 requires: 一行的习惯来代替。每次写新任务时,只把 requires 里写的文件重新摊开对照,就能不翻遍 70 页也拦下金某事故。核心不在层的数量,而在"把绝对不变的那一行单独抽出来,每次都先给 AI 看"的习惯。那一行就是上下文锚点,无论是一个人还是中等规模的团队,都从那里开始。


本章要点

5.2 世界观 → 角色 → 任务 的一致性验证

临近 Beta,QA 提交了一份 Bug 报告。标题是"国王说话不用敬语"。正文很短:"在 3.4 的开场过场动画里,K_001(国王)对玩家说了'喂,等一下'。这个角色从 1.1 到 3.3 一直用的是'阁下'。"

我去问编剧,得到的答复出乎意料。"那句台词不是我写的。"追查之后才发现,是一位外包编剧在赶工填补一条过场动画分支时随手加进去的。我们的角色圣经(character bible)里本来就有 voice_profile,但那位外包编剧从没看过那份文档。规则在文档里,台词却从文档外进来了。

这就是一致性事故的本质。它不是因为没有规则,而是因为规则没能一路跟进到正文里。而如果这一句话出现在过场动画里,那就更可怕了。过场动画通常会配上配音录制。如果只是文本,改一次就完事;可一旦录完音才发现,就会带来重新召集配音演员、重新录音、重新混音这种不可逆的成本。一致性验证真正的目的,是"在录音之前"把问题揪出来。

本章讲的就是如何用规则手册和检查器、而不是人眼,来揪出这类事故的工作流。我们会原样看真实产出:lore_consistency_rule 规则手册如何成为检查器的输入,voice_lint 如何把语气波动挑成可疑候选,以及为什么最终判定无论如何都要留在人的位置上。


5.2.1 一致性事故是从哪里漏出去的

把上线 RPG·MMORPG 的用户评论汇总起来看,叙事一致性事故会收敛成几种固定模式。种类看似不同,原因却几乎只有一个。

这五种看上去像是五种不同的事故,但追查下去,它们都从同一个地方漏出去。是 Layer 0(世界前提)或 Layer 1(规则)变了,而这个变更没有传播到 Layer 2(正文)和 Layer 3(数据表)。规则更新了,正文却还停留在旧规则之上。

想靠人工评审来堵住它是不现实的。一个章节里牵连着 50 名 NPC、2,000 行台词、30 个任务,当你改动一行规则时,要让人去 100% 追踪它的影响波及到哪里,这是不可能的。漏掉的那一行在评审阶段不会被抓到,会在上线后的评论区里被抓到。

可话说回来,自动检查也并不能保证 100%。关键在于职责分工。自动检查负责快速挑出可疑候选,判定由人来做。 自动化的目的是减少人工评审的时间,而不是取消人。一旦这个前提被模糊掉,后面要讲的所有失败都会接踵而至。


5.2.2 lore_consistency_rule —— 喂给检查器的规则手册

项目A 的 L1 文档之一就是 lore_consistency_rule.md。这份文档既是供人阅读的指南,同时也是检查器解析的输入。frontmatter 里的 atomsaffects 把这两个角色绑在了同一份文档上。

---
title: 世界设定(lore)一致性规则
layer: L1
atoms:
  - lore_check_world_rule
  - lore_check_character_voice
  - lore_check_timeline
  - lore_check_faction_relation
related:
  derives_from: [world_premise, narrative_pillar]
  affects: [main_quest/*, character_bible/*, dialogue_id_table]
---

## 1. 世界规则(World Rule)
- 起始状态下魔法被禁止 → 使用魔法时需注明(时点、使用者、正当理由)
- 神处于沉默状态 → 禁止描写其直接回应(允许梦境·幻象)

## 2. 角色语气规则
- 强制参照每个角色的 voice_profile
- 撰写新台词时须遵守 voice_profile 的 5 个项目(词汇、句子长度、敬称、情感表达、禁忌表达)

## 3. 时间线规则
- 所有 NPC 都要定义 status_timeline(存活 / 受伤 / 死亡 / 失踪 / 位置变更)
- 在台词·登场时点自动核查 status_timeline

## 4. 势力关系规则
- 记录 faction_relation_matrix 的变更时点
- 变更之后的台词要反映新的关系

affects 这一行定义了检查器的扫描范围。一旦 world_premise 发生变化,检查器就会把 main_quest/*character_bible/*dialogue_id_table 全部重新过一遍。过去要靠人在脑子里追踪"影响会波及到哪里?"的工作,现在由写在规则手册里的依赖关系图来代替。

voice_profile 是这份规则手册所引用的另一份 L2 资产。一个角色的画像,其各个项目都以数值化·枚举型的方式录入,以便检查器拿来作为比较基准。

# character_bible/K_001_voice_profile.yaml
character_id: K_001
display_name: 国王
voice_profile:
  vocabulary_register: 古风_正式         # 词汇等级
  avg_sentence_len: 18                   # 平均句子长度(字)
  honorific: "阁下"                      # 第二人称敬称(固定)
  emotion_expression: 克制               # 情感外露程度
  forbidden_terms: ["喂", "等一下", "哈"] # 禁忌表达

有了这份 yaml,"国王不用敬语"这件事才会从人的直觉变成机器可比较的项目。honorific 是"阁下",台词里却出现了"喂",那它就不是意见,而是规则违反的候选。


5.2.3 一致性验证流程

变更发生的那一刻,检查器就被触发。流程如下。

flowchart TD A[变更发生: L0 前提 / L1 规则 / L2 正文] --> B{变更分类器} B -->|命中了哪条规则的 affects| C[调用对应检查器] C --> D[扫描 affects 范围内的 L2 正文 + L3 表] D --> E[规则违反/可疑候选列表] E --> F[自动把评论附加到变更请求上] F --> G{人工判定} G -->|确为违反| H[修改正文 → 重新检查] G -->|有意为之的变化| I[更新 voice_profile / 规则手册] G -->|规则过于敏感| J[调整规则本身] H --> K{是否处于文本阶段} I --> K J --> K K -->|是: 可逆| L[可结束评审] K -->|否: 已录音| M[不可逆 —— 重新录音成本] L -.阻断线.-> M classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class B,C,D,F code; class G human; class A,E data; class L pass; class M fail;

最后那个分支才是本章隐藏的脊柱。所有一致性判定都必须在文本阶段、也就是可逆阶段就结束。一旦评审推到录音·选角之后,修改就变成不可逆的了。所以 voice_lint·timeline_lint 这类检查器,关键不在于跑得"快",而在于跑得"早"。过场动画台词在进入录音队列之前,至少要通过一次。

检查器共有四种,各自与规则手册的一个章节一一对应。

四个检查器都不是 100% 准确。所以输出的名字不叫"违反",而叫"可疑候选"。


5.2.4 实操记录(worked transcript):把 voice_lint 跑一遍

光说"有检查器"这种抽象的话,没什么感觉。我们实际跑一遍。下面是复现前面"国王不用敬语"事故的输入。

setup —— 从 dialogue_id_table 取出两行待检查台词。

dialogue_id_204  speaker=K_001  text="喂,等一下啊。你先说。"
dialogue_id_217  speaker=K_007  text="……该死,又失败了。从头再来。"

prompt —— 检查器本体是基于规则跑的(正则·长度比较·禁忌词匹配)。AI 只接在"为什么可疑"这一处——用编剧能读懂的自然语言来解释。检查阶段的 LLM 提示词全文如下。

这些是 voice_lint 挑出来的可疑候选。不要下判定,只针对每个候选,
用 1~2 句话说明它似乎违反了哪个 voice_profile 项目以及依据。
不要断言"违反了/没违反",只说到"疑似……"为止。

[voice_profile]
K_001: honorific="阁下", avg_sentence_len=18, forbidden=["喂","等一下"]
K_007: forbidden=["该死"]

[可疑候选]
- dialogue_id_204 (K_001): "喂,等一下啊。你先说。"
- dialogue_id_217 (K_007): "……该死,又失败了。从头再来。"

Claude 原始输出(不作概括·整理,原样照录)

[dialogue_id_204 / K_001]
这句台词疑似与 honorific 项目冲突。voice_profile 将第二人称敬称
固定为"阁下",而本句使用了"你",敬称本身并未体现出来。此外,
forbidden_terms 中的"喂"和"等一下"都出现了,疑似偏离了词汇等级
(古风_正式)。与平均句子长度 18 字这一基准看上去没有直接抵触,
但语气本身变得随意,这一点疑似是违反信号。

[dialogue_id_217 / K_007]
forbidden_terms 中的"该死"被原样使用,疑似违反禁忌表达项目。
不过由于 K_007(学者)的 emotion_expression 项目未给出,仅凭
现有信息难以判断这一表达是有意的情绪高涨还是单纯的越界。
判定需要编剧来审查。

这段输出里有我们故意留下的两处诚实。第一,在 204 上 AI 说"平均句子长度看上去没有直接抵触",没有把不违反的项目硬说成违反。第二,在 217 上 AI 承认"emotion_expression 项目未给出,难以判断",承认信息不足并把判定交给了人。如果 AI 把所有可疑都强推成"确定违反",那才是更危险的检查器。

verify —— 编剧在变更请求里原样收到这条评论。判定由编剧来做。

两个候选是同一个检查器挑出来的,结局却完全相反。一个是改正文,一个是改规则。机器无法自动完成这个分叉,正是下一节的要点。


5.2.5 为什么判定是人的位置

检查器只挑到可疑为止、把判定交出去,有三个理由。

第一,存在有意为之的违反。 在角色崩溃或转变的章节里,语气会被有意地动摇。上面的 217 就是如此。自动拒绝型的检查器会把编剧的演出意图给挡掉。

第二,规则本身在进化。 如果同一类可疑总是被判定为"有意为之的变化",那就是规则跟不上现实的信号。检查结果不只是让人去改正文,也让人去改规则手册。

第三,新角色·新势力需要一个学习区间。 voice_profile 才填了两三个项目的新 NPC,冒出很多可疑是正常的。在这个时期如果上了自动拒绝,编剧就会把检查器当成敌人。

自动检查和人工判定的边界足够清晰,检查器才能活下来。要是做成自动拒绝型,一个月之内编剧们就会说"把这玩意儿关了吧"。这就像给公司大门装一个太过灵敏的自动感应器,每次有人经过门就关上,最后总会有人把感应器拆掉。检查器不该是关门的装置,而该是"有人从这里经过了"的提示装置。

再补充一个注脚。评审必须在文本阶段结束这条原则(前面流程图里的阻断线),同样适用于人工判定。编剧的"有意为之的违反"判定也得在录音之前完成。录音之后再翻案,问题就不再是检查器的问题,而变成了工序成本的问题(可逆/不可逆边界的全貌见 5.4.5)。


5.2.6 测量 —— 6 个月前后

在项目A 中,我们分阶段引入了 4 种检查器,并测量了 6 个月。下面是基于实测日志,但用方向·比例代替绝对值来呈现的(公司内部测量,非作者推测)。

最后一项最有意思。有了检查器,规则就算频繁修改也很安全。改一行规则,它的影响会被自动可视化,于是变更的恐惧减少了,规则进化得更快了。一致性工具真正的效果,与其说是"减少了事故",不如说更接近于"能够无所畏惧地修改规则了"。

不过,上面这些数字是 4 种检查器全部运转时的数字。更重要的一点是,在引入初期,光是 voice_lint 一个就已经产生了可见的效果。没必要一开始就把 4 种全都打开。


5.2.7 把 AI 放在哪里

自动检查器的本体,基于规则更高效。同样的输入要得到同样的结果,信任才会积累,而 LLM 是非确定性的,不适合放在那个位置。AI 放进另外四个位置。

规则快速且确定,LLM 擅长说明与生成。把这两者的角色混在一起,两边都会坏掉。把检查交给 LLM,就会出现同一句台词昨天通过、今天却被拦下的情况;把说明交给正则,就只能得到"honorific 项目违反"这种机器语。


5.2.8 引入顺序与常见失败

一开始就把 4 种检查器全做出来,负担会先于效果到来。推荐的顺序是从最便宜·效果最大的开始。

  1. voice_profile 5 项目标准化(约 1 个月)—— 先把 character_bible 的格式落地。这件事比检查器更优先
  2. voice_lint 最小版本(约 1 周)—— 只做禁忌词汇匹配。光是拦住一个词,上线后的社交媒体事故就能每季度减少 1\~2 件
  3. timeline_lint(1\~2 周)—— 死亡标记核查。哪怕只抓住死掉的 NPC 重新登场,体感也很大
  4. world_rule_lint + faction_lint(1\~2 个月)—— 剩下的两种
  5. LLM 辅助(再加 1\~2 个月)—— 整合说明·初稿生成

要强调的一点是,光是第 2 步(voice_lint)效果就已经很大。前面那个"国王不用敬语"的事故,正是靠这一步就能抓住的类型。

引入过程中反复出现的失败也几乎是固定的。

最后一项比前面所有项目都贵。别的失败丢的是时间,这个失败丢的是配音演员的档期。


下一章(5.3)讲的是不靠检查器,而靠 AI 辅助来撰写叙事正文的流程。我们会看到如何把 L0 语气和 L1 规则作为上下文注入,让 AI 给出的不是泛泛的答案,而是属于我们这个世界的答案。


本章要点

下一章预告


动手试试

setup —— 从 character_bible 里挑 1 名角色,用 yaml 把 voice_profile 的 5 个项目(词汇等级·平均句子长度·敬称·情感表达·禁忌表达)完整填满。再从 dialogue_id_table 里抽出这个角色已有的 10 行台词,汇集到一个文件里。

prompt —— 原样使用上面实操记录里的检查辅助提示词。核心是两条约束:"不要下判定"和"只说到疑似……为止"。在输入里附上 voice_profile yaml 和那 10 行台词。

verify —— 把输出的可疑候选逐行自己判定一遍。如果是真违反就改正文,如果是有意为之的变化就在 voice_profile 里加上例外标记。同时确认 AI 是否把"不违反的项目"也硬说成违反,是否承认"信息不足"。如果 AI 把所有项目都断言为违反,就强化提示词里"不要下判定"的约束。

单人精简版

如果你是既没有 4 种检查器、也没有规则手册的单人开发者,不要检查器本体,光靠一条提示词也能达到同样的效果。只需手动维护每个角色的 voice_profile yaml,每写一条新台词,就把该角色的 yaml + 新台词附到上面那条辅助提示词里,去拿到"可疑候选"。虽然没有自动化,但判定靠人、AI 负责说明这个核心结构照样活着。只要守住一条就行——在交给录音·语音合成之前,让这道审查走一遍。不越过可逆阶段这条原则,与团队规模无关。

5.3 AI 辅助叙事写作

那天我在为一个新的支线 NPC 草拟第一句台词。我在空白的聊天框里敲下"帮我写 5 句村庄铁匠 NPC 的台词"。5 秒后,屏幕上跳出"勇士啊,把你的武器交给我吧"。那不是一种似曾相识,而是一种我几乎能说出确切在哪儿见过的句子。把同样的提示词放进别的团队的别的游戏里,也会得到一模一样的回答。那一刻我意识到的并不是模型弱,而是我什么都没告诉模型——我没让它了解我们的游戏。

AI 很会写一般化的奇幻句子。可它写不出我们这个世界的句子。差别只有一个,就是上下文注入。每次请求时把 L0 基调和 L1 规则一并送进去,AI 吐出的那一行就会从"似曾相识的句子"变成"这个游戏的句子"。本章讲的是把这种注入按四层来运营的实务,最后把同一原理拔高到世界模拟的规模,把更进取的应用(世界 BT(BehaviorTree,行为树)+ 任务云)作为研发最前沿来点明。


5.3.1 上下文为空时会发生什么

在叙事领域,AI 辅助是导入最快、也最快失去信任的环节。因为它的失败模式几乎一模一样。

你扔一句"给我 5 句任务开场台词",回来的就是 5 种以"勇士啊,我们村子……"开头的一般化奇幻句。你说一句"帮我改改这个角色的台词",声音就被抹平,所有 NPC 都收敛成相似的语气。你说一句"帮我写第 1 章梗概",拿到的就是你见过的那些 RPG 梗概的平均值。

问题不在模型,而在上下文是空的。模型输出的是训练数据的平均值。不想要平均值,就得给它一个能远离平均值的线索。本章的主题就是这个线索怎么造、怎么注入。

在笔者运营的 MMORPG 项目(以下称项目A)里,叙事 AI 辅助会按顺序堆叠四层上下文。5.1 中把 NarrativeDocs 拆解为 Layer 0\~4 的那套结构,在这里被原样复用为注入单元。

Layer A · 系统提示词 作者人设 · 禁忌(几乎不变,定义一次)

Layer B · L0 愿景 world_premise · narrative_pillar · tone_manifesto (≈7,000 tok,缓存)

Layer C · L1 规则(择需注入) 只挑出与任务相关规则的 _summary 节(缓存)

Layer D · L2 邻近正文 同一角色的上一句台词 · 同一章梗概(原样不动,每次都变)

任务指令:"此时此刻 K_007 的台词 3 案"

四层并不是每次都全部塞进去。按任务类型只取出需要的层。如果是一个角色的下一句台词草稿,A + B(只要基调)+ D(那个角色最近的 10 行台词)就够了。如果是新支线任务的梗概,就追加 C(quest 结构规则)。如果是分支结果 4 案,C(分支规则)+ D(分支前的整段正文)就会变重。这就好比从书桌上的文件柜里,按任务大小挑出人设表、世界观一行、规则手册某页、邻近正文一沓,再发出去。


5.3.2 一段实操记录(worked transcript)—— K_007 的第一句情感台词

实操记录(worked transcript)= 完整保留的真实操作过程记录。

与其用抽象去讲,不如把一次真实请求从头跟到尾。这次的任务是为同一个角色(学者型 NPC,公司内部 ID K_007)第一次需要流露情感的场景,草拟 3 案台词。先从提示词全文开始。

发出的提示词(Layer A + B(基调)+ D + 任务指令 + 输出格式):

[系统]
你是项目A 的叙事作者。不要用"勇士""被选中者"这类 RPG 套话,
语气照着下面这几句上一段台词的基调走。不知道的设定别瞎编,
就回答"凭上下文无法判断"。你只写草稿,决定由我来做。

[L0 基调 —— tone_manifesto 摘录]
这个世界的情绪基调是克制。人物不会轻易把感情说出口。
情感不是爆发,而是以裂痕的方式显露。多用省略与短句。

[参考 —— K_007 的上 5 句台词(按时间顺序,原样不动)]
1. "……这事我也是头一回听说。"
2. "有记录吗?没有的话就得怀疑。"
3. "在确定之前,先别动。"
4. "我的意见就这些。"
5. "……抱歉。我是不是太冷淡了。"

[情境]
同伴 NPC 受伤归来。学者第一次必须流露情感。

只要 3 案,每案不超过 25 字。每案末尾加(反应类型:愤怒/悲伤/否认)标签,
说明和开场白都去掉。

Claude 的原始输出(未经摘要、未经改写,原样照录):

1. "……为什么一个人全扛下来。傻不傻。"(反应类型:悲伤)
2. "我该去看记录的。是我,是我漏掉了。"(反应类型:否认)
3. "没事。都没事。所以你别——"(反应类型:否认)

不在这里停下才是关键。作者读完三案并加以验证。1 案基调对了。上一段第 5 句"……抱歉"所显露的那道裂痕,被 1 案用"傻不傻"这一个词接住了。是采用候选。2 案"是我,是我漏掉了"里那种结巴,和学者的克制略有出入。保留。3 案标签错了。反复说"没事"更接近回避而不是否认,而我们的 voice_profile 里并没有学者用回避来处理情感的模式。否决。

重新请求(明确给出否决理由再发一次):

3 案否决。反复说"没事"不是这个学者的模式。他处理情感
不是回避,而是"对没能核实事实的自责"。1 案我采用。
2、3 案的位置,给我两案和 1 案不同调子的。不要结巴,不超过 25 字。

Claude 的第二次输出:

2. "记录不会说谎。是我没去看。"(反应类型:自责)
3. "……坐下。先处理伤口。话以后再说。"(反应类型:悲伤)

这次两案都通过了。2 案把"记录"这个学者的核心词汇(上一段第 2 句"有记录吗?")重新用作自责的媒介,3 案以克制的祈使句展现了学者把情感压成行动的模式。最终采用 1 案 + 2 案 + 3 案。这三行经过 5.2 的 voice_lint 自动验收后被反映进 L2 正文,并在 L3 中获发 dialogue_id

这一段实操记录里装着本章的全部。基调注入(L0)救活了 1 案,原样不动的邻近正文(L2)让学者的词汇"记录"在重新请求时被再利用,输出格式的强制堵住了闲话,作者否决关卡过滤掉了 3 案错误的标签。AI 没有最终决定哪怕一行。


5.3.3 Layer A —— 系统提示词,一行就左右全局

铺在最上面的是人设定义。定一次,几乎不再改。上面那段实操记录里的系统区块就是它的实物。五行里最后一行("写草稿,决定由作者来做")最重要。这行一缺,AI 就会自信地端出装作"最终版"的句子,而作者就从验收变成了打分。其次重要的是第三行("不知道的设定别编,就回答无法判断")。没有这行,模型就会用貌似合理的谎言去填空。在叙事里,貌似合理的谎言会在几天后以世界设定冲突的形式回来。


5.3.4 Layer B —— L0 愿景与缓存的位置

L0 篇幅不大(按 5.1 的标准约 4.5 章篇幅)。几乎每次都能整段注入。以韩文为准估算,world_premise.md 约 2,500 token,narrative_pillar.md 约 1,500 token,tone_manifesto.md 约 3,000 token,合计约 7,000 token。(这些数字是笔者的估算,未经验证。随分词器与文档修订而变。)

每次请求都重新发 7,000 token,成本会累积。所以挂上提示词缓存。这是 Anthropic 与 OpenAI 都支持的功能,缓存命中时输入 token 成本会大幅下降。关键是在消息内部把会变的和不变的分开放。

messages = [
    {"role": "system", "content": SYSTEM_PROMPT},
    {"role": "user", "content": [
        {"type": "text", "text": L0_FULL,      "cache_control": {"type": "ephemeral"}},
        {"type": "text", "text": L1_SELECTED,  "cache_control": {"type": "ephemeral"}},
        {"type": "text", "text": L2_ADJACENT},   # 每次都变 —— 不缓存
        {"type": "text", "text": TASK_INSTRUCTION},  # 每次都变
    ]},
]

挂了 cache_control 的 L0 和 L1 是缓存对象,而 L2 邻近正文和任务指令每次都变,所以不缓存。把缓存块始终聚拢在消息前段,是命中率的关键。会变的块一旦夹在前面,它后面的缓存就全部失效。把这个顺序搞错,是开了缓存却省不下成本的最常见原因。

缓存命中率与成本节省数值的细节,在第 22 部分(成本)章节里讲。这里只要记住"把会变的赶到后面"这条原理就够了。


5.3.5 Layer C —— 不把 L1 规则整个塞进去

L1 规则手册篇幅大,全塞进去上下文会撑爆,更糟的是模型会抓不住要点。只挑与任务相关的规则,而且只挑 _summary 节。

抽取主线任务分支结果时,挑 dialogue_branching_rulefaction_relation_matrix。新 NPC 台词,就挑对应 NPC 的 voice_profiletone_manifesto。世界设定词典新条目,就挑 lore_consistency_ruleworld_premise。支线任务骨架,就挑 quest_templatereputation_model。选择由人亲手做,或沿着 wikilink 图(第 7 部分)自动抽取;自动抽取时,召回率优先于精确率。因为漏掉一条规则的损失,远大于多放一条规则的损失。

不把规则手册正文全塞进去,而是在规则手册文件开头放一个 _summary 节,只注入它。

---
title: 分支规则
layer: L1
---

## _summary
- 分支只在章节末尾发生
- 分支为 2~3 案。禁止 4 案及以上
- 分支选择对声望产生 +/-1 影响,结局分支为 +/-3
- 所有分支结果必须在 24 小时内呈现结果
- 分支不可撤销(建议分离存档,UI 提示)

## 1. 分支发生时点规则
(详细说明,供运营者参考 —— 不注入 LLM)
...

_summary 的 5 行,比正文 50 行对 LLM 输出质量更有效。模型更能遵守简短而斩钉截铁的规则。冗长的说明会分散模型的注意力,而被分散的注意力会以违反规则的形式回来。


5.3.6 Layer D —— 邻近正文不做摘要

上一句台词、邻近任务、同一章梗概。这是变动最大的上下文。同一角色的新台词,就按时间顺序放那个角色的上 10 行台词;章节中段的任务,就放章节梗概和同一章任务的 1 行摘要;分支结果结局,就放分支前的整段正文和选项文本。放太多,LLM 就输出平均值;放太少,就给出一般化的输出。合适的区间在 1,500\~3,000 token 之间(以笔者观察为准,未经验证)。

一条核心规则:邻近正文不加工、不摘要,原样不动地放进去。上面那段实操记录里,学者的上 5 句台词原样不动地放了进去,所以模型才能在重新请求阶段精准地抓住"记录"这个词,把它再利用为自责的媒介。如果当初把那 5 句摘要成"学者谨慎而冷淡"再放进去,作者那些细微的选择就会全部消失,模型也就重新回到平均值。摘要不是在减少信息,而是在抹掉作者已经做出的决定。


5.3.7 作者验收工作流 —— 把废弃率当指标用

AI 输出永远是草稿。验收要通过既定的关卡。

flowchart TD A[AI 输出 N 个] --> B[第 1 道:voice_lint 自动<br/>5.2] B --> C[第 2 道:作者单人挑选·修改<br/>约 15 分钟] C --> D{有采用项吗?} D -->|有| E[第 3 道:主叙事审稿人合议<br/>必要时] D -->|采用 0 个 = 全部废弃| F[重新请求或自己写] E --> G[反映进 L2 正文<br/>+ 发放 L3 dialogue_id] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class B code; class A ai; class C,D,E human; class G pass; class F fail;

作者从 N 个里挑出 0 个(全部废弃)也是正常的。正如上面那段实操记录里 3 案被否决,否决不是失败,而是关卡起了作用的证据。所以要按作者、按角色测量废弃率,把它作为上下文注入质量的指标。

废弃率在 0\~20% 是上下文充分的稳定运营,就照原样放着。20\~50% 是一般运营区间,只做监控。升到 50\~80% 就重新检查是不是漏选了 L1 规则。超过 80% 就不是个别规则的问题,而是系统提示词、人设本身错位了,于是要重写 Layer A。废弃率每周按作者汇总一次,在复盘里共享。

不过废弃率不是绝对指标。变化快的角色(例如上面实操记录里的 K_007,处于第一次流露情感的转折点)即使废弃率高也正常。数字是对话的起点,不是判决。


5.3.8 安全 —— 如何阻止上下文泄露

L0\~L1 是游戏的核心 IP。要是把它们原样发给外部 LLM API 让人放心不下,选项就出现分岔。把外部 API 在"不用于训练"的合同下原样使用最快,但需要法务审查。把公司名、专有名词替换成 placeholder 再发,会增加额外处理成本,也会损伤自然度。自托管(开源模型)数据安全,但质量与运营负担大。把 L0 留在内部、只把草稿送往外部的混合方案,运营复杂。

笔者的项目A 采用第一种方案(外部 API + 不用于训练合同)。第二种方案试过后放弃了。因为 placeholder 替换会把正文抹平成"○○ 王国的 ○○ 学者就 ○○ 说了……"这种形态,把输出质量整个搞垮。匿名化扼杀质量,是贯穿全书反复出现的取舍(参见第 1 部分匿名化章节)。在叙事里这种损伤尤其大。因为专有名词本身就是基调。


5.3.9 常见失败与对策

不给系统提示词、只扔任务指令,出来的就是平均值。先铺好人设与禁忌。每次都整段注入 L0 却不用缓存,成本就会漏。把缓存块聚到前面。把 L1 规则手册整个塞进去,模型就抓不住要点。只抽 _summary 节。把邻近正文摘要后放进去,作者的选择就被抹掉。原样引用。不定输出格式,相当一部分回答就会以"这里是 3 个候选:"开头,甚至会跟来作者把那段开场白误当正文的事故。明确标出数量、长度、标签。把 AI 输出当最终版用,验收关卡就垮了。永远让它通过作者关卡。不测废弃率,工具的健康度就依赖人的印象。按周汇总,在复盘里共享。


5.3.10 从保守的应用到进取的应用

到此为止都是保守的应用。作者用心地注入上下文,AI 只做一行一行的草稿。单位是"这个角色的下一句台词 3 案""这个任务的梗概"这类小活儿。稳定,但在量产规模和动态反应性上有局限。

这里先点明一件事。程序化生成、世界模拟、动态任务,是策划们二三十年前就在纸上画出的愿景。基于确定性规则手册的 PCG 处理过副本房间、武器属性、刷怪分布这类数值领域,却碰不到自然语言正文、角色人设、叙事分支、NPC 对话。也就是说,策划的相当一部分一直停留在纸上。2024\~2026 年 LLM 与图像模型的进步,把那片领域拉到了可实现的位置。AI 进步的核心意义不在于模型分数,而在于那些长久停留在纸上的策划,其可实现性被打开了。只不过,可能性被打开,和落地为可运营的系统,是两回事。

Layer 分解是程序化生成的前提

5.1 的 Layer 0\~4 分解不是单纯的整理,而是程序化生成的前提。五个层级各自对应生成流水线的哪个阶段(L0 锚点 → L1 规则手册 → L2 正文 → L3 数值 → L4 关卡),在 §6.6 和 5.1.11 里讲过了。要点只有一个 —— 在一整块文档上,生成器无法决定从哪里读、往哪里写,不知道 L0 在哪儿导致上下文模糊,L1、L2 在同一个文件里冲突,量产线就垮了。先有 Layer 分解,在它之上程序化生成才能运转。这里我们把这个前提应用到叙事 AI 辅助的量产阶段。

进取应用的骨架 —— 任务云

三个要素被捆在一起。第一,NPC Persona 被程序化生成。主线 NPC 靠作者亲手,但支线 NPC 由 6.2\~6.3 的 generator、Squad 流水线来量产。每个 Persona 连同 voice_profile 一起附上标签(职业·势力·性向·角色)。第二,世界 BT 坐到 Squad 之上。如果说 Squad 把一个狩猎场 NPC 群组的行为捆在一起,那么世界 BT 就在它之上,接收玩家行为的累积数值,刷新整个世界的状态。玩家帮了哪个势力、常去哪个区域、做了哪些决定,会撼动世界 BT 节点状态,被撼动的节点又反映进影响圈内 NPC 的数值(声望·关注点·优先级)。第三,任务是云模型。除主线任务外,所有任务都被程序化生成,各自带着标签(谁·在哪·为何·何时)和触发条件漂浮着。任何任务都不固定在特定 NPC 上。

触发要经过五个阶段。

flowchart TD P[1. 玩家行为累积<br/>势力支援·区域访问·决定] --> W[2. 世界 BT 节点状态变化<br/>刷新 Squad 之上的世界状态] W --> N[3. NPC 数值变化<br/>声望·关注点·优先级] N --> M[4. NPC 标签 + 数值 == 触发条件<br/>生成匹配键] M --> Q[5. 从任务云中<br/>触发匹配任务] Q -.->|经作者关卡后呈现| PL[向玩家登场] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class M code; class P,W,N,Q data; class PL pass;

打个比方,任务不是插在图书馆书架上,而是飘在空中的云。NPC 一旦到达某个状态,适合它的那朵云就会降下来,落到手里。在这个结构里,AI 不是写一行的助手,而是同时处理三件事:程序生成的 Persona、任务的自然语言正文(说明·台词),世界 BT 状态反映进 NPC 时的词汇变动(保持同一个 voice_profile,但话题随世界状态而变动),以及验证阶段的可疑分类器(这个任务可不可以在这个 NPC 身上触发)。

不可逆边界 —— 验收必须在文本阶段结束

进取应用里,可逆/不可逆边界(5.4.5)依然成立。云中触发的任务台词若未通过验收就流向语音流水线,代码回滚也救不回来。所以把所有验收关卡(voice_lint、可疑分类器、作者关卡)放在不可逆边界之前,是进取应用的安全装置。自动量产叠加得越多,这道关卡就要比保守应用更硬。

在哪里停下,以及为何不停下

本书不涉及进取应用的完成形态。那要另写一本书的篇幅,而且每家公司、每个项目的基础设施前提都不同。只要记住两点。首先,做不好保守应用的团队,进取应用也做不了。保守应用里验收关卡转不起来,进取应用里云就会失控。要让运营存活,程序化生成基础设施、世界 BT 节点定义与测试工具、触发任务的自动可疑分类 + 作者关卡、触发率·废弃率·玩家满意度的同步追踪、误触发任务的自动回收与替换,这些必须一并具备。五者缺一,就会在一个季度内被废弃。因为一致性事故暴增,人工验收跟不上。

其次,进取应用不是增加量产的工具,而是增加动态反应性的工具。只盯着量产,云里就会塞满一般 RPG 的平均值,玩家遇到的是一个"看上去更多样、却更空"的世界。话虽如此,这条路也并非只有风险。这片领域是游戏策划研发的最前沿。在程序化生成、模拟、LLM 相遇的地方,最有可能诞生真正全新的游戏形态。即便眼下很少会在公司导入,也值得把它当作未来 5\~10 年的方向之一来留意。


5.3.11 把一整章从头到尾动手试试

下面是把本章的保守应用原样再现的步骤。

setup. 把 L0 的三个文件(world_premise.md·narrative_pillar.md·tone_manifesto.md)合成一段文本,装进 L0_FULL 变量。系统提示词照着上面实操记录那五行原样写,但别漏掉最后一行("写草稿,决定由作者来做")。把要抽台词的角色的上 10 行台词,原样不动地收成文本。

prompt. 把消息按 [系统] → [L0 基调] → [参考:上一段台词原文] → [情境] → [输出格式] 的顺序组装。输出格式里务必明确写出"恰好 N 案 / 每案最多字数 / 标签 / 其余一概禁止"。只对 L0 和 L1 挂 cache_control,把会变的块赶到消息后面。

verify. 把拿到的 N 案逐行验证。基调与上一段台词是否连得上,标签与实际反应是否相符,我们这个角色不用的模式(回避·结巴等)有没有混进来,逐一去看。要否决的案,明确写出理由再重新请求。采用 0 个也正常,要以废弃率记录下来。只有通过的案才经过 voice_lint 反映进 L2,并在 L3 中获发 dialogue_id

单人精简版. 即使没有 API、缓存、voice_lint,核心依旧不变。一个 ChatGPT/Claude 聊天框就够了。每次把角色的上 5\~10 行台词复制粘贴铺在最前面,输出格式 3 行始终附上,在拿到的回答里要否决的写明理由再重新请求。不用缓存,只要保持同一个对话线程,前文上下文就会原样留着。废弃率在一张纸上像"本周采用 12 / 总计 30"这样用手数着记就够了。工具再小,四层注入与验收关卡这套骨架,对单人作者也同样有效。


本章要点

下一章预告

5.4 台词·语音一致性

录音棚里。导演戴着耳机,抬手给出提示。配音演员读出学者 NPC 的台词。"哇,真是太厉害了。"导演的手停住了。这个角色在整整 5 个章节里,一次都没用过"哇"这样的感叹词。他是个连脏话、连现代感叹词都不会挂在嘴边、说话总是把话尾压低的学者。可台词稿上却原样写着这一句。

这里同时产生了两笔成本。第一,要改这一句,配音演员就得重读。出场费和录音棚的时间,钟表早已在转动。第二,更可怕的是导演没能抓住这一句、就这么放过去的情形。录音结束、音源进入构建之后,那位学者在游戏里就永远那样说话了。录下来的音源不像文本那样可以靠一行改动撤回。必须重新约同一位配音演员、同样的状态、同一个录音棚。

本章讨论的就是那个录音棚门前的虚线。虚线之上是文本,可以无限次修改;虚线之下是音源,改不了。所有台词的审校都必须在虚线之上结束。要做到这一点,每个角色"这个人是这样说话的"就必须不是记在脑子里,而是记录成文件,并且每当有新台词上来,都要自动与那份文件比对。那份文件称为 voice_profile,比对工具称为 voice_lint


5.4.1 语音容易动摇的地方

叙事一致性事故中,最常爆发的就是角色语音的动摇。往往要等到上线之后,才会收到"这个 NPC 怎么突然语气变了"的反馈。原因每次都几乎相同。

作者换人,同一个 NPC 就变成了另一个人。即便作者不换,过了 6 个月,本人也会忘掉自己定下的语气。写新台词时若不翻开那个角色之前的台词,上下文就断了。语音规则只在作者脑子里、没有落成文档,就传不到下一个人手中。还有最近两年里增长最快的第五个原因——不带任何角色信息就把"帮我写这个 NPC 的台词"丢给 LLM,AI 会返回一段平均而无可挑剔、因而不属于任何人的台词。

第五个原因是引入 AI 带来的副作用。上一章(5.3)的上下文注入是处方,但要是注入的上下文本身就单薄,那就没什么可注入的。那个上下文正是 voice_profile。本章要看的就是创建这份文件、自动检查、并在录音棚门前收尾的一整个循环。


5.4.2 voice_profile —— 由五个项目固定下来的声音

在项目A中,每个 NPC 都用五个项目持有一份 voice_profile。词汇范围(常用词群/绝对不用的词群)、句子长度(平均·最大字数)、敬称体系(第一·第二人称、敬语比例、称呼)、情感表达(直接·间接·克制中的哪一种方式)、禁忌表达(绝对不用的词·句式)。五个项目全部都要附上具体示例。若只有"沉稳的学者"这类抽象描述,每个人读出来都不一样。下一位作者会按自己的方式去想象那个学者。

学者 NPC K_007 实际的 profile 格式如下。这份文件就成了 voice_lint 的输入。

---
title: K_007 学者 voice_profile
layer: L1
character_id: K_007
atoms:
  - voice_profile_k_007
related:
  derives_from: [character_bible/k_007.md]
  affects: [dialogue_id_table (K_007 的所有台词)]
---

## 1. 词汇范围
- 常用:"记录"、"依据"、"情形"、"推断"、"数据"、"案例"
- 绝对不用:"感觉"、"直觉"、"命运"、"神的旨意"、"内心的声音"

## 2. 句子长度
- 平均:18 字
- 最大:35 字(超过则拆成两句)
- 常见短促停顿:"……并非如此。""先看记录。"

## 3. 敬称体系
- 第一人称:"我"
- 第二人称:职务优先(队长、司令官)。只有在亲近之后才称名字。
- 敬语 100%(回忆场景除外)
- 几乎没有感叹词。有的话也只是"……啊。"

## 4. 情感表达
- 几乎没有直接表达(愤怒·喜悦)
- 以沉默·话尾含糊来表达("……要是那样的话。")
- 悲伤:以转移话题来回避("……说点别的吧。")

## 5. 禁忌表达
- 一切脏话
- 现代感叹词"哇"、"啊哈"、"厉害"
- 一句话里出现两个及以上三音节以上的汉字词
- "命运"、"预言"等神秘主义词汇

这种格式的核心在于,每一处抽象的位置都附着一行示例。不是"克制情感",而是附上 "……要是那样的话。" 这样一句实际台词。这样下一位作者、译者、voice_lint 才会看到同一套标准。 若用翻译·本地化的眼光重新审视这五个项目,会分成两类。语言相关属性(词汇范围·句子长度·敬称体系的表层形态)必须随翻译的每种语言重新设定;而语言无关属性(是克制情感还是直接表达、什么话始终不说出口这样的态度)则无论译成哪种语言都必须照样守住。把本地化工作交付出去时若一并把这个区分传过去,就能避免译者只改表层词汇、却连角色态度都动摇了的情况。


5.4.3 实操记录(worked transcript,完整保留的真实操作过程记录):从正文里把 profile 打捞出来

一开始就从空白开始写 50 名 NPC × 5 项目 = 250 条,会流于抽象。正文一行都没有就写下"这个角色是个冷漠的学者",那份冷漠到底是什么,谁都不知道。所以把顺序倒过来。只有主要 NPC 5~7 名从一开始就完整撰写,其余的等正文台词累积到 20~30 行之后,再从那些台词里反向提取 profile。

下面是 K_007 的台词累积到 25 行之后,抽出 profile 初稿的真实实操记录。不做概括,连 AI 出错的地方也原样保留。

[作者抛出的提示词全文]

以下是角色 K_007 的 25 行台词。

1. "先看记录吧。"
2. "……情形对不上。"
3. "只是推断而已。没有依据。"
4. "那个案例我也知道。"
5. "……并非如此。数据不一样。"
   (…… 第 6~23 条不作省略,全部引用 ……)
24. "……要是那样的话,我就退出。"
25. "说点别的吧。"

请从中提取常用词汇(只取实际出现过的)、平均句子长度、敬称·称呼模式、
情感表达方式、一次都没用过的表达范围,这五项。
每一项用编号引用 2~3 行作为依据的台词,"沉稳的"·"冷漠的"这类
主观印象不要写。

[Claude 的原始输出 —— 未经修饰]

1. 常用词汇
   - "记录"(第1条)、"情形"(第2条)、"推断/依据"(第3条)、
     "案例"(第4条)、"数据"(第5条)
   → 集中于事实·证据类名词。

2. 平均句子长度
   - 25 行平均约 14.6 字。最长 21 字(第24条)。
   → 以短句为主。

3. 敬称·称呼
   - 第一人称"我"(第3·24条)。结句词尾为敬语。
   - 第二人称称呼在这 25 行里未出现。
     → 仅凭此样本无法断定称呼模式。(需作者确认)

4. 情感表达
   - 无直接情感词。以"退出"(24)处理拒绝,
     以转移话题"说点别的吧"(25)处理悲伤/回避。
   → 克制·间接型。

5. 一次都没用过的范围
   - 感叹词、脏话、神秘主义词汇均未出现。
   ※ 但"未出现"不等于"禁忌"。
      样本仅 25 行偏少,可能只是偶然未出现。

这里有两点需要点明。第一,AI 在第 3 项停住了。它自己划出了"仅凭此样本无法断定"的界限。称呼在这 25 行里没出现,所以它没去推测——这才是诚实的输出。要是 AI 填上"第二人称推测为'队长'",那就是幻觉,进入 profile 就是污染。第二,AI 在第 5 项自己加了免责。"未出现 ≠ 禁忌"是关键警告。感叹词在这 25 行里没出现,也可能是偶然。

[作者的验证·驳回]

作者采纳了第 1·2·4 项。第 3 项称呼,作者翻开 character_bible,手动填上"队长优先,亲近后称名字"——AI 留空的位置由人来补。第 5 项,作者照 AI 的警告,没有把"未出现"直接升格为"禁忌"。而是作者对照角色设定,只把"现代感叹词·脏话·神秘主义"确定为禁忌,其余未出现的词汇作搁置处理。

[作者的再次请求]

"命运"、"预言"就定为禁忌。再帮我提取 10 个意思相近的神秘主义词汇。
不过 K_007 作为学者,在反驳·批判的语境里也可能引用,所以那种例外情形
也用一行一起标出来。

这最后一次再请求很重要。机械地扩大禁忌,连"学者在批判迷信时说'什么命运之类的'"这种正当台词都会被挡住。所以让它在定义禁忌的同时一并定义例外语境。AI 把候选范围铺开,作者来划边界。转完这一圈之后,voice_profile_k_007 才确定下来、固定到 L1。

强制要求引用依据("用编号引用")、禁止主观形容词("禁止写沉稳的"),AI 的幻觉就会减少,也给作者留出了可供验证的表层。profile 不是 AI 来写的,而是 AI 铺出初稿、作者来固定的。


5.4.4 voice_lint —— 每条新台词自动比对

只要 voice_profile 落成文件,每当有新台词上来,就可以自动比对。五项检查中,在实战里效果大的是禁忌词汇匹配(词汇是否落入禁忌列表)和词汇范围违规(是否进入绝对不用的词群)这两项。句子长度偏离·敬称缺失·常用词汇比例误报(false positive)多,只作辅助使用。要是连一行回忆场景把平均长度带偏都全抓出来,作者就会对警告变得麻木。

voice_lint 接收一章的新台词集合,给出这样一份报告。

voice_lint 结果(ch04 新台词 32 行,profile=voice_profile_k_007)
─────────────────────────────────────────────
[违规] dialogue_id_412 — K_007
  内容:"哇,真是太厉害了!"
  事由:禁忌词汇"哇"、"厉害"(profile §5)
  → 需作者审阅

[可疑] dialogue_id_421 — K_007
  内容:"那命运难以接受。"
  事由:禁忌词汇范围"命运"(profile §5)
        但'反驳·批判语境'可作例外 —— 由作者判定
  → 需作者审阅

[正常] 30 条台词
─────────────────────────────────────────────
小结:违规 1 / 可疑 1 / 正常 30

违规为红色,可疑为黄色。两者都必须经作者判定才能通过。这里有一条绝对原则——voice_lint 不会自动驳回(5.2 原则的延续)。看上面的 dialogue_id_421。"命运"是禁忌,但若是学者反驳迷信的语境,就可能是正当的引用。那个判断工具做不了。自动驳回型的 lint 会把这种微妙的位置全都挡死,还会从作者手里夺走打磨语气的机会。lint 是标示可疑之处的手电筒,不是锁门的锁。


5.4.5 审校关卡 —— 可逆与不可逆之间的虚线

本章的脊梁是这一张图。一句台词从作者手中出发、走到配音演员口中的途中,审校关卡逐级铺开。而在那条流程正中央,有一条粗虚线。

flowchart TD A["作者撰写正文 (L2)"] --> B["voice_lint 自动<br/>违规·可疑报告"] B --> C["作者自我审校 (15 分钟)"] C --> D["主叙事抽样审校<br/>每章 10% 样本"] D --> E["发放 L3 dialogue_id<br/>+ 翻译键映射"] E --> F["翻译·本地化审校"] F -.->|"━━━ 可逆 / 不可逆 边界 ━━━<br/>此线之上是文本,可无限修改<br/>此线之下是音源,无法修改"| G G["VA 选角"] --> H["配音录制 (最终·不可逆)"] H --> I["音源进入构建"] classDef reversible fill:#e8f4ea,stroke:#3a7d44,stroke-width:1px,color:#1b3a22; classDef irreversible fill:#f7e3e3,stroke:#b23b3b,stroke-width:2px,color:#5a1414; class A,B,C,D,E,F reversible; class G,H,I irreversible;

绿色是可逆阶段,红色是不可逆阶段。虚线之上(绿色)全是文本。一句台词不满意,用键盘改就行。成本是作者的几分钟。虚线之下(红色)是音源。配音演员在录音棚里读了那一句、音源进入构建的那一刻,那句台词就作为资产固定下来了。要改的话,必须重新约同一位配音演员、同样的状态、同一个录音棚,出场费·录音棚·导演时间会和第一次一模一样地再花一遍。日程紧的话,同一位配音演员的追加场次本身可能都约不上。

所以只有一条规则统辖着所有工作流——所有审校关卡都在虚线之上结束。录音不是审校阶段。它是把审校全部完成的结果固定为资产的阶段。若在虚线之下冒出"这句台词不太对劲"的疑问,那不是还要再审校的位置,而是上一级审校有遗漏的信号。手电筒必须在虚线之上全部照完。录音棚不是可以漆黑的地方,而是不可漆黑的地方。

图正中央,主叙事抽样审校定为"每章 10% 样本"。这个比例是审校时间与准确度的平衡点(作者运营值,未验证的估计)。降到 5% 以下事故会漏出,提到 20% 以上则单个主叙事会成为瓶颈。因为 lint 已经预先筛掉了违规·可疑,样本就从 lint 通过的部分里抽——人眼专注于工具抓不到的语境错误(例如:看似正当的"命运"引用其实是角色崩塌)。


5.4.6 角色会变化 —— profile 的版本管理

角色若从头到尾一个样地说话,剧情就停滞了。经历了同伴之死的学者,若还用之前一模一样的语气说话,反倒是假的。若变化是有意为之,voice_profile 也要一起升版本。

---
character_id: K_007
voice_profile_versions:
  - v1: ch01~ch05 (初期 —— 克制情感、短句学者)
  - v2: ch06~ch10 (同伴死亡后 —— 情感表达频率上升)
  - v3: ch11~ (觉醒后 —— 出现直接化的说话方式)
---

每个版本都有各自的 profile 文件,voice_lint 看检查对象台词的章节编号,挑出该用哪个版本。若拿 v1 的"克制情感"规则去套 ch07 的台词,正常的变化台词就会全部被标为可疑。变化不是 bug,而是设计。

升版本的信号有三种。作者有意动摇语气时,就提议新版本并与主叙事达成一致。若 voice_lint 的可疑件数在某个角色身上越来越多,那就是作者在无意识中移动语气的——版本该更新的信号。若 character_bible 里追加了变化事件(死亡·觉醒·背叛),就会弹出 profile 更新 alert。不过一个角色若每章都变,一致性就会崩塌,所以现实中的版本数为每个角色 2~4 个。

[方向标 —— 若以语音空间来看角色之间(眼下还为时尚早)] 如果说 voice_lint 用规则守住的是'单个角色内部'的一致性,那么把各角色实际的台词集合(一个角色说过的全部台词)嵌入(embedding)为一个点的'语音空间',看的则是'角色之间'是否拉开了足够距离——若那些点彼此越凑越拢,那就成了 §5.3.1·§5.4.1 所指出的语音趋同·收敛的直接测量值。但不要把距离阈值固定为绝对数值,只应把它读作'正在凑拢'的方向标,它不是替代 voice_lint 的判定关卡(这个想法与 §8.2.7 的维度向量压缩处在同一位置,概念直观已在附录 M 里用一张地图讲清——它是方向标,不是处方)。


5.4.7 因多语言·VA 而成倍增加的一致性单位

台词被译成多种语言、再配上配音演员的语音之后,管理单位会成倍增加。一行韩文分岔成英文·东南亚各语言,每一种又都承载着语气。

翻译一致性里最常漏的地方,是同一个表达在每一章被翻得不一样(用翻译记忆一致性检查来抓)。角色 voice_profile 未反映到翻译里(另附按角色的翻译指南)、新词汇未登记进术语集(术语集 lint)紧随其后。翻译指南从 voice_profile 自动生成——把"这个角色格式体 100%、无感叹词、神秘主义词汇为禁忌"自动附在翻译指示书的开头。译者把那位学者译成英文时,看到的是同一道边界。

VA(Voice Actor,配音演员)审校,是触及虚线之下前、最后一道文本可逆阶段的审校。语气一致性(愤怒·悲伤表达的强度)由导演和叙事来看,发音准确性(专有名词)由术语集负责人来看,气息·停顿(profile 中"常见短促停顿"之类的指示)由导演来看。审校结果记录在 voice_review_log.md(L4)里,以通过·驳回标注,下次角色选角时参考。

驳回尽可能在选角·录音之前结束。在录音棚里发现的台词稿错误,会把那天的整场录制整个搞垮,连带动摇下一场的日程。但靠推迟录音来关掉审校并不是答案。如果审校频频卡在录音棚前,那是上一级(作者·主叙事)工作流晚了,而不是录音日程的问题。


5.4.8 六个月的测量与成本

在项目A中,对引入 voice_profile + voice_lint 前后追踪了六个月。绝对件数是作者的估计(未验证),只信方向·比例即可。

项目 引入前 引入后 方向
每章语音事故(上线后) 5~8 件 1~2 件 约 1/4
新 NPC 语音定型 3 个章节 1 个章节 1/3
单个作者管理的 NPC 数 约 15 名 约 40 名 约 2.5 倍
翻译一致性事故(每章) 10~15 件 2~4 件 约 1/4
语音审校时间(每章) 3 天 1 天 1/3

最有意义的一行是单个作者管理的 NPC 数。约 2.5 倍并不是说裁减了作者,而是说同一个作者能增加每章的 NPC 多样性。世界更加熙攘。

看成本结构,运营成本远小于引入成本。voice_profile 撰写为主要 7 名、作者 2 周,voice_lint 工具为开发 1~2 周、维护每月 1 天。运营这边为每章作者自我审校 15 分钟、主叙事抽样审校约 2 小时(10% 样本)、变化章节的 profile 更新为每个角色 1~2 天。运营成本小,系统才能存活。运营沉重的工具会在一个季度内悄悄被废弃。


5.4.9 常见的失败

模式 处方
profile 里只有抽象描述("沉稳的") 每个 5 项目强制附实际台词示例
一开始就想着完整写满 50 名 主要 7 名完整 + 其余从正文累积反向提取
voice_lint 自动驳回型 违规·可疑 + 作者判定。驳回只能由人
机械地扩大禁忌 在禁忌处一并写明例外语境("可作反驳引用")
角色变化时 profile 未更新 版本管理(v1·v2·v3),按章节编号套用
翻译中未传达 profile 从 profile 自动生成翻译指南
录音后试图修改台词 录音不可逆。审校在虚线之上收尾
把审校压缩进录音日程 以改进上一级工作流来解决
profile 只存在脑子里 一律落成文件。脑子里的东西会随作者换人而消失

动手试试 —— voice_lint 一个循环

新章节台词上来时,用一份 profile 转一圈的最小流程。

setup 1. 打开目标角色的 voice_profile_<id>.md。没有的话,就先收集正文台词 20~30 行。 2. 把新台词整理成 id / 角色 / 内容 格式的纯文本集合。

prompt

这是 K_007 的 voice_profile §5(禁忌表达)。
[粘贴禁忌列表]

ch04 新台词 32 行。
[按 id / 内容 格式粘贴]

请把每条台词分类为 [违规](直接含禁忌词汇)/ [可疑](触及禁忌范围但
可能有例外语境)/ [正常]。[违规]·[可疑] 用表格列出 id·内容·事由。
判定由我来做,所以不要自动驳回。

verify 1. 先看 [违规]。明显的就改文本(在虚线之上,免费)。 2. [可疑] 按语境判定。是正当引用就通过,否则修改。 3. 从通过的部分里抽 10% 作样本发给主叙事,再过一遍语境错误。 4. 所有判定都结束之后,才发放 dialogue_id 并交给录音队列。录音棚门前不再做任何检查。

单人精简版 —— 如果你是做不出工具的单人开发者,就给每个角色的 voice_profile 只写 §5(禁忌表达)这一项。每次写新台词时,把那份禁忌列表附在提示词开头,让 AI"只标出落在这份列表里的台词"。不用工具,靠一行提示词就能拿到 lint 八成的效果。在交给录音(或 TTS)之前,只要让它过这一遍,录音棚门前的虚线就守住了。


本章要点

下一章预告

6.1 程序化内容生成与 AI —— 两轴交叉的一格

周一早晨的策划会议。白板上写着一行字:"上线前要做 1,000 个支线任务。"有人按起了计算器。一名作者每个任务花一天,就是四年;哪怕五个人一起上,也接近一年。房间里的空气沉了下来。在这个房间里坐了 24 年的我知道,面对这个数字,人们总是分成同样的两派:一派说"砍掉一些量",另一派说"用工具量产"。而几乎每一次,结论都是两者都要。

程序化内容生成(Procedural Content Generation,以下简称 PCG)正是"用工具量产"那一派的老答案。副本房间的布局、武器词条的组合、敌人刷新池,早在 20 年前就靠规则手册和概率表实现了自动化。新出现的并不是 PCG 本身,而是在自然语言、图像、叙事进入的位置上,换成了 LLM 与生成模型。

不过,本书想说的并不是"把 AI 接到 PCG 上"。那谁都会做。问题在于接在哪里。面对一整块内容,如果不把它落在自动化的哪一档强度、结构的哪一层交汇的那"一格"上钉死,就会陷入有工具却没有位置的状态。本章要看的,是如何把那"一格"画成坐标,以及在那一格之上,一块内容如何真正地在流水线上跑完一圈。


6.1.1 PCG 一直止步的地方

传统 PCG 擅长确定性。相同的输入产生相同的输出,并且可以验证。副本房间图、武器词条的 prefix·suffix、敌人刷新分布因此很早就站稳了脚跟。"烈焰之剑 +5"在 20 年前就能自动生成。

问题总是出在紧接着的那个位置。房间布置好了,但房间里 NPC 的名字、外形、简短背景仍留在作者手里。"烈焰之剑 +5"能生成,可"国王遗失的最后一柄剑"这一句却生成不出来。哪怕任务 generator 拼出了目标与奖励的组合,"为什么要做这个任务"仍要由人来写。

在体量大的游戏里,这个位置一直是瓶颈。可量产的部分与需要人工的部分,比例大约是 4 比 6,而那需要人工的 6 成吃掉了排期的大半。哪怕量产线快速产出那 4 成,只要 6 成跟不上,整个周期就被拖到那个速度上。

LLM 和图像模型进入的位置恰好就在那里。规则手册处理不了的自然语言、叙事、视觉领域,也被纳入可量产的范围。但这并不意味着把这个位置整个交给 AI 就是答案。AI 每次给出的答案都略有不同,一旦上下文为空,就会吐出一般 RPG 的平均值。因此需要设计结合点。结合点由两条坐标轴来定义。


6.1.2 第一条轴:自动化强度(L0\~L3)

纵轴讲的是,把人、规则手册和 AI 按什么比例混合。在笔者所在的某家 MMORPG 开发公司(以下称"项目A")里,把它切成四档来用。

L0 —— 完全手工。 所有文字与决定都出自人手。主线任务正文、标志性角色台词、分支结局。一致性与叙事深度直接关系到游戏身份认同的位置。

L1 —— 规则手册自动化。 传统 PCG 的位置。规则手册、概率表、BSP 之类的确定性算法负责产出,人只做评审。副本房间布局、武器词条组合、敌人刷新是代表。

L2 —— 规则手册 + AI 辅助。 规则手册搭好骨架,AI 填充细节。支线任务梗概、普通 NPC 的名字与简短背景、狩猎场介绍文。人只负责输入的元数据和最后的验证关卡(verification gate,即由人或检查器把关的验证环节,类似质量门禁 quality gate)。

L3 —— AI 优先 + 人工评审。 AI 负责生成正文,人只做评审。看着诱人,但非确定性、幻觉、一致性受损的风险都集中在这里。

核心是 L2。它把 L1 的稳定性与 L3 的量产力合到一起,而两边的缺点则用验证关卡挡住。L3 让人很想尽快引入,但笔者多次见到它因评审负担暴增而在一两个季度内被废弃的案例。100 个里有 70 个被标为可疑项,那就比人从头写 100 个还要贵。


6.1.3 第二条轴:Layer 结构(L0\~L4)

只靠纵轴,量产线是转不起来的。内容本身必须被拆解成层,自动化才有可进入的位置。这就是第 5 部分讲过的 Layer 拆解,也是内容领域里的横轴。五个层各自对应程序化生成的一种角色(锚点·规则手册·正文·数值·关卡)这一通用说明已在 §2.3.6 讲过,这里直接套用到内容量产线上。Layer 0 愿景是基调与世界观锚点(每次生成都注入),Layer 1 系统是生成规则手册(规则·概率表·标签体系),Layer 2 内容是生成结果堆积的正文位置(支线任务·NPC 背景·城市介绍文),Layer 3 数据是数值·ID·关系(奖励·刷新·曲线),Layer 4 构建·QA 是验证关卡(lint·一致性检查·作者评审)。

这两条轴讲的是不同的事。纵轴说的是"人插手多少",横轴说的是"内容的哪个部位"。而两者只有相乘才有意义。只有把一块内容钉在两轴的交点,也就是一格上,"这由谁、在哪里、怎么做"才算定下来。


6.1.4 两轴合为一张 —— 自动化 × Layer 矩阵

把一路用文字讲下来的两条轴,叠成一张格子看看。横向是内容的 Layer,纵向是自动化强度。每一格里的标签,是项目A中实际占据那一格的内容。颜色越深的格子,越接近量产线的重心。

自动化强度(纵向) × Layer 结构(横向) → Layer 结构(内容的哪个部位) ↑ 自动化强度(人插手多少)

L0 愿景 L1 系统(规则手册) L2 内容(正文) L3 数据 L4 构建·QA

L0 手工 L1 规则手册 L2 规则手册+AI L3 AI优先

亲手写一句基调 主线任务 标志性台词

副本房间布局 词条·刷新概率表 奖励曲线计算

定义生成规则手册 支线任务骨架 NPC 简短背景·介绍文 ★ 重心 lint·一致性检查

更新公告初稿 作者评审关卡

这张格子是本章的核心。原本散落在文字里的"主线是 L0""支线是 L2""奖励归规则手册"之类的判断,汇聚到一个坐标上。会议上有新内容进入议题时,只需问一句"这是哪一格"就够了。格子一旦定下,那格的纵坐标告诉你谁来插手,横坐标告诉你是哪个部位。

读这张格子,有两点会映入眼帘。第一,重心(深色格)在 L2 行 × Layer 2 列。支线任务骨架、NPC 背景就在那一格,是量产线的心脏。第二,一块内容并不只待在一格里。支线任务的正文(Layer 2)在 L2 格,但它的奖励数值(Layer 3)则下沉到 L1 格。同一个任务,不同部位住在不同格子。这正是把两轴分开的理由。


6.1.5 在一格之上跑动的迷你流水线

在重心格 —— L2 行 × Layer 2 列,支线任务骨架 —— 之上,看一块内容如何真正跑完一圈。流程如下。

flowchart TD A["Layer 0 愿景注入<br/>(基调锚点:东方风乡村)"] --> B["Layer 1 规则手册<br/>(目标·奖励槽骨架生成)"] B --> C["L2 AI 辅助<br/>(填充 NPC 动机·一句背景)"] C --> D{"Layer 4 验证关卡<br/>(基调·一致性 lint)"} D -->|通过| E["Layer 3 数据确定<br/>(奖励数值由规则手册计算)"] D -->|打回| B E --> F["纳入构建"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class B,D code; class C ai; class E data; class F pass;

我们用一份实操记录(worked transcript,即完整保留的真实操作过程记录)把这个流程走一遍。假设要做一个乡村里的杂活任务。

第 1 步 —— Layer 1 规则手册抽出骨架。 规则手册是确定性的,只把槽位填出来。

[任务骨架 —— 规则手册输出]
地区: 乡村_3
委托人角色: 采药人
目标类型: 采集(药材 x6)
干扰要素: 狼群刷新(夜间)
奖励槽: { gold: ?, exp: ?, item: 采集镰 }

第 2 步 —— 向 AI 请求细节。 提示词全文如下。(先把 Layer 0 愿景作为上下文输入。)

你是一款以东方奇幻乡村为舞台的 MMORPG 的任务作者。基调锚点:"朴素,却笼罩着迷信与恐惧的乡野"。接收下面的骨架,写出委托人的一句动机、NPC 名字,以及呈现给玩家的两句委托文。绝对不要编造奖励数值(由系统决定)。输出为 JSON。 骨架: { 地区: 乡村_3, 委托人: 采药人, 目标: 采集 6 个药材, 干扰: 夜间的狼, 奖励物品: 采集镰 }

第 3 步 —— Claude 的原始输出。(不作概括,原样照录。)

{
  "npc_name": "三月婆婆",
  "motive": "每到夜里,狼嚎把药圃糟蹋得不成样子,她再也不敢去挖那最后的药材。",
  "quest_text": "一到夜里,狼群就把药圃翻得乱七八糟。我怕得挪不动步子,劳您替我到山脚下挖六株药材回来吧。",
  "reward": { "gold": 120, "exp": 300 }
}

这里要停下来看一点。AI 在没被要求的情况下,还是造出并填进了 reward 字段。这恰好说明了第一轴与第二轴为何必须分开。奖励数值(Layer 3)是 L1 规则手册的位置,不是 AI(L2)的位置。把它交给 AI,数字就会每次调用都晃动,奖励曲线随之崩掉。

第 4 步 —— 人工验证与拒绝。 评审者做两件事。(1)删除 reward 字段 —— 这是规则手册要填的格。(2)看基调。"三月婆婆"、一句动机、两句委托文都合乎乡村基调,通过。若 AI 塞进了"魔法师公会的委托"这类世界观之外的词,就在这里打回,退回到骨架步骤。

第 5 步 —— Layer 3 数据确定。 被删掉的奖励槽由规则手册重新填上。这是绑定地区等级与目标难度的确定性公式。gold: 85, exp: 240。不是 AI 随口吐出的 120·300,而是符合曲线的值。

这一圈就是重心格的标准循环。规则手册出骨架,AI 添血肉,人做关卡,规则手册再出数值。1,000 个内容全都跑这个循环。格子既已定下,就不必每次再为"这由谁来做"争论一遍。


6.1.6 定格子时要问的五个问题

要决定把新内容放进格子的哪一格,五个问题会有帮助。每当会议上出现量产议题时,把它们记下来一起作答,格子分配的一致性会在一个季度内稳定下来。

一、量产负担有多大。上线前需要 N 个?N 一旦超过 100,L0 行几乎不可能。

二、一致性要求有多高。若内容之间的一致性是体验的核心,验证关卡(Layer 4)就得够强;若多样性才是核心,就有往更上面的行走的余地。

三、能否容忍非确定性。这是每次略有不同的结果会带来丰富度的领域,还是相同结果才是信任核心的领域。

四、评审成本是多少。每块内容是 5 分钟还是 30 分钟,决定了运作周期的长短。

五、出事时的成本是多少。是可以自由废弃、重写,还是一旦放出去就直接酿成用户事故。

把这五问抛给支线任务,答案会朝一个方向汇聚。1,000 个以上(L0 不可能)、一致性低于主线、容许非确定、评审 5\~10 分钟、事故成本低(可单独废弃)。五个答案一汇合,L2 行 × Layer 2 格便顺理成章。把同样的五问抛给主线任务,则朝相反方向汇聚。50 个、一致性与叙事深度最高、不容许非确定、评审成本大、事故成本极大 —— 就是 L0 格。


6.1.7 四个常见陷阱

即便画好了格子,会掉进去的陷阱也大同小异,反复出现的有四个。

第一,从 L3 行起步。 抱着"AI 自动搞定 100 个"的期待出发,评审就会暴增。先让 L1 格落地,再往上升到 L2,L3 只在一部分内容上谨慎使用。上面那条迷你流水线里,人删掉奖励字段的那一个动作,小小地展示了 L3 行为何危险。

第二,不要规则手册,把整件事整个交给 AI。 "帮我做 100 个支线任务"招来的是一般 RPG 的平均值。必须先由 Layer 1 规则手册搭好骨架,AI 在其上填血肉,才能产出属于我们这款游戏的内容。写一本规则手册,是 PCG 里最费工、最无趣的活儿,但一旦跳过它,其上的所有量产都会塌回平均值。

第三,验证关卡(Layer 4)空缺。 AI 输出自动进入构建,就会直接引发一致性事故。无论哪一格,人工关卡都是必需的。

第四,只看成本就定工具。 LLM API 成本每个季度都在下降,但一致性事故的成本不会下降。定工具时,要在 API 成本之上,再加上一致性与评审时间的总和来看。


6.1.8 度量 —— 迁到重心格后的六个月

在项目A,把支线任务从 L0 格迁到 L2 格之后,笔者度量了六个月。下面这些数字里,绝对值是笔者的估算(未经验证),而变化的方向与比例才是实测中观察到的部分。

项目 L0 时期 转为 L2 后
作者人均写 1 个任务 约 4 小时 约 50 分钟(元数据 30 分钟 + AI 5 分钟 + 评审 8 分钟)
每周量产 5 个 30\~40 个
废弃率 几乎 0% 约 20%
一致性事故(每季度) 3\~5 起 5\~8 起(加固后正常)
作者满意度(10 分) 8 6 → 7(政策加固后)

废弃率升到了 20%,但量产速度是原来的 6\~8 倍,净吞吐量因此增加了 4\~5 倍。一致性事故小幅升到每季度 5\~8 起,但通过验证关卡与规则手册的加固,在一个季度内回到了正常范围。

最大的变化不是数字,而是人。起初作者们觉得自己成了"量产评审员",满意度从 8 跌到 6。为了挽回这一点,我们插入了一项政策:在主线任务与标志性支线任务(每座城市 1\~2 个)上,明确保障作者的时间。这是要钉死一点 —— 量产线不是要吸走作者的时间,而是要成为把那些时间送回主线的工具。六个月后,满意度回到了 7。

从这次度量里要带走一点。迁格子的决定,必须让吞吐量、作者时间的分配、满意度一起跟上。只看吞吐量,量产是成功了,人却走了。


6.1.9 先做 Layer 拆解,PCG 在其上

Layer 拆解是程序化生成的前提,这个通用命题在 §2.3.6。这里只看它在 PCG 格子上如何显现。横轴(Layer 0\~4)模糊不清的团队,任何一格都无法稳定运转。不知道 Layer 0 愿景在哪里,每个 generator 的基调锚点就是空的,只会产出一般 RPG 的平均值;Layer 1 规则手册与 Layer 2 正文混在一个文件里,改动一行规则时就得连带触碰正文里的几十处;Layer 3 数据被写进正文里,奖励曲线调整一次,作者就要花上一周 —— 上面那条迷你流水线里,把奖励拆成独立槽位单放,就是这个原因。

所以在引入 PCG 之前,要检查的不是工具选择,而是横轴是否已经拆解。在五层齐备的团队里,接上一个 L1 generator 的成本,是一名作者一个季度。在五层混作一团的团队里,同样的引入会在两个季度内因一致性事故而被废弃。

五层不必一开始就完美。分离要循序渐进,接口要窄。头一个季度里,哪怕只把 Layer 0 的一句基调和 Layer 1 的一本规则手册拆出来,generator 可进入的位置也就打开了。但这并不意味着可以无限拖延。若 Layer 2 正文与 Layer 3 数据始终是一整块,下一章的具体工具也站不住脚。


6.1.10 下一章预告

下一章会解剖一个占据这张格子重心格的具体工具:量产各城市狩猎场的 proj_city_hunting_generator。看输入元数据、规则手册骨架、AI 正文、验证关卡如何被绑成一个循环,以及本章的迷你流水线在真实工具的规模上如何放大。


本章要点


动手试试 —— 把一块内容放到一格上

setup. 选一种量产候选内容(例如支线任务)。把 Layer 0 的一句基调和 Layer 1 的规则手册骨架(槽位定义)拆成单独的文件。奖励数值槽在规则手册一侧留空。

prompt. 先把愿景作为上下文输入,再给出骨架,并明确写上"不要编造奖励数值,输出为 JSON"。把上面第 2 步的提示词直接改一改用就行。

verify. 看三点。(1)AI 若擅自加了奖励字段,就删掉(L3 是规则手册的位置)。(2)有世界观之外的词,就打回到骨架步骤。(3)只有通过的部分,才由规则手册填上奖励数值并纳入构建。

单人精简版. 没有团队也行。你自己用文本文件做出一本规则手册(5 个槽位)和一句基调就够了。把 10 个任务用上面的循环跑一遍,数一数评审时打回了几个。打回率若超过 30%,说明格子选错了 —— 要么把规则手册骨架做得更细密,要么下降一行(L1)重新看。打回率一旦稳定,那就是在你自身的规模上、这一格能运转的信号。

6.2 city_hunting_generator —— 4 周内造出 30 座城市

主要读者:负责内容量产的 MMORPG 策划(中等规模(10\~50 人)团队) 面向单人/业余读者的精简版:§6.2.10「一个人只需做到这些」

我还记得第一次拿到日程表、得知上线前需要 30 座城市那天的估算。一座城市由 5\~10 行的介绍文、3\~5 处狩猎场、每处狩猎场 5\~10 名 NPC 与 2\~3 个支线任务、1\~3 种特产道具、1 只城市 Boss 构成。用手工雕琢一座城市要花 1\~2 周。30 座就意味着一名文案要把整整 6 个月全耗在城市上。

然而那 6 个月根本挤不出来。文案的时间被主线任务和标志性角色占满,30 座城市必须与那些工作并行推进。「让 AI 把 30 座城市都造出来不就行了」——这个最初的冲动很快就破灭了。整包丢给它,只会得到 30 座彼此雷同的奇幻村庄。本章要看的,是为替代这一冲动而打造的工具 city_hunting_generator 如何把输入、规则手册、AI、验证这四个阶段串成一个循环,以及当这个循环真正完整跑完一遍时,会产出什么、又会废弃什么。

作者实际运营备注 本章的 city_hunting_generator 是作者在公司 R&D 文件夹中实际运营的工具经匿名化后的版本。文件名、代码结构、验证项都忠实照搬自真实工具,城市名(silvermark 等)、公司专有名称已替换为书中用名。输出正文是对真实会话的重现。


6.2.1 人只负责元数据和最后的审核

工具的整体流程分为四个阶段。关键在于:第 1 阶段和第 3 阶段是确定性的(规则手册),只有第 2 阶段是 AI。规则手册从两端把住骨架与验证,即便夹在中间的 AI 每次给出略有不同的答案,城市之间的一致性也不会动摇。人只介入最初的输入(元数据)和最后的关卡(审核)。

flowchart TB A["输入:城市元数据 yaml<br/>(人工 15~20 分钟/城市)<br/>lore_seeds 3 个 · forbidden_names 自动附加"] A --> B["第 1 阶段 确定性:rules.py<br/>generate_skeleton()<br/>狩猎场数、敌人分布、奖励曲线、Boss"] B --> C["第 2 阶段 AI:3 种提示词<br/>L0 愿景缓存 + L1 规则注入<br/>+ L2 相邻城市正文<br/>→ 介绍文、NPC、支线任务"] C --> D{"第 3 阶段 确定性:lint<br/>重名、禁忌词、口吻<br/>、篇幅、奖励范围"} D -->|违规 alert| E["第 4 阶段 文案审核关卡<br/>(5~10 分钟/城市)<br/>废弃、重新生成决策"] E -->|重新请求| C E -->|通过| F["构建落地"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class B,D code; class C ai; class A,E human; class F pass;

在这张图里,人工触及的地方只有两处:最上方把一页元数据干净地填进去的位置,以及最下方对 lint 抓不到的语气、叙事作出判断的位置。中间那些枯燥的骨架生成与正文量产,交给规则手册和 AI 去跑。有一处关键设计:lint(第 3 阶段)即便发现违规也不自动废弃,只把 alert 上报给文案关卡(第 4 阶段)——理由将在 §6.2.5 中说明。


6.2.2 输入 —— 一页城市元数据

文案为每座城市写一页元数据。撰写时间 15\~20 分钟。虽短,但这一页就是后续三个阶段的全部输入。

# city_021_silvermark.meta.yaml
city_id: city_021_silvermark
region: west
climate: cold_arid
dominant_faction: scholar_guild
cultural_tone: scholarly_strict
level_range: [25, 30]
lore_seeds:
  - 100 年前是魔法封印的中心地
  - 封印减弱的最初征兆在这座城市被观测到
  - 学者公会总部所在地
neighbors: [city_018, city_023]
# forbidden_names:(脚本自动附加 —— 文案无需填写)

最重要的槽位是 lore_seeds。3\~5 个核心事件决定城市的身份。太少,AI 会吐出一座普通的奇幻城市;太多,事件之间会相互矛盾。以作者的经验,3 个最为稳定。

forbidden_names 不由文案填写。脚本会读取既有城市、角色的名字列表,自动附加到元数据里。因为当 30 座城市 × 平均 50 名 NPC 累积起来,靠人脑去检查 1,500 个名字是否重复是不可能的。无需每次手动写上「别和其他城市的 NPC 重名」。


6.2.3 第 1 阶段 规则手册 —— 用确定性搭好骨架

规则手册接收元数据,生成城市的结构骨架。代码很简单。

# city_hunting_generator/rules.py (骨架)
def generate_skeleton(meta):
    region_rules = REGION_RULES[meta.region]
    hg_count = region_rules.hunting_grounds_range.sample()
    enemy_dist = ENEMY_RULES[meta.climate][meta.dominant_faction]

    skeleton = {
        "hunting_grounds": [
            {
                "id": f"{meta.city_id}_hg_{i}",
                "level": meta.level_range[0] + i,
                "enemy_types": enemy_dist.sample(k=3),
                "reward_curve": calc_reward(meta.level_range[0] + i),
                "npc_count": region_rules.npc_per_hg,
                "sidequest_count": region_rules.sidequest_per_hg,
            }
            for i in range(hg_count)
        ],
        "boss": {
            "id": f"{meta.city_id}_boss",
            "level": meta.level_range[1] + 2,
            "pattern": BOSS_PATTERNS[meta.region],
        },
    }
    return skeleton

结果是确定性的。输入相同的元数据,就会得到相同的骨架。奖励曲线是否落在按 region·level 划定的标准范围内、敌人分布是否符合 climate·faction 规则,都由代码来保证,并有回归测试兜底。这一阶段绝不交给 AI。因为一旦让 AI 每次调用都为奖励曲线抽出不同的数字,城市之间的平衡当场就会动摇。

输入 silvermark 的元数据,rules.py 会返回一个空骨架:4 处狩猎场(city_021_silvermark_hg_0\~hg_3)、每处狩猎场 6 格 NPC 槽位、3 格支线任务槽位,以及 1 只 32 级 Boss。这是一张还没有名字、也没有正文、等待填充的格子表。填这些格子,就是第 2 阶段 AI 的工作。


6.2.4 第 2 阶段 AI —— 生成自然语言正文

规则手册搭好骨架之后,AI 在其上填入自然语言正文。城市介绍文,NPC 的名字、外形、简短背景,支线任务梗概,特产道具的风味文本(flavor text),都在这里产出。

调用模式沿用了上下文注入的四层结构。缓存 L0 愿景(world_premise + tone_manifesto),选择性注入 L1 规则(city_naming_rule + region_west_lore),再加上 L2 相邻正文(其他城市的 NPC 名字列表),最后附上任务指令。城市介绍文的提示词是可以直接复制使用的形态。

[L0 上下文] world_premise + narrative_pillar + tone_manifesto  (缓存)
[L1 上下文] city_naming_rule, region_west_lore
[输入] city_021_silvermark.meta.yaml + lore_seeds 3 个

给这座城市写一篇 6~8 行的介绍文。把三个 lore_seeds 都自然地融进去,
去掉「和平的村庄」这类 RPG 套话。语气要学者化、严格,克制抒情。
只要正文,不要前言或解说。

同样的模式在 NPC 量产、支线任务的调用中原样重复。差别只在上下文和输出格式。NPC 调用中会加入 forbidden_names(禁止与其他城市的 NPC 重名)槽位,支线任务调用中则加入「委托任务的 NPC 须从上面已生成的 NPC 中选取」的约束。

那么实际会产出什么?我们完整地跑一遍。


6.2.5 完整跑完一个循环 —— 输出、验证、废弃

如果只是抽象地写「AI 填充正文」,就无法知道这个工具究竟吐出了什么。我们把 silvermark 这一座城市的循环,从输入一直跟到废弃,完整地走一遍。

第 2 阶段 AI 输出 —— 城市介绍文

西尔弗马克(Silvermark)是一座封印之城。100 年前,七位学者为阻止西境魔力的泛滥,在这里的地下立起了封印阵,而城市便在那座阵上,一块石头一块石头地垒砌、生长起来。学者公会总部的钟楼是研读封印脉络的观测所,而非朝向神明的尖塔。最近 3 年,观测记录里反复写着同一行字:「脉在变冷。」封印减弱的最初征兆,不在别处,正是在这座城市的仪表盘上第一次颤动。街上的学者们不会大声谈论此事,只是钟楼的火整夜不熄的日子,多了起来。

这里,3 个 lore_seeds(封印中心地、减弱的最初征兆、学者公会总部)都写进了正文,没有「欢迎冒险者的和平村庄」这类套话,scholarly_strict 语气(克制抒情,使用观测、记录类词汇)也得到了保持。这就是一份能够通过的输出该有的样子。

接着,同一个循环填充狩猎场 hg_0 的 6 格 NPC 槽位中的 3 格。

第 2 阶段 AI 输出 —— 3 名 NPC

1)多伦·维尔(Doren Vale) —— 钟楼观测助理。50 多岁,一只眼上永久装着黄铜放大镜。记录封印脉络细微震动的工作,已经做了 30 年。话很少,只用数字交谈。「今天 12.4。昨天 12.1。在往上走。不是好事。」

2)米拉·科斯特(Mira Kost) —— 公会档案库管理员。30 多岁,墨渍再也擦不掉地留在手指上。她守护着封印阵的设计原本,却坚信能读懂那张图纸的学者早已全部离世。对外来者戒心很重。

3)格雷姆(Grem) —— 钟楼下的守炉人。身份不明,年龄不详。他唯一的工作就是不让钟楼的火熄灭,对询问封印之事的人,只答一句「看着火就行」。(标记为存疑 —— AI 自行申报)

值得注意的是,第三名 NPC「格雷姆」是 AI 自己给它打上了 存疑标记。好的提示词能让 AI 说出「这一个我没有把握」。现在,第 3 阶段的 lint 要对这一批输出执行检查了。

第 3 阶段 lint 输出

[PASS] 篇幅检查:介绍文 7 行(基准 6~8)
[PASS] 奖励范围:hg_0~hg_3 reward_curve 在标准范围内
[WARN] 重名:"Mira Kost" —— 与 city_014_riverhold 的 "Mira Veldt"
       姓氏(Kost/Veldt)不同,但名字(Mira)相同。forbidden_names 近似冲突。
[PASS] 禁忌词汇:tone_manifesto 违规 0 件
[WARN] 口吻一致性:"格雷姆" 台词 voice_lint 置信度 0.62(未达阈值 0.70)

lint 抓出了 2 件违规,但没有自动废弃任何一件,只是作为 WARN 上报给文案关卡。这正是 §6.2.1 中预告过的设计核心。一旦把自动否决权也交到验证器手里,文案们不出一两个月就会把那个开关关掉。因为机器会把有意为之的变体也一并扼杀,连文案亲自去权衡那条边界的机会都被夺走了。所以,筛出可疑候选的活儿交给机器,而这些候选是留是弃的最后判断,则留在人的手里。

[第 4 阶段 文案审核 —— 判定与废弃]

文案这样处理了这 2 件 alert。

文案一旦决定废弃,就会再跑一次重新请求。「废弃格雷姆的槽位。请重新生成一个符合同一狩猎场学者公会语气(观测、记录、严格)的守炉人 NPC。禁用神秘主义词汇。」AI 这次回以一位记录钟楼炉温、连火都当作数据来看的老人,该输出以 voice_lint 0.81 通过。输入 → 骨架 → 正文 → 验证 → 废弃 → 重新生成的一个循环,在这里闭合。

这一圈就是本书贯穿始终的「Show(展示)」标准。工具吐出什么、什么被拦下、人又亲手毙掉什么——若不曾完整地看过哪怕一次,「用 AI 量产」这句话就是空洞的。


6.2.6 废弃率不是工具的失败,而是关卡在起作用的信号

在上面的循环里,有 1 名 NPC 被废弃。放到整座城市来看,废弃会更多。审核时间平均每座城市 5\~10 分钟,废弃率 NPC 约 20%,支线任务约 33%。

这里如实交代这些比例的计算依据。废弃率是在导入初期,亲自审核包括 silvermark 在内的 5 座城市并逐一计数得出的值。NPC 是审核的 30 名中废弃 6 名(20%),支线任务是审核的 15 件中废弃 5 件(33%)。由于样本仅 5 座城市、规模很小,它不是精确的总体比例,而应当作「五中取一、三中取一」量级的方向值来读。等 30 座城市全部审核完之后的累计比例,可能比这更低,也可能因狩猎场性质不同而更高。

重要的是,废弃率 0% 并不是目标。废弃 0% 更像是审核走了形式的信号。当五名 NPC 中有一名因语气不合而被废弃、三个支线任务中有一个因与 lore_seeds 贴合不上而被重新生成时,审核关卡才是在真正运转。


6.2.7 度量 —— 30 座城市 4\~5 周

我们对比工具导入前后。下面的时间数值是包括 silvermark 在内的早期城市的实测平均,「导入前」一列是工具出现之前手工作业时期文案的估算。没有任何加工过的数字。

项目 导入前(手工) 导入后(实测)
单座城市撰写时间 1\~2 周 约 30 分钟(元数据 15 分钟 + AI 5 分钟 + 审核 8 分钟)
30 座城市总周期 相当于 1 名文案 6 个月 4\~5 周
废弃率(NPC) ——(全部手工撰写) 约 20%(30 名中 6 名)
废弃率(支线任务) —— 约 33%(15 件中 5 件)
一致性事故(每座城市) 几乎没有 0\~1 件

只看表格,亮点在数字;可真正的成效出在别的格子里。原本险些被城市量产捆住的文案时间被释放出来,一名文案每个季度的主线任务产出得以大幅增加(具体倍数每个季度不同,故不下定论 —— 方向是「主线产出明显增多」)。量产工具不是在吞噬文案的时间,而是作为把时间释放出来的工具在运转(§6.1.8 那句警告——一旦文案觉得自己成了「审核机器」,工具就会被抵制——在这里同样适用)。


6.2.8 不放进 generator 的内容

即便自动化的范围变宽了,以下内容仍留在工具之外。

内容 留在工具之外的理由
主线任务正文 一致性、叙事深度直接关系到游戏的身份认同
Boss 招式、演出 视觉、交互细节繁多,交给设计师上手更快
标志性主要角色 需要完整撰写 voice_profile,无法量产
分支结局 属于文案亲自决策的领域
每座城市 1\~2 个标志性支线任务 由文案挑选并亲自制作

「能够量产」这一事实,不该自动导向「就应该量产」的决定。正如在 silvermark 循环中看到的,工具能把 6 名 NPC 中的 5 名量产得很好。然而,要扛起那座城市「封印正在变冷」这一核心张力的那一名标志性 NPC,由文案亲手雕琢。只要自动化的边界足够清晰,量产工具反而会成为守护那块核心领域的工具。


6.2.9 五种常见的失败

失败模式 为何失败 处方
lore_seeds 只写 1\~2 个 AI 输出被拉平到普通 RPG 的平均水准 强制 3 个以上(§6.2.2)
不用规则手册,直接让 AI 整包量产 「造 30 座城市」→ 30 座雷同的村庄 第 1 阶段规则手册无法跳过(§6.2.3)
没有 lint,只依赖文案审核 审核者把时间耗在处理琐碎的规则违规上 先做第一道自动验证(§6.2.5)
遗漏重名检查 1,500 个名字的重复靠人脑无法完成 forbidden_names 自动附加(§6.2.2)
不度量文案满意度 吞吐量上去了,但抢走文案时间就会被抵制 明确保障主内容时间(§6.2.7)

第五种最常被忽视。要让文案乐于作出像废弃 silvermark 的格雷姆那样的判断,就必须给文案留出量产审核之外、亲自雕琢的时间。只度量吞吐量却不度量文案的时间,工具在 KPI 上是成功了,人却会离开。


6.2.10 动手试试 —— 今天就能做的一步

一个人只需做到这些:没有规则手册的代码也没关系。挑一座你自己游戏(或你喜欢的游戏)里的城市、地区,按 §6.2.2 的格式手写一份元数据(3 个 lore_seeds 是关键),再把 §6.2.4 的介绍文提示词原样贴上去,跑一遍看看。从产出的 NPC 里挑一个语气不合的,亲自对它反驳:「这个 NPC 与城市语气相抵触,废弃、重来」——这样一来,你就能亲身体会到审核关卡究竟是怎样一组判断的集合。

如果是团队,就从下面这一步开始。先做出一份元数据 yaml 表单和 forbidden_names 自动附加脚本。规则手册骨架(generate_skeleton)和 lint 放在之后。哪怕只有输入表单和重名检查这两样,也能先挡住 AI 正文量产崩成「30 座雷同村庄」的两种常见失败。


6.2.11 下一章预告

6.3 将讨论 NPC Persona/Squad 流水线。如果说 6.2 的 generator 是把多伦、米拉这样的 NPC 逐个量产,那么 Persona/Squad 则把这些 NPC 以组为单位捆在一起。这是一套让同一狩猎场的五名 NPC 不再是彼此无关的木偶集合,而是作为一个小型社会来运转的方法。


本章要点

下一章预告

6.3 NPC Persona 与 Squad —— 从玩偶博物馆到小社会

主要读者:负责 NPC·狩猎场内容的 MMORPG 策划(中等规模(10\~50 人)团队) 面向单人/业余读者的精简版:§6.3.10「一个人的话,做到这些就够」

我记得那一天,我用 6.2 的 generator 在一个狩猎场里量产了五名 NPC,把它们放进游戏里看了看。名字·外形·简短的背景都填好了,只标了坐标就摆了上去。可当我真正在那个狩猎场里走一圈,却感到一种说不出的死气。五个人待在同一个空间里,却从未彼此提及一句。两个人叠在同一块岩石上。本来需要有人担任商人,五个人却全是学者。多伦也好、米拉也好,单独看都是没问题的 NPC,可一旦捆在一起,就成了一堆玩偶。

这就是玩偶博物馆的状态。单个 NPC 都做好了,作为群体却没有活起来。本章讲的是把那五个人捆成一个小社会的流水线。核心拆解是 Persona 与 Squad。用办公室来打比方,Persona 是员工的个人名片,Squad 是一个团队的组织架构图。名片摞了 50 张,却没有组织架构图,公司就转不起来。而本章的脊梁在最后一步——把捆好的群体是否"像彼此相识那样说话和行动",与 AI 一起走完整整一个循环来验证的那个环节。

作者实际运营备注 本章的 Squad 流水线,是作者对自己在公司 R&D 文件夹中运营的 NPC Persona/Squad 工具做的匿名化处理。yaml 结构·验证项·voice_lint 阈值都忠实照搬了实际工具,城市·NPC 名称则与 6.2 一样替换为书中用名。输出正文是对真实会话的重现。


6.3.1 Persona 是名片,Squad 是组织架构图

Persona 是单个 NPC 的身份。它承载名字·外形·voice_profile·职能。6.2 的 generator 造出来的就是 Persona。多伦·韦尔、米拉·科斯特各自都是一个 Persona。

Squad 是把这些 Persona 捆成群体的单位。它定义在一个狩猎场里五个人以怎样的职能分布、彼此是什么关系、如何移动。

单位 承载内容 创建主体
Persona 名字·外形·voice_profile·职能 generator(6.2)
Squad 职能分布·关系·动线 Squad 流水线(本章)

不把这两者分开,两件事就会同时卡住。只量产 Persona 就成了玩偶博物馆,想从 Squad 做起又没有可填的 Persona。分开之后,每个单位的运营都变得简单。不过,分离并不等于割裂。关键是在两个单位之间铺好复用·验证的通路,这正是本章的正题。

这套 Persona→Squad 的拆解不只是简单的归整,它打开了走得更远的一条路。只有当 NPC 群体以职能·关系·数值被规整化,日后世界状态(玩家行为的累积)才能撼动 NPC 的数值,而这些数值又成为任务触发条件,一路走到动态反应性。本章只点到那种进阶应用的入口,正面处理的部分只到"由人评审的保守量产"为止。


6.3.2 输入 —— 一页 Squad 元数据

Squad 骨架从每个狩猎场一页元数据开始。这与 6.2 的城市元数据是同一套思路。人只定下职能分布和关系意图,填充的活儿交给规则手册与 AI。

# city_021_hg_3.squad.yaml
squad_id: city_021_hg_3_squad
hunting_ground: city_021_silvermark_hg_3
type: hunting_ground_residents
size: 5
roles:
  - role: quest_giver
    count: 1
    voice_traits: [authoritative, scholarly]
  - role: lore_keeper
    count: 1
    voice_traits: [scholarly, withdrawn]
  - role: merchant
    count: 1
    voice_traits: [practical, dry]
  - role: bystander
    count: 2
    voice_traits: [varied]
relationships:
  - between: [quest_giver, lore_keeper]
    type: mentor_and_former_student
  - between: [merchant, bystander_1]
    type: regular_customer
movement_pattern: stationary_with_shifts

最重要的槽位是 relationships。关系若为 0 件,五个人到最后仍是陌路。关系太多(5 人有 5 件以上)则玩家要记的东西太多,反而被淹没。据笔者经验,5 人 Squad 配 2\~3 件核心关系最为稳定。voice_traits 是让五个人各自拥有不同嗓音的装置。若五个人全填 scholarly,在验证阶段就会因 voice 趋同而被拦下。


6.3.3 第 1 步·第 2 步 —— 规则手册骨架与 Persona 填充

规则手册先定下 Squad 骨架的标准。按狩猎场的 region·type,尺寸·职能分布·关系密度·动线模式的默认值都写进了代码。

# npc_squad/templates.py (节选)
SQUAD_TEMPLATES = {
    ("west", "hunting_ground_residents"): {
        "size_range": (4, 6),
        "role_distribution": {
            "quest_giver": 1,
            "merchant": 1,
            "lore_keeper": (0, 1),
            "bystander": (1, 3),
        },
        "relationship_density": 2,        # 建议关系数
        "movement_pattern": "stationary_with_shifts",
    },
    ("east", "outpost_squad"): {
        "size_range": (3, 4),
        "role_distribution": {
            "commander": 1,
            "scout": 1,
            "support": (1, 2),
        },
        "relationship_density": 1,
        "movement_pattern": "patrol_loop",
    },
}

这一步是确定性的。西部居民 Squad 里出现五个全是 quest_giver 这种事故,在代码层面就不可能发生。职能分布一旦越出规则,当场就会被拦住。

接下来给每个槽位填入 Persona。路子有三条。池子里有合适的 Persona 就复用(出场权重 +1),没有就用 6.2 的 generator 新造,若是主线任务的核心人物则由编剧亲自撰写。silvermark 的 hg_3 Squad 把 quest_giver·lore_keeper 用 6.2 已经量产好的米拉·多伦填上,另外新抽了 merchant 与 2 名 bystander。到这里为止,与 6.2 的 generator 循环相同。本章真正的活儿在其后——验证捆好的群体是否真的像一个群体那样运转的那个环节。


6.3.4 走完一个完整循环 —— 关系补充·动线·一致性验证

如果只抽象地写一句"AI 补充关系",就看不出这条流水线到底吐出什么。下面把 silvermark hg_3 这一个 Squad 的后半段循环,从关系文本生成一直跟到废弃·重新请求,完整走一遍。

第 3 步 —— AI 关系补充

Squad 骨架里录入的关系标签(mentor_and_former_student)是抽象的,在游戏里看不见。把它变成一行描写、埋进 NPC 台词·事件,就是第 3 步。提示词是可以直接复制来用的形态。

[L0 上下文] world_premise + tone_manifesto  (缓存)
[L1 上下文] city_021_silvermark.lore(学者公会主宰,scholarly_strict)
[Persona 1] quest_giver —— 米拉·科斯特,公会档案库管理员,30 多岁,墨迹斑斑
[Persona 2] lore_keeper —— 多伦·韦尔,钟楼观测助手,50 多岁,只用数字交流
[关系标签] mentor_and_former_student

请把这两人(师父–昔日弟子)的关系,写成 1~2 行可用于游戏台词的背景描写。
多伦用数字、米拉用文书——让两种口吻不相冲突。语气用严格的学者风,
去掉神秘主义或"老朋友"之类的套话。只要正文。

[第 3 步 AI 输出 —— 关系一行](真实会话重现)

二十年前,多伦教过米拉封印阵观测记录的记法。如今立场颠倒,多伦测出的数值,由米拉誊抄进档案库的账簿。两人每周二都会为观测值与账簿对不上的某一格,短暂地争执一番。

这段输出不错。mentor_and_former_student 被具体化了,多伦的"数字"与米拉的"文书"没有冲突地被拧进同一个场景(把数值誊进账簿),scholarly_strict 语气也保持住了。同一套提示词,也会在 merchant–bystander_1 的 regular_customer 关系上重复一遍。

第 4 步 —— 动线合成

NPC 若整天站在一个地方,就又成了玩偶。规则手册来填动线模式。stationary 是固定一处(卫兵·Boss),stationary_with_shifts 是每 8 小时微调位置(普通),routine_loop 是基于时间表(居民),event_driven 是仅在触发时移动(任务 NPC)。这一步是确定性的,不调用 AI。

第 5 步 —— Squad 一致性 lint(这条流水线的关卡)

现在来查捆好的五个人是否真的像一个群体那样运转。如果说 6.2 的 lint 看的是单个 NPC,那么这个 lint 看的是群体一致性。

[第 5 步 Squad lint 输出](真实格式)

[PASS] 职能分布:quest_giver 1 · lore_keeper 1 · merchant 1 · bystander 2(满足规则)
[PASS] 关系密度:2 件(建议 2,满足)
[WARN] voice 多样性:scholarly 系 3/5 —— quest_giver·lore_keeper·bystander_2
       的 voice_profile 余弦相似度 0.83(超过阈值 0.80)。有趋同风险。
[WARN] 动线冲突:14:00~16:00 区间 merchant·bystander_1 坐标半径 1.5m 重叠
[FAIL] 关系暴露:定义了 2 件关系,但 5 人台词中任何地方都 0 次提及其他成员。
       关系仅存在于数据中 —— 游戏内可见性为 0。

lint 抓出了三件。三件都不自动废弃,而是提交到关卡——可疑候选由机器挑出,但要杀还是要留由人来定,这与 §6.2.5 是同一套设计。

[第 6 步 编剧评审 —— 判定与废弃]

编剧这样处理了这三件 alert。

三件里有两件靠规则·重新生成合上了,最后那个 FAIL 才是这条流水线的核心。编剧追加请求了多伦对话分支的一行。

请在多伦的台词里,插入仅仅一行、像不经意间流露出与米拉关系的话。
不要说明腔,要像顺带一提。学者风语气,只要一行台词。

[重新请求的输出]

"那张图纸在档案库里。去问米拉吧。……二十年前是我教那家伙怎么读的,如今反过来了。"

这一行加进去的一刻,两个 NPC 的关系就从数据表搬到了游戏画面上。输入(Squad 元数据)→ 骨架 → Persona 填充 → 关系补充 → 动线 → 一致性验证 → 废弃·暴露决策,这一个循环在此合上。

这一圈就是本章的 Show 标准。"用 Squad 把 NPC 捆成社会"这句话,若没有哪怕一次看到人把关系暴露 0 件的 FAIL 用一行台词合上的场景,就是空洞的。


6.3.5 Persona→Squad 全流程

把上面的循环用一张图放在这里。要点是:第 1·2·4·5 步是确定性的(规则手册·lint),只有第 3 步是 AI;而且人的手只触及最上面的输入和最下面的关卡。

flowchart TB P["Persona 池<br/>(6.2 generator 产出)<br/>多伦·米拉·……"] --> FILL A["输入:Squad 元数据<br/>(人 10~15 分钟/狩猎场)<br/>职能分布 · 关系 2~3 件意图"] A --> B["第 1 步 确定性:templates.py<br/>职能分布·关系密度·动线标准"] B --> FILL["第 2 步:Persona 填充<br/>复用 / generator / 编剧亲写"] FILL --> C["第 3 步 AI:关系一行补充<br/>L0 缓存 + voice_traits 冲突检查"] C --> D["第 4 步 确定性:动线合成<br/>shift·routine·event_driven"] D --> E{"第 5 步 确定性:Squad lint<br/>职能分布·voice 多样性<br/>·动线冲突·关系暴露"} E -->|"alert"| F["第 6 步 编剧评审关卡<br/>(5~10 分钟/狩猎场)<br/>废弃·规则修正·关系暴露决策"] F -->|"重新请求"| C F -->|"通过"| G["写入构建"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class B,D,E code; class C ai; class A,F human; class P data; class G pass;

人的手触及的地方只有两处。最上面定下职能·关系意图的环节,最下面判定 lint 抓不到的语气·叙事的环节。中间的骨架·动线·验证由规则手册来跑,关系正文由 AI 来跑。


6.3.6 让关系在游戏中可见的三个装置

第 5 步 lint 的 关系暴露 项最常出现 FAIL。因为关系只在数据里活着。把关系从数据里拽进游戏的装置有三个。

第一,台词引用。像 §6.3.4 里多伦那句台词那样,NPC 用一行提及其他成员。最便宜,效果也最大。

第二,动线交叉。每周二多伦与米拉一同待在档案库的场景,能在游戏里被观察到。玩家偶然看到,就会察觉"那两个人是不是有牵连"。只要第 4 步的动线与关系一致,它自然就会出现。

第三,分支条件。拒绝 quest_giver 的请求,lore_keeper 的好感度也会一起下降。这第三个装置正是 §6.3.1 所说进阶应用的入口——关系越过单纯的描写,开始对游戏状态产生影响的那个环节。

三个装置不必都用。在 5 人 Squad 里,哪怕只把 2\~3 件核心关系用第一·第二个装置暴露出来,狩猎场的体感也会大不相同。过多的话,玩家要记的东西就多了。编剧在评审阶段挑选要暴露的关系插进去,其余的只留作数据。


6.3.7 度量 —— 诚实地

比较工具引入前后。不使用加工过的数字。时间·比例是亲手评审 silvermark 等几个早期狩猎场时数出来的值,"引入前"一列则是手工时期编剧的估算。

项目 引入前(手工·估算) 引入后(实测)
一个狩猎场的 Squad 捆绑 约 3\~4 小时 约 25 分钟(元数据 12 分钟 + AI 5 分钟 + 评审 8 分钟)
关系暴露(台词·动线)件数 每个狩猎场 0\~1 件 核心 2\~3 件中暴露 1\~2 件
动线冲突(同一坐标 2 人以上) 每个狩猎场 2\~3 件 用 lint 提前拦截,0\~1 件
voice 趋同废弃 —(无检查) 5 人中 0\~1 人重新生成

样本只有几个狩猎场,数量小,所以不该当作精确的总体比例,而应读作方向值。最大的变化不在表里。因为 lint 的 关系暴露 FAIL 会强行把"这个狩猎场,一件关系都看不见"摆到编剧面前,量产成果作为玩偶博物馆上线的情况在结构上减少了。废弃 0%·暴露 0 件并非目标(§6.2.6),这一点在这里也一样。Squad 吸收了捆绑工作的大部分,但像多伦最后那一行台词那样的核心,必须给编剧留出亲手雕琢的时间。


6.3.8 Persona 池 —— 同一个人物出现在多座城市时

Squad 稳定之后,自然随之而来的运营就是 Persona 池。同一个 Persona 可以在多座城市出场。隶属学者公会的 NPC 在三四座城市里被遇见,反倒很自然。这不会让世界显得狭小,而是显得彼此相连。

persona_pool:
  - id: persona_doren_vale
    voice_traits: [terse, numeric]
    appearance_count: 3
    appearance_cities: [city_021, city_018, city_023]
    signature: false
  - id: persona_mira_kost
    voice_traits: [scholarly, withdrawn]
    appearance_count: 2
    signature: false

复用比例有一个健康区间。

复用比例 状态
低于 20% NPC 数量暴增,识别负担
30\~50% 健康的运营区间
70% 以上 NPC 令人厌倦,多样性受损

不过,让一个 NPC 出现在太多城市,就会变成"这人怎么又来了"。一个 Persona 最多出场城市定为 5 座作为上限。Boss 房间·标志性人物禁止复用(signature: true)。同一个 Persona 从第二次出场起,强制施加视觉变化(灯光·道具)。这个 30\~50% 的区间不是精确数值,而是运营指南——要按团队·游戏规模来校准。

[方向标 —— 若把 Persona 池当作分布来看(眼下还为时尚早)] 如果是池子已长到数百 NPC 的团队,还有一个更进一步的方向——它与 §8.2.7"维度向量"一节处在同一位置,是一块方向标(不是处方——概念直觉见附录 M)。§6.3.4 的 voice_lint 已经用两个 Persona 的余弦相似度(0.83 这样的值)来看"接近程度"。把同一套嵌入(embedding)铺到整个池子上,就能把令人厌倦不靠编剧的印象、而靠分布密度来诊断——"学者口吻扎堆挤在一角"的状态,会呈现为那片区域的点密度。这样一来,与其在低密度区域里再新画一个"又一个相似的学者",不如在相近的两个 Persona 之间填入插值出来的变体,把多样性补上,这条路就打开了。不过,要一并放在心上的注意点有两个。用插值抽出的 Persona 很容易变成把两个 NPC 生硬混合的"死中间值",最终还得由人重新把 voice 救活。而且上面那个厌倦比例(30\~50%)不是精确值,而是运营指南,一旦把它换算成嵌入距离,松散就有可能装成精确的样子藏起来——距离值是帮助编剧判定的信号,而不是判定本身。


6.3.9 六种常见的失败

失败模式 为何失败 处方
只量产 Persona 而无视 Squad NPC 50 名都齐了,狩猎场却死气沉沉 引入 Squad 骨架规则手册 (§6.3.3)
无职能分布规则的自由生成 出现只有五个商人、0 个学者这类分布事故 强制 role_distribution (§6.3.3)
只放关系标签而缺一行描写 关系抽象,在游戏里看不见 第 3 步 AI 关系补充 (§6.3.4)
无关系暴露检查 定义了关系,却在台词·动线上 0 件暴露 第 5 步 关系暴露 lint (§6.3.4)
缺动线冲突检查 同一时间同一地点 2 人,上线后频发 坐标·时段自动检查 (§6.3.4)
复用比例 0% 或 70%+ 0% 是量产暴增,70%+ 是令人厌倦 池运营 + 出场上限 (§6.3.8)

第四种最常被漏掉。定义关系与让关系在游戏里可见,是两件不同的工作,没有检查,后者几乎总是缺席。在 silvermark hg_3 里,如果 lint 没有把关系暴露 0 件作为 FAIL 摆出来,多伦和米拉就只会在数据表里是师徒关系。


6.3.10 动手试试 —— 今天就能做的一步

一个人的话,做到这些就够:没有规则手册、没有 lint 也行。从你自己的游戏(或喜欢的游戏)里某一处的 NPC 中挑 3\~5 名,按 §6.3.2 的格式,用手写下职能和 2 件关系。接着把 §6.3.4 的关系补充提示词原样贴上,拿到一行描写,最后亲自问一句——"这段关系,现在在游戏台词的哪里看得见?"一处都没有的话,那就正是 lint 的 关系暴露 0 FAIL。在一名 NPC 的台词里插进其他成员的一行,亲手把那个 FAIL 合上,你就会切身体会到 Squad 验证抓的到底是什么活儿。

如果是团队,就从下面这一步开始。先做一张 Squad 元数据 yaml 表单,以及第 5 步 lint 中的 关系暴露 检查这一行(用 grep 查每个 NPC 台词文本里是否出现其他成员的名字·职能)。职能分布检查·动线冲突检查放在其后。哪怕只有关系暴露这一项检查,也能先挡住量产狩猎场作为玩偶博物馆上线这一最常见的失败。

用 setup → prompt → verify 概括就是——setup:在 Squad 元数据 yaml 里定义职能·关系,用 templates.py 定下骨架。prompt:按 §6.3.4 的格式拿到关系一行,同时强制禁止 voice_traits 冲突·禁止套话。verify:跑一遍第 5 步 lint 确认 关系暴露 FAIL,把一件核心关系埋进台词,亲手合上。


本章要点

下一章预告

6.4 内容量产工作流 —— 把多个 generator 串成一条产线

三个工具各自完工的那一周,我一口气跑了城市 generator、NPC generator 和道具 generator。三者各自都运行良好。城市生成了 7 座,NPC 生成了 110 名,武器生成了 60 件。可几天后坐下来评审时,我才发现自己把同一个陷阱踩了三遍。

城市 port_harman 被生成成了“没落的渔村”,可布置到那座城市里的 NPC 们,人设却是“繁荣贸易港的富商”。原因在于城市 generator 和 NPC generator 看的是不同的 lore_seeds。武器 generator 则在这座推荐等级为 12\~18 的城市里,把 40 级的传说武器铺进了商店。三个工具各自都没错,却因为没被串起来而错了。

本章讲的不是造一个工具的故事,而是把 6.2 的城市 generator、6.3 的 NPC Squad 以及道具 generator 串成一条生产线来运转的故事。工具有三个,陷阱却不是三个——它们会在工具之间的缝隙里重新冒出来。


6.4.1 生产线这一视角

如果把城市·NPC·道具 generator 各自单独去跑,每个工具的输出就会和另一个工具的输入对不上。解法不是把工具做得更聪明,而是在上游一次性固定好共享的元数据,再让各个工具在它下面排成一队。如果说 6.2 ch2 的城市 generator 是单个工具的范本,那么本章要做的,就是把那个工具降格为产线上的一个工站。

整条产线是这样流动的。

flowchart TD SEED[周一:lore_seeds 表格<br/>region·climate·faction·level_range]:::human SEED --> CITY[周二:城市 generator<br/>L1 规则手册骨架 + L2 AI 正文] CITY -->|city_manifest.json| NPC[周二:NPC generator<br/>继承城市 lore] CITY -->|level_range| ITEM[周二:道具 generator<br/>继承等级段·势力] NPC -->|persona_pool| SQUAD[周二:Squad 部署] ITEM --> SQUAD SQUAD --> LINT[周三:集成 lint<br/>跨 generator 一致性] LINT -->|alert| REVIEW1[周三:一次评审<br/>作者本人] REVIEW1 --> REVIEW2[周四:二次抽样<br/>叙事主管] REVIEW2 --> BUILD[周五:并入构建<br/>+ 准备下周 seed] BUILD -.回收.-> SEED classDef human fill:#fde68a,stroke:#b45309,color:#000;

关键是 city_manifest.json 这条箭头。城市 generator 在造出一座城市的同时,把这座城市的身份(是没落的渔村,还是繁荣的贸易港)产出成一份 manifest,NPC generator 和道具 generator 把这份 manifest 当作输入接收下来。我当时踩的那个陷阱,正是因为缺了这条箭头。所谓把工具串起来,就是在工具之间放进这一行契约。


6.4.2 把工具串起来的一行契约 —— manifest

城市 generator 每造出一座城市,都会一并吐出一份 city_manifest.json,它的实际形态如下。这个文件会成为 NPC·道具 generator 的输入。

{
  "city_id": "port_harman",
  "display_name": "哈尔曼港",
  "lore_seeds": ["没落的渔村", "旧日贸易的余响", "缺盐"],
  "region": "南部沿海",
  "dominant_faction": "渔民公会",
  "level_range": [12, 18],
  "tone": "衰落·坚韧",
  "forbidden_names": ["哈兰", "哈尔门"],
  "neighbors": ["salt_marsh", "old_pier"]
}

NPC generator 继承 lore_seedstone,造出“没落渔村里那群坚韧的人”。道具 generator 继承 level_range,只铺 12\~18 级的武器。forbidden_names 是邻城已经用过的名字,两边都会避开。三个工具看的是同一份契约。

制作这份 manifest 时,我给 Claude 的提示词如下。这是把量产产线上游绑在一起的、最重要的一次调用,所以原样照录全文。

你是 MMORPG 城市 generator 的 manifest 生成器。接收下面的作者元数据,生成 city_manifest.json。

作者输入: - city_id: port_harman - lore_seeds: 没落的渔村、旧日贸易的余响、缺盐 - region: 南部沿海 - level_range: 12-18

规则: 1. dominant_faction 从 lore_seeds 推断,但不得与邻城(salt_marsh、old_pier)的势力重复。邻城势力为 [盐田公会、走私团]。 2. forbidden_names 里填入已在邻近城市使用过的 NPC/地名。提供的列表:[哈兰、哈尔门]。 3. tone 控制在两个词以内,且不与 lore_seeds 矛盾。 4. 区分标注推测的值和作者明确指定的值。

Claude 的原始输出是这样的。不作概括,原样照录。它在一个地方违反了规则。

{
  "city_id": "port_harman",
  "display_name": "哈尔曼港",
  "lore_seeds": ["没落的渔村", "旧日贸易的余响", "缺盐"],
  "region": "南部沿海",
  "dominant_faction": "走私团",   // ← 推断:缺盐 + 旧贸易 = 走私
  "level_range": [12, 18],
  "tone": "衰落·贪婪",
  "forbidden_names": ["哈兰", "哈尔门"],
  "neighbors": ["salt_marsh", "old_pier"]
}

(推断的值:dominant_faction、tone、display_name / 作者明确指定:其余) 注意:我把 dominant_faction 选成了“走私团”,但规则 1 里邻城 old_pier 已经是“走私团”了。这违反了规则。作为替代候选,建议改用“渔民公会”。

这里重要的是 AI 主动报告了自己的规则违反。从 lore_seeds(缺盐·旧贸易)来看,“走私团”是个有吸引力的推断,但在产线层面上却是冲突。邻城已经是走私团了。我采纳了 AI 的建议,把 dominant_faction 改成“渔民公会”,tone 改成“衰落·坚韧”。“贪婪”是从走私团这个前提里推出来的词,和渔民公会并不相配。

这一次的验证·否决·重新指定,守住了产线的上游。manifest 一旦错了,它下面的 110 名 NPC 和 60 件武器就全都会建立在错误的前提之上。在上游花 5 分钟,就能在下游省下 3 小时。


6.4.3 集成 lint —— 检查工具之间的缝隙

单个 generator 的 lint 只看自己的输出。城市 lint 看城市是否遵守了规则手册,NPC lint 看人设是否保持了 voice 一致性。但我一开始踩的陷阱,不在各个工具内部,而在工具之间。所以产线在单个 lint 之上,还需要多一层——把城市·NPC·道具放在一起读、做交叉验证的集成 lint。

集成 lint 实际会抓到的项目如下。

检查项 比较什么 当时漏掉的
lore 一致性 city.lore_seeds ↔ npc.persona 是渔村却有富商
等级段一致性 city.level_range ↔ item.required_level 12\~18 的城市里有 40 级武器
势力冲突 city.faction ↔ neighbor.faction 两座走私团城市相邻
名称重复 全部 city·npc·item 的名称池 forbidden_names 未收集

下面是跑这个集成 lint 时的部分真实输出。它不做自动废弃,只抛出 alert,交由人来判定。

[集成 lint] port_harman 产线检查 —— 3 alert

ALERT-1(lore 一致性)port_harman
  city.lore_seeds = ["没落的渔村", ...]
  npc[merchant_04].persona = "繁荣贸易港的富商"
  → 可能矛盾。需确认是否为有意变形。

ALERT-2(等级段一致性)port_harman
  city.level_range = [12,18]
  item[blade_legend_07].required_level = 40
  → 超出推荐等级段 28。请重新审视商店摆放。

ALERT-3(名称重复)—— 信息
  npc[fisher_02].name = "哈兰"
  city.forbidden_names = ["哈兰", ...]
  → 与 forbidden_names 冲突。建议重新生成 NPC 名称。

看到 ALERT-1,我犹豫了一下。NPC 是“繁荣贸易港的富商”,这未必就一定是错的。如果这座城市是过去繁荣、如今没落的,那么“曾经富有、如今潦倒的商人”反而是个好故事。于是我把 ALERT-1 判定为“有意的变形”而非废弃,同时提出一行修改:把 NPC 人设改成“抓着昔日繁荣贸易港残迹不放的老商人”。ALERT-2 是明显的事故,我把武器删掉了。ALERT-3 只重新生成了名字。

不是自动 lint 挡住了事故,而是自动 lint 把事故摆到了人的眼前,判定由人来做。这正是 6.1 里说的 L2(规则手册 + AI 辅助)的核心。AI 造出骨架和 alert,人做最后的判定。像 ALERT-1 那样,把“看似错误、其实是好故事”的东西分辨出来,是规则手册做不到的。


6.4.4 用一周周期来运转产线

把工具串起来之后,就需要节奏。产线以一周为单位运转最为稳定。一周,短到不会让评审暴增,又长到不会让回收变慢,正好卡在我桌上日历的一格里。

星期 产线工站 作者时间
周一 编写 lore_seeds 表格(manifest 上游) 半天(5\~7 座城市 × 15\~20 分钟)
周二 城市→NPC→道具 generator 连锁执行 无作者介入
周三 集成 lint + 一次评审(本人) 1 小时(5\~10 分钟/座)
周四 二次抽样评审(叙事主管) 2\~3 分钟/座
周五 并入构建 + 准备下周 seed 很短

周二是产线的心脏。城市 generator 产出 manifest,NPC generator 就接住它,道具 generator 也接住它,Squad 一路做到部署。这条连锁在没有作者介入的情况下在后台运转。作者则在这段时间写主线任务(L0 完全手工)。把工具串起来的真正回报就在这里。工具各自为战时,作者周二要动三次手;串起来后,一次都不用动。

一名作者一周量产 5\~7 座城市,连同附带的 NPC·武器一起。4 周就是 20\~28 座城市。30 座的目标,6 周就达成了。


6.4.5 产线的健康度 —— 每周要看的四个指标

产线是否健康,不靠印象,而靠数字来看。这是每周自动汇总的四个指标。

指标 正常范围 偏离时的信号
集成 lint 通过率 80\~95% 低于 60% 说明 manifest 上游坏了
跨 generator 冲突 每座城市 3\~5 件 10 件以上说明 generator 间的契约破裂
人工评审废弃率 10\~20% 30% 以上说明量产参数不对
单个作者的周期时间 5 天 7 天以上说明认知负担过重

最有产线特征的指标是第二个,跨 generator 冲突数。只用单个工具时,这个数字根本不存在。这个数字一旦突然超过 10 件,就不是某个工具坏了,而是工具之间的契约(manifest)破裂了。通常是改了城市 generator 的 manifest schema,而 NPC generator 还在读旧 schema 时爆掉。没有这个指标,那种事故要到上线才会被发现。

这四个指标每周录入,汇入季度复盘。趋势一旦变差,就把下周的量产城市数从 5\~7 座减到 3\~5 座,去查原因。


6.4.6 让产线崩塌的三种事故

把多个工具串起来后,会出现单个工具时没有的事故。这里记下我常见的三种。

第一,契约不一致事故。 城市 generator 的 manifest 里加了新字段,NPC generator 却不认识这个字段。跨工具冲突指标随之激增。工具各自独立开发时,很容易只更新一边。应对办法是在 manifest schema 里放一个 version 字段,让下游 generator 一旦发现版本不一致就立刻抛出 alert。这不是去催逼人,而是强制契约。

第二,上游污染事故。 一旦 manifest 建立在错误的前提上生成(比如 §6.4.2 里的“走私团”),它下面的一切都会被污染。人工评审废弃率超过 30%,可翻看被废弃的输出,NPC 单个的质量都好好的。单个没问题,错的是前提。应对办法是在 manifest 生成阶段再加一道评审。与其评审下游的 110 个,不如评审上游的 1 个。

第三,模型漂移事故。 LLM 自动更新,输出特性随之改变。城市·NPC·道具三个 generator 会同时晃动。这时要检查最近一周的变更,分析 5 个废弃样本,再调整提示词或上下文。监控一周后确认恢复。

三种事故的共同应对是一样的。不责怪人,而是强化契约。 如果原因是作者只写了一行 lore_seeds,那么与其说“请写三行”,不如在 manifest lint 里加一道强制检查。但这并不意味着人的责任为 0。与系统强化并行,事故模式要在复盘中共享。


6.4.7 作者的时间去了哪里

把产线串起来的真正目的,不是取消作者,而是让作者能专注于招牌内容。下面用一张图画出:工具各自为战时作者的时间是怎么散掉的,串起来之后又是怎么聚拢的。

引入前(工具分离) 引入后(产线整合)

主线任务 30% 招牌内容 20% 量产支线评审 30% 量产 NPC 15% 运营 5%

主线任务 50% 招牌内容 30% 评审 15% NPC 5% 运营 0% —— 产线吸收 主线+招牌内容 50% → 主线+招牌内容 80%

主线和招牌内容上聚起了作者时间的 80%。但这个分配不会自动维持。引入产线后,作者时间往往会全流向评审。所以要每月测量时间分配,一旦主线跌破 50%,就减少量产城市数,把主线时间夺回来。时间分配得靠制度来守。


6.4.8 把产线扩展到其他内容

城市·NPC·道具产线稳定之后,就把同一套骨架扩展到副本·图鉴·线上活动。核心是不制造新的模式。“副本和城市不一样,得用别的结构”这种诱惑总会冒出来。但产线的骨架(共享 manifest → generator 连锁 → 集成 lint → 人工评审)是完全一样的。要替换的只有输入元数据的格式和领域规则手册。

如果是副本,dungeon_manifest.json 里会加上 boss_pattern·encounter_flow 这类字段,Boss 走位之类的领域规则会给集成 lint 再添一行。骨架相同,只有规则不同。保持同一套骨架,作者就不必再学一个新工具,集成 lint 的基础设施也能原样复用。不过这并不是说可以忽视领域特殊性。副本里显然需要城市所没有的走位规则。


6.4.9 运转六个月的结果

这是在我的项目里,把这条集成产线运转六个月的结果,与把城市·NPC·道具 generator 各自单独去跑的时期作对比。下面的绝对数字并非精确统计,而是作者推测(未经验证),但方向和比例遵循实测趋势。

指标 工具分离时期 产线整合之后
量产城市(6 周) 18 座 28 座
周二作者介入次数 每座城市 3 次 0 次
跨 generator 冲突(上线后发现) 每季度 8\~12 件 每季度 2\~4 件
单个作者每季度的主线任务 3 个 8 个
上游评审时间 / 下游评审时间 0 / 3 小时 5 分钟 / 1 小时

最重要的变化在最后一行。工具分离时,上游评审是 0,下游评审是 3 小时。把产线串起来、在上游对 manifest 做评审之后,上游的 5 分钟抹掉了下游的 2 小时。事故不再从工具之间的缝隙漏出去,上线后的一致性事故也从每季度 8\~12 件减到了 2\~4 件。

而且权衡(trade-off)变得明确了。以前每个季度都会绕着“量产很危险”这种抽象争论打转。现在则是在“冲突 -8 件 / 主线 +5 个”这样的具体对比之上做决定。


6.4.10 七种常见的失败

1)不把工具串起来、各自单独去跑。陷阱不在工具内部,而在工具之间产生。

2)没有 manifest 就把 generator 连起来。缺了共享契约,下游就会和上游对不上。

3)用单个 lint 代替集成 lint。单个 lint 看不到跨工具冲突。

4)把周期从 5 天压缩到 3 天。5 天是评审的安全余量。

5)不评审上游(manifest),却去评审下游。与其看下游的 110 个,不如看上游的 1 个。

6)把事故只归为人的责任。强化契约·自动化规则才是答案。

7)搭好产线之后却“不用”。强制执行一周周期,和工具本身一样重要。


动手试试

setup. 准备好城市 generator(6.2)和 NPC generator(6.3)。为两者约定一份共享的 city_manifest.json schema。字段至少要有 lore_seeds·region·faction·level_range·forbidden_names·tone·version

prompt. 直接沿用上面正文里的 manifest 生成器提示词。核心是最后两条规则:“不要与邻城势力重复”(防止跨工具冲突)和“区分标注推测的值和明确指定的值”(可评审性)。造出城市就产出 manifest,再把 NPC·道具 generator 连接成以这份 manifest 为输入。

verify. 把集成 lint 跑一遍。它会交叉检查 lore 一致性·等级段一致性·势力冲突·名称重复这四项。alert 一旦冒出来,不要自动废弃,交由人来判定。分辨“看似错误、其实是好故事”(没落贸易港里的老商人),是人的职责。

单人精简版. 就算工具只有城市·NPC 两个,产线也能成立。在一张电子表格里,按城市写下各自的 lore_seeds·level_range·forbidden_names,只要把那一行整段贴进 NPC generator 的提示词,就已经起到了 manifest 的作用。集成 lint 哪怕只有城市-NPC 的 lore 一致性这一行,也能挡住那个陷阱。没有宏大的基础设施,只要守住“在工具之间放进一行契约”这一条原则,产线就能开始运转。


本章要点

7.1 程序化关卡设计总纲

副本第47号房间的出口被堵死了。构建通过了,QA 也通过了。玩家在 Boss 房间前对着墙站着的截图被发到社区,是在上线第三天。那个房间是两个季度前把手工制作的房间复制粘贴过来的,复制的过程中,东侧的一条通路没有连接信息,只留下了视觉外观。没有人验证过它。当时也没有能验证它的工具。

本章讲的是如何构建一种结构,让这类事故在构建阶段被自动拦截。关键不在于绘制空间的手上功夫,而在于用规则来运营附着于空间的数据的方式。


关卡设计的工作现场更接近制图室。图纸一张一张出自人手,但图纸之间的一致性、复用与验证,由图纸柜的运营规则决定。手工画一个副本谁都会,而把100个副本用一致的难度曲线和没有死路的图运营起来,靠的不是手艺,而是系统的问题。

在笔者担任策划总监的项目A(面向国内 + 东南亚的 MMORPG,中等规模(10\~50人)团队,移动端优先)中,这套系统的名字就是一份名为 Procedural_Level_Design_Master 的文档。本章讲的是这份文档整合了什么、AI 介入到哪一步、又在哪里停手。笔者曾主导一款每一局都会重新生成副本的移动端 Roguelite RPG 的策划,以规则运营程序化空间的这段经验,构成了本章的底色。

7.1.1 两条路 —— 是生成空间,还是运营空间的元数据

关卡自动化分为两个方向。一个是对空间本身进行程序化生成。BSP 分割(Binary Space Partitioning,把空间递归地二等分来布置房间的经典手法)、wave function collapse、drunken walk 网格这类传统 PCG(Procedural Content Generation,程序化内容生成)都属于这一类。另一个是运营空间的元数据 —— 房间标签、连接性、难度标签、事件槽位。

传统 PCG 擅长第一种。在 Roguelike 或沙盒这类以"每局都是新地图"为游戏性核心的品类里,第一种才是正解。但 MMORPG 不同。玩家会把同一个副本刷上几十遍,刷到动线都背下来。所以副本必须是手工打磨的固定空间,而自动化能切入的位置不是空间本身,而是让这个空间可被运营的元数据

元数据为什么是运营的脊柱,按产出物逐项来看就一目了然。

产出物 若没有元数据
数十个副本池 无法检索哪个房间在哪里,无法复用
难度曲线验证 没有各房间的难度标签,无法绘制曲线
任务·Boss 位置自动布置 没有事件槽位元数据,只能手动输入坐标
美术团队同步 没有房间类型 → 美术资源集的映射,视觉不一致
玩家动线·停留时间测量 无法进行基于房间 ID 的遥测

没有元数据的副本能构建出来,却无法运营。就像藏书满架却没有索引的图书馆。这正是本章聚焦于"空间元数据运营"的原因。

7.1.2 总纲文档整合了什么

Procedural_Level_Design_Master 把四项标准捆到一份文档里:房间元数据格式、房间标签词典、连接性规则、验证检查清单。先看看这四项散落各处时会发生什么。五名设计师各自从不同文件里参照格式,type 字段就有人写 combat、有人写 Combat、有人写 battle_room。检索坏掉,统计坏掉,最终自动化也坏掉。

把这四项标准按 Layer 归位,各自的位置就很清晰。格式、词典、规则位于支配生成的规则手册(L1);生成出的房间正文位于内容层(L2);表格值位于数据层(L3);验证位于构建·QA 门禁(L4)。

L0 愿景 关卡概念·节奏意图(不变锚点,每次生成·验证都注入) —— art_pack 色调,难度意图

L1 系统 规则手册 —— 房间元数据格式 · 标签词典 · 连接性规则 —— 总纲文档捆绑之处

L2 内容 附着了元数据的房间正文(生成并打磨过的空间)

L3 数据 房间尺寸·连接表·事件槽位 ID·敌人数据

L4 构建·QA 图验证 · 难度曲线验证 · 美术资源集一致性门禁

所谓总纲文档整合四项标准,并不是"把正文全塞进一个文件",而是"把规则汇聚到 L1 这个位置"。正因如此,后文将出现的自动化才能架设在 Layer 边界之上(分离一旦崩塌会发生什么,7.1.11 会讲)。

7.1.3 房间元数据格式 —— 自动化附着的输入位

一个房间遵循以下格式。这份格式就是自动化的输入接口。

room_id: dungeon_021_room_07
dungeon: dungeon_021_silvermark_library
type: combat_room          # combat / puzzle / lore / safe / boss
size: medium               # small / medium / large
difficulty_label: hard_for_level_28
tags: [scholar_theme, vertical_layout, water_hazard]
connections:
  - target_room: dungeon_021_room_06
    type: door
    direction: south
  - target_room: dungeon_021_room_08
    type: passage
    direction: east
event_slots:
  - slot: enemy_spawn_1
    constraints: [scholar_enemy, level_28]
  - slot: lore_object_1
    constraints: [scholar_lore]
movement_complexity: 4     # 1~5
estimated_clear_time_sec: 90
art_pack: scholar_library_v2

每个字段都有一个以上的自动化消费方。type 用于副本池统计和难度计算,tags 用于检索·复用·美术资源集映射,connections 用于图验证(死路检查),event_slots 用于任务·Boss 自动布置。没有消费方的字段就不放进格式里 —— 只会增加输入成本,却没有价值。

7.1.4 房间标签词典 —— 小而正交

标签是元数据的检索键。一旦无限增殖,检索就会坏掉。抽屉上贴了200个标签,就没法找到什么在哪里。所以按5个类别 × 每个类别约6个 enum,合计约30个来运营。

类别 enum 数 示例
theme 8 scholar_theme, ruins_theme, forest_theme …
layout 5 vertical_layout, horizontal_corridor, open_arena …
hazard 6 water_hazard, fire_hazard, falling_hazard …
interaction 4 puzzle_required, lever_activation …
narrative 7 flashback_trigger, dialogue_zone …

一个房间的标签不超过5个,正常是3\~4个。要新增标签,必须通过四道门禁:每季度至少是5个房间的使用候选;无法用现有标签组合表达;检索·美术资源集映射的用途明确;运营1个月后仍能维持5个房间。最后一条是关键。临时造出来的标签只用一次就被弃置,词典就会被污染。

7.1.5 程序化关卡管线 —— 从规则手册到验证

到目前为止的这些标准如何串成一条流程,这条连接线正是本章所支撑的骨架。它是一条从规则手册出发、经过 AI 辅助变奏、以护栏验证收尾的管线。

flowchart TD A["L0 愿景 —— 副本概念·节奏意图"] --> B["L1 规则手册\n标签词典 · 连接性规则 · 槽位规则"] B --> C["房间骨架布置\n设计师手工 + 编辑器"] C --> D["元数据自动提取\nroom_id · connections · type · size"] D --> E["AI 辅助变奏\ntags 提取 · art_pack 映射建议"] E --> F{"词典强制检查\n词典外标签?"} F -->|词典外| E F -->|通过| G["设计师评审\ntags · difficulty_label 确定"] G --> H["L4 图验证\n可达性 · 死路 · 环路 · 分支"] H -->|违规| C H -->|通过| I["难度曲线验证\n房间难度标签累加"] I --> J["美术资源集一致性门禁"] J -->|通过| K["构建 —— 登记到副本池"] J -->|不一致| E classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class B,D,F,H,I,J code; class E ai; class C,G human; class K pass;

这条管线有三个特点值得点明。第一,规则手册(L1)位于一切生成的上游。第二,AI 只在规则手册所定义的词典内变奏 —— F 门禁会把词典外的输出退回。第三,验证(H·I·J)被固定为构建前的门禁,违规由代码拦截,而不依赖人的注意力。第47号房间的事故,正是因为没有 H 门禁才发生的。

7.1.6 连接性规则 —— 用图来验证的护栏

房间元数据的 connections 字段把整个副本变成一张有向图。一旦成为图,验证就是自动的。

检查 违规时的处理
起始房间 → Boss 房间可达 以构建失败拦截
死路(出口1个 + non-safe_room) alert —— 设计师复核
双向连接一致性(有 A→B 却没有 B→A) 自动校正
环路长度 —— 2\~3个房间的短循环 alert
分支宽度 —— 同时4个以上的分支 设计师复核

测量脚本是下面这个样子。它是在标准图算法(最长路径·平均出度·循环计数·最短路径)之上,套了一层副本术语的薄封装。

# level_graph_metrics.py
def measure(dungeon):
    graph = build_graph(dungeon.rooms)
    return {
        "depth":            longest_path_length(graph),
        "branching_factor": avg_out_degree(graph),
        "loop_count":       count_loops(graph),
        "dead_ends":        count_dead_ends(graph),
        "boss_reachability": shortest_path(graph.start, graph.boss),
    }

五个指标会以可与其他副本比较的形式输出,用作副本池的多样性指标。不过指标多样并不意味着副本有趣。指标是用来拦截事故的,不是用来保证趣味的。零死路并不能保证趣味。趣味来自设计师的洞察,而图验证只是托住底,别让那份洞察被事故淹没。

7.1.7 实操示例 —— 把 tags 提取交给 AI、拒绝、再请求

自动化里,人最常想撒手不管的部分就是 tags 输入。给100个房间打标签很枯燥,只看房间截图连人都会犯迷糊。这种重复且判定标准明确的活儿,恰恰是 AI 适合承接初稿的位置。本节把实际跑过的那套工作流 —— 提示词、AI 被拒绝的输出、人的再请求 —— 原样铺开,不加任何修饰。

第一次提示词:

[输入]
- 房间截图:(附件)
- 房间面积:18m × 12m,天花板高9m(垂直2层结构)
- 布置的敌人:scholar_phantom × 3, water_elemental × 1
- 相邻房间 type:lore_room(西侧), combat_room(东侧)
- 房间内有浅水洼

从下面的词典里,只挑3~5个适合这个房间的标签。不要造词典外的标签,
只用逗号分隔标签 enum。

[标签词典 30个]
theme: scholar_theme, ruins_theme, forest_theme, cave_theme,
       crypt_theme, swamp_theme, mechanical_theme, celestial_theme
layout: vertical_layout, horizontal_corridor, open_arena,
        maze_layout, bridge_layout
hazard: water_hazard, fire_hazard, falling_hazard,
        poison_hazard, crush_hazard, dark_hazard
interaction: puzzle_required, lever_activation, key_gate, hidden_passage
narrative: flashback_trigger, dialogue_zone, lore_object_zone,
           ambush_setup, ritual_site, vista_point, rest_anchor

Claude 第一次输出(原始未加工):

scholar_theme, vertical_layout, water_hazard, ambush_setup, two_story_layout

这里直接暴露出两个问题。一是 two_story_layout 是词典里没有的标签——模型被"垂直2层结构"这个输入带偏,造出了词典外的词。二是标签已经触到5个的上限,而 vertical_layouttwo_story_layout 在语义上重叠,相当于在 layout 类别里占了两格。

人的拒绝判断。 two_story_layout 违反了词典强制,属于自动拒绝对象(管线的 F 门禁)。ambush_setup 则两可 —— 虽然布置了3只敌人,但从截图看,究竟是伏击布局还是单纯的战斗房间,并不清楚。这有可能是模型过度解读了。相邻房间都是 lore/combat,叙事性伏击触发的依据很薄弱。拒绝。

第二次再请求:

two_story_layout 词典里没有,而且和 vertical_layout 重叠。去掉。
ambush_setup 也去掉 —— 有敌人不等于就是伏击。
// (意图:narrative 标签只在叙事意图明确时使用,单纯战斗已由 type 表达)
空位不必硬凑。3~4个才正常。重新打标签。

Claude 第二次输出:

scholar_theme, vertical_layout, water_hazard

这次减到了3个,全部在词典之内,也没有类别重复。人接受了这个结果。房间格式里的 tags: [scholar_theme, vertical_layout, water_hazard] 就这样定了下来。

这个实操示例的教训有两条。第一,AI 会对输入里的某个细节("2层")过拟合,从而冲出词典之外 —— 词典强制门禁必须在代码层面把它拦住。第二,AI 有填满空位的倾向 —— 不明说"不必硬凑",它就会把5格全填满。这两种失败都很常见,而两条处方都不能靠提示词,得由规则手册(词典 + 上限)来强制,才稳定。

7.1.8 元数据量产 —— 谁来填,谁来评审

设计师手工填一个房间的元数据要花5\~10分钟。一个副本(20\~30个房间)就是2\~5小时,100个副本就是200\~500小时(笔者估算,未经验证 —— 以每个房间的平均输入时间 × 房间数换算出的上限值)。全部手工填,设计师就成了元数据输入的奴隶。

所以要按领域划分填写的主体。

领域 填写主体
room_id · dungeon · connections 编辑器自动提取(L3)
type · size 基于房间面积·连接数的自动分类
tags AI 辅助 + 设计师评审(7.1.7)
event_slots 按房间 type 的规则手册
difficulty_label 房间内敌人数据汇总的自动计算
art_pack 房间 type · 副本 theme 映射

设计师亲手确定的,大概只有 tags 的评审和 difficulty_label 的最终审批。其余由工具来填,人来评审。自动化的目的,是把设计师从输入中解放出来,让他回到节奏、标志性房间、复用策略这些判断上。

7.1.9 房间复用及其陷阱

总纲标准最大的效用是房间复用。有30个能用标签检索的房间,就能组合出5\~10个副本。但复用比例一高,副本就会变得乏味。所以复用要连带配上护栏。

护栏 定义
一个房间最多出现在5个副本中 自动追踪出现频次
第二次出现时强制视觉变奏 更换灯光·道具
Boss 房间·标志性房间禁止复用 用 flag 强制
追踪复用房间的负面反馈 玩家遥测

复用是降低成本的手段,不是目的。一旦把复用率本身当成 KPI,玩家体验就会变得单调。0%(所有房间都是新的)会让量产成本暴涨,超过70%则各个副本彼此难以区分。从经验看,30\~40% 这个区间是成本与多样性的平衡点(方向性观察,精确阈值因项目而异)。

7.1.10 常见失败与处方

模式 处方
元数据格式被5个人解读出5种 用总纲文档在 L1 统一
标签增殖到50\~100个 30个词典 + 四道门禁
不做死路检查就构建 把图验证设为构建门禁
设计师手工处理所有元数据 编辑器提取 + AI 辅助
AI 生成词典外标签 用词典强制门禁自动拒绝
复用0%或70%+ 30\~40% 区间 + 变奏护栏

7.1.11 Layer 分解是程序化关卡生成的前提

到目前为止,以规则手册·生成·验证展开的 7.1.2\~7.1.6 的结构本身,就是 Layer 分解的产物。"Layer 分解是程序化生成·自动化的前提"这一一般命题(L0 锚点 → L1 规则手册 → L2 正文 → L3 数值 → L4 门禁,揉成一团生成就会崩塌),已在 §6.6 讲过。这里把它应用到关卡元数据运营上。

没有这层分离,房间布置·BSP·节奏·叙事触发器就会混在一个文件里,每移动一个房间,节奏意图·事件槽位·连接性图就会同时坏掉。就像制图室·材料仓库·验收室全堆在一张桌子上,抽走一张图纸,材料送货单和验收单也跟着被带出去。所以 7.1.7 的 AI 辅助能运作,也是靠 Layer。房间 ID·连接性在编辑器(L3 自动提取)填,标签在 AI(L1 词典强制)填,difficulty_label 在汇总(L3→L4)填。自动化是架设在 Layer 边界之上;若架在一整团之上,第一个季度内事故就会暴增,工具本身随之被废弃。

不过,这并不意味着一开始就得把五格抽屉完美备齐。分离要渐进,接口要窄,这是原则。第一个季度里,哪怕只分出 L1 规则手册(标签词典 + 连接性规则)和 L3 表格(房间元数据表),自动化也就有了切入的位置。L0 节奏意图和 L4 验证门禁,在后续季度里逐步填。标准统一了,自动化才有切入的位置;自动化切入得越多,设计师就越是从一个房间的手工操作里脱身,转而专注于节奏、标志性、复用的判断。


本章要点


动手试试

setup. 选一个副本,为每个房间做一张只含 room_id · type · connections · tags 四个字段的 YAML 表。标签则先把5个类别、约30个 enum 的词典固定在一张纸上。

prompt. 放入房间截图 + 面积 + 敌人种类 + 相邻房间 type,用"只从这个词典里挑3\~5个标签,禁止词典外标签,不要硬凑空位"来请求(照搬 7.1.7 的提示词)。

verify. (1) 如果 AI 输出里有词典外标签,就拒绝并再请求。(2) 用 connections 构建图,检查起始→Boss 的可达性和死路 —— 只要出现一处违规,就把那个房间标记为不可构建。

单人精简版

如果你是没有工具基础设施的单人开发者,就把总纲文档从一页 Markdown 开始。标签词典30行、连接性规则5行、验证检查清单5行,就够了。图验证方面,房间在10个以下的话,在纸上画箭头、只用眼睛确认死路,也能拿到80%的效果。关键不是工具,而是"给房间附上数据,再用规则检查这些数据"这个习惯本身。工具等到房间超过50个、手工检查变得吃力时,再引入即可。

下一章预告

7.2 BehaviorTree 编辑器 —— 人与 AI 共同编辑并验证 BT json 的实操记录(worked transcript)

一只见习法师贴着玩家挥刀砍杀。这是一个按远程法术施法者设计的 NPC。它的 HP 薄如纸片,近战中只要挨一下就会死,可它却毫无拉开距离的打算。构建日志里没有任何报错。在编辑器里重新打开 BehaviorTree,节点也都好好地连着。盯着看了一个小时,才找到原因:后退分支的距离条件填的不是 5,而是 0.5。本该在敌人进入 5 米以内时就逃跑,可现在只有对方逼近到 0.5 米——也就是几乎贴脸——后退分支才会触发。

问题只是一个数字。在图形化节点编辑器里,这个数字要展开节点内部的面板才看得到,而且不会留在变更历史里。没有办法追溯是谁在什么时候改了这个值。从那天起,笔者的项目A 就不再用图形方式,而是改用 json 来处理 BehaviorTree。本章记录的,正是人与 AI 共同编辑这份 json、再由机器自动验证的一个完整周期。


7.2.1 BT(BehaviorTree,行为树)从手中失控的地方

行为树是定义敌方 NPC 战斗、移动、反应的事实标准结构。选择器(selector)按优先级依次尝试各分支,序列(sequence)则把条件与动作按顺序串起来。结构本身很简单。问题在于规模。

在笔者的项目A 中,一个敌方 NPC 的 BT 大约由 50\~200 个节点构成,而需要运营的 NPC 超过了 100 个。相乘之后,BT 节点总量就达到数万量级。在这个规模下,总会有那么一刻——面对"改动这个后退模式会影响到哪些 NPC?"这样的问题,人已经无法回答了。这就像桌上摊开着一百本笔记,改了第一本里的一行,却要用眼睛去追剩下九十九本中哪里会受到波及。

笔者从图形化 BT 转向 json 时,提出了四点要求。

以文本形式保存(json) 用 git diff 追踪到每一行改动 "一个数字"的事故会留在历史里

节点元数据标准化 用 category、tags 检索与复用 "查找相似 BT"变成一行查询

subtree 引用(引用复用) 一个公共模式由多个 BT 共享 只改一处 → 批量应用,而非复制粘贴

变更影响自动可视化 subtree 的修改会波及哪些 BT 由脚本算出,而非靠人推测

商用游戏引擎自带的 BT 编辑器集成方便,可视化调试能力强。只是它往往以二进制(binary)资产的形式保存,文本 diff 与变更影响追踪较弱。笔者的项目A 面向的是运营 BT 超过 100 个的长期运营(LiveOps)游戏,因此选择了自行开发独立的 json BT 格式与编辑器。有一点要讲清楚:这并非所有团队的正确答案。如果运营 BT 不足 50 个,直接沿用引擎自带的编辑器几乎总是更划算。自研的正当性,本章末尾会再谈。


7.2.2 BT json —— 用文本描述一个敌人的行为

先看看成品的样子。下面是学者公会远程支援型 NPC 的一部分 BT。核心有两点:一是所有行为都是文本,git 能逐行追踪;二是用 subtree_ref 来引用公共模式。

{
  "bt_id": "bt_scholar_archer_v3",
  "category": "ranged_combatant",
  "tags": ["scholar_faction", "ranged", "support"],
  "description": "学者公会远程支援型。保持距离 + 后退优先。",
  "root": {
    "type": "selector",
    "children": [
      {
        "type": "sequence",
        "name": "low_hp_retreat",
        "children": [
          {"type": "condition", "fn": "hp_below", "param": 0.3},
          {"type": "subtree_ref", "id": "subtree_retreat_to_ally"}
        ]
      },
      {
        "type": "sequence",
        "name": "kite_pattern",
        "children": [
          {"type": "condition", "fn": "enemy_in_close_range", "param": 5},
          {"type": "action", "fn": "move_away", "param": {"distance": 8}}
        ]
      },
      {"type": "subtree_ref", "id": "subtree_ranged_attack_pattern"}
    ]
  }
}

把这棵树画成图,就是选择器自上而下尝试三个分支的结构。请注意:开头那个 bug——enemy_in_close_rangeparam5 还是 0.5——在 json 里变成了一眼就能看见的一行。

flowchart TD R["selector<br/>(自上而下的优先级)"] R --> A["sequence: low_hp_retreat"] R --> B["sequence: kite_pattern"] R --> C["subtree_ref:<br/>subtree_ranged_attack_pattern"] A --> A1["condition: hp_below 0.3"] A --> A2["subtree_ref:<br/>subtree_retreat_to_ally"] B --> B1["condition: enemy_in_close_range 5"] B --> B2["action: move_away dist=8"] style C fill:#e8f0fe,stroke:#4285f4 style A2 fill:#e8f0fe,stroke:#4285f4 style B1 fill:#fce8e6,stroke:#ea4335
元素 作用
bt_id git diff、变更追踪键
categorytags 检索、复用单位
subtree_ref 引用公共模式(改一处 → 更新多个 BT)
description 供策划、剧情作者共享

标红的 enemy_in_close_range 5 就是开头那个让人耗掉一个小时的节点。在 json 里,一次代码评审就能揪出来。


7.2.3 subtree 库 —— 用引用取代复制粘贴

超过 100 个敌人的行为里存在重复的模块。比如"退到盟友身后""退到掩体后""远程攻击模式"这类。如果把它们复制进每个 BT,那么改一处后退逻辑时,就得手动找出一百个地方逐一修改。因此,公共模式被拆分成独立的 subtree 文件,只用 subtree_ref 来引用。

subtree_library/
├── retreat_patterns/
│   ├── subtree_retreat_to_ally.json
│   ├── subtree_retreat_to_cover.json
│   └── subtree_retreat_random.json
├── attack_patterns/
│   ├── subtree_ranged_attack_pattern.json
│   ├── subtree_melee_combo.json
│   └── subtree_aoe_attack.json
└── reaction_patterns/
    ├── subtree_react_to_ally_death.json
    └── subtree_react_to_player_taunt.json

这样安排后,"改动这个 subtree 会影响到谁?"这个问题就不再是人的推测,而成为脚本的输出。影响追踪器很简单:打开所有 BT,收集引用了该 subtree 的 BT 的 bt_id

# bt_impact_tracker.py
import json, glob

def has_subtree_ref(node, target_id):
    if isinstance(node, dict):
        if node.get("type") == "subtree_ref" and node.get("id") == target_id:
            return True
        for child in node.get("children", []):
            if has_subtree_ref(child, target_id):
                return True
    return False

def find_affected_bts(subtree_id):
    affected = []
    for bt_file in glob.glob("bts/*.json"):
        bt = json.load(open(bt_file, encoding="utf-8"))
        if has_subtree_ref(bt["root"], subtree_id):
            affected.append(bt["bt_id"])
    return affected

# 使用
affected = find_affected_bts("subtree_ranged_attack_pattern")
# → ["bt_scholar_archer_v3", "bt_ranger_v2", "bt_sniper_v1", ...]

在笔者的项目A 中,这个函数被绑定到了变更请求(Pull Request)阶段。只要有人改动 subtree 文件,受影响的 BT 列表就会自动作为评论附到 PR 上。评审者在合并前就能看到"只改了一行后退模式,却让 12 个远程敌人全部发生了变化"这一事实。


7.2.4 实操记录 —— AI 撰写新 BT 初稿的一个周期

从这里开始,是本章分量最重的部分。所谓实操记录(worked transcript),就是完整保留的真实操作过程记录:本节把新敌方 NPC"见习法师"的 BT 初稿交给 AI,再由人对其输出进行验证、否决、重新请求——这一整个周期不加修饰、原样呈现。之所以不做顺滑删减,是有原因的:AI 在第一次输出里错在哪里、怎么错的,这种失败的纹理正是本章想要传达的全部。

Step 1 —— 人给出的提示词(全文)

[敌方 NPC 概念]
学者公会的见习法师。HP 低,法术伤害高。
优先规避近战。退到盟友身后后再施放法术攻击。

[category] ranged_combatant

[参考 BT] bt_scholar_archer_v3
(结构相似。只是把弓箭攻击换成法术攻击)

[已定义的函数 enum]
condition: hp_below, enemy_in_close_range, ally_nearby, mana_below
action: move_away, cast_spell, retreat, wait
subtree_ref id(可用列表):
- subtree_retreat_to_ally
- subtree_ranged_attack_pattern   # 基于弓箭。不用于法术
- subtree_magic_attack_pattern

请仅按这个概念编写 BT root 节点的 json。尽量使用 subtree_ref,直接节点尽量少,
函数只能取自上面的 enum。不要添加概念中没有的行为。不要说明,只要 json。

Step 2 —— Claude 的第一次输出(未加工,原样呈现)

{
  "bt_id": "bt_scholar_apprentice_v1",
  "category": "ranged_combatant",
  "tags": ["scholar_faction", "magic", "fragile"],
  "description": "学者公会见习法师。近战规避 + 退到盟友身后 + 法术攻击。",
  "root": {
    "type": "selector",
    "children": [
      {
        "type": "sequence",
        "name": "low_hp_retreat",
        "children": [
          {"type": "condition", "fn": "hp_below", "param": 0.4},
          {"type": "subtree_ref", "id": "subtree_retreat_to_ally"}
        ]
      },
      {
        "type": "sequence",
        "name": "avoid_melee",
        "children": [
          {"type": "condition", "fn": "enemy_in_close_range", "param": 6},
          {"type": "action", "fn": "move_away", "param": {"distance": 10}}
        ]
      },
      {
        "type": "sequence",
        "name": "mana_recover",
        "children": [
          {"type": "condition", "fn": "mana_below", "param": 0.2},
          {"type": "action", "fn": "wait", "param": {"frames": 120}}
        ]
      },
      {"type": "subtree_ref", "id": "subtree_ranged_attack_pattern"}
    ]
  }
}

Step 3 —— 人的验证与否决

拿到输出后,先过一遍 lint(下一节会讲),再用眼睛读。有三处出了问题。

  1. 最后一个分支错了。 subtree_ranged_attack_pattern 是基于弓箭的攻击模式。尽管 enum 注释里写了"不用于法术",AI 还是拿来了名字相似的弓箭模式。见习法师应该用 subtree_magic_attack_pattern。—— 否决。

  2. 添加了概念中没有的行为。 mana_recover 分支(mana_belowwait 120frame)是概念里只字未提的行为。尽管提示词里明确写了"禁止添加未指定的行为",AI 似乎还是判断"既然是法师,管理法力就很自然",于是把它塞了进来。看着合理,却违反了规格说明。—— 否决。

  3. 后退阈值可疑。 hp_below 0.4 比参考 BT(0.3)更早开始后退。见习法师确实更脆弱,所以这可能是有意为之。但 AI 从未给出依据,因此这是该由策划决定的事项。—— 保留,待策划确认。

Step 4 —— 人的重新请求

只改这三处。最后一个分支 subtree_ranged_attack_pattern 是弓箭模式,
换成 subtree_magic_attack_pattern;mana_recover 分支概念里没有,整段删除;
hp_below 0.4 改回参考 BT 的 0.3——如果有理由用 0.4,就在 description 里写一行。
其余保持不变,只要 json。

Step 5 —— Claude 的第二次输出(修订版)

{
  "bt_id": "bt_scholar_apprentice_v1",
  "category": "ranged_combatant",
  "tags": ["scholar_faction", "magic", "fragile"],
  "description": "学者公会见习法师。近战规避 + 退到盟友身后 + 法术攻击。HP 低,后退阈值保持 0.3(与 archer 相同)。",
  "root": {
    "type": "selector",
    "children": [
      {
        "type": "sequence",
        "name": "low_hp_retreat",
        "children": [
          {"type": "condition", "fn": "hp_below", "param": 0.3},
          {"type": "subtree_ref", "id": "subtree_retreat_to_ally"}
        ]
      },
      {
        "type": "sequence",
        "name": "avoid_melee",
        "children": [
          {"type": "condition", "fn": "enemy_in_close_range", "param": 6},
          {"type": "action", "fn": "move_away", "param": {"distance": 10}}
        ]
      },
      {"type": "subtree_ref", "id": "subtree_magic_attack_pattern"}
    ]
  }
}

第二次输出通过了 lint,策划在检查时把 avoid_melee 的距离 6 对齐 archer 下调一档到 5,随后交给了模拟验证关卡。这里能学到的东西很清楚:AI 能快速填出 BT 的骨架,但几乎每次都会犯两类错误——"拉来名字相似的错误 subtree"和"把貌似合理的额外行为在无规格依据的情况下塞进来"。这两类错误只能靠人的眼睛和 lint 关卡拦下。因此 AI 的输出是初稿,而非最终稿。


7.2.5 自动 lint —— 机器先一步抓住人会漏掉的东西

BT 直接关系到用户体验。如果敌人贴脸却不逃跑的事故就这样上线,最终会以评分的形式反噬回来。所以在合并前,先由机器检查一遍。

检查项 违反时
不可达节点 alert(选择器中永远触及不到的分支)
无限循环风险 拦截(没有退出条件的 sequence 循环)
subtree_ref 目标不存在 拦截
动作/条件函数在 enum 之外 拦截
节点数激增(>500) alert(建议拆分 BT)
同一 category 内 BT 响应时间偏差 alert(疑似平衡回归)

最后一项是这套 lint 的独到之处。如果同属 ranged_combatant 的五个 BT,在模拟中的平均响应时间明显拉开,那就是有人不知不觉破坏了某一个的平衡的信号。它是用统计手段去捕捉静态检查抓不到的"苗头"的装置。

静态 lint 之后是模拟验证。无需打出实际构建,直接在模拟器里把 BT 跑 1,000 次,统计出各项数据。

测量项 正常范围
平均生存时间(对标准玩家) 按 category 的基准值
攻击模式多样性(熵) 0.6 以上
后退/接近行为比例 按 category 的基准值
单次行为平均耗时 frame 60 frame 以下

不用打包出实际构建,在 5\~10 分钟内就能看出"这个 BT 是不是死得太快""是不是只重复一种行为"。一旦出现异常信号,就改 json、重跑模拟。这个周期从以天计缩短到以分钟计,正是 json 化的实际收益。

flowchart LR P["人/AI<br/>编辑 BT json"] --> L{"静态 lint"} L -->|拦截| P L -->|通过| R["策划评审"] R -->|否决| P R -->|批准| S{"模拟 1,000 次"} S -->|异常信号| P S -->|正常| M["合并 + 应用到构建"] style L fill:#fef7e0,stroke:#fbbc04 style S fill:#fef7e0,stroke:#fbbc04 style M fill:#e6f4ea,stroke:#34a853

7.2.6 度量 —— 什么减少了

下面用表格列出笔者的项目A 引入前后的对比。绝对数值会随团队规模、游戏类型而变化,因此属于笔者的推测(未经验证)。但方向与比例,是实际运营中观察到的原样结果。

项目 引入前(直接用引擎自带 BT) 引入后(json + 编辑器)
编写一个新敌人的 BT 1\~2 天 2\~4 小时
掌握 BT 变更影响 依赖推测与经验 自动(subtree 影响列表)
变更后验证 需要实际构建 模拟 5\~10 分钟
运营 100 个敌方 NPC 3 名策划全职 1\~2 名策划
上线后 BT 事故(异常行为) 每季度 10\~15 起(笔者推测) 每季度 2\~4 起(笔者推测)

最有意义的是,最后两行同时发生了变化。通常削减人手会导致质量下降。而这里,策划人数减少的同时,事故也减少了。因为原本由人手动追踪的变更影响与验证,被机器接管了。自动化的价值,与其说在于"变快",不如说在于这种"人减少的同时质量反而变好"。


7.2.7 自研,还是借用现成方案

读完本章就下结论"我们也来做一个 json BT 编辑器吧",那可就麻烦了。笔者的项目A 之所以选择自研,是因为特定条件恰好都凑齐了。

选项 优 / 劣
直接使用引擎自带 BT 集成容易 / json 转换、diff 弱
借用外部 BT 库 标准化优势 / 学习曲线、定制受限
自研 json BT 编辑器 + 运行时 自由度、可追踪性最高 / 开发成本大

项目A 选择第 3 项的依据有四点。

开发成本约 1\~2 个月。只有当运营 BT 达到 100\~300 个、且长期运营周期足够长时,才收得回来。在 30\~50 个的规模下是回不了本的。也就是说,自研的投资回报(ROI, Return On Investment)只有在规模和运营周期两者都有保障时才会出现。如果是小团队,那就只从本章带走这几条原则——"用 json 保存""用 subtree 引用""AI 输出要通过 lint + 评审关卡"——而工具则应当搭在自带编辑器或外部库之上使用。


7.2.8 常见的失败

模式 处方
只把 BT 当作二进制资产来管理 保存为 json,让 git 追踪重新可用
不用 subtree,在每个 BT 里复制粘贴相同模式 拆到 subtree 库里去引用
手工做 BT 影响追踪 把影响分析脚本绑定到 PR 上
不用模拟,只在实际构建里验证 运行一个与构建分离的模拟器
不经评审就使用 AI 输出的 BT 让它通过 lint + 策划 + 模拟三重关卡
不衡量自研 ROI 只在 100 个以上、且长期运营时才自研

本章要点


动手试试

这是小团队今天就能尝试的最小周期。

setup —— 把一个正在运营的敌方 NPC 的 BT 亲手写成 json(bt_idcategorytagsroot)。把一块公共的后退/攻击模式拆到 subtree_library/ 里,用 subtree_ref 来引用。

prompt —— 把一个相似的新敌人交给 AI。直接套用上面实操记录里的提示词骨架(概念 + category + 参考 BT + 可用函数 enum + "禁止添加未指定的行为" + "只要 json")即可。

verify —— 让 AI 的输出通过三道关卡后再合并:(1)过滤 enum 之外的函数与不存在的 subtree 的 lint,(2)人眼,(3)模拟或游戏内的简短验证。务必确认 AI 是否塞进了"名字相似的错误 subtree"和"貌似合理却在规格之外的行为"。

单人精简版

如果没有余力去做编辑器,那么工具只要文本编辑器加 git,再加一个 30 行的 bt_impact_tracker.py 就够了。把用自带编辑器写好的 BT 导出成 json 上传到 git,只把 subtree 拆成独立文件来引用。再把影响追踪脚本挂到提交钩子(commit hook)上,哪怕是一个人,也能把"改动这个后退模式会让哪个敌人发生变化"从推测变成输出。仅凭这一个习惯,开头那个"一个数字耗掉一个小时",就缩短成了代码评审里的一行。


下一章预告

7.3 副本·野外模式库

在一次副本评审会上,一位新人关卡设计师把自己做的一个副本投到了屏幕上。狭窄的走廊、从后方紧追的快速敌人、在岔路口的闪避抉择。这是个做得不错的副本。问题在于,它和我们此前已经在十一个不同副本里做过的东西有着微妙的差异。敌人的追击速度、陷阱触发的时机、岔路口出现的时点,没有一处是相同的。这位新人相信自己做出的是名为"追击副本"的同一种体验,但用户实际感受到的手感却副本各异。

那天我们做的决定很简单:把"走廊追击"这种体验精确地定义一次,然后把这个定义固化下来。下次无论谁做追击副本,都不再从零开始搭建,而是取出这个固化好的定义来用。这就是模式库的开端。

如果说房间是空间单元、BehaviorTree(行为树)是行为单元,那么模式就是把空间、行为与事件捆在一起的运营单元。一个模式被多个副本复用后,量产负担会减轻;更重要的是,用户获得的体验能在各副本之间保持一致。


7.3.1 作为运营单元的模式

拿菜谱里的食谱来打比方就很准确。一份食谱上,食材、烹饪步骤、火候、成品照片一并列出。哪怕换了餐厅,只要照同一份食谱来做,味道就一样。只不过每家餐厅允许略作变奏。模式也一样:空间(房间)、行为(BT 子树)、事件(event)、结果(奖励·难度),再加上设计师的意图说明,一并打包在内。

模式 = 五个要素的组合 空间 房间元数据 1~3个 行为 BT 子树 1~2个 事件 event 槽 结果 奖励·难度规则 意图 说明 "走廊追击模式" = 狭窄走廊 + 快速敌人 BT + 陷阱 event + 闪避奖励 → 一经验证,即可在 5~10 个副本中再生产同一种体验

一个模式一旦定义好,就能让每个副本都稳定地产出同一种体验——就像一份经过验证的食谱能在多家餐厅做出同一种味道。只不过同一份食谱,每家餐厅仍会留一点变奏。如何管理这些变奏,占了模式运营的一半分量。后文要讲的 overrides 就是这一环。


7.3.2 模式的组合流程

模式库的核心在于:先把模式固化为规则手册,再把它们组合起来生成副本。设计师不是在空白屏幕上从头搭建副本,而是挑选经过验证的模式加以布置,只对一部分作变奏。

flowchart TD A[观察游戏中的优质体验瞬间] --> B[分解为空间·NPC·事件] B --> C[映射到房间模板·subtree] C --> D{模拟 + 用户测试通过?} D -- 否 --> B D -- 是 --> E[将模式登记到库中<br/>usage_count = 0] E --> F[(模式库<br/>5 个类别 · 30~50个)] F --> G[副本设计:调用模式实例] G --> H[布置 placement + 变奏 overrides] H --> I{变奏比例 20% 以下?} I -- 是 --> J[副本完成<br/>模式 usage_count +1] I -- 否 --> K[考虑拆分为独立模式] K --> B J --> L[追踪模式影响<br/>修改一个模式 → 自动汇总使用它的副本] L --> F classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class L code; class F data; class J pass;

这条流程的左半段(观察→分解→映射→验证→登记)是制造模式的过程,右半段(调用→布置·变奏→完成→追踪)是消费模式的过程。制造很少发生,消费则频繁发生。库运营得当时,这种不对称就会转化为量产效率。


7.3.3 五个基本类别

笔者的项目A属于动作RPG一类,因此把模式分为五个类别。这套分类依赖于品类。若是恐怖游戏,埋伏与叙事节拍的比重会不同;若是解谜游戏,利用环境的战斗会成为核心。不要把分类本身当作绝对,应先确定自己游戏的核心体验是什么,再来划定类别。

类别 核心体验 示例
pursuit 追击·逃脱 走廊追击、峡谷逃脱
ambush 埋伏·突袭 进入房间时埋伏、视野死角埋伏
puzzle_combat 利用环境的战斗 拉杆·陷阱 + 战斗
boss_phase Boss 阶段 Boss 阶段 1\~3 模式
narrative_beat 叙事节拍 回忆触发、同伴登场

在这五个类别里,模式总数大致维持在三十到五十个之间。这个数字是有理由的。一旦模式超过一百个,设计师就无法把整个库装进脑子里。那一刻起,库就变成了检索起来费时的仓库,设计师宁可从头去搭。库一旦开始被冷落,一致性这个初衷就会崩塌。因此,有意识地管理模式数量的上限,和类别设计一样重要。


7.3.4 固化模式的格式

一个模式由一份 YAML 文件固化下来。下面是把项目A实际使用的格式做了匿名化处理的版本。公司专有的资产名和副本编号已被遮去,但字段结构与运营方式保持原样。

---
pattern_id: pattern_corridor_pursuit_v2
category: pursuit
description: 快速敌人在狭窄走廊中从后方追击,玩家在岔路口做出闪避抉择
tags: [horizontal_corridor, scholar_theme_compatible]
rooms:
  - room_template: corridor_long
    size: medium
    connections_required: 2
  - room_template: junction_3way
    size: small
    connections_required: 3
npc_behaviors:
  - subtree_ref: subtree_aggressive_chase
    count: 2
  - subtree_ref: subtree_ranged_support
    count: 1
events:
  - type: trap_activation
    trigger: room_1_midpoint
  - type: enemy_spawn
    trigger: room_1_entry
difficulty_modifier: 1.2   # 相较普通房间的 1.2 倍压力
reward_modifier: 1.3
clear_time_estimate_sec: 60
art_pack_compatible: [scholar_library, generic_dungeon]
narrative_slots:
  - slot: dialogue_during_chase
    constraints: [short_dialogue, fear_emotion]
usage_count: 12            # 在 12 个副本中使用
last_modified: 2026-05-18
deprecated: false
---

这一份文件同时定义了十二个副本各自的一部分。usage_count: 12 这一行的分量正来自于此。修改这个模式,意味着十二个副本会同时受到影响,因此动一个模式文件,与改一个房间是不同分量的事。

subtree_aggressive_chasesubtree_ranged_support 这样的引用,直接指向 7.2 中在 BehaviorTree 编辑器里定义的 subtree。关键在于:模式并不直接把 BT 收纳进来,而只是引用它。改动 BT,所有引用了该 BT 的模式都会自动跟进。空间(房间模板)与行为(subtree)各自在自己的库中管理,模式只承担把这两者编织起来的组合表角色。clear_time_estimate_secdifficulty_modifier 这类数值只是笔者环境下的运营值,并非普适常数。你必须用自己游戏的模拟与用户测试亲自测量后填入。


7.3.5 把模式实例化到副本中

设计副本时,不从头搭建模式。而是从库中调用,指定它放在哪里,再用 overrides 覆盖只在这个副本里需要不同的部分。

---
dungeon_id: dungeon_021_silvermark_library
pattern_instances:
  - instance: corridor_pursuit_1
    pattern_id: pattern_corridor_pursuit_v2
    placement:
      - room_id: dungeon_021_room_03
        as: corridor_long
      - room_id: dungeon_021_room_04
        as: junction_3way
    overrides:
      - field: npc_behaviors.0.subtree_ref
        value: subtree_scholar_chase   # 学者主题变体
      - field: events.0.trigger
        value: room_1_2nd_third         # 触发位置微调
---

这里的副本 021 照原样使用"走廊追击"模式,只是把追击的敌人从普通敌人换成学者主题变体,并把陷阱触发的位置从走廊中段稍微往后挪了一点。模式的 80% 保持原样,只对 20% 作了变奏。

这个比例有来自运营经验的依据。变奏太少(接近 0%)时,各副本会像互相抄袭一样令人厌倦。变奏太多(超过 50%)时,那就不再是同一个模式了——你以为调用的是同一个模式,实际体验却完全不同,又回到了新人当初拿来的那个副本一模一样的处境。因此我们定下一条运营规则:当一个实例的 overrides 超过模式字段的一半时,那就不是变奏,而是新模式的信号,该把它拆分为独立模式了。


7.3.6 改一行,会牵动哪里

修改 pattern_corridor_pursuit_v2,会影响十二个副本。若靠人手工追踪,必定会漏掉一两个。因此我们准备一个自动梳理模式与副本关系的小工具。

# pattern_impact.py
import json
from glob import glob

def find_dungeons_using(pattern_id):
    affected = []
    for d in glob("dungeons/*.json"):
        dungeon = json.load(open(d, encoding="utf-8"))
        for inst in dungeon.get("pattern_instances", []):
            if inst["pattern_id"] == pattern_id:
                affected.append({
                    "dungeon": dungeon["dungeon_id"],
                    "instance": inst["instance"],
                    "has_overrides": bool(inst.get("overrides")),
                })
    return affected

这个函数返回的列表里,关键是 has_overrides 标志。没有 overrides 的副本原样使用模式,因此自动更新也是安全的。有 overrides 的副本,其专属变奏可能与模式修改发生冲突,因此需要人工的额外评审。

与其让人一一去掂量修改的分量,不如让工具在 5 分钟内报告"本次修改影响 12 个副本,其中 4 个有变奏,需要亲自查看"。减轻改动模式时的恐惧,才是这个工具真正的价值。看不见影响范围时,设计师就干脆不去动模式,库便成了一潭死水。


7.3.7 模式如何诞生,以及 AI 的位置

这里正面回应一个最常被问到的问题:"模式的编写不也可以交给 AI 吗?"

答案很明确:不行。编写一个模式,设计师的洞察是脊柱。什么才是好的追击体验、岔路口为什么必须在那里、陷阱为什么不在走廊中段而要在 2/3 处触发才能让紧张感立起来——这是亲手打磨过游戏、看过用户反应的人才有的判断。让 AI 从头去搭模式,所有模式都会收敛成四平八稳的平均形态。库里会塞满"不出错的模式",而"令人难忘的模式"却消失了。

但这并不是说 AI 无事可做。在编写模式的五个阶段中,有两处 AI 是强力的辅助。

阶段 产出 AI 的角色
1. 观察游戏中的优质体验瞬间 笔记 设计师独立完成
2. 把该瞬间分解为空间·NPC·事件 初稿 yaml 设计师独立完成
3. 映射到既有的房间模板·subtree 候选映射 AI 辅助(推荐候选)
4. 模拟 + 用户测试 验证 AI 辅助(执行模拟)
5. 登记到库 usage_count = 0 设计师独立完成

第 3 阶段是 AI 辅助的核心。当设计师写下"快速敌人在狭窄走廊中从后方追击"这样的初稿时,要人从已经积累到数十个的房间模板与 subtree 库中一一翻找哪个符合这个意图,是低效的。这里让 AI 来推荐映射候选。下面是把实际使用的提示词做了匿名化处理的版本。

[输入]
- 设计师的模式初稿 yaml(附于下方)
- 房间模板库(名称 + 标签 + 尺寸清单)
- subtree 库(名称 + 行为摘要清单)

请从库中挑选并推荐用来实现这个模式的房间模板和 subtree。
各推荐 2~3 个,附上适合度 1~5 分,以及基于标签·尺寸的一行依据。
库里没有的不要编造,只做推荐;若全部都在 3 分以下,请说明需要新建。

针对这个提示词,Claude 实际返回的输出如下。为了原样呈现它在哪里犹豫、在哪里断言,这里一字未改地照录。

房间模板候选:
1. corridor_long  (适合度 5)
   依据:标签 horizontal_corridor 一致,size medium 契合"狭窄走廊"的意图。
2. corridor_narrow_short  (适合度 3)
   依据:虽窄但长度较短,难以拉开追击距离。布置岔路口的空间不足。
3. hall_wide  (适合度 1)
   依据:宽阔的大厅与"狭窄走廊"的意图完全相反。追击压迫感立不起来。

subtree 候选:
1. subtree_aggressive_chase  (适合度 5)
   依据:已定义从后方追击的行为,带有移动速度权重。
2. subtree_ranged_support  (适合度 3)
   依据:本身并非追击,但可作为施压辅助布置 1 个。
3. subtree_patrol_basic  (适合度 2)
   依据:巡逻行为与追击意图不符。缺少触发后转入追击的节点。

岔路口(junction)房间模板:
未能在库中找到同时满足 3way 分岔 + small 尺寸条件的项。
junction_3way 最为接近,但 size 标签为空,
无法确定适合度。需要设计师确认。

最后一段是这份输出中最有价值的部分。AI 对岔路口房间模板说了"不确定",没有编造不存在的答案。设计师收到这份报告后,发现 junction_3way 的 size 标签确实是空的,于是先从这条元数据补起。如果 AI 无视空标签、径直给出适合度 5,设计师就会因错误的信任而照单采纳。

这才是 AI 辅助的正确形态。AI 铺开候选、标出不确定,选择与责任则留给设计师。若映射结果的适合度全都偏低,那就产生了编写新模板的另一项工作,而这项编写又回到了人的手上。

[方向标 —— 如果把模式压缩为"体验向量"(眼下仍为时过早)] 请把它当作研究动向而非处方来读。§7.3.1 已经把模式称作"食谱"。一个模式,近似于房间元数据·行为 subtree·event·difficulty/reward_modifier·clear_time 打成一包的坐标值。把这一包压缩为"体验向量",当适合度全都偏低、需要新建时,就不必逐一翻找上面那条流程,而可以把它锁定为压缩空间中的空白区域;§7.3.8 的 deprecated 判定,也可以用坐标距离来强化对近邻重复的识别。只是要附上三条限定。difficulty/reward_modifier 正如 §7.3.4 所说是笔者的运营值,各游戏的轴尺度不同,压缩空间无法原样移植;插值只到给空位"标注"为止,而非模式的"生成";在那个标注之上真正去搭建模式,仍不越过本节的原则——设计师的洞察才是脊柱。这一构想与 §8.2.7 的维度向量压缩处于同一位置,概念直观见附录 M——留作根基足够扎实的团队几年后再去审视的领域。


7.3.8 收拢不再使用的模式

库,填满容易,清空却难。运营大约一年后,那些建好却几乎无人使用的模式会堆积起来。放着不管,库的检索成本就会上升,设计师挑选模式时还得连那些已死的选项一并翻看。因此要定期收拢。

条件 处理
6 个月内 usage_count 零增长 归为 deprecated 候选
在评审会上决定废弃 标记 deprecated: true
已在使用的副本 原样保留(历史性保留)
新建副本 禁止使用该模式

关键在于,废弃并不等于删除。已经在使用该模式的副本照旧保留。因为动一个正在线上运营中运行的副本,比拦住一个新模式更危险。deprecated: true 只是"从现在起不要再新用"的标记,而非抹去过去的命令。

就像每个季度把书桌抽屉里不用的工具拿出来整理一次那样,库也要排定每季度收拢一次的日程。没有这份日程,库就只会朝一个方向膨胀,某一刻便沦为被设计师冷落的仓库。


7.3.9 诚实地衡量成效

这是笔者在项目A中运营模式库一年所观察到的变化。下表中的时间数值是笔者环境下的估计(未经验证),只有方向和相对比例是真实观察到的。

项目 引入前 引入后 备注
单个副本的设计时间 约 2 周 约 1 周 笔者估计,方向明确
副本间的体验一致性 离散度大 稳定 基于用户评价,定性
每个模式的平均使用副本数 约 8 个 量产效率的核心指标
新设计师上手 约 2 个月 约 3 周 笔者估计,体感最明显
摸清模式改动的影响 手工 1\~2 天 自动 5 分钟报告 pattern_impact.py 的引入成效

最令人印象深刻的变化,是倒数第二行——新人上手。模式库无意间充当了设计教科书的角色。新人只要读一份模式文件,就能理解"这款游戏的追击体验是这样做出来的",于是前辈守在旁边讲解的时间大幅减少。当初新人拿来的那个各不相同的副本问题,竟由库本身化解了。

"每个模式的平均使用副本数约 8 个"这个数字,意味着同一个模式被复用了八次,这是量产效率的诚实度量。只不过 8 这个值依赖于笔者游戏的副本规模与模式设计。在副本数量少、或每次都要求不同概念的游戏里,这个值会小得多。


7.3.10 不建库的决定

最后,得讲一个足以推翻整章的观点才算诚实。模式库并非万能。确实存在一些环境,建库与运营的成本收不回来。

条件 建议
副本少于 5 个 手工即可,无需建库
只有 1 名设计师 脑子里就是库
只发行一次,无线上运营 复用机会本身就少
每次都是完全不同的概念 复用比例低,ROI 收不回

库的 ROI(Return on Investment,投资回报率)只有在三个条件同时具备时才收得回:有线上运营、设计师在三人以上、副本超过二十个。长线运营的 MMORPG 之所以是典型的适用对象,原因就在这里。如果自己的项目落在上表的某一行,那就该在动手建库之前停下来重新考虑。工具只有在存在问题时才有价值,而对一个只有五个副本的项目来说,模式库的成本大于它要解决的问题。


7.3.11 常见失败与对策

症状 对策
模式超过 100 个,设计师记不住 精简到 30\~50 个,每季度用 deprecated 收拢
用人手追踪模式影响(会有遗漏) 使用 pattern_impact.py 之类的自动追踪工具
overrides 达 80% 以上(实质上不算复用) 变奏过大 → 拆分为独立模式
把模式编写整个交给 AI 编写靠设计师洞察,AI 只辅助第 3·4 阶段
不测量 usage_count 自动汇总 + 在季度复盘中审查
不向新人讲解库 在上手资料中加入库的导览

这张表的第二行和第四行最常绊住人。不把影响追踪自动化,设计师就会惧怕修改模式,库随之僵化;把编写交给 AI,库则会收敛为平均。这两种失败都会扼杀库的生命——也就是"经过验证的体验的复用"。


7.3.12 第 7 部分收尾

第 7 部分把关卡这一领域垒成了三个层次。7.1 确立了房间元数据、标签与连通性的标准(空间),7.2 讲了基于 JSON 的 BehaviorTree 编辑器、subtree 与模拟(行为),而本章走到了把这两者连同事件一起打包复用的模式库(运营单元)。把空间与行为分开处理的运营中,同一处的决定每周以不同形态摇摆的那个问题,靠模式这一捆包固化下来加以解决——这正是整个第 7 部分的主干。

这条脉络与 Layer 整合设计严丝合缝地咬合在一起。游戏整体空间基调这一愿景在最上层,其下是关卡生成规则与 BT 规则这一系统层,房间、BT 与模式库构成内容层,副本实例与模式使用统计沉淀为数据,lint、模拟与用户遥测在构建·QA 环节对其加以验证。模式库既是这五层中内容层的脊柱,又是向上遵循系统规则、向下生成数据统计的连接环。


本章要点

下一章预告


动手试试 —— 固化一个追击模式

setup

  1. 在工作目录下建立 patterns/dungeons/ 两个目录。
  2. 把房间模板的名称清单和 subtree 的名称清单,各准备成一份每行一个名称的文本文件。(没有现成库的话,各用 5 个虚构名称起步也行。)
  3. 把上文正文里的 pattern_impact.py 原样保存下来。

prompt

由设计师亲自写模式初稿 yaml(这部分是人的活儿)。然后只把映射交给 AI。照用正文里的映射提示词,只是在输入中附上自己的初稿和两份库清单。别漏掉两行关键约束。

- 不要编造库里没有的新模板。只做推荐。
- 若适合度全都在 3 以下,请明确说明需要新建。

verify

  1. 逐行确认 AI 有没有编造库里没有的模板名称。
  2. 看适合度分数有没有附上基于标签·尺寸的依据。没有依据的分数不要信。
  3. 把模式实例化到 2 个以上的副本后,运行 find_dungeons_using("pattern_..."),确认这两个副本被准确捕捉到。

单人精简版

如果你一个人做小游戏,库这套系统就过头了。你只需挑一段自己最中意的副本片段,把那份体验写成一份 yaml 就够了。做下一个副本时,打开那一份复制过来,只改 20%。模式库的本质——经过验证的体验的复用——在一份文件上同样成立。等规模变大,那时再加上类别与追踪工具即可。

8.1 战斗平衡公式 —— 确定性这一规则手册的位置

本章学习目标(难度 🟡 实务 · 前置:四则运算·表格计算):把战斗平衡拆分为公式的位置与数值的位置,并以确定性·可追溯性这两项性质为依据,区分哪些环节可以交给 AI、从哪里开始必须由人用规则手册锁定。

凌晨两点,一条告警弹了出来:线上服务器的坦克职业生存率冲到了 89%。没有一个坦克打不到 Boss 收尾,而不会死的坦克又太多了。为了找出是谁动过手脚的痕迹,我打开了数据表。一行防御系数映入眼帘——DEF / (DEF + 1000)。这个 1000 究竟是在什么时候、经谁的手、以什么依据从 1200 降到了 1000,表里哪儿都没写。于是一场追查开始了:翻聊天记录、翻构建历史,最后不得不追溯到三年前已经离职的那位数值策划的记忆里,才算到头。

只要运营过战斗平衡的人,都会撞见这样的场景一两回。而这一幕的真正病根,并不在于那个 1000 是错的。而在于:这个数字住在公式的位置上,可公式变更的历史却哪儿都找不到。战斗平衡公式是游戏里最应当具备确定性的领域,也是最应当可追溯的领域。这两项性质为何会成为不该把 AI 放进这个位置的理由,正是本章的脊梁。

给非专业读者的一句话。 本部分的 z-score·模拟·曲线即便让你感到陌生,也没关系。你只需带走这一条——"对于同样的输入必须始终给出同样输出的规则(公式),不要把 AI 放进来。" 区分哪里需要确定性、哪里需要探索的这一判断,同样适用于会计规定·结算逻辑·合同条款这类处理"不能出错的规则"的所有岗位。公式本身,从 8.1.2 起再慢慢看也不迟。


8.1.1 公式就是规则手册

做游戏设计做久了,手里会攥着两类文档:经常变的,和几乎不变的。在战斗平衡里,几乎不变的那一类就是公式。"伤害如何计算"一个季度改上一两次,"这个角色的攻击力是多少"一周就要改上五六次。把频率不同的两股流放进同一个文件,经常翻动的手,就会把偶尔才翻的那张纸撕破。

在笔者运营的项目A里,战斗平衡被拆成了两个位置:公式的位置(这里称为 CombatFormula)和数值的位置(CombatBalance)。下面原样引用住在公式位置上的一行。

final_damage = base_damage × dmg_multiplier × (1 − defense_factor) × variation

  base_damage    = skill_base × ATK × skill_coeff
  defense_factor = DEF / (DEF + 1000)
  variation      = uniform(0.95, 1.05)

这个公式就是规则手册。你可以联想桌游的规则书。规则书只会写"掷骰子,按掷出的点数移动",不会写"这一局要是运气好,可以多走几步"。同样的输入,永远给出同样的输出——这就是确定性(determinism)。代入攻击力 180、防御力 80、技能系数 2.1,无论何时何地、算多少遍,都必须得出同样的伤害。假如同样的输入却给出不同的输出,那它就不是平衡工具,而是一台赌博机器。

确定性这一项性质,正是不该把 AI 放进这个位置的第一个理由。稍后再细看。先来看看公式该如何长得像一本规则手册。

战斗公式的核心区域,并不是一行伤害就能了结的。至少有三行是作为一组存在的。

# 伤害
final_damage = base_damage × dmg_multiplier × (1 − defense_factor) × variation

# 暴击
crit_damage  = final_damage × crit_multiplier
crit_chance  = base_crit + (LUK × 0.1)            # 上限 50%

# 治疗
heal         = base_heal × healing_power × (1 − sickness_factor)

把这三行写成代码块而不是自然语言,是有理由的。自然语言会留下解释的余地。"防御力越高,伤害越低"这句话,并没有说清楚是线性递减,还是曲线递减,又在哪里停下。DEF / (DEF + 1000) 只能有一种读法。规则手册的本职,就是把解释的余地压到 0。


8.1.2 曲线决定确定性

防御系数 DEF / (DEF + 1000) 这一行里,装着这款游戏整套平衡哲学。把这一行画成图,就能看出缘由。横轴是防御力,纵轴是所受伤害被削减的比例。

0% 50% ~91% 防御力 DEF →

0 1000 2500 5000 10000

DEF=1000 时伤害减少 50%

(若为线性 —— 未采用) 前期陡峭 后期平缓(收益递减)

这条曲线会缓缓贴向渐近线(asymptote)。它在防御力 1000 处正好把伤害削去一半,再往后无论怎么堆,都够不到 100%。无敌之所以不可能,就藏在这一行里。若像灰色虚线那样是线性的,那么防御力到 1000 时伤害就被全部挡住,再往上就越过界,进入负数伤害(挨打反而回血)这种说不通的区域。所以线性没有被采用。

这里回到凌晨两点的那场事故。假设有人把这个 1000 上调到 1200。整条曲线会向右平移。同样的防御力挡下的伤害变少了,于是全游戏的坦克都被削弱,输出职业的单位时间伤害则上升。公式里的一个常数,就撼动了整个游戏。 这跟改动一个数值(某个角色的攻击力)相比,影响的量级截然不同。这一差别,正是必须把公式和数值放在不同位置的理由,也是公式变更必须附带历史的理由。


8.1.3 公式变更必附带历史

凌晨两点的追查之所以是地狱,原因只有一个:没有变更历史。在项目A里,改公式不是改一行代码,而是记录一件决策。公式旁边跟着一份名为 CombatFormula_Decisions 的独立文档,里面这样写着。

## 决策 D17 (2026-04-22)
- 变更:将 defense_factor 由 DEF/(DEF+1000) → DEF/(DEF+1500)
- 事由:高等级区间(LV40+)坦克生存率 89%(线上实测)。是 Boss 战被拖长的原因。
- 尝试 1:以 800 模拟 → 坦克死亡率暴增,进 Boss 后 1 分钟内团灭者众多 → 回滚
- 尝试 2:以 1200 模拟 → 生存率 75% → 尚可,但高于目标(60~70%)
- 尝试 3:采用 1500 → 模拟生存率 65%(在目标范围内)
- 影响 atom:combat_defense_formula, combat_tank_class_balance
- 事后测量(1 周):线上生存率 67%(相较模拟预测 65% 为 +2%,在范围内)

这一件,回答了六个月后的那句"为什么会变成这样"。更重要的是,尝试 1 和尝试 2 都留了下来。只要记着 800 为何不行、1200 为何没被采用,下一个人就不会重蹈同样的覆辙。当新的数值策划加入团队时,这一组决策日志就是最好的上手资料。

这里有一点要老实点明。上面尝试 1·2·3 的模拟数值(死亡率、生存率 75%、65%)是为了展示运营流程而给出的笔者估算值(未经验证)。每款游戏的曲线和目标范围都不一样。但"变更之后跟着尝试,尝试之后跟着模拟依据,采用之后跟着事后测量"这个结构,与真实运营别无二致。这个结构里只要空掉一格,空掉的那一格就会以凌晨两点的追查回到你身边。

把公式、数值、历史这三个位置放在一起看,就是下面这样。

CombatFormula 公式(规则手册) 每季度改 1~2 次 确定性 · 禁用 AI 影响:整个游戏

CombatBalance 数值(表格) 每周改 5~10 次 通过模拟关卡 影响:对应角色

_Decisions 决策历史(日志) 每次变更 1 件 事由·尝试·事后测量 上手核心资料

一次公式变更 → 一件决策日志(附事由·尝试·事后测量)


8.1.4 一条公式变更的真实流程

现在从头跟一遍 D17 是怎么定下来的。这就是确定性规则手册在实务中运转的方式。

flowchart TD A["线上实测<br/>检测到坦克生存率 89%"] --> B["原因假设<br/>防御系数 1000 在后期过度保护"] B --> C["定义变更候选<br/>1500 / 1200 / 800"] C --> D["Damage Simulator<br/>每个候选确定性运行 1,000 次"] D --> E["结果报告<br/>生存率·平均战斗时长·胜率"] E --> F{"数值策划判断<br/>是否在目标 60~70% 范围?"} F -->|"800:死亡率暴增"| G["驳回 → 记入日志 尝试1"] F -->|"1200:75%,略高"| H["保留 → 记入日志 尝试2"] F -->|"1500:65%,范围内"| I["采用 → 决策 D17"] I --> J["应用到构建(不可逆)"] J --> K["1 周后线上事后测量<br/>67%,相较预测 +2%"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class D code; class B,C,F human; class A,E,K data; class I pass; class G fail;

在这个流程里,要准确看清模拟器的角色。Damage Simulator 会把三个候选各跑 1,000 遍。这里的 1,000 遍,并不是把同样的输入重复 1,000 次。公式里 variation = uniform(0.95, 1.05) 这个 ±5% 的随机数,再加上暴击概率这另一个随机数,让每一局的结果各不相同。跑上 1,000 局,是为了看分布——看平均生存率、最坏情况,以及战斗时长的离散程度。

重要的是,这个模拟器本身必须是确定性的。只要给同一个随机数种子,这 1,000 局就要一字不差地复现出来。唯有如此,"用 1500 得出 65%"这行 D17 的记录,才能在六个月后同样复现、得到验证。如果模拟器每次都给出不同结果,决策日志就成了谎言。

笔者第一次做这个伤害模拟器是在 2008 年。那时它是 Excel 宏,如今在项目A里,它被封装成了 balance-sim 技能。18 年间,工具的外壳换了又换,可里头装的规则手册,从来没有一次是概率性的。这才是关键。


8.1.5 为什么奖励曲线和公式绝对禁用 AI

现在来到本章最想说的一句。在 AI 几乎进入游戏设计每一个位置的今天,有唯一一个位置绝对不能放它进来——那就是战斗公式与奖励曲线这一确定性的核心。

LLM 本质上是概率性的。对同一个问题,每次的回答都会略有不同。这正是它产出好文字和好点子的力量之源,但放到规则手册的位置上却是致命的。若让 LLM 来回答"防御力 80 的角色会受到多少伤害",今天它可能答 92,明天答 94。这就等于桌游规则书每翻一页,骰子点数的含义都在变。

奖励曲线更危险。"从 30 级升到 31 级所需的经验值"一旦定下,就同时规定了数十万人的进度速度。哪怕只掺进 ±2% 的抖动,某些玩家做着同样的刷怪,也会比旁边的人升得慢。公平性就此崩塌。确定性,与公平是同义词。所以奖励曲线由人一手定下、录入表格,再也不交给概率。

但这并不是说要把 AI 从整个平衡领域里赶出去。边界才是关键。

领域 AI 理由
伤害·治疗公式计算 绝对禁止 确定性核心。同样输入 = 同样输出一旦被打破,就是赌博机器
奖励·经验值曲线 绝对禁止 同时规定数十万人的进度。一旦抖动,公平性崩塌
模拟器内部运算 绝对禁止 无法复现时,决策日志即成谎言
模拟结果异常模式检测 可以 从 1,000 条结果中以 z-score 检测"这个角色超出正常范围"
变更候选探索 可以 如"在 base_atk ±10% 范围内提出 5 个候选"这类受限探索
决策日志初稿撰写 可以 会议内容 → Decisions 条目初稿(由人评审)
事后测量报告摘要 可以 对线上数据做自然语言摘要

界线很清楚。AI 只住在确定性核心的外侧。 计算和模拟的内侧是规则手册,分析、提议、落成文字的外侧才是 AI 的位置。这条线一旦越过,同样的输入就会开始给出不同的结果,从那一刻起,平衡工具便失去了信任。

这条边界,与 8.2 将要看到的经济系统是完全相同的结构。在经济里,资源产出·消耗公式同样是确定性的,而通货膨胀模式的检测才是 AI 的位置。整个平衡领域,都以同一副骨架运转。


8.1.6 更进一步 —— 用 z-score 提出候选的进阶式应用

到目前为止,都是人来做候选、模拟来验证的保守式应用。再往前一步,连做候选这件事也可以交给工具代劳。只是,规则手册依旧属于人和确定性。

起点是异常模式检测。从 1,000 局模拟结果中,查看各角色胜率·生存率的分布,用 z-score 度量它偏离均值多少个标准差。z 超过 2 的角色会被自动标记为"超出正常范围"。凌晨两点的那个坦克,想必也会被这套检测揪出来。

要让检测延伸到提出候选,还需要两样东西。第一是变更空间的定义。在 CombatBalance 表格里设一列如 tunable_range,明确写出"这个数值可以在什么范围内改动"。第二是模拟并行化。要在构建关卡的时间内跑完 10 个候选 × 1,000 局 = 10,000 局,就得有并行基础设施。

这三样(z-score 检测 · 变更空间定义 · 模拟并行化)一旦齐备,留在数值策划手里的决策就收窄成"采用哪个候选"这一件。从 0 做出候选,和从五个里挑一个,负担是不一样的。这里 AI 触及的仍只是提出候选与解读报告,模拟内侧的运算与采用的决策,是确定性与人的位置。

最后点一下可逆性。改表格也好、跑模拟也好,都是可逆的,可以随意撤回。唯一不可逆的位置,是应用到构建。上线的数值一旦被玩家看到,就会以社区反响留存下来,即便回滚,痕迹也抹不掉。所以一切评审,都在应用到构建之前、可逆的阶段内收尾。


动手试试 —— 安全地处理一次公式变更

setup. 把战斗公式从自然语言说明中剥离出来,做一份只用代码块书写的 CombatFormula 文档,并在它旁边建一份空白的 CombatFormula_Decisions 日志文档。数值则单独拆到一张表格(CombatBalance)里。

prompt. 不要用 AI 去改公式,只把它用在分析·起草上。例如,把模拟结果的 CSV 交给它,这样请求。

请从附上的 1,000 次模拟结果中,计算各角色胜率的 z-score,
把 z>2 的角色整理成表格。对每个角色,
请结合依据推测哪个数值(攻击力/防御力/技能系数)
最有可能是异常原因。不要改动数值本身 —— 只提出候选。

verify. 不要照单全收 AI 给出的候选。把候选数值直接录入 CombatBalance 表格,用 Damage Simulator(或 balance-sim)给同一个种子,再跑 1,000 次。确认两点。(1) 模拟结果是否落入目标范围。(2) 用同一个种子再跑一遍,是否一字不差地复现。两项都通过就采用,采用后立刻在 _Decisions 里写下事由·尝试(含被驳回的候选)·预测值。应用到构建 1 周后,把线上实测值补记到那份日志里。

单人精简版

即便是没有团队、也没有模拟器的单人开发,骨架照样能运转。把公式记在代码注释里,或另用一张 .md,以代码块写下,并在那个文件的最底部放一节 ## 变更历史。只要改动了公式里的任何一个常数,就写一行日期·事由·改动前的值。模拟器用一段 30 行的 Python 循环就够了。固定随机数种子,把角色数值代入公式跑 1,000 遍,哪怕只输出平均胜率,也已经从"凭感觉改"迈向了"凭依据改"。AI 就只用来读那份输出 CSV、总结"哪个角色不对劲"。唯有把一行公式交给 LLM 去计算这件事,无论规模大小,都别做。


本章要点

8.2 用 Machinations 建模经济 —— 用模拟而非开会来控住通货膨胀

主要读者:负责线上经济的 MMORPG 数值/系统策划(中等规模(10\~50 人)团队) 面向单人/业余读者的精简版:§8.2.10「一个人的话,只需做到这些」

最早察觉金币开始泄漏的,不是账单,而是交易行。上线第二个月,强化石的行情悄悄上涨,一个月后翻了一倍。为了查明原因我召集了一场会议,可会议室里冒出来的全是"感觉"。有人说新副本的奖励给多了,有人说是刷怪点的效率变高了,还有人说不过是高等级玩家变多了而已。每种说法都像那么回事,于是什么也没能定下来。一个小时耗在了猜测上,最后以"先等下周再多看看数据"收场。

问题在于资源不止一种。金币、强化石、声望、荣誉、灵魂石各自都有 source(流入的路径)和 sink(流出的路径),而这些路径又彼此供养。强化石 Boss 也会掉金币。用金币买来的装备又会消耗强化石。当 5 种资源与数十条流动纠缠在一起时,光靠脑子里的心算,连一种资源一周的收支都算不出一个诚实的数字。本章讲的,是把这种缠绕搬进 Machinations 节点模型,并让经济变更的决策不再靠会议上的猜测、而是靠 模拟关卡(simulation gate) 来通过。经济设计的一般理论,别的书里已经讲得够多,本章只聚焦于把那套理论 放进 AI 工作流去跑的那一环

笔者实际运营笔记 本章的案例,是把笔者在公司 R&D 文件夹中运营的经济试点文档(Economy_Machinations_Pilot)与经济调研工作区做了匿名化处理。资源种类、source/sink 结构、Pilot 四个阶段都忠实地照搬了实际运营,而公司专有名称、真实数值则替换为书用版本,或只以比例、方向来表述。AI 输出正文是对真实会话的重现。


8.2.1 经济不是"5 种资源",而是"数十条流动"

把经济资源列成表,只有五行,看上去很简单。陷阱不在资源,而在连接资源的 流动 的数量上。

资源 source(流入) sink(流出)
金币 刷怪、任务奖励、交易行出售 购买装备、强化、修理、税金
强化石 副本 Boss、活动 装备强化、合成
声望 支线任务 阵营商店、转职
荣誉 PvP、公会战 PvP 商店、公会设施
灵魂石 击杀 Boss 角色复活、学习技能

资源虽只有 5 种,但 source 与 sink 加起来有二十几个,而且资源之间还会相互转换(用金币购买强化石的交易行,既是金币的 sink,又是强化石的 source)。一旦这些流动开始彼此供养,"金币多投放 5%,强化石行情会怎样"这类问题,只盯着一种资源就答不出来。这正是角色平衡(8.1)与经济平衡决定性不同的地方。角色平衡用一行公式就能收口,而经济是 随时间累积的动态系统,即便一周收支接近 0,累积 26 周也会把交易行压垮。

所以经济工作的本质,不是"把数字挑好",而是 "用模拟去看流动如何随时间累积"。而手工搭建、修改这套模拟模型既枯燥,每做一次又都会有遗漏。反复而容易漏项的初稿工作,可评审又必须牢牢握在人手里 —— 这种性质的工作,正是 AI 与人的分工线画得最干净的地方。

先用一张图,放上本章要处理的经济循环的骨架。

%%{init: {"flowchart": {"defaultRenderer": "elk"}}}%% flowchart LR subgraph SRC["source(资源产出)"] H["刷怪点"] Q["任务"] B["副本 Boss"] PVP["PvP/公会战"] end subgraph POOL["pool(资源储存)"] G(("金币")) S(("强化石")) end subgraph SINK["sink(资源消耗)"] UP["装备强化"] RP["修理/税金"] SH["阵营/公会商店"] end H --> G Q --> G B --> S PVP --> SH G -->|交易行转换| S G --> UP S --> UP G --> RP UP -.->|强化石需求 ↑| S G -.->|"净流入 > 净流出 时<br/>通胀累积"| POOL classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class G,S data;

虚线才是本章的核心。强化 sink 拉高强化石需求,从而推高强化石行情(UP -.-> S);而当金币净流入超过净流出时,那部分超额每周都会堆进池(pool),累积成通货膨胀。这两条虚线,靠手算根本无法追踪,所以才需要模型。


8.2.2 Machinations —— 把经济搬进节点图的工具

Machinations 是一款把经济流动画成节点图、并在其上运行模拟的工具。这一节要做的,是把 §8.2.1 的 mermaid 搬成一个真正能跑的模型。

节点 作用 在上图中
Pool 资源储存库 金币、强化石
Source 资源产出 刷怪、任务、Boss
Drain 资源消耗 强化、修理、商店
Converter 资源转换 交易行(金币→强化石)
Trigger 条件触发 活动、晋级奖励

用这些节点为经济建模,再把模拟跑 1,000 次,得到的就不是单一结果,而是一个分布。形如"26 周后金币行情中位数 +X%,前 10% 玩家 +Y%"。不过 Machinations 并非万能,引入它本身就是一种成本。

局限 对策
与游戏代码分开运行,同步会错位 用真实 telemetry 每月/每季校准(§8.2.6)
节点图一大,可读性就崩 按资源拆成子图,从单一资源起步(§8.2.4)
模拟用的是简化的玩家模型 用真实行为分布校准,设定误差阈值
结果解读依赖领域知识 把从模拟数值到决策的关卡标准化(§8.2.5)

所以 Machinations 并不是一款无条件引入的工具。只有当 5 种以上资源 + 资源转换流动 + 线上运营 这三个条件叠加时,它才值得。2\~3 种资源的简单经济,用 Excel 就够了,那种情况下引入 Machinations,收益还没到,运营负担先到了。


8.2.3 [实操记录] 用 AI 起草金币单一资源模型

光看工具说明,还是不知道它实际会吐出什么。这里把"仅金币一种资源搬进 Machinations 模型"的一个完整周期,从输入提示词一直跟到人的驳回,走到底。输入提示词可以原样复制使用,输出则是对真实会话的重现。

第 1 步 —— 输入:把金币流动整理成机器可读的表

先把金币的 source·sink 从数据表里提取出来,做成一张表。这不是新写,而是提取。

# gold_flows.yaml —— 金币单一资源流动(现行数据表摘录)
resource: gold
sources:
  - id: hunting        # 刷怪点掉落
    trigger: per_kill
    note: 各等级段的掉落曲线套用 reward_curve 规则
  - id: quest_reward   # 任务奖励
    trigger: per_complete
  - id: market_sell    # 交易行出售
    trigger: per_trade
sinks:
  - id: gear_buy       # 购买装备
  - id: enhance        # 强化费用
  - id: repair         # 修理
  - id: tax            # 交易行税金(既是 sink,也是回收金币的核心)
# 玩家行为分布(每小时刷怪次数·任务完成率)目前仍为空 → 让 AI 若做假设就标示出来

第 2 步 —— 提示词:让它建模,但强制约束假设与格式

附件 gold_flows.yaml 是金币这一单一资源的 3 个 source、4 个 sink。
请据此起草一份可搬进 Machinations 模型的节点规格草案。

规则:
1) 把每条流动按节点类型(Source/Drain/Pool/Converter)分类。
2) 为每个节点提出"以一周为准、每名玩家的期望流量"的算式,
   若需要玩家行为假设(每小时刷怪次数·任务完成率等),
   就把该假设单独另起一行标为"★假设"。不要把假设藏在正文里。
3) 把 source 总流入与 sink 总流出分开,计算一周净收支(net)。
4) 要体现交易行税金(tax)是唯一把金币从经济中永久移除的 sink,
   并区分其他 sink(强化·修理)只是把金币转移给 NPC、并非回收。
5) 凡是你"没有把握"的假设,单独用 ★ 标出,交给人来决定。

输出格式:表格(节点 | 类型 | 一周流量算式 | 假设 | 把握度)。除此之外不要有任何说明。

这段提示词里真正写得长的,不是"去建模"的指令,而是"把假设写在哪里、怎么写"的规则。在经济建模中,若放任 AI 自由发挥,它会把假设藏起来、只吐出一堆看似合理的数字。那样一来,即便模型错了,也指不出 到底哪里错把玩家行为假设从正文里揪出来、单独立成一行 —— 这项请求的分量全压在这里。假设露出来,才能验证。

第 3 步 —— Claude 的输出(重现,节选)

| 节点          | 类型      | 一周流量算式                            | 假设             | 把握度 |
|--------------|-----------|----------------------------------------|------------------|-------|
| hunting      | Source    | 每小时击杀 × 平均掉落 × 周游玩时长        | ★每周 15h,击杀 80/h | 中  |
| quest_reward | Source    | 日常任务数 × 奖励 × 7 × 完成率           | ★完成率 0.7      | 中    |
| market_sell  | Source    | (玩家间转移 —— 非净增)                  | 对净经济零贡献    | 高    |
| gear_buy     | Drain     | 购买频率 × 平均价                        | ★每周 1.2 次     | 低    |
| enhance      | Drain     | 强化尝试 × 每次费用                      | ★每周 8 次       | 中    |
| repair       | Drain     | 刷怪时长 × 修理率                        |                  | 高    |
| tax          | Drain     | 交易额 × 税率(★唯一永久回收的 sink)     | ★税率 5%         | 高    |
| ── 净收支    | net       | Σsource − Σsink ≈ +(以中活跃为准为正)   | 汇总以上假设时    | ★低  |

★假设警告:market_sell 是玩家↔玩家的金币转移,对经济总量零贡献。
把它计入 source 会重复计算通胀。需要在模型中单独处理。
★没有把握之处:gear_buy 频率(每周 1.2 次)是猜测。没有真实购买 telemetry
就可能把 sink 低估/高估 → 需要人来决定。

这份输出里最有价值的不是表格,而是 最下面两行——"★假设警告"与"★没有把握之处"。AI 主动把自己模型的两处弱点上报了出来。好的提示词,能让 AI 说出"这个假设我信不过"。

第 4 步 —— 验证与驳回(人的位置)

这份输出不能直接塞进模型。AI 上报的两处 ★ 里,有一处实际上是会毁掉模型的缺陷。

AI 起初把 market_sell(交易行出售)归为了 Source。可交易行出售,是 玩家 A 的金币转移到玩家 B 的过程,并不是经济里凭空多出了金币。把它计入 source 流入,就会把通货膨胀重复计算一遍。AI 虽然用 ★假设警告自己点了出来,但在表格正文里,它仍旧把这条留在了 Source 一栏 —— 报是报了,却没从模型里剔除,这是一份只对了一半的输出。这同时也是人这一侧的数据缺陷:输入的 yaml 里没有标明 market_sell 的性质(玩家间转移 vs 新增产出)。

于是重新提出请求。

market_sell 是玩家↔玩家的金币转移,并非经济总量的 source(修正输入
遗漏)。请把该节点从 source 汇总中剔除,改为只以"交易行税金(tax)
永久回收转移额的一部分"这一 sink 反映到模型里。请重新计算净收支,
并用一行展示剔除 market_sell 对 net 的影响。

AI 重新给出了一个把 market_sell 从 source 中移除、只留税金作为 sink 的模型。结果净收支(net)比最初的估计更低了 —— 这暴露出:把交易行出售错当成 source 时,通货膨胀是被高估了的。这一次往返正是关键。 人若从头手工搭建,要花半天,而且节点分类的失误自己很难抓出来;可 AI 起草 +"强制标示假设"+ 一次驳回,只需一小时以内,并且靠"AI 上报 ★、人来判定"这一结构,像重复计算这样的缺陷,在进入模型之前就被拦下了(笔者估计 —— 节省的时间因团队、资源数而异,与其看绝对值,不如把它当作"从头手工"与"起草+评审"之间的结构差异来读)。


8.2.4 一次一种资源 —— 用 Pilot 四阶段引入

金币模型收口了,并不意味着可以把全部资源一次性建模。笔者的运营也没有把整体一股脑塞进去,而是走了从单一资源起步、经过验证·校准再扩展的四个阶段。

阶段 范围 核心关卡
1. 单一资源(金币)建模 source 3·sink 4,§8.2.3 会话 节点分类·标示假设
2. 模拟 vs 实际对比 模拟一周 net vs telemetry 一周 是否通过误差阈值
3. 模型精度校准 反映玩家行为分布(低/中/高活跃) 按 segment 重新测量误差
4. 资源扩展(5 种) 逐步加入强化石·声望·荣誉·灵魂石 验证转换流动(交易行)

第 2 阶段的对比验证,是这四个阶段的心脏。模拟与实际若对不上,错的不是游戏,而是模型。用一个对不上的模型去做决策,那决策就会在线上以事故的形式回来。所以扩展(第 4 阶段)永远只在通过第 2·3 阶段的验证之后才做。这个顺序一旦被打破,也就是跳过单一资源验证、把 5 种资源一次性塞进去,就连"哪种资源的模型错了"都没法分离出来指认。


8.2.5 模拟关卡 —— 给经济变更决策架起拦截屏障

模型通过验证后,就要在每一个影响经济的变更决策前面立起 模拟关卡。这是把过去在会议上靠"感觉"放行的决策,改成靠模拟通过来放行的地方。

决策类型 模拟义务
新增 source·sink 必须
变更资源转换比率(交易行汇率等) 必须
设计新副本·活动奖励 必须
价格变更(±10% 以上) 必须
验证新职业效率 必须
UI 变更等与经济无关 豁免

关卡实际如何运作,这里在 §8.2.3 验证过的金币模型之上,把一个决策放上去过一遍。

模拟关卡 —— 活动奖励决策

[变更方案] 周末活动:每日登录奖励 +500 金币
[关卡]     新增 source → 必须模拟
[模拟 1000 次结果]
  - 一周金币净收支:+6,900 → +10,400(+50%)
  - 累积 26 周时金币行情中位数 ~+28%(通胀警告:超过 ±10%)
  - 前 10% 活跃玩家:~+41%(segment 偏差大)
[判定]     FAIL —— 超出稳定区间(±10%/长期)
[校正方案] 给活动 source 同时附上 sink:活动限定商店(回收金币)
           重新模拟 → 累积 26 周 +9%(PASS)

关卡的价值在最后两行。"投放 +500 奖励"这个决策,若是在会议上靠猜测,大概会以"应该没问题"放行。模拟关卡则把这决策换算成 26 周 +28% 的通胀摆出来,还进一步强制给出 要加 source,就把 sink 一起挂上 的校正。把经济变更判定为不是靠猜测、而是靠模拟的通过/失败 —— 这就是关卡的全部。

这里点一个常掉进去的陷阱:segment 偏差。以中活跃玩家为准是 +28%,而前 10% 则是 +41%。赚金币最多的那批玩家,累积通货膨胀也最快,所以模拟不能只看平均,得按 segment 分别去跑。只看平均,就会漏掉由高活跃玩家引发的行情崩塌。


8.2.6 模型在上线后靠 telemetry 每月进化

要让模拟关卡被信任,模型就不能与实际游戏对不上。游戏每周都在变,模型也得跟着校准。上线后,用真实 telemetry 每月(变更少的时期则每季)去校准模型。

模型校准周期(每月)
─────────────────────────────────
1. 提取一个月的真实玩家 telemetry(按资源汇总流动)
2. 按 segment(低/中/高活跃)算出 source·sink 的实测流量
3. 与 Machinations 模拟逐项对比
4. 误差 >15% 的项 = 调整模型参数(该项的 ★假设错了)
5. 调整后重新模拟 → 作为下个月关卡的基准模型使用

核心是第 4 步。误差大的项,正是 §8.2.3 中 AI 用 ★ 上报过的"没有把握的假设"与实际对不上的信号。比如 AI 猜测的 gear_buy 频率(每周 1.2 次),若实测是每周 2 次,就把那条假设换成 telemetry 的值。一旦停下这项校准,模型就会与游戏慢慢拉开距离,某个季度的模拟关卡便会闯出"放行了、实际却来了通胀"的事故。那一刻,模拟本身的信任会在事后崩塌。校准不是运营的附带工作,而是让关卡保持存活的常规周期。


8.2.7 进阶式应用 —— 异常模式检测·变更空间·模拟并行化

到这里为止,是经济建模的"保守式应用":人来发起变更,用模型验证,依结果决策。再往前一步,8.1.6 里见过的进阶式应用的三条轴 —— z-score 检测 · 定义变更空间 · 模拟并行化 —— 在经济这套基础设施之上同样会打开。

第一,异常模式检测。 每月校准周期(§8.2.6)里的误差对比,不再靠人用眼睛看,而是让代码先把"模型-实测偏差超过阈值"的项挑出来报上来。强化石行情翻倍,不再是看着交易行才知道,而是"强化石 source 流量相对模型偏离 +30%"这样一条 alert,会在开会之前就送到。

第二,定义变更空间。 不是"投放 +500 奖励,还是不投放"的二选一,而是把奖励范围(0\~+1000)与同时挂上的 sink 范围定义为一个变更空间,那么就能在这个空间里搜索满足通胀 ±10% 的组合。人来定"从哪到哪",空间内的最优组合搜索则交给自动化。

第三,模拟并行化。 不是把一个变更方案跑 1,000 次,而是把变更空间里的几十个候选各跑 1,000 次并行,一次性对比分布。过去在会议室里一个方案一个方案讨论的场面,变成对候选矩阵的模拟结果做比较。

共同的思想,是把人 发起 变更的位置,挪到让代码 搜索 变更空间的位置。但前提是:保守式应用(§8.2.3\~8.2.6)已稳定运转、模型也经 telemetry 验证之后,才谈得上。若拿没验证过的模型去自动搜索变更空间,错的模型只会自信地给出错的最优值。

激进式应用 —— 把经济压缩成"维度向量"来搜索

这是比进阶式应用再往前迈一步的领域。请把它当作研究动向来读,而非定论(若你是初次接触维度向量·嵌入(embedding),先看一眼附录 M 的那张"地图",下面就容易读了 —— 本书的五个"方向标"全都在那张图上打转)。走到这里的经济模型,是 5 种资源与数十条流动缠在一起的高复杂度系统,而 §8.2.7 的变更空间搜索,归根结底也是把那数十条流动逐一当成参数去跑。激进的想法,是把这份复杂度本身 压缩成维度向量,再在那个压缩空间之上求解。

一个看似遥远的类比可作线索。常被列为定性、难以把握的领域——烹饪食谱,在一项研究(Epicure —— Radzikowski·Chen,2026,arXiv:2605.22391 · 演示 epicure.kaikaku.ai)中,从 11 个来源的 414 万条食谱里,提炼出 1,790 种标准食材,把食材之间的关系压缩成数百维的向量。关键在于:即便是"味道"这样的定性对象,只要把食材间的关系换算成坐标,相似的食谱就会在向量空间里聚到一起,并可在其间做插值来搜索新的组合 —— Epicure 也展示了在这个压缩空间里,把某种食材朝特定菜系方向旋转、以寻找对应食材的插值搜索。

经济的原理也一样。把 source·sink·转换流动各自当作一个维度、用向量来表示经济状态,那么"通胀 ±10% 之内的稳定经济"就会被圈定为该空间中的一块区域。于是,不必再把变更方案一个个送去模拟,而是可以直接在那块稳定区域之内/附近搜索解。过去逐一去跑、去比的 §8.2.7 的并行模拟,有可能被收窄为压缩空间之上的一次搜索。

为何说"目前尚为时过早"。第一,决定把什么当作维度(哪些流动是独立的、哪些是从属的)这件事本身就是领域难题。第二,压缩本质上就是丢弃信息,被丢掉的那个维度上有可能爆出线上事故。第三,这一切只有在保守式应用的 telemetry 验证(§8.2.6)足够扎实时才有意义 —— 若压缩前的模型已与游戏对不上,压缩只会把那份误差也一并干净利落地压进去。所以这一节不是处方,而是 方向标。眼下要做的,是把保守式应用诚实地跑起来;维度向量,则留给那些根基积累得足够厚的团队,在几年后再去审视的研究领域。


8.2.8 度量 —— 会议被模拟替代的地方

对比工具引入前后。下表的时间·频率,承载的是引入初期运营中体感到的方向,与其当作精确的绝对值来读,不如读作它朝哪个方向移动了才对。

项目 引入前(会议·手算) 引入后(模拟关卡)
经济变更决策 → 落地 2\~4 周(反复猜测·再讨论) 1\~3 天(模拟验证 1 次)
通货膨胀事故 每季 1\~2 起(事后发现) 每季 0\~1 起(关卡事前拦截)
新增 source·sink 频率 每季 1\~2 次(心里没底,趋于保守) 每月 1\~2 次(模拟提供安全保障)
经济会议频率 每周 3\~4 次 每周 1\~2 次

比起表里的数字,最后一行的含义更大。会议频率下降,是因为模拟替代了讨论。"我觉得强化石那边可能要来通胀"一旦变成"模拟结果 26 周 +28%",过去围着猜测争论一小时的场面,就以 5 分钟的结果共享收场。这与笔者系统的复盘中固化下来的一个概念(atom automation_signal_value_over_time_savings —— 自动化的价值不在于节省时间,而在于暴露信号)恰好是同一处。模拟关卡真正的产物,不是省下来的时间,而是让数字占据了会议上原本由猜测占据的位置。

不过有一点要诚实说明。表里的"每季 1\~2 起 → 0\~1 起"不是精确测量值,而是运营体感的方向。通货膨胀事故的计数会随定义(把行情 ±百分之几视为事故)而变,所以与其看绝对起数,不如读作"从事后发现转向事前拦截"这一结构变化才对。


8.2.9 常见的失败

模式 为何会失败 对策
把全部资源一次性建模 无法分离出哪种资源的模型错了 从单一资源 Pilot 起步(§8.2.4)
不经评审就接受 AI 模型的假设 交易行重复计算之类的缺陷会原封不动进入 强制标示假设 + 人来驳回(§8.2.3)
不设模拟关卡就变更经济 事后修复通胀的成本极大 定义必须模拟的项目(§8.2.5)
只模拟平均、忽视 segment 漏掉由高活跃玩家引发的行情崩塌 按 segment 模拟(§8.2.5)
上线后不做 telemetry 校准 模型与游戏拉开距离,关卡信任崩塌 每月/每季校准周期(§8.2.6)
用未验证的模型自动搜索变更空间 错的模型会自信地给出错的最优值 保守式应用稳定后再进阶(§8.2.7)

第二种最常被漏掉。就像 §8.2.3 里看到的交易行重复计算,AI 会自信地吐出一个看似合理的模型,却只用 ★ 上报自己假设的弱点、把缺陷留在正文里。那些 ★ 若不由人来判定,错的模型就会被放行,其上的所有模拟决策也就一起错了。


8.2.10 动手试试 —— 今天就能做的一步

一个人的话,只需做到这些:没有 Machinations、没有 telemetry 也行。挑出你自己游戏(或你喜欢的游戏)里的一种资源,把它的 source·sink 写在纸上,再把 §8.2.3 的提示词原样贴上去,拿到一份一周净收支的模型草案。然后挑一条 AI 用 ★ 标出的假设,试着反驳它"这个假设我信不过,重新给出依据",你就会亲身体会到:经济模型是由哪些假设捆在一起的 —— 以及其中一条假设错了,结论会怎样被推翻。

如果是团队,就从下面这一步开始。不要全部资源,而是挑出 最成问题的那一种资源(通常是金币或强化石),先只搭起 §8.2.3 的单一资源模型,再对一种经济变更决策(例如活动奖励)套上 §8.2.5 的模拟关卡。哪怕只有一种资源 + 一种决策,也足以把会议上围着猜测争论的场面,换成一行数字。

用 setup → prompt → verify 来概括 —— setup:把那一种问题资源的 source·sink 提取为 yaml。prompt:按 §8.2.3 的格式拿到节点模型草案,并强制让它用 ★ 标示玩家行为假设。verify:由人亲自驳回并重新请求 AI 上报的 ★ 假设与节点分类(尤其是玩家间转移 vs 新增产出)。


本章要点

下一章预告

8.3 Damage Simulator —— 规格 DPS 与模拟结果分岔的那一天

2008 年的一个凌晨,我对着一张 Excel 表,把同一个数字验算了三遍。规格书里写着某个剑士角色的每秒伤害是 847。可那天我第一次跑出来的模拟器,输入的是同一个角色、同样的参数,却吐出了 612。相差 27%。两个数字里必有一个在说谎,而我还不知道是哪一个。

规格书上的 DPS 是纸面上的承诺。一次技能的伤害乘以发动频率的算术。模拟器的 DPS 则是把这份承诺实际挥动 1,000 次之后的结果。冷却时间相互重叠、施法动作吃掉时间、暴击没有按期望值那样打出——这些纸面不知道的摩擦掺了进来。这 27% 的缝隙,正是数值策划安身立命的地方。信了纸面,上线之后就要哭。

本章讲的是这一件工具的故事。2008 年做出来、至今没有离手的 Damage Simulator。它如何追踪规格与产出分岔的确切位置,以及 18 年后我又如何给这套追踪接上了 AI——本章用一段真实的实操记录(worked transcript,完整保留的真实操作过程记录)一步步走过。


8.3.1 规格 DPS 为什么总是在说谎

先来拆解一下这个 612 对 847 到底是怎么回事。写这份规格书的后辈策划(以下称团队成员 A)并没有做错什么。他只是照着技能表上写的数字相乘。

规格上的 DPS 是这样算出来的。假设某个角色拥有三个技能。

技能 单次伤害 冷却时间 施法时间
横斩 320 3.0s 0.6s
突刺 540 6.0s 0.9s
普攻 180 1.2s 0.4s

团队成员 A 的规格计算,建立在"每个技能一到冷却就毫不遗漏地释放"这一理想假设之上。横斩每 3 秒 320,突刺每 6 秒 540,普攻填满空档。用算术算,干干净净地得出 847。在纸面上,角色仿佛长了好几只手,施法动作彼此互不阻挡。

模拟器之所以给出 612,原因只有一个——手只有一只。在释放 0.9 秒的突刺期间,即便横斩的冷却已经转好,也没法使出来。施法动作彼此吞噬的这种全局冷却(GCD)冲突,在规格里并不存在。纸面是没有摩擦的真空,模拟则是有摩擦的战场。做这行 24 年,我学到的第一条真理就是这个——规格 DPS 是上限,而不是实测值。而玩家玩的不是上限,是实测值。


8.3.2 实操记录 —— 向 AI 追问 612 的出处

2008 年的我,是用手把这 27% 的缝隙填上的。我一行一行地用眼睛读模拟日志,数出技能在哪一帧被挡住。花了半天。18 年后的今天,我把同一份日志扔给 Claude。下面是实际进行的一次会话全文。我不做删减,连打偏的第一次尝试也原样收录——因为那次打偏,正是用好这件工具的一半诀窍。

我抛出的提示词是这样的。

附件是我们的 Damage Simulator(damage_sim/,Python)单个角色 1,000 tick 战斗日志的一部分。规格书上这个角色的 DPS 是 847,而模拟产出是 612。请从日志中找出这 27% 差距的原因。日志格式为 [tick] event skill=SKILL state=STATE。不要猜测,只拿日志里的证据说话。证据不足就说不足。

[0.0] cast skill=突刺 state=START
[0.9] cast skill=突刺 state=HIT dmg=540
[0.9] cast skill=横斩 state=BLOCKED reason=GCD
[1.5] cast skill=普攻 state=HIT dmg=180
[1.9] cast skill=普攻 state=BLOCKED reason=GCD
[3.0] cast skill=横斩 state=HIT dmg=320
[3.6] cast skill=普攻 state=HIT dmg=180
... (此处省略 994 行)
[997.2] cast skill=突刺 state=HIT dmg=540

Claude 的原始输出是这样的(未作润色)。

从日志中可以确认的事实:

  1. state=BLOCKED reason=GCD 事件反复出现。在 [0.9] 处横斩、在 [1.9] 处普攻,都因为 GCD(全局冷却)被挡住了。
  2. 在突刺施法(0.0→0.9,0.9 秒)结束之前,其他技能无法发动。看起来是施法时间占用了 GCD 的结构。
  3. 因此,规格上"每个技能一到冷却就发动"的假设被打破了。实际上施法动作彼此冲突,导致一部分发动被遗漏。

不过所提供的日志只有 1,000 行中的 7 行。这 27% 的确切差距是否仅凭 GCD 冲突就能解释,单靠这 7 行无法断定。 需要统计 BLOCKED 事件的总发生次数,以及由此遗漏的伤害总量,才能验证这 27%。如果你能给出整份日志的 BLOCKED 汇总,我会做定量确认。

在这里停下,很重要。AI 指出了原因,却没能证明这 27%,而且它自己说出了这个局限。换作一个只看纸面的策划,可能一句"是 GCD 的缘故"就把会开完了;而这件工具却坚持"光凭 7 行还不知道"。我判断它的这个要求是正当的——这就是人的验证。于是我把整份日志的 BLOCKED 汇总提取出来,再次抛给它。

这是整整 1,000 tick 的汇总。BLOCKED 共 412 次,其中 GCD 原因 388 次,冷却未就绪原因 24 次。因 BLOCKED 遗漏的估算伤害,按横斩折算约为 19,800。整场 1,000 tick(=秒)战斗中,规格期望伤害为 847,000,模拟实测累计伤害为 612,300。用这些数字重新审视这 27% 的差距。

Claude 的第二次输出。

用汇总来验证。

这个差距的构成: - 因 GCD 冲突直接遗漏(388 次):估算约 19,800 是按横斩折算的,但还应把"被遗漏的槽位本可以填入更高伤害技能"的机会成本一并计入。若把遗漏槽位按平均伤害折算,遗漏部分会大于 19,800。 - 不过 234,700 并不能全部只用 BLOCKED 来解释。规格 847 究竟是"已把暴击期望值按平均计入"的值,还是"不含暴击的基础值",会决定其余差距该如何解释。 如果你告诉我规格 847 的暴击假设,我就把剩余差距分解开。

它又停下了。而且再一次停在了准确的地方。规格 847 是怎么处理暴击的——这是日志里没有的信息,只存在于团队成员 A 的脑子里。AI 知道自己所持数据的边界。我去确认后发现,规格 847 是把暴击 20% 概率、1.5 倍按期望值平摊进去后的值;而模拟则是 seed 固定之后,实际暴击发生次数低于期望值的那一局。也就是说,这 27% 的差距,是 GCD 冲突(结构性、可复现)暴击方差(统计性、这一局的运气) 混在一起的值。

这个分解就是结论。GCD 冲突的那部分是必须靠设计去修的真问题,而暴击方差的那部分,只要换 seed 跑 1,000 局取平均就会消失的噪声。把两者混为一谈,以"角色太弱"为由给它加强,那么在 1,000 局平均里本来正常的角色就会变得过强。纸面不知道、单独一局的模拟不知道、连 AI 自己也不知道的这个区分,是靠日志汇总与规格中隐藏假设的对照——也就是人的验证——才做出来的。


8.3.3 一组输入变成一组输出 —— 解剖模拟器

把刚才那次会话所审视的这件工具的输入输出,作为一整套摊开来看。模拟器归根结底是一个诚实的函数。同样的输入,同样的输出。输入从三条路汇聚而来。

flowchart LR A["游戏数据表<br/>(技能表·角色属性)<br/>read-only"] --> SIM B["场景 yaml<br/>(战斗条件·持续时间·<br/>目标构成)"] --> SIM C["seed=42<br/>(随机数固定)"] --> SIM SIM["Damage Simulator<br/>domain/formulas.py<br/>run_combat() × 1000 tick"] SIM --> R1["实测累计伤害<br/>612,300"] SIM --> R2["BLOCKED 日志<br/>412 次 (GCD 388)"] SIM --> R3["暴击发生<br/>实测 17.2% vs 期望 20%"] R1 --> REP["markdown 报告<br/>规格 847 vs 模拟 612<br/>差距分解:结构 19% + 方差 8%"] R2 --> REP R3 --> REP classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class SIM code; class A,B,C,R1,R2,R3,REP data;

这张图的关键在于:箭头是单向的。游戏数据表只会被模拟器读取,模拟器绝不会去改写数据。18 年来挡下最多事故的规则,就是这一根箭头的方向。一旦模拟器开始把数据复制一份存在自己内部,那么在游戏数据变更的第二天,模拟器模拟的就是昨天的世界。拿这样产出的报告去开会,整场会议就会围绕昨天的世界争论不休。

一组具体的输入(场景 yaml)长这样。

# scenarios/single_dps_check.yaml
scenario: single_target_dps
duration_ticks: 1000      # 假设 1 tick = 0.1s,100 秒战斗
seed: 42                  # 确定性 —— 同样的输入,同样的输出
actor:
  char_id: K_004          # 从游戏数据表 read
  skill_rotation: optimal # GCD 冲突时,优先最高期望伤害
target:
  defense: 1200
  hp: infinite            # 用于测 DPS 的无限血量假人
report:
  compare_to_spec: 847    # 填入规格 DPS,自动分解差距

而一组输出(报告节选)是这样。

# Damage Simulator Report — K_004 single DPS
输入: scenarios/single_dps_check.yaml | seed=42 | data rev. 2026-06-05

## 规格对比
- 规格 DPS:       847   (含暴击 20%·1.5x 期望值平摊)
- 模拟实测 DPS:  612   (这一 seed 的单局)
- 差距:            -27.7%

## 差距分解
- 结构性(GCD 冲突,可复现):   -19.2%  ← 设计复查对象
- 统计性(暴击方差,这一局): -8.5%  ← 预计 1000 局平均时抵消

## 复现验证
- seed=42 重跑 3 次 → 612,300 / 612,300 / 612,300 (一致)
- seed 0~999 共 1000 局平均 DPS → 731 (暴击方差抵消后)

看最后一行。用 seed 0\~999 跑 1,000 局的平均是 731。规格 847 与 1,000 局平均 731 之间的差距 116(13.7%),正是 GCD 冲突这个真正的结构问题的大小。该成为设计会议输入的,不是单独一局的 612,而是这个 731。既不是纸面的 847,也不是运气不好那一局的 612,而是 1,000 局达成共识的 731。把这个数字握在手里之前的全部功夫,就是数值策划的工作。


8.3.4 2008 年的手与 2026 年的手

这件工具活了 18 年,但并不是靠同一份代码活下来的。衣架原样不动,只把衣服换了五次。衣架就是上面那份报告的逻辑——把规格与实测分开看、把差距分解为结构与方差、再用复现来验证的这套流程。这套流程,无论是在 2008 年的 Excel VBA(Excel 宏语言)里,还是在 2026 年的 Python 里,都一字不差地相同。

时期 衣服(技术) 衣架(不变的流程)
2008\~2011 Excel VBA,1:1 对比规格做差距分解
2012\~2016 C# 控制台,N:N
2017\~2020 Python + Web
2021\~2024 Python + ML 〃(+ 反映玩家分布)
2025\~ Python + LLM 辅助 〃(+ 日志查询·假设生成)

撑过五次换衣服的秘诀,刻在文件夹结构里。

damage_sim/
├── domain/          # 衣架 —— 18 年原样不变
│   ├── formulas.py      # 伤害公式·GCD 冲突判定
│   └── metrics.py       # 差距分解逻辑
├── adapters/        # 游戏数据 read-only
│   └── excel_reader.py
├── runners/         # 衣服 —— 每次技术变更就替换
│   └── cli_runner.py
└── reporters/       # 衣服 —— 报告输出格式
    └── markdown_report.py

技术一变,只需重写 runners/reporters/domain/ 里的差距分解逻辑,作为 18 年的资产原样存活下来。2008 年用 Excel 单元格写的 GCD 冲突判定式,如今只换了函数签名,就在 formulas.py 里运行着。把工具钉死在一种技术上,它就会随那种技术一起老去、死掉——这是我送走好几件死掉的工具后才学会的。

2025 年接上的 LLM,不是新衣架,而是新的手。正如前面那次会话所见,AI 是读日志、立假设的手,原本要花半天的日志追踪缩短到了几分钟。但它绝不碰衣架——决定差距是不是 27%、暴击到底打出了百分之几的,依然是 seed 固定的确定性内核。LLM 一旦挤进那个位置,回归验证就变得不可能,工具也就死了。


8.3.5 确定性内核与它的外部 —— 界线该划在哪里

如果在一件数值工具里只能划一条线,我会把它划在确定性内核的边界上。里侧,必须像钢铁一样保证同样的输入得到同样的输出;外侧,人和 AI 尽可以自由地抛出假设。

里侧(确定性 —— 禁止 AI): - 伤害公式、GCD 冲突判定、暴击发生、累计汇总。 - 用 seed=42 跑三次,612,300 必须三次都一模一样。这一点一旦被破坏,昨天的报告和今天的报告就无法比较。

外侧(假设·解释 —— 欢迎 AI): - "为什么这个角色组合的胜率不正常"之类的原因查询。 - 在日志里找 BLOCKED 模式、自然语言报告初稿、场景 yaml 初稿。

前面那段实操记录,恰好就在这条线上移动。AI 在外侧迅速立起了"GCD 冲突是原因"的假设。但 27% 这个数字、612 这个数字,自始至终都是确定性内核算出来的值,AI 只是接过这些值去做解释。而且它两次停下,说"凭这些数据无法断定"——同时要求确定性内核给不了的信息(规格对暴击的假设)。这种停下,正是一件好工具的标志:不把假设错当成诊断。

关于数字,我要诚实地交代一点。本章里 847·612·731·412 次这类具体数字,是为了说明而构造的示例值。但"规格 DPS 总是高于模拟实测"这个方向、"这个差距会分解为结构性冲突与统计性方差"这个结构、以及"seed 固定是回归验证的前提"这条原则,都是从 2008 年起 18 年间实际运用中反复确认过的。比例的大小因项目而异,但方向和结构从未改变。


动手试试 —— 对比规格做一次差距分解

setup. 从游戏数据中挑一个角色,拿到它的技能表(伤害·冷却时间·施法时间)和规格 DPS。如果没有模拟器,就写一个跑 1,000 tick 单目标战斗的最小脚本。关键只有一条:必须能把 seed 作为参数传入并固定下来。

prompt. 把模拟日志(含 BLOCKED 事件)和规格 DPS 一起抛给 AI。

附件是 1 名角色的 1,000 tick 战斗日志和 BLOCKED 汇总。规格 DPS 是 [N],而模拟产出是 [M]。请只凭日志证据分解差距的原因。区分结构性原因(可复现的冲突)与统计性原因(这一局的方差)。证据不足就说不足,并指出还需要什么。

verify. 把 AI 指出的结构性原因,通过换 seed 跑 1,000 局取平均来验证。如果平均之后差距仍在,那就是真正的结构问题;如果消失了,那就是方差噪声。AI 若以"无法断定"停下,那不是失败,而是正常——停下的地方,由人补上规格里隐藏的假设。

单人精简版

如果你是既没有模拟器也没有 ML 的单人开发者,那么只用一张 Excel 和 AI,也能跑同样的流程。把技能表填进表格,用 RAND() 掷暴击,在一列里做一个 1,000 行的模拟。由于无法固定 seed,就用 F9 重算 100 次,用手工的方式看平均。把那个平均与规格 DPS 的差距抛给 AI,让它"分成结构原因和方差原因"。工具虽小,衣架——对比规格做差距分解、结构与方差的分离、复现验证——照样立得住。


本章要点

下一章预告

8.4 AI辅助的平衡模拟

周五下午4点,Alpha版本的5:5 PvP自动模拟跑完了1,200局。结果JSON有4兆字节。其中某处记着"队伍A胜率92%"这一行,而平均胜率是52%。我花了40分钟去找那一行,最终没弄清缘由就下班了。

平衡属于确定性的领域。相同的输入代入相同的公式,总会得出相同的伤害。所以伤害模拟器必须是代码,奖励曲线必须由人亲手绘制——这里是AI不该踏入的位置。然而在那个确定性内核的周边——也就是从1,200局结果中找出异常的一行、为其成因建立假设、筛选出该改动什么的候选、再把这些候选重新投入模拟——正是这些周边劳动吃掉了数值策划一天的大半。本章讲的就是把AI附着到这一周边的事,而内核原封不动。

8.4.1 内核是代码,周边是人的劳动

8.3中见过的那个2008年的伤害模拟器——历经三次更换引擎与公司,确定性内核依旧存活了下来——正是本章的起点。输入相同则输出相同这一性质,就是平衡工具全部的信任所在。同一版本跑两次却得出不同胜率,那这个工具就该被丢弃。

于是把平衡工作的骨架画出来,便是这样一副模样:中间有一团确定性,而它的入口与出口上挂着人的手工劳动。下面是把这副骨架拆解开——用颜色把确定性区域(蓝色)与人·AI介入的区域(橙色)分开。

确定性模拟 simulate_dps() 输入=输出,禁用AI

位置1 场景生成 位置2 改动候选探索

位置3 报告 位置4 异常解读 位置5 行动建议

数值策划(采纳·否决)

中间的蓝色方框只有一个是代码。其余五个橙色方框全是人的判断·解读·撰写这类劳动,而AI能进入的席位只有这五处。一旦让LLM"帮我算算这个角色的DPS(每秒伤害)",相同输入却给出不同数字的非确定性就会渗进内核,那件工具连18天都撑不到就会失去信任。

因此本章的脊梁很简单:把内核自始至终守为代码,同时在入口·出口的五处席位上附加AI,而从最费手的出口一侧——从1,200局结果中找出异常的一行、建立假设——开始自动化。

8.4.2 实操记录(worked transcript):追踪胜率92%的那一行

回到开头那个92%。这一次,不再让人耗费40分钟乱找,而是从头到尾走完这样一个循环:确定性探测器挑出那一行,LLM建立假设,再由模拟来验证。这里不作概括——所谓实操记录,就是把工具真实吐出的原始输出完整保留、原样呈现。

第1步 —— 异常探测由代码来做(z-score)

从1,200局结果中挑出"异常"的那一局,靠的不是LLM,而是统计。先求各指标的均值与标准差,再按偏离均值几个标准差(z-score)来划分。超过阈值就是outlier(离群值)。这是确定性的,没有幻觉可乘之机。

def find_outliers(results, threshold=2.5):
    # results: 每局模拟 {指标名: 值} 字典组成的列表
    means, stds = compute_per_metric(results)   # 各指标的均值·标准差
    outliers = []
    for r in results:
        for metric, value in r.items():
            if stds[metric] == 0:               # 方差0 → 无法比较,跳过
                continue
            z = abs(value - means[metric]) / stds[metric]
            if z > threshold:
                outliers.append((r["scenario_id"], metric, value, round(z, 2)))
    return sorted(outliers, key=lambda x: -x[3])  # 按z从大到小

运行后得到如下结果——1,200局中超过阈值2.5的只有3件。

[("pvp_5v5_S0417", "team_a_winrate", 0.92, 4.1),
 ("pvp_5v5_S0417", "match_duration",  41.0, 2.9),
 ("pvp_5v5_S0822", "team_b_winrate", 0.18, 2.6)]

z最大的第一行——场景 pvp_5v5_S0417 的胜率0.92(z=4.1)——正是开头我乱找了40分钟的那一行。用不着人拿眼睛去扫4兆的JSON,统计已把它缩到3件。到此为止是内核,从这里起才是AI。

第2步 —— LLM建立假设(禁止下确定性诊断)

现在把那一行交给LLM。但不是"帮我诊断原因"。LLM只是凭领域知识抛出几个可能的成因假设,而哪一个是真的,再由模拟来决定。提示词全文如下。

[outlier]
场景: pvp_5v5_S0417 — 5:5 PvP
队伍A构成: [refgame_archer_07, refgame_archer_07, refgame_archer_07,
            refgame_hybrid_21, refgame_hybrid_21]
指标: team_a_winrate 0.92 (整体均值 0.52, z = 4.1)
附带指标: match_duration 41.0s (均值 28s, z = 2.9)

[相关信息]
- refgame_archer_07: 远程支援,技能"标记" — 令目标承受伤害 +12% 的减益
- refgame_hybrid_21: 近战/远程混合输出,技能"追击" — 追踪减益目标时移动速度 +20%
- 两个角色几乎从未被编入同一队伍 (组合频率处于后5%)

要求:
请提出该outlier的3~5个可能原因假设。
- 每个假设附一行验证用的追加模拟
- 禁止下确定性诊断。只以"可能是……"层面的假设呈现。

回答格式: 编号列表。不写开场白与结尾,只列假设。

以下原样照录Claude的原始输出(不作概括·整理)。

1. "标记"减益与"追击"移速增益以乘法累积,
   3名archer_07铺满标记 → hybrid_21得以不间断追击,
   可能形成了正反馈循环。
   验证: 把hybrid_21换成普通近战输出,重跑同一场景。

2. "标记"减益的+12%被错误地设置为可叠加(stackable),
   3名archer_07或许把它叠到了+36%。
   验证: 强制把标记的max_stack设为1后重跑。

3. 5:5中远程3 + 追击型2的构成,可能只是凭借射程优势
   封锁对手进场 (与协同无关,组合本身就强)。
   验证: 把标记减益效果降为0%后,用同一组合重跑。

4. 从match_duration长于均值(41s)来看,队伍A或许并非速胜,
   而是"不落败地拖到后期再翻盘"的结构。
   这种情况下,原因可能不在胜率,而在后期的成长(scaling)数值。
   验证: 单独提取60秒时点的累计伤害曲线做对比。

5. (低概率) 模拟种子偏差 — 或许只有S0417种子反复让队伍A先手。
   有可能是统计假象。
   验证: 用50个种子重跑同一组合,确认胜率方差。

在这里打住很重要。第五个假设留下了"这或许只是种子偏差"这样的自我怀疑——这才是好的输出。若LLM断言"原因就是标记叠加的bug",反倒应该怀疑那份输出。在平衡里,LLM的活儿不是诊断,而是收窄搜索空间

第3步 —— 把改动候选投入模拟(并行)

五个假设各自附有一行验证用模拟。这并不是由人一个个去跑,而是把改动候选打包并行投出。作为核心内核的 simulate_dps 是如下这样可运行的形态——那个存续18年的确定性函数的骨干。

def simulate_dps(attacker, target, formula, ticks=600, seed=0):
    """对一对战斗做确定性模拟。相同的 (输入, seed) 则输出相同。"""
    rng = Rng(seed)                     # 固定种子 → 可复现
    hp = target.hp
    total_damage = 0.0
    for t in range(ticks):              # 假定1 tick = 0.1秒
        # 防御系数: 确定性公式 (不由LLM生成)
        def_factor = target.defense / (target.defense + formula.def_const)
        raw = attacker.atk * (1 - def_factor)
        # 暴击: 基于种子 → 相同seed则暴击时点相同
        if rng.roll() < attacker.crit_rate:
            raw *= attacker.crit_mult
        # 减益(如标记)由formula确定性注入
        raw *= formula.debuff_multiplier(attacker, target, t)
        hp -= raw
        total_damage += raw
        if hp <= 0:
            return {"ttk": t * 0.1, "dps": total_damage / ((t + 1) * 0.1)}
    return {"ttk": None, "dps": total_damage / (ticks * 0.1)}  # 未能在时限内击杀


def run_candidates(base_scenario, candidates, seeds=range(50)):
    """对每个假设的改动候选做50种子并行模拟。连winrate方差一并回收。"""
    out = {}
    for name, patch in candidates.items():           # patch = 覆写formula的一部分
        scen = base_scenario.with_patch(patch)
        wins = [simulate_match(scen, formula=scen.formula, seed=s) for s in seeds]
        out[name] = {
            "winrate": mean(w["team_a_won"] for w in wins),
            "winrate_std": pstdev(w["team_a_won"] for w in wins),  # 用于验证假设5
        }
    return out

把假设搬进 candidates 字典,一次性跑完。

candidates = {
    "基准(无改动)":          {},
    "假设1_hybrid替换":      {"team_a[3:5]": "refgame_melee_03"},
    "假设2_标记_max_stack1": {"skill.标记.max_stack": 1},
    "假设3_标记_效果0":       {"skill.标记.debuff": 0.0},
    "假设5_种子方差确认":     {},  # 同一组合,仅 seeds 取 50 个
}
result = run_candidates(scenario_S0417, candidates, seeds=range(50))

结果(实际运行形态的输出):

基准(无改动)          winrate=0.91  std=0.04   ← 非种子偏差(假设5排除)
假设1_hybrid替换      winrate=0.74  std=0.06
假设2_标记_max_stack1 winrate=0.63  std=0.05   ← 下降幅度最大
假设3_标记_效果0       winrate=0.55  std=0.05   ← 回到均值附近

阅读的顺序即诊断。把基准用50种子再跑一遍,胜率仍是0.91、方差0.04——假设5(种子偏差)被排除。把标记效果压到0,胜率贴到0.55、逼近均值——成因确实出在标记减益一系。而把max_stack锁为1时,跌到0.63的降幅最大,所以核心是假设2 —— 标记减益发生叠加,3名archer_07一路叠到了+36%。LLM抛出的五个候选,人并没有把五个全验一遍,统计只跑了三个就分出了胜负。

第4步 —— 人来采纳,并留下这个决定

在这里,LLM做的并不是说出"标记叠加是个bug",而只是把那个假设列进候选清单。采纳,由看过模拟结果的数值策划来做——"把标记的max_stack固定为1。archer_07单一组合的胜率为0.63,仍高于平均(0.52),因此下一个版本把标记减益数值从12%追加下调到9%后重新测量。"

这个决定由人做出,其依据(z=4.1探测 → 5个假设 → 3次模拟 → 确定假设2)以一行留存。确定性内核自始至终都是代码,LLM只是把40分钟的乱找替换成了五行假设,一步也没踏进内核。

8.4.3 五处席位,以及循环

上面的实操记录,其实是把五处席位中的三处(异常探测·改动探索·异常解读)一口气走了一遍。把五处席位展开成循环,便是这样转动。

flowchart TD A[场景定义] -->|位置1: 场景自动生成| B[数值输入] B -->|位置2: 改动候选探索| C{确定性模拟<br/>simulate_dps} C --> D[原始结果JSON] D -->|find_outliers z-score| E[异常模式探测] E -->|位置4: LLM假设3~5| F[假设 + 验证模拟] F -->|位置3: 自然语言报告| G[数值策划评审] G -->|位置5: 下一步行动建议| H{采纳 / 否决} H -->|采纳| B H -->|否决| A style C fill:#dbeafe,stroke:#2563eb,stroke-width:2px style E fill:#dbeafe,stroke:#2563eb

只有蓝色的两个节点(模拟、z-score探测)是确定性的。其余箭头上的标签——位置1·2·3·4·5——才是AI附着的席位。循环每转一圈,被采纳的改动就再次作为数值输入进入下一次模拟。这个循环若由人手工转,一圈要一天;用AI辅助转,则只需几个小时。

把五处席位逐一简短点一下。

位置1 —— 场景自动生成。 给出"3:3占领战,占领3面旗帜满1分钟即胜,复活10秒"这样一行概念,再附上一两个既有场景的yaml,LLM便会以相同schema填出新的场景yaml。数值策划只需检查"有没有擅自加入概念里没有的规则"。从白纸开始写yaml的1\~2小时,缩短为15分钟的评审。

位置2 —— 改动候选探索。 上面实操记录中的 candidates 字典正是此处。对于"想把坦克的生存提高+49%该动哪里",LLM抛出五个候选(base_def +50、调整def_const等),再把这些候选全部投入模拟,挑出副作用最小的一个。候选是假设,采纳靠模拟。这是最需谨慎对待的席位——因为错误的候选会吃掉验证时间。

位置3 —— 自然语言报告。 用脚本从模拟的原始JSON里抽取指标(确定性),只把这些指标与改动上下文交给LLM,让它写出"可带进会议的一页纸"。核心变化3\~5行、受影响角色TOP 5、后续处置2\~3项。并钉死一条:不得使用所给指标之外的数字。原始数据整理的30分钟,变成5分钟的评审。

位置4 —— 异常模式解读。 上面的第2\~3步就是这个。对z-score挑出的outlier,LLM附上3\~5个假设。禁止下确定性诊断,是这个席位的生命线。

位置5 —— 下一步行动建议。 分析结束后,把"本版本立即处置 / 监控1周 / 1周后再评审的候选"连同优先级做成清单。它是防止数值策划漏掉决定的安全网,而不替代决定本身。

8.4.4 从哪里开始,又到哪里为止

把五处席位一次性全部开启,是最常见的失败。要从效果大、风险小的出口一侧开始开启。

ROI ↔ 引入风险矩阵 (右上 = 优先)

ROI 高 → 风险 低 ↑

位置3 报告 ①

位置4 异常解读 ②

位置1 场景 ③

位置5 行动建议 ④

位置2 改动建议 ⑤ 最谨慎,放到最后

圆圈里的圈码数字(①\~⑤)是引入顺序。位置3(报告)位置4(异常解读)处于右上——ROI(Return on Investment,投资回报)高、风险低的席位——所以先开。仅启用这两处,处理量便增至2\~3倍,引入效果的70%以上在此回收。位置2(改动建议)处于右下的红色席位,错误的候选可能吃掉验证时间,所以最谨慎地放到最后再开。也并非所有团队都要把五处全开——仅凭位置3·4,单人数值策划的一天就会改观。

引入周期的现实感受大致如下(作者估计,未经验证——随团队规模·工具成熟度差异很大)。位置3约1\~2周,再加位置4约2周,再加位置1约一个月,再加位置5约2周,位置2放在最后,约1\~2个月。这是"别一次全开"的另一种说法。

8.4.5 效果与成本,以及最常见的陷阱

在作者的项目A,用六个月陆续开启五处席位之后的变化如下。绝对数值为作者估计(未经验证),只应信任方向与比例——倍数随环境差异很大。

项目 引入前 引入后(方向)
单个数值策划每周模拟循环 5\~7件 25\~35件(约5倍)
报告撰写(每件) 30\~40分钟 5分钟评审
场景撰写(每件) 1\~2小时 15分钟评审
发现outlier → 诊断 1\~2天 4\~6小时
测量结果 → 下一次改动决定 2\~3天 1天

这里重要的不是倍数,而是时间挪到了哪里。人的时间从原始数据整理挪到了决策。并不是数值策划的人数减少了,而是一个人能覆盖的游戏范围变宽了。若把处理量的5倍读作裁员,引入的意义就会流向错误的方向。

成本很小。若采用提示词缓存,五处席位整体的每月LLM成本大约在$75上下(作者估计),不超过单个数值策划人力成本的1/100。因此引入的真正决定变量,不是LLM成本,而是评审负担。人是否有时间去读并筛掉AI抛出的假设与报告——这才是开与关的标准。

最后,留下18年间在同一位置反复出现的几个陷阱,并附上处方。

在平衡里,AI的席位很明确:确定性内核之外,人曾经乱找的那五处席位。把内核自始至终守为代码,只卸下其周边的手工劳动——这就是一个存续18年的模拟器在AI时代也能存活下来的方式。


本章要点

一行动手试试(单人精简版)

下一章预告

8.5 PvP·竞技平衡 —— 胜率矩阵·匹配·服务器权威

到目前为止,本部分的四章都在和同一个对手作战。一个 Boss 能在几秒内被击杀吗、坦克能有 89% 的存活率吗、金币在流失吗——全都是围绕单一目标的伤害、生存与收支的故事。可是在 PvP 中,对手是人。人不会像 Boss 那样按固定套路行动,即便是同一个职业,手法也各不相同,而最关键的是,他们会盯着彼此的弱点下手。PvE 平衡做得再深,PvP 却整块空缺,这种情况之所以常见,正是因为如此。单一目标的 DPS 曲线在 8.1\~8.4 已经讲透,但"剪刀胜过布"这张相克之网,却还一次都没有画过。

本章填补这块空白。要讲的有三样——承载职业与阵容之间相克关系的胜率矩阵、决定让谁与谁对战的匹配/MMR,以及能让上述所有数值沦为虚假的服务器权威·反作弊。而贯穿整个本部分的那条界线,在这里依然不变:战斗公式是确定性的,匹配与相克检测是 AI 辅助。一步都不会走偏。


8.5.1 PvP 区别于 PvE 的唯一一点

在 PvE 中,角色的强度是绝对值。剑士的 DPS 是 800 就是 800,Boss 就实打实地承受这 800。可是在 PvP 中,强度是相对的。剑士的 800 对弓手来说足够,但对能把自己所受伤害降低 30% 的盾兵而言,会被削减到 560 而显得不足。同一个角色的强度,会随着对手是谁而变化。仅此一点,就让 PvP 平衡成为与 PvE 根本不同的问题。

因此,PvP 平衡的单位不是单个角色的数字,而是一对关系。"剑士 vs 弓手"的胜率、"剑士 vs 盾兵"的胜率各自独立存在,把这些关系全部汇总,就成了一张表。横轴和纵轴都放同一份职业清单,每一格里写着"行击败列的概率"。这就是胜率矩阵。如果说 PvE 有 DPS 曲线,那么 PvP 就有这张矩阵。

PvE 是绝对值,PvP 是关系

剑士 DPS 800 Boss 原样承受 800 PvE:强度 = 绝对值

剑士 800 弓手 → 800(有效) 盾兵 → 560(不足) PvP:强度 = 随对手而变

→ 胜率矩阵 行击败列的概率 弓手 盾兵 法师 剑士 .58 .42 .50 弓手 -- .55 .47 盾兵 -- -- .61 (数字为示例 —— 非实测)

读右表的方法很简单。若"剑士 vs 盾兵"这一格是 0.42,就表示剑士击败盾兵的概率为 42%,也就是盾兵占优的相克关系。所有格都接近 0.50 固然是完美的平衡,但这样的游戏并不好玩。要像石头剪刀布那样存在循环相克的关系,职业选择才有意义。问题出在这个循环在某处断裂、出现某个职业击败所有人的格子时。如果说凌晨两点的坦克是 PvE 的事故,那么"盾兵 vs 全职业胜率超过 60%"就是 PvP 的事故。

这里要先钉死一点。填进这些格子的数字(0.58、0.42 等)全都是示例,而非实测。每款游戏的职业数、技能、目标平衡线都不一样。本章要信赖的不是数字,而是如何填矩阵、如何检查、以及在这检查的哪一环接入 AI这一结构。


8.5.2 填矩阵靠模拟,读矩阵靠 AI

填胜率矩阵的一格,用的正是 8.4 中所见的那套确定性模拟工具。把"剑士 vs 弓手"自动模拟 1,000 局,数一数剑士赢了多少局,那就是这一格的胜率。若职业有 N 个,格子就有 N×N 个,每格各跑 1,000 局,一张表就填满了。这套模拟自始至终都是代码——给相同的种子,就必须一字不差地复现出相同的矩阵。唯有如此,"这一版盾兵变强了"这句话才不是假的。

这里有一个 PvP 独有的陷阱。在 PvE 模拟中,对手(Boss)是固定套路,但在 PvP 模拟中,对手也得选择行动。决定剑士如何作战的机器人(bot policy)两边都要有。而这个机器人若是笨的,整张矩阵就都变成假的——让操作糟糕的机器人互相对战,得出的会是"随便什么时候都乱放技能的职业"获胜的矩阵,可在真正熟练的玩家手里结果可能恰恰相反。因此,PvP 矩阵上必须始终附带一条注脚:"这个机器人模仿的是哪种水平的玩法"。机器人通常用启发式规则(冷却好了就放、HP 低于 30% 就撤退等)来编写,而这套启发式规则本身是确定性的。

把机器人策略的要点落成可运行的形态,就是下面这样——输入相同就选相同行动、没有任何幻觉可乘之隙的函数。

def bot_decide(me, enemy, cooldowns, t):
    """确定性机器人策略。(状态)相同就行动相同。不由 LLM 生成。"""
    # 1) 生存优先:HP 低于 30% 就闪避/撤退
    if me.hp_ratio < 0.30 and cooldowns["escape"] <= 0:
        return Action("escape")
    # 2) 克制技能:敌人若非减益免疫则优先标记
    if cooldowns["mark"] <= 0 and not enemy.has("debuff_immune"):
        return Action("mark", target=enemy)
    # 3) 攻击距离管理:近战敌人贴身时拉开距离(远程职业)
    if me.is_ranged and dist(me, enemy) < me.kite_range:
        return Action("reposition")
    # 4) 其他:选冷却就绪的最大伤害技能
    return best_ready_damage_skill(me, cooldowns)


def simulate_pvp_match(class_a, class_b, formula, seed=0):
    """确定性地模拟一场 1:1。伤害直接沿用 8.1 的公式。"""
    rng = Rng(seed)
    a, b = spawn(class_a), spawn(class_b)
    for t in range(MAX_TICKS):
        for me, foe in ((a, b), (b, a)):
            act = bot_decide(me, foe, me.cooldowns, t)
            apply_action(act, me, foe, formula, rng)   # formula = 确定性伤害公式
        if a.hp <= 0 or b.hp <= 0:
            break
    return {"winner": "a" if b.hp <= 0 else "b" if a.hp <= 0 else "draw",
            "duration": t * TICK}

填满一整张矩阵的,是把这个函数在每一格各跑 1,000 次的外层循环。

def build_winrate_matrix(classes, formula, n=1000):
    matrix = {}
    for ca in classes:
        for cb in classes:
            if ca == cb:
                continue
            wins = sum(
                simulate_pvp_match(ca, cb, formula, seed=s)["winner"] == "a"
                for s in range(n)
            )
            matrix[(ca, cb)] = wins / n          # ca 击败 cb 的比例
    return matrix

到这里都是内核,自始至终是代码。AI 接入的不是制作这张表的环节,而是这张表的环节。N 若为 8,格子就有 56 个,让人用肉眼扫过 56 个胜率去找"哪里坏了",这跟凌晨两点的 4MB 的 JSON 是同一种苦工。挑出异常格子的,就交给 8.4 的 z-score 检测原样完成。

def find_broken_cells(matrix, low=0.40, high=0.60):
    """确定性地筛出大幅偏离平衡线(0.5)的格子。"""
    broken = []
    for (ca, cb), wr in matrix.items():
        if wr > high or wr < low:
            broken.append((ca, cb, round(wr, 2)))
    return sorted(broken, key=lambda x: abs(x[2] - 0.5), reverse=True)

检测把格子缩窄后,就把那一格交给 LLM。但纪律与 8.4 相同——禁止下确定诊断,只给假设和验证模拟。例如,给出"盾兵 vs 法师 0.68(z 值最大)"这一行,然后这样请求。

[损坏的格子]
盾兵 → 法师 胜率 0.68(平衡线 0.50,矩阵内 z 值最大)
附带:该对局的平均持续时间 38s(整体平均 22s)

[相关信息]
- 盾兵:所受伤害 -30% 的被动"铁壁",沉默技能"盾击"(2 秒)
- 法师:全部伤害的 70% 集中在一个施法 1.5 秒的技能上
- 两个职业的对战频率在实测队列中居前(热门组合)

请求:这一相克崩坏的可能原因假设 3~5 个 + 每条各配一行验证模拟。
禁止下确定诊断。只用"可能是……"的程度表述。

LLM 只是抛出诸如"铁壁 -30% 与 2 秒沉默叠加,可能形成法师一次都放不出核心施法技能就阵亡的正反馈 / 验证:把沉默持续时间减到 1 秒,对同一格重新模拟"这样缩小搜索空间的假设而已。什么才是真的,还得再按候选逐个跑 build_winrate_matrix 来判定。就连对局持续时间是平均的 1.7 倍这条线索,也由 LLM 一并编入假设——人用肉眼扫 56 格时容易漏掉的那个关联,正是 AI 在这一环节替你省下的时间。


8.5.3 匹配:掩盖相克关系的另一重平衡

即便把胜率矩阵调得完美无缺,让玩家觉得"输了"的真正原因另有其处:跟谁对战。实力 1500 的玩家遇上 2200 的玩家,即便职业相克是 5:5,结果也已注定。所以匹配不是单纯的服务器功能,而是平衡的一部分。如果说矩阵负责职业之间的公平,那么匹配负责实力之间的公平。

大多数竞技游戏都设有 MMR(Matchmaking Rating,匹配分)。这是一个赢则升、输则降的隐藏分数,把分数相近的玩家凑到一起。分数更新用的是确定性公式——Elo 用得最广,而且因为是公开标准,是本书能够引用的少数几个算式之一。

# Elo:公开标准更新式(并非编造的值)
expected_a = 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
new_rating_a = rating_a + K * (score_a - expected_a)
#   score_a:赢为 1,输为 0
#   K:更新强度常数(由游戏自行决定,通常在 16~40 范围内选取)
#   400, 10:Elo 定义中固定的常数

这个式子本身是确定性的,不是 AI 该介入的地方。然而匹配中有一处仅靠确定性公式解不开的张力:公平性 ↔ 等待时间的取舍。只匹配分数完全相同的对手,对局固然公平,但队列里若没有这样的对手,玩家就要等上 10 分钟。宽松地容许分数差,虽然能快速匹配,对局却变得不公平。越是凌晨时段、冷门职业、高分段,这种张力就越严重。

flowchart LR A["匹配请求<br/>玩家 MMR 1500"] --> B{"队列中有 ±50 的对手吗?"} B -->|有| C["立即匹配<br/>最公平"] B -->|无| D["随等待时间<br/>放宽容许范围 ±50→±200"] D --> E{"匹配成功?"} E -->|成功| F["匹配<br/>公平性 ↕ 等待 ↕ 平衡"] E -->|超时| G["投入机器人 / 保持排队<br/>策略决定"] C --> H["Elo 更新(确定性)"] F --> H style H fill:#dbeafe,stroke:#2563eb,stroke-width:2px style D fill:#ffedd5,stroke:#ea580c style G fill:#ffedd5,stroke:#ea580c

只有蓝色节点(Elo 更新)是确定性的。橙色节点——何时把容许范围放宽多少、超时后做什么——才是 AI 辅助触及的地方。但即便在这里,AI 也不做实时匹配决策。那是必须快速且可复现的服务器逻辑,是规则驱动代码的地盘。AI 接入的,是为调优这套规则而做的分析:概括"上周匹配日志中,哪个分段、哪个时段、哪个职业的对局质量(胜率偏差·等待时间)较差",并提出"如何改动容许范围曲线才能缩短哪个区间的等待时间"的候选。这正是 8.4 的位置 3(报告)、位置 4(异常解读)、位置 2(变更候选探索)换到匹配日志这个舞台上而已。

也点一下匹配与胜率矩阵纠缠的那个环节。匹配算法若不考虑职业、只对齐分数,损坏的相克格子就会被原样暴露。盾兵击败法师 68% 的那一格还活着,而匹配又频繁把这两者凑到一起,法师玩家体感上的失败就会比矩阵数值堆积得更多。所以矩阵检查与匹配日志分析并非各转各的,而是同一循环的入口与出口——在矩阵里修复损坏的格子,再到匹配日志里确认那一格实际被凑了多少次。


8.5.4 服务器权威:平衡的前提

到目前为止的所有讨论——矩阵、MMR、模拟——都暗中预设了一件事:玩家上报的结果是真实的。在 PvE 中,这几乎不成问题。一个人打 Boss,能骗谁呢?可是在 PvP 中有对手,赢了分数就涨,于是产生了作弊的动机。一旦出现篡改伤害、篡改位置、无视冷却的客户端,8.1 的确定性公式就只在纸面上是确定性的。在真实服务器上,某人的剑士正打出比公式高两倍的伤害。

所以竞技游戏的第一条平衡规则,排在矩阵之前:别让客户端来决定结果。伤害计算、冷却判定、命中判定——一切触及平衡的运算,权威都在服务器。客户端只发送输入(往哪移动、放哪个技能),而这个输入是否符合公式、冷却是否转好、是否在攻击距离内,全部由服务器重新校验。客户端发来的"伤害 999"被服务器无视,只有服务器按公式算出的值才被采用。

服务器权威 = 平衡公式的唯一执行者

客户端 只发送输入 "使用技能1,坐标(x,y)" 无法决定结果

输入 校验后的结果

服务器(权威) 冷却/攻击距离校验 伤害 = 公式(8.1) 确定性 · 执行 无视"伤害 999"

异常日志 不可能的输入 模式检测 可 AI 辅助

服务器权威一旦崩塌,整个平衡工作都变成假的。胜率矩阵调得再精细,只要线上有一个职业篡改伤害,那张矩阵就只是纸面上的承诺。所以反作弊不是独立的安全工作,而是平衡数据的可信度问题。当线上胜率与模拟矩阵严重不符时,第一个该怀疑的不是"公式错了吗",而应是"这份数据干净吗"。

这里 AI 的位置又一次清晰起来。作弊判定本身——"将此输入判为无效"——是确定性规则的活。0.1 秒内移动 30 米的输入在物理上不可能,所以用规则拦截。相同的输入必须给出相同的判定,而且不能造成冤枉的封禁,因此这里不能放概率性的 LLM。相反,把异常模式作为候选筛出来的活,是 AI 辅助能触及的。从服务器日志中,汇集诸如"这个账号的命中率分布偏离人类分布 z 值几何""这一组账号共享同一种异常模式"之类的候选,提交人工评审。把 8.1 的表搬到 PvP,界线就是这样。

领域 AI 理由
服务器伤害·命中·冷却判定 绝对禁止 确定性内核。相同输入=相同判定一旦被打破,公平性崩塌
Elo/MMR 分数更新 绝对禁止 公开标准的确定性式子。一旦动摇,排名就变成假的
作弊拦截(封禁)判定本身 绝对禁止 不容冤枉的封禁。相同证据=相同判定
胜率矩阵模拟 绝对禁止 无法复现时,"职业变强了"就成了假话
损坏相克格的检测·解读 可以 用 z-score 筛出格子,由 LLM 提假设(禁止下确定诊断)
匹配日志质量分析·调优候选 可以 提出等待/公平取舍的变更候选(经模拟验证)
作弊嫌疑模式候选提取 可以 仅提交人工评审的候选。封禁决定归人与规则

这条界线与 8.1 一字不差。AI 只栖身于确定性内核之外。负责执行的内侧——伤害、分数、封禁——是规则手册,而检测、解读、推候选的外侧,才是 AI 的位置。


8.5.5 串成一个循环

三个主题——矩阵、匹配、服务器权威——不是各转各的三件事,而是同一个竞技平衡循环的三个区段。服务器权威保证数据干净,用这份数据检查矩阵,借匹配日志确认检查结果在真实队列里如何被体感,再用模拟验证候选并反映到构建。

flowchart TD A["服务器权威 + 反作弊<br/>干净的线上数据"] --> B["线上胜率矩阵<br/>(实测)"] B --> C{"与模拟矩阵<br/>严重不符吗?"} C -->|"不符 → 怀疑数据"| A C -->|"一致 → 平衡问题"| D["损坏格检测 z-score"] D -->|"LLM 假设 3~5"| E["变更候选 + 验证模拟"] E --> F["build_winrate_matrix<br/>按候选重新模拟(确定性)"] F --> G["数值策划采纳/否决"] G --> H["反映到构建(不可逆)"] H --> I["匹配日志分析<br/>体感验证 + 调优候选"] I --> A style A fill:#dbeafe,stroke:#2563eb,stroke-width:2px style F fill:#dbeafe,stroke:#2563eb,stroke-width:2px style D fill:#ffedd5,stroke:#ea580c style E fill:#ffedd5,stroke:#ea580c style I fill:#ffedd5,stroke:#ea580c

蓝色节点(服务器权威、模拟重算)是确定性的,橙色节点(检测·假设·匹配分析)是 AI 辅助。这个循环里最常见的失败,是跳过 C 这个分支。线上矩阵与模拟不符时若直接动公式,实际上是追着被作弊污染的数据去削弱一个本无问题的职业。先怀疑数据是否干净的这一个分支,在 PvP 中扮演着与 8.1 的"变更记录"相同的角色——一旦漏掉,凌晨两点就会卷土重来。

最后,留下 PvP 平衡中 18 年里的几个陷阱,并附上处方。

在 PvP 中,AI 的位置与 PvE 相同:确定性内核——伤害、分数、封禁、模拟——之外的检测·解读·候选。内核用代码与服务器权威守护,只减去人扫 56 格、在匹配日志里摸索的那点手工劳动——本章就是整个本部分以同一副骨架运转的最后一个证据。


动手试试 —— 检查一张胜率矩阵

setup. 请编写把 8.4 的 simulate_dps 扩展为 1:1 的 simulate_pvp_match,以及驱动双方机器人的 bot_decide(启发式确定性)。先固定种子,确认同一张矩阵能否复现。伤害务必原样取用 8.1 的公式,并用一行记下机器人所模仿的玩法水平。

prompt.find_broken_cells 筛出平衡线(0.40\~0.60)之外的格子后,只把 z 值最大的那一格交给 LLM。

针对所附的损坏格子(盾兵 vs 法师 0.68,对局持续时间 38s/平均 22s),
请提出 3~5 个可能原因的假设,并为每条各配一行验证用的重新模拟。
相关技能·被动信息附于下方。禁止下确定诊断——只用"可能是……"。
不要直接改数值,只提候选。

verify. 别照单全收 AI 的假设。把每条假设的变更候选放进 build_winrate_matrix,用相同的种子重新模拟,同时确认那一格是否在回到 0.50 附近的同时没有打坏别的格子(PvP 的改动很容易修好一格却弄坏旁边一格)。只采纳满足这两个条件的候选,并像 8.1 那样在决策日志里留下理由·被否决的候选·预测值。应用到构建一周后,把线上实测胜率补记进那份日志。

单人精简版

哪怕是只有两个职业、也没有服务器的单人原型,骨架也是一样的。矩阵 2×2 就够了,模拟只需在 8.1 那个 30 行的循环上,再加一行机器人策略(冷却好了就放最大伤害技能)即可。服务器权威只要用代码结构守住"别让客户端决定结果"这条原则就行,正式的反作弊在还没有玩家之前并不需要。MMR 起初也先省略,只需跑 1,000 局,确认矩阵是否向一边倾斜超过 60%。AI 只用于读那结果、概括"哪场对局坏了、又可能是为什么"。无论规模大小,只有一条线要守住——伤害与胜负由代码和服务器决定,绝不交给 LLM。


本章要点

下一章预告

9.1 把 HUD 截图挂到 lint 上 —— 让 AI 抓出视线偏离与对比度不足的地方

首要读者:负责 HUD·UI 的 UX 策划(中等规模(10\~50 人)团队) 面向个人/业余读者的精简版:§9.1.8 「一个人的话,做到这些就够」

在 QA 构建中把新的减益提示叠加到 HUD 上的那天,设计师说"看得很清楚",而第二天用户论坛上就出现了"减益看不见,结果死了"。提示浮现在屏幕中央,灰底配上浅黄色文字。在设计师的显示器上看得见,但在战斗中爆炸特效铺满屏幕的 6 英寸手机上却看不见。问题在于,这并不是第一次。每一个构建、每一个画面,同一类事故都以"这次应该没事吧"反复上演。

本章聚焦于打断这种重复的一项工作。它是一个 lint 关卡:接收一张完成的 HUD 截图作为输入,自动检测 P0 元素是否偏离了视线所及的区域(顶部状态栏、两侧底部操作角),以及文字对比度是否越过了可读阈值。优先级表、视线流动、平台分支这类 HUD 设计的通用原则,其他书里已经讲得足够多,因此本章只把篇幅用在让这些原则在每次构建中自动强制生效的审查循环上。关键在于,让 AI 看着画面,用坐标和数字说出"这行字对比度只有 2.0:1,不到 WCAG 4.5:1",从而用代码和标准取代"我看着挺清楚啊"这种口舌之争。


9.1.1 审查标准不是"感觉",而是公开标准

HUD 审查之所以每次都因人而异得出不同结论,是因为标准停留在"看得见/看不见"这种主观判断上。所幸,可读性与无障碍的相当一部分,标准机构早已用数字钉死了。无需编造。

审查项 标准依据(出处) 自动判定
普通文本对比度 4.5:1 以上(WCAG 2.1 SC 1.4.3) 可以 —— 用前景·背景颜色值计算
大号文本(18pt+)对比度 3:1 以上(WCAG 2.1 SC 1.4.3) 可以
非文本(图标·计量条)对比度 3:1 以上(WCAG 2.1 SC 1.4.11) 可以
触摸目标最小尺寸 44×44 pt(Apple HIG) / 48×48 dp(Material) 可以 —— 按元素尺寸
拇指可达区域 横握双手操作时,左·右底部角落为"易触"(左拇指=移动,右拇指=技能)。业界通用的 thumb-zone 模型 部分 —— 按区域规则

只有最后一行(拇指可达区域)不是定量合格线,而是业界通用模型,上面四行则是 W3C·Apple·Google 公开的合格线。对比度尤其明确。WCAG 甚至公开了公式,要求用 (L1+0.05)/(L2+0.05) 计算两种颜色的相对亮度。把灰色(#888)背景配浅黄(#D4C84A)文字的对比度代入这个公式,得出约 2.0:1 —— 不到 4.5:1,即标准上明确的不合格。这是"在设计师显示器上看得见"这类反驳站不住脚的地方。

这里先说清一件事。MMORPG·RPG 的手机画面以横屏(landscape)为标准。原因在于信息量与操作。同样的英寸数,横握时一屏能容纳的常驻信息比竖屏多,而且双手拇指可以同时操作左(移动)·右(技能)。竖屏单手握持适合休闲消除·放置类,但不适合同时信息量大、需要双手操作的 MMORPG。因此本章所有的视线·布局判定都以横屏双手握持为前提。画面分为:上方的横向状态栏、左右两个底部操作角、二者之间的中央游戏区域,以及游戏区域下方的中央底部槽位栏(消耗·自动道具·快捷槽)。

这五行,就是本章要交给 AI 的审查规则手册。必须能说出"减益文字对比度 2.0:1,违反 SC 1.4.3",而不是"减益好像有点看不清",无论是人来审查还是 AI 来审查,才能得出相同的判定。

把平台基准和 PC 并列起来,审查的出发点就清晰了。项目A 是移动优先 + PC 辅助,因此把两套基准都放进规则手册。

基准 PC(辅助平台) 移动端(优先平台,横屏)
画面·输入 27 英寸+ / 鼠标 1px 精度·悬停·快捷键 6.x 英寸横屏 / 双手拇指,无悬停
同时常驻信息 可承受 30\~50 种 12\~16 种为上限(作者推测,未经验证)
视线·操作可达 全屏(光标随处可达) 仅顶部状态栏 + 左·右底部角落 + 中央底部槽位栏为"易触"
精度 1px 点击 最小 44pt 触摸目标(HIG)
核心审查风险 信息过密导致的认知负荷 屏幕狭小 + 手指遮挡 + 中央被淹没

PC 凭借鼠标精度·悬停提示·大屏幕,即使显示大量信息,视线与操作也够得着。移动端因为横屏比竖屏好一些,但装不下 PC 那么多,可点击元素被束缚在两侧拇指角上,又没有悬停,因此 P0 信息必须常驻显示。所以移动端 HUD 审查的本质不是"好不好看",而是"P0 是否位于视线所及之处(顶部·两角),文字是否越过标准对比度"。让这一判定不因人而异地用标准钉死,就是本章要做的事。


9.1.2 [实操记录] 把一张 HUD 截图挂到 lint 上

这里完整演示实际运行的一个周期。下面忠实再现了作者项目(移动优先 MMORPG,下称"项目A")的一次战斗 HUD 审查会话——这种把真实操作过程完整保留下来的记录,本书称为"实操记录(worked transcript)"。输入提示词可以直接复制使用,输出则是对真实会话的重构。

第 1 步 —— 输入:把截图 + 元素清单一起丢过去

只丢截图,AI 就会"猜测"画面。因此把构建已经掌握的元素坐标·颜色·分类,作为清单(manifest)一并放进去。这不是重新写,而只需从构建产物中提取即可(提取方法的现实在 §9.1.4 中如实比较)。

# hud_capture_manifest.yaml —— 随 QA 构建截图一并附上
screen: { w_pt: 844, h_pt: 390 }   # 6.x 英寸横屏,pt 单位(横握)
elements:
  - id: hp_bar        # 生命条
    class: P0
    rect_pt: [12, 18, 150, 16]      # x, y, w, h —— 顶部左侧
    fg: "#FF5A5A"  ; bg: "#1A1A1A"
  - id: skill_slot_1  # 技能槽(右拇指)
    class: P0
    rect_pt: [760, 300, 40, 40]     # ← 右下角,注意尺寸
    fg: "#FFFFFF"  ; bg: "#202830"
  - id: debuff_alert  # 减益提示(昨天新增)
    class: P0
    rect_pt: [400, 180, 70, 24]     # ← 屏幕中央,注意位置
    fg: "#D4C84A"  ; bg: "#888888"   # ← 注意对比度
  - id: minimap
    class: P1
    rect_pt: [744, 20, 80, 80]       # 右上角
    fg: "#A0C0FF"  ; bg: "#101820"

第 2 步 —— 提示词:让它审查,但强制标准与格式

附上的截图是项目A 的战斗 HUD(横握双手),yaml 是该画面各元素的坐标·颜色·分类。请对照两者进行审查。
对比度用 fg/bg 按 WCAG 计算,把数值也写出来 —— 文本 4.5:1、图标·大号字 3:1,不达标就判 FAIL。
P0 若偏离顶部状态栏或左·右底部角落、浮现在屏幕中央,就判 WARN(中央会被战斗特效淹没)。
操作元素若小于 44pt,或超出拇指角·中央底部槽位栏,就判 FAIL。
清单里没有但画面上能看到的,单独上报;拿不准的,归为"存疑"交给我。
只用表格给我(元素|检查|测量值|判定|备注),不要解释。
// (意图:P0=必须在 0.5 秒内找到的信息 / 消耗·自动·快捷槽在中央底部才是正常的)

值得注意的是,比起审查规则本身,更多的行数花在了"如何输出这些规则"上。给 AI 自由,它就会吐出"整体挺清爽的"这类恭维。因此要预先钉死计算式(WCAG 对比度)、合格线(4.5:1·44pt)、输出格式(表格)、必须标注测量值的义务,以及"拿不准就交给人"这一出口。这五颗钉子把恭维变成了测量值。

第 3 步 —— Claude 的输出(重构,原始)

| 元素          | 检查   | 测量值                    | 判定 | 备注                              |
|--------------|-------|--------------------------|------|----------------------------------|
| hp_bar       | 对比度 | #FF5A5A/#1A1A1A = 5.7:1   | PASS | 非文本计量条,满足 3:1             |
| hp_bar       | 布局   | 顶部左侧 (12,18)          | PASS | P0 顶部状态栏                     |
| skill_slot_1 | 触摸   | 40×40 pt,右下角          | FAIL | 不到 HIG 44pt,差 4pt(角落 OK)  |
| skill_slot_1 | 布局   | 右下 (760,300)            | PASS | 右拇指可达角                     |
| debuff_alert | 对比度 | #D4C84A/#888888 = 2.0:1   | FAIL | 普通文本不到 4.5:1(SC 1.4.3)   |
| debuff_alert | 布局   | 屏幕中央部 (400,180)      | WARN | P0 却在中央 —— 被战斗特效淹没     |
| minimap      | 对比度 | #A0C0FF/#101820 = 9.8:1   | PASS |                                  |
| minimap      | 布局   | 右上 (744,20)             | PASS | P1,允许置于顶部状态栏右侧         |

追加上报:
- 清单里是 4 种,但截图左下角还能看到一个黄色闪烁图标
  (坐标估计 ~70,330)。疑似清单遗漏。(存疑 —— 仅凭画面
  无法断定是什么元素)

小结:FAIL 2 件(skill_slot_1 触摸、debuff_alert 对比度),WARN 1 件(debuff_alert
布局),存疑 1 件(未登记图标)。

输出中最有价值的部分不是合格/不合格的表格,而是最下方的"追加上报"与"存疑"。AI 在画面上抓出了清单里没有的闪烁图标,又把"它到底是什么"这件自己无法断定的事交给了人。好的提示词,能让 AI 说出"这个我不知道"。

第 4 步 —— 验证与否决(人的位置)

不能照单全收这份输出。AI 的审查本身,要由人再审查一遍。实际上在这次会话里,就有一件被人推翻。

debuff_alert 的对比度 FAIL 和布局 WARN 是对的。灰底浅黄正如 §9.1.1 所见,违反标准;把 P0 提示放在横屏画面中央,也是会被战斗特效淹没的典型错误。到这里为止 AI 是对的。

问题出在 skill_slot_1 的触摸 FAIL 上。AI 照单信了清单里的 40×40 pt,判定为"不到 44pt";但在实际构建中,这个槽位视觉上是 40pt,而触摸命中框四周各扩展了 6pt,实际点击区域是 52pt。清单的 rect_pt 只装了绘制出来的矩形,没有装命中框 —— 也就是说,这是输入数据的缺陷,而非 AI 的误判。AI 在给定的数据范围内判定得很准确(角落位置的判定是对的),而人知道代码不了解的构建实情(命中框扩展)。这个 FAIL 由人驳回。

于是同时做两件事。修改清单提取脚本,让它也提取命中框(修复数据缺陷),并向 AI 重新发起请求。

skill_slot_1 视觉尺寸是 40pt,但命中框四周各扩展 6pt,实际点击区域是 52pt(已在清单里加了 hit_rect)。请按这个基准重新看触摸。
debuff_alert 的 FAIL/WARN 保持不变,请提出 3 组对比度超过 4.5:1 的配色(保留黄色系,背景调暗)。再给一个把它从中央移到顶部状态栏右侧的坐标。

AI 把 skill_slot_1 按命中框 52pt 基准更正为 PASS,并为减益的对比度返回了 3 套配色方案——把背景铺成较暗的 #2A2A00、做出 7.8:1——以及把提示移到顶部状态栏右侧(约 600,18)的坐标。一次往返就结束了。每次构建都靠肉眼扫一遍画面,同样的事故就会反复发生;但把截图+清单挂到 lint 上,对比度·布局·触摸的违规就会落成数字,人只需判定代码不了解的例外(命中框)和存疑(未登记图标)(审查一屏,靠手要十几分钟,用这个循环只要几分钟 —— 作者推测,未经验证的假设。与其看绝对时间,不如从"肉眼扫视"与"用标准测量"的结构差异去理解)。


9.1.3 横屏 HUD 的视线·布局 —— 为什么中央是危险的

把上面那次会话中 debuff_alert 之所以得到 WARN 的原因,以及 P0 信息应当放在哪里,用一张图留存下来,之后所有的布局判定都会更快。横握的手机上,画面分为四处。上方的横向状态栏(视线最先落到、手指不去的只读区),左·右两个底部角落(双手拇指可达的操作位 —— 左拇指=移动,右拇指=技能),二者之间的中央游戏区域(战斗发生的地方),以及游戏区域下方的中央底部槽位栏(放置消耗·自动道具和快捷槽·技能槽的地方)。下图中,绿色·琥珀色是 P0 与槽位安全的区域,红色是 P0 提示会被淹没的游戏中央。

上方横向状态栏 —— 视线第一优先 (HP · MP · 目标,只读)

中央 —— 游戏区域(特效爆发) 放 P0 提示会被淹没 —— debuff_alert 卡住的地方

中央底部 —— 消耗·快捷槽·自动 药水 自动 槽位

左拇指 移动

右拇指 技能

HP MP 目标 地图 P1 减益? 移动 技能 技能 技能

规则很简单。P0 信息(HP·MP·核心提示)要放在绿色区域(上方横向状态栏或两侧底部角落)之内。因为那是视线最先落到、或拇指常驻的必经之处。相反,游戏中央(红色)是战斗本身发生的地方,在这里放 P0 提示,特效铺满屏幕的那一刻信息就会被淹没。有一点要注意 —— 游戏中央与中央底部是不同的。游戏中央危险,但其下方的中央底部槽位栏(琥珀色)是消耗·自动道具和快捷槽·技能槽栖身的地方。为了一眼看到自己使用或自动消耗的东西,才把它放在两拇指之间。此外,只读的信息(HP/MP/目标血量)放在顶部,可点击元素(移动·技能)放在两侧底部角落,消耗·槽位放在中央底部 —— 这三者就是手指·视线的区域。§9.1.2 中减益提示之所以得到 WARN,用这张图一张就能说明 —— 因为把必须在 0.5 秒内看到的 P0,偏偏放到了最看不见的游戏中央。更正方案中把它移到顶部状态栏右侧,正是把它送回了这张图里的绿色区域。


9.1.4 坐标该怎么提取 —— 实现的诚实

本章的 lint 建立在"各元素的坐标·颜色"能干净地进来这一前提上。然而,这些坐标从哪里、怎么提取,才是实际中最现实的分岔口。这是书里常常含糊带过的地方,所以这里如实比较三条路径。正确答案不止一个,会随团队情况而不同。

路径 做什么 优点 缺点 / 现实
① 游戏内遥测(telemetry)日志 由构建直接转储 UI 框架所绘制控件的坐标·尺寸·颜色 坐标精确(非估计),连命中框·锚点都能拿到 需要在 UI 代码里埋入转储钩子。需要程序员协作。一旦铺好,最为可信
② 现成的 vision API 把截图送入 OCR·目标检测 API,提取文本·框的坐标 无需修改构建,外部截图也可以 坐标是近似值,对计量条·图标这类非文本的分类较弱。外传 = 未公开构建的泄露风险
③ 自行实现(像素分析) 直接读取截图,用启发式提取色彩边界·框 依赖最少,用于色彩对比度计算已足够 不知道元素的含义(它是不是 P0)。必须与清单对照才有用。有维护负担

三条路径的关系,恰好解释了本章的实操记录。§9.1.2 中,对比度检查之所以准确,是因为颜色值(fg/bg)通过①·③准确地进来了;而触摸 FAIL 之所以被人推翻,是因为命中框在清单里缺失了(②·③看不到命中框,只有①看得到)。也就是说,对比度仅凭像素也能抓到,但触摸命中框没有①遥测(telemetry)就抓不到。带着对这一局限的认识出发,才能划清 AI 审查结果可信到什么程度的那条线。

作者项目的选择,是以①遥测为正本,AI 则作为对照截图+遥测清单的审查者这样一种结构。只在画面上出现、而清单里没有的东西(§9.1.2 那个未登记的闪烁图标)由 AI 抓,清单里有、而画面上对不上的东西由人抓。只靠其中之一,两边都会留下盲区。

flowchart LR A["QA 构建<br/>HUD 截图"] --> C B["遥测转储<br/>坐标·色·命中框<br/>(路径 ①)"] --> C["清单 + 截图"] C --> D["AI 审查<br/>对比度·布局·触摸<br/>+ 上报未登记元素"] D --> E{"WCAG/HIG<br/>标准判定"} E -->|FAIL/WARN| F["人工审查<br/>仅例外·存疑"] F -->|重新请求| D F -->|通过| G["通过构建关卡"] classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class A,B,C data; class D ai; class E code; class F human; class G pass;

人手触及的只有两处。把遥测转储干净地放进去的位置(最前),以及判定代码·标准抓不住的例外(命中框)·存疑(未登记元素)的位置(最后)。中间那些枯燥的对比度计算与布局对照,交给 AI 和标准去跑。


9.1.5 把规则手册变成代码 —— 对比度·触摸·角落的自动关卡

AI 审查每次都重新算一遍,会耗费 token 和时间。像对比度·触摸·角落可达这类能落到确定性上的项目,由代码先处理。AI 只介入代码抓不住的部分(画面语义解读、未登记元素)。两者不是竞争,而是分工。

# hud_lint.py —— HUD 清单标准校验(骨架)
# 输入:遥测清单(每个元素的 rect/hit_rect/fg/bg/class/interactive)
# 输出:WCAG/HIG + 双手可达违规列表

def _luminance(hex_color):           # WCAG 相对亮度
    r, g, b = (int(hex_color[i:i+2], 16) / 255 for i in (1, 3, 5))
    f = lambda c: c/12.92 if c <= 0.03928 else ((c+0.055)/1.055) ** 2.4
    R, G, B = f(r), f(g), f(b)
    return 0.2126*R + 0.7152*G + 0.0722*B

def contrast_ratio(fg, bg):          # WCAG 明暗对比度
    L1, L2 = sorted((_luminance(fg), _luminance(bg)), reverse=True)
    return (L1 + 0.05) / (L2 + 0.05)

def in_thumb_corner(e, w, h):
    """是否为横握时双手拇指可达的左·右底部角落。"""
    x, y = e["hit_rect"][0] / w, e["hit_rect"][1] / h
    bottom = y > 0.55
    left_corner  = bottom and x < 0.30   # 左手拇指 = 移动
    right_corner = bottom and x > 0.70   # 右手拇指 = 技能
    return left_corner or right_corner

def lint(elements, screen_w, screen_h):
    issues = []
    for e in elements:
        # 规则 A:明暗对比度(文本 4.5:1 / 非文本·大号字 3:1)
        need = 4.5 if e["kind"] == "text" else 3.0
        cr = contrast_ratio(e["fg"], e["bg"])
        if cr < need:
            issues.append(f"[A] {e['id']}: 对比度 {cr:.1f}:1 < {need}:1 (WCAG SC 1.4.3)")
        # 规则 B:触摸目标 —— 以命中框为准(不是视觉尺寸)
        if e.get("interactive"):
            tap = min(e["hit_rect"][2], e["hit_rect"][3])   # ← 是 hit_rect 而非 rect
            if tap < 44:
                issues.append(f"[B] {e['id']}: 点击 {tap}pt < 44pt (HIG)")
            # 规则 C:操作元素应位于双手拇指角(左·右底部)
            if not in_thumb_corner(e, screen_w, screen_h):
                issues.append(f"[C] {e['id']}: 操作元素被放在了双手拇指角之外 "
                              f"(x={e['hit_rect'][0]}, y={e['hit_rect'][1]})")
    return issues

这段代码能终结会议上"这行字是不是有点看不清?"的口舌之争。当代码输出 [A] debuff_alert: 对比度 2.0:1 < 4.5:1 (WCAG SC 1.4.3),就没什么可争的了。改就是了。值得注意的两行:规则 B 看的是 hit_rect 而非 rect,以及规则 C 只让操作元素从左·右两个底部角落通过 —— §9.1.2 中人推翻 AI 的那条教训(命中框),和横屏双手握持的可达极限,一起进了代码。不是单一的"拇指弧"阈值,而是把左拇指(移动)·右拇指(技能)两个角落分开来看,这才是横屏判定的核心。人抓过一次的例外,从下次起由代码来抓。因此留给 AI 的只是一个狭窄的角色:"除了代码判为 PASS 的之外,上报只在画面上出现的异常(未登记元素·视觉重叠·截断)"。能靠确定性抓的交给代码,需要画面语义解读的交给 AI,了解构建实情的例外交给人 —— 这个分工才是核心。


9.1.6 本章数值的出处

本章出现的数值,出处只有三类。对比度 4.5:1·触摸 44pt·48dp 是 WCAG SC 1.4.3·HIG·Material 的官方值,#888 背景配 #D4C84A 文字约为 2.0:1,也是把颜色值代入该公式的计算值(§9.1.1·§9.1.5)。"审查一屏靠手要十几分钟、用循环只要几分钟"·"横屏常驻信息 12\~16 种"是未经验证的作者推测,正文里已如此注明。其余的(各构建的对比度 FAIL 数、触摸命中框不达标数、拇指角偏离数、遥测误触率)都是能从构建日志里直接数出来的值。像用户投诉数这类无法凭一个 HUD 断定因果的结果指标,没有列入 KPI。


9.1.7 常见的失败

模式 为什么失败 处方
在设计师显示器上肉眼审查 缺了 6 英寸·战斗特效的条件,对比度事故反复 把截图 lint 作为构建关卡(§9.1.2)
只把截图丢给 AI 说"帮我看看" 靠猜测坐标做近似判定,不可信 附上遥测清单(§9.1.4)
用视觉尺寸判定触摸目标 漏了命中框扩展,把好端端的按钮判成 FAIL hit_rect 为准检查(§9.1.5)
把 P0 提示放在屏幕中央 被战斗特效淹没,"看不见结果死了" 移到顶部状态栏·两角(§9.1.3)
把操作按钮放在画面左侧中央·顶部 横屏双手握持时拇指够不着 移到左·右底部角落(§9.1.5 规则 C)
以竖屏单手握持为前提设计 MMORPG 以横屏双手为标准,信息·操作对不上 转为横屏双手(§9.1.1)
用"看得见/看不见"讨论对比度 结论因人而异 改用 WCAG 4.5:1 的计算值(§9.1.1)

第四个最常反复出现。急着叠加新提示时,空位只剩画面中央,就放那儿 —— 而那个中央恰恰是游戏发生的地方。


9.1.8 动手试试 —— 今天就能做的一步

一个人的话,做到这些就够:没有遥测、没有清单也可以。给自己的游戏(或喜欢的游戏)拍一张横屏 HUD 截图,用取色器取出最小的两三个文字·图标的前景/背景色并手动记下,再贴上 §9.1.2 的提示词跑一次。挑一个 AI 算出的对比度数值,用在线 WCAG 对比度计算器亲手验算一遍,你就会切身体会到"看得见/看不见"是如何变成数字的。如果 AI 把某个 P0 放在了画面中央,不妨反驳它"再看看为什么中央是危险的"。

如果是团队,就从下面这一步开始。先和程序员就"从 UI 框架转储控件坐标·色·命中框的遥测钩子(路径 ①)"达成一致,然后从 §9.1.5 的 contrast_ratio 这一个函数开始放进构建。对比度计算是标准公式,不会有异议,哪怕只有一个函数,每次构建的对比度 FAIL 也会落成数字。之后再叠上 in_thumb_corner,横屏双手操作元素的角落偏离也能由代码抓住。布局·未登记元素这类解读,在其上叠一层 AI 即可。


本章要点

下一章预告

9.2 技能按钮排布 —— AI 生成 3 个布局方案,lint 负责淘汰

主要读者:移动优先动作·MMORPG 的 UX·战斗策划(中型团队) 面向个人/业余读者的精简版:§9.2.7「一个人的话,做到这一步就够」

如何把新职业的 6 个技能摆到移动端屏幕的哪个位置、以什么方式摆放。每当这个问题被摆上会议桌,最初的 30 分钟总是如出一辙。有人在白板上画出六个圆圈,另一个人说"那个位置拇指够不到",又有人接话"那往上挪的话小地图就被挡住了"。三个人说的都对,可结论一直没出来。到了下一次会议,同一块白板又被重画一遍。

问题在于:画布局草案这件事,和检查这份草案是否遵守规则这件事,在同一个人的脑子里混作一团。画的人往往难以否决自己画出来的东西。本章把这两件事拆开。画多个布局草案这种枯燥的活儿交给 AI,而草案是否违反了重叠·拇指角落·触控尺寸规则,则由代码来淘汰。 人只站在最后一个位置上——从代码放行的方案里,凭"游戏手感"挑出一个。如果说 9.1 立起了整个 HUD 的规则手册,那么本章就是把这份规则手册,一直贯彻到技能按钮——这个手指触碰最频繁的部件——上的一个完整循环。


9.2.1 技能按钮为何棘手 —— 它不是"用来读的信息",而是"用来按的信息"

HUD 上的大多数元素只是用来读的。没有人会去按 HP 条。所以在 §9.1 的拇指角落示意图中,HP·MP·目标血量放在手指够不到的顶部只读区域也无妨。技能按钮恰恰相反。它要以 0.1 秒为单位精准按下,而战斗中视线一直盯着敌人,手指是凭记忆去找位置的。位置只要稍有偏差,当场就会误触。

MMORPG 移动端以横屏双手握持为标准,需要按的元素放在左右两个下角,消耗品/道具槽放在底部中央(为什么横屏是标准、这三个区域各是什么,在 §9.1 中讨论)。在这一标准下,技能几乎全部铺在右手拇指够得到的右下角簇里(左手拇指被绑定在左下角的移动上)。这里做一个区分——以 0.1 秒为单位按下的主动技能落在这个右下角簇里,而消耗品·自动道具·快捷槽则单独放在两个拇指之间的底部中央槽位带上。本章只讨论主动技能按钮,所有坐标判定都以横屏双手握持为前提。

因此技能按钮的布局同时被三条确定性规则绑定——最小触控目标(HIG 44pt)、相邻按钮间距(Material 8dp)、拇指可达性(技能位于右拇指的右下角)。这三项都已写在 §9.1.1 立起的规则手册里,是可以凭坐标和尺寸判定的项目,因此公开标准的数值沿用那份规则手册(触控 44pt·间距 8dp 是公认数值,只有右拇指角落是业界通用模型)。这三项就成为本章淘汰 AI 布局方案的lint 的第一层输入。当代码说的不是"这个按钮是不是有点小?",而是"skill_3 是 40pt,不足 HIG 44pt"时,白板前的 30 分钟就消失了。

把平台基准与 PC 并排来看,布局的出发点就清晰了。PC 是精密·大量,移动端横屏则限于双手角落(完整对比表见 §9.1 规则手册)。单看技能输入,差别很明显——PC 用快捷键,技能无论摆在屏幕哪里,手指都在键盘上,所以可达性不成问题,槽位也可以很多。移动端横屏既没有悬停也没有快捷键,所以技能要按频率铺在右拇指够得到的右下角(同屏最多 6\~8 个),并把最常用的技能放在角落内侧(最容易够到的位置)。因此移动端技能布局的本质不是"好看的排布",而是"在右拇指角落内按频率做优先级布局 + 规则手册验收"。而画多个草案这件事,若由人手工来做,既枯燥又每次标准都会飘。枯燥又善变的重复劳动——正是 AI 比人更不知疲倦地完成的地方。


9.2.2 [实操记录(worked transcript)] 新职业 6 个技能的布局方案 —— 让 AI 生成 3 个方案

本节从输入到废弃,完整展示把新职业"萨满"的 6 个主动技能布局到移动端的一个循环。以下内容忠实再现了作者项目(移动优先 MMORPG,下称"项目A")的新技能 UI 工作会话。输入与提示词可以直接复制使用,输出则是对真实会话的重构。

第 1 步 —— 输入:把技能规格做成机器可读的表

把 6 个技能的使用频率与基本性质做成 yaml。使用频率是从数据表的战斗日志中提取的值,并非新编造出来的。

# skill_set_shaman.yaml —— 新职业"萨满"的 6 种主动技能
screen: { w: 2400, h: 1080, dpr: 3 }   # 以 6.x 英寸横屏为准,pt = px / dpr
skills:
  - id: s1_quickbolt    # 基础攻击,最频繁
    use_rate: 0.41      # 战斗中使用占比(日志提取)
    role: spam          # 连点
  - id: s2_hex          # 减益,频繁
    use_rate: 0.22
    role: core
  - id: s3_totem        # 放置型,一般
    use_rate: 0.14
    role: core
  - id: s4_heal         # 治疗,偶尔但紧急
    use_rate: 0.11
    role: panic         # 危急时立即
  - id: s5_curse        # 群体减益,偶尔
    use_rate: 0.08
    role: situational
  - id: s6_ultimate     # 终极技,罕用
    use_rate: 0.04
    role: burst

核心字段是 use_raterole。最常按的 s1_quickbolt(41%),以及危急时须在 0.2 秒内找到的 s4_heal(panic),必须放在右拇指最容易够到的位置(右下角内侧)。罕用的 s6_ultimate(4%)放在角落边缘、稍远一些也无妨。这一优先级就是下一步 AI 布局的全部输入。

第 2 步 —— 提示词:强制生成 3 个方案,并以数字形式接收坐标

附件 yaml 是新职业的 6 种主动技能。请在横屏双手握持的画面上做出 3 个技能按钮布局方案。技能铺在右手拇指够得到的右下角(左手被绑定在左下角的移动上)。
三个方案要用不同的理念——例如角落扇形、2 行网格、频率加权式。每个按钮给出 x,y,w,h(以 px 为单位,画面 2400x1080,dpr 3,pt=px/3)。
常用或 role=panic 的技能放在右拇指容易够到的角落内侧,触控目标不小于 132px(44pt),相邻间距不小于 24px(8dp)。
每个方案务必一并写上一行设计意图和你自己看到的一个弱点——没有弱点的方案不行。每个方案只给(排布理念|坐标表|设计意图|自评弱点),不要多余说明。

在这段提示词里,比布局指示更下功夫的地方,是强制规定输出格式和弱点申报。坐标不以数字形式接收就无法运行 lint,所以钉死了 px 输出;同时要求一并给出"这个方案的 1 个弱点",让 AI 先说出自己的局限。若是随意地说"帮我做个好看的 UI",出来的就是普通移动端 RPG 的平均布局,而且没有图、只有文字描述。

第 3 步 —— Claude 的输出(真实会话重构,节选)

== 方案 A:右下角扇形 ==
排布理念:以右拇指旋转轴为中心,把 6 个按钮排成扇形。按频率由内向外。
| id          | x    | y   | w   | h   | pt   |
|-------------|------|-----|-----|-----|------|
| s1_quickbolt| 2160 | 912 | 168 | 168 | 56pt |
| s2_hex      | 2016 | 852 | 132 | 132 | 44pt |
| s3_totem    | 2052 | 996 | 132 | 132 | 44pt |
| s4_heal     | 1884 | 924 | 132 | 132 | 44pt |
| s5_curse    | 1920 | 780 | 120 | 120 | 40pt |
| s6_ultimate | 1788 | 840 | 120 | 120 | 40pt |
设计意图:把常用的 s1 做成最大、最易够到,放在右下角内侧。
自评弱点:s5、s6 为 40pt,可能不足 HIG。扇形外侧右拇指可达性模糊。

== 方案 B:2 行网格(2x3)==
排布理念:右下角 2 行 x 3 列网格。优先视觉对齐。
| id          | x    | y   | w   | h   |
|-------------|------|-----|-----|-----|
| s1_quickbolt| 2208 | 912 | 132 | 132 |
| s2_hex      | 2052 | 912 | 132 | 132 |
| s3_totem    | 1896 | 912 | 132 | 132 |
| s4_heal     | 2208 | 756 | 132 | 132 |
| s5_curse    | 2052 | 756 | 132 | 132 |
| s6_ultimate | 1896 | 756 | 132 | 132 |
设计意图:所有按钮同一尺寸,对齐整齐。
自评弱点:上排(s4、s5、s6,y=756)处于右拇指角落的上边界,可达性模糊。
         紧急治疗 s4 在上排,危急时可能找得慢。

== 方案 C:频率加权扇形 ==
排布理念:s1 为中心大圆,其余按频率环绕在右拇指旋转轴周围。
| id          | x    | y   | w   | h   |
|-------------|------|-----|-----|-----|
| s1_quickbolt| 2184 | 900 | 180 | 180 |
| s2_hex      | 2028 | 852 | 144 | 144 |
| s3_totem    | 2052 | 996 | 132 | 132 |
| s4_heal     | 1908 | 912 | 144 | 144 |
| s5_curse    | 1932 | 792 | 120 | 120 |
| s6_ultimate | 1812 | 972 | 120 | 120 |
设计意图:手自然落到 s1,把紧急的 s4 放大并放在角落内侧。
自评弱点:因为是扇形,按钮间距不均匀。s2-s5、s4-s6 有临近冲突之虞。

三个方案都申报了自评弱点,这是这份输出的核心。A 是"担心不足 40pt",B 是"紧急治疗在上排",C 是"担心临近冲突"。AI 先指出了自己所画之图的薄弱之处。但这只是自我申报,真正的判定由代码来做。

第 4 步 —— lint:代码淘汰三个方案

用眼睛比较三个方案,又会开始"B 看着更整齐吧"这类口味之争。于是把三个方案原样喂给 §9.2.3 的 skill_layout_lint.py。结果如下。

[方案 A] 右下角扇形
  [FAIL] B-size  : s5_curse 40pt < 44pt (不足 HIG)
  [FAIL] B-size  : s6_ultimate 40pt < 44pt (不足 HIG)
  [WARN] C-corner: s6_ultimate x=1788 —— 角落左边界,右拇指可达"一般"
  → 通过 4/6,致命违规 2

[方案 B] 2 行网格(2x3)
  [FAIL] C-corner: s4_heal     y=756 (0.70h) 未低于 0.55h → 右拇指角落上方
  [FAIL] C-corner: s5_curse    y=756 (0.70h) 未低于 0.55h → 右拇指角落上方
  [WARN] role    : s4_heal(panic) y=756 —— 紧急技能在上排
  → 通过 4/6,致命违规 2

[方案 C] 频率加权扇形
  [FAIL] A-overlap: s2_hex ∩ s5_curse 间距 18px < 24px (不足 8dp)
  [FAIL] A-overlap: s4_heal ∩ s6_ultimate 间距 12px < 24px (不足 8dp)
  → 通过 4/6,致命违规 2

三个方案全部被淘汰。有意思的是,自我申报与 lint 判定几乎重合。AI 说是"弱点"的地方,实际就出了违规。但自我申报只是"担心",而 lint 给的是"s2_hex 与 s5_curse 间距 18px"这样的数字。会议上没什么可争的。

这一步有一个重要决定。不要因为三个方案全被淘汰就回到起点。把 lint 报告原样贴进下一条提示词,重新请求。

第 5 步 —— 重新请求:把 lint 报告原样喂进去

三个方案的 lint 结果原样贴在这里(上面的报告)。从中只挑违规最容易修的一个方案,只给出把 FAIL 全部变为 0 的一版修正方案。
所有按钮都不小于 132px(44pt),间距不小于 24px(8dp),s4_heal(panic) 放在右拇指角落内、仅次于 s1 最容易够到的位置。
只给坐标表,并逐行写明哪个 FAIL 是如何解决的。
// (意图:角落内 = x>横向70%(1680),y>纵向55%(594))

AI 选了方案 A(角落扇形)。因为它的违规只是"两个按钮尺寸不足",改动量最小。它给出的修正方案是:把因过小而被淘汰的 s5·s6 放大到 132px,并随着尺寸变大把扇形曲线略微推向角落内侧,以确保间距。

== 方案 A'(修正)==
| id          | x    | y   | w   | h   | pt   |
|-------------|------|-----|-----|-----|------|
| s1_quickbolt| 2160 | 900 | 168 | 168 | 56pt |
| s2_hex      | 2010 | 846 | 132 | 132 | 44pt |
| s3_totem    | 2046 | 990 | 132 | 132 | 44pt |
| s4_heal     | 1872 | 918 | 144 | 144 | 48pt |
| s5_curse    | 1908 | 774 | 132 | 132 | 44pt |
| s6_ultimate | 1740 | 858 | 132 | 132 | 44pt |
解决:B-size s5 40→44pt / B-size s6 40→44pt /
     C-corner s6 x=1740(0.725w)·y=858(0.79h) 保持在角落内侧 →
     role: s4_heal 放大到 144px,强化紧急识别。

把方案 A' 再次喂给 skill_layout_lint.py

[方案 A'] 右下角扇形(修正)
  [PASS] B-size  : 全部按钮 ≥ 44pt
  [PASS] A-overlap: 最小间距 30px ≥ 24px
  [PASS] C-corner : 全部操作按钮在右拇指角落内 (x≥1680, y≥594)
  [WARN] C-corner : s6_ultimate x=1740 —— 角落左端,可达"一般"
  → 通过 6/6,致命违规 0,WARN 1

FAIL 变成了 0。剩下的 1 条 WARN(s6_ultimate 在角落左端,右拇指可达不是"容易"而是"一般")代码不会自动扼杀,而是上交给人。而且这条 WARN 其实是有意为之的设计。s6 使用频率 4%,是最罕用的终极技,所以角落最内侧的位置应当让给常用的 s1,把它放在边缘才对。人判定"这条 WARN 是有意的"并放行。输入 → 生成 3 个方案 → lint → 全军覆没 → 重新请求 → 通过的一个循环,在此闭合。

这一圈就是本章的 Show 标准。若不从头到尾看清 AI 画了什么、lint 淘汰了什么、人保住了哪条 WARN,"用 AI 生成了 UI 方案"这句话就是空的。


9.2.3 把 lint 写成代码 —— 重叠·拇指角落·HIG 尺寸

上述循环的心脏,是淘汰三条规则的 30 余行代码。§9.2.1 表中的三个项目原样变成三个函数。

# skill_layout_lint.py —— 技能按钮排布验证(骨架)
# 输入:AI 给出的按钮坐标列表 [{id, x, y, w, h, role, use_rate}]
# 输出:A-overlap / B-size / C-corner 违规列表
# 前提:横屏双手握持。技能铺在右手拇指够得到的右下角。

MIN_TAP_PX    = 132    # HIG 44pt * dpr 3 = 132px
MIN_GAP_PX    = 24     # Material 8dp * dpr 3 = 24px
RIGHT_CORNER_X = 0.70  # 屏幕横向 0.70 右侧 = 右拇指角落
BOTTOM_Y       = 0.55  # 屏幕纵向 0.55 下方 = 底部角落

def in_right_thumb_corner(b, w, h):
    """横屏握持下,是否为右手拇指够得到的右下角。
    (左手拇指=左下角移动,右手拇指=右下角技能)"""
    rx, ry = b["x"] / w, b["y"] / h
    return rx > RIGHT_CORNER_X and ry > BOTTOM_Y

def lint(buttons, screen_w, screen_h):
    issues = []
    # 规则 B: 触控目标最小尺寸 (HIG 44pt)
    for b in buttons:
        side = min(b["w"], b["h"])
        if side < MIN_TAP_PX:
            issues.append(f"[FAIL] B-size : {b['id']} {side//3}pt "
                          f"< 44pt (不足 HIG)")
    # 规则 A: 相邻按钮重叠/间距 (最近两条边的距离)
    for i, a in enumerate(buttons):
        for c in buttons[i+1:]:
            gap = edge_gap(a, c)          # 两个矩形的最短间距(px)
            if gap < MIN_GAP_PX:
                issues.append(f"[FAIL] A-overlap: {a['id']} ∩ {c['id']} "
                              f"间距 {gap}px < {MIN_GAP_PX}px (不足 8dp)")
    # 规则 C: 操作元素须在右拇指角落内。panic 越靠角落内侧越好。
    for b in buttons:
        rx, ry = b["x"] / screen_w, b["y"] / screen_h
        if not in_right_thumb_corner(b, screen_w, screen_h):
            issues.append(f"[FAIL] C-corner: {b['id']} "
                          f"x={b['x']}({rx:.2f}w) y={b['y']}({ry:.2f}h) "
                          f"→ 右拇指角落外")
        elif b.get("role") == "panic" and rx < 0.78:
            issues.append(f"[WARN] role   : {b['id']}(panic) "
                          f"紧急技能靠近角落内侧边界")
    return issues

这段代码让会议上"B 方案更好看啊"这种口味发言失效。好看是在 lint 放行之后才谈的事。凡是被 lint 吐出 [FAIL] 的方案,好看与否都进不了构建。这是把 §9.1.1 立起的 HUD lint 关卡,彻底应用到技能按钮这个最棘手的部件上——凭坐标·尺寸可判定的交给代码,"这条 WARN 是不是有意的"这类判断交给人的分工,在这里同样成立。

整个循环一目了然地看,就是下面这样。

flowchart LR A["技能规格 yaml<br/>(use_rate·role)"] --> B["AI:3 个布局方案<br/>坐标+自评弱点"] B --> C{"skill_layout_lint.py<br/>重叠·尺寸·右拇指角落"} C -->|有 FAIL| D["把 lint 报告<br/>原样重新请求"] D --> B C -->|FAIL 0,仅 WARN| E["人:判定 WARN<br/>是否为有意"] E --> F["布局确定<br/>+ ArtGuide 06_UI sync"] classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class A data; class B ai; class C code; class E human; class F pass;

人经手的地方只有两处。最前端——把输入规格干净地放进去,以及最末端——判定 lint 杀不掉的 WARN。中间那些枯燥的 3 方案生成与坐标检查,由 AI 和 lint 来跑。


9.2.4 记录通过率 —— 用数字看工具的性能

只生成一次布局方案就收工,便无从知道这个工具运作得好不好。所以每一次都把 lint 结果记入日志。记录的值很简单——AI 给出的方案在第一次 lint 中通过了几个(首次通过率),以及经过几次重新请求达到 FAIL 0(往返次数)。

下面的数值,是用这个循环制作 3 个新职业(萨满外加 2 种)技能 UI 时亲手计数的实测值。样本仅 3 个职业(共 9 次布局会话),很小,所以应当把它当作方向值而非精确的总体参数来读。没有任何加工过的数字。

项目 实测 备注
AI 首个布局方案中 lint 首次通过 9 次中 1 次 其余 8 次有 1 个以上 FAIL
首次通过时的平均 FAIL 数 每方案 1.8 件 大多是尺寸不足或在右拇指角落外
达到 FAIL 0 的平均往返 1.4 次 lint 报告再投入方式
最常见的 FAIL 类型 B-size(尺寸不足) 其次是 C-corner(右拇指角落)

最重要的一行是第一行。AI 首次给出的方案,9 次里有 8 次没能通过 lint。 这不是这个工具的失败,而是正常运作的信号。让 AI 自由给出坐标,它就常常违反 HIG 44pt。lint 每次都把它揪出来,再把报告回喂,1\~2 次往返就归零。假如首次通过率是 100%,那意味着 lint 太松,而不是 AI 完美。

这份通过率日志,也成为决定 lint 规则该收紧还是放松的依据。若某类 FAIL 每次都以"其实是有意的"为由被人放行,那这条规则就太严了。反过来,如果上线后收到误触投诉、而 lint 却放行了,那就是规则太松。


9.2.5 把确定方案画成图 —— 按钮排布 SVG

把 §9.2.2 中通过 lint 的方案 A' 按坐标原样画出来,就是下图。表里的数字在真实画面上是什么形状,得看图才抓得住。横屏手机双手握持的姿势下,左手拇指落在左下角(移动),右手拇指落在右下角(技能簇)。圆的大小与触控目标(pt)成正比,颜色表示拇指可达难度(绿色容易 / 黄色一般)。

顶部 —— 仅状态显示(HP · MP · 目标,只读)

游戏画面(战斗发生的地方)

移动 左拇指

右拇指"容易"角落 ↘

HP MP 目标 地图

s1 56pt

s2 44

s3 44

s4 紧急

s5 44

s6 一般

容易 一般(s6=罕用终极技,有意)

看图就能一眼理解 lint 报告中最后那条 WARN。只有 s6_ultimate(黄色)位于右下角的左端,是右拇指可达"一般"的位置。但 s6 是使用频率 4% 的终极技,放在角落边缘才对。最常用的 s1(绿色,最大 56pt)放在右拇指最容易够到的角落内侧右下,紧急治疗 s4(黄色边框)则放大尺寸,好让危急时手能快速找到。左手拇指被绑定在左下角的"移动"上,所以技能全部聚在右侧角落。一张坐标表与一张图精确一致——这正是把坐标以数字形式接收的原因。


9.2.6 常见失败

模式 为何失败 处方
只在白板上画圆圈开会 没有坐标无法 lint,口味之争反复 以 px 接收坐标喂给 lint(§9.2.2)
"AI 帮我做个好看的技能 UI"整包外包 没有规则手册就只是普通 RPG 平均布局 3 方案+坐标+自评弱点的强制提示词
以竖屏单手握持为前提布局 MMORPG 以横屏双手为标准,技能在右拇指角落 以横屏 2400x1080、右下角为基准做 lint
只用眼睛比较布局方案 每次都漏掉不足 HIG·重叠 skill_layout_lint.py 自动判定
首个方案通过 lint → 就安心以为工具做好了 可能是 lint 太松的信号 用通过率日志检查规则收紧(§9.2.4)
连 WARN 都由代码自动拦截 连有意的布局(罕用终极技)也被杀掉 WARN 交由人判定(§9.2.3)

第五种最常被忽视。AI 首个方案每次都通过,心情固然好,但那通常意味着 lint 规则太松。9 次里有 8 次被淘汰才是健康的状态。


9.2.7 动手试试 —— 今天能做的一步

一个人的话,做到这一步就够:没有 lint 代码也行。挑出你自己游戏(或喜欢的游戏)的 4\~6 个技能,按 §9.2.1 的格式手写一份规格(use_rate 大致按频率排序即可),把 §9.2.2 的提示词原样贴上,拿到 3 个方案。然后不用卷尺,只把"44pt = 132px"记在脑子里,在 AI 给出的坐标表中用手找出小于 132px 的按钮并圈出来。再假设是横屏,看看有没有技能落在右下角(横向 70% 右侧 + 纵向 55% 下方)之外。这一次就会让你亲身体会 lint 在做什么。

团队的话,就从下面这一步开始。先把 §9.2.3 的 skill_layout_lint.py 三个函数(尺寸·间距·右拇指角落)用代码固定下来。三个函数就够了。有了规则手册,无论是 AI 布局方案还是设计师草案,都能用同一条线来量,只有通过 lint 的方案才会流转到美术团队的 96_ArtGuide/06_UI/,经 _convert_md_to_html.py_SyncToArtRepo.bat 路径自动 sync。在确定坐标抵达美术团队之前,人的最后一件事,只是把某一条 WARN 判定为"有意"。


本章要点

下一章预告

9.3 ArtGuide/06_UI 协作 —— 策划用 md 写,美术团队只看 html

主要读者:每天与非策划职能(美术)协作的 UX·UI 策划(中等规模团队) 面向单人/业余读者的精简版:§9.3.8「一个人的话,做到这些就够」

策划用 Markdown 把 UI 决定事项整理好,事情就会变得利落:能做版本管理,能看到 diff,还能原样丢给 AI。问题在于美术团队不读 Markdown。更准确地说,他们没有理由去读。对美术设计师说"请从 SVN 拉取 아트_결정사항.md 来看",一半人根本没装 SVN 客户端,另一半人则对着在记事本里打开、## 标题和表格语法全乱掉的画面问"这个要怎么看啊"。

这里错误的处方是"教美术团队用 Markdown"。美术设计师的时间应该花在推敲像素上。花在学习 Markdown 约定、SVN 检出、看 diff 上的时间全是损耗。正确的处方是在策划这一侧把转换与传递自动化,把美术团队的学习负担降到 0。策划用 md 写,脚本把它转成 html,另一个脚本再把它推送到美术仓库,美术团队在浏览器里只看 html。本章会把这条流水线实际从头到尾跑一遍 —— 从用 AI 生成决定事项初稿的环节,到转换·传递的自动化,再到人工究竟拒绝了什么。


9.3.1 协作真正崩掉的地方是"格式"

很多书把策划与美术协作崩掉的原因归结为"决策权模糊":谁定颜色、谁定功能。这种分工固然重要,但无论把分工表画得多好,只要美术团队读不了这张分工表,就什么都不会发生。实务中更常出事的地方不是决策权,而是传递格式。

在笔者的项目(移动优先 MMORPG,下称"项目A")里,实际反复出现的事故是这样的。

事故 表面原因 真正原因
美术拿旧版决定事项在做 "没拉到最新的" 传递是手动(邮件附件),导致遗漏
决定事项表格显示错乱 "这怎么回事" 用记事本打开了 md
"那个决定写在哪儿?" 口头传达 正本(canonical)散落在聊天里

三起事故都不是决策权的问题。它们的根源在于正本文档没有以美术团队能读的格式、自动地、始终保持最新地传递过去。所以本章的工具不是分工表,而是传递流水线。分工只要达成一次共识就完事,而传递在每次决定变化时都会发生。

先看实际的文件夹结构。项目A 的美术指南在 workspace/96_ArtGuide/ 下分为 7 个领域。

96_ArtGuide/
├── 00_Common/      # 通用(风格·配色板·打光基准)
├── 01_Character/
├── 02_Animation/
├── 03_Monster/
├── 04_NPC/
├── 05_VFX/
├── 06_UI/          # ← 本章所讲的领域
└── 07_Env/

另外,这个文件夹里还放着两个运维文件:_convert_md_to_html.py_SyncToArtRepo.bat。这两个文件就是本章的脊梁。


9.3.2 四阶段同步流水线 —— 从策划的 md 到美术团队的浏览器

整个流程分为四个阶段。关键在于人(策划)只碰阶段1的 md,其余三个阶段全部由脚本来跑。美术团队只看阶段4的 html,甚至不需要知道 md 的存在。

flowchart LR A["阶段1 · 策划团队 SVN<br/>아트_결정사항.md<br/>(策划编写/AI 初稿)"] A --> B["阶段2 · _convert_md_to_html.py<br/>md → html 转换<br/>(表格·标题·图片嵌入)"] B --> C["阶段3 · _SyncToArtRepo.bat<br/>自动 push 到美术 SVN<br/>(独立仓库)"] C --> D["阶段4 · 美术团队浏览器<br/>只看 html<br/>(md 约定学习负担为 0)"] D -.反馈/修改请求.-> A classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; class A,D human; class B,C code;

下面逐一点明每个阶段到底做什么。

阶段1(策划,人工) —— 在 06_UI/아트_결정사항.md 里用 Markdown 写下决定事项。如何在这个环节嵌入 AI,是 §9.3.4 的脊梁。决定事项就是"按钮 primary 颜色 #3A7BD5""触控目标最小 44pt"这类条目。

阶段2(_convert_md_to_html.py,自动) —— 把 md 转换为 html。不是简单转换,而是把表格渲染得便于美术团队阅读,把 ![](...) 图片引用内联嵌入,并加上目录。产出的是美术设计师在浏览器里双击一次就能打开的自包含 html。

阶段3(_SyncToArtRepo.bat,自动) —— 把转换好的 html push 到美术团队独立的 SVN 仓库。关键在于策划仓库和美术仓库是分开的。美术团队只看自己的仓库即可,不需要了解策划仓库的权限和结构。

阶段4(美术团队,人工) —— 美术设计师在浏览器里打开同步到自己仓库的 html。既不用学 Markdown 语法,也不用学 SVN 命令,更不用学怎么看 diff。md 约定的学习负担为 0,这正是这条流水线的设计目标,也是它的成功标准。

反馈从阶段4 回到阶段1。美术说"这个决定怪怪的",策划就改 md,阶段2\~3 又会自动跑一遍。美术只需要重新打开更新后的 html 即可。


9.3.3 为什么要把转换·传递自动化 —— 学习负担的不对称

这里先停一下,把设计意图讲明白。把 md 转成 html 这件事本身微不足道。真正的设计在于决定了谁来承担谁的学习负担

选项有两条路。

方案 A —— 美术团队学 md 策划 只写 md 美术 5 人 ×SVN·md 学习 学习成本 = 1 次编写 × 乘以美术人数 负担蚕食像素作业时间 → 最终被弃用

方案 B —— 策划做自动化 策划 md+脚本 1 次 美术 5 人 html 双击 学习成本 = 策划 1 次 (美术负担为 0) 美术只专注于像素 → 得以持续

关键在于不对称。方案 A 的学习成本要乘以美术人数,而且每来一名新人就重复发生一次。方案 B 里策划只要写一次脚本就完事,美术一侧的边际成本为 0。把负担压给能自动化的一侧,而不是人多的一侧 —— 这就是非策划职能协作工具的第一原则。一旦这条原则被打破,也就是协作工具强迫对方职能去学新东西,那么这个工具在一两个季度内就会"没人用"。


9.3.4 [实操记录(worked transcript)] 用 AI 生成 UI 决定事项的 md 初稿

前面说阶段1 由策划来写 md,这里把用 AI 生成这份 md 初稿的环节完整走一个循环。决策会议结束后,会留下零散的记录(聊天·白板照片·口头共识)。把这些整理成正本的决定事项 md 很枯燥,而且每次格式都会走样。这正是最适合交给 AI 的活儿。不过,决定本身由人来做,AI 只负责把决定整理成既定格式 —— 这条边界是关键。

第 1 步 —— 输入:原始会议记录

[UI 决策会议记录 —— 关于 06_UI 技能槽,原始未整理]
- 说要把技能槽按钮尺寸调大。因为在移动端太小了。
- 颜色由美术来定。不过 primary 色调保持蓝色系。
- 槽位禁用(冷却)状态的表现,商定为灰色+数字叠加。
- 多语言……技能名变长怎么办?暂时搁置。
- 啊,还有长按弹出技能说明(这是功能,策划已确定)。

第 2 步 —— 提示词:强制区分决定/搁置/责任方

附件是 UI 决策会议的原始记录。请整理成要交给美术团队的决定事项 Markdown。
把每一项分类为 [确定]/[搁置]/[美术决定](确定=策划拍板、美术照做;搁置=未决+原因;美术决定=属于视觉表现由美术定、策划只给约束),并各附一行依据/约束 —— 如果记录里没有依据,就如实写成"依据未记载",不要用猜测来填。
像触控尺寸这种能用公开标准验证的,要注明标准;记录里没有的项目,绝对不要新编造。
表格用 |项目|分类|决定/约束|依据|是否可验证|,并包含 frontmatter(title、layer: L1),给出完整全文。

这段提示词的重心不在整理本身,而在于强制分类和禁止编造。让 AI 整理决定事项时,它会热心地把记录里没有的"最佳实践"补进来,而这些补充在美术团队看来就成了"策划已敲定的决定"。所以要钉下三根钉子:[确定]/[搁置]/[美术决定] 分类、依据义务,以及"记录里没有就不要编造"

第 3 步 —— Claude 的原始输出(真实会话还原)

---
title: 06_UI 决定事项 —— 技能槽
layer: L1
---

# 06_UI 决定事项 —— 技能槽

| 项目 | 分类 | 决定/约束 | 依据 | 可验证 |
|---|---|---|---|---|
| 技能槽按钮尺寸 | [确定] | 最小 44×44 pt 以上 | 移动端触控目标,Apple HIG 建议 44pt | ✅ 坐标·尺寸 |
| primary 色调 | [美术决定] | 保持蓝色系(约束),精确值由美术定 | 记录:"primary 色调保持蓝色系" | △ 颜色值 |
| 冷却状态表现 | [确定] | 灰色叠加 + 剩余数字 | 会议商定 | ◯ 视觉评审 |
| 技能名多语言长度 | [搁置] | 后续会议 | 记录:"暂时搁置" —— 长文本语言(德·俄)未做验证 | — |
| 长按技能说明 | [确定] | 长按 → 显示工具提示 | 功能确定(策划领域) | ◯ 行为评审 |
| 槽位间距 | [确定] | 建议最小 8 dp | 依据未记载 —— 记录里没有,参考 Material 建议值 | ✅ |

第 4 步 —— 验证与拒绝(人工的环节)

这份输出不能原样当作正本上传。有一行有问题。

就是最后一行 槽位间距 | 最小 8 dp。回头再看记录,会议里根本没提过间距。是 AI 自己判断"既然是移动端 UI,总该有个间距标准吧",于是热心地把 Material Design 的建议值加了进来。这违反了规则 5("禁止编造记录里没有的项目")。AI 虽然如实标注了 依据未记载,但根本就不该造出这个条目本身。这一行一旦到了美术团队手里,就会被读成"策划已敲定 8dp 间距"。

于是重新提出请求。

"槽位间距"在会议记录里没有,是你加上去的。请从表格里删掉。
记录里没有但看起来需要决策的,不要放进表格,只作为候选列到最下面的"## 未决 —— 下次会议议题";决定事项表里只保留记录中实际有的项目。

AI 把间距条目从表格里删掉,并在最下面把"下次会议议题:槽位间距标准(目前未定)、多语言技能名长度处理"单独分离为候选。现在决定事项表里只剩下会议上真正定下的内容,而 AI 想到的合理候选则从"确定"降级为"议题"。这一分离之所以重要,是因为美术团队拿到的文档里一旦混淆了什么是确定、什么还在讨论,美术就会把未定事项当作确定去开工

经过这一次往返,阶段1(md)就完成了。现在它离开人的手,进入阶段2\~3 的自动化。


9.3.5 阶段2\~3 自动化 —— 转换与传递不经人手

完成的 md 现在交给脚本处理。转换脚本的骨架很简单。

# _convert_md_to_html.py (骨架)
# 输入:06_UI/*.md (策划编写的决定事项)
# 输出:同名的 .html (美术团队在浏览器中打开的自包含文件)

def convert(md_path):
    md_text = read(md_path)
    front, body = split_frontmatter(md_text)          # 提取 title·layer
    html_body = markdown_to_html(body, extensions=[
        "tables",        # 表格渲染 (解决美术在记事本里看到的错乱表格)
        "fenced_code",
    ])
    html_body = embed_images_inline(html_body, base_dir=md_path.parent)
    # ↑ 将 ![](char_skill_ui.png) 这类引用内联嵌入 →
    #   美术无需另外获取图片文件
    toc = build_toc(html_body)                         # 自动生成目录
    return render_template(title=front["title"], toc=toc, body=html_body)

这里的关键是,转换不是简单的 md→html,它还多做了三件事:把表格正确渲染(美术在记事本里看到的错乱 |---| 消失了)、把图片内联嵌入(美术不必另外获取图片文件)、自动加上目录(决定事项再长,美术也能跳到想看的条目)。正是这三件事,让"只看 html 就行"真正成立。

传递脚本则是这样组织的。

REM _SyncToArtRepo.bat (骨架)
REM 1) 将 06_UI 的所有 md 转换为 html
python _convert_md_to_html.py 06_UI\*.md

REM 2) 把转换好的 html 复制到美术 SVN 工作副本
xcopy 06_UI\*.html %ART_REPO%\UI\ /Y

REM 3) 自动提交·push 到美术 SVN (独立仓库)
svn add %ART_REPO%\UI\*.html --force
svn commit %ART_REPO%\UI -m "[auto] 06_UI 决定事项更新"

策划要做的只是双击一次 _SyncToArtRepo.bat(或者挂一个钩子,让它在提交决定事项时自动执行)。这样,转换·复制·推送到美术仓库就会一次跑完。美术团队更新自己的仓库,最新的 html 就已经到位了。

AI 能介入到哪一步 —— 这段阶段2\~3 的自动化代码,完全可以让 AI 来写。"写一个脚本,接收 md 文件夹,转换成含表格·图片的 html,再 push 到独立 SVN",这是 AI 擅长的领域。然而哪个决定要定为确定、什么要交给美术决定(§9.3.4),不会委托给 AI。代码交给 AI,决定交给人 —— 全书反复出现的这条分工,在这里同样适用。


9.3.6 图片提示词也要先写"设计意图"

在美术协作中,AI 被用错的典型例子就是图片提示词。策划给美术团队提供参考图、或者想快速把概念可视化时,会用图像生成 AI。此时常见的失误,是一上来就写结果描述("蓝色圆角按钮、发光效果、4K")。

笔者的协作原则之一是 image_prompt_design_intent_first —— 图片提示词也不要先写结果描述,而要先写设计意图

方式 提示词 问题/效果
结果优先(差) "蓝色圆角按钮、发光、4K、游戏 UI" 美术无法追问"为什么是蓝色?"。意图蒸发
意图优先(好) "直观传达技能可用/冷却状态的技能按钮。可用=让人想立刻按下的视觉吸引力,冷却=抑制。色调为 primary 蓝色系" 美术看到意图后,可以反向提出更好的视觉方案

区别在于美术团队拿到提示词后能做什么。只拿到结果描述,美术要么照着画,要么无视,二选一。拿到设计意图,美术就能提出一个把这个意图解得更好的自己的视觉方案。这正是策划在不侵犯美术决定领域(§9.3.4 的 [美术决定])的前提下,还能给出方向的方法。策划给出"为了什么",美术决定"看起来怎样"。

所以,在 §9.3.4 的决定事项 md 里放图片参考时,图注也不写"蓝色按钮",而写"以区分冷却状态为目的的槽位 —— 精确表现由美术决定"。转换脚本会把这条图注连同图片一起嵌入 html,于是美术会同时拿到图片和意图。


9.3.7 度量 —— 什么是能诚实计数的

有一种诱惑,想把这条流水线的效果写成"协作事故减少了 70%"之类的数字。这种数值一旦没经过验证,就会削弱本书的可信度。诚实地做个区分。

可用公开标准验证的 —— 决定事项里出现的触控 44pt·间距 8dp·对比度 4.5:1 这类公开标准,遵循 §9.1 的规则手册。它们不是编造的数值,而是可以原样引用、并用 lint 自动验证的值。

可度量的运营指标 —— 这条流水线实际能计数的是这些:美术拿旧版开工的事故数(传递若是自动的则收敛到 0)、美术团队新人第一次打开决定事项所花的时间(双击 html 的话是分钟级)、决定变更反映到美术仓库的延迟(脚本执行时间)。这三项不是"感觉",而是能用日志·观测来计数的。

笔者的推测(未经验证的假设) —— "比手动邮件传递时遗漏更少"这个方向是明确的,但由于没有另外记录样本,精确的下降率不做断言。与其看绝对值,不如按方向来读:传递若掌握在人手里,忙碌的一周必定会出现遗漏;传递若是脚本,遗漏就会在结构上消失。


9.3.8 动手试试 —— 今天就能做的一步

一个人的话,做到这些就够:没有美术团队、没有 SVN 也没关系。假设你要把 UI 决定传给你委托的外包美术,或者一起协作的朋友。照搬 §9.3.4 的提示词,用 AI 生成一张 md —— 把脑子里零散的 UI 决定分类为 [确定]/[搁置]/[美术决定]。然后从中找出一条 AI"热心补进来"的项目(记录里原本没有的),反驳它一句"这个我没定过,删掉",你就能亲身体会到,在整理决定这件事上,人和 AI 的边界到底在哪。转换只需 markdown 包的一行 python -m markdown decision.md > decision.html 就足够。

如果是团队,就从下面这一步开始。别一上来就写宏大的双向同步,而是先放进一行转换 + 一行传递:一个把决定事项 md 转成 html 的转换脚本(只做 §9.3.5 里的表格渲染·图片嵌入),外加一行把这个 html 复制到美术能看到的位置(无论是共享盘还是独立仓库)。哪怕只有这两行,"美术在记事本里看 md 撞上错乱表格"这一最常见的事故也会消失。分工表·决策权的梳理,是再往后的事。

用 setup → prompt → verify 概括,就是这样。

步骤 要做的事
setup 先放进 _convert_md_to_html.py(转换)+ 一行传递(复制/push)
prompt 用 §9.3.4 的提示词,把会议记录整理成 [确定]/[搁置]/[美术决定] 的 md
verify 拒绝 AI 编造的项目(记录里没有的)→ 自动执行转换·传递 → 美术只确认 html

本章要点

下一章预告

10.1 一致性校验 atom —— 守护 30 张表 FK 的 cascade

周五晚上 6 点 40 分。那天定下要在下周一的公司内部构建里新加入 12 种任务。我在 quest_table 里追加新行,在奖励表里填上对应的行,又在对话表里把 NPC 台词接上。三张表,约 50 行。我用眼睛扫了两遍,看上去没有问题。

周一早上,构建挂了。新任务中有一条所引用的 reward_id 在奖励表里并不存在。周五晚上我把一行奖励删掉后又重新加了回来,过程中把 id 敲错了一个字符——把 rwd_q318 敲成了 rwd_q381。这是一种人眼绝对抓不住的笔误。两张表位于不同的文件夹,由不同的人、在不同的时间去改动。行数只有 50 的时候,眼睛还抓得住。可一旦超过 30 张表开始通过外键(FK)互相引用,人的眼睛就不再是一件检查工具了。

本章要展示的,是一种能在构建挂掉之前抓住这个笔误的检查 atom——integrity_check_fk——它如何校验 30 多张表的 FK 一致性,并在出现断裂时,通过协作工具(一种管理任务与日程的 SaaS——本项目使用 ClickUp,JIRA、Redmine 也是同一类)通知到负责人;我会沿着自己实际跑过的一个会话,把这个流程呈现出来。

我进入这个行业,起点正是从别人做好的东西里找出错位的那一行。单机游戏的 QA 与验收是我的第一份工作,那时手和眼睛是唯一的检查工具。二十多年过去,如今我把同样的活儿交给了代码——就在人眼不再充当检查工具的那个位置上。


10.1.1 检查该抓住什么 —— FK 断裂的结构

先用图来看清楚检查的是什么。游戏数据表就如同关系型数据库。一张表的列指向另一张表的主键。一旦这根箭头断掉,运行时游戏要么崩溃,要么更糟——悄无声息地显示一个空值。

quest_table quest_id (PK) reward_id (FK) npc_id (FK)

reward_table reward_id (PK)

npc_table npc_id (PK)

npc_id 'npc_307' → npc_table 中不存在(断裂的 FK)

绿色实线是活着的引用:quest_table.reward_id 指向的值在 reward_table.reward_id 中确实存在。红色虚线是死掉的引用——某条任务的 npc_id 指向了一个 npc_table 中并不存在的 id。integrity_check_fk 要抓的正是这根红色虚线。

项目A的数据表里,这样的引用绝不止一两个。30 多张表彼此指向,若把 FK 箭头全画出来,一页纸都装不下。让人每次都用手去逐一追踪,是不可能的。


10.1.2 一个会话 —— 检查抓住断裂 FK 的全过程

从这里开始,是我实际跑过的一个会话。我不做概括,而是把提示词全文、原始输出,以及我否决并重新要求的部分,都原样记录下来。

第 1 步 —— 读取表格,让它提取 FK 候选

我先让检查器做出一张"哪个列指向哪张表"的 FK 地图。让人把 30 张表的 FK 逐一登记并不现实,于是我让它依据规格书表的列命名规则(*_id 指向同名的表)自动推断候选。我输入的提示词全文如下。

读取所附的规格书表($스키마 标签页),从所有数据表中推断出看起来像外键(FK)的列。规则:(1)列名为 xxx_id 形式,且(2)在 xxx_tablexxx_list 表中存在同名的 PK(Primary Key,主键)列,则视为 FK 候选。把推断结果以 {source_sheet, source_column, target_sheet, target_column, confidence} 列表形式输出为 JSON。凡是无法用规则断定的,把 confidence 标为 low 并写明理由。

Claude 的原始输出片段(连同含糊的条目一并原样照录):

[
  {"source_sheet":"quest_table","source_column":"reward_id",
   "target_sheet":"reward_table","target_column":"reward_id","confidence":"high"},
  {"source_sheet":"quest_table","source_column":"npc_id",
   "target_sheet":"npc_table","target_column":"npc_id","confidence":"high"},
  {"source_sheet":"quest_table","source_column":"prev_quest_id",
   "target_sheet":"quest_table","target_column":"quest_id","confidence":"medium",
   "note":"名字不是 quest_id 而是 prev_quest_id,因此未命中规则。推测为同表自引用。需人工确认"},
  {"source_sheet":"reward_table","source_column":"item_id",
   "target_sheet":"item_table","target_column":"item_id","confidence":"high"},
  {"source_sheet":"dialogue_table","source_column":"speaker_id",
   "target_sheet":"npc_table","target_column":"npc_id","confidence":"low",
   "note":"无法确定 speaker_id 指向的是 npc_id 还是 player。似乎也存在 speaker 为 'player' 的行。无法断定"}
]

这里有两个条目我没有直接放行。prev_quest_id(自引用)我采纳为检查对象,但亲手加上了"空值(第一个任务)不算违规"这条例外。dialogue_table.speaker_id 我否决了——因为确实存在 speaker'player' 的行,若把它当作 FK 来检查,所有玩家台词都会被判成假违规。

这次否决很重要。若把自动推断出的 FK 地图不经评审就直接使用,首次运行就会涌出数百条假违规。必须由人过滤一遍,检查才能赢得信任。

第 2 步 —— 用评审过的 FK 地图定义检查 atom

我把过滤后的 FK 地图固定为 integrity_check_fk atom 的输入。atom 的格式如下。这是项目A中实际使用的一个检查 atom 的全文。

---
name: integrity_check_fk
description: 依据登记的 FK 地图,校验所有 source 列的值都存在于 target 表的 PK 中
type: integrity_check
category: data
priority: P0          # 断裂的 FK 阻断构建
execution_time:
  - on_save           # 表格保存时仅检查该表
  - on_build          # 构建时检查全部 FK
  - nightly           # 每天午夜全量 + 报告
input:
  fk_map: fk_map.reviewed.json   # 第 1~2 步中由人评审过的地图
output_format: violation_list
on_violation:
  - notify: clickup           # 失败时通知 ClickUp
related_atoms:
  - integrity_check_clickup_notify
  - integrity_check_id_uniqueness
---

检查逻辑本身并不长。它是一个集合成员检查:确认 source 表的每个值是否在 target 表的 PK 集合中。

def check_fk(fk_map, sheets):
    violations = []
    for fk in fk_map:
        pk_set = {r[fk["target_column"]] for r in sheets[fk["target_sheet"]]}
        for i, row in enumerate(sheets[fk["source_sheet"]]):
            val = row[fk["source_column"]]
            if val in ("", None):          # 空 FK 为例外(第 1 步定下的规则)
                continue
            if val not in pk_set:
                violations.append({
                    "fk": f'{fk["source_sheet"]}.{fk["source_column"]}',
                    "row": i + 2,          # 表头 1 行 + 1-index
                    "value": val,
                    "target": fk["target_sheet"],
                    "severity": fk.get("severity", "P0"),
                })
    return violations

第 3 步 —— 跑检查,抓住真正断裂的 FK

我用评审过的地图对全部 30 张表跑了检查。输出是标准的 violation_list。以下是那天实际得到的结果(id 与表名做了匿名化处理,违规条数与结构均为真实)。

{
  "check": "integrity_check_fk",
  "executed_at": "2026-05-18 09:14:02",
  "input_files": 31,
  "violations": [
    {"fk": "quest_table.reward_id", "row": 318, "value": "rwd_q381",
     "target": "reward_table", "severity": "P0",
     "message": "reward_id 'rwd_q381' 在 reward_table 中不存在。推测为 'rwd_q318' 的笔误"},
    {"fk": "quest_table.prev_quest_id", "row": 502, "value": "q_0500",
     "target": "quest_table", "severity": "P0",
     "message": "prev_quest_id 'q_0500' 在 quest_table 中不存在。推测为 'q_500' 的写法不一致(0 填充)"}
  ],
  "summary": {"fk_checked": 23, "rows_scanned": 4117, "violations": 2, "passed": 4115}
}

周五晚上那个笔误(rwd_q381)在第一行就被抓到了。第二条则是我此前并不知道的另一个问题。某条任务的 prev_quest_idq_0500,而实际的任务 id 是 q_500。这是加了 0 填充导致的写法不一致。在人眼里两者看着一样,但作为字符串却是不同的值,于是游戏找不到前置任务,便把这条任务一直留在锁定状态。这是一类若上线就会有玩家来咨询的缺陷。

message 字段里的"推测为笔误""推测为 0 填充",是我让检查器在单纯的成员匹配失败之外,一并给出最接近的 PK 值(以编辑距离为准)的那部分。它能缩短人去追查"这为什么会断"的时间。不过这些推测终究只是提示,真正的修正值由人来定。


10.1.3 断裂发生时 —— 直到协作工具通知的 cascade

到这里,是单个检查的动作。但检查即便抓住了违规,没人看见也就没有意义。关键在于让违规径直抵达负责人的那条流程。在项目A中,这条流程由一个名为 integrity_check_clickup_notify 的独立 atom 负责(在 JIT 元数据中其影响力评分为 294.93,是验证 atom 组里评分最高的 atom 之一——这意味着,让一致性失败抵达人,与检查本身同等重要)。

完整的 cascade 如下。各检查 atom 依次执行,某一环节一旦出现 P0 违规,便流向通知 atom。

flowchart TD A[表格保存 / 构建触发] --> B[integrity_check_id_uniqueness<br/>PK 重复检查] B -->|有重复 P0| F[阻断构建] B -->|通过| C[integrity_check_fk<br/>基于 FK 地图的一致性检查] C -->|断裂 FK 0 条| D[integrity_check_range<br/>奖励·数值范围检查] C -->|有断裂 FK P0| E[integrity_check_clickup_notify] D -->|范围违规 P1| E D -->|通过| G[检查 PASS · 构建继续] E --> H{severity?} H -->|P0| I[创建协作工具任务<br/>+ 提及负责人 + 阻断构建] H -->|P1| J[协作工具评论 + alert<br/>构建继续] I --> F classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class A data; class B,C,D,E,H,I,J code; class G pass; class F fail;

这个 cascade 里包含两个设计决策。

第一,PK 重复检查排在 FK 检查之前。 FK 检查以 target 表的 PK 唯一为前提。若 PK 有重复,"这个值是否在 PK 集合中"这一问题本身就失去了意义。因此我在 integrity_check_fk 的 atom 里显式写入 related_atoms: integrity_check_id_uniqueness,并在 cascade 中固定了顺序。一旦所依赖的检查失败,FK 检查就跳过——因为跑了也只会得到假结果。

第二,通知的强度按 severity 分级。 P0(断裂的 FK)会在协作工具里创建任务,提及 FK 地图中登记的负责人(若是 reward_table 就是奖励负责人),并阻断构建。P1(奖励数值超出建议范围——与其说是错,不如说是需要复核的情形)只留下评论和 alert,构建照常通过。若把所有违规都设成阻断构建,人们很快就会学会无视构建阻断。阻断只用于真正必须拦下的地方。

协作工具中实际生成的任务正文,就是 violation_list 中一个条目原样转换后的形态。

[P0] integrity_check_fk 违规 —— 构建已阻断
表:quest_table  |  列:reward_id  |  行:318
值 'rwd_q381' 在 reward_table 中不存在。
最接近的候选:'rwd_q318'(编辑距离 1)
负责人:@奖励_负责人  |  检出:2026-05-18 09:14  |  构建:nightly-0042

从检查结果到抵达人的收件箱,全程无需一次人工介入。检查 → 分类 → 创建任务 → 提及,是一条流水线。之所以能做到,是因为 violation_list 是标准的输出格式。无论由哪个检查 atom 抓到,输出结构都一样,所以一个通知 atom 就能接收并处理所有检查的结果。


10.1.4 减少假违规的运营 —— 留下评审证据

第一次打开检查,必定会冒出假违规。第 1 步的 speaker_id 就是一例。若放任不管,人们就会把违规报告学成"反正大多是假的,不看也罢"——这是检查器信任崩塌最常见的路径。

在项目A中,我们用 human_review_attestation_evidence_mandatory 这一原则来防止它。当判定为假违规并做例外处理时,必须把是谁、在何时、为何这样判断作为证据留存下来。FK 地图文件(fk_map.reviewed.json)的每个例外条目都会附上以下内容。

{
  "source_sheet": "dialogue_table", "source_column": "speaker_id",
  "excluded": true,
  "review": {
    "by": "李旼洙", "at": "2026-05-18",
    "reason": "speaker_id 取 npc_id 或 'player' 字面量。不适合单一 FK 检查。",
    "follow_up": "新增 speaker_type 列后,考虑以分支检查方式重新引入"
  }
}

没有这份证据,等到很久以后"这一列为什么不检查?"的疑问再次浮现时,便没有依据可答。于是又把它加回检查,又看到数百条假违规。评审证据能让同样的争论不再重复。


动手试试 —— 首次搭建 FK 一致性检查

这是为想在自己的数据表中引入 FK 检查的读者准备的最小步骤。

setup. 把数据表文件夹,以及列的规格说明(哪一列是 PK、哪一列是 FK)集中到一处。如果没有规格说明,只凭列名规则(*_id)也能开始。

prompt. 把下面的内容输入给检查器。

从这些数据表中推断 FK 候选。若 xxx_id 列指向 xxx_table 中同名的 PK,则视为 FK。把结果输出为 {source_sheet, source_column, target_sheet, target_column, confidence} JSON,凡是无法用规则断定的,把 confidence 标为 low 并写明理由。

verify. 对输出的 FK 地图,务必由人逐行评审。 自引用(prev_*)、字面量混杂(如 'player')、多态引用(视情况指向不同表的列),自动推断经常出错。用过滤后的地图跑检查,把首次运行得到的违规逐条分类为"真断裂 / 假违规"。假违规做例外处理,但要把理由留存在文件里。

走完这三步,周五晚上一个字符的笔误在周一弄垮构建这种事就不会再发生。检查会在周六凌晨的 nightly 里抓住那个笔误,而在周一上班之前,一条协作工具任务已在等着负责人。

单人精简版. 即便独自工作、没有协作工具,这项检查依然有意义。手写十来行 FK 地图,只跑上面那个 Python 函数,就能抓到断裂的引用。通知用控制台输出或文本文件就够了。关键不在通知渠道,而在于"让机器抓住人眼抓不住的引用错误,并送达到人"这一流程本身。


本章要点

下一章预告

10.2 决策验证 3-layer 传感器 —— 人工评审证据的位置

晚上 11 点,nightly 任务往协作工具里插入了一张卡片。标题是 [integrity] D17 相符度测量未执行 已过 7 天。这条告警的意思是:一条应用了一周的决策,在"是否真的按意图生效"这一点上还没有任何人确认,却一直留在构建里。数据本身没问题。表格格式、外键(FK)、enum 都通过了。但决策没有得到验证。

这道落差正是本章的起点。即便数据完整无缺,决策仍可能出错,而捕捉这种错误的位置,与数据检查处在不同的地方。把这个位置分成三个层,并明确每一层中 AI 辅助到哪里、人在哪里盖章 —— 这就是决策验证 3-layer 传感器。


10.2.1 数据通过了,决策也可能是错的

check cascade 会一次性运行四类检查 —— doc-audit(文档一致性)、data-qa(数据质量)、integrity(完整性)、link(交叉引用断链)。这四项都通过,意味着"数据没问题"。但在没问题的数据之上,仍可能叠加着一条错误的决策。

奖励表格在格式上再完美,只要它的数值会引发通货膨胀;外键(FK)再唯一,只要两个任务在同一时刻占用同一个 NPC;voice 再一致,只要两个角色的关系设定相互矛盾 —— 数据检查全部通过,而决策却全部错误。数据检查看的是"格子是否填好了",决策检查看的是"这个值与其他决策、其他数据、真实用户是否相符"。用会计来打比方,前者是记账凭证的格式检查,后者是财务报表的一致性审计。

因此,决策验证要设置一个与数据验证相互分离的传感器。一旦捆在同一个检查里,就会被压成"通过/失败"一行,失败时到底是数据问题还是决策问题,解读起来就变得含糊。分离开来,责任才清晰。


10.2.2 三个层、三个时点、三类评审人

3-layer 传感器的核心,在于把验证的维度拆成三个。每一层所看的对象、运行的时点、AI 与人的分工都各不相同。

flowchart TD D["决策 D17<br/>defense_factor 1000 → 1500"] --> L1 subgraph L1["Layer 1 · 决策 ↔ 决策"] L1a["AI:scope 重叠检测 + 矛盾一次判定"] L1b["人:只评审'矛盾'判定 + 盖章"] L1a --> L1b end subgraph L2["Layer 2 · 决策 ↔ 数据"] L2a["AI:提取受影响数据 + 模拟 + ±% 规则"] L2b["人:解读意图-测量偏差"] L2a --> L2b end subgraph L3["Layer 3 · 决策 ↔ 用户"] L3a["AI:反馈分类 + 情感分析"] L3b["人:比对 100 条样本 + 最终相符宣告"] L3a --> L3b end L1 --> L2 --> L3 --> CARD CARD["决策卡 D17<br/>3-layer 验证结果 + 评审证据"] CARD --> ATT["human_review_attestation<br/>强制附上评审人·时间·证据"] style L1 fill:#e8f0ff,stroke:#4a72c0 style L2 fill:#e8f7ed,stroke:#3a9a5a style L3 fill:#fff3e0,stroke:#d08a2a style ATT fill:#fde8e8,stroke:#c04a4a

三个层的最后一道章都由人来盖 —— 这道章正是 human_review_attestation_evidence_mandatory atom 强制要求的证据。每一层看的是什么、由谁在哪里盖章,下面依次来看。


10.2.3 Layer 1 —— AI 筛选,人只评审"矛盾"

这一层检查新决策是否与既有决策相冲突。决策对的数量随决策数的平方增长,200 条决策就约有 2 万对。人无法用手全部看完。于是先由 AI 跑一遍初筛过滤。

# decision_conflict_check.py —— Layer 1 传感器
def check_new_decision(new_decision, existing_decisions):
    conflicts = []
    for existing in existing_decisions:
        if has_overlap(new_decision.scope, existing.scope):   # 机械初筛:scope 交集
            verdict = llm_judge(new_decision, existing)        # AI 二次判定:矛盾/补充/无关
            if verdict.label == "矛盾":
                conflicts.append({
                    "with": existing.id,
                    "label": verdict.label,
                    "reason": verdict.reason,
                    "needs_human_review": True,                # 人工评审标志
                })
    return conflicts

has_overlap 是机械式的初筛过滤器。scope 是 atom 的领域(combat·narrative·economy 等),因此交集计算很精确。不重叠的对立即丢弃。2 万对通常会缩减到几十对。只有这几十对交给 AI 来看。

完整实操记录(worked transcript)—— Layer 1 矛盾判定

这里把实际往返的内容完整展示到底。不做摘要,含糊的输出也原样保留。

① 人发出的提示词(全文)

请判断下面两条决策是否相互矛盾。

[决策 A](2026-03-XX)战斗全局冷却统一为 0.5 秒,适用于所有技能
[决策 B](2026-04-XX)治疗类技能不适用全局冷却

判定格式(只用这个格式,禁止多余的话):
- label: 矛盾 | 补充 | 无关
- reason: 一行
- confidence: 0.0~1.0

不要猜测。依据薄弱时请调低 confidence。

② Claude 的原始输出(未加工)

- label: 补充
- reason: 决策 B 是在决策 A 所定的 0.5 秒统一规则上,追加"治疗类"这一明确例外的结构,并未推翻规则,而是将其收窄。
- confidence: 0.74

③ 人的验证 / 否决

判定是"补充",confidence 0.74。按规则,"补充·无关"是自动通过,但只要 confidence 低于 0.8,就要求人至少确认一行。负责评审的团队成员 A 重新读了决策 B 的原文,发现了一点 —— 决策 B 上附有一条但书:"但治疗类技能也有 1.2 秒的自身冷却"。AI 只收到了摘要后的一行,没能把这条但书作为输入拿到。团队成员 A 将 AI 的判定作接受但补强处理,并把"AI 所看到的输入不完整"记录了下来。

④ 重新请求(补强输入后)

[追加但书] 决策 B 上附有一个条件:"治疗技能免除全局冷却,但拥有 1.2 秒的
自身冷却"。请把这条但书纳入,重新判定。
- label: 补充
- reason: 自身冷却 1.2 秒比全局的 0.5 秒更长,因此治疗技能尽管被免除,
  反而拥有更长的间隔。与 A 的意图(防止乱放技能)不冲突,故补充关系得到加强。
- confidence: 0.91

判定依旧是"补充",但依据变得扎实,confidence 从 0.74→0.91。团队成员 A 在此盖了章。关键不在结果,而在过程的记录 —— AI 一次判定、人发现的输入缺漏、补强后的重新请求、最终评审。这四个步骤原样录入决策卡的 Layer 1 证据栏。

这段实操记录的原则只有一条。即便是 AI 的"补充·无关"判定,也不会无条件放行。不是 AI 错了,而是 AI 拿到的信息不完整,而发现这一点的,是了解决策原文的人。

检查的时点有三处。新增决策时即时检查 + 告警,pending atom 晋升时先检查再晋升,nightly 对全部决策对重新检查。


10.2.4 Layer 2 —— AI 几乎全包,人负责解读偏差

这一层测量决策如何反映到数据上、又是否与意图相符。它最容易自动化,也最精确。只要模拟器和数据表已经就位,加上验证规则即可。

以决策 D17(defense_factor 1000→1500)为例,传感器会自动拉取 CombatBalance 表格、自动模拟结果以及受影响的角色数据,把意图(坦克生存 +49%)与测量(模拟 +52%)进行比较。相符判定的规则是定量的。

测量相对意图的偏差 处理 由谁
±10% 以内 相符(自动通过) AI
±10\~25% 告警 · 复查 人来解读
超过 ±25% 违规 · 必须重新审议决策 人来决定

这里人的角色不是"AI 说相符了,那就通过"。解读告警区间与违规区间才是人的工作。D17 的模拟为 +52%,落在 ±10% 之内,属于自动相符;但同一次模拟还吐出了一个附带效果 —— 混合型角色 K_021 在意图之外变强了 +28%。这并非 D17 的直接意图,因此不触发相符判定规则。规则上是通过的,在人眼里却是事故 —— 抓住这个区间,正是 Layer 2 中人存在的理由。

这一层的自动化率约 95%,是三层里最高的。之所以仍剩下 5%,正是因为这种解读。数字通过规则,与那个数字对游戏而言是否正确,是两个不同的问题。


10.2.5 Layer 3 —— AI 分类,人做最终相符宣告

三层里最难的一层。它看的是决策是否按意图作用到了真实用户身上。它以构建发布后 1\~2 周的实测指标(坦克平均生存时间、含坦克的 5:5 PvP 胜率)和自然语言反馈(论坛·社交媒体)作为输入。

自然语言反馈成为验证的输入,是这一层的特点。约 200 条论坛帖、约 1,500 条社交媒体内容,由 AI 分门别类并标注情感。

[AI 反馈分类 —— 坦克相关 1 周收集]
   正面 62%   负面 23%("坦克太强了"占多数)   无关 15%

在这里停下就是陷阱。AI 的情感分类一旦混入韩语和英语,准确度就会下降("坦克变强了哈哈"到底是正面还是讽刺,判定会摇摆)。因此运营规则规定:每季度由人亲自分类 100 条样本,与 AI 的结果做比对。若比对中的误差超过阈值,该季度的分类就不予采信,由人全量重新分类。

最终的相符宣告由人来做。就 D17 而言,实测 +44%(模拟预测 +52%,误差 8% —— 正常范围),反馈以正面为主。AI 整理并提交了"正面为主 + 落在意图范围内"这一输入,而盖章判定为"相符"的是人。自动化率约 70%,人占 30%。唯独这一层,完全自动在根本上不可能。因为用户的含义,机器无法一路判定到底。


10.2.6 人工评审证据不是可选项,而是强制项

如果三层的最后一道章都由人来盖,那么一旦缺少这道章确实盖过的证据,整个系统就会崩塌。有人只是嘴上说评审过、实际却没做,这种情况如何防范?在项目A中,atom human_review_attestation_evidence_mandatory 强制解决这一点。

这个 atom 的规则简单且不容妥协。只要决策卡的任何一层发生了"AI 判定 → 人工评审",就必须在卡片上附上评审人标识、评审时间、评审证据(补强备注、否决理由、样本比对结果三者中至少一项)。证据一旦为空,该卡片就无法晋升为"验证完成"。

证据为空时,integrity_check_clickup_notify atom 便会启动。它一旦检测到一致性失败 —— 这里指"评审章盖了,证据却没有" —— 就立即在协作工具里创建一张卡片。本章开头那张晚上 11 点的卡片,正是这套机制。

这两个 atom 结成一对,构成"对验证的验证"。3-layer 传感器验证决策,attestation atom 验证这道验证是否真由人做过,notify atom 抓出证据缺漏并予以通报。AI 的辅助再广泛,责任的最后一格,也要由留下证据的那个人的名字来填。


10.2.7 决策卡 —— 验证与证据汇集于一页之处

汇集三层结果与评审证据的单位,就是决策卡。一张卡片是一条决策的完结单元,并流向季度复盘的输入。下面是 D17 卡片的结构。

决策卡 D17 变更:defense_factor 1000 → 1500 · 应用 2026-03-XX

Layer 1 · 决策一致 ✓ 无矛盾决策 · 与 7 条相邻决策为补充关系 证据:团队成员 A,2026-03-XX 14:20,输入缺漏补强备注 1 条 AI 一次判定 → 人工评审(confidence 0.74 → 补强后 0.91)

Layer 2 · 数据相符 ✓ 模拟 +52% vs 意图 +49%(相符,±10% 内) ⚠ K_021 混合型意图之外 +28% —— 人工解读:需要后续决策

Layer 3 · 用户相符 ✓ 实测 +44% vs 模拟 +52%(误差 8%,正常) · 反馈以正面为主 证据:季度样本 100 条人工比对完成,AI 分类一致率 88%

整体:✓ 相符(K_021 副作用后续决策已登记到协作工具) attestation 验证:三个层均确认已附评审证据 → 允许卡片晋升

红色那几行是关键。每一层的"证据:"一行若为空,attestation atom 就会阻止卡片晋升,notify atom 则通知协作工具。半年后若有人问"当初为什么把 defense_factor 定为 1500",这一张卡片就能把意图、测量、实测乃至评审人一并答清楚。决策卡运行在与第18部分决策追踪 atom 相同的元数据流之上。


10.2.8 自动化率与引入顺序

三个层的自动化程度各不相同(分别约为 80%·95%·70%,如前几节所见)。三者都是部分自动、最后一道章也都由人来盖,但人的工作量整体上减少了 80% 以上。

引入从 Layer 2 开始。只要模拟和数据表已经就位,加上验证规则即可,1\~2 个月就能见效。接着是 Layer 1(基础设施投入少而效果大,再加 1 个月),最后是 Layer 3(基础设施投入最大,效果也最大,再加 2\~3 个月)。想从一开始就装上 Layer 3 而搁浅,是常见的失败。

关于数字标注:上面的自动化率与下面的效果比例,是基于作者项目运营观察的作者估计(未经验证)。它们不是精密测量值,应当作方向与大致比例来读。相符判定规则的 ±10%/±25% 阈值是实际的运营规则,atom 名称(integrity_check_clickup_notifyhuman_review_attestation_evidence_mandatory)是真实存在的 atom。

引入前后的变化,按方向来概括是这样的。每季度的决策矛盾事故从若干件降到近乎 0 件;决策后 1 周的相符度测量执行率从少数升到大多数;事故发生前的副作用发现率从不足一半升到大多数。最有意义的变化是可追溯性 —— 事隔许久之后仍能回溯决策背景的比例,从少数变成了几乎全部。因为决策卡保存了游戏的决策历史。


10.2.9 常见失败

模式 处方
只运行 Layer 1(仅做矛盾检查) 增设 Layer 2·3 来补齐维度
一开始就引入 Layer 3 从 Layer 2 起,按基础设施由小到大的顺序
不加批判地接受 AI 的"补充·无关"判定 confidence 阈值 + 人工样本评审
只盖评审章却不附证据 attestation atom 阻断晋升
无视证据缺漏的告警 把 notify atom 的协作工具卡片视为未完成
盲信用户反馈的 AI 分类 每季度人工比对 100 条样本

本章要点


动手试试 —— 单人精简版

setup. 把决策日志汇总到一个文件里(决策 id·scope·意图·应用日期)。scope 像 combat·narrative·economy 那样固定为 enum。若没有模拟器,Layer 2 也可以先从"相关数据表的手动比较"开始。

prompt. 每当出现新决策,就把它与既有决策逐对拿去问 AI。请固定格式。

请判断下面两条决策是否相互矛盾。
[决策 A] ...
[决策 B] ...
只输出格式:label(矛盾|补充|无关) / reason 一行 / confidence 0.0~1.0
禁止猜测。依据薄弱则调低 confidence。

verify. 对"矛盾"判定以及 confidence 低于 0.8 的判定,请由人重新读一遍决策原文加以确认。确认后,必须在决策卡上留下评审人姓名·时间·备注(补强/否决/比对三者之一)。证据栏一旦为空,就不要把那张卡片提升为"验证完成" —— 这一行就是 attestation atom 的单人版。哪怕是单人运营,也要为半年后的自己留下证据。

10.3 Alpha Gap Report —— 用自然语言给缺口分类,由人排定优先级

周一早上 9 点 12 分。Alpha 版本刚上线那一周的首次检查 cascade 结束了。check 把四种(doc-audit·data-qa·integrity·link,10.2)一次性跑完并停下时,控制台上打出的数字是这样的。违规候选 47 条。 其中有几条是 P0、该从哪件开始看、该由谁动手,这些在那 47 行里哪儿都没写。

检查器只知道"错了"这个事实。"这是否会阻塞发布、还是下周再看也行"——它无法判断。Alpha 末期真正的瓶颈,不在于检查器不够,而在于人去给检查器吐出的 47 行分类、结果一上午就没了。本章把 LLM 用自然语言对那 47 行分类、再由人接过分类来排优先级的一次完整实操循环,整段照搬过来。


10.3.1 检查结果不是决策

在 10.1 中做了 30 余种验证 atom,在 10.2 中建立了用 3-layer 传感器过滤决策的结构。这两章造出来的是日志。日志不是决策。在日志与决策之间,存在一道过去靠人手工填补的间隙。

检查 cascade 自动 · 47 行日志

间隙 由人手动 分类·定优先级

每周决策 负责人·截止·关卡

← 一上午都消失在这道间隙里 Gap Report = LLM 分类,人定优先级

Alpha 末期这道间隙代价高昂,原因很简单。检查器一小时能跑几十次,但人读这 47 行、分类为"q_142 是死路,阻塞发布;voice_lint 412 等待编剧判定"的工作,每次都得重新做。把这份分类工作交给自然语言模型,就是 Gap Report 的起点。


10.3.2 实操记录 (worked transcript) —— 把 47 行交给 LLM

下面是那个周一早上,把检查 cascade 的原始日志原样粘贴给 Claude 并请求分类的真实会话——完整保留的真实操作过程记录(worked transcript)。不做摘要,原样搬过来。连模型判断错的地方、人否决的地方都照原样留着。这是本章的脊柱。

① 提示词(全文)

以下是 Alpha 版本每周检查 cascade(doc-audit/data-qa/integrity/link)合并吐出的违规候选。请分类,好在周会上用。
把每一项分为 P0(阻塞发布)/P1(评估)/P2(观察),各写一行依据 —— 若是猜测就标"推测"。severity 你不要下定论,只做'建议',确定由我来做。
同一根源的合并到一起,并推荐负责方向(关卡/叙事/数值/UI/数据)。判定不了的就如实归入"无法分类,需人工确认"。

[原始日志]
INTEGRITY  q_142    quest_graph: node 'end_021' has no outgoing edge (dead-end), zone=dungeon_021
INTEGRITY  q_158    quest_graph: reward node references item_id 'it_9920' not in item table
DATA-QA    reward_curve  dungeon_017 reward_exp delta +35.2% vs sibling median (threshold 25%)
DATA-QA    reward_curve  dungeon_017 reward_gold delta +31.0% vs sibling median
DOC-AUDIT  voice_lint    dialogue_id_412 contains banned term (refgame slang leak)
DOC-AUDIT  voice_lint    dialogue_id_511 tone drift: formality score 0.31 (npc 'elder' expects >0.7)
DOC-AUDIT  voice_lint    dialogue_id_512 tone drift: formality score 0.29
DOC-AUDIT  voice_lint    dialogue_id_513 tone drift: formality score 0.33
DATA-QA    loc_overflow  ui_btn_enhance EN string 24 chars > slot 18 (overflow 6)
DATA-QA    loc_overflow  ui_btn_enhance TH string overflow 11
LINK       doc_link      gdd_dungeon_021.md -> [reward_table] target moved (404)
INTEGRITY  k_skill       skill 'k_017_charge' cooldown 0.0 (suspicious, min expected 0.5)
... (共 47 行,以下为同型模式 —— 无省略,已完整附上)

② Claude 原始输出(未做摘要·整理)

已对原始日志进行了分类。severity 是我的"建议",最终确定应由人来做。

## P0 候选(可能阻塞发布 —— 需人工确认)

1. [q_142] quest_graph dead-end (dungeon_021)
   依据:没有出口边的终止节点 → 玩家在推进任务途中被困住。
   死路无法继续,因此为 P0 候选。(判定依据明确)
   推荐负责方向:关卡/叙事

2. [q_158] reward node 引用了不存在的 item_id 'it_9920'
   依据:发放奖励时出现 NULL 引用 → 可能崩溃或漏发奖励。
   P0 候选。但 item_id 也可能尚未录入(仍在处理中),
   究竟是"数据未完成"还是"笔误",需人工确认。
   推荐负责方向:数据

3. [voice_lint dialogue_id_412] 违禁词汇(refgame 术语泄漏)
   依据:若其他游戏的专有术语残留在台词中,存在法律·品牌风险。
   P0 候选。(违禁词汇通常一律拦截)
   推荐负责方向:叙事

## P1 候选(评估后决定)

4. [reward_curve dungeon_017] exp +35.2% / gold +31.0%(两项均超过同级中位数)
   ※ 两行合并为 1 条 —— 同一副本的奖励曲线异常。
   依据:虽超过阈值(25%),但可能是"有意设计的高难度副本奖励"。
   究竟是数值上的有意设计还是失误,仅凭数据无法判定 → P1。
   推荐负责方向:数值

5. [voice_lint 511·512·513] tone drift 3 条(formality 0.29~0.33,elder NPC 期望 >0.7)
   ※ 3 行合并为 1 条 —— 同一 NPC 'elder' 连续台词的语气偏离。
   依据:同一 NPC 连续掉入非敬语口吻的模式 → 可能是同一编剧
   在一次会话中的产物。建议合并评估而非逐条处理。
   推测:也可能是有意的角色变化(如愤怒场景),等待编剧判定。P1。
   推荐负责方向:叙事

6. [loc_overflow ui_btn_enhance] EN 超 6 字 / TH 超 11 字
   ※ 2 行合并为 1 条。同一按钮的多语言溢出。
   依据:截断会损害 UX,但不会崩溃。需要扩展槽位或缩短译文。P1。
   推荐负责方向:UI

## P2 候选(观察)

7. [doc_link gdd_dungeon_021.md -> reward_table 404]
   依据:文档内部链接失效。不影响游戏构建,属于文档一致性问题。P2。
   推荐负责方向:数据(文档所有者)

## 无法分类 —— 需人工确认

8. [k_017_charge cooldown 0.0]
   依据:冷却时间 0.0 究竟是"有意设计的被动型技能"还是"漏填输入",
   不了解技能的设计意图就无法判定。integrity 检查器怀疑其低于 0.5,
   但我不了解这个技能的设计意图。
   → 需由数值负责人亲自确认。

(其余 39 行也归入上述 7 个模式之一 —— 本回复仅展开代表项)

③ 人工验证与否决(原样转录)

从这里开始才是核心。并没有原样采纳 LLM 的分类。这是作者在会议前亲自审阅、动了红笔的部分。

模型把它压缩成 7 个合并组,这本身就很有价值。若由人从头分类这 47 行,一上午就没了。但3 条 P0 候选中有 1 条(412)被人降级,1 条 P1 候选(158)被人升级。 分类有 60% 是对的,而代价高昂的那 30% 由人来纠正。这个比例正好是"LLM 负责加工,决策归人"的分界线。

④ 再次请求 —— 把人工修正后的结果再交回模型

好。你的分类里我改了两处。
- q_158:确定为 P0(it_9920 是已删除的道具,失效引用)
- voice_lint_412:降级为 P1(有意引用的古旧说法,已在违禁词词典中加入例外)
把这两处反映进去,渲染成用于周会的 1 页 Gap Report Markdown。顺序为 摘要→P0→P1→P2→趋势。
趋势数字我来给 —— 上周 P0 5 条、P1 22 条、误报 12%。

模型接收这一输入后,原样输出了下文 §报告格式 中的 1 页内容。人工修正的两行被精确反映,趋势数字直接采用了人给出的值(没有编造)。这一来一回,就是生成一份 Gap Report 的全部过程。


10.3.3 Gap 分类流程 —— 自动与人工的边界

把上面的记录归纳成流程,就是下面这样。关键在于:所有关键分叉点都落在人这一侧。

flowchart TD A[检查 cascade check<br/>doc-audit+data-qa+integrity+link] --> B[原始违规日志 47 行] B --> C{LLM 首轮分类} C -->|severity 建议| D[P0 候选] C -->|severity 建议| E[P1 候选] C -->|severity 建议| F[P2 候选] C -->|无法判定| G[无法分类<br/>需人工确认] C -->|同一根源| H[重复合并] D --> I{人工验证} E --> I F --> I G --> I I -->|采纳| J[severity 确定] I -->|升级/降级| K[人工修正] K --> J J --> L[渲染 Gap Report 1 页] L --> M[周会输入] M --> N{一致性失败?} N -->|是| O[integrity_check_clickup_notify<br/>协作工具即时通知] N -->|否| P[分配负责人·截止] style C fill:#e8f0fe style I fill:#fef3e8 style O fill:#fde8e8

LLM 触碰的方框只有蓝色那一个。所有 severity 都在橙色(人工验证)处确定,而在红色处,一致性失败会立即弹到协作工具。检查·判定·确定全部归人与 atom,模型只负责首次分类这一次。


10.3.4 一致性一旦被破坏,就不等会议

分类流程的末端挂着 integrity_check_clickup_notify atom(10.1)。这个 atom 与生成报告的环节相互独立,在一致性检查失败的那一刻,不等会议就把卡片抛到协作工具上。 如果说 Gap Report 是每周的节奏,那么这个 atom 就是打断该节奏切入进来的中断。

像 q_158(引用了已删除的道具)这样可能破坏构建本身的违规,无法等到周一的会议。cascade 抓到它的那一刻,协作工具上就会自动生成"P0 疑似:q_158 失效引用"并分配给数据负责人。Gap Report 则是把这些中断以周为单位重新汇总、以趋势呈现的背板。两层一起运转,"紧急的即时、全局图景按周"这两个节拍才能对上。


10.3.5 留下人工评审的证据

人验证过 LLM 分类这一事实,若只停留在口头就会蒸发。 因此评审环节挂着 human_review_attestation_evidence_mandatory atom(10.2)—— 人工评审必须有证据。

上面记录的第 ③ 步 —— 把 412 降级、把 158 升级的那个判断 —— 会以评审者 ID·时间戳和"变更项"清单的形式录入报告页脚。下个季度若有人问"为什么 412 上线了",记录会作答:"在 2026-W21 的评审中判定为有意引用的古旧说法,已加入违禁词词典例外"。没有这份记录,LLM 分类就与从未验证过的自动输出无从区分。


10.3.6 报告格式 —— 不超过 1 页

作为再次请求 ④ 的结果,模型渲染出的 1 页是这样的形态。上文记录中的分类原样流入其中。

# Alpha Gap Report — 2026-W21

## 摘要
- 检查 cascade 47 条违规候选 → 归为 7 个合并组
- P0 确定 3 条 / P1 4 条 / P2 1 条 / 无法分类 1 条
- 阻塞发布:q_142(死路)、q_158(失效引用)
- 人工评审变更:voice_412 降级(P0→P1)、q_158 升级(P1→P0)

## P0 —— 立即处理(人工确定)
| ID | 违规 | 领域 | 备注 |
|---|---|---|---|
| q_142 | dungeon_021 死路 | 关卡/叙事 | LLM·人工一致 |
| q_158 | 引用已删除的 it_9920 | 数据 | 由人升级 |

## P1 —— 评估后决定
- reward_curve dungeon_017: exp+35%/gold+31%(数值,等待确认意图)
- voice 511·512·513: elder 语气偏离 3 条合并(叙事,编剧判定)
- voice_412: 引用古旧说法(叙事,已作违禁词例外处理)
- loc_overflow ui_btn_enhance: EN/TH 截断(UI)

## P2 —— 观察
- doc_link 404(文档一致性,不影响构建)

## 无法分类 —— 需人工确认
- k_017_charge cooldown 0.0(数值,设计意图不明)

## 趋势(与上周相比)
- P0: 3 条(上周 5 条)
- P1: 4 组(上周 22 条 —— 因合并分类而改变了计数方式)
- 误报:人工修正 2/8 = 25%(上周 12%,↑ —— 合并后样本变小)

---
评审:李旼洙 / 2026-W21 / 变更 2 条(证据:§评审日志)

请注意:报告并没有掩盖误报率上升到 25% 这件事。样本缩小到 8 个,人工修正了 2 个,算术上就是 25%。报告不会为了好看而编造数字。与上周 12% 简单相比看似恶化,但分类方式改为合并、样本随之不同的背景,已用一行附注说明。不以一周的比率下结论——这条原则在此发挥了作用。


10.3.7 度量 —— 分类工作去了哪里

在作者的项目A中,比较引入 Gap Report 实操分类前后的情况。下列数字中,处理比率·时间取自会议记录与协作工具时间戳的实测值,而检查器的误报率因样本逐周波动,只记录方向

项目 引入前 引入后 依据
47 行首轮分类耗时 人工 \~40 分钟 LLM 1 次 + 人工评估 \~12 分钟 会前工作日志(实测)
检查结果 → 反映到决策 仅部分 大部分 会议记录比对(实测,未统计精确 %)
P0 平均解决时间 3\~5 天 1\~2 天 协作工具卡片创建→完成时间戳(实测)
LLM 分类人工修正率 以 W21 计 2/8 作者估计(未验证,逐周变动)
一致性失败感知延迟 等到会议 即时(atom 通知) clickup_notify 引入效果(方向)

没有把修正率 2/8 当作炫耀来写,是有原因的。那只是一周的样本,某些周模型会看错 5 条。确切的收益是分类工作从 40 分钟降到 12 分钟,而模型分类本身的准确度每周都在波动 —— 变快并不是因为信任模型,而是因为它把内容加工成人能在 12 分钟内验证的形态。


10.3.8 常见失败

模式 对策
每次都由人手动分类 47 行 LLM 首轮分类 → 人工验证,分工协作
直接把 LLM 的 severity 定为最终 severity 只是"建议",确定归人(第 ③ 步)
把同一根源的违规逐条计数 在提示词中明确要求合并
评审事实只停留在口头 用 human_review_attestation atom 强制留证
紧急的一致性失败一直等到会议 用 clickup_notify atom 即时通知
趋势数字由模型编造 趋势由人输入,模型只做渲染(第 ④ 步)
报告变长,会议上没人看 强制 1 页,原始日志另行保存

本章要点


动手试试

setup 1. 把检查 cascade(或你手头的 lint·一致性检查器组合)的输出汇集到一个文件里。 2. 让团队各用一行,就 severity 的 3 个等级(P0 阻塞 / P1 评估 / P2 观察)达成一致。 3. 制作一个把评审者 ID·时间戳写入报告页脚的模板。

prompt

以下是每周检查的输出。请为周会做分类。severity(P0/P1/P2)各附一行依据,只做'建议'(若为猜测就写"推测"),确定由我来做。把同一根源的违规合并,并推荐负责方向(不要具体人名)。判定不了的就如实归入"无法分类"。
[粘贴原始日志]

verify 1. 由人逐条验证全部 P0 候选,记录降级/升级(第 ③ 步)。 2. 挑一项反向追溯,确认模型合并的项是否真的同一根源。 3. 在页脚确认趋势数字是否出自人手(模型有没有擅自填入)。

单人精简版

如果是一个人工作,atom·协作工具·周会都可以没有。把检查器输出以文本形式粘贴,用上面的提示词只拿到分类,再亲自用眼睛验证 P0 候选那 3 条,然后当场处理。把分类交给模型,而只把要验证的项收窄到 P0 —— 单这一点,在单人规模下最能节省时间。报告的 1 页,用一条 Notion 备忘代替也无妨。

11.1 命名规范与技能-美术映射

冲刺(sprint)结束前两天,战斗美术师通过团队即时通讯工具发来一段短视频。新武士职业的三段连招。第一段和第二段都有刀风声,第三段却一点声音都没有。无声。他本人说声音都加好了,音效负责人说文件都交付了。两人都不是在说谎。声音文件确实在仓库里,名为 combo3_swing_final_real.wav。而游戏代码要找的名字是 sfx_K012_combo3_swing.wav。两者没有一个字符重合。

追查这起无声事故,花掉了那天整个下午。这不是某一个片段、某一个声音的问题。只要名字由人随意来起,这类事故每个季度都会重新冒出几十起。本章讲的就是把这种自由变成规则的故事。

本章要回答的问题 - 在 1万个资源的规模下,名字为什么不是自由而是规则 - 把命名规范以 atom(最小知识单元)强制固化、并用 lint 自动校验后,能封住什么 - 一个技能所挂的动画、VFX、音效、图标映射,由 AI 起草、由人采纳的实操记录

给非专业读者的一句话。 1万个资源、fbx 文件名格式,看上去像是游戏行业特有的事情。但你要带走的那一点,并不挑领域——"一旦名字可以随意起,检索、自动化、连接就会一起被锁死。" 规模一大,命名就必须从个人偏好变成规则,而只有成为规则的名字,代码才能自动找到并使用——这个原则,适用于任何处理文档、资产、客户记录的工作。


11.1.1 1万个资源这个规模

笔者所主导的项目A是一款移动优先的 MMORPG。角色动画资源的大致规模如下。玩家职业数量、敌方 NPC 种类是实际运营数值,片段数量与总量估计为笔者估算(未经验证)。

资源 数量
玩家角色职业 6
敌方 NPC 种类 80\~100
单个角色平均片段 100\~150(笔者估算)
片段总量估计 约 10,000\~15,000(笔者估算)

1万个。这相当于 1万个抽屉。站在 1万个没有贴标签的抽屉前找"攻击动作放哪儿了",等于把赌注押在人的记忆力上。而这个赌注一定会输。找不到,结果只有两种。要么工作时间翻倍,要么因为没找到而把同一个动作重新做一遍。后者更糟。因为资源会变得臃肿,而且日后同一个动作会以两个略有差异的版本到处流动。

名字若处于自由地带,被锁死的不只是检索。"由代码凭技能 ID 自动取到动画文件"的自动路由也会一起被锁死。如果无法从名字里读出规则,代码就必须为每一个技能都握着一张手写的映射表,记录该用哪个文件。每进来一个新角色,这张表就得靠人手动加长。


11.1.2 五槽位命名格式 —— 固化为 atom

项目A的动画文件名固定为五个槽位。

<role>_<id>_<category>_<action>_<variant>.fbx

char_K001_idle_default_v1.fbx
char_K001_locomotion_walk_forward.fbx
char_K001_combat_attack_combo1_v2.fbx
char_K001_react_hit_heavy.fbx
enemy_E021_combat_skill_aoe_v1.fbx

五个槽位都遵循既定的 enum。允许自由输入的槽位只有 id 一个,而且这个槽位也被约束为 [A-Z]\d{3} 格式。

槽位 enum 数量 示例
role 4 char, enemy, pet, mount
id 格式固定 K001, E021, P003, M005
category 8 idle, locomotion, combat, react, death, social, cinematic, system
action 每个类别 10\~30 walk, run, attack, skill_aoe, hit_heavy
variant 格式固定 default, v1, v2, _short, _long

这里的关键不是格式本身,而是把格式输入(存放)在哪里。如果把命名规范写在一页 wiki 文档里,那就是一张没人读的标签。笔者把这套规范做成了名为 Char_Anim_Naming_Convention 的单一事实源(single source of truth)atom,让人、lint、LLM 全都只盯着这一个 atom。当格式不再是文档、而是被固化为 atom 的那一刻,命名的性质就从"建议事项"变成了"必须通过的关卡"。

action 槽位的 enum 可能无限膨胀,这是它的弱点。因此要按类别用一部字典来管理标准 action。

combat:
  - attack_basic
  - attack_combo1
  - attack_combo2
  - skill_<skill_id>
  - parry
  - dodge_forward
  - dodge_back
react:
  - hit_light
  - hit_heavy
  - knockback
  - stagger
  - stun
locomotion:
  - idle
  - walk_forward
  - run_forward
  - sprint
  - jump_start
  - jump_loop
  - jump_land

是否把新 action 加入字典,由一套流程来判断。每个季度是否有 3 个以上角色会用到,用现有 action 是否真的表达不出来,类别是否明确,以及最重要的一点——是否可以用 variant 吸收掉。只要能用 variant 处理,就不新增 action。action 字典保持在 100 个以内,是运营健康的信号。不过这并不当作绝对上限。新类型或新职业进来时,一次可能就增加 30\~40 个。要拦住的不是数字,而是无节制的增殖。


11.1.3 lint 拦下提交

把格式输入为 atom 之后,就需要一个自动强制执行该 atom 的校验器。人不可能每次都用肉眼去检查五个槽位。下面就是这个 lint 的骨干。

# anim_naming_lint.py
import re, yaml

NAMING_PATTERN = re.compile(
    r"^(?P<role>char|enemy|pet|mount)_"
    r"(?P<id>[A-Z]\d{3})_"
    r"(?P<category>idle|locomotion|combat|react|death|social|cinematic|system)_"
    r"(?P<action>[a-z_]+?)"
    r"(?:_(?P<variant>v\d+|short|long|light|heavy|left|right|forward|back))?"
    r"\.fbx$"
)

ACTION_DICT = yaml.safe_load(open("char_anim_naming_convention.yaml"))

def check(filename):
    m = NAMING_PATTERN.match(filename)
    if not m:
        return f"命名规则违规(5槽位格式不匹配): {filename}"

    category, action = m.group("category"), m.group("action")
    # skill_<id> 形式是动态 action,因此只检查 prefix
    base = "skill" if action.startswith("skill_") else action
    if base not in ACTION_DICT.get(category, []):
        return f"不在 action enum 内({category}): {action}"

    return None

新的 fbx 一进入仓库,这个检查就会运行。若违规,提交(commit)就会被拦下。这里重要的一点是,不把违规归咎于人。与其责怪造成无声事故的美术师,不如把责任推给工具——"那个名字本就不该被提交进来"。人会犯错,工具去拦住这个错误。这就是命名系统的基本姿态。

命名一旦被强制,作为回报,自动路由就被打通了。

def play_skill_animation(character, skill_id):
    anim_path = f"char_{character.id}_combat_skill_{skill_id}.fbx"
    if not exists(anim_path):
        anim_path = f"char_{character.id}_combat_skill_default.fbx"  # fallback
    play(anim_path)

手写的映射表消失了。即便进来新角色、新技能,只要按规范添加动画文件,代码一行都不用改。回到那起无声事故——如果那个声音文件只能以 sfx_K012_combo3_swing.wav 这个规范名字进来,那么 combo3_swing_final_real.wav 一开始就会在提交阶段被弹回,那天整个下午也就保住了。

variant 槽位是守住 action enum 的安全阀。同一动作的版本(v1、v2)、长度(_short、_long)、强度(_light、_heavy)、方向(_forward、_back)全部由 variant 吸收,从而不必让 action 细分,而是把它们接住。而游戏代码可以根据上下文来选用这个 variant。

def select_variant(base_action, context):
    if context.distance < 3:
        return f"{base_action}_short"
    if context.distance > 10:
        return f"{base_action}_long"
    return base_action

这等于说,命名规范成了代码的分支点。


11.1.4 一个技能十个资源 —— 映射 yaml

如果说命名是 L1,那么连接技能与资源的映射就是 L2。一个技能通常会牵着 2\~3 个动画、1\~3 个 VFX、2\~5 个音效、1 个 UI 图标。平均下来是 10 个资源。200 个技能就是约 2,000 个映射对象。靠人脑管理这个规模是不可能的。因此为每个技能设一份 yaml,把该技能的资源绑定为只从这一份里读取。

---
skill_id: skill_K001_combo1
description: K001 连招1(三段连续)
type: melee_combo
animations:
  - clip: char_K001_combat_attack_combo1_v2.fbx
    role: main
    bone_alignment: spine_03
vfx:
  - asset: vfx_K001_combo1_slash.vfx
    socket: weapon_tip
    timing_ms: [0, 150, 300]
  - asset: vfx_hit_blood_light.vfx
    socket: target
    timing_ms: [150]
sound:
  - asset: sfx_K001_combo1_swing.wav
    volume: 0.8
    timing_ms: 0
  - asset: sfx_hit_metal_light.wav
    volume: 0.6
    timing_ms: 150
ui_icon: icon_skill_K001_combo1.png
ui_tooltip_key: skill_K001_combo1_tooltip
verified: true
---

这一份就是一个技能的全部资源。而这份 yaml 里所有的资源路径都遵循 11.1 的五槽位规范。命名 lint 一垮,这套映射也跟着垮。两层作为一对协同运作。

映射一旦集中到一处,影响追踪就自动被打通。当你想彻底替换某一个 VFX 时,不必再靠人手翻查它会影响到哪些技能。

def find_skills_using(asset):
    affected = []
    for path in glob("skills/*.yaml"):
        skill = yaml.safe_load(open(path))
        for cat in ("vfx", "sound", "animations"):
            for entry in skill.get(cat, []):
                if entry.get("asset") == asset or entry.get("clip") == asset:
                    affected.append(skill["skill_id"])
    return affected

# find_skills_using("vfx_hit_blood_light.vfx")
# → ["skill_K001_combo1", "skill_K005_combo2", "skill_E021_attack_basic", ...]

在资源替换会议上,受影响的技能清单会自动附上。在"改了这个会影响到哪儿?"这个问题被问出来之前,答案就已经摆在会议记录旁边了。

映射也配有 lint。所有资源文件是否真实存在,animations.main 与 ui_icon 是否各有一个,timing_ms 是否落在动画时长之内,以及——所有资源路径是否都通过 11.1 的命名规范。最后一项就是把两层钉在一起的那根钉子。构建时自动运行。


11.1.5 命名·映射的校验流程

把到目前为止的命名 lint 与映射 lint 如何汇成一道关卡,用流程图梳理一下。

flowchart TD A[新资源/技能 commit] --> B{五槽位命名 lint<br/>Char_Anim_Naming atom} B -->|违规| X[拦截 commit<br/>返回违规消息] B -->|通过| C{映射 yaml lint} C -->|资源不存在 / main·icon 缺失| X C -->|引用违反命名规范的资源| X C -->|通过| D[更新资源池统计] D --> E{是否为 LLM 命名·映射候选?} E -->|是| F[人来采纳/驳回<br/>可逆阶段] E -->|否| G[并入构建] F -->|采纳| G F -->|驳回| H[废弃候选<br/>可逆,成本 0] G --> I{外发动作捕捉·语音录制?} I -->|是| J[进入不可逆阶段<br/>无法回退] I -->|否| K[保持可逆资产] classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class A,D data; class B,C,E code; class F,I human; class K pass; class X,H,J fail;

请注意,这条流程的末端有一道可逆/不可逆的边界。yaml 修改、LLM 候选、关键帧,这些全都是可逆的。不满意就废弃即可,成本几乎为 0。然而一旦进入动作捕捉拍摄、配音演员录音、标志性嗓音选角,就变成不可逆了。这牵涉到演员与录音棚的预约、录音间、合同、市场认知。因此,所有命名、映射、人格设定(persona)的决策,都必须在不可逆阶段之前——也就是在 yaml、LLM 候选、关键帧这一可逆区域之内完成。


11.1.6 实操记录 —— 把新技能映射初稿交给 AI

到这里为止讲的是系统,现在原封不动地展示一段真实会话,看 AI 究竟在哪里介入——这就是实操记录(worked transcript,完整保留下来的真实操作过程记录)。这一幕,是让 LLM 为新火焰技能 skill_K012_flame_burst 起草资源映射初稿。

提示词(全文):

以下是我们项目的角色动画命名规范(atom)、现有资源池统计,
以及一份相似技能的 yaml。

[命名规范]
<role>_<id>_<category>_<action>_<variant>.fbx
role: char|enemy|pet|mount / category: idle|locomotion|combat|react|death|social|cinematic|system

[资源池 TOP 使用资源]
sfx_hit_metal_light.wav (47 个技能), vfx_dust_kickup.vfx (38 个技能),
vfx_fire_burst_med.vfx (22 个技能), sfx_fire_whoosh.wav (19 个技能)

[相似技能 yaml]
skill_id: skill_K005_fire_slash / type: melee_fire
animations: [char_K005_combat_attack_fire_v1.fbx]
vfx: [vfx_K005_fire_slash.vfx, vfx_fire_burst_med.vfx]
sound: [sfx_K005_fire_swing.wav, sfx_fire_whoosh.wav]

新技能: skill_K012_flame_burst (K012 武士, 范围火焰爆发, 单段)
请编写这个技能的映射 yaml 初稿。尽量复用现有资源池,
对需要新制作的资源用 [NEW] 标签标注。

Claude 原始输出(未作摘要·整理):

skill_id: skill_K012_flame_burst
description: K012 武士 范围火焰爆发 (单段)
type: aoe_fire
animations:
  - clip: char_K012_combat_skill_flame_burst_v1.fbx   # [NEW] 需要新建动作
    role: main
    bone_alignment: spine_03
vfx:
  - asset: vfx_fire_burst_med.vfx     # 复用 (资源池 22 个技能)
    socket: ground
    timing_ms: [200]
  - asset: vfx_K012_flame_ring.vfx    # [NEW] 表现范围用的新建资源
    socket: ground
    timing_ms: [200, 350]
sound:
  - asset: sfx_fire_whoosh.wav        # 复用 (资源池 19 个技能)
    volume: 0.8
    timing_ms: 0
  - asset: sfx_explosion_low.wav      # [NEW] 推测为新建爆炸音
    volume: 0.9
    timing_ms: 200
ui_icon: icon_skill_K012_flame_burst.png   # [NEW]
ui_tooltip_key: skill_K012_flame_burst_tooltip
verified: false

备注:在 char_K012_combat_skill_flame_burst_v1.fbx 中,action 是 skill_flame_burst,但按命名规范,skill action 应为 skill_<skill_id> 格式,是否正确需确认。规范里没有明确说明该用 skill_K012 还是 skill_flame_burst,因此这里做了推测。

人工验证/驳回:

最后那条备注精准地点出了弱点。并不是 LLM 不懂,而是规范 atom 里那个槽位的格式写得含糊。实际上,项目A 的规范用的是 skill_<skill_id> 格式。因此正确的文件名是 char_K012_combat_skill_K012_v1.fbx。LLM 生成的 skill_flame_burst 被驳回。而 sfx_explosion_low.wav 虽被标为 [NEW],但重新查看资源池后发现,sfx_impact_heavy.wav 已经在 47 个技能里被当作爆炸音使用。因为给 LLM 的资源池统计只到 TOP 4,所以漏掉了。于是把新制作改为复用。

再次请求:

按以下两点修改后重新编写。
1. skill action 的格式是 skill_<skill_id>。K012 技能对应的 action 就是 skill_K012。
2. 资源池里有 sfx_impact_heavy.wav (47 个技能, 爆炸/冲击音)。
   请复用它,而不是新制作 sfx_explosion_low.wav。
完整的资源池统计如下。[附全部 38 种]

在这一轮循环里,LLM 做的是"像模像样的初稿",人做的是"发现规范的含糊之处、发现资源池遗漏、做出复用决策"。LLM 有一种倾向,太容易把资源候选标成 [NEW],所以复用判断始终握在人手里。不过,从空白页面从头写 yaml,和拿到一份可采纳/可驳回的初稿再修改,两者的工作负担并不一样。


11.1.7 从保守到进步 —— 人只负责采纳的阶段

上面这段记录,正是进步式应用的一个场景。命名·映射的运营分为两个阶段。

在保守阶段,由人来赋予命名、编排映射,自动化只负责校验(lint)与追踪(find_skills_using)。目前大多数 MMORPG 的角色·资源运营都停在这里。在进步阶段,命名初稿、映射初稿,乃至 NPC 人格设定的生成,都由 LLM 给出候选,留在人手里的决策收窄为"采纳哪个候选"这一件。

进步阶段要站稳脚跟,需要具备三样东西。第一是命名规范的 lint 引擎。LLM 给出的命名候选,也要和人写的一样通过五槽位 lint,才会被采纳。上面记录中 LLM 的 skill_flame_burst 被驳回,靠的就是这道关卡。第二是 NPC 人格设定的自动生成器。只要把角色 yaml 拆解为 voice_profile·anim_set·skill_set 三条轴,LLM 就能接收"五十多岁武士、沉稳、低嗓音"这样的描述,分别为三条轴各自给出候选。为 100 个 NPC 从零编排三条轴,和在每个人格的几个候选里挑选,负担并不相同。第三是映射候选的生成器。它是 find_skills_using 的反方向——把"适合这个新技能的现有资源"检索与资源池统计绑在一起,按槽位给出复用候选。这是既降低新制作成本、又提高复用率的双向效果。

三个要素都跑在同一套基础设施(yaml·lint·资源池统计)之上。只有当命名规范与映射 yaml 对齐为单一事实源时它们才运转,一旦对齐崩坏,连给 LLM 的输入本身都不存在了。

值得一提的是,这三个要素在 2010 年代理论上也是可行的。卡住的有三处。无法用自然语言理解一个动作是什么,因而给不出五槽位候选;把 voice·anim·skill 分开再组合,当时属于人的直觉领域;想用文字描述去找"感觉相似的 VFX"也很困难。2023 年以后,随着 LLM 的发展,这三处都进入了可以辅助的范围。原本只停留在纸面上的进步式角色资源化愿景,相当一部分已经移到了可以实务应用的阶段。


11.1.8 度量 —— 引入前后

这是项目A 命名·映射引入前后的对比。检索时间与新人上手周期是笔者实际体感·记录到的走向,比例类项目是季度复盘中汇总的实测值。需要说明,部分绝对数值为笔者估算(未经验证)。

项目 引入前 引入后
动作检索时间(动画师) 5\~10 分钟 30 秒
重复制作比例 12\~15% 1\~2%
新角色路由代码改动 50\~100 行 0 行
新技能资源缺失事故 每季度 5\~8 起 0\~1 起
未使用资源积压(库中占比) 约 30% 约 8%
新动画师上手 2 周 3 天

最后一项最不起眼,却是效果最大的。一个命名规范 atom,本身就成了上手指南。对新动画师只要一句"名字就按这五个槽位来起,lint 拦你就听 lint 的",第一天就能开始干活。


11.1.9 常见的失败

模式 处方
命名规范只放在 wiki 文档里 固化为单一 atom + lint 强制
action enum 无限增殖 字典 + 新增流程
未经命名校验就提交 用自动 lint 拦截提交
在代码里硬编码映射表 基于命名的自动路由
不用 variant 而让 action 细分 用 variant 槽位吸收
资源映射分散在代码·表格·文档中 统一到一份 yaml 文件
未经校验就采纳 LLM 映射候选 命名 lint + 人工复用判断
把命名违规归为人的责任 加强 lint,把责任交给工具

本章要点

动手试试

setup —— 把动画文件名定义为 <role>_<id>_<category>_<action>_<variant>.fbx 五个槽位,并把各类别的 action 字典汇集到一份 yaml 文件里。把这份 yaml 声明为团队的单一事实源。

prompt —— 给 LLM "[命名规范 yaml] + [资源池统计] + [相似技能 yaml 1 份]",请它给出新技能的映射 yaml 初稿。明确要求它区分复用资源与新制作资源([NEW] 标签)。

verify —— 让 LLM 输出的所有资源路径都通过命名 lint(即上文的 anim_naming_lint.py)。通不过就驳回。通过的候选中带 [NEW] 标签的,由人重新翻查资源池,判断是否可以复用。

单人精简版

下一章预告

11.2 宠物·坐骑系统 —— 从1种模板到50种实例

策划会议一开始,宠物清单就摆上了台面。狼系十二种、猫系八种、鸟系五种。没有人说"那我们一只一只地做吧"。因为与角色不同,宠物从一开始就以"要量产50种"为前提。问题不是"如何把一种做好",而是从"让多少种共享同一副已做好的骨骼"开始。

角色的每一种对用户来说都是独一无二的存在,因此要一种一种地精心打磨。而宠物·坐骑大多是"在同一副骨骼上只改颜色和能力的变体",所以从设计之初就要备好命名规范·模板·lint,铺设量产管线。若把一种精心做好之后,任由它被复制成十二份,那么只是颜色不同的十二只狼就会各自塞进相同的动画片段,文件夹膨胀到4GB。那不是量产,而是没有量产的结果。核心不在于"做得多好",而在于"尽量少做、尽量多共享"。

因此本章会完整走一遍这样一个流程:用 yaml 定义一种狼系宠物模板,让 AI 量产继承其骨骼的实例,再用 lint 验证,并测量有百分之多少被废弃。

11.2.1 模板与实例的分离

三者的资源结构相似,但在用户认知中的比重不同。角色是用户与之共度100%游戏时间的自己。宠物是陪在身边的同伴,占50\~70%的时间;坐骑则是只在移动时才拿出来的工具,停留在10\~20%。认知比重越低,用户越少留意细节。把倾注在角色上的心力同样倾注到坐骑上,就像用同一份预算去打理每天都坐的书桌和偶尔才展开的折叠椅。

因此宠物·坐骑采用"模板-实例"结构来运作。先做出一种承载骨骼·动作·基础能力的模板,再在其上叠加只改颜色·图标·细微能力的实例。实例共享模板所拥有资源的90%,所以实际新做的只有剩下的10%。把这种分离画成图,如下所示。

模板(1种) pet_template_canine 骨骼 skeleton 共享动画4种 共享能力2种 基础 BT 资源90%(只制作一次)

pet_P003(灰狼) override: skin=gray, icon, 能力1种

pet_P004(黑狼) override: skin=black, icon, 能力1种

pet_P005(雪狼)……直到 P012 override: skin=snow, icon, 能力1种 —— 仅10%资源为新增

左侧的模板整块只做一次,右侧的各个实例只需替换颜色、图标和一行能力即可。前面说的"4GB 文件夹",正是漏掉这层分离、90%的资源被复制十二次时出现的景象。

11.2.2 命名与资源样式 —— 从角色减去一格

宠物·坐骑的命名规范,是在11.1的角色命名基础上减去一个槽位的形式。角色使用 char_<id>_<category>_<action>_<variant> 5个槽位,而宠物·坐骑省略 variant,采用4个槽位。若需要 variant,则合并进 action。

pet_<id>_<category>_<action>.fbx
mount_<id>_<category>_<action>.fbx

例:
pet_P003_idle_default.fbx
pet_P003_combat_bite.fbx
mount_M005_locomotion_run.fbx

资源映射 yaml 也从角色样式中减去 vfx·sound 槽位,做得更轻。若实例整块保留这些槽位,就会变成满是空格的样式,让 lint 每次都发出无谓的警告。

现在进入正题。我们来定义一种狼系模板,并由此量产实例。

11.2.3 实操记录:1种模板 → 量产实例 → lint → 废弃率

第1步 —— 手动编写模板 yaml

在让 AI 量产之前,先由人手动敲定一种模板。这一种会成为数十种实例的质量基准,所以不自动化。狼系(canine)模板是这样定的。

# pet_template_canine.yaml
template_id: pet_template_canine
skeleton: skel_quadruped_medium      # 四足中型公用骨骼
shared_animations:
  - clip: pet_template_canine_idle_default.fbx
  - clip: pet_template_canine_locomotion_walk.fbx
  - clip: pet_template_canine_locomotion_run.fbx
  - clip: pet_template_canine_combat_bite.fbx
shared_abilities:
  - id: pet_template_canine_passive_speed
    description: 同伴移动速度 +3%
  - id: pet_template_canine_active_bite
    description: 单体目标撕咬,冷却时间 12s
bt_ref: bt_pet_canine_default        # 跟随 + 战斗辅助的基础 BT
instance_overridable:                # 实例可以修改的字段白名单
  - visual_skin
  - ui_icon
  - ui_tooltip_key
  - extra_ability                    # 每个实例最多可追加1种能力

这里的 instance_overridable 是核心机制。它用白名单钉死实例可以改动的字段。AI 在量产途中若擅自改动骨骼或共享动画,就等于碰了不在这份清单里的字段,于是 lint 会揪出来。先定义"可以改的东西",正是量产的安全带。

第2步 —— 请 AI 量产实例(提示词全文)

以下是量产10种实例所用的提示词全文。不作摘要,原样刊出。

[提示词]
你是协助编写宠物数据的助手。请以下面的模板为基准,
生成10种狼系宠物实例的 yaml。

[模板] pet_template_canine.yaml
(粘贴上面的 yaml 全文)

[规则]
1. 每个实例都必须明确写出 template: pet_template_canine。
2. overrides 中只放入 instance_overridable 白名单里的字段。
   白名单里没有的字段(skeleton、shared_animations 等)绝不改动。
3. visual_skin 必须是狼的自然变体(颜色·花纹·体型)。
4. extra_ability 每个实例最多1种。passive 或 active 二选一。
   不得与既有的 shared_abilities 效果重复。
5. ui_icon、ui_tooltip_key 遵循与 pet_id 匹配的命名规则。
6. pet_id 赋值为 pet_P003 ~ pet_P012。

输出仅限10个 yaml 块。不要附加说明文字。

规则2与第1步的白名单相互配对。规则4的"不得重复"是防止 AI 偷懒复制能力的约束。若不加这类约束,后文会看到,AI 会收敛到最安全的选项(照搬既有能力)。

第3步 —— Claude 的原始输出(不作摘要·改写)

从 AI 吐出的输出中,把成功·失败·模棱两可混在一起的原样摘录出来。不作整洁修饰。

# 输出 1 —— 正常
pet_id: pet_P003
template: pet_template_canine
overrides:
  visual_skin: wolf_gray
  ui_icon: icon_pet_P003.png
  ui_tooltip_key: pet_P003_tooltip
  extra_ability:
    id: pet_P003_active_howl
    description: 5秒内敌人移动速度 -20%,冷却时间 30s

# 输出 2 —— 正常
pet_id: pet_P004
template: pet_template_canine
overrides:
  visual_skin: wolf_black
  ui_icon: icon_pet_P004.png
  ui_tooltip_key: pet_P004_tooltip
  extra_ability:
    id: pet_P004_passive_crit
    description: 同伴暴击率 +2%

# 输出 5 —— 模棱两可(能力与共享能力实质重复)
pet_id: pet_P007
template: pet_template_canine
overrides:
  visual_skin: wolf_brown
  ui_icon: icon_pet_P007.png
  ui_tooltip_key: pet_P007_tooltip
  extra_ability:
    id: pet_P007_passive_speed_boost
    description: 同伴移动速度 +3%   # ← 与 shared 的 passive_speed 效果相同

# 输出 8 —— 失败(侵入白名单之外的字段)
pet_id: pet_P010
template: pet_template_canine
overrides:
  visual_skin: wolf_white
  ui_icon: icon_pet_P010.png
  shared_animations:                 # ← 不在 overridable 白名单中
    - clip: pet_P010_combat_pounce.fbx
  extra_ability:
    id: pet_P010_active_pounce
    description: 扑跃攻击,冷却时间 20s

# 输出 9 —— 失败(违反命名规则)
pet_id: P011                          # ← 缺少 'pet_' 前缀
template: pet_template_canine
overrides:
  visual_skin: wolf_silver
  ui_icon: pet11_icon.png            # ← 违反 icon_pet_P011.png 规则
  ui_tooltip_key: pet_P011_tooltip
  extra_ability:
    id: pet_P011_passive_dodge
    description: 同伴闪避 +1%

10种中,正常的有 P003·P004·P005·P006·P008·P012 六种,因能力重复而模棱两可的有 P007 一种,因侵入白名单·违反命名而失败的有 P009·P010·P011 三种。尽管加了规则4,AI 还是在 P007 抄来了共享能力(最安全的选择);尽管加了规则2,AI 还是在 P010 碰了骨骼动画。即便明示约束,量产物中仍有一定比例会漏出,这是现实。所以需要下一步。

第4步 —— lint 验证

不用人眼一一去看10种,而是跑 lint。lint 规则直接取自第1步模板的白名单和11.1的命名规范。检查项有4项。

flowchart TD A[实例 yaml 10种] --> B{template 字段<br/>存在 &amp; 有效?} B -->|缺失/拼写错误| F[REJECT: 模板引用错误] B -->|OK| C{overrides 字段是否<br/>在白名单内?} C -->|侵入外部字段| F2[REJECT: 违反白名单] C -->|OK| D{pet_id·ui_icon<br/>命名规则通过?} D -->|违反| F3[REJECT: 违反命名规则] D -->|OK| E{extra_ability 是否<br/>与 shared 重复?} E -->|重复| W[WARN: 能力重复待复查] E -->|唯一| P[PASS] classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class A data; class B,C,D,E code; class P pass; class F,F2,F3,W fail;

每个实例若通过4道关卡则为 PASS,中途卡住则落为 REJECT 或 WARN。把实际验证结果整理成表如下。

pet_id template 白名单 命名 能力重复 判定
pet_P003 OK OK OK 唯一 PASS
pet_P004 OK OK OK 唯一 PASS
pet_P005 OK OK OK 唯一 PASS
pet_P006 OK OK OK 唯一 PASS
pet_P007 OK OK OK 重复 WARN
pet_P008 OK OK OK 唯一 PASS
pet_P009 OK OK 违反 REJECT
pet_P010 OK 侵入 REJECT
P011 OK OK 违反 REJECT
pet_P012 OK OK OK 唯一 PASS

PASS 6、WARN 1、REJECT 3。WARN 只要改一行能力就能救活(P007),REJECT 的3种则废弃。

第5步 —— 测量废弃率与再次请求

这一轮的废弃率是 REJECT 3 / 总共 10 = 30%。若把 WARN 也归为"需要修补的",则修补率为40%。这个数字是量产管线的健康指标。废弃率若为30%,就意味着要确保50种宠物,需要生成约72种(50 / 0.7 ≈ 71.4)。生成很便宜,所以这种程度的超量是可以承受的。不过,废弃率若历经多轮仍不下降,那就是提示词约束不足的信号。

因此把废弃缘由反馈进提示词。把 REJECT 3种的缘由(命名缺失、侵入白名单、图标规则违反)汇总起来,在再次请求中逐条各加一行。

[再次请求追加规则]
7. pet_id 必须以 'pet_' 前缀开头。(上一批中 P011 缺失)
8. ui_icon 无一例外为 icon_<pet_id>.png 格式。(禁止 pet11_icon.png 之类的变形)
9. overrides 中绝不放入 shared_animations / skeleton / bt_ref。
   若想改变动作,只能用 extra_ability 来表达。(P010 案例)

加上这三行后,再跑下一批10种,REJECT 从 3 减到 1。废弃率30% → 10%。把废弃缘由升格为规则的这种反馈,正是让量产质量每一轮都往上走的机制。人不必每次都评审50种,只需做一件事:把废弃缘由挪成一行规则。

11.2.4 坐骑 —— 连骨骼都共享,几乎只有数据

坐骑比宠物更简单一级。既没有技能,也没有 BT(BehaviorTree,行为树),只有移动参数、能否战斗之类的数据。所以坐骑实例实质上就是表格的一行。

# 基于 mount_template_equine.yaml 的实例
mount_id: mount_M005
template: mount_template_equine
overrides:
  visual_skin: horse_white
  movement:
    run_speed: 7.0
    sprint_speed: 12.0
  combat:
    allow_combat: false       # 战斗中不可使用
    dismount_on_damage: true
  ui_icon: icon_mount_M005.png

坐骑量产的 lint 更短。除命名·模板引用·白名单外,只需检查"movement 参数是否在允许范围内"(例如 sprint_speed 是否大于 walk_speed,是否未超过上限)即可。这是沿用宠物那套管线、只减少关卡数量的形式。给坐骑加战斗功能要慎重。一旦把 allow_combat 打开为 true,游戏复杂度就会翻倍,还得重新做与宠物·角色系统的冲突验证。

11.2.5 测量 —— 简化不会削减体验

把宠物·坐骑完整套用角色模式的情形,与用模板-实例加以简化的情形,在作者的项目A中做了对比。下面的数字里,时间·资源数为作者估算(未经验证),废弃率和资源共享率则是遵循实测方向的比例。

项目 完整套用 模板-实例
宠物1种资源工作时间 1\~2周(作者估算) 3\~5天(作者估算)
宠物资源库资源数 约 2,000(作者估算) 约 600(节省70%)
每种实例的新增资源比例 100% 约10%
首批量产废弃率 30%(实测方向)
反馈后废弃率 10%(实测方向)
用户体感(宠物多样性) 基准 几乎相同

样本·测量。 上表是作者环境中1个项目(项目A)对宠物1条线的观察(n=1条线)。"节省70%"·"约10%"并非独立测量,而是从同一行的估算资源数(约 2,000 → 约 600)得出的算术比例,所以正如前面的绝对值是估算,这个百分比也应当当作估算来读。废弃率30%·10%来自首批\~反馈的单一量产循环的实测方向,并非重复测量的样本。请勿引用为贵团队的节省依据,而应以同样的方式在自己的线上亲自测量。

最后一行就是本章的整体结论。即便共享90%的资源、边测量废弃率边量产,用户所感受到的宠物多样性与完整制作几乎没有差别。前面说的4GB 文件夹,正是把资源复制十二份到用户最终也分辨不出的细节上时所付出的代价。以量产为前提铺开后,减少的是运营成本,而不是体验。

11.2.6 运营中的陷阱

陷阱 处方
把角色系统原封不动移植到宠物·坐骑 减去 variant 槽位·vfx·sound 的4槽位变体
把同骨骼宠物复制为独立资源 1种模板 + 实例,用白名单强制共享
未经评审就提交 AI 量产物 lint 4道关卡 + 废弃率测量
废弃率每一轮都不下降 把废弃缘由升格为提示词规则(反馈)
给宠物赋予角色级技能 每个实例 extra_ability 上限1种
给坐骑赋予战斗功能 allow_combat 要慎重,做好复杂度 ×2 的准备

11.2.7 AI 的位置与人的位置

宠物·坐骑对用户体验的影响较小,所以 AI 的自由度比角色大。把概念匹配到合适的模板、提出能力候选、量产实例 yaml,这些 AI 都能快速完成。只是若因自由度大就省掉验证,上面看到的30%废弃物就会原样混进构建里。人的位置有两处。第一,手动敲定一种模板,把质量基准固定下来。第二,读懂什么被筛掉了、为什么,从而打磨约束,让下一批更少漏出。量由 AI 来填,基准线及其校正由人来握——正是这种分工,让这套系统运转起来。


本章要点

下一章预告


动手试试

setup 1. 为一个宠物系列(例如狼)确定公用骨骼·共享动画4种·共享能力2种,保存为 pet_template_<系列>.yaml。 2. 在模板中明确写出 instance_overridable 白名单(可以修改的字段)。 3. 用脚本准备好 lint 4道关卡(模板引用 / 白名单 / 命名规则 / 能力重复)。

prompt 4. 粘上模板 yaml 全文 + 量产规则(禁止白名单之外的字段、禁止能力重复、命名规则),请求10种实例。 5. 把输出格式固定为"仅 yaml 块,禁止说明"。

verify 6. 跑 lint,把结果分类为 PASS / WARN / REJECT 并计算废弃率。 7. 汇总 REJECT 缘由,在提示词中逐条各加一行规则,再跑下一批。确认废弃率是否下降。

11.2.8 单人精简版

如果是一个人做的游戏,没有 lint 脚本也行。手写一张某个宠物系列的模板 yaml,然后对 AI 说:"在这个模板上只改颜色·图标·能力,做5种实例,骨骼和共享动画绝对不要碰。"把收到的5种用眼睛扫一遍,只把碰了骨骼的·违反命名规则的丢掉。把丢弃的理由在下一次请求里加上一行。只要有模板 1.1 和"把丢弃理由反馈回去",没有工具,本章的核心也照样运转。

12.1 AI 美术资产管线 —— 在可逆阶段量产,在不可逆关卡前停下

主要读者:与美术团队协作的游戏策划·美术总监(中等规模(10\~50 人)团队) 面向个人/业余读者的精简版:§12.1.8「一个人的话,做到这些就够」

我记得把 AI 生成的 100 张概念图贴在会议室墙上的那一天。30 秒内打印出的 100 张里,美术总监选中的只有 3 张,其余 97 张当场被丢弃。有人把这叫作“97% 的浪费”。可如果是手绘,画师为了抵达那 3 张,大概要花上两周。什么才是浪费,在这里被颠倒了过来。

本章讨论的,是把这种颠倒转化为运营的方法。核心只有一句话。AI 美术在可逆阶段(概念·纹理探索)可以尽情量产,而在不可逆阶段(最终渲染·动作捕捉·并入构建)之前,设一道由人把守的关卡。 在可以丢弃的地方就丢掉 99 张,在无法回退的地方则一张也不随意放行。美术工具的用法在别的书里已足够多,本章只专注于把这些工具安全地嵌入策划管线的那个位置


12.1.1 美术管线上存在一条可回退的界线

美术资产从概念走到游戏内,一共 7 个阶段。把作者项目(以下称“项目A”)的角色资产流程原样搬过来就是这样。重要的不是阶段数量,而是穿过其正中央的可逆/不可逆分界线

flowchart TB subgraph 가역["可逆 —— 丢弃也零成本(积极用 AI 量产)"] direction LR C1["1 概念<br/>2D 插画"] --> C2["2 模型设定图<br/>正·侧·后视"] C2 --> C3["3 3D 建模"] C3 --> C4["4 纹理<br/>材质量产"] end 가역 -.->|"不可逆关卡<br/>必须通过人工评审"| 비가역 subgraph 비가역["不可逆 —— 回退需返工·重录·重新发布"] direction LR I5["5 骨骼绑定·蒙皮"] --> I6["6 动画<br/>动作捕捉"] I6 --> I7["7 游戏内整合<br/>最终渲染·上线曝光"] end classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; class C1,C2,C3,C4 ai;

左侧四个阶段(概念\~纹理)是可逆的。抽出 100 张概念图丢掉 97 张,损失的只是 token 成本;纹理重新生成五次,覆盖掉文件就结束了。所以这一段是 AI 量产能带来最大 ROI(Return on Investment,投资回报)的位置。量产工具以自托管的 Stable Diffusion(SDXL)/ComfyUI 为主轴。原因是 IP 保护 —— 不把资产上传到外部的封闭服务,而在本地运行,并用以角色微调过的 LoRA 与 ControlNet,在每次反复生成时控制同一人物的一致性。封闭式工具(Midjourney 等)只在快速铺开初期情绪板时有限使用,而需要一致性·反复控制的正式量产则交回 SD/ComfyUI。

右侧三个阶段(骨骼绑定之后)是不可逆的。动作捕捉会绑定录制棚·演员的档期,最终渲染并入构建、上线曝光后,就会伴随玩家的记忆与社区的反应。一旦跨过去,回退的成本会大于制作的成本。所以在分界线上立起一道由人把守的关卡。无论 AI 在可逆区间量产多少,能跨入不可逆的资产,只有通过人工评审的那些。

这一张图就是本章的骨架。“AI 在美术上用到什么程度”这个问题,其实是“这项工作在分界线的哪一侧”的问题。


12.1.2 [实操记录] 一个概念,从量产到废弃·再请求的全过程

这里把可逆区间的第一步——概念量产,完整展示一个周期。若只是抽象地写“AI 生成概念”,就无法知道真正产出了什么、又废弃了什么。下面是对项目A中量产学者公会资深 NPC 概念的一次会话的忠实再现——一份实操记录(worked transcript,即完整保留的真实操作过程记录)。提示词可以直接复制使用,输出则由真实会话重构而来。

第 1 步 —— 输入:先明确策划意图

这里有一个最常出错的地方。就是把提示词以“视觉描述”开头。公司的反馈 atom image_prompt_design_intent_first 所固化的原则恰恰相反 —— 图像提示词也是设计意图优先。不是罗列外形形容词,而是把这个角色在游戏中承担什么功能·叙事放在前面。

# concept_brief_scholar_senior.yaml —— 概念量产输入
asset_id: npc_scholar_senior_01
role: 学者公会资深成员 —— 最先观测到封印减弱的人物
function: 主线任务发布 NPC(玩家必须信任的信息源)
narrative_seed:
  - 30 年间在钟塔记录封印脉络的人
  - 把情感藏在数字背后(scholarly_strict 基调)
style_anchor: semi-realistic, painted, 东亚奇幻   # 在 L0 愿景中固定
forbidden: anime 风格 · 现代服饰 · 普通奇幻法师长袍

functionnarrative_seed 在外形之前。只有当输入本身握着“为什么这个角色必须长成这样”,才能在量产结果中判断“为什么这个更好”。

第 2 步 —— 提示词:量产,但强制格式与禁忌

基于附上的 concept_brief,生成 6 个角色概念方向提案。
这是用于探索的量产 —— 不是最终稿,而是供美术总监挑选的候选。

规则:
1) 把 function 与 narrative_seed 翻译为视觉。禁止单纯的美型。
   (例:“把情感藏在数字背后” → 用表情·道具·姿态如何体现)
2) 不要偏离 style_anchor。forbidden 项绝对禁止。
3) 6 个方案彼此须有足够差异。细微变体的 6 张没有探索价值。
4) 为每个方案附上一行意图依据(为什么这个方向契合 function)。
5) 你怀疑‘可能与 style_anchor 冲突’的方案,单独标注出来。

输出格式:(方案编号 | 核心视觉 | 意图依据 | 疑似冲突)。除此之外不要任何说明。

在这段提示词里,视觉描述所占的篇幅几乎没有。大部分都用在锁定输出格式、钉死禁忌上。因为一旦给 AI 自由,它就会回归到训练数据的平均值——普通奇幻法师。意图翻译义务、固定 style_anchor、强制 6 个方案的多样性,以及“若有冲突就上报”这条退路,是阻止回归的四道门闩。

第 3 步 —— Claude 的输出(重构,节选)

| 方案 | 核心视觉                             | 意图依据                          | 冲突 |
|------|--------------------------------------|-----------------------------------|------|
| 1    | 黄铜放大镜永久佩戴,墨渍手指          | 观测即身份 —— 工具被身体化        |      |
| 2    | 面无表情的老学者,手持带刻度盘的卷轴   | 把情感藏在数字背后 = 无表情+记录物 |      |
| 3    | 钟塔背景,身后冷却中的封印脉络之光      | 职务空间说明人物                  |      |
| 4    | 华丽的星纹刺绣长袍,手持魔力球体        | 强调威严                          | ★    |
| 5    | 一只眼为义眼,毕生观测的痕迹           | 30 年职务的身体代价               |      |
| 6    | 年轻端正的学者,强调美型               | 好感度优先                        | ★    |

输出中最有价值的不是表格,而是标了 ★ 的两行。AI 自己在 4 号方案(华丽法师长袍、魔力球体)与 6 号方案(美型优先)上上报了“可能与 style_anchor·forbidden 冲突”。若没有第 5 条规则那条退路,这两个方案就会以与其余四个方案一模一样的面孔混进表里。让 AI 自己举手标出可疑之处 —— 这正是自由量产与受控量产的分野。

第 4 步 —— 验证与否决(人的位置)

不会原样接收这份输出。美术总监会拿 brief 把 6 个方案过一遍。实际上在这次会话里,判定是这样分开的。

这里废弃的 2 件不是损失。若是手绘,要弄清这两个方向是错的得花上好几天,而量产把 6 个方案同时铺开,在一小时之内就筛掉了它们。

第 5 步 —— 再请求

将 1 号方案(放大镜身体化)与 5 号方案(义眼)的方向合并。
- 把黄铜放大镜 + 一只义眼整合到同一人物
- 情感克制(scholarly_strict):表情为无,仅用道具诉说职务
- 再次确认 forbidden:法师长袍·魔力球体·强调美型全部禁止
这是要生成‘最终候选 1 案’、交给美术总监手工精修的阶段。

AI 再次给出了把放大镜与义眼整合到一位老学者身上的单一方向,那一张图交到概念美术师的桌上,由手工收尾。量产(6 案) → 废弃(2 案) → 收敛(1 案) → 人工收尾的一个周期在这里闭合。AI 做出的不是最终资产,而是供美术总监挑选的候选范围。

这一整圈就是本书全书的 Show 标准。若没有哪怕一次从头看到尾——AI 吐出了什么、什么被废弃、人又收尾了什么——那么“用 AI 量产了概念”这句话就是空洞的。


12.1.3 废弃率高,是探索深入的信号

在上面这次会话里,6 个方案中有 2 个被废弃。若从整条概念线来看,废弃会堆得多得多。贴在会议室墙上的 100 张里,采用的只有 3 张。

要诚实地对待这个比例。这是亲自数了导入初期几次概念会话得到的方向值,而不是精确的总体比例(作者估算,未经验证——会随角色性格·brief 质量大幅波动)。因此,应当把它当作“比起手工时期,废弃变得自由得多”这个方向来读,而不是“精确的百分之几”。

重要的是,废弃率 0% 并不是目标。一张纸很贵,就会把一张纸打磨到底。100 张纸在 30 秒内印出,丢掉 99 张也没有负担,探索的幅度也随之变宽。废弃率上升,是探索深度加深的信号。试图压低废弃率本身的运营——比如“AI 生成的东西差不多就用吧”这种压力——会把探索的价值一起削掉。§12.1.2 中之所以能毫不犹豫地丢弃 4·6 号方案,正是因为丢弃的成本为 0。


12.1.4 纹理量产 —— 可逆区间的第二个位置

与概念一并,在可逆区间里 ROI 很大的另一个位置就是纹理。这是为 3D 模型生成材质的阶段,而这里 AI 介入的格子与确定性负责的格子也划分得很清楚。

flowchart TD A["UV 展开<br/>(人工)"] --> B["基础纹理<br/>(AI 生成或手绘)"] B --> C["法线·粗糙度·金属度<br/>确定性提取(Materialize 等)"] C --> D["引擎 import +<br/>光照预览"] D --> E{"美术总监评审<br/>(可逆 —— 可自由重新生成)"} E -->|"色调不一致"| B E -->|"通过"| F["登记资产 ID·材质键<br/>(L3 数据表)"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class C code; class B ai; class A,E human; class F data;

AI 介入的只有基础纹理一格。法线·粗糙度·金属度这类 PBR 贴图不会让 AI 每次生成得都不一样,而是交给确定性提取工具。因为只有从同一基础生成同一贴图,材质才会一致。这与 §6.2 城市生成器中不把奖励曲线交给 AI、而由规则手册把控是同样的分工 —— 能用确定性保证的交给代码,需要探索的交给 AI。

即便是基础纹理,也并非对所有资产都适合用 AI。像角色面部这种细微细节左右游戏辨识度的地方,依然以人手为先。所以当评审关卡抓到“色调不一致”时,不是自动废弃,而是退回去重新生成。到这里为止全都在分界线左侧 —— 无论重跑多少次都不会有损失的可逆区间。


12.1.5 不可逆关卡 —— 一致性验证与视觉回归

在跨过分界线之前,检查可逆区间量产的资产是否与整个游戏的调性相违。这是一处仅凭人眼会漏看的地方,所以先由代码打第一遍。

# visual_regression.py —— 资产替换时检测意图之外的变化(骨架)
# 输入: 资产 ID + 替换前/后相同条件的渲染截图
# 输出: 变化等级(向人工评审关卡发出 alert)

def compare_renders(asset_id, before_png, after_png, threshold=(1.0, 5.0)):
    diff = pixel_diff(before_png, after_png)   # 归一化到 0~100
    if diff > threshold[1]:
        return ("BLOCK", f"{asset_id}: 变化较大 {diff:.1f}% —— 评审前禁止进入不可逆")
    elif diff > threshold[0]:
        return ("WARN",  f"{asset_id}: 轻微变化 {diff:.1f}% —— 需确认是否为有意变更")
    else:
        return ("PASS",  f"{asset_id}: 无变化")

这 30 行,会在进入不可逆之前就抓住“换了一张纹理,结果另一个角色的阴影就崩了”这类事故。关键的设计在于,BLOCK 不是自动废弃,而是只向评审关卡发出 alert —— 如果连有意的变更(重新设计)都被代码扼杀,画师们在一两个季度内就会说“关掉吧”。可疑的候选由机器挑出,但是否放行到不可逆,由人来决定。

评审要抓的另一件事是风格一致性。AI 的输出每次都会有细微差异,所以量产出的概念·纹理是否维持游戏的质感,由人来做最后的把关。只有通过这道关卡的,才会进入骨骼绑定·动作捕捉·最终渲染这些不可逆阶段。因为一旦捕捉了动作并并入构建,一致性事故就只能靠返工·重录·重新发布来修复。


12.1.6 交给美术团队的只有决定 —— md→html→sync

即便策划用 AI 量产了概念·纹理,真正画图的美术团队仍是另一个组织。这里协作的核心,是让美术团队不必学习策划团队的工具·约定。项目A的美术指南(96_ArtGuide/)用自动化来解决这一点。

美术决定由策划团队用 md 写,_convert_md_to_html.py 转换成 html,再由 _SyncToArtRepo.bat push 到独立的美术仓库。美术团队在那个仓库里只看 html —— 既不用懂 md 约定,也不用懂策划团队的 SVN(管线示意图见 §12.2.4)。

而且这份决定文档被分成 7 个域(00_Common·01_Character\~07_Env),各自握着自己的风格规则,在统一关卡处汇合。这就是下一章(12.2)要讲的 ArtGuide 7 个领域,先只说核心就是 —— 把风格规则手册不放进一格、而是分成 7 格抽屉,AI 量产提示词就不必每次在画师脑中重新组装,而是从抽屉里取出。 §12.1.2 的 style_anchor·forbidden 正是从那些抽屉里取出的输入。只有规则手册被分开,量产结果才不会回归到普通奇幻的平均值。

但这并不意味着每款游戏都得凑齐 7 个领域。若是休闲品类,角色·环境两格也就够了。分离要循序渐进,接口要保持狭窄。


12.1.7 诚实对待数值与风险的方法

本章的数值只有三类。(1) 方向·比例 —— “量产 100 张采用 3 张”是基于作者经验的方向值(未经验证),因此不当作绝对值,而是读作“在可逆区间废弃成本收敛于 0”这个方向。(2) 测量值 —— 视觉回归变化率(diff %)、一致性事故件数、BLOCK 处理件数由 visual_regression.py 以数字吐出,因此在会议上可以用数字而非“感觉”来说话。反过来,“留存率上升了”并不由单一美术左右,所以不对因果下定论。

(3) 把风险放进运营成本之内。AI 美术的三重风险——训练数据版权、风格一致性受损、美术师的岗位——不在 ROI 计算之外,而在其内。作者的方针是:可逆区间积极用 AI,跨入不可逆的最终资产为手工精修,而直接进入构建的资产其 AI 输出比例以 0 为原则。不过这只是一种政策 —— 也有团队只用许可明确的模型,连最终资产也用 AI。法务政策因公司而异,本书给出的不是标准答案,而是划分界线的方法。

三重风险中最常被忽略的是第三个。如果不把 AI 定位为“拓宽探索幅度、增大美术师决定权的辅助”,而是当成“取代美术师的量产机”,那么这个工具即便在 KPI 上成功,也会被组织拒绝。这也是 feedback atom design_intent_vs_automation_boundary(设计意图 vs 自动化边界)所固化的地方。


12.1.8 常见的失败

模式 为何失败 处方
把 AI 概念直接作为最终资产投入构建 未经人工评审就通过不可逆阶段 分界线前设关卡(§12.1.1)
提示词以外形描述开头 function 误译 —— 回归为美型法师 设计意图优先(§12.1.2,image_prompt_design_intent_first
量产的 6 个方案是细微变体 无探索价值,没有可废弃的 强制多样性(§12.1.2)
试图压低废弃率 把探索深度一起削掉 把可逆区间的废弃视为信号(§12.1.3)
连纹理 PBR 贴图也用 AI 生成 材质一致性每次调用都晃动 分离出确定性提取(§12.1.4)
不做视觉回归就替换资产 意图之外的变化漏进不可逆 visual_regression.py 关卡(§12.1.5)

12.1.9 动手试试 —— 今天就能做的一步

一个人的话,做到这些就够:没有美术团队、也没有数据表也无妨。挑选你自己游戏(或你喜欢的游戏)里的一个 NPC,按 §12.1.2 的 concept_brief 格式,先于外形写下 functionnarrative_seed,再把 6 案量产提示词原样贴上去跑一次。从跑出的 6 个方案里挑一个与意图相悖的,试着反驳“这是 function 误译,废弃重来”,你就会亲身体会到:可逆区间的废弃不是损失,而是探索。

如果是团队,就从下面这一步开始。在管线上明确地画一条可逆/不可逆分界线(§12.1.1)。就“到哪个阶段为止是‘丢弃也为 0’、从哪里起是‘回退很贵’”达成一致,并在那条界线上设一道人工评审关卡。界线一旦画好,“AI 用到哪里”这场每次都从头打起的争论,就会变成“这项工作在界线哪一侧”的一次性判定。

用 setup → prompt → verify 概括就是 —— setup:在管线上定义可逆/不可逆分界线与评审关卡。prompt:按 §12.1.2 的格式先输入设计意图,量产 6 案,同时强制禁忌·多样性·上报。verify:在可逆区间亲自挑出 1 件意图误译,用废弃·再请求闭合一个周期,并在进入不可逆之前用 visual_regression.py 打一遍意图之外的变化。


本章要点

下一章预告


12.2 ArtGuide 七大领域(角色·动画·怪物·NPC·VFX·UI·环境)

周四的整合评审。当我们把七个新资产贴在同一屏幕上一看,所有人同时笑了。学者角色是灰色调、沉稳的剪影,而在它旁边炸开的技能 VFX 却是荧光粉。两者在各自的领域里都是完美的决定。角色总监严格遵守了自己的 _STYLE_GUIDE.md,VFX 美术也忠实执行了我"要显眼"的规格要求。谁都没有错,可放在同一屏幕上,两款游戏却在互相打架。

这一幕既是把 ArtGuide 拆成七大领域的理由,也是必须把七大领域重新捆在一起的理由。ArtGuide 是游戏的视觉宪法。按领域划分,各领域总监就拥有了自治权,决策随之加快;而若不通过整合评审重新捆合,上面那种荧光粉式的事故就会一个季度一个季度地累积。策划在这一平衡的哪个点上落手,便是本章的全部内容。


12.2.1 一张资产:七大领域的真实结构

在笔者担任总监的项目A(东方奇幻基调、移动优先的 MMORPG)的设计仓库里,有一个名为 96_ArtGuide/ 的文件夹。编号 96 是为了让美术指南在仓库排序规则下排到几乎最后而加上的,其下则分成七个领域。这不是抽象的"项目美术文件夹",下面就是该文件夹的真实子结构。

96_ArtGuide/

00_Common 通用规约 调色板·规则 01_Character 玩家 角色 02_Animation 所有 动画 03_Monster 敌方NPC 视觉 04_NPC 友好NPC 关系·voice 05_VFX 视觉效果 技能·演出 06_UI 画面·HUD (9.3)

07_Environment 背景·道具·地标

每个领域 = 总监/资深1人自治 + 各领域 _STYLE_GUIDE.md(宪法) 00_Common = 横跨七个领域之上的通用上位规约(色彩·材质·时代基调)

图示的要点有两个。第一,七个领域并排、平等地拥有自治权。可以想象一间办公室里,同一层排开七间工作室。每个房间的负责人握有本房间的决定权,但在走廊相遇时,不能失去"同属一款游戏"的感觉。第二,其上叠着 00_Common。七个房间都必须遵守的通用规约——即整体色彩调色板、材质基准与时代基调——都住在这里。06_UI 与 9.1.3 讨论过的 UI 协作标准属于同一领域,因此本章只划出边界便略过。

12.2.2 每个领域策划介入的深度各不相同

策划并不会以相同强度介入七个领域。"策划决定意图与叙事、美术决定视觉"这一原则对所有领域都一致,但意图把视觉牵引到哪一步,则因领域而异。

领域 策划介入 策划不该越过的界线
01_Character 到概念·性格·势力·角色定位为止。脸部比例·笔触则不属于
02_Animation 到技能动作的"种类·反应"为止。帧时序则不属于
03_Monster 到敌人概念·势力·生态为止。鳞片纹样细节则不属于
04_NPC 到角色定位·关系·voice_profile 为止。服饰刺绣则不属于
05_VFX 到"缓慢发射、大爆炸、紫色"为止。粒子数量则不属于
06_UI 到信息结构·优先级为止(9.3)。像素间距则不属于
07_Environment 到氛围·地标意图为止。树木多边形则不属于

右侧那一栏才是本表的真正内容。即便在标注介入为"强"的领域,策划也有不能越过的界线。角色概念可以强力牵引,但一旦连脸部比例都动手,角色总监的自治就在那一刻崩塌。而且强与弱的边界本身会随品类而摇摆。若是恐怖游戏,VFX 是恐惧的核心,策划介入随之增强;若是休闲解谜,角色介入反而会减弱。上表是项目A的品类基准,并非普遍法则。

12.2.3 领域的宪法:_STYLE_GUIDE.md

每个领域都以一组标准文档来运营。来看 01_Character/ 领域的真实文件构成。

01_Character/
├── _STYLE_GUIDE.md          — 角色整体风格(宪法)
├── _COLOR_PALETTE.md        — 色彩·材质指南
├── _PROPORTION_REFERENCE.md — 比例·剪影规则
├── _DO_AND_DONT.md          — 允许·禁止
├── individual/              — 各角色卡
│   ├── K_001_director.md
│   ├── K_007_scholar.md
│   └── ...
└── _REVIEW_LOG.md           — 评审记录

_STYLE_GUIDE.md 是领域的宪法。各个角色卡(individual/)都在这部宪法之上变奏。宪法一旦动摇,其下所有角色都会动摇,因此这一份文件是领域中被最频繁审阅的文档。骨架如下。

---
title: 01_Character Style Guide
layer: L1
---

## 1. 基调
- 19世纪工业革命以前的韩国奇幻氛围
- 写实比例(7~7.5 头身,禁止 Q 版变形)

## 2. 色彩
- 饱和度:中等(约实拍的 60~70%)
- 主调色板:继承 00_Common
- 各角色的强调色(1~2 个)

## 3. 服饰规则
- 按势力区分服饰(学者 → 灰色 + 紫色强调)
- 依职业·阶级而定的服饰细节

## 4. DO
- 在 5m 距离仅凭剪影即可辨认是谁
- 以视觉表现势力身份

## 5. DON'T
- 日式动漫风格
- 非古装元素(现代服饰·道具)
- 饱和度过高

这里有一行很关键。## 2. 色彩 中的"主调色板:继承 00_Common"。这是明文规定:角色领域不自行决定色彩,而是继承上位的通用规约。这一行正是从结构上堵住开篇那起荧光粉事故的装置。只要所有领域的 _STYLE_GUIDE.md 在色彩上都继承 00_Common,至少色彩冲突在宪法层面就被阻断了。

12.2.4 如何把非策划人员拉进协作

这里出现了项目A实际撞上的、最现实的问题。美术团队不读 Markdown。更准确地说,不该强迫他们去读。让美术人员学习 git diff、frontmatter 与 Markdown 标题层级的成本,几乎总是大于由此学习换来的协作效率。把策划团队的工具原封不动地塞给美术团队的那一刻,协作反而会变慢。

所以项目A的流水线可以用一句话概括:"策划团队用 md 做决定,美术团队只看 html"。

flowchart LR A["策划团队:决定 ArtGuide<br/>(更新 _STYLE_GUIDE.md)"] --> B["_convert_md_to_html.py<br/>(md → 美观的 html)"] B --> C["_SyncToArtRepo.bat<br/>(push 到独立的美术 SVN)"] C --> D["美术团队:只查阅 html<br/>(md 学习成本为 0)"] D -. 反馈 .-> A classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; class B,C code; class A,D human;

关键在于两项自动化资产。_convert_md_to_html.py 把领域的 Markdown 指南转换成美术人员可以在浏览器里舒适阅读的 html。色彩调色板会渲染成实际的色块,DO/DON'T 会渲染成视觉对比。_SyncToArtRepo.bat 则把那份 html 推送到独立的美术专用仓库,而非策划仓库。分离仓库的理由很简单。美术人员只拉取自己的仓库,就不会接触到策划团队内部的 md 历史、正在编写的草稿、其他领域的决策过程。美术人员看到的,只有已确定决策的、易读的成品。Markdown 学习成本降为 0。

在这一结构中,策划多了一项责任。一旦更新 md,就必须跑一遍转换·同步步骤。只更新而漏掉同步,美术团队就会拿着昨天的决定画今天的图。决策与传达之间的这一格若是空着,自治也好、整合也好,都失去了意义。

12.2.5 图像提示词也是决策:设计意图优先

作为非策划协作的延伸,在概念阶段使用生成式 AI 时,策划要守住一条原则。以项目A内部规约的名称来说,它叫 image_prompt_design_intent_first,展开来讲就是"图像提示词也要先写设计意图"。

用生成图像探索概念时,常见的失败是提示词只被结果物的外观描述填满。比如"身着灰色道袍的 50 多岁东方男性,神情沉静,写实风"。这样的提示词能出图,却装不下为什么要这样,于是美术总监在变奏这张图时会迷失方向。设计意图优先原则强制在提示词之前加上一个意图块。

[设计意图]
- 角色定位:学者势力的精神支柱,玩家的第一位导师
- 必须被读到的:即便在 5m 距离也能读出"知识分子·非战斗"的剪影
- 势力信号:学者 = 灰色 + 紫色强调(继承 00_Common)
- 禁止:携带武器、华丽甲胄(会被误读为战斗职业)

[提示词]
身着灰色道袍的 50 多岁东方男性学者,紫色衣带强调,
无武器,神情沉静而好学,19世纪以前的韩国奇幻,
写实比例 7.5 头身,饱和度中等,……

意图块位于提示词之上时,那张图就成了决策的一部分。下一个人生成同一角色的另一个姿势时,不是照抄外观,而是再次满足意图。当图像不满意需要废弃时,也能围绕"意图中哪一点没被读出来"来讨论。只写外观的提示词止于"我的口味觉得这个更好",不留下可供验证的依据;而意图在前的提示词,则留下了可以推敲何者已满足、何者被遗漏的记录。

这里,工具选择与意图优先原则咬合在一起。要按意图反复生成"同一角色的另一个姿势",光靠提示词是不够的。项目A的工具分工与 §12.1.1 相同——正式量产在自托管 SD(SDXL)/ComfyUI 上挂载角色 LoRA(固定脸部·服饰)与 ControlNet(固定姿势·剪影),让意图块所要求的"5m 距离辨识度"即便姿势改变也得以保持,同时也守住 IP。封闭式工具(Midjourney 等)则只用于早期情绪板一类的用途。

12.2.6 实操记录(worked transcript):角色概念规格的一个周期

为了看清领域自治与策划介入实际如何运转,我们把 01_Character 的一个周期从头到尾跟一遍。以下是笔者把学者角色(K_007_scholar)的概念规格初稿交给 AI 处理的真实工作的还原——所谓实操记录,即完整保留的真实操作过程记录。不做概括,连别扭的输出与笔者的驳回也原样呈现。

① 笔者抛给 AI 的提示词(全文)

你是我们项目的角色概念助理写手。请遵守以下规约,用 Markdown 写出
K_007_scholar 的概念规格书初稿。

规约:
- 遵守 01_Character/_STYLE_GUIDE.md:19世纪以前的韩国奇幻,7~7.5 头身,
  饱和度中等,禁止日式动漫风格。
- 色彩继承 00_Common 调色板,学者势力的强调色为紫色。
- 必须包含 voice_profile 的 5 个条目(语气/口头禅/禁用语/情绪默认值/关系称呼)。
- 只决定到概念·性格·势力·角色定位为止,脸部比例或笔触之类的视觉细节
  请留空(属于角色总监的自治领域)。
- 输出采用 individual/ 角色卡格式。

② Claude 的原始输出(含败笔,未作改动)

# K_007 学者(Scholar)

## 角色定位
学者势力的精神支柱,也是玩家的第一位导师。

## 外形
- 身高 180cm,7.5 头身,清瘦体型
- 白须,深刻的皱纹,炯炯的目光
- 灰色道袍配紫色衣带
- ★ 推荐:以微微发光的金色瞳孔强调神秘感

## 性格
博学而不傲慢,对弟子严格却不失温和。

## voice_profile
- 语气:缓慢而沉静
- 口头禅:"……正是如此",句尾拖长
- 情绪默认值:平静
- 关系称呼:称玩家为"年轻的朋友"

③ 笔者的验证·驳回(何处、为何有误)

输出里有两处缺陷。第一,## 外形 中的"★ 推荐:金色瞳孔"。这是明显的越权。瞳孔颜色之类的视觉细节属于角色总监的自治领域,AI 却在概念阶段把它钉死了。若原样保留,总监要么以"策划已经定了"为由收起自己的判断,要么无视它而产生冲突,二者必居其一。第二,voice_profile 不是 5 个条目,而是 4 个。"禁用语"整个漏掉了。导师角色绝对不会说哪些话,这与性格同样重要,AI 却遗漏了。

④ 再次请求(只精确修正两处)

只改两点。
1. 在 ## 外形中删除"金色瞳孔"那一行推荐。瞳孔颜色属于角色
   总监的决定领域。外形条目只写到剪影·体型·势力色为止,
   细部色彩·材质则以"(总监决定)"留空。
2. 在 voice_profile 中补上遗漏的"禁用语"条目。明确指出作为学者导师
   不会说的话(脏话、粗俗玩笑、现代用语)。

这个周期的教训不是工具,而是边界。AI 很快给出了像模像样的初稿,却替策划越过了不该越的线(视觉细节),又漏掉了必须写进去的东西(禁用语)。验证的标准不是"写得好不好",而是"是否守住了领域自治的边界"。只要还在把 AI 用于角色概念,这道边界的审校就必须由人牢牢握到最后。

12.2.7 如何把各领域之间的一致性重新捆合

七个领域一旦拥有自治权,就会像开篇的荧光粉那样,在领域之间产生不一致。常见的类型是固定的几种。

不一致类型 实际示例
角色—环境基调差异 角色沉稳,背景却华丽,二者各说各话
角色—VFX 色彩冲突 角色为灰色调,技能 VFX 却是荧光粉
NPC—Monster 边界模糊 明明是友好 NPC,却被读成怪物般具有威胁
UI—角色色调不一致 UI 是冷色调,角色是暖色调

捕捉这种不一致的装置,是每周一次的整合评审。流程很简单。

每周一次 ArtGuide 整合评审(周四)
─────────────────────────────────
1. 随机抽取当周新资产 5~10 个
2. 一起排布到同一屏幕(游戏内模拟)
3. 七个领域总监 + 游戏总监同时评审
4. 发现不一致 → 补强对应领域的 _STYLE_GUIDE
   或补强 00_Common 上位规约

关键在第 3 步和第 4 步。评审由各领域总监同时进行,以及把发现的不一致不是靠修改单个资产了事,而是回归到指南文档。开篇那起荧光粉事故,如果只把那一个资产改成灰色就收手,下周会一模一样地复发。反之,若在 00_Common 中加上"技能 VFX 的饱和度须在角色调色板饱和度 +20% 以内"这条规约,同样的事故就从结构上被关闭。每月累积 4 次的这一周期,是阻止自治固化为孤岛的唯一护栏。

12.2.8 自治不是责任的分散,而是决策时间的再分配

把七大领域分离带来的变化,从项目A的运营经验中梳理如下。下表数值中,周期天数与时间基于笔者的运营经验,属于笔者估算(未经验证),并非精确测量值,只应作为分离前后的方向与大致比例来读。

项目 分离前 分离后 性质
美术决策周期 1\~2 周 3\~5 天 笔者估算(未经验证)
领域间一致性事故 每季度多起 显著减少 仅方向
游戏总监美术评审时间 每周多个小时 大幅减少 仅方向
新领域总监上手 数月 约 1 个月 笔者估算(未经验证)
美术资产废弃率 下降 仅方向

诚实地读这张表,能够断言的只有一点:所有项目都朝同一方向移动了。最明确的效果是,游戏总监的时间被收回了。在没有自治的结构里,所有美术决策都必须经过游戏总监一个人的桌子。这是纸张堆在一张桌子上的结构。分成七大领域后,纸张分散到了七张桌子。纸张的总量不变,但没有任何一张桌子会被压垮。

这里必须斩断最常见的误解。自治不是责任的分散,而是时间的再分配。领域总监决定自己的领域,并不意味着整款游戏的视觉责任被打散成七块。在整合评审上,那七块又重新聚到一处,最终的视觉责任依然收敛于一个人。一旦把自治当作逃避责任的借口——一旦"那不在我的领域内"成了口头禅——开篇那抹荧光粉就会在无人认领的状态下进入构建。

还有一个附带条件。七大领域自治是规模的函数。在小规模(\~10 人)团队里,它反而是过度工程。若还处在一名总监身兼五顶帽子的阶段,那么需要的不是七份领域指南,而是一份整合指南就够了。自治只有在桌子开始不够用时,才真正体现价值。

12.2.9 常见的失败与处方

模式 处方
不做领域分离,所有决策都集中到游戏总监 引入七大领域自治(但从中等规模(10\~50 人)起)
缺少领域 _STYLE_GUIDE 将各领域宪法的编写设为强制门禁(gate)
缺少领域间的整合验证 每周一次整合评审 + 回归指南
策划连视觉细节都决定 只规格到意图·叙事为止,细节交给总监
自治固化为孤岛 在整合评审上回归到 00_Common
更新 md 后漏掉同步 养成 _convert_md_to_html.py_SyncToArtRepo.bat 的习惯
图像提示词只有外观描述 先行设计意图块(image_prompt_design_intent_first)

本章要点

下一章预告


动手试试

setup —— 从最费功夫的一个领域(通常是 01_Character)开始。在领域文件夹里建立 _STYLE_GUIDE.md(宪法)、_COLOR_PALETTE.md_DO_AND_DONT.mdindividual/。在色彩条目中,务必写上"主调色板:继承 00_Common"这一行。

prompt —— 用 AI 起草单个资产卡时,在提示词中写明:(1) 对应领域 _STYLE_GUIDE.md 的规约,(2)"只决定到概念·叙事为止,视觉细节以'(总监决定)'留空",(3) 不可遗漏的必需条目(例如 voice_profile 的 5 个条目)。若是图像提示词,则在外观描述之上先写 [设计意图] 块。

verify —— 拿到输出后,不以"写得好不好"、而以"是否守住了领域自治的边界"来审校。只看两点:① AI 有没有替策划决定了不该越界的视觉细节,② 必需条目有没有遗漏。只把出错的地方精确点出来再次请求。每周四,把 5\~10 个新资产聚到同一屏幕上,与各领域总监同时查看。不一致回归到指南文档(00_Common 或领域 _STYLE_GUIDE),而不是回归到资产。

单人精简版 —— 如果是一个人做的游戏,就别建七个领域。把色彩调色板·时代基调·DO/DON'T 汇总到 00_Common 一张里,在其中只用标题把角色·环境·VFX 分成若干小节。把资产交给 AI 时,把这一张整个贴进提示词,并一并要求它"若有违反本指南之处,请标出来"。整合评审只需一个人每周一次、把当周做的东西摆到同一屏幕上看的 5 分钟仪式就够了。只是没有可以分担自治的人而已,宪法一张 + 每周一次对齐这一骨架,在单人团队里同样能体现价值。

12.3 规格书 → 概念 → 游戏内资产的流转

冲刺末期,概念美术师在团队即时通讯工具(IM)里丢来一张角色草图。"这是学者公会的资深成员吧?"画面里的人物是一名 30 多岁的男性,穿着皮甲。而规格书上写的是 40 多岁女性、灰色学者长袍。追查究竟哪里出了偏差后发现,概念美术师拿到的资料是两个月前版本的规格书,而在这期间外形指南已经改过两次。知道这一变更的,只有策划本人。

这起事故不是技术问题,而是流程问题。规格书的一页要变成游戏内的资产,平均需要 4\~8 周,在这期间,一个角色的信息会从策划的脑海里,经手到概念美术师、模型师、动画师,一手接一手地传递下去。每一次交接,格式都可能出现偏差;若在有偏差的情况下被接收,接手者就会用猜测去填补空白。而这份猜测,会在两个月后化作团队 IM 里的一句话回到你面前。

本章讨论的,就是如何把这种一手接一手的流转,从一个人的记忆搬到系统之上。


12.3.1 一手接一手 —— 4 个阶段与转换点

在项目A中,角色资产流转的路径分为四个阶段。重要的不是阶段本身,而是阶段与阶段之间的转换点。事故不是在阶段内部发生,而是在把资产从一个阶段交给下一个阶段的那一刻爆发。

与其用文字说明这一流转,不如画成图示。而贯穿全书的 24 个部分里,我一直不靠手绘方框,而是让 Claude 生成 mermaid 代码再渲染。本章正是把这一手法应用后得到的结果放进正文,相当于用自己的正文来印证自己的手法。下面就是我请 Claude"用 mermaid 把 spec→asset 的 4 阶段流转画出来,并让转换点关卡清晰可见"后得到的输出,原样渲染的结果。

flowchart TD A["阶段 1 · 规格书<br/>character_spec.md"] -->|规格→视觉转换| G1{关卡 1<br/>外形 6 项检查} G1 -->|通过| B["阶段 2 · 概念美术<br/>concept_K_001_v3.png"] G1 -.->|退回| A B -->|视觉→3D 转换| G2{关卡 2<br/>模型表检查} G2 -->|通过| C["阶段 3 · 3D 资产<br/>model_K_001.fbx"] G2 -.->|退回| B C -->|静态→动态转换| G3{关卡 3<br/>资产 lint} G3 -->|通过| D["阶段 4 · 游戏内整合<br/>动画·VFX·音效·代码"] G3 -.->|退回| C D --> G4{关卡 4<br/>综合评审} G4 -->|通过| E["纳入构建"] G4 -.->|退回| D classDef gate fill:#fde2c8,stroke:#d2691e,color:#5a2e00; classDef asset fill:#dbeafe,stroke:#2563eb,color:#0b2545; class G1,G2,G3,G4 gate; class A,B,C,D,E asset;

三个转换点(规格→视觉、视觉→3D、静态→动态)各自都立着一道关卡。关卡就像是在把审批文件交给下一个部门之前检查格式的窗口。格式不符就会被退回(虚线),回到上一个阶段。若接收了格式不符的文件,下一个部门就会用猜测填补空白。这起 IM 事故,正是在没有关卡 1 时发生的。

mermaid 的优势在这张图里显现出来。想要再加一道关卡,或调整阶段顺序时,不必重画方框,只需改动一行文本。因为图示就是文本,它便成了版本管理的对象,和规格书一起被提交(commit)。


12.3.2 阶段 1 —— 规格书是一切输入的根

流转的起点,是一份 Markdown 规格书。这份文档是随后三个阶段的全部输入。这里若有空白,空白不会消失,而是被甩给下一个阶段,变成猜测。

下面是实际使用的 character_spec 格式。related_atoms 字段把这份规格书连接到 JIT atom 系统(参见第 11 部分)。

---
title: 学者公会资深成员 K_001 角色规格书
type: character_spec
layer: L2
related_atoms: [character_K_001, voice_profile_K_001]
status: draft
---

## 1. 身份
- 名称:(TBD)
- 职责:学者公会资深成员、主要 NPC、可结为同伴
- 势力:scholar_guild
- 性格:学者_严格,威严但公正

## 2. 外形指南
- 年龄:40 多岁
- 性别:女性
- 体格:略高于平均(约 170cm)
- 服装:灰色 + 紫色点缀,学者长袍,眼镜

## 3. 表情·姿态
- 平时:沉着,嘴角下垂
- 愤怒时:沉默 + 回避视线
- 悲伤时:转移话题,表情变化细微

## 4. 游戏内职责
- 主线任务第 1·5·12 章登场
- 发布 8 个支线任务
- 第 7 章加入同伴

## 5. 语音·台词
- voice_profile: scholar_K_001
- 代表性台词 3 句:
  - "那是没有记载的故事吗?"
  - "不要凭感情做决定。数据优先。"
  - "……是你的选择啊。我知道了。"

## 6. 参考资料
- 5 位真实学者的图片(法务审查完毕)
- 参考 refgame 的角色基调

外形 6 项(第 2 节的年龄·性别·体格·服装,以及第 3 节的表情·姿态)构成了关卡 1 的检查清单。造成这起 IM 事故的"40 多岁女性 / 灰色长袍"那两行,正出自这一节。倘若关卡 1 起了作用,概念美术师所拿到资料的外形 6 项是否与当前规格书一致,本会在交接之前得到检查。

实操记录(worked transcript) —— 填补规格书空白的一次往返

与其从零手动填满整份规格书,不如只定好身份与职责,把外形·表情的初稿交给 Claude 来给出。下面是一次真实往返的全文,未作概括。

提示词(全文):

以下角色的身份·职责已确定。请据此填写 character_spec 的"2. 外形指南"与"3. 表情·姿态"初稿。 - 职责:学者公会资深成员、主要 NPC、可结为同伴 - 性格:学者_严格,威严但公正 - 势力:scholar_guild 约束:refgame 基调(东方奇幻,低饱和度)。外形须仅凭剪影就能与其他主要 NPC 区分开。表情采用不直接流露愤怒·悲伤的克制型。

Claude 原始输出(原样,未加修饰):

2. 外形指南

3. 表情·姿态

还有一点:既然"可结为同伴",战斗中的表情大概也会需要,那是否在这份规格书的范围之外?

人工验证·驳回:

这份输出只采纳了一半。表情·姿态一节几乎原样采用 —— 它准确把握了"克制型"这一约束,而"以望向窗外的动作代替悲伤"是规格书里原本没有的好细节。外形则被否决,理由有两点。其一,它把体格定为"偏瘦,165cm",而已确定的规格书写的是"略高于平均,170cm"。Claude 只拿到了身份·职责,无从得知体格,于是做了猜测 —— 这正是关卡该拦下的猜测。其二,服装的藏青色虽符合 refgame 基调,却与我们游戏中 scholar_guild 的代表色(灰色+紫色)相冲突。Claude 抛出的问题("是否另有势力代表色?")恰恰提前点出了这一冲突。

重新请求:

很好。表情·姿态采纳。外形按以下固定后重新整理:体格 = 略高于平均的 170cm,服装 = 灰色学者长袍 + 紫色点缀(scholar_guild 代表色),佩戴眼镜。战斗表情在这份规格书范围之外,去掉。

这一次往返值得学习的一点是:Claude 用猜测填补空白的那个位置,恰恰就是规格书里的空白。遇到未知值时,Claude 分成了两种做法。势力配色和战斗表情,它以"这一点我不知道"为由用提问的方式挑了出来,而这些提问比关卡检查清单更早点出了缺漏。相反,体格却没有任何"不知道"的标示,直接用一个看似合理的数字填了进去。只要后一种情况存在,人工逐行对照已确定规格书的验证就无法省略。


12.3.3 阶段 2 —— 概念美术,以及关卡 1

已确定的规格书交到概念美术师手上。流程与 §12.1.2 的概念工作流相同:用 AI 量产数十到数百张,筛选到寥寥几张,再手工打磨出 1\~3 个方案,然后制作模型表(正面·侧面·背面)。

关键在于立于这一阶段末尾的关卡 1。在模型表进入阶段 3(3D)之前,检查以下五项。

项目 确认标准
符合规格书外形 6 项 服装·体格·年龄·性别·表情·姿态与当前规格书一致
主要 NPC 之间剪影可区分 仅凭 silhouette 就能与其他角色区分
遵守 ArtGuide 01_Character/_STYLE_GUIDE 无违反领域风格指南之处
与 voice_profile 无矛盾 视觉印象不与语音印象冲突
缩小识别性 缩到 UI·小地图大小仍能辨认出是谁

这里,image_prompt_design_intent_first atom 发挥作用。概念美术师写提示词时,也不是先罗列"灰袍女性学者"这类外形词,而是先放入规格书的设计意图("威严但公正""克制情感的学者")。只攥着外形关键词量产数百张,就会得出一大堆衣服颜色对、眼神却不像学者的图 —— 把意图摆在最前面,正是为了预先减少这类"外形对、印象错"的一堆废图。量产工具与 §12.1.1·§12.2.5 相同 —— 在自托管的 SD(SDXL)/ComfyUI 上,同时挂载角色 LoRA(锁定脸部·服装)与 ControlNet(锁定姿势·剪影),使同一人物即便以不同姿势抽出数百张,脸部也不会崩。

关卡 1 的首项 —— "符合规格书外形 6 项" —— 正是拦住这起 IM 事故的直接门闩。由于在概念草案定型为模型表之前就与当前规格书作了对照,拿着两个月前版本作业造成的偏差,便会在这里被卡住。


12.3.4 与非策划的协作 —— md 只给策划团队,美术团队只看 html

这里要指出一个运营上的不对称。目前所看到的规格书全是 Markdown,而概念美术师和 3D 模型师并不是为了读 Markdown 才进游戏公司的。因此,项目A 把 §12.2.4 里看到的单向转换管线("策划团队用 md 做决策,美术团队只看 html")原封不动地用在 spec→asset 流转上。策划团队做出的 md 决策被转换成 html,推送到独立的美术 SVN,美术团队只看 html —— md 的学习成本为 0。

flowchart LR P["策划团队<br/>character_spec.md"] --> CV["_convert_md_to_html.py"] CV --> H["96_ArtGuide<br/>character_spec.html"] H --> SY["_SyncToArtRepo.bat"] SY --> AR[("美术 SVN<br/>(独立仓库)")] AR --> ART["美术团队<br/>只查看 html"] classDef plan fill:#dcfce7,stroke:#16a34a,color:#052e16; classDef art fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef tool fill:#f3e8ff,stroke:#9333ea,color:#3b0764; class P,CV plan; class H,SY,AR,ART art; class CV,SY tool;

_convert_md_to_html.py 把 md 转成便于阅读的 html,_SyncToArtRepo.bat 再把结果 push 到美术 SVN 而非策划 SVN。分开两个仓库的理由,与 PC 分离原则相同 —— 是为了保护一方的工作流不覆盖另一方。转换始终是策划 → 美术的单向流动,即便美术团队改动了 html,也不会倒流回策划的 md。

这一转换的终点 96_ArtGuide 分为 7 个域(00_Common·01_Character\~07_Env)。各域以各自的 _STYLE_GUIDE 自治,但 00_Common 统管所有域的公共约定(饱和度范围·命名·分辨率)(结构图示见 §12.2.1)。关卡 1 的第三个检查项,正是是否遵守这个 01_Character/_STYLE_GUIDE


12.3.5 阶段 3 —— 3D 资产与自动 lint

模型表进入 3D 阶段后,要经过 8 道工序:高模建模 → 拓扑重建(游戏用低模) → UV 展开 → 贴图 → 绑定·蒙皮 → 测试姿势 → 检查。这一阶段是 AI 最薄弱的环节。由于 3D 生成模型尚无法给出游戏品质的拓扑重建·UV,人与传统工具才是主角。

作为替代,这一阶段附带关卡 3,即自动资产 lint。不是每次由人去数多边形数量,而是在资产被提交(commit)的那一刻自动检查。

检查项 通过条件
多边形数量 每个角色的标准范围(作者运营基准 40,000\~80,000)
贴图分辨率 2048×2048 标准
UV unwrap 效率 利用面积 80% 以上
骨骼(bone)数量 遵守标准骨骼集
资产命名规则 遵守第 11 部分命名规范

一旦查出违规,通知就会发给对应的 3D 美术师。这是把原本依赖人眼力的检查移到了确定性之上。多边形数量·分辨率这类项目对错分明,所以既不归 AI 也不归人,而是 lint 脚本的活儿。

这里出现一个不可逆的阶段:烘焙贴图的渲染工序。一旦烘焙(bake)过的贴图无法还原,因此在渲染前关卡 3 会再运作一次。阶段 4 的动作捕捉同样不可逆 —— 捕捉环节在重新召集演员与设备之前,是无法重来的。不可逆阶段之前的关卡,要比其他关卡运作得更严格。


12.3.6 阶段 4 —— 游戏内整合与综合评审

3D 资产与动画·VFX·音效·代码合到一起,首次出现在游戏中。这是所有工种齐聚一堂的阶段,关卡 4(综合评审)是最后一道门闩。

评审项 负责
符合规格书意图 策划
视觉基调·一致性 美术总监
动画自然度 动画总监
游戏内辨识度 游戏总监
性能(frame 负担) 技术美术

每个角色由 5 人花 30 分钟\~1 小时来审。这一阶段的 lint 由资产-资源映射(Skill_Art_Resource_Mapping)自动运行,检查游戏内实际挂上的资源与规格书所指的资源是否一致。在整合阶段,AI 的作用仅限于视觉回归测试与 lint 自动化 —— 它不是决定要展示什么,而是逐像素对照昨天与今天的帧是否在无意间发生了变化的确定性工作。


12.3.7 变更一旦触及某一阶段,下游便全部动摇

本章开头的这起 IM 事故,其实是两起事故的叠加。一是关卡 1 缺失(有偏差的资料通过了),二是变更追踪缺失(外形指南改过两次的事实没有传播到下游)。阻止第二起事故的,是变更影响追踪。

一个角色任何阶段的资料一旦改动,其下游的所有资料都会受影响。这件事若每次由人手工推算,必然会遗漏。因此,设置一个根据链条位置自动扒取下游资料的工具。

# spec_change_impact.py
# 链条的任一环节发生改动,就把其下游(downstream)资产全部收集起来。

CHAIN = ["spec", "concept", "model", "texture", "rig", "anim", "vfx", "ingame"]

def find_downstream_artifacts(spec_id, changed_field):
    artifacts = []
    chain_position = get_chain_position(changed_field)   # 例:"外形.服装" → "spec"(0)
    for stage in CHAIN[chain_position + 1:]:              # spec 下游全部
        artifacts.extend(get_artifacts(spec_id, stage))
    return artifacts

# 用法:K_001 的服装改动时?
changed = find_downstream_artifacts("K_001", "外形.服装")
# → ["concept_K_001_v3.png", "model_K_001.fbx",
#     "texture_K_001_diffuse.png", "rig_K_001.fbx", ...]

changed_field"外形.服装" 时,链条位置为第 0 位(spec),其下游的 concept·model·texture·rig 便全部被纳入影响列表。这份列表会以自动通知的形式发给各负责人。用桌上审批夹的比喻来看,修改 1 号审批夹的那一刻,2\~8 号审批夹上会自动插上红旗,插了旗的审批夹便重新进入审阅队列。这起 IM 事故,恰恰是因为没有这面旗才发生的 —— 1 号(规格书外形)改过两次,2 号(概念)上却没插旗。


12.3.8 度量 —— 4 阶段标准化的效果

下面是作者所运营的项目A标准化前后的对比。绝对时间·件数为作者估算(未经验证),可信的是方向和大致的比例。

项目 标准化前 标准化后 方向
单个角色(规格书→游戏内) 8\~12 周 4\~6 周 约减半
阶段间猜测事故 每季度 10\~15 起 每季度 2\~3 起 大幅减少
变更遗漏事故 每季度 8\~10 起 每季度 1\~2 起 大幅减少
综合评审时间(每个角色) 分散·重复(共 4\~6 小时) 30 分钟\~1 小时集中 集中化
新角色设计师上手 约 2 个月 约 1 个月 约减半

角色周期大致减半。但不能误解这个数字。标准化不是把所有角色以同一速度批量印出的传送带。主要角色仍要投入近 8 周,配角则 4 周完工。标准化所做的,不是让速度变得均一,而是让各阶段的时间差等能稳定地维持下去。标准一旦滑向管控,就会化作削减创作者创意时间的事故回到你面前 —— 标准化的目的是消除猜测与遗漏,而不是压缩时间。


12.3.9 各阶段中 AI 的位置

阶段 AI 的作用 强度
1. 规格书 起草辅助、缺漏提问(策划评审)
2. 概念 Stable Diffusion(SDXL)·ComfyUI 量产(LoRA·ControlNet)、LLM 提示词
3. 3D 生成模型尚不成熟,人·传统工具为主
4. 整合 视觉回归·lint 自动化 确定性

阶段 1·2 中 AI 强,阶段 3 由人负责,阶段 4 交给确定性工具。这种分工一旦确立,各阶段的责任就变得清晰 —— 哪些属于 AI 的初稿、从哪里开始是人的决定,在关卡面前不会含糊。


12.3.10 常见失败与对策

模式 对策
规格书缺失外形·表情 6 项 阶段 1 必查,让 AI 提出缺漏问题
略过概念阶段的关卡 强制在模型表定型前对照外形 6 项
手工推算变更影响 用 spec_change_impact 自动追踪
综合评审全堆到最后 每个阶段分散设置关卡
无资产 lint 就构建 关卡 3 自动拦截
强制把所有角色压进 4 周 维持各阶段的时间差等

第一行与第三行,正是本章开头 IM 事故的直接对策。


本章要点

下一章预告


动手试试 —— spec→asset 流转的最小搭建

setup 1. 创建一份 character_spec.md 格式(身份·外形 6 项·表情·职责·语音·参考 6 个小节,含 related_atoms 字段)。 2. 备好 md→html 转换脚本(_convert_md_to_html.py 之类),只向美术团队共享 html。 3. 在 4 个转换点挂上关卡检查清单(外形 6 项 / 模型表 / 资产 lint / 综合评审)。

prompt

以下 character_spec 的身份·职责已确定。请填写"外形指南"与"表情·姿态"初稿,但对不知道的值不要猜测,而要以提问的方式标出。约束:refgame 基调,仅凭剪影即可区分,克制型表情。

verify 1. 把 AI 猜测的值(尤其是体格·颜色)与已确定的规格书逐行对照 —— 若有偏差就先驳回,再以固定值重新请求。 2. 在交给模型表之前,让关卡 1 检查清单的 5 项全部通过。 3. 故意改动一行外形,确认 spec_change_impact 是否准确吐出下游资产列表。

单人精简版

如果是一个人作业,转换管线·美术 SVN·5 人评审就太重了。只保留最少的两样。(1) 一份 character_spec.md 格式 —— 外形 6 项必填,禁止留空。(2) 每次改动外形时,养成把"这次变更所触及的下游文件"用一行手写在规格书最底部的习惯。即便没有工具,那一行也能阻止变更遗漏事故。

13.1 将数百条自由回答归为主题——聚类交给AI,诊断交给人

第一读者:需要解读用户反馈与元游戏的MMORPG策划(中等规模(10\~50人)团队) 面向个人/爱好者读者的精简版:§13.1.8「一个人的话,做到这些就够了」

上线更新的第二天早上,我还记得游戏内问卷的自由回答栏里堆了312条的那个画面。从只有一句话的短评到写满五行的怒火,什么都有。策划团队没有一个人把那312条全部读完。准确地说,是读不完。就算读了,也只是带着"强化大概太肝了的抱怨很多"这种印象走进会议,而这个印象不过是嗓门最大的那5条制造的错觉。那312条到底在说什么,没有人知道。

本章讲的是:如何在人不必读完那312条的前提下,依然能说清"什么话题有多少条"。核心有两点。第一,把数百条自由回答归为主题并标注情感这种枯燥的分类工作交给AI。第二,不照单全收AI的聚类,而是由人抓出一条错误分类,予以驳回并重新请求。FAQ与元游戏分析的通论在别的书里也有,本章只聚焦于把这类分析放进AI工作流里运转的环节


13.1.1 自由回答不是"用来读的材料",而是"用来分类的材料"

FAQ与自由回答是一面镜子,照出策划设想的游戏与用户实际体验的游戏之间的差距。同一个问题一天在服务台被问30次,该做的不是增加接待人手,而是重新设计指示牌。问题在于如何数出这个"30次"。自由回答不是结构化日志,套不上 GROUP BY。"强化太贵了"和"资源不够养不起"是同一个主题,字符串却不同。靠人用眼睛归类,312条要花两三个小时,而且归类标准因人而异。

这正是AI该上场的地方。自由回答分类是一项(1)量大、(2)枯燥、(3)需要自然语言语义判断的工作——也就是说,确定性代码做不了,让人来做又很贵。不过有一点要在开工前钉死。AI产出的是主题聚类(假设),而不是确定的诊断。"强化不满38%"只是AI打标签的结果,不能直接导向"把强化削弱"的决定。贯穿整个第13部分的原则在这里同样成立——KPI定义与最终诊断交给人,自然语言归类与初步打标签交给AI。

自动化真正的价值也在这一点上。把分类自动化,与其说让分析本身变快,不如说关键在于312条这个信号每周早上以分好类的形态送到你的办公桌上。自动化的价值不是节省时间,而是暴露信号(团队运营概念 automation_signal_value_over_time_savings)。这就好比原本只是在信箱里堆积的信件,如今每天被分好类、投递到对应的部门。


13.1.2 [实操记录] 自由回答312条 → 主题聚类

下面完整地走一遍一个周期,展示实际是怎么运转的。以下是把作者项目(移动优先的MMORPG,下称"项目A")游戏内问卷的自由回答做主题聚类的一次会话的忠实再现——所谓实操记录(worked transcript),即完整保留的真实操作过程记录。输入提示词可以照原样复制使用,输出则是对真实会话的重现。

第1步——输入:原样丢进自由回答(不加工)

先把原始自由回答提取成机器可读的形态。这只需从问卷数据库里导出即可,并不是重新写。关键是不美化、不摘要,连错别字、脏话、单个词的回答都原封不动地放进去。原文越是原始,分类的准确度就越高。

# survey_freetext_2026-W21.jsonl (摘录,312条中的6条)
{"id": 0041, "text": "强化费用疯了 = = 冲10强资源根本攒不够"}
{"id": 0088, "text": "Boss招式挺好玩但奖励太抠了"}
{"id": 0102, "text": "公会战匹配太久了 等了5分钟多"}
{"id": 0156, "text": "不氪金就没法强化 这也叫游戏?"}
{"id": 0203, "text": "新副本氛围很好 音乐也不错"}
{"id": 0274, "text": "邮件里的奖励怎么没到?好像是bug"}

第2步——提示词:让它聚类,但强制给出类别、依据、模糊项申报

把附件 survey_freetext_2026-W21.jsonl(问卷自由回答312条)归为6~10个
主题,并给每条回答打上负面/中立/正面标签。不要拆得太细,一条回答
只归入一个聚类。归不进去的放到"其他"。按聚类给出条数、比例、主要情感、代表原文1条,
以表格呈现;分配没把握的不要硬塞,拿到[模糊]清单里,每条写一句理由。
去掉讲解,只要(甲)聚类定义 (乙)汇总表 (丙)[模糊]清单。

这条提示词里真正让它分类的话只有头两行,其余全是把输出形状框死的约束。给AI自由,它要么把聚类拆成20个,要么把所有回答硬塞进某个类别。挡住这一点的,是聚类数上限(6\~10)、单一归类、情感三分类,以及"没把握就丢进[模糊]"这个出口四条约束。分类的准确度就出自这道围栏。

第3步——Claude的输出(重现,摘录)

(甲)聚类定义
C1 强化/成长成本   —— 抱怨强化、资源获取过于繁重
C2 付费压力        —— 认为零氪/微氪被排除在成长之外
C3 内容奖励不足    —— 认为Boss、副本等付出与奖励不成正比、奖励太抠
C4 匹配/等待时间   —— 抱怨公会战、匹配等待过长
C5 内容好评        —— 对新副本、演出、音乐等的正面反馈
C6 Bug/发放遗漏    —— 奖励未发放、报错
其他               —— 不属于以上6类的零散项

(乙)汇总表
| 聚类 | 条数 | 比例 | 主要情感 | 代表原文                                  |
|------|------|------|----------|-------------------------------------------|
| C1 强化成本 | 71 | 23% | 负面 | "强化费用疯了 = = 冲10强资源根本攒不够" |
| C2 付费压力 | 58 | 19% | 负面 | "不氪金就没法强化 这也叫游戏?"          |
| C3 奖励不足 | 49 | 16% | 负面 | "Boss招式挺好玩但奖励太抠了"             |
| C4 匹配等待 | 34 | 11% | 负面 | "公会战匹配太久了 等了5分钟多"           |
| C5 内容好评 | 41 | 13% | 正面 | "新副本氛围很好 音乐也不错"              |
| C6 Bug发放 | 28 | 9%  | 负面 | "邮件里的奖励怎么没到?好像是bug"         |
| 其他       | 31 | 10% | 混合 | (零散项居多)                            |

(丙)[模糊]清单(4条)
- id 0088 "Boss招式挺好玩但奖励太抠了"
  → 归入了C3(奖励不足),但前半句也与C5(好评)重叠。复合回答。
- id 0156 "不氪金就没法强化" → C1与C2的边界。归到了C2。
- (另外2条)

输出中最有价值的部分不是汇总表,而是最底下的[模糊]清单。这是AI申报自己分配的不确定性、交给人来处理的地方。好的提示词能让AI说出"这条我没把握"。

第4步——验证与驳回(人的位置)

这份输出不能原样报上去。人要亲自核对原文样本。实际上,这次会话里就逮到了一条。

翻开C2(付费压力)的58条、逐条扫读原文时,id 0156 "不氪金就没法强化 这也叫游戏?" 这一条扎眼。AI把它归到了C2(付费压力)。可这句话的第一痛点不是"付费",而是"没法强化"——也就是C1(强化成本)。用户是被强化的墙挡住了,把这堵墙的成因指向了付费,而付费本身并不是不满的核心。C1与C2相邻、容易混淆确实如此,但把这条算作C2,"强化成本"这个信号就会显得比23%更小,真正该动手调整的强化曲线就会在优先级上被挤后。一条错误分类,就是能改变决策方向的边界案例。

于是驳回并重新请求。

C1(强化成本)和C2(付费压力)的边界有点混。第一痛点若是"成长墙本身"就归C1,
若是"不付费就被排除的公平性"就归C2,重新划一遍。id 0156的核心是"没法强化",
所以归C1。按这个标准把卡在边界上的重新分配,只告诉我改动了多少条。

AI重新划定边界,把原在C2的9条移到了C1。结果C1从71→80条(26%),C2从58→49条(16%)。"强化成本是单一最大主题"这个图景没变,但它的大小从23%变得更清晰为26%。一次往返,信号的轮廓就更清晰了。这个重新分配的条数(9条)与比例变化,是这次会话里实际数出来的值(样本312条,单一周次)。

这里要讲清一点。人之所以驳回,并不是因为"AI错了"。归到C2在解释上也说得通。人所做的,是把聚类定义(即KPI定义)打磨得更锋利,再反馈给AI。定义由人来定,而用这个定义把312条重新过一遍的劳动,由AI来干。


13.1.3 流水线——从自由回答到决策关卡

把上面这次会话每周自动跑一遍,就成了流水线。人手要碰的只有两处。把聚类定义打磨锋利的环节(前),以及把分类结果连向决策的关卡(后)。这中间的312条归类与打标签,由AI来跑。

flowchart TB A["原始自由回答312条<br/>(问卷数据库导出,禁止美化)"] --> B["第1段AI:主题聚类<br/>6~10个 + 情感标签 + [模糊]申报"] B --> C{"第2段人工验证<br/>原文样本 + 边界案例核对"} C -->|错误分类·定义模糊| D["重新定义聚类<br/>→ 请求AI重新分配"] D --> B C -->|通过| E["周汇总表<br/>主题 × 条数 × 情感"] E --> F{"策划决策关卡<br/>(总监·策划)"} F --> G["三路分流:数值复审<br/>· UI/教程 · Bug修复"] classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class B ai; class C,D,F human; class A,E data;

决定性的设计在于:第2段(人工验证)不会让AI的输出自动通过。做成自动通过型,AI一旦划错一次边界,就会每周朝同一个方向扭曲信号。可疑候选(模糊清单)由AI挑出,但要不要改聚类定义,由人来定。而且汇总表本身并不是决策,只不过是决策关卡的输入。"C1强化成本26%"是让总监去审视强化曲线的信号,而不是自动削弱的触发器。


13.1.4 元游戏——把自由回答与行为日志叠在一起看

如果说自由回答是"用户说了什么",那么元游戏(meta-game)就是"用户实际做了什么"。游戏上线后,会沉淀下策划并未设想的玩法,这就是元游戏。比如构筑meta(特定技能组合的扎堆)、动线meta(偏好的刷怪路线)、交易meta(与官方行情不同的玩家共识价)之类。这与自由回答不同,是用行为日志做定量测量的,由确定性代码(Python)来汇总。这不是AI该插手的地方。

关键是把两者叠在一起看。上面那次会话里,C1(强化成本)的不满以26%居首。此时若行为日志中的构筑多样性指数(头部技能组合的集中度)在同一周下降,那么"无论嘴上还是行动上,都在向单一构筑、单一成长路线收敛"这两个信号就指向同一个方向。当定量与定性一致时,决策才有把握。反过来,若自由回答风平浪静,行为日志却单单向一种构筑扎堆,那可能是用户感到不适却不说出口(即静默流失前夕)的风险信号。

flowchart LR A["定性:自由回答聚类<br/>(AI聚类 + 人工验证)"] --> C["叠加解读<br/>同一方向?还是背离?"] B["定量:行为日志汇总<br/>(Python确定性:构筑多样性·动线·行情)"] --> C C --> D["策划决策关卡"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; class B code; class A ai; class C,D human;

这里的分工同样清晰。行为日志的汇总由代码而非AI来做。因为构筑占比或交易行情是不能每次调用都变的确定性数值。AI只用来归类自由回答这种非结构化文本,而定量KPI由代码钉死。


13.1.5 本章数值的出处

本章的比例遵循序言〈一个承诺〉的原则。§13.1.2中的"C1 23%→26%、重新分配9条"是从样本312条(单一周次)实际数出来的值,因此不作为绝对值,而是当作"强化成本是单一最大主题"这个方向来读。因果不下断言——不存在"做了FAQ分析,留存率就上去了"这类表格。取而代之,这套工作流真正可测量的有三项:聚类验证中被人推翻的错误分类条数(为0则是验证流于形式的信号)、算出周汇总所花的时间、定量与定性信号是否一致。


13.1.6 废弃·重新请求不是工具的失败,而是关卡的信号

在§13.1.2里,人推翻了C2的9条分配。每周跑验证,这种推翻每次都会出现0到几条。重要的是推翻0条并不是目标。若验证中一条都没被推翻,那只有两种可能——AI完美(罕见),或者验证者没看原文、只是盖了个章。后者占压倒性多数。

每周逮到一两条边界案例,并以此为契机让聚类定义一点点变得锋利,这时验证关卡才算真正在运转。这是"人必须定期对AI分类的准确度做抽样评审"这一通用原则的具体形态。同一类用户被分散到不同主题的错误分类,若不做评审、只信任自动分类,就会每周累积。


13.1.7 常见的失败

模式 为何失败 处方
只靠人用眼睛扫读自由回答 嗓门大的5条代表312条的错觉 用AI聚类做全量分类(§13.1.2)
"AI帮我分析下用户反馈"整个甩给它 聚类被拆成20个,或强行分配 强制聚类数上限·单一归类·[模糊]
不加验证就上报AI的汇总表 边界错误分类改变决策方向 原文样本 + 亲自核对边界案例
把汇总比例直接等同于决策 "不满26%所以削弱"式的自动触发 汇总表只是决策关卡的输入
只看定性、无视行为日志 错过无声的静默流失 把定量(代码)·定性(AI)叠在一起读(§13.1.4)
让AI去汇总定量KPI 每次调用数值都变,平衡随之动摇 构筑·行情的汇总用确定性代码

第三条最常被忽略。汇总表很干净,让人忍不住原样相信。但正如id 0156那一条,边界上的一个错误分类,就能把优先级整个改写。验证不是把312条重读一遍,而是只把最大的两三个聚类的边界案例拿原文核对。


13.1.8 动手试试——今天就能做的一步

一个人的话,做到这些就够了:没有问卷数据库也行。把你自己的游戏(或喜欢的游戏)的商店评论、社区帖子只收集30\~50条文本,原样贴上§13.1.2的提示词跑一次。从跑出的聚类里挑一条"这归得有点怪"的分配,试着反驳"这条回答的第一痛点是别的主题,请重新确定定义并重新分配",你就会切身体会到,聚类原来是一堆判断的集合。

如果是团队,就从下面这一步开始。把一周的自由回答不加美化地导出为 survey_freetext_YYYY-Www.jsonl,用§13.1.2的提示词跑一次。然后只把最大的两个聚类的边界案例拿原文核对。聚类定义一旦打磨锋利,之后每周用同一条提示词就能自动积累起可复现的周汇总。


本章要点

下一章预告

13.2 KPI 定义·追踪 —— 定义由人来做,异常信号诊断交给 AI

主要读者:对运营指标负责的运营/数据策划(中等规模(10\~50 人)团队) 面向单人/兴趣读者的精简版本:§13.2.8「一个人的话,只需做到这些」

每个周一早晨,同样的场景都会重演。数据团队发来的每日仪表盘截图被投到会议屏幕上,有人说「DAU(Daily Active Users,日活跃用户)好像掉了一点」,又有人接一句「那是因为上周做了停服维护」。数字就摆在那里,可是判定这个数字到底是异常信号还是噪声,这项在人脑里进行的工作每周都要从头开始。而且这个判定因人而异。

先把本章的结论写在前面。在 KPI 上,人必须做的事只有两件:决定把什么定为 KPI,以及判定要把 AI 提交的异常信号升级为确定诊断,还是驳回。夹在这两件事中间的两项——每天在同一时刻从 raw 日志里提取数字,以及就相较上周有什么发生了波动用自然语言写出初稿——分别交给确定性代码和 AI。KPI 定义的一般论(压缩到 5\~7 个、当心古德哈特定律(Goodhart's law))在别的书里已经讲得够多,本章只聚焦于把这份定义放进 AI 工作流去运转的环节。


13.2.1 KPI 定义是人的职责 —— 但接下来的不是

在 KPI 运营中,有两项判断只有人能做。第一,把什么定为 KPI。第二,把每个 KPI 的定义用一句话钉死。这两项是游戏的价值判断,无法委托给 AI。「把 Active 视为游玩 5 分钟以上」这个决定里,包含着游戏把什么看作健康状态。

问题在于,这个定义一旦动摇,建立在它之上的所有数字都会跟着动摇。如果一边的查询把「Active User」算作登录 1 次,另一边的查询算作10 分钟 + 狩猎 1 次,DAU 就会整个错位。所以,比起定义本身,守住定义的一致性这件事占了运营的一半。而且一致性检查该由代码来做,而不是人脑(§13.2.5)。

定义钉牢之后的工作,就不是人的职责了。每天在同一时刻提取数字的抽取,浏览相较上周的变动、记下异常信号候选的初稿撰写——这两项每天重复,一旦由人来做,标准会天天飘移,正是该下放给机器和模型的那类工作。抽取交给确定性(代码),初诊交给 AI。人只接收 AI 提交的候选,判定要确认还是驳回

阶段 由谁 为何在此
KPI 选定·定义 游戏的价值判断,不可委托
每日 raw 抽取 代码(确定性) 相同输入 → 相同数字,可回归验证
相较上周的异常信号初稿 AI 自然语言摘要对 AI 友好,但只到「假设」为止
确定诊断·细分确认指示 对 AI 假设升级/驳回,是责任所在

这一分工是本章整体的骨架。下面我们把一个循环从头到尾跑一遍。


13.2.2 [实操记录(worked transcript)] 每日仪表盘 raw → 异常信号自动撰写

下面把一个循环从输入到人工判定完整展示,看它实际是怎么运转的。以下是把作者项目(移动优先的 MMORPG,以下称「项目A」)的每日 KPI 诊断会话匿名化后重现的内容。raw 日志的模式(schema)、抽取代码结构、提示词都取自真实工具,而数字是为展示形式而设的示例值,并非实测 KPI。

第 1 步 —— 输入:确定性抽取吐出的 raw 数字

首先,代码每天 09:00 从日志 DB 中提取 KPI。AI 不制造这些数字——只负责接收。抽取结果是一份把上周同一星期几并列摆放的 JSON。

// kpi_daily_2026-06-05.json — extract_kpi.py 产出 (LLM 输入)
{
  "date": "2026-06-05",
  "compare_to": "2026-05-29",   // 上周同一星期几 (周五)
  "active_def": "min10_hunt1", // 所应用的 Active 定义 ID
  "L0": {
    "ltv_12m_est":   {"v": 0,    "prev": 0,    "delta_pct": null},
    "d30_retention": {"v": 0,    "prev": 0,    "delta_pct": null}
  },
  "L1": {
    "dau":            {"v": 0, "prev": 0, "delta_pct": -0.0},
    "session_len_min":{"v": 0, "prev": 0, "delta_pct": -0.0},
    "sessions_per_u": {"v": 0, "prev": 0, "delta_pct": 0.0},
    "d7_retention":   {"v": 0, "prev": 0, "delta_pct": 0.0}
  },
  "segments": {
    "dau_by_platform": {"ios": 0, "aos": 0},
    "dau_by_region":   {"kr": 0, "sea": 0},
    "dau_by_newbie":   {"d0_7": 0, "d8plus": 0}
  }
}

值都留空为 0,重点在结构。每个 KPI 都带有当前值·上周值·变动率,最下方还一并载有细分拆解(平台·地区·新增/老用户)。要让 AI 不停在「DAU 掉了」,而是走到「去确认是哪个细分掉了」,这份拆解就必须存在于输入之中。

第 2 步 —— 提示词:强制格式·依据·「禁止确定诊断」

附件 kpi_daily_2026-06-05.json 是 09:00 自动抽取的每日 KPI,compare_to 是上周
同一星期几。从 L0~L1 里,只挑最多 2 件值得视为异常信号的。要挑相较上周的变动
超出平时该星期几波动幅度的。不知道平时波动幅度就别编,写「不明」并排除。
每个候选写明指标·变动幅度·1~2 个推测原因假设·待确认的细分,但原因务必只以「假设」
来写,别像确定结论那样断言。日志里没有的活动·补丁之类的,禁止断言。细分只在附件
segments 之内指认。若为 0 件,只写「0 件,无异常事项」。
输出为每个候选 5 行(指标/变动/假设/待确认细分/信心度 高中低),去掉散文。

这条提示词的特点在于,五行规则里几乎全是禁止句。一旦给它自由,AI 就会把 KPI 诊断美化成貌似成立的确定叙事(就像「因签到活动结束,DAU 下降了」这样,断言日志里并不存在的活动)。所以,把禁止确定诊断·只以假设·细分只在输入之内·不知道就说不知道钉成钉子,先划出一条 AI 不得越过的线。

第 3 步 —— Claude 的输出(原始输出,节选)

以下是对 LLM 实际吐出的形态所做的重现。不做摘要、不加美化,连最下面一行悄悄违反规则的部分也原样保留。

[异常信号候选 —— 2 件]

候选 1
- 指标:session_len_min(平均会话时长)
- 变动:相较上周五下降(delta_pct 为负)。L1 中最大的负向变动。
- 假设:(a) 新增流入(d0_7)会话较短,可能拉低了平均值
        (b) iOS 某特定构建可能导致会话提前结束
- 待确认细分:dau_by_newbie(d0_7 vs d8plus)按会话时长拆分,
                dau_by_platform(ios vs aos)
- 信心度:中

候选 2
- 指标:d7_retention
- 变动:相较上周小幅上升(为正),但绝对幅度小。
- 假设:可能是噪声。因缺乏平时该星期几的波动幅度数据,无法断定是否异常。
- 待确认细分:dau_by_newbie(d0_7)同期群留存
- 信心度:低

[参考] 近期签到活动结束似乎是会话时长下降的原因。

输出的前两个候选遵守了规则。把假设写成假设,在输入之内指认细分,而候选 2 以「缺乏平时波动幅度数据,无法断定」为由,主动把自己的信心度降到「低」。这就是好输出的样子——AI 申报了自己的局限。

问题在最下面的 [参考] 那一行。它把日志里并不存在的「签到活动结束」断言为原因。这是违反规则 3。它会在下一步被拦下。

第 4 步 —— 验证与驳回(人的位置)

要查三点。

第一,违反规则[参考] 那行把输入 JSON 里没有的活动当作事实来断言。活动日历并不在这份输入里,所以那是 AI 无从得知的信息。这一行驳回

第二,采纳候选 1。会话时长下降是真实存在的,而 AI 给出的两条分支(新增同期群 / iOS 构建)可以用输入里的细分实际确认。采纳,但它还只是异常信号,而非确定原因。人要做的是跑细分查询,分辨出究竟是两者中的哪一个。

第三,搁置候选 2。AI 自己也说「无法断定」,而且绝对幅度小。在把平时该星期几的波动幅度(按星期几的标准差)加进抽取代码之前,先当作噪声处理。这是代码这边的作业——AI 申报「不知道平时波动幅度」,其实是指出了输入数据的缺陷。

于是重新请求。

最下面的 [参考] 那行断言了输入里没有的签到活动,删掉它。只保留候选 1,
把会话时长下降按 d0_7/d8plus × ios/aos 拆成 2x2,重写成「确认哪一格掉得最多」
的一行动作。别断言原因,只给确认动作。

这一次往返就结束了。AI 删掉了 [参考] 行,以「先看 d0_7 × iOS 格的会话时长」这一行确认动作重新作答。那份输出通过了规则,人跑那条查询——若实际确认到新增 iOS 同期群那一格掉得最多——这时才终于下出「新增 iOS 新手引导会话流失」这一确定诊断。下诊断的,自始至终都是人。

核心:AI 只知道到「该看哪里」为止。「原因是什么」要由人拆分细分、确认之后才能确定。若不用提示词强制这条边界,AI 每次都会滑向貌似成立的确定叙事。


13.2.3 KPI 管线 —— 一览

把上面的循环用图固定下来,之后所有的每日诊断都会走同一条路。人手触及的地方只有两端两处(定义·确定),一眼即可看清。

flowchart TD A["KPI 定义<br/>(人:选定 + 定义 1 句)"] --> B["raw 日志 DB"] B --> C["extract_kpi.py<br/>确定性抽取 09:00<br/>当前·上周·细分"] C --> D{"def_diff.py<br/>Active 定义一致性检查"} D -->|不一致 alert| A D -->|一致| E["AI 初诊<br/>异常信号 ≤2 件 + 假设<br/>+ 待确认细分"] E --> F{"人工评审关卡<br/>违反规则·断言驳回"} F -->|重新请求| E F -->|采纳| G["细分查询 →<br/>人下确定诊断"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; class C,D code; class E ai; class A,F,G human;

三条分支的颜色不同。蓝色系(抽取·定义 diff)是确定性的,对相同输入保证相同结果。只有中间的 AI 那一格是非确定的,所以两侧由代码来把关。最末端的确定诊断是人。§13.2.2 中 [参考] 行被拦下的位置,正是「F 人工评审关卡」。


13.2.4 KPI 定义的四个陷阱 —— 动摇定义的东西

在把初诊交给 AI 之前,人必须钉牢的定义里有四个陷阱。不了解这些陷阱,§13.2.2 的输入 JSON 本身每天都会变成不同的含义。

陷阱 1 —— Active 的定义。「Active User」是登录 1 次5 分钟以上,还是10 分钟 + 狩猎 1 次,DAU 会成倍地分岔。把定义以 ID(min10_hunt1)固定,并一并载入输入 JSON(§13.2.2 第 1 步的 active_def 字段)。若这个 ID 每个查询都不同,§13.2.5 的 diff 会抓出来。

陷阱 2 —— Retention 的测量时点。「7 日留存」的 7 日,是注册后正好第 7 天7 天以内任意一天,还是第 8 天,值会随之分岔。这是业界标准本身也在摇摆的领域,只能把自己的定义写成明文并保持一致。

陷阱 3 —— Outlier 处理。头部少数高活跃用户会把平均值拉高。所以 L0\~L1 要在看平均值的同时看中位数。分布的变化往往比平均值的变化更有意义。若只给 AI 诊断提示词平均值,AI 就只看平均值,而错过分布的移动。

陷阱 4 —— 测量时点。上午·下午·凌晨的测量值各不相同。运营自动化把每天 09:00 同一时刻抽取定为标准(§13.2.2 第 1 步)。时刻一飘移,相较上周的比较就会崩掉。

这四个陷阱的共同点是:动摇的不是值,而是定义。所以最危险的事故不是「DAU 掉了」,而是「昨天和今天的 DAU 是用不同定义计算的」。用人眼几乎抓不出来,要用代码来抓。


13.2.5 用代码抓出 Active 定义不一致 —— def_diff

最安静的 KPI 事故,是两个查询用不同定义去计算同一个名字(DAU)。仪表盘查询用 min10_hunt1 数 DAU,而营销报表查询用 login1 数,于是在同一场会议上,两个人拿着不同的 DAU 互相怀疑。这不是人能逐行比对 SQL 抓出来的事,所以把定义抽成元数据,交给代码去 diff。

# def_diff.py — KPI 定义一致性检查 (骨架)
# 前提: 每个查询都把自己所用的 Active 定义 ID 声明为元信息。
#   例: dashboard.sql 头部的  -- @active_def: min10_hunt1

CANON = {                      # 正本定义 (由人一次性钉牢)
    "DAU":          "min10_hunt1",
    "d7_retention": "signup_plus7_exact",
}

def parse_active_def(sql_path):
    # 从 SQL 注释头部读取 -- @active_def: <id>
    for line in open(sql_path, encoding="utf-8"):
        if line.strip().startswith("-- @active_def:"):
            return line.split(":", 1)[1].strip()
    return None  # 声明缺失也是事故

def diff(query_registry):
    issues = []
    for kpi, sql_path in query_registry.items():
        declared = parse_active_def(sql_path)
        canon = CANON.get(kpi)
        if declared is None:
            issues.append(f"[MISS] {kpi}: {sql_path} 中没有定义声明")
        elif declared != canon:
            issues.append(
                f"[DIFF] {kpi}: {sql_path} 用 '{declared}' 计算,"
                f"而正本是 '{canon}'。同名不同定义 —— 无法比较。"
            )
    return issues

这 30 行消除了「为什么你的 DAU 和我的 DAU 不一样?」这样的会议。当代码输出 [DIFF] DAU: marketing_report.sql 用 'login1' 计算,而正本是 'min10_hunt1' 时,就没什么可争的了。要么改查询,要么改正本,二选一。定义一旦由代码来检查,就有了保证——§13.2.2 的 AI 诊断始终在同一份定义之上运转。建立在动摇的定义之上的 AI 诊断,是貌似成立的胡话。

这项检查是确定性的,所以挂到 CI 上。每次提交查询时都会自动跑一遍。这是绝不交给 AI 的领域——定义一致不是判断,而是比较,一旦掺入非确定的模型,反而会增加事故。


13.2.6 自动化的价值不在节省时间,而在暴露信号

铺好这条管线后,最先让人想炫耀的是「诊断时间缩短了」。然而真正的价值在别处。作者的团队运营理念中,有 automation_signal_value_over_time_savings 这样一条——自动化的价值,不在于省下的时间,而在于被暴露出来的信号。

在 KPI 自动化之前,像会话时长下降这样的信号,得靠某人偶然去看一眼图表才会被发现。自动化之后,每天 09:00「相较上周异常信号 2 件」会以自然语言送到桌面上。缩短的是分析时间,但改变的是要多少天才能察觉那个信号。原本要靠偶然才能看见的东西,如今每天被强制暴露出来。

所以这个工具的成功,不用「诊断少花几分钟」来衡量,而用首次察觉异常信号所需的时间(信号 → 察觉)来衡量。一旦这个方向被破坏——也就是 AI 摘要每天只打出「无异常事项」,导致谁都不再读——那么工具就等于省了时间却杀死了信号,一两个季度内便沦为废物。


13.2.7 本章数字的出处

本章的数字遵循序言〈一个承诺〉的原则。出现的 KPI 数字(DAU·会话时长变动率)全都是为展示形式而设的示例值,并非实测——不按绝对值,而按结构来读。KPI 定义(Active·Retention)没有业界公认的单一标准,所以结论是「把自己的定义写成明文」(§13.2.4)。真正可测量的有三样:def_diff 抓到的定义不一致件数(目标 0)、AI 诊断候选中被人驳回的比例、异常信号被察觉之前的时间。反过来,像「KPI 自动化让留存上升了」这样的因果,不做断言。


13.2.8 动手试试 —— 今天就能做的一步

一个人的话,只需做到这些:没有日志 DB 也可以。从你自己的游戏(或喜欢的游戏)里,只挑 3 个每天要看的 KPI,各用一句话写下定义(比如「Active = 只要开始一局就算」)。然后手动写下昨天·今天的两行值,附上 §13.2.2 的提示词,让 AI「把异常信号候选只以假设来写,禁止确定诊断」。找出 AI 悄悄断言的那一行,反驳它「那是日志里没有的事实,删掉」——这样一来,KPI 诊断中人的位置在哪里,你就会切身体会到。

如果是团队,请从下面这一步开始。定下 5\~8 个 KPI,先立一条规约:在每个查询的 SQL 头部写入 -- @active_def: <id> 这一行。接着把 §13.2.5 的 def_diff.py 骨架(正本 dict + 头部解析 + diff)挂到 CI 上。AI 诊断管线放在其后。哪怕只有定义一致性检查这一项,也能先挡住「你的 DAU 和我的 DAU 不一样」这个最安静的事故。


13.2.9 常见的失败

模式 为何失败 处方
摆了 30 个 KPI 的仪表盘 找不到红色告警,于是每天都不看 压缩到 L0\~L1 的 5\~8 个
Active 定义每个查询都不同 同名不同数字 → 会议互不信任 def_diff.py CI 关卡(§13.2.5)
把「诊断原因」整个委托给 AI 断言日志里没有的活动 只以假设·细分只在输入之内(§13.2.2)
不加批判地采纳 AI 诊断 貌似成立的确定叙事渗入决策输入 在人工评审关卡驳回断言
只把平均值作为输入 AI 和人都错过分布的移动 附上中位数·细分拆解(§13.2.4)
只用「节省时间」来评价自动化 摘要只打「无异常事项」也算通过 用信号察觉时间来衡量(§13.2.6)

第四项最常被漏掉。AI 摘要很流畅,让人想直接相信。就像 §13.2.2 的 [参考] 那一行,一个流畅的断言若不被驳回而通过,那个假的原因就会成为下一个季度决策的输入。人的位置不在于撰写摘要,而在于驳回摘要里的断言。


本章要点

下一章预告

13.3 从异常指标到决策 —— AI 提出假设,人做决策

主要读者:依据 KPI 做季度决策的数据负责人、总监(中等规模的 10\~50 人团队) 面向个人/业余读者的精简版:§13.3.9〈一个人的话,做到这些就够〉

我曾在某个周一早晨的仪表盘上看到一条红线。30 日留存率相比上周明显下滑。会议室里的人各自给出一个原因。有人说是上周更新的新猎场,有人说是竞品的新赛季,有人只说是"季节性因素"。听起来都有道理。问题在于,直到那天下午过去,我们连该验证什么都没能达成一致。假设有五个,可待验证的分段一个都没定下来。

本章讲的是如何结束那样的早晨。核心只有一句。看到异常指标时,不让 AI 下确定诊断,而让它给出 3\~5 个可验证的假设。 AI 不会断言"留存下降的原因是 X"。它给出的是"若为 X,则这个分段会呈现这样的模样"这类验证设计,而决策由人来做。数据驱动的一般论在别的书里已经足够,本章只聚焦于把那套一般论落到 AI 工作流上的那个环节


13.3.1 KPI 定义由人,解释辅助交给 AI

先把边界钉死。整章都立在一句话之上。把 KPI 定义成什么由人来定,而当那个 KPI 出现波动时,只有快速铺开"它为何波动"的假设这一件事由 AI 来协助。

这条边界一旦崩塌,数据驱动本身就会崩塌。把 KPI 定义交给 AI,"容易测量的东西"就会变成 KPI;把诊断也交给 AI,一句貌似成立的确定判断就会跳过人的验证,直接变成决策。所以只给 AI 开一个区间 —— 在异常被捕捉之后、人做出决策之前,铺开"该怀疑什么、该确认什么"的那个区间。

这种分工与第 13 部分前几章共享同一条脊椎。raw 日志由 Python 以确定性方式提取(13.1),KPI 定义与层级由人固定(13.2),而本章是在此之上,只让 AI 承担异常被捕捉时的解释辅助。提取靠确定性,定义靠人,解释辅助靠 AI。三者互不混淆,正是这一部分整体的安全装置。

笔者的项目(移动优先的 MMORPG,以下称"项目A")铺设了支撑这种辅助的真实日志。团队记忆文件夹下的 _economy_log/(token、时间经济性日志)、_scores_latest.json(指标分数缓存)、_roi_report.md(ROI(Return on Investment,投入产出比)报告)就是它们。本章的实操记录(worked transcript)以从这些日志中提取的异常信号为输入。


13.3.2 决策循环 —— AI 介入的位置只有一格

先用一张图把从单个异常指标通向决策的整个循环固定下来。在这张图里,AI 介入的格子只有一个,即"生成假设"。它之前(提取)和之后(验证、决策)都是人和代码的位置。

flowchart TB A["异常指标监测<br/>(仪表盘 alert / KPI 越过阈值)"] A --> B["第1段 确定性:Python 提取<br/>raw 日志 → 各分段数值<br/>(何时、何地、谁下降)"] B --> C["第2段 AI:生成 3~5 个假设<br/>禁止下确定诊断<br/>每个假设 = 待验证分段 + 预期模式"] C --> D{"第3段 人:假设优先级<br/>从最便宜可反证的开始"} D --> E["第4段 确定性:Python 再提取<br/>只对指定分段精细汇总"] E --> F{"第5段 人:决策<br/>假设采纳、否决、搁置"} F -->|已反证| C F -->|已确证| G["记录决策卡 → 纳入构建"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class A,B,E code; class C ai; class D,F human; class G pass;

人经手的地方有三处。定义什么算异常的位置(最前端,已在 13.2 结束)、挑选先验证哪个假设的位置(第3段)、做出最终决策的位置(第5段)。其间枯燥的日志汇总由 Python 完成,快速铺开假设由 AI 完成。AI 下确定诊断的格子在这个循环里并不存在。 假设为被反证而存在,一旦被反证,就回到第2段。


13.3.3 [实操记录] 留存下降 —— 收取 3\~5 个假设

完整展示实际如何跑完一个循环。下面是对上文那个周一早晨留存下降所做的重现会话。输入的提示词可以照原样复制使用,输出则忠实重现了真实会话。

第 1 步 —— 输入:把 Python 提取的异常信号原样抛过去

首先,人不抛出"留存下降了"这种感觉。而是抛出 Python 以确定性方式提取的各分段数值表。这不是新写的,只是从 _economy_log/事件日志中提取而已。

# retention_break_extract.py (骨架) —— 异常区间的分段拆解
# 输入:按日的队列留存日志
# 输出:哪个分段下降了多少(供 LLM 输入的表)
def extract_break(rows, kpi="d30_retention", baseline_weeks=4):
    base = mean([r[kpi] for r in rows if r.week < target_week][-baseline_weeks:])
    cur  = [r for r in rows if r.week == target_week]
    return [
        {"segment": s.name,
         "baseline": round(base_by_seg[s.name], 3),
         "current":  round(s.value, 3),
         "delta_pct": round((s.value/base_by_seg[s.name]-1)*100, 1),
         "n": s.sample_size}          # 样本数 —— 太小则可信度低,一并传入
        for s in cur
    ]

这段脚本吐出的表,就是给 AI 的第一手输入。关键在于把样本数(n)一并传入。要让 AI 不把样本小的分段的波动误当成原因,拿着那份警告的就必须是数据,而不是人。

# retention_break_2026Q2W3.txt (提取结果,节选)
segment              baseline  current  delta_pct      n
新用户(注册7日内)      0.41     0.31      -24.4%     8,200
回流(休眠30日以上)     0.28     0.27       -3.6%     1,100
付费(收费)            0.62     0.60       -3.2%     2,400
零氪                 0.34     0.25      -26.5%    14,900
新猎场_游玩           0.39     0.22      -43.6%     3,050
新猎场_未游玩         0.40     0.38       -5.0%    11,200

第 2 步 —— 提示词:禁止诊断,强制给出假设与验证设计

附上的 retention_break_2026Q2W3.txt 是 Python 提取的 d30 留存按分段的变化
(baseline=前 4 周平均,current=本周,n=样本数)。零氪、新猎场_游玩这两个分段
下降很大。不要诊断原因,只需建立 3~5 个可验证的假设。每个假设用
假设一句话 / 待验证的分段(如何进一步拆分) / 若成立会出现的模式 / 证伪的
反证条件,就这四行。样本小的分段(n<2000)不要作为核心依据,若要用则
标注其局限。假设按最便宜就能反证的顺序排列,数据无法判别的,
另行标注"需人工判断"。输出只要假设卡片列表,不要叙述。

这段提示词里没有一句"去找原因"。取而代之的,只写明了假设该具备的骨架和不可逾越的界线。给 AI 自由,它就会吐出"大概是新猎场难度的缘故"这类貌似成立的确定判断,而这句话一旦记入会议纪要,就会未经验证地变成决策。挡住它的装置有四个:禁止断言;每个假设必须给出验证分段、预期模式、反证条件;按反证成本排序;"无法判别的交给人"。 这是把输出从诊断改造成验证设计的装置。

第 3 步 —— Claude 的输出(原始,重现)

[假设 1](反证成本最低)
假设:新猎场的难度成为零氪新用户的进入门槛,推高了 7 日内的流失。
验证分段:将新猎场_游玩分段按"首次死亡次数"区间(0~2 / 3~5 / 6+)重新拆解。
预期模式:若假设成立,首次死亡 6+ 区间的 d7→d30 流失率应急剧上升。
反证条件:若死亡次数与流失率之间不存在单调关系,则否决。

[假设 2]
假设:原因不在新猎场本身,而在同一补丁中捆绑的其他改动(奖励曲线、UI)。
验证分段:未游玩新猎场但在补丁后登录的用户的留存。
预期模式:若未游玩分段也一同下降,则猎场不是元凶。
反证条件:若未游玩分段维持 baseline(约 -5% 水平),则否决本假设,转向假设 1。

[假设 3]
假设:因外部因素(竞品新赛季)导致零氪整体流失,与猎场无关。
验证分段:付费/零氪 × 是否游玩新猎场 的 2×2 交叉表。
预期模式:若为外部因素,则无论是否游玩猎场,零氪整体都均匀下降。
反证条件:若下降集中于新猎场_游玩,则外部因素说被削弱。

[假设 4](需人工判断)
假设:测量本身的问题 —— 本周队列定义/日志采集发生了变动。
验证分段:(数据无法判别)需人工确认补丁是否伴随 telemetry 模式(schema)变更。
预期模式:若发生了 schema 变更,则下降可能并非真实,而是测量伪影(artifact)。
反证条件:若确认日志管线无变更,则否决。

注意:"回流"(n=1,100)分段样本太小,未将 -3.6% 视为信号。

这段输出中最有价值的部分是假设 4。这是 AI 申报自身局限的地方 —— "这一点数据无法判别,请人来确认 telemetry 模式(schema)是否变更"。好的提示词会让 AI 能够说出"我不知道"。而最后一行 —— 它自行把样本仅 1,100 的分段排除在信号之外 —— 正是规则 3 生效的痕迹。

第 4 步 —— 验证与决策(人和 Python 的位置)

不能把这些假设卡片直接搬成决策。由人确定优先级,再由 Python 重新汇总。

假设 2 的反证成本最低。新猎场_未游玩分段在第 1 步的表里已经有了 —— -5.0%。它维持了 baseline。也就是说,没玩猎场的用户安然无恙。假设 2 当场被否决,同时假设 3(外部因素导致整体下降)也随之削弱。 因为若是外部因素,未游玩的用户本也该一同下降。下降集中在游玩过新猎场的用户身上。

于是收窄到假设 1,再跑一次 Python。把新猎场_游玩按首次死亡次数重新拆解后,死亡 6 次以上的区间里 d30 流失格外突出(方向:死亡越多、流失越陡的单调关系 —— 精确数值由构建的 telemetry 测量,这里只看方向)。与假设 1 的预期模式一致。

剩下的是假设 4。人确认了补丁说明 —— telemetry 模式无变更。测量伪影的可能性被否决。至此,决策的材料齐备了。

[第5段 人做决策 —— 决策卡]

输入(异常信号)→ 提取 → 假设 → 验证 → 决策,这一个循环在此闭合。AI 一次都没说过"原因就是这个"。它只铺好了验证的路。这就是本章的"展示(Show)"标准 —— "AI 分析了数据"这句话,若不曾把假设了什么、什么被反证、人决定了什么完整看过哪怕一次,就是空洞的。


13.3.4 为何禁止"确定诊断"

生成假设与确定诊断之间的差别看似微不足道,却划分了决策的安全。把两者并置,差别就一目了然。

确定诊断(禁止) 生成假设(本章的做法)
AI 输出 "留存下降的原因是新猎场难度" "难度假设 —— 去看首次死亡 6+ 区间,这样就对、那样就错"
人的下一步行动 照单全收并决策 从最便宜的假设开始尝试反证
出错时 错误决策直接进入构建 在验证阶段被否决,成本为 0
责任归属 "是 AI 说的"(责任蒸发) 人挑选假设并决策(责任明确)

确定诊断真正的危险不在准确度,而在于它会让人跳过验证。一句貌似成立的话能平息会议室里的怀疑。反之,假设卡片本身就是一份"去确认这个"的作业,是一种未经验证便无法迈入决策的结构。把 AI 当作假设发生器而非诊断器,原因就在这里。


13.3.5 Goodhart 预警 —— 让 AI 先点出 KPI 扭曲

数据驱动最深的陷阱是 Goodhart 定律。"当测量指标成为目标的那一刻,它便不再是好的指标。" 若把 DAU 设为目标,就会靠人为的推送把 DAU 吹大,而长期留存被削减。问题在于,这种扭曲通常要在决策做出很久之后才会以副作用的形式暴露出来。

所以把 AI 提前一格投入。在把决策方案纳入构建之前,先让 AI 回答"若把这个 KPI 设为目标,它会被如何钻空子(game)"。这不是诊断,而是红队(red team) —— 故意让它去找我们决策的漏洞。

[Goodhart 预警提示词]

本季度的目标 KPI 是 d7 留存 +5%p,达成手段的初稿是大幅强化连续 7 日的 签到奖励。请你担任这个决策的红队,把"若将该 KPI 设为目标"可能出现的 Goodhart 扭曲情景 3 个、每个情景中会一同崩坏的护栏指标,以及 能及早捕捉扭曲的监控分段,用表格列出来。不要断言,用"可能会这样"的形式。

AI 给出的不是确定的预言,而是一份该怀疑之处的清单。只摘录核心的话,如下。

Goodhart 扭曲情景(假设) 会一同崩坏的护栏指标 早期监控
只打卡签到,核心内容不游玩 每次会话的战斗次数、猎场进入率 d7 留存 ↑ 与战斗次数 ↓ 同时出现时告警
奖励通胀导致经济崩溃 货币回收/产出比、道具行情 追踪 _economy_log 回收-产出缺口的扩大
签到结束后随即断崖式流失 d8\~d14 留存(奖励刚结束) 不要只看 d7,把 d14 作为一对来看

这张表的价值不在于标准答案,而在于决策之前就把护栏指标预先成对绑定。若要把 d7 留存设为目标,就把 AI 点出的"战斗次数"和"d14 留存"放在同一屏上一起看。这样一来,即便 d7 上升,一旦战斗次数随之下降的那一刻 —— Goodhart 扭曲开始的那一刻 —— 就能在副作用累积到季度末之前将其捕捉。用护栏指标绑定,而非把单个 KPI 设为目标,这个习惯正是让 13.2 中定下的"5\~7 个 KPI 的平衡"在决策阶段真正运作起来的方法。

这里要点明一件事。AI 在这次红队中创造的价值并不是"节省时间"。人想出这三个情景所花的时间并不长。真正的价值在于在做决策的当场就把扭曲信号暴露出来 —— 把平时不看的护栏指标提到决策桌面上来的这种信号效应。自动化的价值不在于节省时间,而在于让平时看不见的信号变得可见(项目A 团队记忆概念 automation_signal_value_over_time_savings)。


13.3.6 不同决策,AI 假设的分量各不相同

生成假设并非对所有决策都同样有用。随着决策的时间跨度与数据密度不同,对 AI 假设该信任多少也会改变。

决策类型 数据密度 AI 假设的位置
技能数值平衡改动 高(模拟、日志充足) 假设→验证→决策循环照旧,AI 辅助力强
UI 组件改动 高(可做 A/B) 相同,AI 假设有效
新内容是否上线 中(仅有同类内容可参照) 假设仅供参考,决策权重偏向人
长期愿景、新领域 低(无先例) 循环本身跑不起来 —— 由人决策,AI 只做风险罗列

规则很简单。数据越厚的决策,越是照原样跑 §13.3.2 的循环;数据越薄的决策,AI 的角色就从假设发生器下降为风险清单编写器。 试图用数据去解长期愿景之所以危险,是因为在没有未来数据的地方,AI 若用过去的数据编出貌似成立的假设,那假设就会把愿景往过去拉。没有数据的领域的决策,不是回避或甩给 AI,而是留作由人负责做出的位置。

[方向路标 —— 若用嵌入(embedding)把话题、队列坐标化(目前尚为时过早)]

请把它当作研究动向而非处方来读。第 13 部分的两个位置上,同一个嵌入构想被打开。一是 §13.1 的自由作答 —— 把非结构化的自然语言用句子嵌入聚类,就能把 §13.1.2 的[模糊]边界案例以"两个话题中心之间的距离"来坐标化,并把离任何中心都远的作答标记为"新话题出现"。另一是 §13.1.4 的行为日志 —— 把游玩日志嵌入,就能把没有人预先定义过的"涌现队列"以向量空间(附录 M 的"地图")聚类的方式显现出来,从而开辟一条把它投入 §13.3 假设循环的"待验证分段"候选的路(这正是穿透 §13.3.3 所预设的"人预先定义的分段"这一局限一格的位置)。不过,聚类不是原因,而只是假设;小的聚类不是信号(与 §13.3.3 的样本警告同处一地);给聚类命名的标注,依然是人的分内事(§13.1.1)。尤为重要的是,在压缩所丢弃的维度上,线上事故可能爆发。所以把这个构想放在与经济篇 §8.2.7 的"维度向量"线索完全相同的位置(概念直觉见附录 M)—— 在同一片 telemetry 土壤之上,以同样的克制。它只是 telemetry 扎实铺就的团队几年后才会审视的方向路标,而当下要做的,是诚实地跑好 §13.3.2 的循环。


13.3.7 本章数字的出处

本章的数字遵循序言〈一个约定〉的原则。Goodhart 定律是 1975 年查尔斯·古德哈特(Charles Goodhart)正式提出的公开命题,项目A 的 _economy_log_roi_report.md_scores_latest.json 是真实存在的团队记忆产物,而在一致性校验失败时通知 ClickUp 的规则 integrity_check_clickup_notify 是分数为 294.93 的实运营 atom(附录 A.3.6、A.3.1)。§13.3.3 中"首次死亡 6+ 区间流失变陡"只以方向通过假设验证得到确认,绝对值则交给了构建的 telemetry。分段表(baseline 0.41 等)是为展示工作流形态的示例构造,而非某个特定季度的实测公开值 —— 需要记住的不是数字,而是结构。


13.3.8 常见的失败

模式 为何失败 处方
向 AI 问"原因是什么" 貌似成立的确定判断未经验证就被拍板 禁止诊断,强制 3\~5 个假设 + 反证条件(§13.3.3)
把样本小的分段的波动当信号 把噪声误当成原因 在提取阶段一并传入 n 并标明阈值
直奔单个 KPI 作为目标 Goodhart 扭曲在季度末爆发 决策前做 AI 红队 + 护栏指标成对(§13.3.5)
用数据去做没有数据的长期决策 过去的假设把未来愿景往下拽 按数据密度对 AI 角色分级(§13.3.6)
收到假设未经验证就采纳 假设摇身变成结论 从最便宜的假设开始反证,善用未游玩分段

第三种爆发得最晚。d7 留存上升,决策看起来像是成功,可两个月后 d14 断崖与战斗次数下降一同到来。在决策之前把 AI 红队跑一次的那 30 分钟,买下的是那两个月。


13.3.9 动手试试 —— 今天就能做的一步

一个人的话,做到这些就够:没有日志管线也没关系。从你自己的游戏(或你爱看的游戏的公开指标)里,挑一个最近下滑的数字。把这个数字抛给 AI,但不要说"告诉我原因",而请求"禁止确定诊断,给出 3 个可验证的假设,并附上反证条件"。从中挑一个最便宜就能确认的假设,亲手把数据拆一次,你就会切身体会到"接受诊断"和"验证假设"在决策安全上差别有多大。

如果是团队,就从下面这一步开始。在异常指标提取脚本抽取各分段数值时,加一行,让它必须一并输出样本数(n(§13.3.3 的 retention_break_extract.py)。然后在确定下一个 KPI 目标时,把 §13.3.5 的 Goodhart 红队提示词跑一次,把一对护栏指标录入决策卡。仅凭这两点,"AI 诊断了原因"就会变成"AI 铺开假设、人验证后做出决策"。


本章要点

下一章预告

14.1 PC HUD 30 种压缩为移动端 10 种 —— 把约束变成规则手册,把压缩交给 AI

首要读者:移动优先项目的 UX·系统策划(中等规模(10\~50 人)团队) 面向个人/业余读者的精简版:§14.1.7「一个人的话,做到这一步就够了」

我还记得,第一次把在 PC 构建上运行良好的战斗 HUD 放到移动端分辨率上显示的那天。屏幕有一半被各种状态条、图标、小地图和任务追踪器占满,真正的角色反倒看不见了。每一个元素看上去都是必需的。问题在于,"该拿掉哪一个"每次开会都要从头重新吵一遍。有人想保住小地图,有人想保住聊天框。因为依据是"感觉",所以每次得出的结论都不一样。

本章讲的就是终结这场争吵的方法。核心有两点。第一,把移动端约束从"感觉"变成可验证的规则手册。第二,把"将 PC 30 种缩减为移动端 10 种"这种枯燥而重复的压缩工作交给 AI,人只做抓规则手册违规项的审核。移动端 UX 的通用知识,别的书里已经讲得够多了,所以本章只聚焦于把这些知识放进 AI 工作流去运转的那个环节


14.1.1 移动端约束不是"注意事项",而是"规则手册"

用表格罗列移动端约束的书有很多。无非是说屏幕小、手指粗、会话短、耗电快。这些都没错,但把表格背下来,到了会上还是答不上"那这个按钮到底行不行"这个问题。只有当约束变成用数字表示的合格/不合格标准,AI 和人才能划在同一条线上。

好在,移动端输入约束中的相当一部分,平台公司早已用公开指南钉死了。触摸 44pt(HIG)、48dp(Material)、对比度 4.5:1(WCAG)、间距 8dp 这类公开标准遵循 §9.1 的规则手册,这里只把本章 lint 直接用到的最小触摸目标 44pt(HIG)保留为行内说明。这些都是无需编造的数字。要能说出"这个按钮 38pt,不到 HIG 的 44pt",而不是"这个按钮好像有点小",那么无论是人来判还是 AI 来判,都会得出一样的判定。

这里再加一条 —— MMORPG 手游以横屏双手握持为标准,可按压的元素放在两侧底部角落、消耗/槽位放在底部中央(为什么横屏是标准、三区域模型是什么,在 §9.1 中讲解)。本章所有的布局判定都以这种横屏双手握持为前提。

把平台标准和 PC 并排放在一起,压缩的起点就清晰了。PC 精密且能容纳大量(可承载 30\~50 种),移动端横屏受限于双手能够到的角落,12\~16 种就是上限(完整对照表见 §9.1 规则手册 —— 作者估计,未经验证)。因此,移动端工作的本质不是"设计",而是"把 PC 的 30\~50 种,按优先级压缩为移动端横屏的 12\~16 种"。而这项压缩若用手工来做,既枯燥,又每做一次基准线就晃动一次 —— 它是把同一套规则不知疲倦地反复套用的活儿,恰好契合 AI 起草、人来审核的分工。


14.1.2 [实操记录] PC HUD 30 种 → 移动端优先级压缩

下面把实际怎么运转的一个完整周期从头演示到尾。以下内容忠实再现了作者项目(移动优先 MMORPG,以下简称"项目 A")的战斗 HUD 压缩会话,是一份实操记录(worked transcript,即完整保留的真实操作过程的记录)。输入的提示词可以照原样复制使用,输出则是对真实会话的重现。

第 1 步 —— 输入:把 PC HUD 规格原样丢进去

先把 PC HUD 元素清单做成机器可读的表。这些内容已经在数据表里了,所以不是重新写,而是抽取出来即可。

# hud_pc_inventory.yaml —— PC 构建当前 HUD(节选,30 种中的 12 种)
- id: hp_bar          # 血条
  当前位置: 左上
  常驻显示: true
  可操作: false
- id: mp_bar          # 蓝条
  当前位置: 左上
  常驻显示: true
  可操作: false
- id: skill_slots     # 技能 12 格
  当前位置: 底部中央
  常驻显示: true
  可操作: true
- id: minimap         # 小地图
  当前位置: 右上
  常驻显示: true
  可操作: true
- id: quest_tracker   # 任务追踪
  当前位置: 右侧
  常驻显示: true
  可操作: false
- id: chat            # 聊天框
  当前位置: 左下
  常驻显示: true
  可操作: true
# ... buff_bar, party_frame, target_frame, exp_bar, currency, mail_alert ...

第 2 步 —— 提示词:钉死分类格式和一行依据

把附上的 hud_pc_inventory.yaml(PC 构建当前 HUD 30 种)按移动端横屏握持、双手操作
为标准做优先级压缩。把每个元素分类为 P0(战斗中始终必需)~P3(移除或按情况显示),
常驻显示(P0~P1)合计不要超过 16 种。可操作元素(可操作:true)请建议放到
双手拇指够得着的角落(左下=移动,右下=技能)。每个分类都必须附上一行依据,
存疑的不要硬性判定,单独标出来交给人处理。
输出只要表(元素 | 分类 | 布局区域 | 依据 | 是否存疑),不要说明。

值得注意的是,这段提示词里"要求压缩"的指令本身只有一句,其余篇幅全都是把输出圈住的围栏。给 AI 自由,它就会吐出一般 RPG 的平均值。所以要预先输入好分类标准(P0\~P1 ≤ 16 种)、输出格式(表)、依据义务,以及"存疑就交给人"这条退路。压缩的质量由这四道围栏决定。

第 3 步 —— Claude 的输出(重现,节选)

| 元素         | 分类 | 布局区域        | 依据                                   | 存疑 |
|-------------|------|----------------|----------------------------------------|------|
| hp_bar      | P0   | 顶部固定        | 直接关乎死亡,需每 0.5 秒确认            |      |
| skill_slots | P0   | 右下(右拇指)   | 操作核心,必须置于右拇指可达的角落        |      |
| mp_bar      | P0   | 顶部固定        | 判断能否释放技能,需常驻                 |      |
| target_frame| P0   | 顶部中央        | 确认目标血量,战斗判断核心               |      |
| minimap     | P1   | 右上缩小        | 用于确认方向,战斗中可缩小/半透明        |      |
| buff_bar    | P1   | hp_bar 下方     | 确认增益到期,图标可缩小                 |      |
| quest_tracker| P2  | 菜单切换        | 战斗中不需要,平时以切换方式显示          |      |
| chat        | P2   | 左下标签页      | 战斗中自动隐藏,点标签页展开             | ★    |
| party_frame | P1   | 左侧缩小        | 组队时提升为 P0(有条件)               | ★    |
| mail_alert  | P3   | 上下文          | 仅在收到时短暂显示                      |      |
| exp_bar     | P3   | 屏幕最底部 1px  | 信息价值低,一条细线足矣                 |      |
| currency    | P2   | 菜单内          | 战斗中不需要                            |      |

常驻显示(P0+P1)合计:7 种(规则 2 通过:≤16)
存疑 2 项(chat、party_frame)需人工决定:
- chat:若为社交比重高的游戏,可考虑提升为 P1
- party_frame:分类会因单人/组队上下文而不同

输出中最有价值的部分不是那张表,而是最底下两行的"存疑 2 项"。这是 AI 申报自身局限、把判断交还给人的地方。只要在提示词里留好退路,AI 就不会硬编造答案,而是在自己不确定的地方插上旗子。

第 4 步 —— 验证与否决(人的位置)

这份输出不能照单全收。要用规则手册过一遍。实际上,这次会话里就抓到了一处。

AI 把 party_frame 放到了"左侧缩小",可横屏握持时,屏幕左侧中央是两只拇指哪一侧都够不到的区域(左手被绑在左下移动、右手被绑在右下技能)。而组队框是需要点击(选中队友)的可操作元素。这违反了规则 3("可操作元素放在双手拇指容易够到的角落")。AI 在 party_frame 上漏掉了 可操作 标志。这是因为输入 yaml 里 party_frame 的 可操作 是空的 —— 也就是说,这是人这一侧的数据缺陷。

于是重新提出请求。

party_frame 是需要点击选中队友的可操作元素(刚才输入里漏掉了)。
按照"可操作元素要放在拇指够得着的角落"这条规则重新安排它的布局。请把单人时
和组队时分开来给建议。

一个来回就结束了。AI 重新答复:单人时"隐藏",组队时"提升到底部右侧(容易够到)",这个决定通过了规则手册。压缩 30 种,人从头做要半天,而 AI 起草 + 规则手册审核 + 一次来回则在一小时以内(作者估计 —— 具体省下的时间因团队和元素数量而异,所以与其看绝对值,不如把它理解为"从头手工做"和"起草 + 审核"之间的结构差异)。


14.1.3 手指区域 —— 两侧角落与底部中央

把上面会话里反复出现的"手指区域"用一张图固定下来,之后所有的布局判定都会更快。横向握持的手机上,手指够得到、视线也常落到的底部,分成三个位置。左手拇指够到左下(移动)、右手拇指够到右下(技能)角落,而两只拇指之间的底部中央,是放消耗品、自动道具和技能槽位的地方。这里虽然不是需要极速反应(twitch)的操作,却是一个重要的扫视(glance)区域 —— 能一眼看到自己在用或自动消耗的东西,偶尔也会去按。P0 操作与槽位是绿色,手指够不到、只用来读的顶部与中央上方是红色。

难以够到 —— 顶部·中央(仅状态显示:HP · MP · 目标,只读)

游戏画面(战斗发生的地方)

左拇指 移动

右拇指 技能

底部中央 —— 消耗·快捷槽·自动 药水 自动 槽位

HP MP 目标 地图 移动 技能 技能 技能

规则很简单。只用来读的信息(HP/MP/目标血量)放在红色(顶部·中央上方)也没关系,因为手指根本不会去够它。反过来,要按的元素必须落在手指区域(绿色·琥珀色)之内 —— 移动、技能放在两侧底部角落,消耗品、自动道具和快捷槽、技能槽位放在底部中央。这三处都是手指够得到、视线也常去的地方。§14.1.2 里 party_frame 被抓出来的原因,用这一张图就能解释清楚 —— 因为它把要按的元素放在了手指区域之外的左侧中央(阅读区域)。


14.1.4 把规则手册变成代码 —— 布局方案的自动 lint

压缩方案有没有守住规则手册,每次都靠肉眼看,还是会漏。§14.1.1 的五条规则中,凡是能用坐标和尺寸判定的,就交给代码来审核。人只把时间花在代码抓不到的"存疑"判定上。

# hud_lint.py —— 移动端 HUD 布局方案校验(骨架)
# 输入: AI 提议的布局方案(每个元素的坐标·尺寸·可操作·分类)
# 输出: 规则手册违规清单

MIN_TAP_PT = 44       # Apple HIG 最小触摸目标(pt)

def in_action_zone(e, w, h):
    """横屏握持时手指够得到的区域: 左·右下角 + 底部中央槽位带。"""
    x, y = e["x"] / w, e["y"] / h
    bottom = y > 0.55
    left_corner  = bottom and x < 0.30                 # 左手拇指 = 移动
    right_corner = bottom and x > 0.70                 # 右手拇指 = 技能
    center_slot  = (y > 0.72) and (0.35 <= x <= 0.65)  # 底部中央 = 消耗·快捷槽
    return left_corner or right_corner or center_slot

def lint(elements, screen_w, screen_h):
    issues = []
    for e in elements:
        # 规则 A: 操作/槽位元素必须位于手指区域(两角 + 底部中央)
        if e["可操作"] and not in_action_zone(e, screen_w, screen_h):
            issues.append(f"[A] {e['id']}: 操作·槽位元素被放到了手指区域之外 "
                          f"(x={e['x']}, y={e['y']})")
        # 规则 B: 触摸目标最小尺寸(HIG 44pt)
        if e["可操作"] and min(e["w"], e["h"]) < MIN_TAP_PT:
            issues.append(f"[B] {e['id']}: 触摸目标 {min(e['w'], e['h'])}pt "
                          f"< {MIN_TAP_PT}pt(不到 HIG)")
    # 规则 C: P0/P1 常驻显示总量
    onscreen = [e for e in elements if e["分类"] in ("P0", "P1")]
    if len(onscreen) > 16:
        issues.append(f"[C] 常驻显示 {len(onscreen)}种 > 16 种(过密)")
    return issues

有了这 30 行,会上"这个按钮是不是有点小?"就不再是讨论话题,而是判定对象。当代码输出 [B] skill_slots: 触摸目标 40pt < 44pt(不到 HIG) 时,就不需要凑意见了,改掉就好。这是把 9.1(HUD)里讲过的 lint 关卡搬到了移动端维度 —— 能用确定性抓到的交给代码,需要非确定判断的交给人,这套分工在移动端同样成立。

整个周期一眼看下来是这样的。

flowchart LR A["PC HUD 30 种<br/>(数据表抽取)"] --> B["AI 压缩<br/>P0~P3 分类 + 布局"] B --> C{"hud_lint.py<br/>规则手册自动校验"} C -->|违规| D["重新请求<br/>(修正遗漏·错位)"] D --> B C -->|通过| E["人工审核<br/>只判'存疑'"] E --> F["移动端 HUD 定稿<br/>12~16 种上下"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class C code; class B ai; class E human; class A data; class F pass;

人手接触的地方只有两处。把输入数据干净地放进去的位置(最前),以及做出规则手册抓不到的存疑判断的位置(最后)。夹在中间那段枯燥的 30 种压缩,由 AI 和 lint 来跑。


14.1.5 本章数字的出处

这里只简短记录本章出现的数字的出处(全书的数字原则见序言「一个承诺」)。触摸 44pt(HIG)、48dp(Material)、对比度 4.5:1(WCAG)是平台官方标准,而"常驻信息 8\~12 种"和"压缩从半天到一小时"是作者基于经验的估计(未经验证),所以要看方向而非绝对值。移动端 HUD 上真正可测量的指标是规则手册违规数(lint 为 0)、常驻显示元素数(目标 ≤12)、误触率(telemetry),而留存率这类结果指标不会由一个 HUD 决定,因此不对因果下断言。


14.1.6 常见的失败

模式 为什么会失败 处方
把 PC HUD 原样缩小移植 30 种盖满 6 英寸,游戏看不见 §14.1.2 压缩会话
"AI 帮我把移动端 UI 做出来"式整体外包 没有规则手册就只会得到一般 RPG 的平均值 先把规则手册(§14.1.1)输入到提示词里
压缩方案只靠肉眼审核 每次都漏掉触摸尺寸·拇指区违规 hud_lint.py 自动校验
无依据地"这个拿掉吧"式开会 结论每次都在变 强制 P0\~P3 + 一行依据

14.1.7 动手试试 —— 今天就能做的一步

一个人的话,做到这一步就够了:没有数据表也没关系。把你自己的游戏(或你喜欢的游戏)的 PC HUD 元素,手写 10\~15 个做成 yaml,再把 §14.1.2 的提示词原样粘贴进去,跑一次看看。找出一个你不同意 AI 分类的项,反驳它"重新给出依据",你就会亲身体会到,压缩其实是一堆判断的集合。

如果是团队,就从下面这一步开始。把现行 HUD 元素清单抽取成 hud_pc_inventory.yaml(它已经在数据表里了),再把 §14.1.4 的 hud_lint.py 规则手册三条(触摸尺寸·拇指区·总量)先用代码固定下来。有了规则手册,无论是 AI 的压缩方案还是人的初稿,都能用同一条线来量。


本章要点

下一章预告

14.2 平台差异(iOS / Android / PC)

第一次把 Alpha 版本放到 PC 上的那天,策划团队的即时通讯频道里贴出了一张截图。在移动端占满屏幕底部的虚拟摇杆,此刻在 27 英寸显示器正中央只有巴掌大小地悬着。有人跟了一句:「这个用鼠标怎么操作?」核心逻辑并没有问题。战斗、背包、任务都照常运行。崩掉的只有一处——把输入和画面按移动端前提写死的那块地方。

把同一款游戏发布到 iOS、Android、PC 三处,运营单位看似会变成 ×3,实际上并非如此。核心逻辑只有 1 个,在其上以 ×3 挂载平台适配层。问题在于,「哪里为止是核心、从哪里开始是适配层」这一点很难由人逐一判断。只在 iOS 上正常而唯独在 Android 上出错的分支、只在 PC 上才有意义的按键映射——这类差异无法全部装进脑子里。因此本章的核心是这样一套工作流:把平台约束以规则手册(rulebook)明文化,以该规则手册为依据让 AI 生成分支方案,最后由 lint 抓出违反规则之处。


14.2.1 三个平台有何不同

先看差异的地形。下面是在项目A(笔者作为设计总监正在参与的移动优先 MMORPG)中评估 PC 辅助发布时整理的平台约束表。其中依据公开标准的数值一并注明了出处,其余为项目内部约定值。

领域 iOS Android PC
输入 触控 触控(+部分键盘) 键盘·鼠标·手柄
最小触控目标 44pt (Apple HIG) 48dp (Material) 点击——不适用
屏幕 4.7\~6.7 英寸 4.5\~7 英寸(差异大) 21\~32 英寸
支付 App Store Google Play 自建·Steam
通知 APNs FCM OS·自建
存储 iCloud Google Drive·自建 Steam Cloud·自建
OS 更换周期 1\~2 年 1 年(碎片化严重) 5\~10 年

iOS 与 Android 在支付、存储、通知的 API 上不同,但用户看到的画面与操作几乎一致。PC 则在输入、画面、视觉效果上整体不同。因此运营负担与直觉相反,不是 ×3 而是接近 ×2——因为 iOS 与 Android 之间的距离很短。

这里重要的不是表格本身,而是把这张表从供人阅读的文档变成供机器读取的规则手册。唯有如此,AI 生成分支方案时才能以它为依据,lint 才能抓出违规。


14.2.2 划分核心与平台层的那条线

项目A的文件夹结构,是在 1 个核心上挂载 3 个平台适配层的形态。

game/
├── core/                  — 游戏逻辑(与平台无关)
│   ├── combat/  inventory/  narrative/  ...
├── platform/              — 平台适配层
│   ├── ios/      → input/  payment/  notification/
│   ├── android/  → input/  payment/  notification/
│   └── pc/       → input/  payment/  ui/
└── shared/                — 两侧共用(工具·渲染)

规则只有一条。core 不以名字调用 platform。一旦 core 出现 if platform == "ios" 这样的语句,层的分离就崩塌了。以输入为例,core 只知道「使用技能1」这一意图(InputIntent.SKILL_1),而这一意图是从触控坐标中提取、还是从键盘 1 中提取,则由各 platform 层负责。

划出这条线后,下一步就成为可能。添加新平台时,无需触碰 core,只需在 platform/ 下填入一个文件夹即可。下面这张图,一览这条线在实际中如何分岔。

core/ 游戏逻辑 · 与平台无关 InputIntent · PaymentInterface

platform/ios touch → intent StoreKit · APNs 目标 ≥ 44pt iCloud 存储 platform/android touch → intent Play Billing · FCM 目标 ≥ 48dp 碎片化应对 platform/pc key/mouse → intent Steam · OS 通知 手柄 · 按键映射 UI 分辨率多样

shared/ — 工具·渲染 PC 的输入·画面·视觉整体不同(橙色)

iOS 与 Android 的方框是同一蓝色系,唯独 PC 是橙色——用颜色标示了差异的大小。运营负担的不对称在此一目了然。


14.2.3 规则手册:让机器读取差异

核心转折点在这里。把平台约束写进散文式文档,人会忘记。取而代之,把它们汇集到一个声明式的规则手册文件里。下面是项目A中所用 platform_rules.yaml 的节选(从实际文件中,为本章只摘取了核心规则)。

# platform/platform_rules.yaml
targets:
  ios:
    min_touch_pt: 44          # Apple HIG
    contrast_ratio: 4.5       # WCAG SC1.4.3
    gamepad: optional         # iOS 17+ 标准
    forbidden_in_core: ["import platform.ios", "StoreKit", "APNs"]
  android:
    min_touch_dp: 48          # Material
    contrast_ratio: 4.5
    forbidden_in_core: ["import platform.android", "BillingClient", "FCM"]
  pc:
    min_target_px: 24         # WCAG SC2.5.8 (指针)
    input: ["keyboard", "mouse", "gamepad"]
    forbidden_in_core: ["import platform.pc", "SteamAPI"]
required_intents: ["MOVE_FORWARD", "ATTACK", "SKILL_1", "SKILL_2"]

这个文件同时做三件事。(1) AI 生成分支方案时读取的规格,(2) lint 校验所依据的基准,(3) 人记录共识的单一出处forbidden_in_core 尤为重要——它是绝不允许在 core 文件夹内出现的 token 列表,因而成为机械地抓出层越界的依据。


14.2.4 实操记录(worked transcript):规则手册 → AI 分支方案 → 验证

现在从头到尾跟一遍实际操作。情况是这样的。在准备 PC 辅助发布的过程中,需要把原本只存在于移动端的输入处理器分支出 PC 版本。以规则手册为依据,向 Claude 索取 PC 输入适配器的初稿,用 lint 验证,直到拒绝·再请求,走完一个循环。

1) 提示词(全文)

下面是我们项目的平台规则手册节选和现有的 iOS 输入处理器。

[粘贴上面 platform_rules.yaml 的节选]

# platform/ios/input/touch_handler.py
from core.input.input_intent import InputIntent

def map_touch_to_intent(touch_event):
    if touch_event.location in skill_button_1_area:
        return InputIntent.SKILL_1
    if touch_event.is_drag_up:
        return InputIntent.MOVE_FORWARD
    return None

请参考它,做一份 platform/pc/input/keyboard_handler.py 的初稿。约束:(1) 可以 import core,但绝不要 import platform.ios/android,(2) 把 required_intents 的 4 个全部映射,(3) 按键映射要能让用户修改,拆分为 dict。没有把握的部分请标出来。

2) Claude 的原始输出(未经摘要·整理)

这是初稿。我把按键映射抽取到模块顶部的 dict,使用户可以自定义。

# platform/pc/input/keyboard_handler.py
from core.input.input_intent import InputIntent

DEFAULT_KEYMAP = {
    "w": InputIntent.MOVE_FORWARD,
    "1": InputIntent.SKILL_1,
    "2": InputIntent.SKILL_2,
    # ATTACK 假定为鼠标左键——键盘默认不指定
}

def map_keyboard_to_intent(key_event, keymap=DEFAULT_KEYMAP):
    return keymap.get(key_event.key.lower())

def map_mouse_to_intent(mouse_event):
    if mouse_event.button == "left":
        return InputIntent.ATTACK
    return None

⚠️ 没有把握的部分:required_intents 中含有 ATTACK,但我把它映射到了鼠标而非键盘。规则手册的 required_intents 究竟是「只要与输入设备无关地完成映射即可」,还是「要在单一处理器内全部处理」,没有明确规格,因此我假定为前者。若为后者,lint 可能会因 ATTACK 缺失而抓出。

3) 人工验证 / 拒绝

这里 Claude 指出的 ⚠️ 之处,恰恰是陷阱所在。我们的 lint(platform_lint.py)不是以处理器文件为单位,而是以平台为单位检查 required_intents 是否满足。即便 ATTACK 不在 keyboard_handler.py 中,只要在 mouse_handler 一侧存在,就能通过。然而 Claude 给出的输出,把鼠标映射一并塞进了 keyboard_handler.py 文件里——文件职责混在了一起。结构上虽能通过,却违反了我们的文件夹规则(按输入设备分离文件)。拒绝。

拒绝理由用两行说清。(1) 把鼠标映射分离到独立的 mouse_handler.py。(2) 为使 ATTACK 在键盘上也能使用,把 Space 设为 fallback。

4) 再请求 → lint 通过

把再请求后得到的分离版本交给 platform_lint.py。lint 读取规则手册,检查以下各项。

$ python platform_lint.py platform/pc/
[core-leak]    PASS  — core/ 内 forbidden token 0 处
[intent-cover] PASS  — pc: MOVE_FORWARD, ATTACK, SKILL_1, SKILL_2 (4/4)
[touch-target] SKIP  — pc 为 min_target_px=24 (在 UI 层单独检查)
[no-cross-import] PASS — platform.pc 未引用 platform.ios/android

关键在于 intent-cover 落到 4/4。AI 生成的初稿是否满足规则手册的基准,不再由人眼、而是由脚本来确定。这一行,替代了在多平台运营中人每次都要在脑中核算的工作。

把这个循环压缩成一张图,如下所示。

flowchart LR R[platform_rules.yaml<br/>规则手册] --> P[提示词中<br/>规则手册+现有处理器] P --> A[Claude 分支方案<br/>+ 不确定标注] A --> H{人工验证} H -->|拒绝:文件职责混合| P H -->|接受| L[platform_lint.py] L -->|FAIL| P L -->|PASS| M[进入构建分支] R -.提供基准.-> L classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class L code; class A ai; class H human; class R data; class M pass;

规则手册向提示词与 lint 两侧供给基准,正是这一结构的核心。AI 生成、人判断、lint 确定——三种角色看的是同一份规则手册。


14.2.5 构建分支:相同的核心,不同的组装

处理器齐备后,构建就是简单的组装。core 与 shared 固定,只替换 platform 文件夹一个。

[core/ + shared/ + platform/ios/]      → iOS 构建
[core/ + shared/ + platform/android/]  → Android 构建
[core/ + shared/ + platform/pc/]       → PC 构建

在 CI 中,这三者不是串行而是并行运行,每次构建后立即自动执行 platform_lint.py。若串行运行,构建时间会变成 3 倍;若省去 lint,违反规则之处会一直存活到部署阶段。并行构建 + 自动 lint,这两点是多平台 CI 的最低要件。

发布周期因平台而异,因此不会因为构建通过就同时部署。iOS 审核通常为 1\~3 天,对频繁发布较为保守;Android 在数小时内即可生效,可以更频繁地发布;Steam 则在 1\~2 天上下。即便是同一处改动,iOS 也是最晚上线的一方,因此热修复的排期始终以 iOS 为基准倒推。


14.2.6 UI 变体:通用 80 · 变体 15 · 专用 5

在代码之下,画面也会分岔。以经验来看,推荐的分布是通用组件 80%、平台变体(仅大小·位置不同)15%、平台专用 5%。不过这一比例会因品类而波动——若是休闲益智类,通用可升至 90%,而 MMORPG 因输入差异,变体会更多。

专用组件是发挥平台魅力的地方,并非一味通用化就是答案。移动端的虚拟摇杆·振动、PC 的按键映射 UI·手柄设置这类只在该平台上才有意义的东西,都归入此处。不过专用一旦超过 30%,那就不是魅力,而是运营负担的信号——在 lint 中挂上 platform-specific-ratio 警告,即便人忘了,构建也会替你指出。

这里也是 AI 辅助的界线所在。平台差异大多属于确定性规则的领域,因此 AI 与其自由地探索候选,不如用于生成满足规则手册的分支方案。输入映射推荐、Figma 设计稿的平台变体转换、多语言×多平台的文本适配,大致就是 AI 实质上添力的地方,而其输出始终必须通过 lint。在进步式自动化之前,先要做的是适配器标准化。


14.2.7 分离的价值——以及常见陷阱

层分离最大的效果是新增平台的速度。在单一代码库上堆叠 if 语句来贴合 PC,实际上接近于做一款新游戏的成本;而不触碰 core、只填充 platform/pc/,那段时间就会大幅缩短。新增平台加快的比例因项目而异,所以不断言具体倍数——不过在我们内部评估中,推算 PC 辅助新增的排期相比单一代码假设可缩减到一半以下(笔者估算,未经验证)。作为附带效果,各平台的事故被隔离,core 改动的可信度也随之提升(只改一处即可一致地反映到三个构建中)。

常踩的陷阱与对策如下。

陷阱 对策
core 中 if platform == ... 分支激增 forbidden_in_core lint 拦截,拆分为适配器
仅凭人眼评审 AI 分支方案 platform_lint.py 确定 intent-cover
把输入设备映射全塞进一个文件 按设备分离处理器(keyboard/mouse)
专用组件 30%+ platform-specific-ratio 警告,评估通用化
构建一通过就三平台同时部署 按发布周期差异,以 iOS 为基准倒推

这些陷阱的共同点在于「试图靠人的记忆来防堵」。写进规则手册、挂到 lint 上,即便人忘了,构建也会记得。


本章要点

下一章预告


动手试试

setup. 在项目中创建 platform/platform_rules.yaml,像上面的节选那样,按平台写下 min_touchcontrast_ratioforbidden_in_corerequired_intents。数值不要编造,而要取自公开标准(触控 44pt·48dp·对比度 4.5:1 等公开标准遵循 §9.1 规则手册;PC 指针目标 24px 为 WCAG SC2.5.8)。

prompt. 把规则手册节选 + 现有某一平台的处理器一并粘贴,并这样请求。「请遵守这份规则手册,做一份 platform/<新平台>/input/ 处理器的初稿。绝不要放入 forbidden_in_core token,把 required_intents 全部映射,没有把握的部分用 ⚠️ 标出。」

verify. 运行 platform_lint.py(40 行左右的脚本就够),它读取规则手册并检查以下各项。(1) core 文件夹内 forbidden_in_core token 0 处,(2) 各平台的 required_intents 全部映射,(3) platform 文件夹之间无 cross-import。只要有一项 FAIL,就回到提示词,写下拒绝理由并再请求。

单人精简版

如果你独自工作、也没有构建 CI,就把规则手册从 YAML 缩减为一张 Markdown 检查清单。「目标 ≥44pt,禁止在 core 中 import 平台,映射 4 个意图」三行就够。不用 lint 脚本,把成果交给 AI,让它「把这份检查清单的 3 项逐一判定为通过/失败」,就能代替人的核算。关键不在工具的规模——而在于把基准写在脑袋之外,并把生成与验证分开。

14.3 触摸 / 鼠标输入设计

拿到 QA 构建的组员 B 单手握着手机,皱起了眉头。"技能按了三次,只放出来两次。"我凑近屏幕一看,拇指按下技能按钮的那一瞬间,同一根手指正好盖住了按钮旁边约三分之一的区域。用鼠标测试时从未出现过这个问题——因为鼠标没有手指。

这个场景用一句话概括了触摸与鼠标的本质。两者都是"指向某一点"的输入,但一种的指向工具会遮住屏幕,另一种不会。为什么同一个行为要在两种输入上用不同方式来实现,原因就从这里开始。本章先梳理两种输入的差异,随后完整跟随一条实操记录(worked transcript,完整保留的真实操作过程记录)的主干——先让 AI 提出输入映射方案,再亲自验证冲突与可达性。


14.3.1 两种输入的本质差异

手指的粗细、视野遮挡、多点触控上限、精度全都不同。在用表格把它固化下来之前,先用一个画面来建立直观感受。鼠标光标是一支 1 像素的笔尖,手指则是一枚直径将近 1 厘米的印章。笔尖能写字,但一次只写一个字。印章盖得快却写不了字,而且盖下去的那一刻就看不见纸了。

属性 触摸 鼠标
精度 约 7\~10mm(手指接触面) 1px 级
视野遮挡 手指遮住接触点周围
可否悬停 几乎不可能(接触 = 输入) 自由(移动 ≠ 输入)
同时输入 2\~10 点多点触控 左·右·中·滚轮
拖动/点按区分 需靠时间·距离推断 点击/拖动明确
触觉反馈 可以 几乎没有

这里对设计影响最大的两行是"视野遮挡"和"悬停"。视野遮挡强制决定了结果显示在哪里,而悬停的缺失意味着在移动端,工具提示(tooltip)这一整个信息通道被彻底抹去。其余四行更接近由这两行派生出来的细节。

公开标准把这些差异用数值钉死了——触摸 44pt(HIG)·48dp(Material)·对比度 4.5:1·触摸目标 24 CSS 像素(WCAG SC2.5.8)这类公开标准遵循 §9.1 规则手册。这些数字不是凭喜好,而是人体与测量的产物,所以验证映射时用来衡量的尺子最终也是这套标准。

14.3.2 按游戏行为的映射

移动·攻击·技能这三种行为,用两种输入来实现时会如下分岔。一个行为有三种方式,并不意味着没有正确答案,而是意味着游戏定位会强制做出选择。

我所在的项目A(移动优先的 MMORPG)在移动这一行为上:移动端采用 ⓐ+ⓑ 混合方案(摇杆与自动移动并行),PC 端采用 WASD+自动移动。攻击方面,移动端为 ⓑ+ⓐ(点按敌人后再按按钮),PC 端可在 ⓐ·ⓑ 之间自由选择。技能方面,移动端为 ⓐ,或在锁定目标时用 ⓑ,PC 端在按键 1\~8 之上叠加鼠标瞄准。同一款游戏、同一个行为,映射表却出现了两张——这正是本章的全部要点。

问题在于,映射表越长,冲突就藏得越深。长按技能槽(技能瞄准)与拖动屏幕(镜头旋转)在同一区域重叠时会怎样?按键 1\~8 已经被技能占用,若有人把背包快捷键也提议设为 1,又会如何?靠人眼扫一遍表格是会漏掉的。因此需要这样一套工作流:让 AI 提出映射方案,而把验证交给工具。

14.3.3 实操记录 —— AI 映射方案与 lint 验证

从这里开始就是本章的主干。让 Claude 提出 PC 键盘/鼠标映射的初稿,由人来验证其输出,运行冲突·可达性 lint 加以驳回,再重新请求——我把这一整个循环原样搬过来。不做概括。原始输出里也含有错误,而我们正是要看清抓出这些错误的过程。

提示词(全文)

你要设计一款 PC MMORPG 的输入映射。请针对下面的行为列表,提出一份将其映射到键盘/鼠标的 JSON 方案。

行为:移动(前后左右)、奔跑、跳跃、普通攻击、技能1\~8、背包、地图、角色面板、菜单、视角锁定切换、放大/缩小、上下文菜单(辅助行为)、自动移动切换。

约束: - 遵循业界惯例(WASD 移动、ESC 菜单等)。 - 不要在同一个物理按键/按钮上重叠两个行为。 - 每一项包含 actioninputnote 字段。 输出仅为 JSON 数组。

Claude 原始输出(原样,未做改动)

[
  {"action": "move_forward",  "input": "W",      "note": "标准移动"},
  {"action": "move_back",     "input": "S",      "note": "标准移动"},
  {"action": "move_left",     "input": "A",      "note": "标准移动"},
  {"action": "move_right",    "input": "D",      "note": "标准移动"},
  {"action": "run",           "input": "Shift",  "note": "按住时奔跑"},
  {"action": "jump",          "input": "Space",  "note": "标准"},
  {"action": "attack_basic",  "input": "Mouse1", "note": "左键主行为"},
  {"action": "skill_1",       "input": "1",      "note": ""},
  {"action": "skill_2",       "input": "2",      "note": ""},
  {"action": "skill_3",       "input": "3",      "note": ""},
  {"action": "skill_4",       "input": "4",      "note": ""},
  {"action": "skill_5",       "input": "5",      "note": ""},
  {"action": "skill_6",       "input": "6",      "note": ""},
  {"action": "skill_7",       "input": "7",      "note": ""},
  {"action": "skill_8",       "input": "8",      "note": ""},
  {"action": "inventory",     "input": "I",      "note": "Tab 让给目标切换"},
  {"action": "map",           "input": "M",      "note": ""},
  {"action": "character",     "input": "C",      "note": ""},
  {"action": "menu",          "input": "Escape", "note": "标准"},
  {"action": "cam_lock",      "input": "Mouse3", "note": "滚轮点击切换视角锁定"},
  {"action": "zoom_in",       "input": "WheelUp",   "note": ""},
  {"action": "zoom_out",      "input": "WheelDown", "note": ""},
  {"action": "context_menu",  "input": "Mouse2", "note": "右键辅助行为"},
  {"action": "auto_move",     "input": "Mouse2", "note": "右键双击自动移动"}
]

输出看上去很整洁。正因如此才危险。看最后两行。context_menuauto_move 都被绑定到了 Mouse2(右键)。尽管约束里明确写了"不要在同一个键上重叠两个行为",模型却在 note 里附上"用双击加以区分"的托词,硬是让它们重叠了。这正是不能把 AI 输出原样放进构建的理由。人扫一遍表格,很容易漏掉 23 行中第 22 行与第 23 行的冲突,而模型会为自己的冲突找合理化的借口。

因此把验证交给代码,而不是眼睛。运行一个小巧的 lint,检查冲突(同一输入重复)与可达性(缺失必需行为、双手拇指够不到的角落)。

# input_lint.py — 输入映射的冲突·可达性检查
import json, sys
from collections import defaultdict

REQUIRED = {"move_forward","move_back","move_left","move_right",
            "attack_basic","menu","inventory","map"}

def lint(mapping):
    errors, warns = [], []
    seen = defaultdict(list)
    for m in mapping:
        seen[m["input"]].append(m["action"])
    # 1) 冲突:同一输入绑定 2 个以上行为
    for inp, acts in seen.items():
        if len(acts) > 1:
            errors.append(f"CONFLICT  {inp} <- {', '.join(acts)}")
    # 2) 可达性:缺失必需行为
    actions = {m["action"] for m in mapping}
    for r in sorted(REQUIRED - actions):
        errors.append(f"MISSING   required action '{r}'")
    # 3) 空 note 警告(未记录设计意图)
    for m in mapping:
        if not m["note"].strip():
            warns.append(f"NO_NOTE   {m['action']} ({m['input']})")
    return errors, warns

data = json.load(open(sys.argv[1], encoding="utf-8"))
errs, warns = lint(data)
for e in errs:  print("[ERROR]", e)
for w in warns: print("[WARN] ", w)
print(f"\n=> {len(errs)} error(s), {len(warns)} warning(s)")
sys.exit(1 if errs else 0)

把上面的 JSON 保存为 claude_map.json 并运行 lint,实际输出如下。

[ERROR] CONFLICT  Mouse2 <- context_menu, auto_move
[WARN]  NO_NOTE   skill_1 (1)
[WARN]  NO_NOTE   skill_2 (2)
[WARN]  NO_NOTE   skill_3 (3)
... (skill_4~8 相同)

=> 1 error(s), 8 warning(s)

lint 精确地揪出了人眼漏掉的唯一一处冲突。可达性检查通过了(8 个必需行为全部存在)。8 处空 note 只是警告,并不阻断构建,但它暴露出未记录设计意图这笔债。现在带着驳回理由把它退回给模型。

人工驳回 + 重新请求

lint 结果显示 Mouse2 上 context_menu 与 auto_move 重叠,予以驳回。用双击来区分会带来右键延迟,在战斗中导致误操作。请把 auto_move 分离为独立输入。另外 skill_1\~8 的 note 是空的——请为每个槽逐行填上它属于哪一类技能。

Claude 重新输出(仅摘录解决冲突的部分)

  {"action": "context_menu", "input": "Mouse2",      "note": "右键 = 仅辅助/上下文行为"},
  {"action": "auto_move",    "input": "Numpad0",     "note": "自动移动切换,与战斗键物理分离"},
  ...
  {"action": "skill_1", "input": "1", "note": "近战主力技能"},
  {"action": "skill_8", "input": "8", "note": "紧急闪避/生存技能 —— 小指可达受限,考虑重新绑定到 Q"}

重新输出的最后一行很有意思。模型主动申报了可达性问题,说"8 号键是小指的可达极限"。这与我们下一节要讨论的可达性验证正是同一个主题。再跑一次 lint,会以 0 error(s) 通过。要点在于:AI 能快速生成一份 23 行的初稿,但这份初稿是否合规,由人定义的规则(REQUIRED 集合、冲突的定义)和代码来保证。提案由模型出,判定由工具做,决定由人拍板。

14.3.4 输入流 —— 从映射到抵达屏幕

把一次物理输入被转换为游戏行为的路径画出来,就能看清上面的 lint 卡在哪个环节。

flowchart TD A[物理输入<br/>触摸坐标 / 按键·鼠标] --> B{输入分类} B -->|接触 200ms↓ &amp; 5px↓| C[点按 / 点击] B -->|接触 200ms↑ or 5px↑| D[拖动] C --> E[查询映射表] D --> E E --> F{是否为 lint<br/>通过的映射?} F -->|冲突·缺失| G[阻断构建<br/>input_lint.py] F -->|正常| H[分发游戏行为] H --> I[执行行为] I --> J[反馈输出<br/>视觉+触觉/音效] G -.修正后重新提交.-> E classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class B,E,F code; class G fail;

从左上方进入的物理输入,首先被分类为点按还是拖动(依下一节的 200ms/5px 标准)。分类后的输入会去查询映射表,而这张表在进入构建之前必须通过 input_lint.py——这正是图的核心。一旦存在冲突或缺失,就到不了分发环节,会被阻断。映射验证必须在运行时之前、在构建关卡(build gate)处结束。

14.3.5 触摸设计的 5 条原则

现在假设映射已经通过,接下来设计这套映射与手指相遇的那个表面。

原则 1 —— 最小触摸面积。 Apple HIG 的 44pt、Material 的 48dp 是下限。在 HD 屏幕上取大约 100px(Retina 2 倍环境为 200px)上下,就能同时满足两项标准。低于这个值,开头那句"按三次出两次"就会以统计数据的形式显现出来。

原则 2 —— 拇指可达区域。 MMORPG 移动端以横屏双手握持为标准,需要按压的元素放在下方左右两角、消耗品/技能槽放在中央下方(三区域模型的依据见 §9.1)。P0 行为(左=移动,右=攻击·技能)放在左右下方两角之内,不常看的信息放在可达极限之外的上方。两角相加也不到屏幕的一半,这一点是输入设计的核心。下面的 SVG 展示了横屏模式下双手拇指的可达区域与中央下方的槽位栏。

上方 = 可达之外(信息显示)

中央下方 = 消耗品·快捷槽 左拇指 右拇指 舒适 舒适 深色区域 = P0 按钮布置 / 浅色区域 = 可达极限

深色扇形是拇指能轻松够到的地方,浅色扇形则是必须伸手才够得着的极限。若说 14.3.3 重新输出中模型申报的"8 号键小指极限"是 PC 版的情形,那么移动版对应的失误,正是把 P0 按钮放进这块浅色区域。

原则 3 —— 规避视野遮挡。 手指不只遮住接触点,其上方还有整只手覆盖屏幕。点按右下角的技能时,右下角约四分之一会看不见。因此把行为的结果(伤害数字、状态变化)显示在手指够不到的区域。握着左侧摇杆的手会侵占角色与小地图的位置,所以把小地图移到右上角。

原则 4 —— 拖动/点按的区分。 与鼠标不同,触摸必须靠时间和距离来推断用户的意图。在整个游戏里统一为一个标准——例如接触在 200ms 以内且移动在 5px 以内为点按,超过则为拖动。这两个数字若忽大忽小,"本想点按结果角色翻滚了"这类意图失败就会累积。上面 mermaid 的分岔点正是这一判定。

原则 5 —— 触觉。 振动是无需看屏幕也能传达的唯一通道。不过若对每一次输入都施加振动,就会变成噪音。普通点按不振动,使用技能短促振动,支付确认这类高风险行为强烈振动,击杀敌人则轻微振动——控制在 4\~5 种以内。

14.3.6 鼠标设计的 5 条原则

鼠标享有触摸所没有的三种奢侈:悬停、多按钮、光标精度。

原则 1 —— 悬停。 鼠标不按下也能指向。把鼠标悬在技能槽上,会弹出名称·冷却时间·说明的工具提示,点击则会释放。触摸没有这个中间状态,所以悬停是 PC 得以叠加更多信息的通道。但要在 14.3.3 的映射阶段就预先意识到:只依赖悬停的信息,到了移动版会无处安放。

原则 2 —— 多按钮。 左键是主行为,右键是辅助/上下文,滚轮点击是视角重置,滚轮是缩放。上面 lint 抓出的冲突,正是在这个右键上重叠了两个行为的例子。因按钮多就想把它们全填满,于是制造出冲突。

原则 3 —— 键盘标准。 ESC=菜单,M=地图,1\~8=技能,WASD=移动,Shift=奔跑,Space=跳跃。用户不用学也应该能猜到。偏离标准的键,要把相应的理由写进 note 里——14.3.3 中把 Tab 让给目标切换而非背包的决定就是一例。

原则 4 —— 视角控制。 用鼠标拖动来旋转视角,同时明确切换:把光标锁定在屏幕上的游戏模式,与释放光标的 UI 模式。这个切换若含糊,就会出现"关掉菜单后光标却消失了"的混乱。

原则 5 —— 宏·自动化的允许范围。 自动攻击·自动移动允许到什么程度,是游戏定位的问题。放得太开,PC 就变成一片宏的画面;一味禁止,从移动端过来的用户门槛就会变高。正确答案是按游戏调性在这条谱系上选取某一个点,而不是两个极端。

14.3.7 两端通用 —— 统一输入反馈

原则因平台而异,但用户对同一个行为所获得的"感觉",即便换了平台也应当一致。不能让从移动端转到 PC 的用户重新学习按钮高亮的含义。

情形 触摸 鼠标
输入识别 按钮高亮 + 短促触觉 按钮高亮 + 点击音
输入失败 按钮抖动 + 触觉 按钮抖动 + 警告音
冷却进行中 环形进度条 环形进度条
恢复可用 高亮 + 触觉 高亮 + 音效

视觉通道(高亮·抖动·进度条)两端相同,只有辅助通道按平台在触觉↔音效之间分岔。这种一致性把多平台用户的学习成本减半。

14.3.8 常见失败与处方

模式 处方
按钮低于标准下限(44pt/48dp) 强制取 100px 上下
在手指遮挡区域显示结果 移到非遮挡区域
滥用触觉 控制在 4\~5 种以内
右键重叠两个行为 用 lint 阻断冲突后分离
把仅悬停的信息原样搬到移动端 移动端用点按/长按替代通道
键位映射写死 允许用户自定义
强制两端相同映射 各平台采用自然的映射

这张表的第四行就是 14.3.3 实操记录的结论。右键冲突靠人眼扫表几乎每次都会漏掉,而把 lint 挂在构建关卡上则几乎每次都能抓到。


本章要点

下一章预告


动手试试(setup → prompt → verify)

  1. setup —— 把行为列表和约束整理到一个文件里(移动·攻击·技能·UI·视角)。把上面的 input_lint.py 放进项目。将 REQUIRED 集合替换为你自己游戏的必需行为。
  2. prompt —— 直接使用 14.3.3 的提示词全文,只替换行为列表。把输出固定为仅接收 JSON 数组。
  3. verify —— 用 python input_lint.py claude_map.json 运行收到的 JSON。在 ERROR 归零之前,明确写出驳回理由(冲突输入·缺失行为)并重新请求。WARN(空 note)作为未记录设计意图的债务另行记录。

单人精简版

如果是独自开发的小游戏,就精简工具。行为在 10 个以内时,只保留 REQUIRED 集合与"同一输入重复"两项检查的 20 行 lint 就足够了。让 AI 给出映射,用这个迷你 lint 只筛掉冲突,然后在真机上按一按,看看拇指(或小指)能否够到。提案由模型出,冲突判定由代码做,可达判定靠你自己的手——只要守住这三点,无论规模大小都行得通。

15.1 运营(LiveOps)总览 —— 活动候选由 AI 组合,规则手册筛除,人来选定

主要读者:首次负责上线后运营(LiveOps)的策划(中等规模(10\~50 人)团队) 面向单人/业余读者的精简版:§15.1.7「一个人的话,只做这些」

前提:笔者亲历过一款全球上线的移动端 MMORPG 的运营,连同 P2E(Play To Earn,边玩边赚)经济一并经历过,并在此之上叠加了当前项目上线前的 AI 工作流来写作本章。实操记录(worked transcript,即完整保留的真实操作过程记录)是把"输入→AI 组合→规则手册验证→人工选择"的模式,以运营样式实际跑一遍的结果。凡属推测与观察,都明确标注为推测·观察;编造的 KPI 表一概不放。

上线次日清晨的办公室,与上线前不一样了。里程碑结束了,活儿却没有减少,反而只是单位变得更小。原本以季度为单位排布的日程,被切成周·日·小时为单位。而每周都有同一个问题回到会议室:"这个周末上什么活动?"

如果这个问题每周都从一张白纸重新开始,运营团队很快就会疲惫。本章要讲的,就是把这个问题从白纸中解放出来的方法。核心有两点。第一,与其每次都从头绞尽脑汁想活动和赛季,不如把它们沉淀为由已验证样式构成的库。第二,把"组合这些样式、生成下周 5 个候选"这种枯燥的初稿工作交给 AI,人只决定在通过规则手册验证的候选中采纳哪一个。从 0 开始做,和从 5 个里挑,工作负担是不同的。


15.1.1 运营不是"感觉",而是"循环"

把运营的标准周期做成表让人背诵的书有很多。无非是周一汇报、周二三准备、周五发布。这些都没错,但只背表格,看不出"本周活动"这个每周都要回来的决策究竟是怎么做出来的。运营的本质不是日程表,而是闭合的循环——候选产生、通过验证、由人选定、进入构建发布、用户数据又成为下一批候选的输入,如此一圈。

在这个循环之上,运营的四条轴(内容·活动·数值平衡·CS(客服))各以各自的速度运转。内容是月\~季度,活动是周\~月,数值平衡是周\~双周,CS 是日·小时为单位。四条轴各转各的,那么看着同一份用户数据,每周也会得出不同的决策。因此,把四条轴拧成一个循环,并把这个循环中的一格(活动候选生成)做成 AI 能够运转的形态,就是本章的目标。

flowchart TD A["输入<br/>KPI 走势 · 用户 segment<br/>· 上次活动效果"] --> B["AI 组合<br/>赛季规则 × 活动模板<br/>库 → 5 个候选"] B --> C{"规则手册验证<br/>通胀上限 · 目的冲突<br/>· 奖励范围 · 时长"} C -->|违规 alert| D["重新请求<br/>(替换违规候选)"] D --> B C -->|通过| E["人工选择<br/>总监采纳/否决"] E --> F["构建·发布<br/>(不可逆:赛季开始·公告)"] F --> G["用户数据·反馈<br/>(作为下一轮循环输入)"] G --> A classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class A,G data; class B ai; class C,F code; class E human;

需要人动手的地方只有两处。最上面把输入(KPI·segment·过往效果)干净地喂进去的那一处,以及在通过验证的候选中决定上哪一个的那一处。夹在中间那些枯燥的"绞出 5 个组合"和"筛掉规则违反",由 AI 和规则手册来跑。而最下面那一行——已发布的活动所产生的用户数据又回流为输入——正是它让这个循环成其为运营。上线前的设计一旦发出去就结束了,而在运营中,结果会成为下一次的输入。

进入这个循环的两个库(赛季规则·活动模板)的具体内容将在 §15.2 展开,最后一格(用户反馈自动分类)在 §15.3 来看。本章专注于把循环完整地走完一圈。


15.1.2 [实操记录] 组合 5 个活动候选 → 规则手册验证 → 人工选择

下面把实际怎么跑的完整地展示一整个循环。以下是把笔者在上线前的内容工具中验证过的"库组合 → 规则手册验证 → 人工选择"模式,搬到运营样式(赛季规则 + 活动模板)上实际跑一遍的会话的还原。输入提示词可以直接复制使用,输出则是对那次会话的还原。

第 1 步 —— 输入:把库和当前状况原样丢进去

先把组合所需的两样材料,放成机器能读的形态。活动模板库(已验证的样式)和赛季规则库,以及本周的当前状况(KPI·segment)。库一旦建好,每周都可复用。

# event_templates.yaml —— 已验证的活动模板库(节选,9 种中的 4 种)
- id: tpl_attendance      # 签到奖励
  目的: [拉新, 回流]
  建议时长: 7~14天
  奖励等级: 低~中
- id: tpl_coop_raid       # 协作团本
  目的: [存量激活, 社区]
  建议时长: 3~7天
  奖励等级: 中~高
- id: tpl_pvp_season      # 竞技赛季
  目的: [社区, 存量激活]
  建议时长: 14~28天
  奖励等级: 高
- id: tpl_limited_package # 限定礼包
  目的: [营收]
  建议时长: 3~7天
  奖励等级: 高 (付费联动)

# season_rules.yaml —— 赛季规则片段(节选)
season_inflation_cap: 每季度'高'等级奖励活动 ≤ 3 次
purpose_conflict_rule: 同一周内禁止同时安排 2 个 [营收] 目的活动
overlap_rule: '高'奖励活动禁止同时 2 个(疲劳·通胀)

# current_state.yaml —— 本周状况
周次: 2026-W23
前两周营收活动: 1 次(本季度累计'高'等级 2 次)
DAU走势: 缓慢下降(过去 4 周 -6%,据行业观察处于'警戒'区间)
主要segment: 可回流的休眠用户占比上升
临近的外部日程: 无

第 2 步 —— 提示词:让它组合,但强制规定格式和规则

用附上的模板·赛季规则·本周状况 yaml,只组合出下周的 5 个活动候选。
不要创造新机制,只用所附模板的组合;是否违反赛季规则由你自己标注,
并为每个候选补上一行:它为何契合当前信号(DAU 下降·休眠回流)。
让目的不要只偏向营收,要分散开;含糊的就标出来交给我。
只用表格,列为:候选·模板组合·主要目的·时长·规则违反·依据·含糊。

请注意,这五行规则中没有任何一行在说"要做什么"。它们全是"在什么框架内、以什么为依据来回答"的约束。因为一旦给 AI 自由,它就会吐出一般 RPG 的平均活动。把材料限制在库以内(规则 1)、赛季规则自查义务(规则 2)、对当前信号作出应对的依据义务(规则 3)、目的分散(规则 4),以及"含糊就交给人"这个出口(规则 5)——五行合起来,规定的不是候选的样子,而是候选的资格。

第 3 步 —— Claude 的输出(还原,节选)

下面是未经摘要的原始输出。其中混着规则违反与含糊的自报。

| 候选 | 模板组合                      | 主要目的        | 时长  | 规则违反 | 依据                                       | 含糊 |
|-----|------------------------------|----------------|-------|---------|--------------------------------------------|------|
| C1  | tpl_attendance               | 回流           | 10天  | 无      | 直接应对可回流休眠层占比上升,奖励'中'      | -    |
| C2  | tpl_coop_raid                | 存量激活       | 5天   | 无      | DAU 下降期提供上线动机,刺激社区            | -    |
| C3  | tpl_attendance+tpl_coop_raid | 回流+激活      | 7天   | 无      | 用签到吸引回流 + 用团本留存,组合协同        | -    |
| C4  | tpl_pvp_season               | 社区           | 21天  | 无      | 用长期赛季直接抬升留存曲线本身              | ★    |
| C5  | tpl_limited_package          | 营收           | 5天   | ★违反   | 本季度'高'等级累计 2 次 → 再加则逼近/超出上限 | -    |

规则检查结果:
- C5: 有违反 season_inflation_cap 的风险。本季度'高'等级奖励活动
  已有 2 次。再加上 C5 就到 3 次,逼近上限。DAU 下降期推营收
  可能加重用户疲劳,与信号也方向相反。
- C4: 虽无规则违反,但 21 天的 PvP 赛季占用的不是这一周,而是 3 周。
  它作为'本周候选'是否合适,需要运营策略判断 → 标为含糊。
- C1·C2·C3 通过规则,与当前信号(休眠·DAU)相符。

输出中最有价值的部分不是表格,而是最下面那三行规则检查。AI 自己抓出并标记了 C5 的通胀违反,又把 C4 以"不算违反,但判断有分歧"为由交给了人。好的提示词,能让 AI 主动申报自身的局限、把球传给人。

第 4 步 —— 验证与选择(人的位置)

不能把这份输出照单全收。要用规则手册再过一遍,然后才由人来挑。在这次会话中,实际上有两处出现了分歧。

首先,C5 拒绝。AI 已经标出了通胀违反,规则手册代码(§15.1.3)也给出了同样的判定。它撞上了季度'高'等级上限,而 DAU 下降期推营收与当前信号方向相反。没什么可讨论的。拿掉。

接下来是 C4(21 天的 PvP 赛季)。这是 AI 以"含糊"交出来的那一项。虽无规则违反,但这不是"本周活动",而是"本赛季的决策"。它不该在一周一圈的循环里当场拍板,而应上升到赛季统合会议。因此本周候选中先搁置,单独抽出作为赛季日历的议题。

在剩下的 C1·C2·C3 中由总监来选。与当前信号(可回流休眠层上升 + DAU 缓慢下降)最契合的,是 C3(签到+协作团本 组合)。用签到把休眠层引进来,再用团本把引来的用户留住,这种组合协同与本周信号相符。C1·C2 留在下周的候选池里。

这里还有一个没有就此收尾的候选。定下采纳 C3 之后,发现 7 天的周期与临近的例行维护日重叠了一天。于是又跑了一次重新请求。

采纳 C3。不过 7 天周期中的最后一天与例行维护日重叠。
请调整时长后重新提案,别让维护把活动末尾的参与切断。
奖励总量保持不变,只把日程提前。

AI 把开始日提前了一天,让活动在维护前结束,重新作答;这一调整通过了规则手册。输入 → AI 组合 → 规则手册验证 → 人工选择 → 日程再调整的一个循环,到这里闭合。

这一圈,就是本书全书的 Show 标准。若不曾把 AI 组合了什么、规则手册筛掉了什么、人选了什么又拒绝了什么完整地看到底一次,那么"用 AI 来产出活动候选"这句话就是空的。


15.1.3 把规则手册写成代码 —— 候选自动验证

候选是否守住了赛季规则,若每周都靠肉眼看,又会漏。§15.1.2 的三条规则里,凡能用数字判定的,就让代码来把关。人只把时间花在代码抓不住的"含糊"与"选择"上。

# event_lint.py —— 下周活动候选验证(骨架)
# 输入: AI 组合出的候选列表 + 赛季规则 + 季度累计状态
# 输出: 规则违反列表(不是自动拒绝,而是 alert)

def lint(candidates, season, quarter_state):
    issues = []
    high_used = quarter_state["high_reward_count"]  # 季度累计'高'等级次数
    for c in candidates:
        # 规则 A: 通胀上限(每季度'高'等级 ≤ 3)
        if c["奖励等级"] == "高" and high_used + 1 > season["inflation_cap"]:
            issues.append(f"[A] {c['id']}: 追加'高'等级将超出本季度上限 "
                          f"{season['inflation_cap']} 次(当前 {high_used})")
        # 规则 B: 禁止同一周内 2 个 [营收] 目的
    sales = [c for c in candidates if "营收" in c["目的"]]
    if len(sales) > 1:
        issues.append(f"[B] [营收] 目的候选 {len(sales)} 个同时 → 限制为 1 个")
        # 规则 C: 目的扎堆(5 个中若某一目的过半则分散不足)
    from collections import Counter
    top = Counter(c["主要目的"] for c in candidates).most_common(1)[0]
    if top[1] > len(candidates) // 2:
        issues.append(f"[C] 目的 '{top[0]}' {top[1]} 个扎堆(分散不足)")
    return issues

这段代码,把会议上"这个奖励是不是太强了?"的争来争去,用一行数字了结。当代码输出 [A] tpl_limited_package: 追加'高'等级将超出本季度上限 3 次(当前 2) 时,没什么可讨论的。拿掉就行。这是把 §14.1(移动端 HUD)中讲过的 lint 关卡搬到运营层面——能用确定性抓的交给代码,需要判断的交给人,这种分工在运营中同样成立。

只是有一点不同。这个 lint 即便发现了违反,也不会自动废弃候选。它只上报 alert。这与 §6.2(城市生成器)中见到的是同一种设计。一旦装上自动拒绝式的验证,连有意为之的变体(例如:明知季度上限,仍刻意把营收活动排进去的推广活动决策)也会被机器一并杀掉。可疑的候选由机器挑出,但杀还是留,由总监来定。§15.1.2 中拒绝 C5,也不是 lint 杀掉的,而是人看了 lint 的 alert 之后作出的决定。


15.1.4 上线前与上线后 —— 什么变了

上面这个循环,与上线前的设计循环有两处决定性的不同。与其列成表格,不如把这两点精确点出来。

第一,结果会成为下一次的输入。上线前,写好策划案后一路单向流到构建。而在运营中,本周活动所产生的用户数据(参与率·流失·营收·反馈)会作为下周候选组合的输入(current_state.yaml)回流。§15.1.1 循环最下面那根箭头就是这个回归。所以运营的 KPI 不是"一次就猜准",而是"每周对着信号做调整"。

第二,实验成本变小了,但不可逆的点更加尖锐。上线前是一次决策左右整个季度,而在线上,先跑一个一周的活动,不合适下周就改。可回滚的实验多了起来。然而,赛季开始与活动公告是不可逆的。§5.4.5 中讲过的"录音·配音选角 = 不可逆步骤"原则在此照样起作用。用户已经看到的赛季规则·奖励,即便"取消"也会在社区认知里留下痕迹。因此 §15.1.1 循环中的所有验证(AI 组合·规则手册·人工选择),都必须在进入构建·公告这个不可逆格子之前、在可逆阶段就结束。把 C4(21 天赛季)从本周当场拍板中抽出、上升到赛季会议,也是这个原则——不可逆的点越是重大的决策,越要经过更长的可逆审议。

这两点,让运营成为一件不同于上线前设计的事。其余的(时间单位从季度→周,反馈从 Beta→实时)都是这两条轴的派生。


15.1.5 从保守应用到进取应用

§15.1.2 的实操记录是进取应用的一个场景。AI 组合候选,人决定采纳。但并非所有团队一开始就能走到这一步。这里有阶段之分。

保守应用中,由人来提出候选。运营团队在周一会议上亲手策划活动、手写赛季规则、手动分类用户反馈。自动化只负责测量(KPI 仪表盘)和回归检查(构建验收)。据行业观察,目前大多数线上 MMORPG 的运营都接近这一阶段。

进取应用中,连"活动候选的提出"和"反馈分类"也由 AI 出初稿。§15.1.2 是前者的场景,后者(反馈自动聚类)在 §15.3 来看。人的决策收窄为"采纳哪个候选""如何对待 AI 分类出的反馈"这类元决策。

进取应用要立得住,需要具备三样东西。把活动模板·赛季规则拆分·积累为可重新组合单位的(§15.1.2 的 event_templates.yaml 就是它的种子),接收当前信号并把候选以初稿形态产出的候选生成器(§15.1.2 的提示词),以及把进来的反馈自动分类的聚类(§15.3)。这三样与 §5.3.12(世界 BT(BehaviorTree,行为树)·任务云)·§8.1.8(进取式数值平衡)是同一副骨架——这正是本书一以贯之的信息:领域不同,但"把已验证的碎片沉淀为库,由 AI 产出组合候选,由人采纳"这个结构是相同的。

这里先说清一点。库·候选生成器·聚类这类构想,在 2010 年代理论上也是可行的。被卡住,是因为 AI 那时写不出活动公告文·规则说明这类给用户读的自然语言,也无法把每天数百\~数千条反馈用自然语言来概括·分类。LLM 发展(2023\~)之后,这两道墙降了下来,原本只停留在纸面上的进取式运营,相当一部分进入了可实现的领域。


15.1.6 常见的失败

模式 为何失败 处方
每周从白纸策划活动 运营团队很快耗竭,候选质量随状态起伏 用活动模板库来积累(§15.1.2)
"AI 帮我做个活动"整个甩给它 没有库·规则,只会得到一般 RPG 的平均水平 限制材料 + 强制赛季规则自查(§15.1.2)
候选只靠肉眼验收 每周都漏掉通胀·目的扎堆 event_lint.py 自动验证(§15.1.3)
把 lint 做成自动拒绝式 连有意为之的推广决策也被机器杀掉 只上报 alert,采纳由总监(§15.1.3)
把不可逆决策在周循环里当场拍板 赛季公告后回滚会在社区留下痕迹 重大决策分离到赛季会议(§15.1.4)
只追求单一 KPI(DAU·营收) 用户疲劳累积,采纳与信号方向相反的候选 向 current_state 输入多轴信号(§15.1.2)

15.1.7 动手试试 —— 今天就能做的一步

请按 setup → prompt → verify 的顺序,只做一步试试。

一个人的话,只做这些:库 yaml 也好,lint 代码也好,都不需要。只要回想你喜欢的游戏上个季度的 5\~6 个活动,用"目的·时长·奖励"三栏记下来。仅凭这一点,你就能看出:那款游戏并不是每周从白纸绞出来的,而是在反复套用样式。这张表,就是你的第一个模板库。

如果是团队,就从下面这一步开始。把过去 1\~2 个季度的活动收集起来,规整成 event_templates.yaml(只留已验证的样式),把三条赛季规则先用 event_lint.py 放进代码里。有了库和规则,无论是 AI 组合的候选还是人的初稿,都能用同一把尺子来量。


本章要点

下一章预告

15.2 活动·赛季运营 —— 一张模板生成 10 个变体候选,评审只由人来做

第一读者:负责运营(LiveOps)的 MMORPG 策划(中等规模、10\~50 人的团队) 面向单人/业余读者的精简版:§15.2.9「一个人的话,做到这些就够了」

回想一款上线运营到第 4 年的游戏的周一例会。下周的活动要用什么,每周都从一张白纸开始。有人说"上次的签到活动奖励调高一点再来一次?",就有人回"那是两个月前做过的",而奖励要调高多少,又是凭感觉定。会议一结束,一名运营策划就要花上半天,从头把活动表单填满。每周,从白纸开始,半天。

问题不在于缺想法。运营团队脑子里其实已经有几套经过验证的活动骨架 —— 签到·协作·竞争·回流。往这些骨架里换上主题和奖励,一个为期一周的活动就出来了。只是这个"换装"每次都靠手工、靠感觉,所以又慢,结果又飘忽。

本章要讲的,就是把这个"换装"交给 AI 的方法。核心有两点。第一,把已验证的活动骨架作为可变体的模板 yaml录入。第二,把从模板中抽出下周多个候选这件枯燥事交给 AI,而人则用代码卡住奖励范围·重复之后,只评审调性。活动策划的一般理论(签到利于拉新、协作利于活跃之类)其他书里已经讲得够多,本章只专注于把那些知识放进 AI 工作流的那个位置

作者运营经验备注(坦白说) 上线后以 1\~2 年为单位亲自负责运营的经验,只占作者职业生涯的一部分。本章的工作流是作者把正在运营的量产·评审工具(内容·HUD)搬到活动领域的产物,而效果数值是业界观察 + 作者推测,这一点会在正文中随处标明。工具结构(模板 yaml·lint·评审关卡)与作者实际运营的内容量产工具是同一套骨架。


15.2.1 人只做模板编写和最后的评审

活动量产的整体流程分四步。核心在于:第 1 步(模板)和第 3 步(lint)是确定性的,只有第 2 步是 AI。这与内容量产(§6.2)·HUD 压缩(§14.1)里看到的是同一种分工。规则手册从输入和验证两头把关,那么夹在中间的 AI 每次给出略有不同的变体,奖励平衡和日程也不会乱。

flowchart TB A["输入:活动模板 yaml<br/>(已验证的骨架 —— 签到·协作·竞争·回流)<br/>+ 本季度主题·禁用奖励·日历槽位"] A --> B["第 2 步 AI:生成变体候选<br/>相同骨架 × 不同主题·奖励·周期<br/>→ 5~10 个候选 (草案)"] B --> C{"第 3 步 确定性:event_lint.py<br/>奖励范围·通胀限额·日程重叠<br/>·最近 N 周重复相同骨架"} C -->|违规 WARN| D["运营评审关卡<br/>(采纳·驳回·微调)"] C -->|通过| D D -->|重新请求| B D -->|采纳| E["纳入构建 → 公告<br/>(不可逆关卡)"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class A data; class B ai; class C,E code; class D human;

在这张图里,人的手只碰两个地方。最上面,把模板和本季度的约束干净地放进去的位置;最下面,判断 lint 抓不到的"这个主题合不合当下我们游戏的氛围"的位置。而这中间枯燥的候选量产和奖励算术,由模板、AI 和 lint 来跑。

决定性的设计在于:即便 lint(第 3 步)发现了违规,也不会自动丢弃候选,而只是把 WARN 上报给运营关卡(第 4 步)。理由见 §15.2.5。还有,最后那条箭头(公告)是不可逆的,这一点把运营和其他量产区分开来。城市 NPC 不满意的话,构建前废弃即可;但已向用户公告的活动,想收回时要付出社区信任的代价(§15.2.7)。


15.2.2 输入 —— 活动模板 yaml

把运营团队手里已验证的骨架固化为表单。若放任为自由格式的策划案,AI 就不知道该变体什么。槽位分开了,"只换这个槽位"才成立。

# event_templates/coop_raid.yaml —— 协作团本骨架 (已验证,运营 4 次)
template_id: coop_raid
purpose: [现有活跃, 社区]             # 只 1~2 个。禁止同时追求 4 个
core_loop: 活动期间全服累计贡献 → 按阶段解锁全服奖励
duration_range: [5, 10]              # 天。超过 10 天会累积疲劳
slots:                               # ← AI 变体的槽位。骨架固定
  theme: { type: 自由, 约束: 遵守季度主题 }
  boss_or_target: { type: 自由, 约束: 优先复用现有Boss素材 }
  reward_tiers: { type: 奖励列表, count: 3~5, 约束: 参照 reward_policy }
reward_policy:                       # ← lint 读取的槽位。禁止变体
  强化石_per_event_max: 30           # 单次活动发放上限
  金币_per_event_max: 50000
  限定时装: 允许 (永久拥有, 经济影响 0)
  现金类资源_直接发放: 禁止
inflation_guard:
  强化石_季度_累计上限: 90           # 季度内所有活动合计
post_event_kpi:                      # ← 事后自动测量槽位
  - 参与率 (相对活动曝光,参与 1 次以上)
  - 强化石价格变动 (事后 30 天,目标 ±10%)
  - 活动后工作日游戏时长 (依赖度信号)

最重要的分离是 slots(AI 变体)和 reward_policy(lint 读取,AI 不得触碰)。主题和 Boss 每次都可以不同,但强化石发放上限是游戏经济定下的红线。这条红线若被 AI 每次调用都抽出不同的数字,通胀就当场开始。所以奖励项目由 AI 提议,但奖励数量只能在策略范围内浮动,这由 lint 来卡。

同一个文件夹里,daily_attendance.yaml(签到)、pvp_ladder.yaml(竞争)、comeback.yaml(回流)以相同格式放着。这四张,就是本季度候选生成的全部输入池。


15.2.3 [实操记录] 一张模板 → 生成变体候选

把实际怎么跑的一个完整周期从头到尾展示一遍。输入提示词可以直接复制使用,输出则还原了真实的量产会话。

第 1 步 —— 提示词:让它做变体,但强制骨架和策略

用附上的 coop_raid.yaml(经 4 次验证的协作团本骨架),只给出下周(W2)的 5 个变体候选。
本季度主题是"夏日 —— 水·祭典·炎热"。
绝对不要动 core_loop,只改 slots(主题·Boss·奖励),
奖励只能在 reward_policy 上限之内,每个候选都附一行说明为何是这个主题·这个奖励。
最近 3 周是 签到·PvP排位·协作团本,所以对又跑协作团本的候选打 [重复注意],
拿不准就打 [存疑] 交给我。只用表格,列为 候选名·主题·奖励tier摘要·依据·标记。

六行规则里,指定主题或奖励的那种行,一行也没有。全都是"只能改到哪里、不许越过什么、拿不准就上报"的围栏。因为像"帮我做个夏日活动"这样放开,出来的就是普通手游 RPG 的平均水平。变体范围(只 slots)、奖励上限(reward_policy)、重复注意(最近 3 周的历史),以及"拿不准就交给人" —— 这道围栏让骨架被守住、让变体被放开。

第 2 步 —— Claude 的输出(还原真实会话,节选)

| 候选名        | 主题            | 奖励tier 摘要                  | 依据                              | 标记         |
|---------------|----------------|-------------------------------|----------------------------------|-------------|
| 拦洪之堤      | 阻挡河流泛滥    | 强化石 25 / 金币 4万 / 时装     | 协作=筑堤的隐喻,与累计贡献直连     |             |
| 放河灯        | 夏日水祭        | 强化石 20 / 金币 3万 / 称号     | 节日氛围,比起竞争更重烘托气氛      |             |
| 酷暑Boss战    | 讨伐酷暑化身    | 强化石 30 / 金币 5万 / 时装     | 复用现有Boss素材,奖励达上限        | [重复注意]   |
| 洋流竞速      | 乘潮流竞争      | 强化石 28 / 金币 4.5万 / 坐骑   | 协作+竞争混合                     | [存疑]       |
| 锦鲤收集      | 收集型协作      | 强化石 22 / 金币 3.5万 / 称号   | 轻度玩家进入门槛低                 |             |

[重复注意] 酷暑Boss战:core_loop 与 W-3 的协作团本实质相同。
  间隔 3 周重复相同骨架 → 恐引发用户疲劳。建议改用其他骨架(签到/收集)。
[存疑] 洋流竞速:把协作累计 + 个人竞争混进了同一个活动。
  purpose 槽位扩展为 [活跃+竞争]。可能与 1~2 个目的的原则冲突。

输出里最有价值的部分不是表格,而是最下面那两行。AI 报告了自己的局限并交给人 —— "酷暑Boss战和 3 周前的骨架相同""洋流竞速的目的增加到了两个"。好的提示词,能让 AI 说得出"这个我没把握"。

现在把这一批候选交给 lint 来卡。


15.2.4 第 3 步 lint —— 用代码卡住奖励范围·重复

候选是否守住了奖励策略和日程重叠,每次都用眼睛看,又会漏。凡是靠 reward_policy·inflation_guard·日历就能判定的,交给代码来评审。人只把时间花在代码抓不到的调性·趣味判断上。

# event_lint.py —— 活动变体候选验证 (骨架)
# 输入: AI 提出的候选列表 + 模板策略 + 季度日历
# 输出: WARN 列表 (不自动废弃 —— 上报到运营关卡)

def lint(candidates, policy, quarter_ledger, recent_weeks):
    warns = []
    stone_used = sum(quarter_ledger.强化石)   # 本季度已发放的累计
    for c in candidates:
        # A: 单次活动奖励上限 (策略)
        if c.强化石 > policy["强化石_per_event_max"]:
            warns.append(f"[A] {c.name}: 强化石 {c.强化石} > 上限 "
                         f"{policy['强化石_per_event_max']} (单次活动超出)")
        # B: 季度通胀累计上限
        if stone_used + c.强化石 > policy["强化石_季度_累计上限"]:
            warns.append(f"[B] {c.name}: 季度累计 {stone_used + c.强化石} > "
                         f"{policy['强化石_季度_累计上限']} (通胀限额)")
        # C: 最近 N 周重复相同骨架
        if c.template_id in recent_weeks[-2:]:
            warns.append(f"[C] {c.name}: {c.template_id} 骨架在最近 2 周出现过 (重复)")
        # D: 日历槽位冲突 (同一周有其他大型活动)
        if quarter_ledger.slot_taken(c.week):
            warns.append(f"[D] {c.name}: W{c.week} 槽位已排入大型活动")
    return warns

把上面实操记录里的五个候选放进这段代码,结果如下。

[PASS] 拦洪之堤: 强化石 25 ≤ 30, 季度累计 65+25=90 ≤ 90 (触及边界)
[WARN] [C] 酷暑Boss战: coop_raid 骨架在最近 2 周(W-3)出现过 (重复)
[WARN] [B] 洋流竞速: 季度累计 65+28=93 > 90 (超出通胀限额)
[PASS] 放河灯: 强化石 20 ≤ 30, 季度累计 65+20=85 ≤ 90
[PASS] 锦鲤收集: 强化石 22 ≤ 30, 季度累计 65+22=87 ≤ 90

这里有意思的是 洋流竞速。AI 因为目的冲突打了 [存疑],而 lint 却因为完全不同的理由 —— 超出季度通胀累计上限 —— 把它拦下。加上强化石 28,季度累计变成 93,超过策略规定的 90。AI 没看到的算术,被代码抓住了。反过来,酷暑Boss战则是 AI 的 [重复注意] 和 lint 的 [C] 指向了同一件事。人·AI·代码三者各用不同的网来筛。

多亏这 30 行,"这次奖励是不是有点重?"不再以感觉对感觉收场。只要代码打出 [B] 季度累计 93 > 90,就没什么可争的。要么下调奖励,要么换候选。


15.2.5 把一个周期走到底 —— 评审·驳回·重新请求

若只抽象地写"运营团队来评审",就看不出这道关卡实际筛掉了什么。这里把通过 lint 之后人杀掉什么、留下什么,完整地跟到底一次。

[第 4 步 运营评审 —— 判定]

运营策划这样处理了 5 个候选。

这里,人把通过了 lint 的 拦洪之堤 从第 1 位上动摇,正是这道关卡的核心。代码把 90 ≤ 90 判为 PASS。按策略不算违规。然而运营策划看的是整个季度的奖励节奏。lint 看的是单个活动的合法性,而人看的是季度末的赛季收尾。所以要走一次重新请求。

重新生成"拦洪之堤"的变体,把奖励从强化石 25 → 18 下调。
理由:6 月最后一周的赛季收尾冲刺需要留出 12 点强化石余量。
奖励吸引力下降的部分,改用限定时装·称号来
补强体感价值,据此重构 reward_tiers。

AI 把强化石降到 18,并把限定时装增加到 2 种(经济影响为 0 的永久拥有奖励),重新给出了候选。再跑一次 lint,季度累计 65+18=83 ≤ 90,赛季收尾还剩余量 7。输入 → 候选量产 → lint → 评审 → 驳回 → 重新请求的一个周期,在这里闭合。

这一圈,是本书全书的 Show 标准。工具吐出什么、什么被拦、人杀掉什么,若不哪怕一次地从头看到尾,"用 AI 量产了活动"这句话就是空的。

没有装自动废弃型 lint 的理由也在这个周期里。假如 lint 把 [B] 违规自动丢弃,运营团队就失去了学习 洋流竞速 真正问题(目的冲突)的机会,也失去了动摇 拦洪之堤 这种合法但按季度节奏有风险的候选的位置。可疑的候选由机器来挑,而采纳和驳回由人来定。


15.2.6 赛季 —— 更大的节奏,相同的分离

如果说活动是周\~月的节奏,那赛季就是季度的节奏。运营方式相同。赛季也一样,把已验证的要素分离成槽位,每季度就只换主题。

赛季槽位 变体(AI·人) 固定(策略·lint)
赛季主题 夏季·冬季·新年 (自由)
赛季通行证奖励轨道 各阶段奖励项 阶段数·完成难度·奖励上限
赛季 PvP 排行 排行奖励项 奖励通胀限额
Meta 洗牌 新角色·数值平衡 变更幅度护栏(§8.1)

赛季通行证里,人以策略固定的核心数值是完成率目标。把难度定到让大约 70% 的活跃用户能到达最终阶段,这是业界常被引用的基准(作者推测 —— 各游戏不同,所以应当读作方向而非绝对值:低于 30% 会挫败,超过 90% 则缺乏挑战感)。这个目标一旦录入槽位,AI 提议赛季通行证变体时,也能被强制一并算出"预期完成率"。

季度日历要一眼看清,活动和赛季才不会冲突。这更接近运营团队公用的台历。谁看都看到同一张图,冲突才会减少。

第2季度整合日历 —— 赛季 1 个(季度),活动以周为单位

赛季"夏日祭"(赛季通行证 50 阶 · PvP 排行) —— 4 月~6 月常驻

4月 5月 6月

W1 签到 W2 协作(堤坝)

W3 PvP排位 W4 收集协作

W5 回流 W6 赛季收尾

季度强化石通胀累计 (上限 90) 当前累计 83 / 限额 90 (余量 7 = 赛季收尾冲刺的份额)

签到·收集 协作·回流 竞争(PvP) 赛季活动

这一张图,把 §15.2.5 的判断用视觉解释了。颜色就是骨架种类。同一种颜色在 2\~3 周内出现两次,§15.2.4 的 lint [C] 就会响。 而下方的通胀量表已逼近红线(上限 90),6 月赛季收尾(W6)能用的余量只勉强剩 7 —— 正是把 拦洪之堤 奖励降到 18 才保住的那个 7。


15.2.7 不可逆关卡 —— 在公告前结束所有评审

城市 NPC(§6.2)或 HUD(§14.1)与运营有一点决定性的不同。公告无法收回。 NPC 调性不合,构建前废弃即可,用户根本不知道那个 NPC 存在过。然而已向用户公告的活动,其奖励·周期·规则都留在了社区里。开始之后再说"活动奖励太重了,要收回",就伴随着不可逆的代价。

flowchart LR A["模板变体候选"] -->|可逆| B["lint 检查"] B -->|可逆| C["运营评审·驳回·重新请求"] C -->|可逆| D["构建评审<br/>(可暂缓发布)"] D ==>|不可逆关卡| E["活动公告·开始"] E -.->|恢复成本高| F["只能做后期微调<br/>(回收奖励·变更周期都是信任成本)"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class A data; class B code; class C,D human; class F fail;

本书全书的原则(§5.4.5 配音录制、§8.1 线上构建、第 12 部分最终渲染都是同一条讯息),在运营中同样成立。所有评审 —— 奖励范围、通胀限额、日程冲突、调性 —— 都必须在公告前的可逆阶段结束。§15.2.3\~5 的量产·lint·评审·重新请求整个周期,之所以都在这道不可逆关卡的左侧转,原因就在这里。越过关卡之后能做的,顶多是 §15.2.8 的后期微调,而那也在一点点消耗用户信任。


15.2.8 运营中的信号与处方

公告之后 KPI 照看。只是,与公告前的评审不同,这里能做的只有后期微调。把自动测量的信号和人的处方分开。

信号 (自动测量) 处方 (人来决定)
参与率低于 50% 后期小幅增强奖励,或延长周期 +2 天 (在公告可信范围内)
参与率 95% 以上 太简单 —— 记下一周期的难度备忘,当前活动保持不变
强化石价格事后 30 天跌幅超过 -10% 强化 sink(限定商店),下季度下调通胀限额
活动后工作日游戏时长下降 活动依赖信号 —— 增强工作日内容吸引力,调节活动频率

最后一行(工作日游戏时长下降)是最常被漏掉的信号。只看活动期间的 DAU(Daily Active Users,日活跃用户),活动总像是成功。然而活动结束后,若用户在工作日不回来,就意味着活动在吸食平日游戏本身的吸引力。所以在 §15.2.2 模板的 post_event_kpi 里,从一开始就把"活动后工作日游戏时长"作为槽位录入。不测量,就无法处方。


15.2.9 效果能诚实地说到哪一步

活动这一章,很容易起想放一张"跑了协作活动,留存率就从 30% 涨到 50%"这样的表的念头。这类数字如果未经验证,会削掉本书的信任。本章能说的只有三点。

第一,方向可以用业界观察来说。 签到奖励强化型活动会拉高短期活跃用户数,协作活动会提升社区凝聚,限定礼包会拉高活动期间的营收 —— 这是观察在线运营游戏而来的业界通识。只是多少会因游戏·用户构成而偏差很大,把别家公司的数字照搬过来很危险。

第二,作者的推测就写成推测。 "赛季通行证完成率目标 70%""活动周期超过 10 天会累积疲劳""活动量产半天→一小时"都是作者基于经验的推测,是未经验证的假设。别去背绝对值,读作结构(模板+lint 取代白纸策划)就行。

第三,只把可测量的东西作为 KPI 来承诺。 留存率这类结果指标不会由单个活动左右,所以不去断定因果。相对地,这套工作流真正能让其变得可测量的,是这些东西 —— lint WARN 件数(直到奖励违规变为 0)、季度通胀累计(相对上限)、相同骨架重复间隔(周)、各活动的参与率与事后强化石价格变动。这四项,在会议上就能用数字而非"感觉"来说。


15.2.10 常见的失败

模式 为何失败 处方
每周从白纸开始策划活动 慢且结果不稳定 把已验证的骨架录入模板 yaml (§15.2.2)
"AI 帮我做个夏日活动"整包外包 出来的是普通 RPG 平均水平的活动 固定骨架 + 只变体槽位 (§15.2.3)
奖励数量让 AI 自由提议 通胀当场开始 用 lint 强制 reward_policy (§15.2.4)
只用眼睛评审候选 每次都漏掉季度累计·重复间隔 用 event_lint.py 自动验证 (§15.2.4)
lint 通过 = 直接采纳 看不见季度节奏·目的冲突 人工关卡要看整个季度 (§15.2.5)
公告后试图回收奖励 不可逆的信任成本 所有评审都在公告前完成 (§15.2.7)
只测量活动期间的 DAU 看不见对工作日吸引力的侵蚀 事后工作日游戏时长槽位 (§15.2.8)

第五个最常被漏掉。一 lint PASS 就直接送去公告,那么像 拦洪之堤 那样合法但会把季度末奖励余力变为 0的候选,就没了被动摇的位置。代码看单个活动的合法性,人看整个季度的节奏。


15.2.11 动手试试 —— 今天就能做的一步

一个人的话,做到这些就够了:不用 lint 代码也行。从你自己的游戏(或你喜欢的在线运营游戏)里,挑一个常见的活动骨架,按 §15.2.2 的格式手写一份模板 yaml(core_loop·slots·reward_policy 这三栏是核心)。然后贴上 §15.2.3 的提示词抽出 5 个变体候选,再从中挑一个你觉得"奖励太重"的,反驳它"这个超出了本月的奖励余力,调低了重来"。采纳和驳回究竟是怎样一组判断,会亲身体会到。

如果是团队,就从下面这一步开始。把常跑的 3\~4 个活动骨架录入为模板 yaml,先把 event_lint.py 的三行(奖励上限·季度通胀累计·重复间隔)做成代码。哪怕只有模板和这三行,也能先挡住"每周白纸策划"和"奖励凭感觉定"这两种常见失败。这套工作流,是 §15.1.5 的渐进式应用骨架三要素 —— 活动模板·赛季规则库、AI 活动候选生成器、事后自动测量 —— 的第一个实务实现。


本章要点

下一章预告

15.3 把100条反馈归为主题——聚类交给LLM,优先级交给人

主要读者:负责运营(LiveOps)中用户应对的策划、总监(中等规模(10\~50人)团队) 面向个人/业余读者的精简版:§15.3.7「一个人的话,只做这些」

先坦白一点。笔者在产品上线后以1\~2年为周期亲自负责运营的经验并不算长。本章的相当一部分,是建立在24年从业经历之上的业界观察与相邻经验。所以本章不会断言"运营就该这么做"。相反,笔者把上线前量产内容时验证过的输入 → AI → 验证 → 人工决策这一循环,原样套到用户反馈这个输入上,完整地跑一遍,看会得出什么。工具的骨架与§6.2的city_hunting_generator相同,只是输入从"城市元数据"换成了"100条用户反馈"。

运营第一周的景象大都相似。论坛、Discord、客服工单、商店评论每天堆积成百上千条。人不可能全部读完,而不读的话,同一个bug会有50条报告被埋掉。本章讲的方法是:先让LLM把这堆反馈按主题归类、并按情感打分,然后人只投入到"那么这周该修什么"这一优先级决策上。


15.3.1 反馈不是'读物',而是'分类输入'

把反馈分成4个渠道(游戏内问卷、论坛/Discord、商店评论、客服工单)、再归为4种类型(bug、请求、不满、表扬)的表格,哪本运营教科书里都有。这些说的都对。问题在于,就算背下这张表,也答不出"今天涌进来的412条该怎么处理"。只要还把反馈看成供人阅读和分类的对象,反馈量就永远会压过运营团队的人手。

换个视角。一条反馈就是一份结构化输入,是一条拥有 {来源, 原文, 主题, 情感, 严重度} 五个槽位的记录。这样一看,工作的本质就变了:不是"全部读完",而是"按主题归类、排出优先级"。而且,主题聚类和情感打分,人来做既枯燥、每次的标准还会飘移,但机器会用同一把尺子同样地量这100条。这正是LLM比人更擅长的那类工作。§6.2中量产30座城市时的那套分工(规则手册=确定性,正文=AI,审核=人),在这里同样成立。唯一的不同是:最后人做的事不再是"正文审核",而是"优先级决策"。

这里点明反馈类型的一个分布特征。会主动发言的用户,往往不是满意的用户,而是心怀不满的用户。满意的客人会悄悄离开,不满的客人才会重新走回柜台。所以论坛、评论里的情感分布,往往会比实际全体用户的满意度更偏向负面(笔者的观察——具体偏差幅度因游戏、渠道、时期而异,所以不该当成绝对数字,而应作为方向来读)。只有把这种偏差放在心里,当你在聚类结果里看到"负面60%"时,才不会误读成游戏正在走向失败。


15.3.2 [实操记录] 100条反馈 → 主题聚类 + 情感

下面实际把一个循环完整跑一遍。输入是某一周从4个渠道汇集的100条反馈,输出是主题聚类、情感、优先级。输入提示词可以原样复制使用,下面的输出是按真实分类会话的格式重现的。

第1步——输入:把反馈做成机器能读的表

把从各渠道抓取来的原文,规范化成一行一条记录。这不是重新写,只需要抽取、整理即可。

{"id": "fb_0001", "src": "discord",     "text": "强化12级失败了50次。这概率正常吗?请退款"}
{"id": "fb_0002", "src": "store_review","text": "画面很漂亮,但太卡了,每次公会战都闪退"}
{"id": "fb_0003", "src": "cs_ticket",   "text": "已经付款了但钻石没到账,附上订单号"}
{"id": "fb_0004", "src": "forum",       "text": "新职业弓箭手什么时候出啊 呜呜 预约的时候不是承诺过了吗"}
{"id": "fb_0005", "src": "discord",     "text": "开服第一周,运营团队沟通得不错,公告也快。以后也拜托了"}
{"id": "fb_0006", "src": "store_review","text": "某个Boss(黑狼)的伤害太离谱了。满装备也被一下秒杀。要求做平衡性补丁"}
{"id": "fb_0007", "src": "cs_ticket",   "text": "教程第5步无法继续,按钮点不动(设备:Galaxy A系列)"}
// ... fb_0008 ~ fb_0100 (省略)

记录在输入阶段先把 主题·情感·严重度 留空。填上这些空格,就是第2步LLM的工作。

第2步——提示词:让它聚类,但强制约束标签、格式与退出口

把附件feedback_100.jsonl(一周的100条反馈)按主题归类,并同时给情感打分。
主题只能从这个列表里选(禁止自由生成):强化/概率、平衡、服务器/性能、支付/退款、
新内容请求、教程/新手引导、UI/操作、表扬/支持、其他。「其他」超过8条,就再提出新主题候选。
情感分为负面、中立、正面,严重度用S1、S2、S3、S4。
// (意图:S1仅限具体、可复现、功能阻断的情况。单纯的强烈不满归为S2)
没把握的条目就放进「其他」,并在id末尾加★交给我,不要硬凑。
只给两张表 —— 表A(逐条):id、主题、情感、严重度 / 表B(按主题):主题、条数、负面%、代表原文1条、是否含S1。

在这条提示词里,真正左右分类结果的,既不是主题列表,也不是情感刻度,而是固定标签、钉死格式、留出退出口这三道控制装置。如果放任LLM自由生成主题,它就会造出"整体不满意""希望改善游戏性"这类无法用于排优先级的笼统标签。允许的标签列表(规则1)、严重度定义(规则3——S1的狭窄定义是关键)、输出格式(两张表),以及"没把握就加★交给人"(规则5)。这四点一旦缺失,表格就会从优先级工具沦为又一份读物。这份标签列表本身,就是§15.1.5里说的反馈L4层的分类体系——标签固定下来,才能每个季度都用同一根轴来比较走势。

第3步——Claude的输出(重现,节选)

[表A] 逐条分类(节选)
id       | 主题           | 情感 | 严重度
fb_0001  | 强化/概率       | 负面 | S2
fb_0002  | 服务器/性能      | 负面 | S2
fb_0003  | 支付/退款       | 负面 | S1
fb_0004  | 新内容请求       | 中立 | S3
fb_0005  | 表扬/支持       | 正面 | S4
fb_0006  | 平衡           | 负面 | S1
fb_0007  | 教程/新手引导    | 负面 | S1
fb_0042★ | 其他           | 中立 | S3
...

[表B] 按主题汇总(全部100条)
主题           | 条数 | 负面% | 代表原文                        | 含S1
服务器/性能     | 24  | 92%  | "每次公会战都闪退"                | 是(部分可复现)
强化/概率       | 21  | 95%  | "12级失败了50次"                 | 否
平衡           | 14  | 86%  | "黑狼满装备也被秒"                | 是
新内容请求      | 12  | 8%   | "弓箭手什么时候出"                | 否
教程/新手引导   | 9   | 78%  | "第5步按钮点不动(Galaxy A)"       | 是
支付/退款       | 8   | 100% | "钻石没到账"                    | 是
UI/操作        | 6   | 67%  | "技能按钮太小误触"                | 否
表扬/支持       | 4   | 0%   | "运营团队沟通得不错"              | 否
其他           | 2   | -    | (fb_0042★、fb_0067★ 待人工判定)   | -

整体情感:负面71 / 中立18 / 正面11
「其他」2条(不足8条)→ 无需提出新主题
未达把握★标记2条:fb_0042、fb_0067(请求人工判定)

这段输出里最有价值的部分不是表格,而是最下面两行,那"★标记2条"。这是LLM主动申报自己归不了类、并把它交给人的地方。这与§6.2中AI给NPC'格雷姆'自行打上"存疑标记"是同一套设计。好的提示词,会让AI能够说出"这个我没把握"。

第4步——验证与否决(人的位置)

这份输出不能照单全收。有一处确实卡住了。

强化/概率 主题的21条全部被归为S2(不满)。可其中fb_0001带了一句"请退款"。LLM只把它看作"强烈不满(S2)"。这里就需要人介入。对强化概率的不满——只要数据显示概率是按规格运作的——就不是S1事故。因为对按规格运转的概率的不满,是设计、体感问题,而不是bug。LLM的S2判定没错。只是"要求退款"这个信号,应当cross-link到支付主题,让客服另行处理。LLM只给主题打了单一标签,漏掉了一条同时横跨两个主题的情况。

于是重新请求。

新增规则:如果一条同时横跨两个主题(例如:强化不满+退款要求),在主主题之外,
在「cross」列写上次要主题。给表A增加cross列后重新输出。
但强化概率的不满本身,只要数据上概率符合规格,就不算S1,而保持为S2。

这一次往返就结束了。LLM对fb_0001重新给出了 主题=强化/概率, cross=支付/退款, 严重度=S2,而那★标记的2条则由人亲自阅读,把fb_0042重新归入 UI/操作、fb_0067归入 教程/新手引导100条若由人从头读、从头分类要耗上大半天,而LLM初稿 + 人工审核 + 一次往返则在一小时以内(笔者估计,未经验证的假设——具体能省多少,会随反馈条数、渠道数量而变,所以与其记绝对时间,不如把它当作"从头手工"和"初稿+审核"之间的结构差异来读)。


15.3.3 优先级LLM给不了——人的位置

这里划出一条决定性的界线。上面的表B只说到"哪个主题有多少条、有多负面"为止。"那么这周该先修什么",LLM是给不出的。 那是牵涉成本、日程、游戏愿景的决策,而这个决策的责任在总监身上。

面对同一张表,两个运营团队可能做出截然相反的决定。只看条数,服务器/性能(24条)和强化/概率(21条)排在第1、2位。可优先级并不按条数顺序走。原因在于严重度与可逆性

flowchart TB A["100条反馈<br/>(4渠道规范化jsonl)"] --> B["LLM聚类<br/>主题·情感·严重度打分"] B --> C{"人工审核<br/>★条目·cross·误分类"} C -->|重新请求| B C -->|确认| D["按主题汇总表<br/>条数·负面%·含S1"] D --> E["优先级决策<br/>(总监——LLM不可)"] E --> F1["S1事故:立即热修复<br/>支付·教程阻断"] E --> F2["S2不满:确认数据后<br/>设计判断"] E --> F3["S3请求:季度待办<br/>固化到voice槽位"] F1 --> G["回复循环<br/>(§15.3.4)"] F2 --> G F3 --> G classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class A,D data; class B ai; class C,E human;

在这个流程里,人手真正触及的只有两处:中间的审核关卡(判定★、cross、误分类),以及最下方的优先级决策。中间那枯燥的100条分类,交给LLM去跑。而优先级决策真正的逻辑,不是条数,而是下面三根轴。

主题 条数 优先级判断(总监的职责)
支付/退款 (S1) 8 第1优先。 条数虽少,但功能阻断 + 不可逆(涉及钱)。24h热修复
教程/新手引导 (S1) 9 第2优先。 直接关系到新用户流失。特定设备可复现 → 打补丁
服务器/性能 24 第3优先。 条数最多,但属于基础设施改动 = 周期长。无法热修复,放到下周
强化/概率 (S2) 21 维持。 数据上符合规格就不是bug。作为设计决策另行研究
新内容请求 12 待办。 负面8%(=正面期待)。固化到季度voice槽位

条数第1的服务器/性能之所以降到优先级第3,是因为它是热修复修不了的基础设施工作;条数第6的支付/退款之所以升到第1,是因为它是牵涉金钱的不可逆事故。这种重新排序,LLM做不到。 LLM只能给出"支付8条、负面100%"这样的事实。至于"它是第1优先"这个判断,则属于懂得成本、法律风险、游戏愿景的人。这正是§15.1.5里所说"AI负责分类、生成候选,人则专注于采纳与愿景决策"在反馈领域的真实样貌。


15.3.4 回复——不可逆阶段,审核关卡更重

优先级定下来后,就要回复用户。在运营中,回复缺位是对信任伤害最大的地方。哪怕没有可答的内容,一句"正在处理"也胜过毫无回应。回复草案同样可以让LLM按主题生成。

[回复草案——LLM输出,按主题]

这里与§6.2有一处决定性的不同。回复的发送是不可逆阶段。 城市NPC废弃后重做即可,但用户一旦看过的公告、回复文本,是收不回来的。若自动发出"24小时内发放",实际却花了三天,那个承诺就会以不可逆的痕迹留在社区里。所以§15.1.4的不可逆阶段原则,在反馈领域比在其他领域运作得更重。自动回复草案交给LLM生成,但在通过客服审核关卡之前,一个字也不自动发送。 审核者只看:日程承诺(24h、下周)是否与实际工作日程相符,以及敏感个案(法律纠纷、退款纠纷)有没有混进自动发送池。这是让人来承担lint抓不到的判断的位置。

阶段 可逆性 由谁
反馈聚类、情感打分 可逆(可自由重跑) LLM
主题审核、优先级决策 可逆(确定前) 人(总监)
回复草案生成 可逆(可废弃、重写) LLM
回复发送、公告发布 不可逆(用户已感知) 人(客服审核后)

15.3.5 把用户voice固化进季度复盘

要让同样的反馈不至于每个季度被摇摆成不同的决定,就得把聚类结果固化为季度复盘的固定输入槽位。不是凭一时印象说"最近强化的不满好像挺多",而是每个季度用同一根标签轴汇总出的表,进入复盘表格之中。§15.3.2里禁止自由生成标签、用允许列表把它固定下来的理由,在这里得到回收。

2026 Q2 用户voice(LLM自动汇总,季度累计)

4类渠道累计约5,000条聚类(条数为季度实际统计——并非加工)

负面靠前主题:   强化/概率 > 服务器/性能 > 平衡 > 支付/退款
请求靠前主题:   新职业 > 新狩猎场 > 公会系统 > UI改进
季度情感走势:   Q1负面68% → Q2负面71%(小幅恶化——强化主题拉动)

这张表成为季度决策的输入。决策本身归总监,输入归用户。季度走势("Q1 68% → Q2 71%")只作为方向来读。信号不是单个季度的绝对值,而是同一根标签轴上的变化方向。如果负面%升高了,就回溯"是哪个主题把它拉上去的",并连接到下一季度的优先级。这份季度报告的草案本身,也由LLM用自然语言生成,人只在上面加决策批注——这正是§15.1.5里所说季度报告自动初稿的真实位置。


15.3.6 诚实对待数字的方法

运营这一章,很容易忍不住想放上"引入反馈循环后,NPS从20升到了45"这样的表格。笔者从未测量过那种因果,所以不写。本书的原则是以下三条之一。

第一,实测统计的条数照原样使用。 §15.3.2里按主题的条数(服务器24、强化21、支付8)和§15.3.5的季度累计,都是把分类结果一条条数出来的值,而不是为了好看而凑出的比例。

第二,估计就写明是估计。 "100条分类从大半天→一小时"(§15.3.2)、"论坛情感偏向负面"(§15.3.1)是基于笔者经验、观察的估计,是未经验证的假设。不必去记绝对值,只要作为方向(反馈量永远压过人手;主动发的帖子往往偏向不满)来读就好。

第三,只把可测量的东西作为指标来承诺。 反馈循环真正可测量的,不是结果满意度(NPS),而是过程指标——未分类反馈的存量(目标0)、S1事故发现→热修复的前置时间(lead time)、回复响应时间、"其他"主题的占比(允许标签装不下现实时,"其他"就会膨胀)。这四项,在会议上都能用数字而非"感觉"来说。


15.3.7 动手试试——今天就能做的一步

一个人的话,只做这些:不需要客服系统,也不需要数据集。把你自己游戏(或你喜欢的游戏)的商店评论、社区帖子,手动复制20\~30条做成jsonl({"id":..., "src":..., "text":...}),把§15.3.2的提示词原样贴上去跑一遍。在得到的表B里,找出"条数第1的主题"和"你最想先修的主题"不一致的那一处,用一行字写下为什么不同——你就会切身体会到,优先级为什么不是LLM的活,而是人的活。

如果是团队,就从下面这一步开始。先把"将4渠道反馈汇集成一行一条记录jsonl的抽取脚本",以及§15.3.2的允许主题标签列表固定下来。标签固定了,无论是LLM分类还是人工分类,才能用同一根轴来衡量、比较季度走势。自动回复是再往后的事——回复不可逆,没有客服审核关卡,就绝不接入自动发送。


本章要点

下一章预告

16.1 战斗 TF 运营 —— 在隔离的工作空间里,只让决策进入正本

周四下午4点。战斗 TF 会议结束,7个人各自回到工位。白板上留着关于是否要把全局冷却(GCD)从0.8秒下调到0.5秒的痕迹。资深数值策划说"在我的模拟里0.5才对",代码主程说"0.5的话服务器 tick 跟不上"。UI 设计师说"两边我都说不好,只知道冷却条的宽度会变得太窄"。

三个人说的都对。而一旦三个人都开始把各自的结论写进自己领域的文档,下周这三份文档就会相互冲突。数值表里写着0.5,代码规格里写着0.8,UI 指南里写着0.6。谁看都分不清哪一份才是正本。

战斗 TF 存在的理由,正是在一个地方吸收这种冲突。而这种吸收的产物——唯一的一个决策——才应当进入正本文档。其余讨论的残渣都应当止步于隔离的工作空间之内。本章讨论的就是这种隔离与吸收的机制。


16.1.1 TF 不是常设部门,而是隔离的工作空间

战斗系统的大改不会只涉及一个工种。只要动一个全局冷却,平衡(数值)、代码(服务器 tick)、UI(计量条呈现)、动画(动作时长)、音效(打击感)就会同时受到牵动。如果把这类议题按领域各自推进,决策会拖上2\~4周,而且即便做出决策,各领域之间也会彼此错位。

TF(TaskForce,专项组)就是为了防止这种错位,把多个工种暂时聚到一个工作空间里的单位。关键在于"暂时"和"隔离"。如果把 TF 的讨论原样流入公司的正本文档体系,未经验证的讨论、被否决的方案、正在试验中的数值就会污染正本。因此我们在 SVN 里建立一个以 95_ 编号开头的隔离工作空间。

95_BattleTF。95 号段的编号是一个约定,表示短期 TF 工作空间。一般的正本 docs 使用 10 号段、20 号段的编号,而 90 号段是"临时·隔离·计划终止"的信号。只看文件夹编号,就能立刻传达出"这里不是正本,不要引用在这里看到的数值"。

隔离的规则很简单。

TF 固化为常设部门之所以危险,原因就在这里。一旦隔离被打破,TF 工作空间里未经验证的数值就会开始被当作正本引用,同样的决策每个季度都会在别的场合被重新推翻。


16.1.2 从隔离到吸收:整体流程

战斗 TF 的一个周期,结构上就是:开启一个隔离空间,在其中积累讨论、试验与决策,终止时只把决策吸收进正本。

flowchart TD A["战斗议题出现<br/>(全局冷却 0.8→0.5?)"] --> B["开设 95_BattleTF 隔离空间<br/>(SVN 95_ 号段)"] B --> C["隔离内部产出累积<br/>会议记录·试验表·被否方案·原始笔记"] C --> D{"得出决策?"} D -->|尚未| C D -->|确定| E["更新 TF_결정사항_요약.md<br/>(隔离空间内部)"] E --> F{"TF 终止?"} F -->|存续| C F -->|终止| G["仅将 TF_결정사항_요약.md<br/>一份晋级到正本 docs"] G --> H["其余全部<br/>降级到 95_BattleTF/archive/"] G --> I["只向美术团队交付 html<br/>(md 原件不共享)"] H --> J["关闭隔离空间"] style B fill:#fff3cd,stroke:#d39e00 style G fill:#d4edda,stroke:#28a745 style H fill:#f8d7da,stroke:#dc3545

议题从左上角进来,在黄色的隔离空间里处理掉所有杂音,只有绿色的那一格——决策摘要——离开进入正本。红色代表降级。这一张图就是 95 号段工作空间运营的全部。


16.1.3 实操记录(worked transcript)—— 把决策摘要整理成可吸收的形态

TF 终止时最费手的事,是从一个季度的会议记录、试验表里筛出"只有要晋级到正本的决策"。讨论很长,被否决的方案和已确定的方案混在一起,同一个数值在每次会议里写得都略有不同。这件事若由人手工整理,光是终止工作就要花掉一整天。

以下是我实际运行的提示词、Claude 的原始输出,以及我如何验证、否决、再请求它的全过程。这份实操记录(worked transcript,完整保留的真实操作过程记录)不做摘要,原样刊出。

第一次提示词(全文)

从下面 95_BattleTF 的 6 份会议记录中,只挑出要晋级到正本的已确定决策,
生成 TF_결정사항_요약.md 的草稿。TF 即将终止。
只要已确定的(排除被否决、试验中、"下次再说"的),每条决策
按 决策ID·主题·确定值·依据(数据来源)·决策者·确定日期 的格式。
同一主题若在每次会议里数值有变,只保留最终值,舍弃之前的。
是否确定若不明确,不要舍弃,单独归入 [需复核];若没有依据来源,标注 [依据未记录]。

会议记录:
--- 会议记录 2026-W14-mon ---
议题1:全局冷却 0.8→0.5 评估。数值模拟(sim_gcd_0407.xlsx)显示
0.5 使 DPS 方差 -12%。代码主程:0.5 因服务器 tick(33ms)上限,请求暂缓。
决策:初步一致同意 0.6(暂定)。下次会议前由代码侧做 0.6 负载测试。
议题2:闪避 i-frame 12f→10f。动画总监称动作为 8f,10f 会造成视觉断裂。
决策:确定维持 12f。决策者 策划主管。
...(无省略,实际放入的是 6 份会议记录全文)...
--- 会议记录 2026-W16-thu ---
议题1:全局冷却。0.6 负载测试通过(服务器 tick 余量 18%)。
决策:确定 0.6。决策者 策划主管。依据 sim_gcd_0407.xlsx + load_0415.log。
议题3:击杀奖励金币 1.2 倍。数值提案,无依据。暂缓。

Claude 的原始输出(原样)

# TF_결정사항_요약.md(草稿)

## 已确定决策

### D-GCD-01 | 全局冷却
- 确定值:0.6 秒(原 0.8 秒)
- 依据:sim_gcd_0407.xlsx(DPS 方差影响)、load_0415.log(服务器 tick 负载余量 18%)
- 决策者:策划主管
- 确定日期:2026-W16-thu
- 备注:W14 曾提出 0.5,但因服务器 tick 上限收敛到 0.6。0.5 方案作废。

### D-IFRAME-01 | 闪避 i-frame
- 确定值:维持 12 帧
- 依据:[依据未记录] —— 仅记录了"动画动作 8f 会造成视觉断裂"这一定性判断
- 决策者:策划主管
- 确定日期:2026-W14-mon

## [需复核]

### R-COIN-01 | 击杀奖励金币 1.2 倍
- 状态:暂缓(数值提案,无依据数据)
- 判断:因未提交依据无法确定。建议 TF 终止后转为数值团队的单独议题。

我的验证 —— 接受了什么,拒绝了什么

我逐条看了三个项目。

D-GCD-01 原样通过。 舍弃 W14 的 0.5/0.6 暂定方案、只留下 W16 的 0.6 确定值,这是准确的。规则 3 得到了正确执行。把两个依据来源合在一起也没错。

D-IFRAME-01 被我拒绝了。 Claude 把它归类为"确定",但依据是 [依据未记录]。仅凭定性判断(视觉断裂)就放进确定决策,看正本的其他人就找不到"为什么是 12f"的依据。这是规则 1 与规则 5 的冲突情形——Claude 因为决策者已确定就当作"确定",而我必须应用"没有依据的确定不能进正本"这条我们的 docs 政策。这条政策没有写在会议记录里,Claude 自然无从知晓。

R-COIN-01 分类正确,但开的"处方"过头了。 "建议转为数值团队的单独议题"是 Claude 编造出来的流程。我们公司没有那样的转交轨道。分类(需复核)予以采纳,处方句子则舍弃。

第二次再请求提示词

把验证结果反映进去,重做一遍。D-GCD-01 原样保留,
D-IFRAME-01 因为依据只有定性判断,从确定里拿掉,移到 [补充依据后晋级]。
R-COIN-01 保留分类,但去掉"建议转交"之类的处方句子——我们公司的流程你不清楚,只写状态和事实。
另外,不要编造会议记录里没有的流程、轨道、团队名称,不确定就用 [需策划主管判断] 留空。
// (意图:D-IFRAME-01 晋级正本的条件 = 会议记录附上动画总监的动作帧测量数据)

第二次输出(最终版)

# TF_결정사항_요약.md

## 已确定决策(晋级正本对象)

### D-GCD-01 | 全局冷却
- 确定值:0.6 秒(原 0.8 秒)
- 依据:sim_gcd_0407.xlsx、load_0415.log(服务器 tick 负载余量 18%)
- 决策者:策划主管 / 确定日期:2026-W16-thu
- 备注:0.5 方案因服务器 tick 上限作废。

## [补充依据后晋级]

### D-IFRAME-01 | 闪避 i-frame 维持 12f
- 仅记录了定性判断(相比动画动作 8f 的视觉断裂)。
- 晋级条件:会议记录附上动作帧测量数据。

## [需策划主管判断]

### R-COIN-01 | 击杀奖励金币 1.2 倍
- 状态:暂缓。数值提案,无依据数据。无确定值。

在这份最终版里,只有"已确定决策"这一节——D-GCD-01 一条——晋级到了正本 docs。其余两节都下沉到了 archive/。一个季度的 6 份会议记录,就此被吸收成正本里的一行。

AI 做到的和做不到的,在这里分野。AI 横跨 6 份会议记录,追踪同一主题的数值变化,分离被否方案,标注依据缺失——把六份会议记录一行一行对照的这种简单重复,恰恰是最容易从人手里漏掉的环节。但"没有依据的确定不能进正本"的政策适用、"没有转交轨道"这一公司事实、"确定/暂缓"的最终判断,全都由人来做。抹去 AI 那部分,抽取与整理的劳动就消失了;但正本里放什么,这个决定仍留在人的手里。


16.1.4 外部请求以 3-track 分类后进入

进入 TF 的议题并非都来自内部。发行商、美术外包、商务团队会提出"帮忙做一下这个战斗相关的事"的请求。如果不加甄别地都当作 TF 议题接下来,TF 就会沦为对外的接诉窗口。

因此,外部请求一收到就分成三条支线。只有需要战斗决策的才投入 95_BattleTF,一个领域就能了结的由负责人单独处理,超出范围、依据不足的则写明理由后回复、暂缓。进入 TF 的只有第一条支线——这是防止 TF 变质为接诉窗口的第一道防线。分类本身是人的判断,但读一读收到的请求文本、先给"这件事牵涉几个领域"打上初步标签这种程度,可以先让 AI 过一遍。

这套三角分类(request-triangulate)的判定顺序、实操记录、各轨道的后续处理,由下一章 16.2 专门讲。这里只点明"TF 只接第一条支线"这条入口规则。


16.1.5 只向美术团队交付 html —— md 无需学习

TF 决策晋级为正本后,就把它共享给相关团队。这里存在一种不对称。对美术团队不给 Markdown 原件(.md),只交付渲染后的 html。

理由很简单。美术团队只需要知道决策的结果。"冷却条请按 0.6 秒为基准重新确定宽度"——这一句就是他们需要的全部。md 原件里含有决策 ID 体系、atom 引用、被否决的 0.5 方案的痕迹、依据数据的文件名。这些是策划与代码共享的工作语言,并不是美术需要去学习的东西。

若原样把 md 给过去,美术团队要付出两种成本。其一,为解读与自己无关的标注体系而耗费时间。其二,可能把未经验证、已被否决的信息误当成决策。html 挡住了这两点——只呈现干净渲染后的决策结果,内部标注在构建过程中被过滤掉。

写成原则就是:工作语言(md)只在使用它的工种内部流转,对外只输出成果物(html)。 这与 TF 工作空间隔离(95 号段)是同一套哲学。内部使用的原始素材留在内部,对外只送出被吸收后的结果。


16.1.6 TF 运营的根基 —— 五条原则

隔离·吸收机制要运转起来,底下必须铺着五条运营原则。缺了任何一条,TF 都会垮成一个清谈场。

五条原则捆在一起运作时,隔离的 95 号段空间就不再是清谈场,而成为决策工厂。


16.1.7 常见陷阱

整理 TF 运营中期以后反复出现的陷阱与处方。

陷阱 症状 处方
变质为清谈场 只交换意见,不出决策 每次会议强制留 N 个决策槽位
越权 TF 干预其他领域的决策 明确决策权表
成员负担过重 重复参加 5\~6 个 TF,侵蚀本职工作 TF 参与合计每周上限 8 小时
永久化 不解散,反复开同样的会 季度再评估
隔离泄漏 95 号段未经验证的数值被当作正本引用 晋级正本只限决策摘要一份
对外脱节 决策不向外部共享 晋级正本 + 交付 html

隔离泄漏最安静,也最危险。文件夹编号的约定一旦崩塌,一切都会崩塌。


16.1.8 度量 —— TF 所吸收的东西

仅从作者项目A的运营记录中摘取方向与比例。下面的数值不是绝对值,而是相对于没有 TF 时、运营 TF 后的变化方向——绝对周期因团队规模、构建周期而异(基于作者环境的观察)。

项目 无 TF 运营 TF 方向
单条战斗决策的周期 各领域分头,数周 数天量级 缩短
决策后领域间冲突 每季度多起 每季度少数 减少
游戏总监升级(escalation) 每周多起 每周 1\~2 起 减少
领域间信息共享 零散 靠会议记录、晋级正本固定下来 体系化

回收最多的,是游戏总监的时间。领域间的决策被 TF 在隔离空间里吸收掉,因此上升到总监层面的冲突就减少了。TF 归根结底是一个装置:把"过去由总监逐一调解的领域间共识",下放到一个工作空间里来处理。


本章要点


游戏之外的应用。 "在隔离的工作空间里,只让决策进入正本"这一原理,原封不动地适用于所有与游戏无关的跨部门项目。比如设想一个由市场、法务、销售一起讨论新版条款修订的 TF。把会议记录、评审意见、被否决的措辞草稿放在共享云盘的临时文件夹(像 95_약관TF 这样的隔离空间)里,TF 结束后只把 최종_확정문구.docx 一份上传到公司正本文档库,其余下沉到归档。这样一来,六个月后当有人问"这条条款当初为什么这么定"时,就能避免未确定的草稿冒充正本混进来的事故。


动手试试 —— 季度终止时的决策吸收

setup - 在 SVN(或文件夹)里建立 95_BattleTF/ 隔离空间,把一个季度的会议记录都收进去。 - 预先建好 95_BattleTF/archive/(降级对象要去的地方)。

prompt - 把本章第一次提示词连同会议记录全文一起贴上。核心规则:① 只要已确定决策 ② 同一主题只留最终值 ③ 不明确时不要舍弃,分开标注 ④ 无依据就写明 ⑤ 不要编造公司流程、团队名称。

verify - 逐条查看输出中的"确定"分类。依据只有定性判断的项目,从"确定"里拉下来(适用晋级正本政策)。 - 检查 AI 生成的处方句子(转交、轨道、建议)里是否混入了并不存在的流程,发现就删掉。 - 只把"已确定决策"这一节复制到正本 docs,其余下沉到 archive/


16.1.9 单人精简版

对独自工作的单人开发者,隔离·吸收同样有效。把"TF"换成"我脑子里的多个角色"就行了。

有了隔离空间,仅凭文件夹位置就能区分"这个数值是已确定还是还在试验中"。即使是一个人,这也是不把同样的混乱留给未来的自己的最便宜的方法。

16.2 与其他工种的协作 —— 把外部请求分入三条轨道(3-track)

周二上午,即时通讯工具几乎同时响了三次。

美术主管:"战斗特效的配色,现在的色调太暗沉了,可以做得更明快一些吗?"

QA主管:"存在公会签到奖励发放两次的情况。附上复现视频。"

发行商负责人:"请在东南亚版本中落实伊斯兰文化圈的指南。要在下个季度审核之前完成。"

三条消息的字数相近。然而,一条是30分钟就能搞定的事,一条是必须立刻拉住代码主管处理的事故,一条是要塞进季度计划里的外部排期。仅仅因为它们落进了同一个收件箱就以同等分量对待的话,就会在30分钟的小事上耗掉半天,而真正的事故却被搁置到傍晚。

涌向策划的请求,性质各不相同,差异之大不亚于工种的多样。问题在于,它们全都以"一行消息"这种相同的形态到达。本章讨论的,就是在收到这些一行消息的瞬间将其分入三条轨道的工作。轨道一旦分清,该立刻停下手头做什么、又该把什么推到之后,就随之确定了。


16.2.1 协作左右着本职工作

策划既不直接写代码,也不直接做美术和音效。他只是撰写规格说明、传达意图、验证结果。所有产出都要经由其他工种之手才能问世。因此,协作的质量直接决定策划产出的质量。

在笔者担任总监的项目A(移动优先的MMORPG,中等规模(10\~50人)团队)中,策划日常协作的工种铺开来看是这样的。

策划 时间的40~60%

开发(代码·工具)每天 美术每周2~3次 音效每周1~2次 动画每周1~2次 QA每周1次+MS 运营·CS每周1次 外部(发行·平台)每季度1~2次

策划要与七个工种从每天到每季度不等地相互咬合。策划在工位上花费的时间里,有40\~60%都投入到这类协作中。花在本职(设计)上的时间,不过是剩下的那一半而已。既然如此,压缩协作时间,就等于延长本职工作的时间。而吞噬协作时间的最大原因,在于无法对收到的请求分类,把精力耗在了不该耗的地方。


16.2.2 一行请求所隐藏的三种性质

回到前面的三条消息。从表面上看,它们都是"请帮我……"。但在这背后,隐藏着三种不同的性质。

笔者用一个词分别称呼这三种性质:对齐 (align)缺陷 (defect)排期 (schedule)。把收到的请求先塞进这三者之一 —— 在项目A中,这项工作被固化为一个名为 request-triangulate 的工作流。之所以取名三角测量 (triangulate),是取其用三个基准点(工种性质·紧急度·外部依赖)将一个点(请求)包围起来、从而确定其位置之意。

分类流程如下。

flowchart TD A[外部请求1件到达] --> B{是否受外部截止/合约<br/>约束?} B -- 是 --> S[Track-S: 排期 schedule] B -- 否 --> C{是否为影响用户的<br/>Bug/缺陷?} C -- 是 --> D[Track-D: 缺陷 defect] C -- 否 --> E{是否为品味·意图的<br/>对齐问题?} E -- 是 --> F[Track-A: 对齐 align] E -- 否 --> G[待定区:<br/>请求补充信息] S --> S1[纳入季度路线图<br/>确保充足的前置时间] D --> D1[判定优先级P0~P2<br/>立即对接代码主管] F --> F1[仅传达意图<br/>表现交由对应工种] style S fill:#fde2c4,stroke:#c98a3a style D fill:#f6c6c6,stroke:#c25151 style F fill:#c9e4d0,stroke:#4f9d6a style G fill:#e0e0e0,stroke:#888

提问的顺序是关键。之所以最先追问排期依赖,是因为对于有外部截止期的事,前置时间优先于内部判断。如果把距季度审核还剩3周的事归类为"以后对齐一下就行",那么等对齐完成时,截止期已近在眼前。把缺陷放在第二位的理由是,已经在影响用户的事,永远优先于品味层面的讨论。对齐排在最后。只有既不紧急、又不受外部约束、也不伤害用户的事,"慢慢对齐"才成立。

如果三个问题的答案都是"否",那不是分类失败,而是信息不足。这种时候不要硬给它定轨道,而是放进待定区并反问回去。"这个是必须进下一个版本,还是先评估一下就好?"这样一句话,通常就能定下轨道。


16.2.3 实操记录(worked transcript):把收件箱分入轨道

如果这套分类只在脑子里做,忙碌的日子就会崩掉。因此,笔者的做法是把收到的一批请求整个丢给AI做初步分类,再对结果加以验证·修正。下面几乎原样照搬了实际跑过的一轮。不做概括,连AI出错的地方和笔者否决的部分都一并保留。

提示词全文

请把下面这些外部请求分入三条轨道 —— Track-S(排期:受外部截止·合约·审核约束的),
Track-D(缺陷:影响用户的Bug,P0~P2判定)、Track-A(对齐:品味·意图,以对齐收尾)。
按外部依赖 → 用户影响 → 品味/意图 的顺序审视,归入最先命中的轨道并据此确定,
三者都不是则放入待定,并给出1个反问的问题。
输出按每条请求列出 | 轨道 | 依据(1行) | 下一步行动(1行) |。

[请求列表]
1. 美术主管:战斗特效配色暗沉。可否做得更明快?
2. QA主管:公会签到奖励重复发放Bug。有复现视频。
3. 发行商:在东南亚版本中落实文化圈指南。下个季度审核前。
4. 音效总监:Boss BGM的前奏比战斗开始时机晚了0.5秒。
5. 运营团队:多名用户反馈"公会聊天字体太小"。

Claude原始输出

| # | 轨道 | 依据 | 下一步行动 |
|---|------|------|-----------|
| 1 | Track-A | 配色色调属品味·意图范畴,与外部/Bug无关 | 仅传达意图(战斗的紧张感),饱和度决定交由美术 |
| 2 | Track-D | 奖励重复=直接影响用户资源,可复现 | 判定P0,立即对接代码主管,评估热修复 |
| 3 | Track-S | 受季度审核这一外部截止期约束 | 纳入季度路线图,拆解指南条目后估算前置时间 |
| 4 | Track-A | BGM时机看似是演出品味问题 | 与音效总监对齐 |
| 5 | Track-D | 字体可读性属UI缺陷 | 登记为P2 Bug |

人工验证 / 否决

在第4条和第5条上,笔者停了下来。

否决第4条。 AI把BGM前奏延迟看成了"演出品味(Track-A)"。但"比战斗开始时机晚0.5秒"不是品味,而是同步缺陷。问题很可能不在于更换音源本身,而在于BT(行为树)上的触发时点与音频提示 (cue) 错位。这件事不会靠对齐了结,而是要动到动画·代码那边的时序数据。→ 更正为Track-D。 但因不影响用户资源,定为P2。

第5条降级为待定。 AI把"字体太小"径直断定为UI缺陷(Track-D)。然而,这到底是缺陷还是品味,仅凭消息本身无法分清。如果字体是按设计规格渲染的、只是"感觉偏小",那更接近对齐(Track-A);如果是渲染得比规格更小、显示出错,那就是缺陷(Track-D)。→ 待定。向运营团队反问:"是相对于规格中的字号实际显示偏小,还是希望把规格本身调大?"

再次请求

把否决的两条纳入考虑后再次投出的提示词,追加指令很简短。

将第4条的'相对战斗开始延迟0.5秒'重新归类为同步缺陷(Track-D, P2)。
补充1个问题,确认时序是在BT触发/音频提示中的哪一处错位。
将第5条作待定处理,并明确写出询问'是否为相对规格的实际渲染'的问题。

再次输出将第4条更正为 Track-D / P2 / "确认BT战斗开始节点的音频提示偏移量是否为0,或BGM片段本身是否含有0.5秒静音",将第5条更正为 待定 / "由运营团队再确认:是相对规格渲染偏小,还是请求上调规格",如此回传。至此,分类宣告完成。

在这里,AI做的事和人做的事被清晰地区分开来。AI快速地对五条做了初步分配,给出了一张没有空格的表。人则揪出了其中轨道边界微妙的两条(看似品味实为同步缺陷的BGM,看似缺陷却可能是品味的字体)。把五格无遗漏地填满,和察觉其中两格填错了,是两种不同的能力;这份实操记录正是把这两件事分别交给各自擅长的一方来做。


16.2.4 每条轨道的处理各不相同

分类结束后,每条轨道都进入完全不同的后续工作。虽然从同一张表出发,终点却各不相同。

被归入 Track-A(对齐) 的请求,按"只传达到意图为止,表现交由委托"的原则处理。对于美术的配色请求,笔者给出的回答不是饱和度数值,而是意图。"这场战斗是Boss第1阶段,紧张感是核心。希望压迫感优先于明快感。在此前提下,饱和度交由美术判断。"策划一旦直接指定饱和度数值,美术的自主性就被削减,产出的责任归属也随之模糊。守住意图与表现之间的界线,就是对齐轨道的全部。

被归入 Track-D(缺陷) 的请求,接下来是优先级判定与对接代码。公会奖励重复(P0)当场移交给了代码主管,BGM同步(P2)则登记进待办列表 (backlog),同时附上推测原因的问题。在缺陷轨道中,策划的工作不是"修复",而是排定优先级并给出准确的输入。区分P0与P2的标准是"当下是否影响用户资源·游戏进度"。奖励重复直接关系到资源,故为P0;BGM延迟0.5秒虽令人不适,却不妨碍进度,故为P2。

被归入 Track-S(排期) 的请求,进入季度路线图。发行商的文化圈指南虽是一行请求,实际上却会拆解成多个条目 —— 宗教象征物的表现、色彩禁忌、文本方向、角色服饰。关键在于,一收到它就回复"会评估",并将其整体搁上季度计划。有外部截止期的事,哪怕看着不大,前置时间也是命脉,一旦起步太晚,必定出事故。

把这三条岔路一目了然地对比,是这样的。

Track-A · 对齐 Track-D · 缺陷 Track-S · 排期 判定问题 是品味·意图吗? 策划的工作 仅传达意图, 表现交由委托

<text x="273" y="92">判定问题</text>
<text x="273" y="112" fill="#7a2e2e">影响用户的Bug?</text>
<text x="273" y="142">策划的工作</text>
<text x="273" y="162" fill="#7a2e2e">P0~P2判定,</text>
<text x="273" y="180" fill="#7a2e2e">对接代码主管</text>

<text x="508" y="92">判定问题</text>
<text x="508" y="112" fill="#7a5320">受外部截止期约束?</text>
<text x="508" y="142">策划的工作</text>
<text x="508" y="162" fill="#7a5320">拆解条目,</text>
<text x="508" y="180" fill="#7a5320">确保前置时间</text>

分类准确时,同一收件箱里的五行就会干净利落地分散到三条不同的处理线上。分类出错时,缺陷会被拖进对齐会议白白耗掉时间,或者排期事项起步太晚,在截止期前夕爆发。


16.2.5 用TF隔离来协作

有时请求不是一两条,而是成团涌来。比如发行商审核前的那几周,或战斗系统全面改版这样的局面。这种时候,把工作本身隔离到 95_BattleTF 这样的临时工作空间里,结束后只把决定提升为正本。那套隔离·吸收机制,以及"只向美术团队交付html(md学习为0)"的做法,已在前一章16.1中全部讲过。

从分类(3-track)的角度再补一句,是这样的。膨胀成一整团的协作,大多是Track-S(排期)事项跨越多个工种被拆解的局面;当逐条按轨道处理已应付不来时,就把它挪进"隔离工作空间"这个高一层的容器里盛放。也就是说,如果3-track分类是入口,那么TF隔离就是容纳通过入口的那一大团的房间。


16.2.6 常见失败与处方

失败模式 处方
把所有请求以同等分量处理 一收到就做3-track分类,从外部依赖开始问
把排期事项误分为对齐 把判定顺序的第1位固定为外部截止期的提问
把看似品味的同步缺陷当作对齐处理 "时机/数值错位"先怀疑是缺陷
把看似缺陷的品味断定为缺陷 反问"是否为相对规格的实际渲染",转入待定
在对齐轨道中策划连表现也一并决定 只到意图为止,表现交由工种委托
排期事项起步太晚 立即纳入季度路线图,确保前置时间

这张表的一半是分类阶段的失误,另一半是分类之后处理的失误。即便分类准确,若各轨道的处理手法出错,效果也会烟消云散。(与TF隔离·提升·媒介相关的陷阱,参见16.1的陷阱表。)


本章要点


游戏之外的应用。 一行请求仅因落进同一个收件箱就被以同等分量对待,这个问题并非游戏独有,而是所有服务策划·PM的日常本身。把涌入的请求分为"对齐(品味·方向)·缺陷(影响用户的Bug)·排期(外部截止期)"三条轨道的分类法,换个领域照样奏效。比如,某位Web服务PM的即时通讯工具里同时落进"把按钮颜色调亮一点(对齐)""支付收据被重复发送(缺陷)""个人信息保护法修订落实截止还剩3周(排期)",那么按外部截止期→用户影响→品味的顺序,归入最先命中的轨道,就能立即为支付Bug安排人手,而法律修订则从确保前置时间做起。


动手试试

Web聊天机器人最简路径(无需终端) —— 本章的核心不在于工作流脚本,而在于"把一行请求分入对齐·缺陷·排期三条轨道"这一构想。这个构想,即使没有CLI·hook·atom这套基础设施,仅凭Web聊天机器人(ChatGPT或Claude网页版)也能原样再现。下面两个步骤是主干。 1. 把当天收到的请求不拘格式地一行行汇集起来。从即时通讯工具·邮件·备忘录哪里扒来都行。 2. 在Web聊天机器人的输入框里贴上下面的提示词,再在其下粘贴汇集好的请求列表。这就是把 request-triangulate 所做的初步分类,用手工做一遍。 请把下面这些请求分入Track-A(对齐)/Track-D(缺陷)/Track-S(排期)。 按外部截止期 → 影响用户的Bug → 品味·意图 的顺序审视,归入最先命中的轨道并确定, 三者都不是则放入待定,并给出1个反问的问题。输出为 | 轨道 | 依据1行 | 下一步行动1行 |。 [粘贴请求列表] 接下来,只需人工验证输出表中的两格 —— 如果"时机·数值错位"被归为对齐,就怀疑是同步缺陷;如果"某某偏小/偏慢"这类主观感受上的不满被断定为缺陷,就反问"是否为相对规格的实际渲染",将其降为待定。脚本·工作流,只需等到这套分类做熟了、每天的批量开始吃力时,再引入即可。

setup. 把涌入的外部请求汇集到一处(频道·文档)。把三条轨道的定义各写一行 —— 对齐(品味·意图)、缺陷(影响用户的Bug)、排期(外部截止期)。

prompt. 把汇集好的请求批量丢给AI,并固定判定顺序。

请把下面这些请求分入Track-A(对齐)/Track-D(缺陷)/Track-S(排期)。
按外部截止期 → 影响用户的Bug → 品味·意图 的顺序审视,归入最先命中的轨道并确定,
三者都不是则放入待定,并给出1个反问的问题。输出为 | 轨道 | 依据1行 | 下一步行动1行 |。
[粘贴请求列表]

verify. 在输出表中亲自验证两处。(1)如果"时机·数值错位"被归为对齐,就怀疑它是不是同步缺陷。(2)如果"某某偏小/偏慢"这类主观感受上的不满被断定为缺陷,就反问"是否为相对规格的实际渲染",将其降为待定。只要人把握住这两处边界,其余的就可以信任。

单人精简版

如果你是既没有团队也没有TF的单人开发者,那就保留轨道不变,只更换对象。把商店评论、Discord反馈、Beta测试者的备注汇集到一份文档里,每周1次用上面的提示词批量分类。对齐(品味)按"只要不与我的愿景冲突就采纳",缺陷(Bug)在当周处理,排期(商店审核·活动截止)则连同前置时间一起录入日历。集中整备期间的隔离文件夹运作,照16.1的单人精简版做即可。

16.3 一个决定,三种包装 —— 按职能包装产出物的 framing

95_BattleTF 会议室。把公会签到奖励确定为资源 +5 的那天下午,我把同一个决定分发到了三个地方。策划组频道里放的是一份规格说明 markdown,程序组那里是一行数据字段,美术组那里是一张单屏 html。三边几乎同时给了回应。主程问"触发时机在哪里",美术总监问"签到按钮的位置和 06_UI 指南对得上吗",动画师则什么都没说。明明是同一个决定,三个人看到的却完全不同。

本章正是把这种"看法各异"从事故变成设计的记录。把同一个决定按职能包装成不同形态 —— 这就是 framing。


16.3.1 同一个决定,五个人各读各的

公会签到奖励这一个决定,牵涉的受众有五类。他们读同一句话,也只挑自己领域的部分读,其余的都略过。被略过的地方就会出事故。

受众 会专注去读的 本能会跳过的
主程 数据字段·接口·触发时机 色调·叙事·演出
美术总监 界面布局·组件·风格指南 数据完整性·触发
音效总监 行为触发·氛围·时长 数据细节
动画师 动作·时序·状态转换 视觉基调·数值
QA 验收标准·风险·边缘场景 实现方式的内部

问题不在信息的量,而在呈现的方式。把一份厚厚的规格说明一模一样地摆到五个人的桌上,五个人各自翻开不同的页、合上不同的页。framing 不把这种"翻开"交给偶然,而是有意去安排。

下面这张 framing 矩阵,展示同一个决定在跨越职能边界时会换上怎样的形态。

决定:签到奖励 = 资源 +5 95_BattleTF / 单一事实

策划 → markdown 意图·规则·依据全文 含供学习的上下文 spec_guild_attendance.md

美术 → html 单屏·布局·组件 md 学习 0(仅传达) guild_screen_v3.html

程序 → 数据 字段·接口·触发 明示校验 lint 项 guild_table 1 row

不变事实(三种包装都必须保留的内容) 数值 = +5 · 时点 = 每日首次登录 · 范围 = 全体公会成员 包装可以不同,但这三个值只要对不上,framing 就失败

包装因受众而异,但垫在底层的不变事实(数值·时点·范围)在任何一种包装里都不能动摇。framing 的技艺不在于"呈现得不一样",而在于"呈现得不一样的同时,保留同样的内核"。


16.3.2 实操记录 —— 把一个决定做成三种包装

如果每次都靠手工重新塑造 framing,负担太大,最后就干脆不做了。所以我采用这样一种方式:抛出一个决定,一次性拿到各职能的包装初稿。下面是围绕公会签到奖励这个决定实际往来的完整记录。未加修饰,原样搬来。

提示词全文

[决定事实 —— 在 95_BattleTF 上确定]
- 公会签到奖励 = 公会资源 +5
- 触发:公会成员当天首次登录时 1 次
- 范围:该公会的全体公会成员各自
- UI:公会界面顶部显示资源总量 + "签到"按钮

把这一个决定按三个职能分别包装一下。
策划用 markdown 写意图·规则·数据影响;美术把要放进 html 的界面规格
以布局·组件·风格指南引用为中心(禁止提及数据·触发);
程序用一行数据·接口·触发·校验项。
三种包装都要把不变值(+5、首次登录 1 次、全体公会成员)一模一样地钉进去,
不确定的值不要编造,用 [待确认] 标出。

Claude 原始输出(未做摘要与整理)

1) 策划 → markdown

## 公会签到奖励
- 意图:把每天 1 次的登录换算成公会层面的协作资源,以提升留存率。
- 规则:公会成员当日首次登录时,公会资源 +5。每天 1 次。以公会为单位累计。
- 数据影响:guild_resource_total 增加。需监控资源通胀 [待确认:上限未定]。

2) 美术 → 界面规格(放进 html 的内容)
- 在公会界面顶部区域显示资源总量(progress_bar + 数字 badge)。
- 右侧放"签到"按钮。按下时的状态变化遵循 06_UI 指南的按钮状态规则。
- 样式:符合 06_UI 指南。颜色 [待确认:公会主题色 token 未指定]。
- (数据数值·触发在本包装中有意排除)

3) 程序 → 数据/接口
- 数据:在 guild_table 中新增字段 —— guild_resource_total (int, default 0)
- 接口:GuildResourceManager.add(guild_id, amount)
- 触发:在每日首次登录事件钩子中调用 add(guild_id, 5) 1 次
- 校验:integrity_check_guild_resource(不可为负 / 拦截每日超过 1 次的调用)
- 并发:同一公会成员在多设备同时登录时的重复累加 [待确认]

人工验证·否决

原始输出没有直接采用。我抓出了三处。

再次请求

修改一下美术包装:
- 把"遵循 06_UI 指南"这类引用外部文档的措辞全部删除。
- 把按钮按下/等待/完成 3 种状态的视觉差异直接写进界面规格里。
- 以美术组只看这一张就开工为前提,做到自足,不跳转到其他文档。

程序包装里的 [待确认] 2 项从产出物中拿掉,
改为在最上面单独放一个"需 95_BattleTF 重新确认的项"区块。

经过这一次的否决·再次请求,产出物变成了三个职能各自在自己岗位上拿来就能用的形态。AI 塑造出了三份包装初稿,还在缺口处标了记号;但在哪一种包装里删掉什么 —— 从美术包装里去掉外部引用、从程序包装里剔除未定项 —— 这一刀终究还是落在我手里。framing 的核心判断不在于纳入,而在于排除。


16.3.3 三种 framing 方式与回本时点

包装放在哪里,方式就随之分岔。三种之中用哪一种,取决于规格说明的体量和运营的余力。

(1) 同一文档内的按受众摘要。 在正文之后附上各职能的摘要小节。五个人共享同一个文件,但各自只读自己那一节。

## 按受众摘要

### 代码(实现)
- 数据:guild_table.guild_resource_total (int)
- 接口:GuildResourceManager.add(guild_id, amount)
- 触发:每日首次登录 1 次
- 校验:integrity_check_guild_resource

### 美术(视觉)
- 界面:公会顶部资源总量 + 签到按钮
- 组件:progress_bar、badge、button(3 状态)
- 优先级:本次里程碑

### QA(验证)
- 验收标准:签到后公会资源 +5 生效,拦截每日超过 1 次
- 风险:资源通胀、多设备重复累加

(2) 按受众拆分的独立产出物。 在一个正文之外,再为各职能各自分出文件。95_BattleTF 里只给美术组发 html、不发 md 的做法,就是这种方式的实战形态 —— 同一个决定,不同职能连媒介本身都不同。

spec_guild_attendance.md     — 策划正文(完整上下文)
guild_screen_v3.html         — 美术(仅 html,md 学习 0)
guild_table 1 row + add()    — 程序(数据/接口)
qa_guild_attendance.md       — QA(验收标准·风险)

适合体量大的规格说明,而且媒介能直接进入各职能的工具。代价是一个决定一改,就得连着修改多个产出物,运营负担很大。

(3) Wikilink 图。 正文里只放各职能的起点链接,各人沿着自己的分支去探索。

[[spec_guild_attendance]]
   ├── [[code_guild_table]]
   ├── [[ui_guild_screen_v3]]
   └── [[qa_guild_attendance]]

三种方式的成本与回本如下。下表数值中的"效果"为作者估算(未经验证),只应信赖方向与相对比例。

方式 成本 回本时点
(1) 按受众摘要 正文篇幅 +30% 上下 几乎所有规格说明中都能立刻回本
(2) 独立产出物 运营 N 份产出物 仅当体量大且媒介按职能不同时才回本
(3) Wikilink 图 前期投入图谱基础设施 当规格说明累积到图本身成为资产时才回本

大多数规格说明适用 (1)。成本最小,回本最快。(2) 只在像美术 html 这样媒介已经分岔的场合使用;(3) 则在规格说明积累足够、链接图能产生探索价值时才开启。


16.3.4 把受众固定为五类

如果每份规格说明都重新定义受众,framing 就每次都要重新塑造。所以要把代号固定下来。

受众代号 领域
code 代码·系统·数据
art 美术·视觉·UI
sound 音效·音响
anim 动画·动作
qa QA·验证

这五类就是内部运营标准。外包·法务这类外部受众,在这套标准之外另行处理。固定为五类之后,把 framing 交给 LLM 时,就不必每次重写受众定义,还能用清单抓出被漏掉的受众。


16.3.5 自动化及其陷阱

如果每份规格说明都手工写五个职能的摘要,最后就不写了。所以我把流程这样串了起来。

flowchart LR A[策划:撰写决定事实] --> B[LLM:5 受众包装初稿] B --> C[策划:否决/补强/保留判断] C --> D{发现缺口?} D -- 是 --> E[退回 95_BattleTF 重新确认] D -- 否 --> F[最终规格说明 + 各职能 framing] E --> A classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class A,C,D human; class B ai; class F pass;

策划只写决定事实,LLM 就生成五份包装初稿,策划再判断否决·补强·保留。一旦出现缺口([待确认]),就不在 framing 阶段处理,而是退回决定阶段 —— 因为 framing 不是填补决定漏洞的工具,而是搬运既定决定的工具。

在这个循环里反复踩中的四个陷阱,连同对策一并列在下面。

陷阱 症状 对策
信息重复 同样的内容在正文·摘要里重复,加重运营负担 正文写 1 次,摘要只写差异项
信息遗漏 对某个职能至关重要的值整块缺失 用 5 受众固定清单检查遗漏
忽略正文 只看摘要,漏掉正文的上下文 在摘要末尾注明"依据见正文"
媒介错配 给美术发 md,增加学习负担 固定职能媒介原则(美术=html)

自动化能把撰写负担降到每份规格说明 5 分钟上下,但否决·补强·保留的判断并不会随之自动化。那份判断才是人的位置。


16.3.6 度量 —— 开启 framing 之后

下面是作者所运营的项目A中,framing 引入前后的对比数值。绝对数值为作者估算(未经验证),值得信赖的是变化的方向与相对比例。

指标 无 framing 有 framing 方向
各职能误读事故 每季度 15\~20 起 每季度 3\~5 起 大幅减少
受众阅读规格说明的时间 15\~30 分钟 5\~10 分钟(只读自己那节) 减少
从决定到开工 1\~2 天 4\~8 小时 缩短
各职能间的解读冲突 每季度 8\~12 起 每季度 2\~3 起 减少
规格说明的撰写时间 1\~2 小时 1.5\~2.5 小时(LLM 辅助) 小幅增加

规格说明的撰写本身会稍微变长,因为要叠加各职能的包装。但在那之后,各职能的作业周期缩短,从决定到开工的整体时间随之减少。这一权衡正是引入 framing 的核心依据。如果团队觉得 LLM 辅助审校负担重,那么先在方式 (1) 中把手写的 5 受众摘要固定下来、再叠加自动化,这个顺序更稳妥。


游戏之外的应用。 把同一个决定按受众包装成不同形态、而不变事实(数值·时点·范围)在任何地方都保留,这套 framing 不只用于游戏,也原样适用于任何组织的公告与发布沟通。比方说,如果决定了"订阅费从 7 月 1 日起上调到 9,900 韩元(约 50 元人民币)"这一件事,那么对开发组是计费表字段·生效时点这类数据,对设计组是一张通知横幅界面,对客服组则是预估咨询的应答话术,包装就此分岔。三种包装各不相同,但"9,900 韩元·7 月 1 日·新老订阅用户全体"这三个数字,只要在任何一种包装里对不上,那一刻客户纠纷就会爆发。


16.3.7 动手试试

setup

prompt

[决定事实]
(数值·时点·范围逐行写)

把这个决定按 code·art·sound·anim·qa 中相应的职能来包装。
每种包装都去掉该职能不关心的信息,但不变值(数值·时点·范围)在任何包装里都一模一样地钉进去,
美术包装不引用其他文档、仅凭这一张就自足;不确定的值不要编造,用 [待确认] 标出。

verify

单人精简版

如果你是一个人工作,就把受众减到两类 —— "以后的我"(实现)和"审校者"(QA)。写下一行决定事实,请 LLM"把它拆成实现备忘和审校清单两份",然后只需对照这两份里的核心数值是否一致即可。哪怕受众只有两类,"把同一个决定包装成不同形态、但保留不变值"这一 framing 骨架照样运作。


本章要点

17.1 会议记录为何是最大的痛点

会议结束已过去5分钟。会议室的白板上还留着字迹。"战斗受击判定,采用客户端先行处理。但服务器验证的优先级放到下一个冲刺。"这是5个人花了30分钟才得出的结论。所有人都点了头,有人还拍了照片。

3周后,同样的5个人再次聚到同一间会议室。议程列表的第一行写着:"战斗受击判定 —— 客户端先行处理 vs 服务器验证,需要决策。"没有人记得3周前的结论。白板照片存在某人的相册里的某个角落,而那个人今天休假。又花了30分钟。这次得出了相反的结论。

这就是会议记录之所以是最大痛点的全部原因。会议上会作出决策。然而那个决策却无法流转(propagate)到下一次会议、下一份文档、下一个构建。决策作出了,却无法传播。本章讲的正是用数据接续那道断裂环节的故事。


17.1.1 会议 → 决策 → 执行,断在何处

笔者运营的个人 RnD 系统分成了17份文档:atom 命名规范、关系图自动化、Layer 映射指南、JIT 注入基础设施等。其中吞掉最多时间的单一文档,是会议记录改进计划。它的分量之大,足以与其他16份加在一起相提并论。

起初我感到不解。会议记录嘛,不就是照着记下来就行了吗?然而实际测量后发现,痛点的位置不在会议记录的撰写,而在会议记录之后。会议上决策确实作出了。问题在于:这个决策由谁负责、依据什么、期限到何时、接续到什么 —— 这些在走出会议室门的那一刻就蒸发了。

把这道断裂画成一个场景,便是如此。

flowchart LR M[会议<br/>30分钟讨论] --> D[产生决策<br/>全员同意] D -.断裂.-> X[在下次会议<br/>重新提上议程] D -.断裂.-> Y[未反映到文档] D -.断裂.-> Z[在构建中遗漏] X --> M style D fill:#ffe6cc,stroke:#d79b00 style X fill:#f8cecc,stroke:#b85450 style Y fill:#f8cecc,stroke:#b85450 style Z fill:#f8cecc,stroke:#b85450

虚线就是断裂的传播。决策(橙色)作出了,却没有流向三条分支(红色)中的任何一条。无法流出的决策会在3周后回到会议上。箭头向上弯折、重新回到会议的那个循环,正是痛点的本体。

一旦传播断裂,四件事会同时发生。

失去决策的历史。面对"当初为什么那么定?",只能以"记不清了,再约一次会吧"来回答。重复会议增多。同一议题每个季度都被重新提上议程。新加入者抓不住上下文。由于看不到决策的累积,每次都得一对一地解释。而且 AI 辅助变得无力。因为没有上下文,回答只停留在泛泛之谈。会议记录一旦散落各处,就无法把"我们团队以前对这个议题是怎么决策的"提供给 AI,于是 AI 只会返回互联网上的平均值。

这四件事都出自同一个根源。因为决策被当作备忘,而非当作数据来对待。备忘会挥发,而数据会流动。


17.1.2 把会议记录看作决策数据库,而非产出物

这里需要把视角翻转一次。若把会议记录看作"会议的产出物",那么记下来、存档,任务就结束了。存档的会议记录就像桌上的便签纸。当天看得见,到了下周就不知道去哪儿了。

若把会议记录看作"决策数据库",它就变成了一项完全不同的工作。资产不是会议记录本身,而是从会议记录中提取出来的决策;这个决策必须可检索、可引用、可传播。会议记录只不过是一条把决策开采上来的矿脉罢了。

用代码强制推行这一转变,正是第17部分的主干。笔者的系统里有一个支撑这一转变的 atom,名为 decision_summary_not_clickup_mirror。展开来说,它是"会议记录的决策摘要不是 ClickUp(任务追踪器)的镜像"这一原则。

这个 atom 为何必要,正戳中了痛点的要害。很多团队会把会议记录的决策槽直接誊抄到任务看板上。这样一来,"要做的事"留下了,"为什么那么定(依据)"却消失了。任务追踪器装的是要做什么,而不装为什么那么定。3周后会议重复的原因,恰恰就是这个。要做的事已经关闭,却没有依据,于是有人问起"话说这个当初为什么决定要这么做来着",却没人答得上来。所以决策摘要不能成为追踪器的镜像,而必须是一份怀有依据(rationale)的独立资产。atom 名称本身就是这条禁止线。


17.1.3 把决策拆成四个字段

能够传播的决策与会挥发的决策,差别在于结构。会挥发的决策是"采用客户端先行处理"这样一个句子。能够传播的决策则被分解成4个字段。

一条决策 = 4个字段

decision 定了什么 "受击判定 客户端先行处理"

owner 谁来负责 团队成员 A(没有则 [MISSING])

rationale 为什么那么定 "体感响应速度优先,承受外挂风险"

follow_up 接续到什么 "下一个冲刺的服务器验证任务"

owner 为空时管线以 [MISSING] 上报 —— 强制传播的责任线

4个字段中最重要的是 owner。决策若没有负责人,那个决策就不是任何人的事;而不是任何人之事的决策,不会传播到执行。所以笔者的提取管线在 owner 为空时不会就那样放过,而是用 [MISSING] 明确上报。它把"责任线为空"这一事实本身抬升到表面上来。

rationale 是前面所说的 decision_summary_not_clickup_mirror 原则所栖身的地方。没有依据,3周后会议就会重复。follow_up 是决策通向实际执行的桥。这个字段一旦为空,决策就只停留为决策,触及不到构建。


17.1.4 提取管线 —— 从会议记录中开采决策

这4个字段由人每次手动填写也是可行的,但那样强制力就弱。笔者的系统使用一条从会议记录中自动提取决策、并上报缺失字段的管线。3个脚本串行相接。

flowchart TD R[会议记录 .md<br/>标准格式] --> L[meeting_lint.py<br/>格式校验] L -->|通过| P[decision_parser.py<br/>提取4字段] L -->|退回| R P -->|owner 缺失| MISS["[MISSING] 上报"] P -->|4字段齐备| PEND[pending atom<br/>1周验证等待] PEND --> PROM[promote.py<br/>每周1次晋升] PROM --> ATOM[正式 atom<br/>+ 注册到 JIT manifest] style L fill:#dae8fc,stroke:#6c8ebf style P fill:#d5e8d4,stroke:#82b366 style PROM fill:#e1d5e7,stroke:#9673a6 style MISS fill:#f8cecc,stroke:#b85450

第一步 meeting_lint.py 检查会议记录是否遵循标准格式:是否有 frontmatter,议题/决策/行动/下次会议这4个槽是否已填。格式损坏的会议记录会在这里被退回,回到撰写者手中。自动解析器只能处理格式受强制约束的输入,因此这道 lint 充当整条管线的入口关卡。

第二步 decision_parser.py 是核心。它读取决策槽,分解为4个字段(decision/owner/rationale/follow_up)。这里若找不到 owner,不会丢弃那个决策,而是以 [MISSING] 上报。因为让没有负责人的决策悄然通过是最危险的。

第三步是:被提取出来的决策不会立刻成为正式资产,而是以 pending 状态等待1周。这段验证期就是可逆关卡。若在1周内暴露出"这不是决策,而是讨论"或者"依据错了",就将其废弃。然后 promote.py 在每周1次的评审中,只把存活下来的决策移到正式 atom 文件夹,并注册到 JIT manifest。被注册的决策会从下一次会话起自动注入到相关工作中。至此,决策才开始流动。

可逆与不可逆的边界就在这里。到 pending 废弃为止都是可逆的。然而一旦 promote 结束、决策传播到其他文档、数据表、构建,从那时起就是不可逆的。因为团队成员的认知会改变,而从属决策会在其之上层层堆积。所以一切审校都必须在 promote 之前 —— 也就是 pending 的可逆区间内 —— 完成。


17.1.5 实操记录 —— 让一份损坏的会议记录通过

只讲抽象难以体会,所以这里原样搬来一份把实际损坏的会议记录放进管线跑一遍的实操记录(worked transcript)。输入是一份格式塌了一半的会议记录。

输入 —— 2026-06-02-battle.md(格式不良)

---
type: meeting_note
date: 2026-06-02
category: battle
---

## 议题
1. 受击判定位置(客户端 vs 服务器)

## 决策
- 受击判定决定在客户端先行处理。体感响应速度比服务器往返
  更重要的意见占了上风。外挂风险予以承受。
- 服务器验证放到下一个冲刺,作为独立任务。

## 行动项
- [ ] 服务器验证设计

这份会议记录虽然写了决策,但 owner 哪里都没有。谁来负责服务器验证是空的。下次会议的日程槽也整个缺失。我把它放进解析器,看看会得出什么。

提示词(附加在 decision_parser.py 上的 LLM 辅助提示词全文)

读取以下会议记录的 "## 决策" 槽,为每条决策提取4个字段:
decision、owner、rationale、follow_up。
- 若无法从正文中确定 owner,必须标注为 "[MISSING]"。
  不要靠推测填充。
- rationale 只引用正文中写明的依据。没有则填 "[MISSING]"。
- follow_up 与行动项或下一个冲刺的提及相关联。
仅以 JSON 数组输出。

[粘贴会议记录正文]

Claude 原始输出(不作摘要,原样呈现)

[
  {
    "decision": "受击判定在客户端先行处理",
    "owner": "[MISSING]",
    "rationale": "体感响应速度比服务器往返更重要。外挂风险予以承受",
    "follow_up": "服务器验证作为下一个冲刺的独立任务推进"
  },
  {
    "decision": "把服务器验证分离为下一个冲刺的独立任务",
    "owner": "[MISSING]",
    "rationale": "[MISSING]",
    "follow_up": "与行动项'服务器验证设计'关联(负责人未指定)"
  }
]

在这里停下,检查了输出。两条决策的 owner 都被判为 [MISSING]。准确 —— 会议记录里哪里都没有负责人,所以没有靠推测填充是对的。第二条决策的 rationale 也是 [MISSING],实际上正文中确实没有第二条决策的依据。这意味着解析器没有编造,让人放心。

不过有一处含糊。第一条决策的 follow_up 是"把服务器验证放到下一个冲刺",这与第二条决策内容重叠。解析器把两条决策的边界看得略微模糊了。这是需要人介入的地方。

人工否决 + 重新请求

第一条决策的 follow_up 与第二条决策正文重复。
把第一条的 follow_up 收窄为"创建服务器验证任务",
服务器验证的具体内容移交给第二条决策。
并且两条决策的 owner 都是 [MISSING],因此
在把这份会议记录放上 pending 之前,需要确定 owner 这一点,
把该警告行加到输出的最顶端。

Claude 重新输出(原始)

⚠️ 警告:2条决策的 owner 均为 [MISSING]。pending 晋升前
   需请会议主持人确定 owner。

[
  {
    "decision": "受击判定在客户端先行处理",
    "owner": "[MISSING]",
    "rationale": "体感响应速度优先,承受外挂风险",
    "follow_up": "创建服务器验证任务(详情见决策 #2)"
  },
  {
    "decision": "把服务器验证分离为下一个冲刺的独立任务",
    "owner": "[MISSING]",
    "rationale": "[MISSING]",
    "follow_up": "关联'服务器验证设计'行动项"
  }
]

这段实操记录展示的正是第17部分的核心。解析器把决策开采了上来,却没有隐藏 owner 这条责任线为空的事实。[MISSING] 被标了两次,而这作为"请确定 owner"的信号回到了会议主持人那里。决策要传播到执行,就必须有负责人;没有负责人时,系统会把它推到表面上来。一份损坏的会议记录要通过这道关卡,人就只能把 owner 填进去。传播的第一个结,就在这里打上。

顺带一提,上面输出中的 ⚠️ 只是控制台的警告行,并非正文格式的一部分。会议记录本身仍然是干净的4槽 Markdown。


17.1.6 剪去旁枝,立起脊柱

第17部分原本设计为6章(动机·提取·分类·图注·同步·AI 辅助),后来合并精简为4章。因为图像图注与同步是会议记录的旁枝,而把最大的痛点"决策传播"归拢为一章、立在前面才是对的。我把最大的痛点(会议 → 决策 → 执行 传播)提到 §17.1,再把纾解这一痛点的管线(meeting_lint → decision_parser → promote)紧随其后安排。

把会议记录当作数据库来对待的视角、把决策拆成4字段的结构、owner 为空时以 [MISSING] 上报的关卡、pending 1周的可逆验证 —— 这四者把断裂的传播重新接续起来。决策作出了却不流动的痛点,只要把决策做成能够流动的形态,就能纾解。


本章要点


游戏之外的应用。 "会议上决策作出了,那个决策却无法流转到下一次会议"这一痛点,不只发生在游戏开发,而是每周在所有职场的会议室里重复上演。把决策不写成一句话的备忘,而是拆成四个字段(做什么·谁·为什么·下一步行动)来记录,负责人为空时以 [MISSING] 抬升到表面 —— 这种做法可以原样搬到任何会议中。比如在营销周会上,如果达成了"下一次活动以 Instagram 为中心"的共识,那就为它附上 owner(谁来执行)、rationale(为什么是 Instagram —— 依据上季度的转化率)、follow_up(编写预算方案)。3周后"那件事谁说要做来着"这样的疑问,就再也不会出现。


动手试试

Web 聊天机器人最简路径(无需终端) —— 本章的核心不是脚本,而是"把决策拆成4字段、让它流动"这一构想。这一构想无需 CLI、hook、atom 基础设施,仅凭 Web 聊天机器人(ChatGPT 或 Claude 网页版)也能原样重现。以下三个步骤才是主线。 1. 会议结束后,把会议记录(或会议备忘)原样复制。没有格式也没关系。 2. 在 Web 聊天机器人的输入框里粘贴下面的提示词,并在其下方粘贴会议记录([会议记录正文] 处)。这就是把 decision_parser.py 所做的事手动做一遍。 从以下会议记录中,为每条决策以表格形式提取4个字段: decision(做什么)、owner(谁负责)、rationale(为什么)、follow_up(下一步行动)。 - 无法确定 owner 时必须填 "[MISSING]"。禁止推测。 - rationale 只取正文中写明的依据。没有则填 "[MISSING]"。 [会议记录正文] 3. 在输出表中,把标了 [MISSING] 的格子向会议主持人确认后填上。把完成的表格按日期顺序追加堆叠到 decisions.md 这样一份文档里,这份文档就是决策数据库(DB)。检索用文档内查找(Ctrl+F)就够了。脚本、atom、JIT 等,等到这个习惯积累起来、检索变得吃力时再引入即可。

setup(基础设施版本 —— 在上面的最简路径上手之后) - 确定一份会议记录标准格式。frontmatter(type/date/category)+ 4个槽(议题/决策/行动/下次会议)。 - 准备3个脚本:用于格式校验的 meeting_lint.py、用于决策提取的 decision_parser.py、用于晋升的 promote.py(起初仅有 lint 与 parser 就够了)。 - 明确决策的4个字段:decision、owner、rationale、follow_up。把 owner 为空时强制 [MISSING] 的规则放进 parser。

prompt(附加在 decision_parser 上的 LLM 辅助提示词)

读取以下会议记录的 "## 决策" 槽,为每条决策提取4个字段:
decision、owner、rationale、follow_up。
- 无法确定 owner 时必须填 "[MISSING]"。禁止推测。
- rationale 只引用正文中写明的依据。没有则填 "[MISSING]"。
- follow_up 与行动项、下一个冲刺的提及相关联。
仅以 JSON 数组输出。
[会议记录正文]

verify - 确认输出中每条决策的 owner 都已填上。只要还有一个 [MISSING],就必须先向会议主持人请求确定 owner,之后才放上 pending。 - 查看 rationale 是否编造了正文中没有的依据(没有时应为 [MISSING] 才正常)。 - 在 pending 放置1周后,只把每周1次评审中存活下来的决策用 promote.py 晋升为正式 atom。

17.1.7 单人精简版

如果3个脚本让你觉得负担,就这样精简。只把会议记录格式统一为4个槽,会议结束后仅取出决策槽,用上面的提示词在 LLM 上跑一次。只对 owner 显示为 [MISSING] 的决策,当场把负责人填进去。即使没有自动化,仅做这一点,也能挡住"决策没有负责人"这一最常见的传播断裂。lint、promote 等,等到会议记录堆积、需要检索时再添加即可。

17.2 从会议纪要中挖掘决策的提取管线

周三早上,刚到公司,团队即时通讯工具里就弹出一条提醒。"上周不是定好把背包格子数增加到30格了吗?谁负责改数据表来着?"没有人能在这条讨论串里给出答案。会议纪要肯定是有的,在某个文件夹里。打开一看,议题和讨论密密麻麻,可"到底决定了什么、谁来负责"却化在了字里行间。结果下一次会议上,又把同一个议题从头再提一遍。

本章讲的是填补那三天空白的机器。一份会议纪要进来后,通过格式检查,提取出决策的四个字段,没有负责人的决策会被贴上 [MISSING] 标签,生成候选文件,一周后经过评审,成为可自动注入的资产。人工只触及两端两处——撰写会议纪要的入口,以及每周评审一次的出口。


17.2.1 管线整体流程

先用一张图看全貌。每一个方框要么是一段小脚本,要么是人的判断。需要人工经手的方框只有两个,其余都自动流转。

flowchart TD A["召开会议"] --> B["按标准格式撰写会议纪要<br/>(人工)"] B --> C{"meeting_lint.py<br/>格式检查"} C -->|违规| B C -->|通过| D["decision_parser.py<br/>提取决策4个字段"] D --> E{"是否有 owner?"} E -->|无| F["[MISSING] 报告<br/>请求指定负责人"] F --> B E -->|有| G["生成 pending atom<br/>候选文件"] G --> H{"每周评审一次<br/>(人工)"} H -->|升格| I["promote.py<br/>正式 atom + JIT 注册"] H -->|废弃| J["保留废弃记录"] H -->|保留| G I --> K["从下次会话起自动注入"] style B fill:#e8f0ff,stroke:#3366cc style H fill:#e8f0ff,stroke:#3366cc style F fill:#fff0e8,stroke:#cc6633 style J fill:#f0f0f0,stroke:#999999

只有两个蓝色方框(撰写会议纪要、每周评审一次)是人工,其余都是脚本。橙色方框([MISSING] 报告)是自动检查重新把人叫回来的地方。当决策没有负责人时,管线并不是就此停下,而是把决策退回到撰写会议纪要的步骤,直到定下谁来负责为止。这正是这条管线的核心设计——不悄悄放过空白,而是大声报告出来。

整个资产的文件夹结构是这样安排的。

meeting_pipeline/ scripts/ meeting_lint.py 格式·必需章节检查 decision_parser.py 提取决策4个字段 + owner [MISSING] 报告 promote.py pending → 正式 atom + 更新 JIT manifest meetings/ 2026-05-18_battle_tf.md 标准格式会议纪要(输入) atoms/pending/ meeting_decision_2026-05-18_D1.md 候选(等待1周验证)


17.2.2 第1步 —— 强制格式的 lint

要能提取,会议纪要就得是机器可读的样子。如果没有 "## 决策" 章节,或者决策混在一整段行文里,解析器就什么都提不出来。所以最先加入的是格式检查。meeting_lint.py 做的事很简单:是否有必需的 frontmatter,是否有必需的章节,决策槽位是否以 D1D2 的格式填好。

# meeting_lint.py 骨架
REQUIRED_FRONTMATTER = ["type", "date", "category", "attendees"]
REQUIRED_SECTIONS = ["## 议题", "## 决策", "## 行动项", "## 下次会议"]
ALLOWED_CATEGORIES = ["art", "battle", "daily", "issue", "review"]

def lint(meeting_note_path):
    fm, body = parse_markdown(meeting_note_path)
    errors = []
    for key in REQUIRED_FRONTMATTER:
        if key not in fm:
            errors.append(f"缺少 frontmatter: {key}")
    if fm.get("category") not in ALLOWED_CATEGORIES:
        errors.append(f"category 值不合法: {fm.get('category')}")
    for section in REQUIRED_SECTIONS:
        if section not in body:
            errors.append(f"缺少章节: {section}")
    if "## 决策" in body:
        block = extract_section(body, "## 决策")
        if not any(l.strip().startswith("- D") for l in block.split("\n")):
            errors.append("决策槽位为空(需要 D1、D2… 格式)")
    return errors

把这项检查挂到会议纪要的提交前钩子上。一旦违反格式,提交本身就会被拦下。若只当作建议,忙的时候就会悄悄跳过,而跳过一次的格式到了下周就会垮掉。只要被拦上1\~2周,格式就会成为习惯。不过,要是太严苛,连撰写会议纪要本身都会被拖延,所以在适应期之后把 false positive 集中清理一次,才是现实的运营方式。


17.2.3 第2步 —— 挖掘决策四个字段的解析器

在通过格式检查的会议纪要中,decision_parser.py 读取决策槽位。从一条决策里要提取的,正好是四样东西。决定了什么(decision)、谁来负责(owner)、为什么这么定(rationale)、接下来该做什么(follow_up)。 这四个字段让决策成为资产。尤其是 owner。没有负责人的决策不是决策,而是一厢情愿的希望。所以当 owner 为空时,解析器不会悄悄留个空格,而是填入 [MISSING] 予以报告。

从这里到最后,我们不跳过任何一行,跟着一份会议纪要变成资产的全过程走一遍。这是从输入到 atom 升格的单一连贯示例。

================ 输入: meetings/2026-05-18_battle_tf.md ================
---
type: meeting
date: 2026-05-18
category: battle
attendees: [이민수, teammate_a, teammate_b]
related_atoms: [combat_global_cooldown_constant]
---
## 议题
- 统一战斗全局冷却(GCD)值
- 回复技能是否作为 GCD 例外

## 决策
- D1: 将战斗全局冷却统一为0.5秒。(负责人: teammate_a) [依据: 与 refgame 对比的输入响应体感测试中,0.5秒最为稳定]
- D2: 回复技能从全局冷却中排除。[依据: 担心回复循环被打断]

## 行动项
- @teammate_a: 将战斗数据表的 cooldown 列批量设为0.5 (~MM-DD)

## 下次会议
- MM-DD 14:00,评审回复循环1周测试结果

================ $ python meeting_lint.py meetings/2026-05-18_battle_tf.md ================
[OK] frontmatter 4/4,章节 4/4,检测到2条决策槽位。允许提交。

================ $ python decision_parser.py meetings/2026-05-18_battle_tf.md ================
[
  {
    "id": "D1",
    "decision": "将战斗全局冷却统一为0.5秒。",
    "owner": "teammate_a",
    "rationale": "与 refgame 对比的输入响应体感测试中,0.5秒最为稳定",
    "follow_up": "将战斗数据表的 cooldown 列批量设为0.5 (~MM-DD)",
    "source_meeting": "2026-05-18_battle_tf.md",
    "category": "battle",
    "related_atoms": ["combat_global_cooldown_constant"]
  },
  {
    "id": "D2",
    "decision": "回复技能从全局冷却中排除。",
    "owner": "[MISSING]",          # ← 未填写负责人。解析器予以报告
    "rationale": "担心回复循环被打断",
    "follow_up": null,             # ← 也没有后续行动
    "source_meeting": "2026-05-18_battle_tf.md",
    "category": "battle",
    "related_atoms": ["combat_global_cooldown_constant"]
  }
]
[WARN] D2: owner=[MISSING] —— 没有负责人的决策。暂缓生成 pending,退回会议纪要撰写者。

================ 生成 pending: 仅 D1 通过 ================
$ cat atoms/pending/meeting_decision_2026-05-18_D1.md
---
name: meeting_decision_2026-05-18_D1
description: 战斗全局冷却统一为0.5秒的决策
status: pending
type: decision
source_meeting: 2026-05-18_battle_tf.md
owner: teammate_a
category: battle
related_atoms: [combat_global_cooldown_constant]
created: 2026-05-18
---
## 决策
将战斗全局冷却统一为0.5秒。
## 依据
与 refgame 对比的输入响应体感测试中,0.5秒最为稳定。
## 后续行动
- [ ] @teammate_a: 将 cooldown 列批量设为0.5 (~MM-DD)

================ 1周后的每周评审 ================
$ python promote.py atoms/pending/meeting_decision_2026-05-18_D1.md
[PROMOTE] → atoms/combat_global_cooldown_constant_decisions/meeting_decision_2026-05-18_D1.md
[JIT] manifest 注册: trigger=(전투|쿨다운|GCD|cooldown), atom 18个 → 19个
[OK] 从下次会话起,输入"全局冷却"时自动注入该决策。

这一个方框就是管线的全部。值得注意的是 D2。决策内容没问题,依据也有,可 owner 是空的。解析器不会就这么放它通过。它填入 [MISSING],暂缓生成 pending,并退回给撰写者。D2 会在几天后"评审回复循环1周测试结果"的会议上获得负责人,再次进入流程。正是这一次拦下空白的退回,让三天后团队即时通讯工具里"那件事到底谁负责来着?"永远不再出现。

"没有 owner 就报告"这条规则本身,用一个 atom 固化了下来(decision_summary_not_clickup_mirror,§17.1.2)。任务工具里也许挂着一条"修改数据表"的待办,但这条待办为什么、是哪个决策的结果,只留在会议纪要 atom 里。


17.2.4 第3步 —— 在 pending 里静置一周

经解析器通过的决策不会立刻成为正式 atom,而是在 pending/ 里等待一周。因为在会议上信心十足定下的事,运营一周后被推翻是常有的。上面例子里的 D2 正处在这样的危险地带。"回复技能排除在 GCD(全局冷却)之外"这一决策,若在1周测试中回复循环出现问题,就可能再次被推翻。pending 就是强制留出让墨水变干的时间的那一格。

而且,废弃也要作为资产留存。假如像 D2 这样的决策在1周测试中垮掉了,不是直接删除,而是生成一个废弃记录 atom。

---
name: meeting_decision_2026-05-18_D2_DISCARDED
status: discarded
discarded_reason: 1周测试结果显示回复循环 DPS 曲线崩溃
---
## 原决策
对回复技能也应用0.5秒的全局冷却。
## 废弃原因
1周测试中回复循环 DPS 下降,导致整体平衡崩溃。回退为排除决策。
## 教训
"回复排除在 GCD 之外为标准" → 升格为 combat_healing_skill_cooldown_exception atom。

废弃记录会成为下次会议上"这个议题以前没试过吗?"的答案。它是防止同样的错误犯第二遍的最廉价的工具。不过废弃记录堆积起来会变成检索噪声,因此需要每季度清理重复项、只留下教训的整理。


17.2.5 第4步 —— 每周评审一次与升格

每周在固定的时间集中查看 pending 候选。结果是三者之一。

结果 处理
升格 pending → 移动到正式 atom 文件夹,注册 JIT manifest
废弃 决策被推翻 → 从 pending 移除,保留废弃记录 atom
保留 信息不足 → pending 延长1周

评审大约每10个 atom 花15分钟。一旦决定升格,promote.py 会一次性处理文件移动和 manifest 更新。

# promote.py 骨架
def promote(pending_path):
    fm, body = parse_markdown(pending_path)
    target = ATOM_BASE / f"{fm['related_atoms'][0]}_decisions" / f"{fm['name']}.md"
    move(pending_path, target)
    manifest = json.load(open(JIT_MANIFEST))
    manifest['atoms'].append({
        "name": fm['name'],
        "path": str(target),
        "trigger_regex": build_trigger(fm),   # related_atoms + category 关键词
        "description": fm['description'],
        "added": today(),
    })
    json.dump(manifest, open(JIT_MANIFEST, "w"), indent=2)
    log_promotion(fm['name'])

trigger_regex 在下次会话中与用户输入匹配时,这条决策就会被自动注入。在上面的例子里,输入"全局冷却",D1 决策及其依据就会一并进来。这正是过去靠手工誊抄的决策,在需要的那一刻自动浮现、成为资产的转折点。


17.2.6 度量 —— 与手工誊抄相比有何不同

这是笔者在项目A的运营经验中,把只定下标准格式的阶段与启动了管线的阶段作对比后的印象。下面的数字并非精确计量,而是运营中体感到的方向和大致比例,其中掺有笔者的推测(未经验证)。

项目 仅格式(手动提取) 启动管线
会议纪要 → 决策提取时间 每次会议 20\~30分钟 不到1分钟
决策的 atom 升格比例 5\~10%(整理时间不足) 60\~80%(全量审查)
"以前不是决定过吗?"的重复会议 每季度 5\~10次 每季度 0\~2次
负责人不明的决策 无法追踪 通过 [MISSING] 报告即时可见

变化最大的是升格比例。靠手工整理时,由于没有时间,90%以上的决策都挥发掉了。一自动化,全量审查成为可能,有价值的决策便一条不落地留存下来。方向是明确的。比例的精确数值则随团队规模和会议频率而变化。


17.2.7 常见失败与处方

模式 处方
只把 lint 当作建议来运营 用提交钩子强制
在决策槽位里连讨论也写进去 决策只写一句话,依据放到单独字段
让 owner 空白就这么通过 [MISSING] 报告 + 暂缓 pending 予以退回
pending 评审一再拖延 在周复盘里设固定槽位,哪怕5分钟也每周做
不留废弃记录 废弃也作为单独 atom 保留

这五行几乎就是全部。尽可能减少需要靠人的意志力去坚守的环节,把格式检查和 owner 检查交给机器,这就是这套系统的稳定点。


本章要点


游戏之外的应用。 把一份会议纪要送上"格式检查→决策提取→1周验证→正式注册"的传送带,人只在入口(撰写)和出口(每周评审一次)两处经手——这套结构不止适用于游戏,也能移植到任何知识劳动团队的文档运营中。比如咨询团队处理客户会谈笔记时,只要统一笔记格式,用 LLM 先把"决策·负责人·依据·下一步行动"这些槽位做一次初步提取,负责人为空就弹出 [MISSING] 予以退回,只把静置了一周的决策升格为正式的行动追踪器即可。手工整理时挥发掉90%以上的会议决策,一旦放上传送带,就成为全量审查的对象,一条不落地留存下来。


动手试试

setup. 在会议纪要文件夹里放一份标准格式模板,把 meeting_lint.py 挂到提交前钩子上。把 frontmatter 的4个字段和4个章节设为必需。

prompt. 把一份会议纪要交给解析器,像下面这样下达指令。

从这份会议纪要的 ## 决策 章节中,为每条决策提取 decision / owner / rationale / follow_up 四个字段,输出为 JSON。对于没有明确写出 owner 的决策,把 owner 标记为 [MISSING],并单独汇总到警告行里。不要靠推测来填。

verify. 若输出的 JSON 中有决策被填入了 [MISSING],就不要为该决策生成 pending,而是退回给会议纪要撰写者。只为 owner 全部填好的决策生成 pending 候选文件,一周后在每周评审中定下升格·废弃·保留。

单人精简版

如果是一个人工作,三个脚本加提交钩子就太重了。只把会议纪要的 ## 决策 章节标准化,每条决策用一行写下 D1: 什么 / 负责人: 我 / 依据: 为什么。每周一次,把那一周会议纪要里的决策行摘出来,汇集到一个文件(decisions.md)里,负责人为空的行就亲手留个 [MISSING] 标记,下周补上。脚本以后手忙不过来时再加也不迟。关键是"决策一行·写明负责人·每周汇集一次"这三个习惯。

17.3 会议分类·图注·同步 —— 让会议记录成为资产的三条主轴

会议记录的目的不是堆积。真正的目的是:半年后仍能被检索到、能引向决策、并在两台 PC 上呈现为相同的状态。


周二下午。我想起来,一年前的一次会议上明明约定过,要把角色服装的饱和度降低一档。可是那份会议记录怎么也找不到。打开文件夹一看,meeting_0413.md회의_수정본_final.mdIMG_2034.png 之类的两百来个文件只是按日期堆在一起。既没有分类,没有图注,也没有统一的命名。决策就在某个地方,但通往那个决策的路径已经消失了。

会议记录要成为资产,需要三件事同时运转。分类建立检索的第一入口,图注(caption)让一半的图像保持可检索状态,同步则让处理成本即便在超过 1,000 份时也只绑定在变更部分上。这三者只要缺一,会议记录就会沦为越堆越沉的死堆。

在 §17.1·§17.2 中,我们搭建了把会议记录转换为提取管线的流程 —— 用 meeting_lint.py 检查格式,decision_parser.py 提取决策的四个字段(decision / owner / rationale / follow_up),owner 缺失时以 [MISSING] 上报,先汇入 pending atom,再由 promote.py 提升。本章讲的是支撑这条管线长期不崩坏的三项运营标准。


17.3.1 分类 —— 检索的第一入口

会议记录随着时间推移会累积到数百、数千份。检索不到的资料不是资产。分类就是检索的第一个分岔口。这就像在办公室的文件柜上贴标签。没有标签的柜子,最终没有人会去打开。

笔者负责的项目A(MMORPG 开发)把分类归为五个。关键在于保持小而正交

art 视觉·美术方向 概念评审 环境色调共识 → 图注占比↑

battle 战斗·平衡 冷却时间·DPS 伤害曲线 → atom 提取↑

daily 例行进度共享 站会 今日待办 → 几乎无决策

issue 紧急问题处理 构建失败 上线前事故 → 必须事后整理

review 里程碑·QA MS 验收 季度复盘 → 摘要 atom

五格互不重叠 —— 一次会议正好归入一格 一旦扩到六格,"这算 art 还是 battle?"就会每周堵住会议

五个并不是所有团队的标准答案。如果是以非战斗系统为核心的项目,就需要把 battle 换成 system 这样的调整。关键不在于数字,而在于把分类保持得足够小,小到分类决定本身不会堵住会议这一原则。

一次会议一个分类

会议横跨两个格子的情况经常发生。如果在评审角色概念时顺带敲定了战斗动作,那算 art 还是 battle?原则是只以主产出物为准取其一。如果概念是主产出物,就归为 art,战斗动作则用 sub_topic 字段作辅助记录。

---
type: meeting_note
category: art
sub_topic: [character, battle_motion]
date: 2026-05-18
attendees: [teammate_a, teammate_b, teammate_c, 李旼洙]
related_atoms: [character_concept_kim, battle_motion_kim]
confidential: internal
---

sub_topic 只是检索的第二层过滤,不用于路由决策。路由始终只以 category 这一个值运作。一旦这条单值原则被破坏,§17.2 的 promote.py 就无法判断该把 atom 送往哪个文件夹,各分类统计之和也会对不上。正交性不是美观问题,而是管线完整性的前提。

每个分类的运营各不相同 —— 这才是拆分的真正价值

把它分成五格的真正理由,并不是检索标签。而是因为每一格的运营方式都不同,只有拆开,差异化运营才能自然而然地被设计出来。

art 的附件图像多,下一节的图注标准是必需的。由于决策以视觉为中心,决策槽里会放入 ![](images/decision_a.png) 这样的图像引用。battle 的决策是数值·规则,atom 自动提升的比例最高,而且一行决策会牵动配置表的批量变更,因此影响范围的可视化(第11部分的关系图)很重要。daily 几乎没有决策才是常态,又因累积很快,故按周拆入自动文件夹(daily/2026-W21/)。issue 的会议记录较为杂乱,因此把事后 24 小时内整理定为义务,并把防止复发的 atom 提取到 issue_postmortem/review 篇幅长,故另行撰写 5\~10 行的摘要 atom,让它在下一次季度复盘中被自动引用。

新增分类要极为谨慎。每季度发生 5 次以上、运营方式与既有五个明显不同、需要单独的路由文件夹、且一个月后仍能维持 5 次以上 —— 只有全部通过这四个条件才予以考虑。就笔者的运营经验而言,五个维持了一年以上,即便 tech_reviewexternal 这样的候选一度浮现,最终也都被 sub_topic 吸收了。

AI 分类器只应作为辅助

分类以人在撰写时直接录入为主。只有像从外部收到的资料那样存在缺失的会议记录,才用 AI 分类器来辅助。用关键词词典能捕获约 90%,剩下的 uncertain 才交给 LLM 或人来判定。

委托给 LLM 时,施加强约束的提示词更稳定。下面是实际使用的提示词全文。

以下是会议记录。请归入 5 个分类之一。

分类:
- art: 视觉·美术方向
- battle: 战斗系统·平衡
- daily: 例行进度共享
- issue: 紧急问题处理
- review: 里程碑·QA 评审

会议记录:
[全文或前 500 字]

响应格式:只给一个分类单词。禁止任何说明·依据·不确定表述。
若响应不是 5 个分类之一,则视为系统失败。

把同一份会议记录(下面是 art 会议的开头)输入进去时,Claude 的原始输出是这样的。

输入的会议记录: 角色 K_007(学者)概念 v3 评审。有意见认为服装色调的饱和度过高。达成一致:降低一档。约定下次会议一并检查战斗动作的色调。

Claude 输出: art

干净利落地只输出了一个单词。然而,把 daily 会议记录喂给同一个提示词时,也发生过这样的情况。

输入:今天的构建在凌晨挂了,原因看起来是配置表合并冲突。计划先热修复,之后再正式修改。

Claude 输出: issue

表面上这是 daily 站会里冒出的一句话,但 Claude 根据内容把它分到了 issue这正是不能把分类器当作首选的原因。 人会做出这样的运营判断:"这是 daily 当中突然冒出的构建事故,应该拆成单独的 issue 会议。"而 AI 只看文本就贴标签。标签也许没错,但要不要拆分会议,它决定不了。所以人为主,LLM 只止步于对缺失部分的辅助。

在季度复盘中,会统计各分类的会议数量,以观察"时间花在哪里"。下面的分布是笔者的估算(未经验证),绝对数量只是示例,只有比例的大小关系与实际运营的体感一致。

分类 占比(估算) 备注
daily 约 1/3 每日例行,几乎没有决策
battle 约 1/5 战斗 TF 每周 2 次
art 约 1/7 美术评审 + 外部会议
issue 构建事故等
review 最低 里程碑·季度复盘
其他 约 1/5 1:1、外部等非分类

如果 issue 在某个季度格外突出,那么改善构建·CI 稳定性就会浮上为下一个优先事项。分类不仅用于检索,也是映照组织时间分配的一面镜子。


17.3.2 图注 —— 让一半图像存活下来的一行字

art 会议记录的正文有一半是图像。而没有图注的图像,就像堆在桌上的一叠照片。当天什么都记得,可一个月后,只有在背面写了一行备注的照片才能存活下来。

flowchart LR A["会议刚结束<br/>只有参会者能懂"] --> B["1 周后<br/>连撰写者也只记得一部分"] B --> C["1 个月后<br/>不清楚与哪个决策相关"] C --> D["6 个月后<br/>实际上已废弃 · 无法检索"] A -.图注一行字.-> E["即便 6 个月后<br/>仍能用决策 ID 反向引用"] style D fill:#fcd6d6,stroke:#d94a4a style E fill:#d6fce0,stroke:#4ad97a

图像占了会议记录的一半,若无法检索,就等于会议记录资产的一半消失了。让那一半存活下来的,就是一行图注。

图注三要素

项目A 的图注标准三行就写完。

![](images/2026-05-18_art_review/character_kim_concept_v3.png)

**[图 1]** 角色 K_007(学者)概念 v3 —— 服装色调饱和度降低一档
*决策:D2(服装饱和度 -10%) | 下一步行动:v4 制作(~MM-DD)*

三个要素各自打开不同的检索路径。编号 + 一行说明留下在正文中以"参见图 1"引用的路径,决策 ID 引用(D2)留下"与此决策关联的图像"这一反向引用,下一步行动留下后续工作的线索。三行都能在 1 分钟内写完。"即时附加"并不意味着"在会议中撰写"。现实的做法是:会议中只整理决策,结束后立刻在 10 分钟内补齐图注。

文件名和文件夹是第一入口

和图注同样重要的是文件名。因为文件夹和文件名本身就是检索的第一入口。

会议记录文件夹/
├── 2026-05-18_art_review.md
└── images/
    └── 2026-05-18_art_review/
        ├── character_kim_concept_v3.png
        ├── env_palette_comparison.png
        └── reference_external_game_a.png

规则是 <主题>_<条目>_<版本 or 备注>.<ext>,禁止使用韩文·空格·特殊字符(防止路径编码事故)。IMG_2034.png(毫无含义)、김캐릭터 v3.png(韩文·空格)、final_final_v3_real.png(版本无意义)、untitled.png(废弃候选)全都是反模式。与其依赖人的自觉,不如在 meeting_lint.py 里加一条检查规则来强制执行更好 —— 在 §17.2 中把格式检查自动化的那个 lint 上,再叠加一行文件名检查就够了。

外部资料出处与 confidential 等级

会议中经常会引用外部游戏·美术作为参考。若没有出处,就会直接酿成版权事故。

![](images/2026-05-18_art_review/reference_external.png)

**[图 3]** 参考图像 —— refgame(Developer Y, 2024)
*引用理由:比较相似概念的饱和度处理。无直接借用。*

出处(游戏名·开发商·年份)·引用理由·是否直接借用,都要一一注明。而且图像比文本的泄露风险更大,因此在 frontmatter 中标注等级。

confidential: internal   # internal / restricted / external_ok
images:
  - file: character_kim_concept_v3.png
    confidential: restricted
    reason: 未公开的角色设计

internal 指公司内部共享,restricted 指仅限该 TF·负责人,external_ok 指获准用于营销·外部共享。会议记录构建时按等级分离输出,非 external_ok 的图像在外部共享版中自动做模糊处理。这一自动分离带来的直接效果,是把外部共享的遮罩事故实质上降为 0。

图注也让 AI 打初稿

给 50 张图像手写 50 条图注是个负担。把正文和文件名交给 AI,批量拿到初稿。

以下是会议记录正文 + 图像文件列表。

[会议记录正文]
[10 个图像文件名]

请为每张图像撰写图注初稿。

格式:
- [图 N] <说明> —— <核心决策或变化>
- *决策:D? | 下一步行动:?*

对于在正文中找不到依据的图像,标注为"内容不明 —— 需撰写者确认"。

这里最后一行才是关键。输入同一份会议记录时,Claude 对正文中有依据的图像都加了图注,但对 reference_external_game_a.png 则这样回答。

Claude 输出(节选): [图 3] reference_external_game_a.png —— 内容不明,需撰写者确认。正文中未注明这张外部参考图像的引用理由。

这是 AI 把不知道的事情如实上报为"不知道"。撰写者据此补上引用理由。若仅凭正文上下文仍不够,就只挑核心的 5\~10 张送入 Vision 模型(由于图像 token 成本高,不会全部跑一遍)。

# 只选用核心的 5~10 张图像 —— 每张图像的 token 成本很高
response = client.messages.create(
    model="claude-opus-4-8",
    messages=[{
        "role": "user",
        "content": [
            {"type": "image", "source": {"type": "base64", "data": img_b64}},
            {"type": "text", "text": "用一行中文描述这张图像。禁止猜测,只描述看到的。"},
        ],
    }],
)

撰写者把这一行整理成图注格式。没必要对所有图像都跑 Vision。仅核心的 5\~10 张,就足以显著提升可检索性。

图注写得好的一年份会议记录,本身就成为一份视觉开发文档(visual development document)。character_kim v1 → v2 → v3 的视觉变化可以用决策 ID 追溯,只筛选 external_ok 等级就能自动整理出对外汇报资料,把各领域的核心图像 + 图注汇集起来则成为新团队成员的入职资料。若以笔者的估算(未经验证)来表达引入图注前后的变化,方向是这样的 —— 半年前会议记录的检索成功率大幅上升,"这张图我在哪见过?"这类重复提问大幅减少,外部共享的遮罩事故收敛于 0。绝对数值因团队而异,但仅凭阶段 1·2(文件名标准 + 图注格式),那个方向就已经清晰地显现出来。


17.3.3 同步 —— 不是全量,而是只处理变更部分

会议记录本身是文本文件,用 git 就够了。同步真正的对象,是从会议记录中派生出的数据 —— §17.2 的 pending atom 候选、JIT manifest、分类统计、决策索引(decision_index.json)、图注索引、按 confidential 等级的构建输出,以及用于向量检索的 LLM 嵌入(embedding)。这些数据都必须对会议记录的变更做出响应。

问题在于:会议记录一旦超过 1,000 份,每次都重新处理全量的成本会占到运营的一半。这就像停下整条生产线,把所有零件重做一遍 —— 明明只改了一个零件。

flowchart TB subgraph Full["Full Sync · 100 份以下有效"] F1["全部会议记录 1,000 份"] --> F2["所有派生数据<br/>从头重新生成"] F2 --> F3["索引·嵌入全部替换"] end subgraph Inc["Incremental · 超过 200 份则必需"] I1["用 git diff<br/>只检测变更的 N 份"] --> I2["只重新生成<br/>那 N 份的派生数据"] I2 --> I3["索引部分更新<br/>新增·修改·删除分支"] end Full -.会议记录突破 200 份.-> Inc style Full fill:#fce7d6,stroke:#d98a4a style Inc fill:#d6fce0,stroke:#4ad97a

Full Sync 实现简单,状态不一致的风险为 0,在引入初期(100 份以下)反而更安全。这并不是说 Full 是坏做法。只是与会议记录数量成线性正比的成本,会从超过 200 份的那个位置开始成为瓶颈。到那时就切换到 Incremental。

变更检测以 git diff 为准

Incremental 的第一步,是准确判定"哪些文件发生了变更"。文件 mtime 虽快,但只要 touch 一下就会被当作变更,精度低。文件哈希以内容为准,准确,但对新增·删除的区分较弱。笔者推荐基于 git diff。记录下最后一次 sync 时的提交哈希,只处理其后发生变更的文件。它能准确捕获新增·修改·删除,同时额外的状态管理负担最小。

# incremental_sync.py 骨架
def get_changed_files(last_sync_commit):
    result = subprocess.run(
        ["git", "diff", "--name-only", last_sync_commit, "HEAD", "--", "meetings/"],
        capture_output=True, text=True
    )
    return result.stdout.strip().split("\n")

def sync():
    last_commit = read_state("last_sync_commit")
    for path in get_changed_files(last_commit):
        if not os.path.exists(path):
            handle_deletion(path)        # 批量删除 atom·索引·嵌入
        elif is_new(path, last_commit):
            handle_creation(path)        # lint → 提取决策 → pending atom → 索引 → 嵌入
        else:
            handle_modification(path)    # 使既有派生失效后重新处理
    write_state("last_sync_commit", get_current_commit())

这里还有一个对成本影响最大的分支。即会议记录是正文也变了,还是只有 frontmatter 变了。

def detect_change_scope(file_path, last_commit):
    diff = subprocess.run(
        ["git", "diff", last_commit, "HEAD", "--", file_path],
        capture_output=True, text=True
    ).stdout
    fm_lines, body_lines = split_diff_by_section(diff)
    return {"frontmatter_changed": bool(fm_lines), "body_changed": bool(body_lines)}

scope = detect_change_scope(path, last_commit)
if scope["body_changed"]:
    full_reprocess(path)          # 含嵌入重新生成
elif scope["frontmatter_changed"]:
    metadata_only_update(path)    # 嵌入重新生成为 0

如果只是 categoryconfidential 这类元数据变了,就没必要重新生成 LLM 嵌入。嵌入通常是同步成本中最大的一块,因此这一次分支能大幅降低成本。嵌入以 content_hash 为准做缓存 —— 正文哈希相同就直接复用缓存的嵌入,而仅修改 frontmatter 时,嵌入调用为 0。

成本差异的方向很明确(下面是笔者估算,并非绝对值)。在每周变更 50 份左右的运营中,相比每周 Full re-embed,Incremental 的嵌入成本降到了几十分之一的水平。会议记录越增加,Full 的成本就与累积量成正比地变大,而 Incremental 的成本只绑定在每周的变更份数上,与累积量无关,几乎是平的。这种"与累积无关"的性质,正是 Incremental 的本质价值。

两道安全网 —— 定期 Full re-sync 与单 PC sync

Incremental 快,但代价是背负着累积性不一致的风险。若因一个小 bug 漏掉了一条 atom,这个遗漏不会在下一次 Incremental 中自行修复。因此加装护栏 —— 每天 Incremental,每周对最近 1 周份做 Partial Full(验证),每月一次全量 Full re-sync 来检查索引·嵌入的一致性。若在每月检查中发现不一致,就加固变更检测逻辑。这每月一次,是长期运营的最后一道安全网。

在此之上,还叠加一层 PC 分离运营。笔者在公司 PC 和家里 PC 两处处理会议记录。原则是 sync 作业只在一台 PC 上执行。

流程 处理
公司 PC → git push 由公司 PC 负责 sync 作业(重新生成派生数据)
家里 PC → git pull 只更新 last_sync_commit,无需重新处理
两侧都变更后 merge 以 merge 结果为准重新计算 changed 文件

若两侧同时 sync,last_sync_commit 状态就会冲突,而这种冲突会悄悄地让索引错位。把一台 PC 固定为 sync 主体这条简单的规则,才是最可靠的防御。


17.3.4 三条主轴在同一条管线中交汇之处

分类·图注·同步并不是各自为政的标准。三者在 §17.2 的提取管线之上被拧成一条流。

会议记录一经撰写,category 就决定 promote.py 的路由,图注的决策 ID 与 decision_parser.py 提取的决策四字段相连接,而如此生成的所有派生数据,再由 Incremental sync 只挑变更部分来更新。本章运营的出发点是 decision_summary_not_clickup_mirror(§17.1.2)。分类打开通往决策的路径,图注留下决策的视觉证据,同步则把那份决策资产在两台 PC 上保存为相同的状态。

周二下午的那种茫然 —— 明明达成了一致,却无路可达的那种状态 —— 会在这三条主轴运转的瞬间消失。用 category: art 把文件夹收窄,用图注里的 决策: D2 触及准确的决策,同步再把那份会议记录在家里也呈现为相同的样子。


游戏之外的应用。 资料只有被检索·被引用·被同步,才终于成为资产 —— 这条原则并非游戏会议记录独有,而是所有与文档打交道的职场人的共同课题。分类(小而正交的类别)·图注(附件图像的一行说明)·同步(不是全量,只有变更部分)这三条主轴,即便换掉领域也照旧成立。比如,销售团队若积累一年份的客户会谈资料,只要把分类固定在"新提案·合同谈判·售后支持"等五格以内,给每张附上的报价单截图都配一行像"[图 1] A 公司第 2 轮报价 —— 单价下调 5%"这样的说明,云同步只挑改动过的文件来处理就行。这样,半年后想找"当时为什么把单价压下来了",一行图注就能立刻查到。


17.3.5 动手试试

setup 1. 把会议分类定义在 5 个以内(以 art / battle / daily / issue / review 为出发点,按团队情况替换 1\~2 个)。 2. 在 meeting_lint.py 中追加两项检查 —— category 是否为已定义值之一,图像文件名是否符合 <主题>_<条目>_<版本> 模式(无韩文·无空格)。 3. 在 frontmatter 中加入 confidential 字段,并准备用于记录 last_sync_commit 的状态文件。

prompt(辅助分类缺失的会议记录)

以下是会议记录。请归入 5 个分类之一。
[5 行分类定义] / [会议记录前 500 字]
响应格式:只给一个分类单词。禁止任何说明·依据·不确定表述。
若响应不是 5 个分类之一,则视为系统失败。

verify 1. 亲自检索一下:任取一份半年前的会议记录,能否仅凭分类 + 图注决策 ID 找到它。 2. 确认 git diff --name-only <last_sync_commit> HEAD 捕获的变更文件数,是否与实际修改的会议记录数一致。 3. 不要不加批判地接受 AI 的分类结果,对 uncertain 以及"daily 当中出现的决策"这类情况,让人再看一遍。


17.3.6 单人精简版

如果你是独自工作的策划,就这样精简。

即便在单人规模下,不变的核心只有一个 —— 留下通往决策的路径。分类·图注·同步只不过是撑起这条路径的三根支柱,完全可以按规模把它们立得纤细一些。


本章要点

下一章预告

17.4 把会议记录变成决策数据库 —— AI 自动化的 5 个切入点

距离里程碑演示还有三天,午饭时,一位策划放下餐盘问道:"把任务奖励金币提高到 1.5 倍那件事,是会上定下来的吧?可以录入配置表了吗?"旁边的人答道:"那不是有人只是提议'要不试试看'吗。"90 分钟的录音文件和某人用键盘敲下的两页备忘录,确实都在。但那份记录装下了"讨论了什么",却没能装下"决定了什么、由谁负责、为什么这么定"。

把公司的 17 份 R&D 文档按"痛点"程度排序时,占比最大的是会议记录改进计划书。这出乎意料。既不是战斗平衡,也不是内容量产管线。最痛的地方只有一个:会上做出的决策没有传导到执行。

于是我把会议记录系统重新设计成了决策追踪数据库,并用六个月的亲自运营,验证了在这条流程的哪些环节该放入 AI、哪些环节不该放。本章就是这 5 个切入点的地图。


17.4.1 AI 自动化可以介入的 5 个切入点

从录音文件开始,到决策图谱更新为止,在整条会议记录管线中能安放 AI 助手的位置,正好有 5 个。但不会 5 个同时放入。因为每个位置的成熟度和出事风险各不相同。

flowchart TD A[会议录音音频] -->|"位置 1<br/>STT"| B[原始文本] B -->|"位置 2<br/>生成初稿"| C[会议记录初稿] C -->|"meeting_lint.py"| D[标准格式会议记录] D -->|"位置 3<br/>补全决策槽位"| E[决策 4 字段完成] E -->|"decision_parser.py<br/>位置 4 路由"| F[pending atom] F -->|"promote.py<br/>位置 5 关系抽取"| G[正式 atom + 图谱] style C fill:#ffd9d9,stroke:#c0392b style E fill:#d9f0ff,stroke:#2980b9 style F fill:#d9f0ff,stroke:#2980b9

红色(位置 2)是最诱人也最危险的位置,蓝色(位置 3、4)是最先引入的安全位置。管线中间的 meeting_lint.pydecision_parser.pypromote.py 不是 AI,而是确定性脚本。AI 只进入这个确定性骨架之间"需要判断的缝隙"。

用一句话概括各切入点的性质,如下。


17.4.2 确定性骨架 —— 先造出让 AI 介入的缝隙

在谈 AI 自动化之前,得先看不属于 AI 的脚本骨架。因为会议记录之所以能成为决策数据库,关键不在 LLM,而在三个小小的 Python 脚本。

标准格式的会议记录在末尾有一个决策块。块中的每条决策都强制要求四个字段。

## Decisions

D1:
  decision: 将战斗全局冷却统一为 0.5 秒
  owner: teammate_a
  rationale: 技能连招测试中 0.3 秒经常丢输入(正文 14:22)
  follow_up: 在连招设计表中落实 GCD 0.5,6/13 前完成

这个块由 decision_parser.py 读取。核心动作很简单——四个字段中只要有一个为空,就打上 [MISSING] 上报。尤其是没有 owner 时,那条决策就是"无人负责的决策",也就是不会被执行的决策,因此拦得最狠。

$ python decision_parser.py 2026-06-06_combat-sync.md

D1: OK   (owner=teammate_a)
D2: [MISSING owner]  "治疗技能是否排除 GCD,待议" —— 无 owner,阻止晋级
D3: [MISSING rationale]  依据字段为空,警告

被打上 [MISSING owner] 的 D2,连 pending 文件夹都进不去。在人补上 owner 之前,它不会被当作决策。这就是从结构上防止"会开了,却什么都没推进"的装置。

通过的决策由 promote.py 变成 pending atom,在每周一次的评审关卡上经人批准后,晋级为正式 atom。此时适用的原则就是 decision_summary_not_clickup_mirror atom(§17.1.2)。任务看板追踪"要做什么",决策数据库追踪"为什么这么定"。把两者混在一起,两者都会坏掉。

这三个脚本是骨架,AI 是填补这副骨架空白的助手。顺序一旦颠倒——由 AI 来搭骨架——幻觉就会摧毁决策数据库的信任本身。


17.4.3 最危险的位置 —— 为什么把位置 2 放到最后

位置 2(STT 文本 → 自动生成会议记录初稿)是所有团队最想先做的位置。因为"只要把录音丢进去,会议记录就出来"这幅图景太诱人了。而恰恰是这份诱惑,让它以最高的代价失败。

失败的样态有四种。

flowchart TD R[STT 文本 → AI 初稿] --> R1["幻觉决策<br/>把没达成一致的记成决策"] R --> R2["决策遗漏<br/>把真正的决策放走"] R --> R3["说话人误判<br/>把提议者张冠李戴"] R --> R4["语气抹平<br/>反对意见与细微差别丢失"] R1 --> X["会议记录赖以存在的<br/>决策追踪意义崩塌"] R2 --> X R3 --> X R4 --> X style X fill:#ffd9d9,stroke:#c0392b

其中最致命的是幻觉决策。会上有人只是抛出一句"全局冷却是不是 0.5 秒更好?"的意见,AI 初稿却写成"全局冷却已达成一致为 0.5 秒"。三周后,这一行被落实进配置表,连招设计在其上叠加,QA 用例也随之写就。一个从未达成一致的决策,不可逆地扩散开来。

因此位置 2 适用几条绝对原则。

这不是说位置 2"永远不做"。当位置 3、4、1 稳定下来、主持人亲身体会到 AI 输出的局限之后,引入位置 2 的价值是足够大的。只是顺序上排在最后而已。


17.4.4 位置 3 —— 补全决策槽位为何 ROI 第一

这是六个月运营中产出效果最大的位置。人声明决策的存在,AI 填写这条决策的附属字段。与位置 2 的决定性区别在于:决策存在这一事实本身,由人先行钉死

有三样东西,人亲手填太费时间,便由 AI 起草。

关键在于提示词强制引用依据、并明确禁止幻觉。下面是实际运营中使用的补全提示词全文。

实操记录(worked transcript)—— 补全决策槽位

提示词(由人输入):

看下面的会议正文,只填主持人已声明的决策 D1("将战斗全局冷却统一为 0.5 秒")的
owner / rationale / affected_atoms / follow_up 这四栏。
不要新造决策。rationale 要把正文发言连同时间戳原样引用,
没有依据的栏不要臆测,写上"无依据"。
// (意图:affected_atoms 只取正文提到的系统与表,最多 3 个)

[会议正文]
(14:18) teammate_a: 现在 GCD 是 0.3 秒,连续按技能时输入老是被吞。
(14:22) teammate_b: 昨天的连招测试里,0.3 秒下第二个技能放不出来的
        情况大概占 30%。我留了日志。
(14:25) teammate_a: 那就提到 0.5 秒吧。我来在连招表里落实。
(14:26) 主持人: 就走 0.5 秒。定了。
(14:31) teammate_c: 治疗技能如果套 GCD,危急时就用不了,那个怎么办……
(14:33) 主持人: 那个下次单独看。

Claude 输出(原样):

1. owner: teammate_a
   依据:(14:25) "我来在连招表里落实。" —— teammate_a
   明确表示由本人负责。

2. rationale:
   - (14:18) teammate_a: "GCD 0.3 秒下连续按技能时输入被吞"
   - (14:22) teammate_b: "连招测试中 0.3 秒下第二个技能未触发
     的情况约 30%,有日志"
   → 0.3 秒的输入丢失问题,是 0.5 秒决策的直接依据。

3. affected_atoms (基于正文提及的候选):
   - atoms/combat/global_cooldown
   - 连招设计表(正文 14:25 提到"连招表")
   - atoms/combat/skill_recovery  ← 但 (14:31) 中治疗技能的
     GCD 例外讨论"被推迟"(14:33)。是否纳入本决策的影响范围
     尚不确定。依据薄弱,需人工确认。

4. follow_up:
   - teammate_a: 在连招设计表中落实 GCD 0.5 秒
   - [单独议题] 治疗技能是否 GCD 例外不在本决策范围内,
     拆分为下次会议议题(14:33 主持人发言)

人的核验/否决:

主持人审核了输出。owner 和 rationale 的引用准确,直接采用。affected_atoms 的第三个候选 skill_recovery,AI 自己上报了"依据薄弱,需人工确认",经主持人判断,把它从本决策的影响范围中排除了——治疗技能的例外是另一条决策的事,而不是这次 D1 的影响。follow_up 中"拆分为单独议题"的建议被采纳,登记为下次会议的议题。

这里重要的是,AI 没有用幻觉硬把不确定的项目塞进来,而是主动上报了不确定性。正是"禁止臆测与幻觉,没有依据就写明无依据"这条提示词约束,造就了这份诚实的输出。去掉约束,AI 会理直气壮地把 skill_recovery 放进 affected_atoms,而那个幻觉会扩散到图谱里。

审核完毕的决策块通过 decision_parser.py——因为四个字段都已填满,没有 [MISSING]——进入 pending atom


17.4.5 位置 4 —— atom 路由,以及位置 5 —— 关系抽取

当通过位置 3 的 pending atom 晋级到正式文件夹时,由 AI 推荐送往哪个文件夹(位置 4)。

把这个 atom("战斗全局冷却统一为 0.5 秒 / owner teammate_a")放进下面哪个
文件夹更合适,按优先级最多选 3 个。不要提议新建文件夹,
只在这个列表里选。
- atoms/combat/  atoms/character/  atoms/operations/  atoms/visual/

"禁止新建文件夹"是核心约束。去掉它,AI 就会没完没了地提议 atoms/combat_timing/atoms/gcd_rules/ 之类的文件夹,类别无限增殖,检索与自动注入随之崩坏。类别要保持少而正交,并以一年以上不变动为原则。AI 只在那个封闭列表里挑选。

位置 5(atom 之间的关系抽取)最晚、最慎重地引入。它是推断已晋级 atom 之间依赖关系的位置。

新增 atom A: "治疗技能排除全局冷却"
既有 atom B: "全局冷却统一为 0.5 秒"

推断出的关系:
  A.exception_of: [B]
  A.derives_from: [B]
  B.affects: [A]   ← 反向自动赋予

问题是,这种推断正面暴露在 LLM 的非确定性之下。同样的输入,昨天和今天会得出不同的关系。缓解装置有三种——temperature=0,并在支持的模型上固定 seed;给出候选、由人批准的审核关卡;以及只抽取单向、反向再由脚本确定性补齐的方式。因为若把双向都交给 LLM,总会漏掉一边。


17.4.6 一次 1\~2 个 —— 引入顺序即是安全装置

同时开启 5 个切入点,是最常见也最昂贵的失败。运营负担比效果更早到来,团队会把整套系统一起丢弃。下面是实际遵循的顺序。

第 1 步 · 位置 3 补全决策槽位 1~2 个月 · ROI 第一,从风险最低起步

第 2 步 · 位置 4 atom 路由推荐 再加 1 个月 · 强制文件夹候选封闭列表

第 3 步 · 位置 1 STT 自托管基础设施落地后 · 出于安全规避外部 API

第 4 步 · 位置 2 会议记录初稿 上述 3 个稳定后,最为慎重 · 绝对禁止自动提交

第 5 步 · 位置 5 关系抽取

把位置 2 放到最后,是这个顺序的核心。最想做的位置最后才做——这有违直觉,但让最熟练的手落在最危险的操作台上,才是作业现场的安全原则。

从成本上看,这个顺序也合理。以每月 100 场会议计,位置 3 约 $5\~10,位置 4 约 $1\~2(基于作者运营环境的估算,未经验证),只开这两个,每月也不到 $10。产出效果最大的两个位置,恰恰最便宜。


17.4.7 before / after —— 同一场会议,两份会议记录

同一场会议用两种方式记录时的差异,就是本章的全部概括。

Before —— 自由叙述式会议记录(无 AI,或把位置 2 连决策槽位都交出去的情形):

## 2026-06-06 战斗同步会议

讨论了 GCD 相关。有人提出 0.3 秒太短。
说连招测试中出过问题。提到了 0.5 秒。
治疗技能例外也被顺带提了一句。
气氛上大体朝 0.5 秒的方向收拢。

三周后再打开这份会议记录,"朝 0.5 秒收拢的气氛"到底是决策还是意见、谁负责落实进表、治疗技能例外是定了还是推迟了,谁都无法复原。没有说话人,没有 owner,依据也只在正文某处,只能重新去听录音。

After —— 决策槽位 + 位置 3 补全的会议记录:

## 2026-06-06 战斗同步会议

### 议题摘要(AI 辅助)
- 战斗全局冷却(GCD)0.3 秒的输入丢失问题
- 治疗技能是否 GCD 例外(拆分为单独议题)

### Decisions  (人声明 + AI 补全)
D1:
  decision: 将战斗全局冷却统一为 0.5 秒
  owner: teammate_a
  rationale: |
    - (14:18) teammate_a: 0.3 秒下连续按技能时输入被吞
    - (14:22) teammate_b: 连招测试 0.3 秒第二个技能未触发 ~30%,有日志
  follow_up: teammate_a —— 在连招设计表中落实 GCD 0.5 秒(6/13 前)
  affected_atoms: [atoms/combat/global_cooldown, 连招设计表]

### 拆分出的议题
- 治疗技能 GCD 例外 → 下次会议(14:33 主持人决定)

三周后,这份会议记录已被 decision_parser.py 读取并连入图谱,任何人问起"为什么是 0.5 秒",都能凭 rationale 的两行引用即时作答。因为写明了 owner,便能追踪 follow_up 是否已执行;就连治疗技能例外不是决策而是被推迟的议题这一事实也得到保存。

造成差异的不是 AI 的篇幅,而是在保留人声明决策的位置的前提下,只把填依据交给 AI 的结构。把上面 After 会议记录中 AI 填写的段落(议题摘要、rationale 引用、affected_atoms 候选)全部删掉,剩下的只有一行决策和 owner——会议记录的信息量有一半以上来自 AI 补全,但关键在于,这一半全都是通过了人工审核的依据引用。


本章要点


游戏之外的应用。 "决策的存在由人声明,AI 只填依据、责任人与影响"这条原则,不止游戏,对所有用 AI 整理录音的职场人都同样适用,是一条安全线。最诱人的位置(把录音整段自动生成为会议记录)之所以最危险,是因为 AI 会把"是不是 0.5 秒更好"这样的意见,变造成"已达成一致为 0.5 秒"这样的决策——正是这种幻觉。比如人事部门整理考评会议的录音时,只由主持人亲自钉死"确定为 B 级"这条决策,而只让 AI 做"从录音中引用支撑这个等级的发言,没有就说没有"。一旦让 AI 来做决策,一个从未达成一致的评价,就会不可逆地留在人事记录里。


动手试试

setup 1. 在会议记录标准格式中建立 ## Decisions 块,并对每条决策强制 decision / owner / rationale / follow_up 四个字段。 2. 编写 decision_parser.py——四个字段中只要有一个为空就输出 [MISSING <字段>],尤其当 owner 为空时阻止晋级。 3. 把"决策摘要不是任务看板的镜像,而是承载'为什么'的独立资产"这条规则(decision_summary_not_clickup_mirror)写成明文。

prompt 4. 写位置 3 的补全提示词。必须包含的约束:"不要新造决策 / 引用正文依据并附上时间戳 / 没有依据就写明'无依据' / 禁止臆测与幻觉"。要求 rationale、owner、affected_atoms、follow_up 四个槽位。 5. 在位置 4 的路由提示词中放入"禁止新建列表之外的文件夹 + 封闭的文件夹列表"。

verify 6. 用 decision_parser.py 跑一遍补全后的决策块,确认没有 [MISSING]。 7. AI 填写的 affected_atoms 中被标为"依据薄弱"的项目,由人亲自审核后排除或采纳。任何情况下都不做自动提交。

单人精简版 如果你独自工作或没时间装工具,不用脚本,只在会议记录末尾手写四行决策块(决定 / 负责 / 依据 / 下一步行动)即可。即便 owner 就是你自己,也要写上名字。对 AI 只需说"从会议备忘里引用这条决策的依据,没有就说没有"。即便没有管线,只要声明决策的位置强制引用依据的提示词这两样,会议记录就会开始变成决策数据库。

18.1 决策追踪系统

那是季度会议进行到一半的时候。战斗设计师提议"把全局冷却(GCD)统一为 0.5秒",大家都点头赞同。可是坐在旁边的资深同事举起了手。"这不是和去年第四季度定的 0.3秒相冲突吗?当时为什么定成 0.3秒来着?"会议室一时安静下来。没有人记得那个决定的依据。翻遍了会议记录,却只有"在战斗 TF 中讨论过"这一行字。最后花了 30分钟去重现去年的决定,即便如此,"为什么是 0.3"始终没能找到。

决策的难点不在于做出,而在于追踪。一年积累数百件之后,哪个决策还有效、哪个已经废弃,哪个决策又以另一个决策为前提,单靠人脑已经跟不上。本章讨论的,是把决策固化为 atom(最小知识单元)、将其转化为可追踪资产的系统。核心很简单:把一个决策记录为一张带有 decision_idownerrationale 的卡片,用 wikilink 把卡片彼此相连形成图谱,再用 grep 反向追溯影响波及到哪里。

18.1.1 决策卡:固化为 atom

决策追踪的最小单位是决策卡。这里原样取自笔者所负责的项目A(MMORPG 开发)中实际使用的一张卡片,正是前面会议上发生冲突的那个 0.5秒统一决定。

---
decision_id: D2026_Q2_017
title: 战斗全局冷却统一为 0.5秒
type: system_change
status: active        # active / superseded / deprecated
created: 2026-04-18
owner: teammate_a      # 战斗设计师,决定的发起者·所有者
approved_by: 李旼洙    # Design Director
approval_meeting: 95_BattleTF_2026-04-18

scope:
  - combat_system
  - all_active_skills

content: |
  对所有战斗主动技能应用 0.5秒全局冷却。
  治疗技能除外(单独决定 D2026_Q2_018)。

rationale:
  - 连招输入的可读性问题(用户反馈累积)
  - 模拟中战斗平均时长呈增加方向
  - 新用户学习曲线趋于平缓

affected_atoms:
  - combat_global_cooldown_constant
  - combat_skill_cooldown_rule

affected_files:
  - CombatBalance.xlsx
  - CombatFormula_v3.md
  - UI/skill_cooldown_indicator

implementation:
  target_build: 2026-05-09
  impl_owner: teammate_b    # 代码负责人
  qa_owner: teammate_c      # 资深 QA

related_decisions:
  - supersedes: D2025_Q4_034   # 之前的 0.3秒决定
  - relates_to: D2026_Q2_018   # 治疗例外
---

三个字段是脊椎。decision_id 为决策赋予永久地址。owner 钉死"谁对这个决策负责"。rationale 回答半年后的"当时为什么这么做?"。会议上没能找到的那个"为什么是 0.3",本应正是写在 D2025_Q4_034rationale 字段里的内容。其余字段(scopeaffected_atomsrelated_decisions)是为影响追踪和图谱连接而铺设的线路。

这里引入了一项设计决策。如果强制填满全部 12个字段,人们就会连卡片本身都不愿写。因此把它分成必填的 5个字段(decision_idtitleownerstatusrationale)和选填的 7个字段。会议上做出决定后即使只填这 5个字段,卡片也是有效的,其余的在实现阶段再补。

18.1.2 决策追踪的整体流程

一张卡片从产生到废弃要走过怎样的路径,构成了追踪系统的骨架。请注意不可逆关卡位于何处。

flowchart TD A[会议·即时通讯中产生决策] --> B[起草决策卡<br/>必填5字段] B --> C[赋予 decision_id·登记索引] C --> D{影响范围分析<br/>impact} D --> E[填充 affected_atoms·affected_files] E --> F[用 wikilink 连接图谱] F --> G{owner·approved_by 评审关卡} G -->|驳回| B G -->|批准| H[纳入构建] H -.不可逆.-> I[传播至其他文档·决策] I -.不可逆.-> J[事后测量·验证] J --> K{演进判断} K -->|被替代| L[status: superseded<br/>supersedes 链接] K -->|有效| M[保持 status: active] style H fill:#ffe0e0 style I fill:#ffe0e0

从起草(B)到评审关卡(G)全部是可逆阶段。无论是修改还是废弃卡片,成本都几乎为零。然而纳入构建(H)之后就是实质上的不可逆了。用户已经体感到的变更,即便用热修复回退,也会在社区认知中留下痕迹;而当后续决策开始以这个决策为前提层层累积时,回退成本会呈指数级增大。因此决策者的一切评审都必须在关卡 G 处结束。这与第5部分讨论的"录音·选角是不可逆阶段"原则,是完全相同的结构。

18.1.3 决策图谱:把卡片连起来

把卡片做成 atom 之后,卡片彼此就能相连。related_decisions 中的 supersedesrelates_to 成为图谱的边。前面会议上的冲突,其实就是这张图谱的一个片段。

D2025_Q4_034 全局冷却0.3秒 (deprecated)

D2026_Q2_017 全局冷却0.5秒 (active)

D2026_Q2_018 治疗技能冷却例外 (active)

D2026_Q2_025 PvP 全局冷却变体 (active)

supersedes

relates_to

relates_to

如果当时有这张图谱,会议 30秒就能结束。打开 D2026_Q2_017 就能看到 supersedes: D2025_Q4_034,点一下那张卡的 rationale,"为什么是 0.3"就原样出现了。图谱是决策的演进历史,而决策的演进历史就是游戏的历史。连像 PvP 变体(D2026_Q2_025)这样从本决策派生出的分支,也能一目了然地追踪到。

18.1.4 自动提取影响范围 —— impact

决策卡的 affected_atomsaffected_files 若由人一个个去填,总会漏。项目A里有一个叫 impact 的影响范围提取流程。它接收一个决策 atom,朝三个方向扫描图谱。

三条路径的并集,才是决策真正的影响范围。在此之上,portal_layer_change_impact_check atom 会额外检查"是否触及门户层(对外暴露的文档·API 规格)"。一旦涉及门户层,等级就上调一级。因为对外传播的回退成本更高。

18.1.5 实操记录(worked transcript):从会议记录到决策卡

理论到此为止。下面把实际向 LLM 抛出一整段会议记录、拿到决策卡的全过程,连同提示词全文和原始输出原样刊出——这就是实操记录(worked transcript,完整保留真实操作过程的记录)。不做摘要。Claude 混淆的地方、人工驳回的地方,乃至重新请求,全部呈现。

第1次提示词(全文)

把下面的会议记录转成决策卡 YAML。schema 为
decision_id / title / type / status / owner / approved_by / scope /
content / rationale / affected_atoms / affected_files / related_decisions。
owner 是发起者,approved_by 是审批者。affected_atoms 和 related_decisions
只写会议记录里实际出现的,不要臆测。
// (意图:decision_id 为 D{YYYY}_Q{n}_{seq},不知道 seq 就填 XXX)

[会议记录]
- 决定把战斗全局冷却统一为 0.5秒(teammate_a 发起)。
  当前每个技能各不相同,有反馈称连招可读性差,已累积。
- 有人指出去年定的 0.3秒太快,连招会糊成一团。
- 治疗技能若套用 0.5秒就无法应对危机,需要做例外处理。
- 总监(李旼洙)审批。teammate_b 负责实现,teammate_c 负责 QA。
- target build 为 5月9日。

Claude 第1次输出(原样,未加工)

---
decision_id: D2026_Q2_XXX
title: 战斗全局冷却统一为 0.5秒
type: system_change
status: active
owner: teammate_a
approved_by: 李旼洙
approval_meeting: 95_BattleTF
scope:
  - combat_system
  - all_skills          # ← 明明有治疗技能例外,却当作了 all_skills
content: |
  对所有战斗技能应用 0.5秒全局冷却。
rationale:
  - 连招可读性反馈累积
  - 去年 0.3秒太快,连招糊掉
affected_atoms:
  - combat_global_cooldown      # ← 与实际 atom 名不符。属于臆测
affected_files: []              # ← 留空。会议记录未明确说明
implementation:
  target_build: 2026-05-09
  impl_owner: teammate_b
  qa_owner: teammate_c
related_decisions:
  - supersedes: D2025_Q4_XXX    # 去年的 0.3秒决定。不知道 seq
  - relates_to: 治疗技能例外决定(ID 未定)  # ← 用叙述而非链接填写
---

实话说,这份输出只值 60分。做对的和做错的混在一起。

人工验证·驳回

笔者没有原样接受这张卡。把 ①②③ 写成驳回理由重新发起了请求。驳回的核心是 atom 名的臆测。atom ID 哪怕错一个字符,后续的 grep·反向引用就会全部落空。追踪系统的准确度,系于 atom ID 的准确度。

第2次提示词(重新请求,全文)

改三处。scope 因为治疗技能属于例外,别用 all_skills,
改成 all_active_skills,治疗例外写进 content。affected_atoms
之前叫你别臆测你却臆测了——不知道 atom 实名就留成 [待确认],只加注释
说明要确认什么,索引我自己来看。related_decisions 把治疗例外
用 relates_to 指向 D2026_Q2_018,supersedes 指向 D2025_Q4_034。affected_files
也只作为 [待确认] 候选。

Claude 第2次输出(未加工)

affected_atoms:
  # [待确认] 以下为候选。需在索引中确定准确的 atom ID。
  - combat_global_cooldown_constant   # 全局冷却常量值
  - combat_skill_cooldown_rule        # 各技能的冷却规则(含例外处理)
affected_files:
  # [待确认] 会议记录中没有。按 type=system_change 的通常影响候选:
  - CombatBalance.xlsx        # 冷却数值表
  - CombatFormula_v3.md       # 战斗公式文档
  - UI/skill_cooldown_indicator   # 冷却 UI 指示器
related_decisions:
  - supersedes: D2025_Q4_034
  - relates_to: D2026_Q2_018
scope:
  - combat_system
  - all_active_skills
content: |
  对所有战斗主动技能应用 0.5秒全局冷却。
  治疗技能除外(单独决定 D2026_Q2_018)。

第2次变得诚实了。它没有臆测 atom 并下定论,而是打上 [待确认] 标记并附上依据注释。笔者打开 atom 索引,确认 combat_global_cooldown_constantcombat_skill_cooldown_rule 这两个名字确实存在,便摘掉了标记。affected_files 的三个候选也在与索引比对后予以确定。本章开头刊出的最终卡片,就是这一成果。

这份实操记录的教训只有一个:LLM 作为决策卡的初稿撰写者很强大,但 atom ID 与决策 ID 的最终确定,必须由人与索引比对。 AI 探索候选,人来采纳。两者角色一旦混淆,错误的 atom 名就会污染整张图谱。

18.1.6 用 grep 反向追溯影响

当卡片与图谱以 atom ID 相互绑定时,"这个决策会影响到哪里"用一行 grep 就能给出答案。把决策 D2026_Q2_017 的核心 atom combat_global_cooldown_constant,在稿件·数据表·决策卡全体中做反向引用扫描。

rg "combat_global_cooldown_constant" --type md --type yaml -l
# → D2026_Q2_017.yaml          (决策卡本身)
#   D2026_Q2_025.yaml          (PvP 变体 —— 再次引用了该常量)
#   CombatFormula_v3.md        (公式文档)
#   95_BattleTF_2026-04-18.md  (会议记录原件)

这个结果就是一张"改动这个常量,就会牵动四处"的影响地图。PvP 变体卡片引用了同一个常量这一事实,靠人的记忆很容易漏掉,而 grep 不会漏。这之所以可能,正是因为 atom ID 准确——如果用第1次输出的 combat_global_cooldown 去 grep,这四行里一个也不会命中。等级分类(§18.2)、全周期工作流(§18.3)、grep 工作流的精细化(§18.4),全都立于这份 atom ID 的准确性之上。

18.1.7 追踪系统带来的差异

下面比较笔者的项目A引入追踪系统前后的情况。以下数字是笔者的推定(未验证),建议按方向和比例来读,而非绝对值。

条目 无系统 系统运行 方向
"以前是否决定过?"的重议 每季度 8\~12件 每季度 0\~2件 大幅减少
掌握决策影响范围 1\~2天 用 grep 数分钟 大幅缩短
追踪决策演进历史 依赖资深成员记忆 图谱自动 消除对人的依赖
新成员学习决策历史 1\~2个月 1\~2周 效果最大

效果最大的是最后一行。新成员不再拉着资深同事追问"这游戏为什么变成现在这个样子",而是顺着决策图谱自己读下去。决策追踪也就成了公司的决策学习资产。只是系统刚引入的那个季度,写卡片的负担确实存在。先从必填的 5个字段落实、再逐步扩大,是稳妥的路径。

18.1.8 从保守到进步 —— 自动化立于 atom 分解之上

到目前为止的运营都是保守式应用。人在会议上做决定、写卡片、识别受影响的 atom,自动化只负责索引·检索·grep·图谱可视化。人负责核心判断,自动化负责保管与检索。

下一步就是上面实操记录所展示的方向。以会议记录的自然语言为输入,LLM 填写决策卡 12个字段的初稿,顺着图谱探索受影响的 atom 候选,连等级也一并推荐。留在人手里的工作,收窄为"检查 AI 填好的卡片与 atom 名是否与索引相符"和"最终审批"两件。从零开始填满 12个字段的负担,和在索引中核对 LLM 初稿 atom 名的负担,性质不同。

要让这种进步式应用站稳脚跟,需要三根骨架。其一,所有决策都以 atom 登记、用 wikilink 相连的决策图谱。一整段会议记录成不了自动化的输入——它必须被分解到决策的粒度。其二,在图谱之上计算受影响领域数·回退成本·用户影响范围,进而推荐等级的影响等级自动机(§18.2)。其三,以 atom ID 和 wikilink 精确运作的grep·LLM 影响追踪(§18.4)。

这里,贯穿全书的信息再一次浮现。把决策分解为 atom·图谱·等级,表面是"检索与反向引用的便利",本质却在于:面对一整段未经分解的会议记录,自动影响分析连什么才是决策的单位都无从知晓。"分解以统一协作语言为表面目的、以程序化自动化的前提为本质目的"这一普遍命题(§6.6),在决策领域体现为决策图谱·atom·等级。这与第5部分的世界 BT(BehaviorTree,行为树)·任务云,以及第8部分的进步式平衡,是同一根骨架。2010年代理论上就已可行,但把会议记录自动分解为决策 atom 这件事一直受阻;2023年之后 LLM 承担起这一分解的初稿工作,原本只停留在纸面上的愿景,相当一部分进入了可实现的领域。

本章要点

游戏之外的应用。 决策卡并不限于游戏,它是让任何组织在半年后仍能回答"当时为什么那样定"的装置。市场部为了"上个季度决定砍掉这个渠道,到底是为什么来着"在会议记录里翻不到一行、白白耗掉 30分钟这样的事,只要有一张带 decision_idownerrationale 三个字段的卡片就会消失。比如人事部在决定"远程办公统一为每周2天"这类政策时,若在那张卡上写清发起者·审批者·依据(生产力数据·员工问卷)和被替代的旧政策 ID,一年后政策复审的场合,过去的判断依据便原样鲜活地留存着。

动手试试

网页聊天机器人最简路径(无需终端) —— 本章的核心不在于决策卡目录或 grep,而在于"给决策固化永久地址(decision_id)·责任人(owner)·依据(rationale),并在做出新决策之前先查一遍过去的决策"这一构想。这个构想不用 CLI·atom 索引,仅凭网页聊天机器人(ChatGPT 或 Claude 网页版)就能重现。下面三步是主线。 1. 把一个决策写成一行。用一张叫 decisions.md 的普通文档就够了。既不需要 YAML,也不需要脚本。 - [D17] 全局冷却统一为 0.5秒 (owner: 我, 依据: 连招可读性, 替代: D08) 2. 要把会议记录变成卡片时,在网页聊天机器人里贴入下面这段。它把第1次提示词的 4条约束原样搬了过来。 把下面会议记录里的决策转成表格。栏目为 decision_id / title / owner / rationale / 被替代的旧决策。 无法确定 owner 就填 [MISSING],不知道 atom·文件名就留 [待确认], 不要臆测。 // (意图:decision_id 为 D{年份}_{序号},不知道序号就填 XXX) [会议记录正文] 3. 在做出新决策之前,先用文档内查找(Ctrl+F)搜一遍 decisions.md —— "以前是否决定过"这一个问题就靠它解决。这就是 grep 反向追溯的手工版。atom 索引·YAML 卡片·rg 工作流,等到决策积累数百件、用单个文档搜索开始吃力时再引入即可。

setup(基础设施版 —— 上面的最简路径上手之后)—— 请创建决策卡目录与索引文件。

decisions/
  D2026_Q2_017.yaml
  _index.json        # by_status / by_scope / by_quarter 汇总

prompt —— 把会议记录中的决策议题抛给 LLM 时,务必包含上面第1次提示词的 4条约束。尤其要写明"不要臆测 atom 名,而是留成 [待确认]"。

verify —— 把产出卡片的 affected_atoms 条目与 atom 索引比对,确认实名后移除标记。然后用核心 atom 执行 rg "<atom_id>" -l,交叉验证受影响文件是否与卡片的 affected_files 一致。

单人精简版

如果没有团队基础设施、一个人用,就把 YAML 卡片扔掉。把一个决策写成 Markdown 一行。

- [D17] 全局冷却统一为 0.5秒 (owner: 我, 依据: 连招可读性, 替代: D08 的 0.3秒)

把这些一行行堆进 decisions.md 一个文件,做出新决策之前,先用 rg "쿨다운" decisions.md 搜一遍过去的决策。没有卡片、没有图谱、没有工具,但"以前是否决定过"这一个问题就能解决。追踪系统的90%,就从这一行的习惯开始。

18.2 影响传播·等级分类

会议结束后,我正在整理会议记录。上面写着一条决策。"全局冷却统一为 0.5 秒。"这在会上不到 30 秒就达成了一致。所有人点头,便转向了下一个议题。

那一行字吃掉了接下来的两个月。战斗数据中的 277 个技能全部受到影响,UI 的冷却进度条演出被重画,数值表被推翻重做了两次。同一份会议记录里写着的另一条决策"修正新手引导提示文案的错别字",只花了 5 分钟就完成了。

两条决策在会议记录上同样都是一行,字数也差不多。然而一条是 5 分钟,一条是两个月。让这一差异在写下会议记录的那一刻就变得可见——这就是影响等级分类。等级若不可见,两个月的决策就会被埋没在与 5 分钟决策相同的一行里。

本章讨论如何将决策的波及自动分为五个等级,并在决策 atom 图谱上追踪这波及蔓延到何处。所用工具是前一章积累的决策 atom 与 impact 提取,以及 portal_layer_change_impact_check atom。


等级不可见时会发生什么

先来看看没有等级分类的状态是什么样子。当决策全部放在同一行上,两类事故会交替发生。

一是处理不足。像全局冷却这样撼动整个季度的决策,被当作"5 分钟的活儿"未经验证就进入了构建。直到两个月后波及才显现,而那时回退成本已如山堆积。

另一是处理过度。为改一个错别字而召集 TF(专项组),还要获得游戏总监的审批。决策周期暴增,而总监本该用在 T0 决策上的时间,却被吸进了错别字会议里。

两类事故看似截然相反,根子却相同。决策的分量不可见。分量不可见,于是把力气花在轻的上面,把重的放任流走。等级分类就是给决策贴上分量标签的工作,标签一旦贴上,处理方式便自动分流。


18.2.1 影响的 5 个等级 —— 从 T0 到 T4

在笔者所在的 MMORPG 开发商项目A,决策的影响被分为五个等级。越往上越重,处理需要更多的人力与时间。

等级 定义 示例 决策者 周期
T0 游戏愿景·核心系统 移动端优先决策、核心机制变更 游戏总监 + CEO 季度
T1 系统·跨领域 全局冷却统一、新增职业 TF 组长 + 总监 1\~2 周
T2 领域·中等 特定技能数值调整、新增 UI 组件 领域总监 3\~5 天
T3 单次·小型 修改单个 NPC 台词、颜色微调 资深人员 1 人 1\~2 天
T4 即时·热修复 修复 Bug、文本错别字 负责人 小时级

只看表格,它像教科书一样清晰。然而实务的难点不在于背表,而在于判断眼前的一条决策该放进哪一格。"全局冷却统一"是 T1 这一点,不能等会议结束之后才知道,而要在写下会议记录的那一刻就知道。因此下一节的 3 条标准才是关键。


18.2.2 划分等级的 3 条标准

等级不靠直觉来定。评估三条标准,采纳其中最高的等级。

等级判定矩阵 —— 3 标准 × 5 等级

标准 \ 等级 T0 T1 T2 T3 T4

影响领域数 5+ 2~4 1 1 1

回退成本 非常大 中等 非常小

用户影响范围 全部 中等 非常小

三条标准中,影响领域数可以在决策 atom 图谱上机械地数出来。把决策所触及的 atom 归属于哪个领域(战斗·UI·数据·叙事等)的标签汇总起来即可。

问题在剩下的两条。回退成本用户影响范围无法换算成图谱上的数字。"要在两个月后回退这条决策,得花多少代价"是一种自然语言判断。恰恰是这一点,在 2023 年之前一直是影响自动分类的最后一堵墙。影响领域数已经自动化,但两格自然语言判断空着,结果还是要人从头重新评定。随着 LLM 读取决策 atom 的正文、为这两格填写初稿,这堵墙降低了。

这里要如实说明。LLM 填写的是初稿,而非定论。即便 LLM 把回退成本估为"大",领域总监也可以判定"按我们的表结构,这个是中等"。自动分类并不替代人的判断,而是让人不必从空格开始


18.2.3 实操记录(worked transcript)—— 让 LLM 对一条决策分类

这是把前一章制作的一条决策 atom 原样交给 LLM、让它评定等级的真实过程。实操记录即完整保留的真实操作过程记录:不作摘要,原样搬录全过程,连否决与再次请求也一并收录。

输入 —— 决策 atom 原文

# decisions/D2026_Q2_017.md (前一章 18.1 中登记的 atom)
id: D2026_Q2_017
title: 全局冷却统一为 0.5 秒
date: 2026-04-18
proposer: teammate_a
decider: 李旼洙    # Design Director (与 18.1 的 approved_by 相同)
context: 各职业的冷却标准各不相同,每次新增职业都会累积
         数值重算成本。提议以 0.5 秒全局标准统一。
affected_atoms: [combat_cooldown_rule, skill_table, ui_cooldown_gauge]
reversible: ?
status: proposed

提示词全文

请把下面这条决策 atom 的影响等级评定为 T0~T4 中的一个。
分别看影响领域数 / 回退成本 / 用户影响范围这三条标准,
取其中最高的等级。没把握的格子就写"不确定",
并说明需要人来看。
// (意图:T0=领域 5+·成本非常大·全部,T1=2~4·大·大,T2=1·中等,T3=1·小,T4=1·非常小)

[附上 atom 原文]

Claude 原始输出(未经编辑)

3 条标准评估:

1. 影响领域数
   看 affected_atoms:
   - combat_cooldown_rule → 战斗领域
   - skill_table → 数据领域
   - ui_cooldown_gauge → UI 领域
   明确写出的领域是 3 个。
   不过看 context 中"每次新增职业都要重算数值"这句
   话,数值领域也很可能受到间接影响。
   直接 3 + 间接 1 = 实际按 2~4 个范围来看。→ T1 信号

2. 回退成本
   把全局冷却统一之后若要回退,就得把 skill_table 中
   所有技能的冷却时间按各职业标准重新分叉。
   由于不知道 skill_table 的行数,确切成本尚不确定,
   但"统一 → 恢复分叉"通常成本很大。→ 大(T1 信号)
   ※ 这一格若不知道 skill_table 的规模就无法确定。标为不确定。

3. 用户影响范围
   冷却时间适用于所有战斗情形,因此几乎所有用户都
   能体感到。→ 大(T1 信号)

采纳等级:T1
依据:3 条标准都指向 T1。不是 T0 ——
因为还谈不上"核心机制变更",而是对既有机制的参数
统一。

需要人工确认:
- 回退成本格:需确认 skill_table 的实际行数
- reversible 槽以"?"留空 → 本次分类以将 reversible
  填为"困难"为前提。

人工验证与否决

这里没有原样接受。我对输出的第 1 项动了手。LLM 把数值领域看作"间接影响",但在项目A的实际结构中,冷却统一会直接推翻数值表——因为新增职业的数值测算公式本身就把冷却值作为输入。所以数值不是间接,而是直接的第 4 个领域。影响领域数为 4 个。即便如此,采纳等级 T1 并未改变。

还有一点,回退成本格里的"不确定"标记是准确的。skill_table 实际上有 277 行技能,若回退统一,就得把全部 277 行的冷却时间重新分叉。成本更接近"非常大"而不是"大"。不过,一格往上升并不会让采纳等级变成 T0 —— 因为影响领域数与用户影响都在 T1 范围,最高等级仍然是 T1。

再次请求

更正。请在 affected_atoms 里加上 balance_sheet(数值公式把冷却值
作为输入,是直接影响)。那么影响领域就是 4 个,skill_table 是 277 行。
把这些反映进去,填好 reversible 并确定最终等级。
已反映。

影响领域:战斗·数据·UI·数值 = 4 个
回退成本:非常大(skill_table 全部 277 行重新分叉)
用户影响:大

reversible:困难(实质上接近不可逆 —— 277 行重新分叉需要
1~2 次构建外加一轮数值重新验证)

最终等级:T1
注意:仅看回退成本一格是 T0 信号,但
影响领域 4 个·用户影响"大"处于 T1 范围。
3 条标准中有 2 条指向 T1,所以 T1 是对的。
不过建议把这条决策标注为"T1 中偏重的一类" ——
因为回退成本达到了 T0 级。

最后这句比人还谨慎。等级虽是 T1,但回退成本达 T0 级,于是提议将其标注为"T1 中偏重的一类"——这是最初提示词里没有的判断。我采纳了。我给这条决策额外贴上了 weight: heavy 标签,并提高了事后追踪的优先级。

这份实操记录展示的核心只有一个。LLM 生成分类的初稿与理由,人用领域事实(数值是直接影响、277 行)来校正。只靠其中之一都不行。只靠人,就得从空格开始,慢;只靠 LLM,就会在不知道 277 行的情况下写下"大"。


18.2.4 自动追踪影响传播的代码

等级确定之后,接下来是"波及到何处"。把前一章的 impact 提取 —— 入边、本体中的 affects 关系、wikilink 反向引用 —— 应用到决策 atom 上。

# impact_propagation.py —— 追踪决策 atom 的传播范围

def trace_impact(decision):
    # 一级:决策直接触及的 atom·文件
    direct = decision.affected_atoms + decision.affected_files

    # 二级:以 wikilink 反向引用一级 atom 的 atom(impact 入边)
    secondary = []
    for atom in direct:
        secondary.extend(find_inbound_refs(atom))   # [[atom]] 反向引用
        secondary.extend(find_affects_edges(atom))   # 本体 affects

    secondary = dedup(secondary) - set(direct)

    return {
        "direct": direct,
        "secondary": secondary,
        "affected_fields": determine_fields(direct + secondary),
        "estimated_hours": estimate_hours(direct, secondary),
    }

核心是 find_inbound_refs —— 一个在 atom 图谱中收集用 [[...]] 指向该 atom 的入向箭头的函数。决策自身触及什么(出向箭头)写在 atom 里,但谁依赖该 atom(入向箭头)必须反向扫描整个图谱才能看到。两个月的波及,几乎总是藏在这入边一侧。

在此如实记下对 D2026_Q2_017 跑这一追踪的结果。direct 是上面确定的 4 个 atom。secondary 是反向引用 skill_table 的那些 atom —— 技能说明文本、技能图标映射、各职业技能树等 —— 一连串地被牵了出来。数字因时点而异,故不下定论。追踪抓住的事实是"secondary 为 direct 的数十倍"这一方向,准确的 atom 数会随图谱状态而变。仅凭方向就够了 —— secondary 比 direct 大一个数量级,那就是 T1 信号,意味着它是事后追踪的对象。


18.2.5 等级分类嵌在何处 —— mermaid

分类不是独立的步骤,而是作为关卡固定在决策流程的正中央。决策候选一经登记,自动分析就推荐等级,只有在人评审、调整之后,才进入决策会议。

flowchart TD A[决策候选登记<br/>会议记录 → 决策 atom 初稿] --> B[自动影响分析<br/>impact 提取] B --> C{3 条标准自动评估} C -->|影响领域数| D1[在图谱上计算] C -->|回退成本| D2[LLM 初稿 → 标为不确定] C -->|用户影响| D3[LLM 初稿 → 标为不确定] D1 --> E[采纳最高等级<br/>推荐 T0~T4] D2 --> E D3 --> E E --> F{人工评审} F -->|领域事实校正| G[确定等级 + weight 标签] F -->|退回·重新分类| C G --> H{按等级分流} H -->|T0| T0[总监+CEO / 季度周期] H -->|T1| T1[TF / 1~2 周] H -->|T2| T2[领域总监 / 3~5 天] H -->|T3| T3[资深 1 人 / 1~2 天] H -->|T4| T4[负责人即时处理] T0 --> I[反映进构建 —— 不可逆] T1 --> I T2 --> I T3 --> I T4 --> I I --> J[事后追踪<br/>估计 vs 实际 → 学习到下次估计] J -.可逆吸收.-> B classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class B,C,D1,E,I code; class D2,D3 ai; class F,G,T0,T1,T2,T3,T4 human; class A data;

在这一流程中,不可逆的步骤只有一个,就是反映进构建(I)。它之前的全都是可逆的 —— 即便等级推荐错了,人退回即可,weight 标签也可以撕掉。只有在进入构建、传播到其他文档之后才变得不可逆。因此关卡(F 的人工评审)设在构建之前。在跨过不可逆这条线之前,人先拦一次。

最后那条虚线 —— 事后追踪(J)返回下一次决策的自动分析(B)的箭头 —— 让这套系统成为一个学习循环。不可逆步骤中产出的实测数据(例如:QA 时间比估计更长)被吸收进下一次决策的可逆步骤。


18.2.6 事后追踪 —— 把估计与实际的落差转为学习

决策进入构建后 1 周\~1 个月,把估计与实际对照。这是 D2026_Q2_017 的事后追踪表单。

决策 D2026_Q2_017 事后追踪  (表单示例 · 数字为虚构输入)
─────────────────────────────────
工作时间 (估计 → 实际)
  代码:    16h → 22h  (+38%)
  数据:     8h →  6h  (-25%)
  UI:       4h →  4h  (=)
  QA:       8h → 12h  (+50%)
  total:   36h → 44h  (+22%)

影响 atom (估计 → 实际)
  direct:    4 →  4   (准确)
  secondary: 估计数十 → 实际数十  (方向一致,准确数值不公开)

事故发生: 0 起
误差模式: QA 每次都超出估计 (本次 +50%)
下次决策应用: 对 QA 估计默认加 +20% 余量

上面这个块是展示事后追踪长什么样的表单示例。时间·百分比数值并非实际项目数据,而是填入表单的虚构输入,在你自己的项目里换成你自己的数字填写即可 —— 正如本书的承诺,我们展示结构,数字由你自己测量。与表单无关、真正为真的只有一件事。"QA 每次都超出估计"这一误差的方向,以及把这一方向反馈进下一次决策的流程。于是就有了从一开始就给下次估计加上 QA 余量(例如 +20%)的处方。

事后追踪的价值不在于猜中准确的数字,而在于把误差的方向反馈回去。估计越准确,等级分类的可信度就越高,可信度越高,委派就越有可能。


18.2.7 各等级的事故模式与处方

各等级反复出现的事故各不相同,处方也不同。

等级 事故模式 处方
T0 愿景模糊 → 整个季度都混乱 强制在决策文中写明一行愿景
T1 领域间冲突 → 进度延误 让全部受影响领域的代表出席 TF
T2 遗漏相邻系统的影响 → 后续决策暴增 必须做 secondary 追踪
T3 小决策累积 → 一致性受损 在季度复盘中成批检查 T3
T4 验证不足 → 二次热修复 热修复也至少经 1 人评审

这张表里的处方全都由前面各节给出的工具来执行。T2 的"必须做 secondary 追踪"就是 §18.2.4 的 find_inbound_refs,T1 的"全部受影响领域代表出席"则由 §18.2.4 抓出的 affected_fields 来确定谁该到场。

最昂贵的事故不在表中任何一处。那就是把等级本身弄错。T0 若由资深人员一人决定,愿景就会受损;T4 若由总监亲自处理,就会产生瓶颈。等级一旦弄错,其下所有处方都会在错误的位置运作。因此 §18.2.3 的人工评审关卡不是单纯的形式。


18.2.8 度量 —— 等级运营的效果

比较项目A引入等级分类前后的情况。下面数值中的绝对值是加工过的示例,而方向(不等号)是实际趋势

对比项 无等级 等级运营
决策周期 全部统一为 1\~2 周 从 T0 季度到 T4 小时级分化
处理错误的决策 每季度多起 每季度少数
总监的每周决策负担 大(所有决策都汇到总监) 小(T2\~T4 委派出去)
热修复周期 1\~2 天 4\~24 小时
季度复盘的决策分析 难以归类 按等级统计汇总

把表写成方向而非确定数字,原因由第 3 项就能说明。总监时间的回收是等级分类最大的效果。没有等级时,从错别字到愿景,所有决策都涌向总监一个人。有了等级,T2 及以下就分流给领域总监·资深人员·负责人,总监则专注于 T0·T1。委派成为可能,也就意味着总监夺回了真正用于重大决策的时间。


18.2.9 本章在进取式应用骨架中的位置

前一章制作了决策 atom 图谱,本章在这张图谱之上叠加了等级分类自动机。两者不是各自为政的工具,而是同一骨架中相连的位置。

flowchart LR A["① 决策 atom 图谱<br/>(前一章 18.1)<br/>会议记录 → 12 槽 atom"] --> B["② 影响等级分类自动机<br/>(本章 18.2)<br/>图谱 → T0~T4 + 估计"] B --> C["③ wikilink 影响追踪<br/>(18.4 grep 工作流)<br/>atom ID → 传播范围"] C --> D["人工评审·批准<br/>不可逆关卡"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class C code; class B ai; class D human; class A data;

三个要素是串联的。图谱制造输入(①),分类器评定分量(②),影响追踪铺开传播范围(③)。本章处在中间的位置。

三个要素都是在 LLM 发展之后才进入可实现的领域。最晚被攻破的墙是②的两格自然语言评估 —— 回退成本与用户影响范围(§18.2.2 的"最后一堵墙")—— LLM 填补其初稿之后,①→②→③才终于开始串联运转。

可逆·不可逆的排布也与这一骨架相扣。正如 §18.2.5 的流程图,不可逆的线只有反映进构建这一条,反映进构建本身无法回退,但其实测结果会作为让下一次决策更准确的可逆学习返回来。


18.2.10 常见的失败

模式 处方
用同一周期处理所有决策 按等级分化周期
没有等级分类,各自自行判断 3 条标准分类关卡
无视等级(T0 由资深人员决定) 强制执行决策者表
省略事前的影响度评估 把自动分析设为决策会议前的关卡
把自然语言格按 LLM 输出原样定稿 由人用领域事实校正
不做事后追踪就结束决策 1 周\~1 个月做估计 vs 实际的比较
不把估计误差反映到下次决策 把误差方向应用到下次估计的余量

游戏之外的应用。 影响等级是一个把"一行字的请求究竟是 5 分钟还是两个月"提前显现出来的标签,因此在任何决策纷至沓来的职场都适用。公司维基上一行文案的修改,与"全部门休假政策变更",若同样作为"一件议题"进来、走同一条审批线,轻的事就会被过度处理,重的事则未经验证便流走。举例来说,在接到运营团队的工作请求时,只要按影响部门数·回退成本·客户影响范围这三条标准贴上 T0\~T4,负责人可即时处理的事与需要主管审批的事就会自动分开,管理者的时间也会被回收到真正重大的决策上。

动手试试 —— 给一条决策分类等级

setup. 请准备一条前一章制作的决策 atom。affected_atoms 槽必须已填好。若为空,就无法计算影响领域数。

prompt. 请原样使用 §18.2.3 的提示词全文。别漏掉关键的三行 ——(1) 分别评估 3 条标准,(2) 采纳最高的等级,(3) 没把握的格子标为"不确定"并请求人工确认。少了第三行,LLM 就会连不知道的也一并断定。

verify. 收到 LLM 输出后,请亲自确认两点。第一,即便 LLM 数过了影响领域数,也要在 atom 图谱上亲自重数一遍 —— 它可能把间接影响当成直接,或有所遗漏(实操记录中的数值案例)。第二,标了"不确定"的格子用领域事实来填(如 skill_table 277 行这样的实际规模)。完成这两项确认之后,才确定等级并交给构建关卡。

单人精简版

如果是既没有团队也没有 TF、独自开发的项目,5 个等级就太多了。请缩减为 3 个等级。

工具也请从零行代码开始。记录决策时,只在前面加上 [重] [中] [即时] 标签。仅凭这一点,你就会在贴了"重"标签的决策前多停一次 —— 等级分类的本质说到底就是在重大决策前停下的习惯,而自动化不过是让这份停顿在团队规模上也能运作的装置。


本章要点

18.3 决策前后影响追踪工作流

上线三周后,那是一场复盘会议,用来回溯 PvP 平衡崩坏的原因。在白板上一路倒推,最终到达的起点,是一个月前的一条决策。"将全局冷却从 0.3 上调到 0.5。"它是因为收到"看不到连招"的反馈、在两小时内就达成一致的、合理的提案。然而那个改动把坦克职业的生存率比预期又拉高了 14%,正是它让 PvP 崩溃了。在做决策的现场,没有任何人指出那条决策会波及到坦克。决策本身并没有错。事故的原因在于,没能在做出决策之前看清这条决策会波及到哪里。

影响追踪必须发生在两处。在按下决策之前(pre)看清它会波及到哪里,在决策落地之后(post)确认它是否真的只波及到了那里。本章把这两次追踪合并为一个工作流。


18.3.1 事前追踪与事后追踪是把同一张图读两遍

决策影响分析的核心出乎意料地简单。把一个决策 atom 看作一个节点,读取流入该节点的边流出该节点的边。事前追踪问的是"改动这条决策会影响到哪里"(出向 + 反向引用),事后追踪问的是"那些影响是否真的按意图发生了"(把同一批边与实测值比对)。

在作者的项目A中,决策以 atom 的形式留存在 decisions/ 文件夹里。目前已累积 26 个,每个 atom 都以 frontmatter 携带日期、当事人、依据、影响范围。抽取影响范围的工具是 impact,而把这套抽取规则按决策单元强制执行的 atom 是 portal_layer_change_impact_check。这三者就是事前、事后追踪的实际资产。

决策 atom D2026_Q2_017

依据:用户反馈 上级决策 D_011 反向引用:GDD 链接

CombatFormula.md CombatBalance 表 UI 连招显示

入向(为何做此决策) 出向(波及何处)

事前追踪读取右侧(出向),来预测"会波及到这里";事后追踪则取来右侧各节点的实测值,与预测比对。这是把同一张图以不同目的读两遍。


18.3.2 事前追踪 —— 在按下决策之前读图

从这里开始是真实的会话。不做概括,而是原样刊出提示词全文、Claude 的原始输出,以及人如何拒绝它并重新提出要求。

impact 是一个接收决策 atom 的 ID、抽取影响图的小型管线。它把入向边(这条决策的依据)、出向边(这条决策将修改的文件)、本体中的 affects 关系,以及维基链接反向引用(通过 [[D2026_Q2_017]] 指向这条决策的其他文档)合并起来,生成影响候选清单。

提示词(全文)

评估 decisions/D2026_Q2_017.md 在 proposal 阶段的事前影响度。
- 这条决策将修改的文件(出向)
- 通过 [[D2026_Q2_017]] 反向引用这条决策的文档(入向 wikilink)
- 通过本体 affects 关系连接的系统
把这三者合并给出影响候选,并为每个候选区分它是需要我确认的项(T1),
还是可以自动确定的项。不要用猜测来填补,若没有依据文件
就标注为"无依据"。

Claude 的原始输出

已读取 decisions/D2026_Q2_017.md。以下是从 frontmatter 抽取的影响图。

出向(这条决策直接修改) - data/CombatFormula.md —— global_cooldown 字段(atom 中已明示) - sheets/CombatBalance —— 回复技能例外行(atom 中已明示) - ui/ComboIndicator —— 依据 atom 的 affected_teams: [ui] 推定。但具体文件不在 atom 中 → 无依据(推定)

入向 wikilink(反向引用这条决策) - 用 grep[[D2026_Q2_017]] 检索,但当前索引中反向引用为 0 条。这是一条新决策,看来还没有任何人引用它。

本体 affects 关系 - 本体中存在 CombatFormula → affects → TankSurvival 关系。 - 因此这条决策会对 TankSurvival 指标产生间接影响。推定影响 +12%。

此处人叫停了。问题出在最后一行"推定影响 +12%"。

人的验证·拒绝

拒绝。"推定影响 +12%"这个数字是从哪来的?atom 里、本体里都没有这个数值吧。如果是你编造的就删掉。只需报告 affects 关系存在这一事实,影响的大小要跑模拟才能得出。还有,ui 推定项你既然归类为"无依据",为什么又把它以 T1 放进了候选清单?

Claude 的再次响应

指正得对。我更正两处。

  1. 撤回"+12%"。 atom 与本体中都没有这个数值的依据。这是我生成的值。只应报告 TankSurvival 存在影响这一关系,大小要用模拟(Layer 2)的结果来填充。当前值为 未测量
  2. ui 项重新分类。 由于找不到具体文件,在候选清单中不应作为 T1(需确认),而应降级为 依据不足 —— 需由人指定文件。更正后的表如下。
影响候选 边类型 依据 分类
CombatFormula.md global_cooldown 出向 atom 明示 自动确定
CombatBalance 回复例外行 出向 atom 明示 自动确定
TankSurvival 指标 affects 本体关系 有影响 / 大小未测量
ui 连招显示(文件未知) 推定 无依据 需由人指定文件
入向反向引用 wikilink grep 0 条 不适用

这张表就是事前追踪的产出。核心是 TankSurvival 那一行。开篇中让 PvP 崩溃的那个影响,在事前追踪里已经以"有影响 / 大小未测量"浮现了出来。如果在会议现场看到这一行,就会变成"那就用模拟测出大小再做决定"。若放任 AI 编造出 +12%,反而更危险。因为虚假的精确度会让人跳过验证。


18.3.3 决策与不可逆阶段

事前追踪结束后就在会议上做决策。决策以 atom 形式确定的那一刻,两个不可逆阶段就开始了。

flowchart TD A["事前追踪<br/>(impact 图)"] --> B{决策会议} B -->|拒绝| Z["废弃 proposal<br/>(记录理由·可逆)"] B -->|批准| C["决策 atom 确定<br/>D2026_Q2_017"] C --> D["⚠ 不可逆 1:构建落地<br/>修改 CombatFormula·表格"] C --> E["⚠ 不可逆 2:传播至其他文档<br/>更新 GDD·反向引用"] D --> F["启动事后追踪"] E --> F F --> G{预测 = 实测?} G -->|相符| H["关闭决策卡"] G -->|偏离| I["副作用 atom<br/>后续决策候选"] I --> B classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class D code; class B human; class A,C,E,I data; class H pass; class Z fail;

不可逆的原因很简单。落地到构建的数值,用户已经玩过了;传播到其他文档的内容,团队成员已经据此开始了下一步工作。因此在这两个阶段的紧前,portal_layer_change_impact_check atom 会作为关卡拦在前面。这个 atom 的规则可以用一句话概括:"若决策包含跨 Layer 的变更(数据 → 公式 → UI),则在落地前必须由人对影响候选清单批准一次。"只有自动确定项时放行,只要出现一个"需由人指定文件"就拦截。18.3.2 中的 ui 项就成了拦截理由。


18.3.4 事后追踪 —— 给同一张图附上实测

在构建落地后 1 周\~1 个月运行事后追踪。这是给事前追踪生成的影响候选表的每一行附上实测值的工作。

提示词(全文)

D2026_Q2_017 事后追踪。给事前追踪表(18.3.2)的每个候选附上实测。
实测来源:
- CombatBalance 表 git diff(用于确认落地)
- 模拟 1000 次日志 sim_2026q2_017.json
- 上线 1 周 KPI export kpi_w1.csv
计算意图值与实测值的差异,只单独汇总偏差超出 10% 范围的行。
缺失的数据标为"未收集"。

Claude 的原始输出

已将实测与事前追踪表结合。

候选 意图 实测 差异 判定
global_cooldown 0.3→0.5 落地 git diff 确认 已落地
回复例外行 落地 表格确认 已落地
战斗时间 +15% +14.2% -0.8%p 范围内
TankSurvival (未测量) +44% 无基准 无法判定
5:5 PvP 胜率 +15%p +29%p +14%p 偏离
ui 连招可读性 改善 用户正面 65% 相符

超出误差范围(±10%)的行:PvP 胜率 +29%p(意图 +15%p)TankSurvival 因事前未测量大小、没有意图值,故无法判定。

把这两行联系起来看,TankSurvival +44% 看来是 PvP 胜率偏离的原因。事前追踪中把 TankSurvival 留作"大小未测量",预示了这次偏离。

人的验证

准确。没有用模拟测量 TankSurvival 就是事故的根源。做一个后续决策 atom。方案是通过坦克职业组的 dmg 修正,把 PvP 胜率拉回意图范围。

Claude 的再次响应

已生成 decisions/D2026_Q2_025.md 草案。在入向挂上 [[D2026_Q2_017]],用反向引用明示这次决策是对 17 号的副作用修正。现在再用 impact 查询 17 号 atom,就会捕获到 1 条入向反向引用(25 号)—— 这正是 18.3.2 中为 0 条的那个位置。

至此图闭合了。在事前追踪中还是"有影响 / 大小未测量"的节点,在事后追踪中被确认为偏离,而后续决策以指向该节点的反向引用进来了。决策的整个周期在同一张图上转了一圈。


18.3.5 运行追踪的实际命令 —— grep 工作流

impact 的入向反向引用抽取不是什么花哨的工具,而是一行 grep。它在全部文档中查找指向决策 atom 的维基链接。

# 反向引用 D2026_Q2_017 的所有文档(入向 wikilink)
grep -rln "\[\[D2026_Q2_017\]\]" decisions/ manuscript/ gdd/

# 决策 atom 的出向 —— 抽取 frontmatter 中的 affected_files
grep -A20 "affected_files:" decisions/D2026_Q2_017.md

# 事后追踪:仅意图对比中偏离的行(判定列)
grep -E "이탈|판정 불가" tracking/D2026_Q2_017_post.md

三行命令就能让事前、事后追踪的骨架运转起来。LLM 的位置是读取并解读这些结果,而不是替代检索本身。grep 给出事实(哪些文件指向这条决策),LLM 把这些事实编织成影响候选表,人对影响的大小与判定负责。正是这种分离,才让 §18.3.2 中"不要编造 +12%"得以成立。


18.3.6 测量 —— 当把前后追踪合并时

这是作者的项目A中,把决策周期标准化前后比较得到的数值。绝对时间数值取决于团队规模(中等规模,10\~50 人),属作者推定(未验证);而比率与方向是在实际运营中观察到的。

对比项 前后追踪分离 前后追踪合并
实际运行了事后追踪的决策比例 约 30% 90% 以上
事前浮现却在事后酿成事故的影响 常见 几乎没有(事前设关卡)
副作用 → 后续决策的连接率 低(口头传达) 通过反向引用自动候选化
决策图的入向反向引用完整性 稀疏零散 闭合回路

核心只有一点。当事前追踪与事后追踪共享同一张候选表时,事前留作"大小未测量"的那个缺口,会在事后恰好在同一位置被确认。若二者分离,事前所见与事后所测是不同的格式,无法比对,于是追踪率停留在 30%。不过若一开始就把反向引用完整性定为 100% 的目标,只会徒增运营负担。现实的做法是,先养成在决策 atom 中写 affected_files 的习惯,再把反向引用 grep 嵌入复盘周期,逐步扩大。


18.3.7 常见的失败

模式 处方
事前看到了影响却没测大小就做决策 "大小未测量"的行在模拟前暂缓决策
LLM 编造影响数值 没有依据文件就标"无依据",大小只用模拟得出
事后追踪与事前表格式不同 只在同一张候选表上追加实测列
副作用以口头方式传递 强制后续决策 atom + 反向引用 wikilink
跨 Layer 变更未经关卡就落地 强制通过 portal_layer_change_impact_check

本章要点


游戏之外的应用。 在按下决策之前看清"会波及到哪里"(事前),在落地之后确认"是否真的只波及到了那里"(事后),这种读两遍的做法,不只是游戏,而是一切变更管理的基本动作。当公司调整价格政策时,若事前把受影响的部门(销售·客服·结算)以候选表浮现出来,并留作"大小在模拟前未测量",就能预先防止上线后"为什么结算部门不知道这件事"的事故。例如,在引入新的会员等级之前,若在事前表里把客服咨询量·流失率这类事后指标的格子留空,一个月后就能把实测填进那些格子,在同一张表里直接比对意图与实际的差异。

动手试试

setup —— 创建决策文件夹与追踪文件夹。

mkdir decisions tracking
# 在 1 个决策 atom 中用 frontmatter 填写 affected_files、affected_teams

prompt —— 事前追踪后,把事后追踪接到同一张表上。

评估 decisions/<ID>.md 的事前影响度:把出向(将修改的文件)·
入向 wikilink·本体 affects 合并成影响候选表,
无依据的项标为"无依据",大小标为"未测量"。不要编造数值。

(构建落地后)
在同一张候选表上只追加实测列,只汇总相对意图偏差超出 10% 的行。
偏离的行做成后续决策 atom 草案,并挂上 [[<ID>]] 反向引用。

verify —— 用 grep 确认图是否闭合。

grep -rln "\[\[<ID>\]\]" decisions/   # 捕获到后续决策的反向引用即闭环
grep -E "이탈|미측정" tracking/<ID>_post.md   # 确认剩余的缺口

单人精简版

如果你是独自作业的个人游戏开发者,会议·负责人·期限都可以全部去掉。在 decisions/ 的 Markdown 里写下一行决策时,只填两格即可:affected_files:(这条决策会触及的文件)与 expected:(意图的变化)。构建之后打开那些文件,用眼睛看是否如意图所愿,若有偏差就在同一文件里加上一行 actual:。工具只需 grep -rln "[[决策ID]]" 一个就够了。事前一格,事后一格 —— 这就是前后追踪的最小形态。

18.4 文档影响面 grep 工作流 —— 用 impact 提取影响范围

周一上午 10 点。负责战斗的团队成员 A 在团队即时通讯工具里丢下一句话:"全局冷却从 0.5 秒下调到 0.4 秒可以吗?"这是改一个数字的事。表面上是。我读到这句话,手停住了。这个数字被录入了几份文档、以这个常量为前提搭建的技能平衡 atom 有几个、改动它会让哪张表格的公式失效——我脑子里浮现不出来。若自以为浮现出来了,那就是事故。每个季度都会爆出 8 到 12 起的"没看到那份文档"式遗漏,其真身正是这种错觉。

所以我决定不去背答案。而是敲一行命令。

impact combat_global_cooldown_constant

本章原原本本地看这一行命令吐出了什么。它要展示的是:提取影响范围不是抽象的说法,而是用 grep 把入站边、本体 affects、wikilink 反向引用这三条路径搜罗到一起的具体动作。


18.4.1 影响范围从三条路径进来

"改动这个 atom 会影响到什么"这个问题,其实是三个问题。混在一起答案就模糊,拆开来每一个都能落成一行 grep。

第一,入站边(inbound edge)——谁指向我。atom A 引用 atom B,就是 A→B 方向的边。改动 B 时危险的是那些指向 B 的 A,也就是进入 B 的箭头。所以看的不是出站(我看向谁)而是入站。变更的冲击波沿着箭头逆流而上。

第二,本体 affects——在语义上影响到什么。这是 atom 的 frontmatter 中写明的 affects: 字段。即使名字没有直接出现,它也是设计者预先声明的"这个会影响那边"的语义连接。它把 grep 抓不到的别名、同义词问题,由人预先录入了进来。

第三,wikilink 反向引用——以 [[atom_id]] 形式显式链接到我的文档。可信度最高。因为这不是偶然的词语匹配,而是作者有意建立的链接。

把这三条路径的关系画成图,如下。

combat_global _cooldown_constant

入站边 谁在引用我

本体 affects affects: 字段声明

wikilink 反向引用 [[atom_id]] 显式链接

影响范围列表 去重 · 附加等级

把三条路径捆进一个工具,就是 impact。下一节我们完整地看这个工具实际吐出的输出。


18.4.2 实操记录(worked transcript)—— 跑一次 impact

与其用话解释,不如从头到尾跟完一个真实的循环——把真实操作过程原样记录下来。它包含提示词全文、工具的原始输出(未加修饰)、我验证并否决的部分,直到重新请求为止,全部收录。

提示词(全文)

我请 Claude 跑一遍 impact 工作流。工作流本体是调用 ripgrep 的 shell,但结果解读和等级推荐由 Claude 负责。

请对 combat_global_cooldown_constant 运行 impact 工作流。
分别用 grep 提取三条路径(入站边 / affects / wikilink 反向引用),
合并重复项后,为每个影响对象附上影响等级推荐。
变更内容为"全局冷却 0.5 秒 → 0.4 秒"。

Claude 原始输出(不作摘要,原样呈现)

我将按顺序执行这三条路径。

[1/3] 入站边 —— 引用该 atom 的位置
$ rg -n "combat_global_cooldown_constant" --type md docs/ atoms/

atoms/combat/combat_skill_cooldown_rule.md:34:  base: combat_global_cooldown_constant
atoms/combat/combat_gcd_ui_feedback.md:12:  depends_on: combat_global_cooldown_constant
docs/spec/combat_v3.md:42:  以全局冷却(combat_global_cooldown_constant)为基准
docs/spec/skill_design.md:18:  冷却时间按 combat_global_cooldown_constant 的倍数设计
docs/balance/refgame_comparison.md:77:  refgame 为 0.5 秒,我们的 combat_global_cooldown_constant 也相同

→ 发现 5 处直接引用。

[2/3] 本体 affects —— 通过 affects 字段指向该 atom 的位置
$ rg -n "^\s*-?\s*combat_global_cooldown_constant" -B3 atoms/ | rg -B3 "affects:"

atoms/combat/combat_dps_curve.md:
  affects:
    - combat_global_cooldown_constant   ← (注意:需确认方向)

→ 1 处。不过这是 dps_curve 声明"施加影响"的记录,
  方向可能相反。需人工审核 —— 参见下方验证项。

[3/3] wikilink 反向引用 —— [[...]] 形式的链接
$ rg -n "\[\[combat_global_cooldown_constant" --type md

docs/spec/combat_v3.md:42:  ...[[combat_global_cooldown_constant]]...
atoms/combat/combat_skill_cooldown_rule.md:34: ...[[combat_global_cooldown_constant]]...

→ 2 处。两处都是 [1/3] 中已捕获的位置(重复)。

[汇总 —— 去重后的唯一影响对象]
1. atoms/combat/combat_skill_cooldown_rule.md   (入站边+wikilink)
2. atoms/combat/combat_gcd_ui_feedback.md       (入站边)
3. docs/spec/combat_v3.md                        (入站边+wikilink)
4. docs/spec/skill_design.md                     (入站边)
5. docs/balance/refgame_comparison.md            (入站边)
6. atoms/combat/combat_dps_curve.md              (affects —— 方向未定)

[等级推荐 —— 依据 18.2 影响 5 级标准]
1. combat_skill_cooldown_rule    → T1(所有技能冷却时间都是该常量的倍数)
2. combat_gcd_ui_feedback        → T2(UI 反馈时机,需视觉校正)
3. combat_v3.md                  → T3(规格文档数值更新)
4. skill_design.md               → T3(设计文档数值更新)
5. refgame_comparison.md         → T2(基准对比表,需重算与 refgame 的差距)
6. combat_dps_curve.md           → 待定(确认方向后再定等级)

人工验证 / 否决

这里我停下来的地方有两处。

否决 1 —— affects 方向combat_dps_curve.mdaffects: 字段里写下我们的常量,意思是 dps_curve 施加影响给这个常量的声明。可现在我们要改的是常量这一侧。也就是说,箭头方向相反。不是 dps_curve 改动常量,而是常量一改,dps_curve 的计算前提就会动摇。Claude 老实地以"需确认方向"挂起,这是对的。我把这一项保留为影响对象,但重新归类为"反向语义依赖"而非 affects。确定为 T2。

否决 2 —— 把 refgame 对比表看作 T2是高估了。refgame_comparison.md 是与外部游戏做的单纯数值对比表。我们的数字一变,只需把表里一格从 0.5 改成 0.4 就完事,没有任何系统以这张表为输入。下调为 T3(文档数值更新)。

重新请求

请做两处修改后重新整理影响表。
1) combat_dps_curve.md 保留为影响对象,标签改为"反向语义依赖",
   等级确定为 T2。
2) refgame_comparison.md 从 T2 下调到 T3(没有系统以它为输入,只改表中一格)。
另外,只汇总 T1·T2,拆分为"变更前必须检查"清单。

Claude 整理的最终结果如下。

[变更前必须检查 —— T1·T2]
T1  combat_skill_cooldown_rule.md   所有技能冷却时间都是该常量的倍数
T2  combat_gcd_ui_feedback.md       UI 反馈时机重新校正
T2  combat_dps_curve.md             反向语义依赖 —— 重新确认 DPS 曲线前提

[仅文档数值更新 —— T3]
T3  combat_v3.md / skill_design.md / refgame_comparison.md

起初在脑子里"浮现不出来"的 6 个影响对象,经过一次 grep 循环和两次人工判断,变成了一份带优先级的清单。这就是影响范围提取的实质:工具把候选全量搜罗,人来定方向和等级。


18.4.3 提取流水线 —— 哪些是自动,哪些是人工

把上一节的一个循环概括成流程,如下。关键在于自动环节和人工环节在哪里分道。

flowchart TD A[指定变更目标 atom<br/>combat_global_cooldown_constant] --> B{执行 impact} B --> C1[入站边 grep<br/>rg atom_id] B --> C2[affects 字段 grep<br/>rg affects: 块] B --> C3[wikilink 反向引用 grep<br/>rg 方括号 atom_id] C1 --> D[去重 · 汇总] C2 --> D C3 --> D D --> E[LLM 等级推荐<br/>T0~T4 标签] E --> F{人工验证} F -->|方向错误·等级过高| G[否决后重新请求] G --> E F -->|通过| H[确定变更前检查清单<br/>拆分 T1·T2] H --> I[自动附加到变更请求评论] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class B,C1,C2,C3,D,I code; class E ai; class F,G human; class A,H data;

自动的部分是三条路径的 grep 与汇总,以及等级初稿。人工的部分只有一个,方向与等级的最终判断。上一个循环里看到的 affects 反向和 refgame 下调,正是在这个位置发生的。如果 100% 信任工具,就会出两种事故:要么把 affects 反向从影响对象里剔除,要么过度保护对比表,每次都跑一遍不必要的评审。把自动与人工的边界放在这一个点上,正是这套工作流的设计意图。


18.4.4 三条路径 grep 模式参考

把 impact 内部调用的 ripgrep 模式原样写下来。这就是这个工具的真身——不是华丽的基础设施,而是三行经过验证的正则表达式。

入站边。atom ID 在文档正文中出现的所有位置。搜罗得最宽。

rg -n "combat_global_cooldown_constant" --type md docs/ atoms/

affects 字段。只看 atom ID 落在 affects: 块内的情形。用 -B3 一并带出前 3 行,由人用眼睛确认那是 affects 块还是别的字段。

rg -n "combat_global_cooldown_constant" -B3 atoms/ | rg -B3 "affects:"

wikilink 反向引用。只看用两个方括号包起来的显式链接。可信度最高,是优先检查的对象。

rg -n "\[\[combat_global_cooldown_constant" --type md atoms/ docs/

这三个模式的可信度与召回率恰好成反比。wikilink 几乎 100% 准确,但作者不建链接就抓不到。入站边全都搜罗,却混进偶然的词语匹配(噪声)。affects 抓得住语义,方向却容易搞混。三者合起来才能补上窟窿。只用一个,就一定会有漏的地方。


18.4.5 与决策卡绑定 —— portal_layer_change_impact_check

影响范围提取是决策周期(§18.3)中的一步。决策卡登记的那一刻,impact 便以该卡的 affected_atoms 槽为输入运行。验证这一连接的 atom 就是 portal_layer_change_impact_check

这个 atom 的职责是"当变更跨越 Layer 时,不让影响检查被跳过"。冷却常量的变更只是 L1(系统)里的一个数字,但其影响会蔓延到 L3(数据表公式)和 L4(构建 QA 项)。portal_layer_change_impact_check 判定变更是否越过 Layer 边界,越过就强制执行 impact。

---
name: portal_layer_change_impact_check
type: gate
description: 跨越 Layer 边界的变更,在影响检查通过前禁止纳入构建
trigger:
  - 决策卡登记时 affected_atoms 非空
  - 变更 atom 的 layer != 影响 atom 的 layer
action:
  - 执行 impact(三条路径 grep)
  - 若存在 T1·T2 影响对象,在勾选"检查完成"前阻止合并
---

在冷却时间这个案例里,这道关卡抓到的不是 combat_skill_cooldown_rule(L1),而是以该 rule 为输入的 CombatBalance 表格(L3)。表格里的冷却倍数列是以常量为前提搭建公式的。grep 从文档中搜罗 atom,关卡则推着你"这个越过了 Layer,连表格一起看"。两者不绑定,就会出现文档更新了、表格却停留在旧前提的典型遗漏。


18.4.6 度量 —— 工作流回收了什么

这是作者在项目A运营中观察到的变化。时间数值为作者估算(未经验证),而遗漏事故的件数是季度复盘中实际统计的值。

条目 无工作流 运行 impact
掌握影响 atom 的时间 依赖记忆(不完整) 1\~2 分钟(全量 grep)
变更遗漏事故 每季度 8\~12 起(统计实测) 每季度 1\~2 起(统计实测)
变更请求附带影响清单 偶尔靠人 由关卡强制
新成员掌握影响 数天(口头逐一传达) 30 分钟(工具 + 卡片)
基础设施成本 考虑引入图数据库 只用 ripgrep + shell

最后一行就是整章的结论。项目A 曾考虑过图数据库和搜索索引,最终落定在 ripgrep 和一个小 shell 上。精密测量仪器确实比卷尺准。但每天都要拿出来用的工具,会收敛到不会坏、无需基础设施的卷尺那一侧。遗漏事故从 8\~12 起降到 1\~2 起,不是因为工具精巧,而是因为每次都毫无遗漏地跑一遍。


18.4.7 局限 —— grep 抓不到的东西

即便捆起三条路径,也还有漏的地方。知道局限而用它,与不知情而盲信它,是两回事。

别名与缩写。如果正文只写"GCD(Global Cooldown,全局冷却)",就不会被 combat_global_cooldown_constant 的 grep 捕获。补救办法是把检索词扩展成正则——(combat_global_cooldown_constant|GCD|전역\s?쿨다운)。另外维护一份团队缩写词典,检索时自动合成。

不可逆领域。grep 是可逆阶段的工具。纳入构建之前,文档、atom 与表格之间的影响全都能用 grep 看到。但构建发布出去、用户体感到 0.4 秒冷却之后的反应——社区不满、体感节奏变化——不是 grep 的对象。所以原则很简单。所有 grep 检查都在纳入构建之前完成。一旦进入不可逆阶段,grep 能知道的东西就急剧减少。

LLM 审核的位置。就像上一个循环里由人来定 affects 方向和等级那样,在 grep 候选的适配性判定中插入 LLM,噪声就会被滤掉。不过 LLM 也不是 100%,所以最终批准由人来做。在工具、LLM、人各过滤一道的结构里,准确度才能达到可运营的水平。哪怕少一道,那一道原本会漏掉的那类事故就会重新进来。


游戏之外的应用。把"改动这一项会牵动哪里"用全量检索而非记忆去搜罗,这个习惯对任何用文档、电子表格工作的白领都能带来同样的效果。修改某一条条款时,若想靠脑子回忆写明了该条款编号的合同、通知邮件、客户 FAQ 分布在几处,一定会遗漏;但用关键词把整个文件夹 grep 一遍全量搜罗,再由人分类为"必须改 / 仅标记 / 无关",遗漏就消失了。举例来说,会计人员变更某个科目代码时,把引用该代码的结算表格、报表模板、宏做全量检索,整理成变更前检查清单,就能从结构上杜绝"漏看了那一张表"式的季度结算事故。

18.4.8 动手试试

setup

只要文档和 atom 以纯文本(.md)管理、并装好了 ripgrep(rg),准备就绪。若有 atom ID 命名规范(蛇形命名、唯一 ID),grep 的准确度会大幅提升。

# 验证:某个 atom ID 在全部文档中出现了多少次
rg -c "combat_global_cooldown_constant" docs/ atoms/

prompt

给出变更目标 atom 和变更内容,请求三条路径提取 + 等级推荐。

请对 <atom_id> 运行 impact。
分别用 grep 提取入站边 / affects / wikilink 反向引用三条路径,
去重汇总后,推荐 18.2 的影响等级(T0~T4)。
变更内容:<把什么改成什么>。
只把 T1·T2 拆分为"变更前检查"清单。

verify

别原样相信工具输出,手动确认两点。

  1. affects 方向 —— 看被 affects 抓到的项,是"我施加影响的一侧"还是"我被影响的一侧"。方向相反就改标签。
  2. 等级过高/过低 —— 若表格或用于对比的文档被列进 T1·T2,就问"有没有系统以这份文档为输入"。没有就下调到 T3。

确认后,只把 T1·T2 清单贴到变更请求评论里,一个循环就闭合了。

单人精简版

如果是既没有工具也没有 atom 图谱的个人作业,用一行命令和一格备注也能取得同样的效果。

# 用要改的概念名称在全部文件夹中做全量搜索
rg -n "전역쿨다운|GCD|global_cooldown" .

把检索结果原样贴进记事本,在每一行旁边手动写上"必须改 / 仅标记 / 无关"三者之一。这就是单人版的 impact。关键不在工具的精巧,而在"不依赖记忆、全量搜罗后由人分类"这个流程本身。有流程,遗漏就减少;没有流程,周一上午那份茫然就每次重演。


本章要点

下一章预告

19.1 把愿景用作决策的评分表 —— 将 decisions/ 中的 26 个决策交给 LLM 检验

主要读者:带领中等规模(10\~50 人)团队的设计总监·主策划 面向单人/业余读者的精简版:§19.1.8「一个人的话,做到这些就够」

即便是把愿景文档在一页内写得很好的团队,也会反复出现同一种事故。愿景挂在墙上,可每周不断堆积的决策是否与这份愿景相符,却没有人去确认。季度复盘时会翻出来看一次,但那时早已在偏离的决策之上又叠了三个左右的后续决策。要让愿景成为“争议的基准点”,比起撰写,更重要的是每做一个决策就拿去与愿景对照。而这项对照工作,若由人手工去做既枯燥又容易遗漏——正是适合交给 AI 的事。

本章把两件事合在一起讲。前半部分是把已经写好的愿景当作决策评分表来运转的工作流——把作者项目中实际的 26 个决策 atom 放到 LLM 上,得到“违反愿景槽位”的判定,并由人抓出其中一处误判的一整个循环。后半部分回答的是这张评分表能覆盖到谁的决策这一问题,也就是授权委派。领导力的一般论(愿景为何重要、委派为何是成长的工具)在其他书里已经讲得够多,本章只专注于把这些原则放到 AI 工作流中运转的那一环。


19.1.1 愿景·路线图·排期 —— 只点明三层为何不同

先要理清“愿景筛选决策”这句话。愿景、路线图、排期并不是一回事。它们的时间单位与变更频率不同,而当这种区别崩塌时,排期压力就会动摇愿景。

周期 变更频率 与愿景对照的意义
愿景 5\~10 年 几乎不变 决策必须符合的基准线
路线图 1\~3 年 每季度 把愿景翻译成排期的中间层
排期 1\~3 个月 每周 不与愿景直接对照

关键在于:决策所要对照检验的对象是愿景(最不易变的那一层)。不是因为排期紧张就去改愿景,而是当排期与愿景相冲突时去调整排期。只有这一层级关系清晰,下一节的自动检查才有意义。因为如果检查的基准线每周都在摇动,检查本身就毫无意义。

愿景用一页、5 个槽位就写完。作者项目的愿景文档是下面这副骨架。这些槽位将成为 §19.1.3 中 LLM 检查的评分标准,所以先把它的形态看一遍。

---
title: 项目A 愿景 v2
layer: L0
locked: true   # 变更时需游戏总监 + CEO 达成一致
---

## 槽位 1. 我们要做的东西
韩国奇幻世界观的移动优先 MMORPG。

## 槽位 2. 为谁而做
30~50 多岁、以移动端为主、喜欢厚重叙事的用户。

## 槽位 3. 为什么(差异化)
- 以多层叙事实现深度叙事(重深度而非量产)
- 东南亚 + 韩国同时运营

## 槽位 4. 怎么做(价值)
- 尊重用户的时间(最小化无谓消耗的内容)
- 数据 + 人的平衡决策
- 团队共识优先于决策速度

## 槽位 5. 不是什么
- 不是 F2P 激进付费模式
- 不以 PvP 为中心
- 不强制每天在线 N 小时

槽位 5(“不是什么”)在检查中出力最多。因为违反往往不是出在“决定要做的事”上,而是出在悄悄去做“决定不做的事”的地方。


19.1.2 决策早已以 atom 形式积累

拿愿景去检验什么?作者团队把所有重要决策都以一张 atom 的形式固化到 decisions/ 文件夹中。这是标明了日期、当事方、依据的事实记录,目前已积累 26 个。检查的输入就是这 26 个——不是新造,而是把已有的拿去检验。

一张决策 atom 的实际形态如下(已匿名化)。

---
type: decision
id: D0019
date: 2026-05-12
deciders: [游戏总监, 数据总监]
tier: T1
---
# refgame_selective_adoption_for_mobile
将参考 MMORPG 的部分战斗数据选择性地采用到移动端构建中。
依据:6 英寸移动端上已有经过验证的战斗节奏,若从 0
重新设计,Alpha 排期会推迟一个季度。但付费、签到诱导
结构不予采用。

从 26 个中挑出若干个用作检查输入的代表(实际 atom 名,§A.3.3)。

atom id atom 名(匿名化) tier 一句话要点
D0007 claude_role_transition_phase2 T1 将 Claude 从被动辅助提升为主动伙伴
D0014 dataset_scope_alpha_split T2 确定 Alpha 数据集的拆分标准
D0019 refgame_selective_adoption_for_mobile T1 选择性采用参考游戏的战斗数据
D0021 procedural_capability_frontier_5stage T1 定义程序化生成能力的 5 个阶段
D0023 class_keyword_world_only T2 将职业关键词限定在世界观之内

这张表就是下一节提示词的输入数据。一次性把 26 个都拿去检验是关键所在,而如果人在复盘时手工把 26 个逐一与愿景对照,要花上半天,并且从中途开始注意力下降,就会漏掉违反项。这项枯燥的初步对照交给 LLM。


19.1.3 [实操记录] 把 26 个决策放到愿景上检验

下面把一个完整循环从头看到尾。输入的提示词可以原样复制使用,输出则是对真实会话的重现。

第 1 步 —— 提示词:把愿景作为评分表给出,并强制判定格式

请把 26 个决策 atom 逐一放到下面的愿景 5 个槽位上检验,判定每个决策相对于
愿景属于符合/补充/违反/无关中的哪一种。若为违反或无关,请用一行注明是哪个
槽位以及原因(尤其要仔细看槽位 5“不是什么”),存疑的用“存疑”标出交给我。
符合的可以不写依据。
只给表格(id | 判定 | 冲突槽位 | 依据 | 存疑),不要总评。

[愿景 5 个槽位 —— 粘贴 §19.1.1 全文]
[26 个决策 atom —— id·正文·依据]

提示词有一半是在“强制判定格式”。若给它自由,LLM 会把 26 个全都含糊成“大体符合”。设定 4 级判定标签、只对违反·无关强制附依据,以及留一个“存疑就交给人”的出口,才是这条提示词的核心。在依据句中明确点名槽位 5,也是有意为之——因为违反出在“决定不做的事”上。

第 2 步 —— Claude 的输出(重现,节选)

| id    | 判定 | 冲突槽位 | 依据                                     | 存疑 |
|-------|------|----------|------------------------------------------|------|
| D0007 | 无关 | —        | Claude 运营决策。与游戏愿景的评价维度不同   |      |
| D0014 | 无关 | —        | 数据集拆分。属内部基础设施决策             |      |
| D0019 | 违反 | 槽位 5   | 采用参考游戏战斗数据 → 与“不是 F2P 激进     | ★    |
|       |      |          | 付费模式”存在冲突之嫌                     |      |
| D0021 | 符合 | 槽位 3   |                                          |      |
| D0023 | 符合 | 槽位 1   |                                          |      |
| ...   |      |          |                                          |      |

(26 个中:符合 18 · 补充 3 · 无关 3 · 违反 2)
违反/存疑判定待确认:D0019、D0026 —— 需人工评审

输出中最有价值的部分不是表格,而是最下方的 2 处违反与存疑标记。LLM 自动过滤掉 26 个中的 24 个,只把该由人来看的 2 处呈上来。半天的对照缩减为 2 处的评审。然而这 2 处里有一处是误判。

第 3 步 —— 验证与否决(人的位置)

人再读一遍 D0019(refgame_selective_adoption_for_mobile)的判定。LLM 看到“采用参考游戏的战斗数据”,便判定它与槽位 5 的“不是 F2P 激进付费模式”相冲突。表面上的字眼看起来说得通——因为参考游戏以激进付费而闻名。

然而,如果把 atom 正文读到最后,会看到最后一句。“但付费、签到诱导结构不予采用。” 这个决策只取用战斗节奏数据,而把付费结构明确排除在外。它反而是一个维护槽位 5 的决策。LLM 没能把 atom 正文最后这句限定句纳入判定权重,被“参考游戏”这个出处字眼牵着走,把它归为违反。这不是违反槽位 5,而是符合

出现这种误判的原因很清楚。LLM 把决策的出处(从哪款游戏取来)与决策的内容(取来什么、舍弃什么)看得同等重要。而人知道“但……不做”这样的限定分句才是决策的核心。于是人否决它,并重新请求。

请重新看一下 D0019。正文最后一句“但付费、签到诱导结构不予采用”才是核心。
请把采用的部分(战斗节奏数据)和排除的部分(付费、签到结构)分开,
分别判定各自落在哪个槽位上。

LLM 重新作答:“采用的对象(战斗数据)符合槽位 1·2,排除的对象(付费结构)积极支持槽位 5。综合判定:符合。上一次的违反判定是对出处字眼过度反应的错误。”经过这一次往返,D0019 就从违反更正为符合。剩下真正需要评审的只有 D0026 一处。

这个循环就是本章的核心。LLM 会把 26 个缩减为 2 处,但这 2 处里可能有一处是误判。 自动检查并不是取消人的评审,而是让人把精力从 26 个转到 2 处上的工具。如果人不把这 2 处读到最后,好端端的决策就会以“违反愿景”的名义被搬上会议,制造出无谓的争执。


19.1.4 一图看懂检查流程

把上面的循环画成图留存下来,之后每个季度就能重复同一套流程。关键在于:LLM 的判定不会自动推翻决策。只把违反·存疑上报到人工关卡,而废弃·更正·批准由人来做。

flowchart TB A["愿景 5 个槽位 (L0, locked)<br/>评分基准线"] B["decisions/ atom 26 个<br/>id·正文·依据"] A --> C["LLM 初步判定<br/>符合/补充/违反/无关 + 依据"] B --> C C -->|符合·补充·无关 24 件| F["通过 —— 记入季度复盘"] C -->|违反·存疑 2 件| D{"人工评审关卡"} D -->|确认为误判| E["重新请求<br/>(补充限定句·上下文)"] E --> C D -->|确为违反| G["召集决策复议会议"] D -->|修正后符合| F classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class C ai; class D,E,G human; class A,B data; class F pass;

人经手的地方只有两处。一处是干净地录入愿景与决策(最上方),另一处是把 LLM 判为违反·存疑的少数条目读到最后并作出判定(中间的关卡)。中间那段枯燥的 26 个对照由 LLM 来跑。这与 §6.2 城市生成器中 lint 不自动废弃违规、而只向作者关卡发出 alert(警报)的设计相同——机器挑出可疑候选,而杀还是留由人来定。


19.1.5 愿景检查覆盖到谁的决策 —— 授权委派

这里自然会冒出一个问题。这 26 个决策都是游戏总监一个人做的吗?那样可不行。主管若亲自做所有决策就会成为瓶颈,若全部授权下放又会削弱愿景。愿景检查同时也是一张安全网——用来把授权下放的决策也纳入同一张评分表来筛选

决策是有等级的,而等级就意味着权限。下面是作者团队的权限矩阵。

等级 决策者 评审者 通知 是否纳入愿景检查?
T0 愿景·核心 游戏总监 + CEO 全体组长 全团队 愿景本身(检查基准线)
T1 系统·跨领域 TF 组长 + 游戏总监 TF 成员 相关领域团队 ✅ 必须
T2 领域·中等 领域总监 资深人员 相关领域团队 ✅ 必须
T3 单次·小型 资深人员 负责人 直接相关方 抽样检查
T4 即时·热修复 负责人 资深人员(事后) 游戏总监(事后) 不纳入检查

decisions/ 里的 26 个大多是 T1·T2——都是授权下放的决策。游戏总监并不亲自过目所有 T2。取而代之的是,愿景检查(§19.1.3)每季度把授权下放的 T1·T2 决策拿到愿景上检验一次。可以说,授权的安全网就是愿景检查。 T0 不是检查对象,而是检查的基准线;T4 热修复数量多、对愿景几乎无影响,因此排除在外。

授权本身不是一步到位,而是分 4 个阶段渐进推进。

阶段 权限 与 LLM 检查的关系
1. 信息传达 “照这样做” 由授权者决定,无需检查
2. 建议 + 上报决策 “考虑 X 后再决定” 上报时一并对照愿景
3. 事后上报 “决定后告知结果” 固化为 atom → 纳入季度检查
4. 自主决策 无上报义务(在等级限度内) 只要留下 atom,检查便可事后覆盖

第 4 阶段的自主决策与愿景相偏离的风险最大,而正是这种风险由 §19.1.3 的检查在事后抓住。自主做出的 T2 决策,只要固化成 atom,就会自动进入季度检查。授权的自由与愿景的一致性之所以不冲突,原因就在这里——自由地做决策,但决策要留成 atom,而 atom 每季度都会被拿去与愿景对照。


19.1.6 如何诚实地对待数字

写愿景·授权这一章,很容易忍不住想放进“引入愿景后会议时间从 90 分钟减到 45 分钟”“授权后总监的决策负担从每周 30 件降到 5 件”这类表格。这类数字若未经验证,反而会削弱本书的可信度。本章的数字只按以下三种方式之一来处理。

第一,能数的就按实测来写。 decisions/ 的 atom 目前是 26 个(以 2026 年 5 月的实测为准)。从 LLM 初步判定上报到人工关卡的件数、其中被更正为误判的件数,都是可由会话日志计数的实测值。上面的实操记录中,2 件违反判定里有 1 件(D0019)是误判,这也是真实会话的结果。

第二,效果只谈方向。 “半天的对照缩减为少数几处的评审”讲的是结构的方向,而不是绝对时间。确切能省下多少时间会随决策数量、团队规模、atom 正文长度而变,所以应把它当作“手工过 26 个”与“LLM 初步判定 + 人工关卡”之间的结构差异来读。会议时间、士气分数这类结果指标不会由愿景一项决定,因此不对因果下定论。

第三,只承诺可测量的东西。 这套工作流实际可测量的是——每季度纳入愿景检查的决策数、通过人工关卡的件数、误判率(LLM 判为违反、被人更正为符合的比例)、atom 固化遗漏件数(已授权下放却因没有 atom 而漏出检查的决策)。这四项在会议上可以用数字而非“感觉”来讲。尤其是误判率,它每个季度都用数字证明为什么不能照单全信 LLM 的判定。


19.1.7 常见的失败

模式 为何失败 对策
只写愿景,不拿去检验决策 愿景沦为墙上装饰,决策各行其是 每季度把 26 个 atom 放到愿景上检验的 §19.1.3
把 LLM 的违反判定直接搬上会议 误判(如 D0019)引发无谓的争执 违反·存疑的条目由人把 atom 正文读到最后
决策不留成 atom 授权下放的决策整个漏出检查之外 在事后上报(授权第 3 阶段)中强制固化为 atom
连 T4 热修复也全部检查 只增加数量,对愿景几乎无影响 把检查范围限定在 T1·T2
授权跳过 1→4 阶段 自主决策在偏离愿景的情况下不断累积 分阶段授权 + 季度检查来事后覆盖

第三项最常被忽略。越是能自主顺畅运转的团队,越容易只在口头上达成决策而不留下 atom。这样一来,§19.1.3 的检查只看得到已固化的决策,于是最自由做出的决策反而落入检查的盲区。授权的自由,只有以固化 atom 为前提才安全。


游戏之外的应用。 把愿景拿去逐一检验每个决策,以及授权委派,并不只是游戏团队的功课,而是每一位管理者的工作。如果把部门的使命用一页 5 个槽位(“我们做什么 / 为谁 / 为什么 / 怎么做 / 不是什么”)钉死下来,就能每季度用 LLM 对每周堆积的实务决策是否偏离这份使命做一次初步对照——尤其容易抓出悄悄去做“决定不做的事”这类违反。举例来说,把团队负责人授权下放的决策每季度拿到部门使命上检验,就能成为一张安全网,事后抓出自主做出的决策是否偏离了方向。不过,LLM 判为“违反”的条目不要直接搬上会议,其中一处可能是误判,所以必须由人读到最后。

19.1.8 动手试试 —— 今天就能做的一步

一个人的话,做到这些就够:没有决策 atom 文件夹也没关系。先把你自己项目(或业余游戏)的愿景,用 §19.1.1 的 5 个槽位写满一页就好。接着把最近做的 5\~10 个决策各用一行记成便签,再把 §19.1.3 的提示词原样贴上,交给 LLM 过一遍。只要出现哪怕一个“违反”判定,就把那个决策的便签重新读到最后,亲自反驳一下 LLM 是否正确。这样一来,你就能亲身体会到愿景检查究竟是一组怎样的判断,以及为什么不能照单全信 LLM 的判定。

如果是团队,就从下面这一步开始。先把愿景固定为 5 个槽位、一页纸(L0, locked),再从把最近一个季度做出的 T1·T2 决策以一张 atom 的形式固化到 decisions/ 文件夹做起。哪怕只积累了 10 个 atom,也可以把 §19.1.3 的提示词跑一遍;只要在这第一个循环里抓出授权下放的决策中一处偏离愿景的,这套工作流的价值就会立刻显现。


本章要点

下一章预告

19.2 对冲突分类,不让会议中的决策流失——会议领导力的 AI 辅助

主要读者:每季度在会议上作出50件以上决策的总监·团队负责人(中等规模(10\~50人)团队) 面向个人/爱好者读者的精简版:§19.2.8「如果只有你一个人,做到这些就够了」

我曾把一场会议顺利开了90分钟,一周之后,同一个议题却又摆回了会议桌上。明明已经拍板,可是谁负责什么却哪里都没记下来。会议记录里只留下了"讨论了全局冷却",而"定为0.5秒,负责人团队成员A"这句,在当时在场者的脑海里只过了一周就挥发殆尽。领导的会议崩掉的地方,大多不在会议进行中,而是在会议刚结束、决策尚未固化为记录之前的那道短暂缝隙

本章处理团队负责人工作的两大块。前半部分是不必每次都从零开始解决冲突,而是把它送往按类型划分的标准处方的方法;后半部分则是本章的脊柱——让 AI 提取会议中产出的决策,但一旦缺少负责人(owner)或依据(rationale)就强制不予通过的实操记录(worked transcript)。领导力的一般论(提出愿景·倾听·共情)在别的书里已经讲得够多,因此本章只聚焦于把这套一般论跑成 AI 工作流、从而防止决策遗漏的环节


19.2.1 冲突的目标不是归零,而是分类

以为零冲突的团队才是健康团队,这是一种误解。一个中等规模(10\~50人)的团队每季度作出50件以上决策,却一次摩擦都看不见,那不是没有冲突,而是冲突沉到了水面之下,而沉下去的冲突更危险。

领导要做的不是消灭冲突,而是尽快对类型分类,把它送往标准处方。若同样的冲突每次都以不同方式解决,那么解决所花的时间每次都要从零重新累积。

冲突类型 冲突的本质 标准处方
价值冲突 对愿景的不同解读(营收 vs 用户时间) 引用愿景槽位
事实冲突 对同一数据的不同解读 核对数据(元游戏报告)
优先级冲突 "我的领域更重要" 比较影响等级·KPI 影响
权限冲突 "这是我的决定" 重新确认权限矩阵
个人冲突 人际关系·沟通风格 一对一,事实/情绪分离(系统之外)

前四种,处方都是引用系统。愿景·数据·KPI·权限矩阵一旦被明文化,决策的分量就从人的嘴转移到系统一侧,讨论随之变短。只有第五种个人冲突在系统之外——一对一,以及事实·情绪的分离,除了时间与真心之外的工具几乎都不起作用。不过,"系统解不了"并不是领导可以撒手的借口。系统解不了的领域,归根结底也是领导的活儿,这正是此处的棘手之处。

分类不是每次从头解一遍,而是转成一条流程。

flowchart LR A["识别冲突"] --> B["类型分类<br/>(5类)"] B --> C["应用标准处方<br/>愿景·数据·KPI·权限·一对一"] C --> D["1周后跟进确认"] D --> E{"复发?"} E -->|"同类反复"| F["检查系统·规则<br/>(季度复盘冲突槽位)"] E -->|"化解"| G["结束"] F --> A classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class A,B,C,D,E,F human; class G pass;

关键在右侧的分岔点。若同一类冲突反复出现,那就不是人的问题,而是系统的问题。那时,不去调解人,而是去修整愿景·权限矩阵这类规则。这将成为 §19.2.7 要讲的季度复盘冲突槽位的输入。


19.2.2 会议是产出决策的地方,而决策不能流失

正如冲突处方的四种全都是"引用系统",会议归根结底也是一台产出决策、并把该决策固化为记录的装置。领导在会议中要守的五条原则彼此相扣。哪怕只缺其中一条,其余的也会跟着动摇。

  1. 议题在会议24小时前共享。(没有准备就聚在一起,会议就会滑向辩论场)
  2. 为每个议题强制设定时限。(信息共享5分钟·决策15\~20分钟·讨论30\~45分钟,超时则顺延)
  3. 在会议结束时明确"今日决策"。(不作决策就结束,下次会议会再次打开同一议题)
  4. 会议记录在结束后立即生成。(若指望人事后再整理,就会挥发)
  5. 每条决策都追踪负责人·依据·后续行动。(没有追踪的行动,不到下周就会消失)

这五条里,第3·4·5条崩掉,就是开头那起事故。决策用嘴说了(原则3部分满足),却没有固化为记录(原则4失败),负责人也没有录入(原则5失败)。于是一周之后,同一议题又摆了上来。

问题在于,若把原则3·4·5交给人的意志,它们会在繁忙的一周里最先崩塌。会议一结束,领导就已经奔向下一场会议。因此,要把这三条原则搬到 AI 辅助流水线上。 也就是从会议记录文本中自动提取决策,而一旦负责人或依据为空,就不让它通过。这条流水线,是从领导视角再看一遍第17部分中建立的会议→会议记录→atom 提取流程(§17.2)。


19.2.3 [实操记录] 从会议记录中提取决策,但缺少负责人就拦下

下面把实际怎么跑的完整走一遍。舞台设在作者项目(移动优先的 MMORPG,以下简称"项目A")的战斗 TF 会议刚结束之时。输入提示词可以照抄使用,输出则是对真实会话的还原。

第1步——输入:直接把未经整理的会议记录正文扔进去

不把会议记录整理得漂漂亮亮。发言彼此夹杂,是否算决策存疑的行也原样保留——这样粗糙的文本就是输入。整理是 AI 该做的事,不是人先要做的事。

[2026-06-05 战斗 TF 会议记录正文——摘录,未经整理]

团队成员A:全局冷却设为0.5秒的模拟结果很稳定。
团队成员B:如果把恢复技能也一起绑进0.5秒,恢复循环可能会被打乱。
团队成员A:那就单独拿出来。恢复作为全局冷却的例外。
李旼洙:好,全局冷却统一为0.5秒,恢复作为例外。A 帮忙把数据表
        cooldown 列统一过一遍。
团队成员C:目标选取优先级规则,下周再看看再定……
团队成员B:小地图缩小切换得和 UI 组一起看才行。先搁置。
李旼洙:好,那个放到下次会议。

这里混着两条决策(全局冷却0.5秒、恢复例外)和两条搁置(目标选取、小地图)。靠人用眼睛挑,总会漏掉一件。那就是开头那起事故。

第2步——提示词:让它提取,但不许把负责人·依据留成空栏

从附上的会议记录中只提取"决策"。讨论·搁置·信息共享都不算决策。
每条决策填写 decision(一句话)/ owner / rationale / follow_up 四栏,
如果在正文中找不到 owner 或 rationale,不要猜测,
写上"[MISSING——会议中未确定]"。搁置·顺延到下次会议的记为 deferred,
是否算决策存疑的行记为 ambiguous 交给我。输出
只用 decisions / deferred / ambiguous 三个区块。

请注意,这段提示词有一半都在"强制空栏"。给 AI 自由,它就会把 owner 编得像模像样,或把搁置项晋级为决策。留下一个"找不到就别猜、上报 [MISSING]"的出口,正是这套工作流的核心。决策要由人明确宣布才有价值(§17.6.3 的原则),AI 只做到把"此处为空"这一事实暴露出来为止。

第3步——Claude 的输出(对真实会话的还原)

decisions:
  - decision: 将战斗全局冷却统一为0.5秒。
    owner: 团队成员A("A 帮忙把数据表 cooldown 列统一过一遍"——李旼洙)
    rationale: "全局冷却设为0.5秒的模拟结果很稳定"(团队成员A)
    follow_up: 统一检查 cooldown 列

  - decision: 恢复技能不适用全局冷却。
    owner: [MISSING——会议中未确定]
    rationale: "如果把恢复也一起绑进0.5秒,恢复循环可能会被打乱"(团队成员B)
    follow_up: 将恢复技能 cooldown 例外处理写入数据

deferred:
  - 目标选取优先级规则(团队成员C,下周追加评估)
  - 小地图缩小切换(需与 UI 组协商,搁置)

ambiguous:(无)

输出中最有价值的部分,不是第一条决策那张干净的表,而是第二条决策的 owner: [MISSING]。恢复例外分明已经拍板,可会议记录里哪儿都没写"由谁写入数据"。AI 没有用猜测去填那个窟窿,而是诚实地上报了。好的提示词,能让 AI 说得出"这个位置是空的"。

第4步——验证与拒绝(领导的位置)

不能就这么把这份输出照单全收。冒出 [MISSING],意味着会议只把决策做完了一半。此处领导要做的,不是去改 AI 的输出,而是把会议上落下的决策补齐。

作者在这一步,用内部即时通讯向团队成员A问了一句:"恢复例外的数据写入,也是 A 一起来看,对吧?"A 回了"是"。这一句话就把缺失的负责人确定了下来。接着再次请求。

第二条决策(恢复例外)的 owner 已确定为团队成员A(通过内部即时通讯本人确认)。
把这个反映进去,重新给出 decisions,并把两条决策也
转成 pending atom 候选格式。
// (意图:包含 status: pending、source_meeting、owner、related_atoms——§17.2.4 格式)

AI 把两条已填好 owner 的决策,转换成两个 pending atom 候选后再次作答。这些候选不会立刻成为正式决策,而是以 pending 状态经过1周验证期(§17.2.4)。因为会议上定下的东西,也可能在运营一周后被推翻。相当于给墨水留出晾干的时间。输入 → 提取 → 上报 MISSING → 人补全决策 → 再次请求的一个循环,到这里闭合。

这一圈,从结构上堵住了开头那起事故。当决策只做了一半时,这个事实不是在会议结束一周之后,而是在会议刚结束的当场就暴露出来。


19.2.4 完整流水线——人手只落在两处

把上面的实操记录叠到第17部分的会议记录流水线之上,整幅图景就是这样。领导的手能触及的只有两处:在会议上宣布决策之处(最前),以及补全 AI 上报的 [MISSING] 之处(中间)。两者之间的提取·转换·登记都是自动的。

flowchart TB A["会议进行<br/>(领导:口头宣布决策)"] --> B["会议记录文本<br/>(未经整理的正文)"] B --> C["AI 提取决策<br/>强制4字段 + 上报 MISSING"] C --> D{"owner·rationale<br/>是否为空?"} D -->|"MISSING"| E["领导补全<br/>(即时通讯确认 → 确定负责人)"] E --> C D -->|"全部填满"| F["pending atom 候选<br/>(1周验证期 §17.2.4)"] F --> G["每周1次评审<br/>晋级·废弃·搁置 §17.2.5"] G --> H["JIT manifest 登记<br/>下次会话自动注入 §17.2.6"] classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class C,D ai; class A,E,G human; class B,F,H data;

在这条流水线里,AI 不做的事更重要。AI 不制造决策。不编造负责人。不把搁置项晋级为决策。AI 所做的,仅止于从会议记录里挑出决策候选、并把空栏暴露出来。决策的宣布与空栏的补全由人来做。这正是 §17.6.3 所说"决策槽位禁止 AI 自动生成"原则在领导视角下的应用——因为决策一旦传播到别的文档·会话·构建中就会留下不可逆的痕迹,所以在入口关卡处,要保留由人明确宣布的环节。


19.2.5 [MISSING] 强制支撑起平等决策文化

在公司 PC 的团队共享 atom 中,有一个名为 team_equal_decision_culture 的概念 atom。它把复盘中反复引用的措辞固化下来,用一个词指代"决策靠依据而非职位"的团队文化。这种文化不是总监一句"我定了,就这样"压下去,而是每条决策都留下谁·为何,从而让日后任何人都能以该决策的依据去回溯

§19.2.3 的 [MISSING] 强制,正是这种文化的技术支撑。不让负责人和依据以空栏通过,意味着决策的权威落在"因为它出自正文的某句发言",而非"因为总监说了"。依据引用一旦为空,决策就会被拦下,于是靠职位压出来的决策,在结构上无法成为 atom。

这种文化,与 §19.2.1 的冲突处方也一线相连。用引用愿景解价值冲突、用数据解事实冲突、用矩阵解权限冲突,全都是以记录下来的依据、而非人的一张嘴来解决这同一条原理。平等决策文化是冲突处方的土壤,而 [MISSING] 强制,则是每逢一次会议就把这片土壤夯实、不让它松动的工具。

在此之上,还叠加着团队文化的另一根轴——公开与封闭的边界。会议记录·决策卡·KPI 数据·事故报告放在公开区域,而一对一谈话·人事评价·薪资·个人情况放在封闭区域。决策提取流水线所处理的,全都在公开区域。个人冲突(§19.2.1 的第五种)之所以在系统之外,原因也一样——它属于封闭区域,不固化为 atom。


19.2.6 诚实对待数字的方法

领导力这一章,很容易被诱惑去放一张"引入会议流水线后,会议时间减半"之类的表。这样的数字若未经验证,就会削弱本书的可信度。本书的原则有以下三条。

第一,只把可测量的东西承诺为指标。 会议流水线真正能数的是这些——每条决策的 owner·rationale 缺失件数(目标为0)、从会议记录中提取的决策里晋级为 pending atom 的比例、"这个之前不是没定过吗?"的重开会议件数。这三项,在会议中能用数字而非"感觉"来说话。

第二,作者的推测就写明是推测。 会议刚结束时提取决策所花的时间——"手工整理会议记录20\~30分钟 → AI 起草 + 补全5分钟以内"——是作者基于经验的推测,是未经验证的假设。不必去记绝对值,而应把它读作结构差异("人从头开始挑拣" vs "AI 提取 + 只补空栏")。准确的节省时间,会随会议规模·决策数量而变化。

第三,不对因果下断言。 不把"重开会议变少了"完全归功于这条流水线。团队成熟度·项目阶段也在同时起作用。只说方向(决策遗漏若在会议刚结束时暴露,就会朝着重开会议减少的方向起作用),而不编造倍数。


19.2.7 季度复盘的冲突·决策槽位

冲突处方与决策流水线,会在季度复盘中走一遍检查循环。复盘中设有"冲突槽位"和"决策遗漏槽位"。

2026 Q2 季度复盘——冲突·决策槽位
─────────────────────────────────
[冲突] 本季度主要3件
1. 全局冷却(价值冲突)→ 以引用愿景结束。
   学习:再次确认愿景5槽位可作为决策依据发挥作用。
2. 新副本优先级(优先级冲突)→ 比较 KPI 影响。
   学习:因缺少优先级表,每次都临时比较 → 下季度引入表格。
3. 角色设计权限(权限冲突)→ 重新确认权限矩阵。
   学习:矩阵需增加"视觉 vs 功能"分工项。

[决策遗漏] 本季度发生的 [MISSING]
- 恢复例外决策 owner 未记录(2026-06-05)→ 通过内部即时通讯补全。
  学习:TF 会议宣布决策时,把立即点名 owner 加入进行检查表。

冲突也好,决策遗漏也好,都是复盘的输入。若同一类冲突反复出现,就修整系统(愿景·权限表);若 [MISSING] 经常以相同模式冒出,就修整会议的进行方式。§19.2.1 流程图中向右岔出的"检查系统·规则",在这里被具体化。


游戏之外的应用。 "分明已经拍板,一周后同一议题却又摆上来"这种会议事故,不挑行业。把会议记录正文不加整理、原样喂给 LLM 只提取决策,而一旦负责人或依据为空,就不用猜测去填、而让它以 [MISSING] 上报,那么决策只做了一半这一事实,就会在会议刚结束的当场暴露出来。比如在销售周会上,"这个客户由 A 负责"若只是口头说说、没有记录,下周就会悬在半空;而当 AI 提取冒出 owner: [MISSING] 时,就能当场用一句即时通讯确定负责人,消掉一场重开的会议。决策宣布与空栏补全由人、提取由 AI 承担,这样的分工是关键。

19.2.8 动手试试——今天就能做的一步

如果只有你一个人,做到这些就够了:没有团队、没有会议记录流水线也无妨。把你最近参加过的会议(读书会·社团·一人项目的商议都可以)的笔记,原样贴进 §19.2.3 的提示词里跑一次。只要 AI 冒出哪怕一条 owner: [MISSING] 的决策,那就是你的团队(或你自己)一周之后会再拿出来的议题。仅仅现在就把那个空栏填上,就能少掉一场重开的会议。

如果有团队,就从下面这一步开始。把下一份会议记录不加整理、原样放进 §19.2.3 的提取提示词,只启用规则2([MISSING] 强制)。pending atom·JIT 登记(§17.2)是之后的事。哪怕只有"强制空栏"这一行,也能在会议刚结束时抓住"以为定了、其实没记"这个最昂贵的遗漏。


19.2.9 常见的失败

模式 为何失败 处方
用同一种方法解决所有冲突 哪一类都无法彻底解决 5类分类 → 按类型处方(§19.2.1)
满足于零冲突的团队 冲突沉入水面之下(更危险) 冲突是健康信号,设季度复盘槽位
决策只用嘴说、不记录 一周后同一议题重开会议 AI 提取 + pending 固化(§19.2.3)
AI 用猜测填负责人 错误的负责人固化为 atom [MISSING] 强制,禁止猜测(§19.2.2)
AI 自动生成决策 决策的权威脱离依据 决策由人宣布,AI 只做增补(§17.6.3)
把搁置项晋级为决策 未确定的议题被不可逆地传播 用 deferred 区块分离(§19.2.3)

第三条和第四条最常捆在一起爆发。不记录决策的团队,会把整摊事甩给 AI——"你看着整理吧",AI 则很贴心地把负责人编出来。那个编出来的负责人一旦固化为 atom,一周之后就会冒出"我没答应过要负责啊"这种更昂贵的冲突。[MISSING] 强制,用一行就堵住这两个失败。


本章要点

下一章预告

19.3 AI引入战略与说服管理层——从保守到激进,ROI 绝不加工

主要读者:需要决定是否给团队引入AI、并向管理层说明其成本的主管(中等规模(10\~50人)团队) 面向个人/业余读者的精简版:§19.3.12「一个人的话,做到这些就够」

我曾在CEO办公室里被问到这样一个问题:"每月花在AI工具上的钱不少,那到底换来了什么改善?"当时我手里只有一张幻灯片,上面写着"生产力提升3\~5倍"。CEO又追问:"这3\~5倍是从哪来的数字?"我答不上来。那个数字不过是我从某处看到的博客平均值搬过来的,并不是我们团队实测的值。

那天之后,我把AI引入报告里所有加工过的数字全部删掉。取而代之,我开始如实汇报系统实际留下的东西——积累了多少个atom(最小知识单元)、有多少个技能在运行、日志里哪些输入会调出哪些上下文。本章讨论两件事。第一,把AI引入这一决策拆成从保守(人来决策、AI验证)到激进(AI生成候选、人来采纳)的分阶段决策框架。第二,把这次引入的ROI不用博客平均值、而是用我系统的实测日志向管理层说明的方法。领导力的一般论述在别的书里已经够多,因此本章只聚焦于用AI来辅助"是否引入AI"这个决策本身,并把其依据从系统日志中汲取出来的场景。


19.3.1 引入不是开关,而是阶段

若把AI引入看成"引入/不引入"的二分法,从第一步就会走偏。一次性打开五个工具,运营负担会先于效果到来;因为害怕而干脆不开,则永远无法起步。引入是一种从低风险的位置起步、经验证后再逐步扩大权限的分阶段决策。

贯穿全书的准绳在这里同样适用。由人来决策、AI只做验证的保守应用,由AI探索候选、人来采纳的激进应用。引入也遵循这一顺序。从注入上下文(保守)起步,待验证不断累积后,再过渡到自动生成(激进)。若反向跳跃——不经验证就先打开自动生成——就会事故不断,团队则会要求关掉工具。

flowchart TD S0["阶段0:手动<br/>无 AI"] --> S1["阶段1 保守:注入上下文<br/>人来决策 · AI 起草/验证<br/>(试点 1~3 个月)"] S1 --> G1{验证<br/>事故率·满意度} G1 -->|通过| S2["阶段2:验证自动化<br/>lint·规则手册把关<br/>(扩展 3~6 个月)"] G1 -->|未达标| S1 S2 --> G2{验证<br/>废弃率·复发} G2 -->|通过| S3["阶段3 激进:自动生成<br/>AI 探索候选 · 人来采纳<br/>(落地 6~12 个月)"] G2 -->|未达标| S2 S3 --> G3{不可逆关卡<br/>雇佣·组织变更} G3 -->|仅当可逆验证完成| S4["阶段4:角色演进<br/>达成共识后推进"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; class S2 code; class S3 ai; class S0,S4,G1,G2,G3 human;

关键在于各阶段之间的关卡。要迈入下一阶段,前一阶段的测量值(事故率·废弃率·满意度)必须通过基准。尤其是最后的阶段4(角色演进)是不可逆的。这一阶段会改变人的岗位、牵动招聘计划,因此在前面的可逆阶段完成验证之前绝不触碰。这一关卡结构,能防止被"听说AI不错"的气氛裹挟、一次性跳到激进应用的事故。


19.3.2 [实操记录(worked transcript)] 从系统日志中汲取用于说服管理层的ROI

假设已经决定引入。下一道关口,是审批这笔费用的管理层。在这里,主管最常犯的错误,就是把"生产力提升N倍"这类没有出处的数字塞进幻灯片。这种数字在第一个问题面前就会崩塌。

换一种做法。让AI清点我系统实际留下的资产,并把它整理成ROI(Return on Investment,投资回报)幻灯片,但绝不许编造没有出处的数字。下面把这一个循环从输入到废弃·再生成完整地誊录下来(实操记录=完整保留的真实操作过程记录)。输入提示词可以直接复制使用,输出则是对真实会话的还原。

第1步——输入:把系统留下的实测资产原样抛给它

先把无需编造、系统里已经存在的数字汇总起来。公司PC的团队记忆清单与个人PC的JIT日志,是第一手输入。

# ai_adoption_inventory.yaml —— 引入1年后的实测资产(以 book_appendix_A 为准)
team_atoms:                         # workspace/team_memory/atoms/
  rules: 244
  concepts: 19
  decisions: 26
  feedback: 11
  rnd: 4
  total: 304
skills:                             # workspace/skills/
  wrapper: 44
  meta: 4
  total: 48
jit_manifest:
  hot_atoms_injected: 221           # score>=20 OR manual_weight>=4
  external_export_atoms: 207        # 供 GPT/Gemini 注入的单个 md
operating_cost_usd_month: "需实测"  # 留空 —— 不要编造
hot_atom_example:
  - view_html_filename_convention: 356.53   # _scores_latest.json
  - xlsm_svn_update_before_edit: 349.26
  - claude_role_transition_phase2: 341.03   # decision atom

这份yaml里没有假货。304·48·221·207是从团队记忆清单里数出来的值,356.53这类分数是记录在_scores_latest.json(由atom_score.py产出)中的实测值。运营成本那一栏是我故意留空的——那是要从财务那里拿来填的值,不是AI该估算的值。

第2步——提示词:禁止生成没有出处的数字

用附上的 ai_adoption_inventory.yaml(引入1年后的实测资产)做一张给管理层看的
1页ROI幻灯片。只用yaml里有的数字,没有的比例·倍数·金额
绝对不要编造。不要用"生产力提升N倍"这类说法,而要用可清点的事实(atom数、技能数、
JIT注入条数)。operating_cost是空栏,不要用数字去填,
留成"[需财务确认]",作为审批问题保留。给不出依据的主张
另行标注、交回给我。按CEO关心的顺序(相比成本减少了什么)排列。
输出为1页:1行摘要 → 实测资产表 → 审批问题。

值得注意的是,这条提示词有一半都在讲"禁止捏造数字"。LLM有很强的倾向,会用看似合理的数字去填补空栏。只用yaml里的数字·空栏保持空栏·没有依据就上报这三根钉子,正是这条提示词的脊梁。

第3步——Claude的原始输出(还原、节选——含失败)

# AI引入1年ROI摘要(1页)



一行:引入1年间,团队共享知识已沉淀为atom304个·自动化技能48个, 其中221个已进入按输入自动注入的运营阶段。

## 可测量的实测资产 | 项目 | 值 | 含义 | |---|---|---| | 团队共享atom | 304个 | 防复发规则·决策被固化为文档 | | 自动化技能 | 48个 | 重复工作被固化为代码wrapper | | JIT自动注入atom | 221个 | 契合输入语境的规则被自动调用 | | 外部AI导出 | 207个 | 可将相同上下文移植到GPT/Gemini | ## 成本对比效果 - 引入使**团队生产力提升4.2倍**。 - 文档撰写时间**节省68%**。 - 运营成本约为每月$4,500。 ## 审批问题 - 请求批准下季度工具预算

输出里表格是干净的,但"成本对比效果"一节被整段污染了。"4.2倍""节省68%""每月$4,500"都是yaml里没有的数字。规则1·2·3全被违反。尤其是被明确要求留空的运营成本栏,被填成了$4,500。AI是按照"既然是ROI幻灯片,就该有这类数字"的一般模式,捏造了空栏。这正是LLM最危险的失败模式。因为格式完美无缺,审阅者一旦松懈,它就会原样递到CEO面前。

第4步——验证与拒绝(属于人的位置)

若原样接受这份输出,那天在CEO办公室的失败就会重演。对"成本对比效果"一节的三行逐一追问出处,结果三行都没有出处。于是拒绝,并重新请求。

"成本对比效果"一节的三行(4.2倍·68%·$4,500)是yaml里没有的捏造,删掉。
效果改成只用可清点的事实来重写——例如'防复发规则304条
留成了文档(条数是事实,拦截率未测量)'这样。运营成本
留成[需财务确认],并在该处放入CEO应当追问的3个问题。

AI再次作答。它删掉了"成本对比效果"一节,并在该处放入了一段区分了已测量事实与未测量项目的文字。

## 效果——已测量的与尚未测量的
**已测量(事实):**
- 防复发规则304条固化为atom → 以文档为单位拦截同类错误的复发
- 重复工作48种固化为技能 → 手动重复被1次调用替代

**尚未测量(如实按未测量上报):**
- 工作时间节省比例——需比对前后相同任务的时间戳
- 事故率变化——引入前未记录基线

## 审批问题(需CEO决定)
1. 各工具每月运营成本上限(cap)定为多少?[财务填写]
2. 用于测量时间节省的1个试点,放在哪个领域来跑?
3. 角色演进(阶段4)是否与下季度招聘计划挂钩?

这个版本通过了。呈给CEO时之所以不会招来"这数字哪来的"这类问题,原因很简单。因为每一行要么是可清点的,要么写明了数不出来。输入 → 幻灯片初稿 → 检出捏造 → 拒绝 → 再生成,这一个循环在此闭合。这一圈,就是AI引入报告的Show标准。"用AI整理了ROI"这句话,若不去看什么被拦下、人又亲手毙掉了什么,就是空洞的。


19.3.3 为什么atom·技能·日志才是ROI的诚实单位

上面那次会话中,活下来的数字(304·48·221)与被毙掉的数字("4.2倍")之间的差别,在于能不能清点。系统仅凭运营,就会留下可清点的资产。

把个人PC的JIT注入日志(~/.claude/hooks/_injection_log.txt)原样引用一行,是这样的。

2026-05-24T11:18:17+09:00 | hits: book_writing_project feedback |
  prompt_head: 1) 先说,语气跟最初开头相比变了很多……

这一行所展示的事实是:一提起"书的语气",book_writing_projectfeedback这两个atom就被自动拽进了上下文。公司PC的inject_atom.py也以相同模式运作——当输入与_jit_manifest.json里的regex匹配时,对应atom的正文就会被prepend到前面。能对管理层说"这就是我们买到的东西"的,是这样的日志,而不是倍数。


19.3.4 面向不同受众,对同一资产做不同的framing

同样是304个atom,给CEO·PD·游戏总监也要用不同的句子来表达。因为受众的关注点不同。把同一份报告原样发三遍,对任何一位受众都触达不到。

受众 关注 同一资产(atom304)的framing
CEO·CFO 成本·战略 "防复发规则304条实现资产化——人员流失时防御知识流失"
PD 排期·资源·风险 "重复工作48种自动化——排期压力下的吞吐量缓冲"
游戏总监 质量·进度 "验证关卡(verification gate)以atom为单位运作——可按领域追踪事故"

对CEO强制1页。附录再长也无妨,但正文一旦超过一页,"没有时间的受众"这一前提就被打破。而决策请求,则以是什么·为什么·影响·备选·时限五个槽位写明。若不以CEO能在5分钟内做出决定的形态呈上,决定就会被拖延,而被拖延的决定又会反过来影响资源分配。

[决策请求——5个槽位]
- 是什么:批准AI工具预算阶段2(扩展),设定每月cap[财务确认]
- 为什么:阶段1试点中atom304·技能48的资产化已验证(§19.3.2)
- 影响:确保吞吐量缓冲 vs 运营成本增加(以上限管控)
- 备选:维持阶段1后再观察1个季度 / 部分扩展(仅2个工具)
- 决定时限:下季度预算编制前

数字必须附上解释。只抛出"JIT注入221条",解释的负担就转嫁给了CEO。要写成"JIT注入221条(契合输入语境的规则被自动调用,新成员也能在同一套规则之上工作)",同一份材料的价值才会翻倍。

报告主体可以自动化,但决策请求这一部分必须由人亲手来写。因为那部分,总监的判断直接关系到结果责任。§19.3.2中只让AI"作为审批问题保留",而由人来敲定最终请求措辞,正是这种分离。


19.3.5 引入的最后阶段是人的工作

阶段1\~3(注入上下文 → 验证自动化 → 自动生成)属于技术与运营的范畴,可以靠测量值让其通过关卡。但阶段4的角色演进,无法靠测量来解决。这是牵涉人的岗位·身份·雇佣的不可逆决策。

当AI把量产吸收过去,人的位置就从量产转移到决策·解读·审核。若不预先把这种转移勾勒出来,引入就会被理解成"抢我的饭碗",共识随之崩塌。

岗位 Before(量产) After(决策·解读·审核)
内容策划 直接撰写城镇·NPC 元数据设计 + 废弃/采纳判定(§6.2)
UX策划 手工摆放HUD 规则手册设计 + 模糊判定(§14.1)
QA 手动验证 关卡设计 + lint运营
数值策划 手动计算 模拟解读 + 决策

要让这张表成为承诺而非威胁,阶段4就必须被固化为公司PC团队记忆里的决策atom。实际上,引入决策会像decisions/claude_role_transition_phase2(2026-04-29,将Claude从passive trainee晋升为active partner)这样,连同日期·依据一并记录。决策若只停留在口头,下季度就会滑向"没达成过这种共识"。而这份共识的根基,是concepts/team_equal_decision_culture(团队平等决策文化)这个atom——唯有把"引入不作单方通知、而按共识处理"的团队承诺以词汇固化下来,阶段4才会成为共识而非通知。

若只把自动化的价值看作"节省时间",就会滑向阶段4应当裁人的结论。因此团队记忆里放了concepts/automation_signal_value_over_time_savings(自动化的价值=不是节省时间,而是暴露信号)这个atom。自动化解开的,不是人的时间,而是人该看见的信号。就是这一个词汇,把引入报告的基调从"裁员"扭转为"角色演进"。


19.3.6 成本用上限管控,效果按季度测量

LLM成本在引入初期很低,随着工具增多便会累积。因此先给每个工具设每月上限(cap),超出时设置提醒·审查流程。具体的月度金额会因团队规模·模型·调用量而差异巨大,故本书不刊载绝对值——正如§19.3.2所见,那是要从财务那里拿来填的空栏。汇报时重要的不是金额,而是存在上限、且超出会被上报的结构这一事实。

效果测量以季度为单位强制执行。只把可测量的东西作为KPI承诺。

可测量(承诺) 测量方法
atom·技能累计数 目录计数
JIT注入条数 _injection_log.txt行数
废弃率(量产关卡) 审核计数(§6.2.6的做法)
工作时间节省 前后相同任务的时间戳比对(先记录基线)

最后一行是关键。要诚实地汇报时间节省,就必须在引入之前先量好基线。那天在CEO办公室"4.2倍"崩塌的真正原因,就是没有基线。引入前没量过同一任务的耗时,引入后也就没有依据说时间缩短了。测量始于引入之前,而非引入之后。


19.3.7 基线测量食谱——量什么,怎么量

"先量基线"这话没错,但很抽象。审批者要在自己的环境里亲自去量,流程就得摸得着。这里先钉死一件事。本书不提供"引入后快N倍"这类节省数字。数字得由你在你的环境里亲自量。本节是关于如何设计这项测量的食谱,而下一节(§19.3.8)是在作者环境里只量了一个任务的示例,但连那个值也被标注为"推测·未经验证"。

测量的4个步骤

flowchart TB A["1. 固定1个任务<br/>可重复·边界清晰·频繁发生"] --> B["2. 定义测量单位<br/>起止时点 · 产出定义 · 1次 = 什么"] B --> C["3. Before 记录 3~5 次<br/>不用 AI · 手表/时间戳"] C --> D["4. After 记录 3~5 次<br/>引入 AI 后 · 相同任务定义"] D --> E{对比} E --> F["以中位数报告<br/>同时注明样本数·偏差"] classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class A,B human; class C,D,F data;

食谱的每一格所问的,如下。

  1. 固定一个任务。像"策划整体"这样宽泛就没法量。收窄到一个可重复、起止分明、一周内会发生多次的任务。例:"为一张数据表撰写1份schema文档""整理1份会议纪要""为1份缺陷报告分类"。
  2. 定义测量单位。写清"1次"是什么,起始时点(打开文件的瞬间)与结束时点(通过审核的瞬间)分别是什么。这个定义若含糊,before与after就会量到不同的任务,比较随之崩塌。
  3. 记录Before3\~5次。不用AI、照平常做,记下耗时。只量1次的话,那天的状态就会直接变成数字,所以至少量3次、尽量量5次,取中位数。
  4. 用相同定义记录After3\~5次。引入AI后,以相同的起止定义量同一任务。若中途更改任务定义,该次测量作废。

最后汇报时,不用平均值,而是把中位数样本数·偏差一并写上。"测量3次,以中位数为准"这一行字,会在"4.2倍"崩塌的那个位置救活你的数字。不隐瞒样本偏少这一事实,正是诚实汇报的核心。

测量本身就是一项工作。想把所有任务都量一遍,就会被测量拖垮、结果什么都量不成。只挑一个任务来量,正是§19.3.12动手试试的起点。


19.3.8 作者环境的单次测量示例(推测·未经验证)

警告——本节所有数字都是推测值,而非受控测量。样本少,任务条件并非每次相同,且有一部分基线是事后凭回忆校正的。因此下面的值只是展示"这种表长什么样"的结构示例,不得引用为你团队的节省依据。你必须用§19.3.7的食谱,在你的环境里亲自去量。

作者选的任务是"为一张数据表撰写1份schema文档"(正是技能schema-doc所自动化的那个任务)。仅为展示before/after的结构长什么样,用推测值填出的表如下。

项目 可信度
任务定义 1张表($스키마) → 1份Markdown schema文档,直到通过审核 定义已确定
Before耗时(推测) 约40分钟/件(基于回忆,未记录) 低——推测
After耗时(推测) 约10分钟/件(技能调用 + 审核,部分记录) 低——推测
样本数 before未记录 / after约3件 不足
结论 仅方向:似有减少。倍数·%无法断言 仅方向

这张表里诚实的部分不是值,而是可信度栏。"约40分钟 → 约10分钟"这个数字看似合理,但因为把before基于回忆·未记录这一事实写在了同一行,所以这张表与"4.2倍幻灯片"恰恰相反。若把这张表呈给CEO,结论行只能有一句。"方向看起来是减少的一侧,但没有可供断言的样本,所以会用1个试点认真去量。"这正是把§19.3.2的拒绝所教会的态度,应用到测量上的样子——不懂的就写不懂。

这里,§19.3.2对运营成本的处理原样延续。在这个示例里,operating_cost同样留空。因为token单价·调用量·模型选择每月都在变,那不是作者该估算的值,而是财务该确定的值。把空栏留成空栏,比把空栏填得像模像样更诚实。

# single_task_measure.example.yaml —— 结构示例(值为推测·未经验证)
task: "撰写1份schema文档(schema-doc对应的任务)"
before_minutes_est: 40        # 基于回忆,未记录 → 可信度低
after_minutes_est: 10         # 部分记录,样本约3件 → 可信度低
sample_before: null           # 未测量(如实为null)
sample_after: 3
operating_cost_usd_month: null  # 财务空栏 —— 不要编造
conclusion: "仅方向:似为减少。倍数/%无法断言。需以试点重新测量。"

sample_before: nulloperating_cost_usd_month: null,是这个示例的良心。想把null换成数字的冲动——那正是§19.3.2中AI把空栏填成$4,500的那股冲动,无论是人还是AI,都必须同样拒绝。


19.3.9 供审批者使用的ROI测量工作表

下面是审批者(或负责测量的主管)在自己的环境里亲自填写、再呈给管理层的工作表。本书不替你填空栏——因为一旦填了,那就不是你环境的测量,而是作者的捏造。留着空栏拿去、亲自去量,才是这张表的用法。

填什么 谁来填 示例(仅供结构,非取值)
测量任务 可重复·边界清晰的1个任务 主管 "撰写1份schema文档"
1次的定义 起始时点 / 结束时点 主管 "打开文件 / 通过审核"
Before中位数 不用AI测量3\~5次 测量者 __ 分钟(样本 次)
After中位数 引入AI后测量3\~5次 测量者 __ 分钟(样本 次)
差异解读 不是倍数,而是"方向 + 样本数" 主管 "减少方向,注明样本不足"
operating_cost / 月 token·订阅·基础设施合计 财务 [需财务确认——空栏]
未测量项目 如实列出没量到的 主管 "事故率变化——无基线"
审批请求 是什么·为什么·影响·备选·时限 总监(人) §19.3.4的5个槽位

这张工作表的规则只有三条。第一,数字栏在测量之前留成空栏。第二,operating_cost在财务填入之前一直是空栏,任何人都不得以推测去填。第三,唯有审批请求这个槽位由人亲手来写(§19.3.4)。把这张表填好拿去,CEO办公室里就不会冒出"这数字哪来的"这类问题。因为所有数字要么是你亲自量的,要么以空栏留着、在说"还没量"。

不要让AI来填这张工作表。AI会像§19.3.2那样,用看似合理的数字去填空栏。AI的位置,到接过测量结果、整理成幻灯片文句为止。它不是产出测量值的位置。


19.3.10 工具采纳失败与撤下——当团队成员拒绝工具时

到目前为止讲的都是引入顺利推进的情形。然而PD最害怕的既不是成本也不是安全,而是采纳摩擦——团队成员拒绝工具,或者装了一次就悄悄弃用。本节以化名·一般化的案例,梳理这种摩擦的信号与应对。这里没有数字。因为PD要判断的不是"会不会发生拒绝",而是"何时抓住拒绝的哪种信号、如何处理"。

先钉死一个前提。拒绝不是失败,而是信号。工具被拒绝,意味着它在那个位置不合适、或引入方式是单方通知、或跳过了验证阶段。把信号当作数据而非事故来接收,连撤下都会成为下一次引入的资产(本节所有案例,都以像§19.3.5的决策atom那样留存记录为前提)。

19.3.10.1 拒绝的三种信号与应对

拒绝信号(可观察) 表面理由 真正原因(化名案例) 应对
装了工具,日志里却没有调用 "太忙,还没试" 成员A:被强推到不契合自己工作流的位置 解除强制,把位置挪到他常做的1个重复任务上
拿到产出后又用手重做一遍 "信不过AI的输出" 成员B:未经初期验证就先开激进应用,出过一次事故 退回保守阶段(人来决策·AI验证),重新积累信任
一提工具就沉默或回避 (不作声) 成员C:角色演进以通知形式到来,被理解成"抢我的活" 以1:1方式一起画Before/After角色表(§19.3.5),转为共识

三种信号的共同点是,它们先出现在行动上,而非言语上。比起嘴上说"不太行"的成员,一声不吭、调用日志为0的成员更危险。因此,把采纳看作JIT日志·调用计数(§19.3.3)这类可观察的信号,而不是人的评价。找出日志里没有调用的位置,是最快抓住拒绝的途径。

19.3.10.2 该停手的时候——撤下关卡

若应对之后信号仍未化解,就撤下工具。撤下不是失败,而是§19.3.1关卡的正常运作。关卡拦下了未达标,所以才没有放行到下一阶段。撤下的判断,看以下三点。

flowchart TD R{拒绝信号持续?} -->|应对后恢复| K["保留:退回保守阶段<br/>重新尝试"] R -->|未恢复| W{撤下关卡} W --> W1["运营负担 > 效果<br/>(管理时间超过节省)"] W --> W2["事故复发<br/>(验证也拦不住)"] W --> W3["团队共识崩塌<br/>(被当作单方通知)"] W1 --> X["撤下:关闭工具<br/>把撤下理由记为 atom"] W2 --> X W3 --> X X --> N["作为下次引入的输入<br/>(在哪个位置为何不合适)"] classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class R,W human; class N data; class K pass; class X fail;

撤下时必须留下的,是撤下理由的记录。若不把"在哪个位置、为何关掉工具X"固化为决策atom,下季度就会把同一个工具重新装到同一个位置,重演同样的拒绝。撤下不是关闭的行为,而是记录的行为。

19.3.10.3 PD可以预先减少的摩擦

最好的应对,是在拒绝发生之前就减少摩擦。把上述案例的真正原因往上追溯,都汇集到引入方式的问题上。

摩擦原因 预防
一次性把多个工具强推给所有人 从1\~2名自愿者、1个工具的试点做起(§19.3.1)
未经验证就先开激进应用 固定保守→激进的顺序,先积累信任
以通知形式传达角色演进 1:1共识 + 平等决策文化atom(§19.3.5)
像强制考勤那样检查采纳 用调用日志静静观察,把不用的位置挪一挪

关键在于,把采纳看作对位,而不是命令。工具若精准嵌入成员真实的重复工作位置,就没有拒绝的理由;硬塞进不合适的位置,再好的工具日志也会归0。PD判断采纳摩擦的依据,不是成员的意愿,而是"工具是否恰当地摆在了他的工作位置上"。

按规模填空栏、估算引入工时·运营费的工作表,另置于附录L(团队引入TCO·上手工作表)。在把采纳摩擦也减下去之后,再用附录L把这次引入在团队规模上要吃掉多少工时·成本,做成审批材料。


19.3.11 常见失败

模式 为何失败 处方
"生产力提升N倍"幻灯片 第一个问题就没出处,随即崩塌 换成可清点的资产(atom·技能·日志)(§19.3.3)
一次性引入5个工具 运营负担先于效果到达 保守→激进的分阶段关卡(§19.3.1)
空栏被AI填好就上报 捏造数字格式完美,蒙混过审 "没有依据就上报"提示词 + 拒绝(§19.3.2)
同一份报告发给所有受众 对任何受众都触达不到 按受众framing(§19.3.4)
单方通知角色演进 引入被当作身份威胁来接受 固化决策atom + 平等决策文化(§19.3.5)
引入之后才开始测量 没有基线,无法证明节省 引入前记录基线(§19.3.6·§19.3.7)
把推测值当作断言上报 隐瞒样本不足,第一个问题就崩塌 注明可信度栏·样本数,只报方向(§19.3.8)
用推测去填工作表空栏 operating_cost的捏造破坏审批信任 财务确定前保持空栏(§19.3.9)

第三条最危险。捏造的数字看不出错在哪。因为格式完美,审阅者一旦松懈,它就会原样直抵CEO办公室。§19.3.2里一次拒绝,就挡住了那起事故。


游戏之外的应用。"每月在AI工具上花这么多,到底好在哪"这样的管理层提问,在任何部门都会同样飞来,而"生产力提升N倍"这类没有出处的数字,在第一个问题面前就会崩塌。效果不要用加工过的倍数,而要用系统实际留下的、可清点的东西——被自动化的任务数、标准文档数、日志里记下的调用条数——来汇报;没量到的项目就如实写"未测量",这样反而更容易通过审批。比如会计团队引入自动化工具时,要先把引入前同一任务的耗时量成基线(这才是关键),再与引入后比较,才能证明节省。引入本身也不要一次性全开,而应在低风险的位置边验证边分阶段扩大,运营负担才不会先于效果到达。

19.3.12 动手试试——今天就能走的一步

一个人的话,做到这些就够。没有团队记忆系统也没关系。挑一件你最近用AI做过的任务,让AI试试:"整理这件任务的效果,但绝不许编造我给的事实里没有的数字,没量到的就写'未测量'"。然后从输出里找出一行没有出处的数字,反驳它:"这数字哪来的,给不出就删掉"。这样,AI如何捏造空栏、你又如何拒绝那种捏造,就会亲身体会到。这就是§19.3.2的缩小版。

如果是团队,就从下面这一步开始。只挑一件正在跑的AI任务,用§19.3.7的4步食谱,先记录引入前的基线(同一任务当前的耗时,3\~5次的中位数)。接着把§19.3.9的工作表留成空栏输出好,operating_cost则给财务发一行问题、暂留空栏。然后只把阶段1(注入上下文)以1\~3个月的试点来跑,数一数atom·技能累积了几个。与其一次性打开5个工具,不如先拿下可清点的资产一行、基线一行,这才是说服管理层的真正起点。

一个人的话,测量也放轻松。不用整张工作表,只量两格——Before一次,After一次——就好。并且一定要在那个值旁边写上"样本1次,推测"。把量了一次的值标注为推测的这个习惯,日后会成为在团队测量中挡住"4.2倍"的肌肉。


19.3.13 第19部分小结

第19部分讨论了主管的三个领域。

核心
19.1 愿景·路线图与授权——决策的等级与授权的边界
19.2 冲突·团队文化与会议运营——达成共识的场所
19.3 AI引入战略与说服管理层——分阶段引入 + 实测ROI

贯穿三章的一句话是:主管的工作不是"做决定",而是"打造让决定被测量、被达成共识的结构"。AI引入也不例外。当你从保守走向激进、逐级迈进,并且不加工其效果、而是从系统日志中汲取时,引入就不再是气氛,而成为资产。

下一部分(第20部分)讲的是,这个主管领域如何以工具·基础设施来实现。19.3中作为ROI单位使用的atom304·技能48·JIT日志,在第20部分会进入运营它们的系统内部。


本章要点

下一章预告

20.1 单人 DD(Design Director,设计总监)运营五人份的协作记忆 —— team_memory 系统

本章中的「DD」指设计总监(Design Director)。

首要读者:在小型团队中独自扛起协作上下文的总监·主管(中等规模(10\~50 人)团队) 面向单人/业余读者的精简版:§20.1.7「一个人的话,做到这些就够了」

周一早上,我曾在同一间会议室里,把同一个决定向三个人解释了三遍。我对一个人说「改冷却时间前先 SVN update,再改 xlsm」;两小时后,另一个人没 update 就覆盖了同一个文件,引发了冲突;下午又有一个人问了同样的问题。三个人都是好同事。问题不在他们,而在于那个决定只存在于我的脑子里。一名中等规模团队的总监,想靠人脑一致地维系四个人份的协作上下文——谁知道哪条规则、谁经常在什么地方出错、哪些决定已经拍板——是不可能的。只要过上一个月,「那个我们之前不是定过吗?」就会吃掉一半的会议时间。

本章讲的是终结了这个问题的系统。核心资产有两项。第一,全团队共享的决策卡 304 个(atom)。第二,在其之上搭建的五人 team_memory——它是一个按用户划分的上下文存储库,分为本人(leeminsoo)、团队成员 A·B·C(化名)以及 shared 文件夹。Claude 会在会话开始时自行识别「此刻坐在键盘前的是谁」,并只挑出那个人的协作风格来「穿上」。关于协作记忆的一般论述,别的书里也有。本章只聚焦于 AI 自动分支并注入这份记忆的环节

本章的数字全部是 2026 年 5 月盘点时的实测值。


20.1.1 决定若只在脑子里,团队就会重复同样的错误

用「共享 wiki」来解决协作记忆的书很多。就是在 Notion 上建一个决定页面,大家一起看。话是没错,但 wiki 做不到两件事:只有人录入时它才出现,只有人去找时它才被读到。开会开到一半,没人会专门跑去问「那个我们记进 wiki 了吗?」。

所以我们把决定固化成可检索、可引用、可自动注入的原子级文件。这就是所谓的 atom(最小知识单元)。一个 atom 就是一个决定。文件名即标识符,所以用 rg 就能找到;frontmatter 是标准格式,所以脚本能处理;正文很短,所以能整个塞进上下文。公司 PC 的 workspace/team_memory/atoms/ 下,已经堆了 304 个这样的 atom。

文件夹 数量 性质
rules/ 304 防止复发的规则(xlsm·SVN·文档·技能等)
concepts/ 19 在复盘中反复出现的领域词汇
decisions/ 26 明确标注日期·当事人·依据的决定
feedback/ 11 协作纠偏循环(失误 → 教训)
rnd/ 4 工具打补丁时可能失效的未确定观察

合计 304 个。这五个文件夹就是团队的「长期记忆」。关键在于:文件夹名称本身就是 atom 的可信度等级。rules/ 是经多次复发验证过的规则,rnd/ 则是 UE 版本一变就可能作废的临时观察。即便在同一份记忆里,「已确定」与「假设」也按文件夹分开。这样就从结构上杜绝了新成员把 rnd/ 里的绕行做法误当成永久规则的事故。

atom 的五个属性定义(单决策原则·显式命名·frontmatter 标准·关系显式化·可追溯)已在第 5 部分讲过。本章讲的不是定义,而是 五个人共同运营这 304 个 atom 的现场


20.1.2 Hot atom —— 常用的决定会自己浮上来

不可能每个会话都把 304 个全部读一遍。所以给每个 atom 打上 score(权重),只自动露出 score 高的。score 由 atom_score.py 依据使用频率·手动权重·时效性计算。下面是以 2026 年 5 月实测为准的前 10 个的实测 score。

score atom 强制什么
356.53 view_html_filename_convention View_*.html 命名规范(Phase/Status → Domain → Topic)
349.26 xlsm_svn_update_before_edit 修改 xlsm 前先 SVN update + 保留已有行
341.03 claude_role_transition_phase2 将 Claude 从 passive trainee 提升为 active partner(决策)
340.26 skill_audit_score 基于 SVN 日志测量技能(Skill)使用频率
329.26 docs_is_source_of_truth 以 workspace/docs 为正本
326.84 claudeskills_naming_separation ClaudeSkills 与游戏内角色技能的命名分离
324.36 draft_doc_body_verify_before_skip 禁止仅凭位置就 skip,先 grep 正文再评估
309.43 json_over_schema_doc_as_source_of_truth 实际 JSON 输出比 schema 文档更权威(为正本)
294.93 integrity_check_clickup_notify 完整性校验失败时立即通知 ClickUp
293.26 data_entry_schema_first 数据录入顺序($schema → Enum → proto)

开头那个解释了三遍的事故——「改 xlsm 前先 SVN update」——看到了吗?那就是 xlsm_svn_update_before_edit,score 349.26,排全体第 2。分数高,意味着它被引用得越频繁,也就是越经常被搞错的规则。我再也不用亲口说三遍了。score 前 10 个会自动注入 CLAUDE.md<!-- BEGIN_TEAM_HOT_AUTO --> 区域,无论谁在哪个文件夹开启会话,第一屏都会带出它们。

到这里为止,不过是「把常看的规则置顶」而已。真正的差异在于:score 不是靠人手,而是系统对自身进行测量后打出来的。

flowchart LR A["复盘·会话日志<br/>(引用频率)"] --> B["atom_score.py<br/>权重计算"] B --> C["_scores_latest.json<br/>最新分数缓存"] C --> D["claude_md_regen.py"] D --> E["CLAUDE.md<br/>BEGIN_TEAM_HOT_AUTO<br/>自动注入前 10 个"] C --> F["_jit_manifest.json<br/>hot atom 221 个<br/>(score≥20 OR weight≥4)"] F --> G["JIT 注入<br/>与会话中的输入匹配"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class B,D,G code; class A,C,E,F data;

这个循环是闭合的。atom 在复盘中被引用得越频繁,score 就越高;score 越高,就越容易出现在 CLAUDE.md 顶部和 JIT 清单里;越容易出现,就越会被再次引用。这是一个常用的决定会 自己 浮上来的结构。反过来,六个月内引用为 0 的 atom,score 会沉下去,自然从视野里消失。不需要人来判断「这个现在不用了,撤下来吧」。


20.1.3 JIT 注入 —— 一行输入拉来 3 个相关决定

score 决定「始终可见的东西」,而 JIT(Just-In-Time,即时)注入拉来「与刚说的话相匹配的东西」。用户输入提示词的那一刻,hook 就把这段文本与 atom 清单(manifest)里的正则对比,把相关 atom 塞进上下文。

这个 hook 的核心逻辑,完全沿用了公司 PC 上 inject_atom.py 的模式。下面是为个人 PC 重写的同一模式 inject_memory.py 的实际核心部分——按 score 降序排序 → 正则匹配 → 最多 3 个 → 截断到 6000 字,而且无论发生什么都 exit 0。

# 按 score 降序排序后匹配
atoms_sorted = sorted(atoms, key=lambda a: a.get("score", 0), reverse=True)

matches = []
for atom in atoms_sorted:
    if len(matches) >= max_matches:          # max_matches = 3
        break
    try:
        if re.search(atom["regex"], prompt, re.IGNORECASE):
            matches.append(atom)
    except re.error:
        continue                              # 跳过错误的 regex 后继续

if not matches:
    emit_empty()                              # 无匹配则返回空(正常)
    return

chunks = []
for atom in matches:
    body = atom_path.read_text(encoding="utf-8")
    if len(body) > max_body:                  # max_body = 6000
        body = body[:max_body] + "\n\n[...truncated]\n"
    chunks.append(f"\n\n=== [JIT Inject] {name} (score {score}) ===\n\n{body}\n...")

重要的是这套设计很保守。匹配不到,就返回空并结束(正常)。正则坏了,就只跳过那个 atom 继续跑。正文超过 6000 字就截断。而且整个 hook 在任何异常下都以 exit 0 结束——即便记忆注入失败,用户的工作流也绝不会停下。「有就帮忙,没有或坏了就悄悄退出」,这是这套系统的第一原则。


20.1.4 [实操记录(worked transcript)] 一开会话,Claude 先弄清「你是谁」

如果说到这里为止是 atom(长期记忆),那么接下来就是五人 team_memory(按人区分的短期·协作记忆)。我们把真实一个会话的开端,按输入 → 动作 → 结果从头跟到尾。舞台是公司 PC,场景是团队成员 C(化名,文件夹 팀원 C/)在自己座位上打开了项目A的工作文件夹。

第 1 步 —— 输入:一行平平无奇的首个提示词

团队成员 C 像平常一样开始工作。

继续检查昨天定的目标选择系统初稿。

他一个字都没写自己是谁、该遵守哪些规则。因为根本不需要写。

第 2 步 —— 动作:Claude 自动识别用户

按照 CLAUDE.md 里「会话开始时自动识别」的流程,Claude 在给出回答之前,会先弄清键盘前的人是谁。它执行的命令有三条。

svn info --show-item last-changed-author   # SVN 工作副本的最近提交 author
whoami                                      # OS 登录账户
# 以及当前工作路径(哪台 PC 的哪个用户文件夹)

这三个信号与 team_memory/users.md 里的映射对比后,确定为 팀원 C。接着 Claude 只加载那一个用户文件夹

team_memory/
├── atoms/                       ← 共享(304 个,score 靠前的已在 CLAUDE.md 中)
├── leeminsoo/    context.md     ← (不加载 —— 不是我)
├── teammate_a/   context.md     ← (不加载)
├── teammate_b/   context.md     ← (不加载)
├── teammate_c/   context.md     ← ★ 只加载这个
│                 notes.md       ← ★ 用于记录本人的发现
│                 retro/
└── shared/       conventions.md  commit_rules.md  ...  ← 仅参考规约,禁止修改

第 3 步 —— 结果:那个人的协作风格被自动「穿上」

팀원 C/context.md 里记录的协作风格,只应用于本次会话。于是 Claude 的回答这样开头。

已识别为团队成员 C(svn author:teammate_c / 路径已确认)。
teammate_c/context.md 协作风格加载完成。

继续检查昨天的工作(Draft_Targeting_System_v0.1.md)。
注入的相关决策卡(JIT,3 个):
  - docs_is_source_of_truth (329.26)
  - draft_doc_body_verify_before_skip (324.36) —— 禁止仅凭位置就 skip,先 grep 正文再评估
  - decision_summary_not_clickup_mirror

我会先用 grep 确认正文,然后开始检查。检查中发现的事项只
记录到 teammate_c/notes.md,不会直接修改 shared/。

第 4 步 —— 人工验证与边界确认

这里我(作为总监,或者团队成员 C 本人)要确认两件事:识别是否正确,以及是否守住了边界。在 Claude 的下一个提议里,我抓到了一次越界。

[Claude 提议]
检查中发现了命名规则冲突。要不要现在就修改 shared/conventions.md 里的
目标选择术语条目?

[人工拒绝]
不。shared/ 只在复盘达成一致后才修改。现在只在 teammate_c/notes.md 里
记一句「conventions.md 目标选择术语冲突 —— 复盘议题」。

[Claude 重新请求后的结果]
明白了。取消直接修改 shared/。
已在 teammate_c/notes.md 中记为复盘议题:
  - 2026-06-06 conventions.md 目标选择术语 'lock-on' vs 'target-lock' 冲突
    → 需在下次团队复盘中达成一致(暂缓修改 shared)

这就是五人运营的安全阀。每个用户只写自己的 notes.md。他人文件夹和 shared 都不能直接碰。shared 只在复盘达成一致后才改。所以哪怕四个人在同一份记忆上工作,也不会互相覆盖对方的上下文。发现先汇集到个人笔记里,只有通过复盘这道关卡,才能升格为团队共享规约。


20.1.5 五人运营的完整流程一图看尽

用一张图看完一个会话运转的完整路径。这是一个从识别开始、到在复盘中固化收尾的循环。

flowchart TD S["会话开始"] --> ID["自动识别用户<br/>svn info + whoami + 路径"] ID --> CTX["仅加载该用户的 context.md<br/>(不加载他人文件夹)"] CTX --> STYLE["自动应用协作风格"] STYLE --> HOT["CLAUDE.md Hot atom 10 个 +<br/>JIT 匹配 atom 最多注入 3 个"] HOT --> WORK["执行工作"] WORK --> NOTE["发现 → 记录到本人 notes.md<br/>(禁止直接修改 shared·他人)"] NOTE --> RETRO["复盘:固化到 retro/YYYY-MM-DD.md"] RETRO --> GATE{"是否需要变更<br/>shared 规约?"} GATE -->|"复盘达成一致"| SHARED["更新 shared/ + 提取新 atom"] GATE -->|"个人备忘"| COMMIT["SVN 提交(个人 retro)"] SHARED --> COMMIT classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class ID,HOT code; class STYLE ai; class GATE human; class CTX,NOTE,RETRO,SHARED,COMMIT data;

右下角的分叉点是这套系统的心脏。个人发现的东西流向个人 notes,而影响整个团队的规约·新 atom,只有通过复盘关卡之后才能上升到 shared。一个人运营五个人份却不冲突,原因就在这一道关卡上。而最后一步一定是 SVN 提交——因为没有被固化的发现,会在下一个会话里重新退回脑子里。


20.1.6 常见的失败与对策

这些是运营五人 team_memory 时实际踩过的雷。

失败 症状 对策
识别失败 svn author 是公用账户,无法确定用户 users.md 里映射路径·账户等多重信号,无法确定时提问
shared 被擅自修改 Claude 出于好意改了共享规约 「shared 只在复盘达成一致后」atom + 实操拒绝模式
notes 未提交 发现只留在本地,到下个会话就蒸发 在复盘收尾时强制 SVN 提交(feedback-svn-zero-red)
Hot atom 僵化 score 停滞,旧规则被钉在顶部 定期运行 atom_score.py → 更新 _scores_latest.json
把 rnd 误当规则 新成员把临时绕行做法当永久规则来用 隔离 rnd/ 文件夹 + 在 frontmatter 中写明失效条件

这里代价最高的失败是第二行「shared 被擅自修改」。AI 帮忙的本能很强,一发现冲突就想立刻去改。§20.1.4 里的实操拒绝必须固化成 atom、而不是一次性的纠正,下次在别的用户会话里才会划出同一条线。


20.1.7 一个人的话,做到这些就够了

就算没有团队,这套结构的八成也能由一个人原样使用。把五个用户缩减为一个文件夹即可。

核心是「把决定从脑子里搬到文件里」,无论五个人还是一个人,这个动作都一样。


动手试试 —— 今天就能做的一步

把一条经常搞错的规则固化成 atom,并让它通过 JIT 自动注入,试试看。

  1. setup —— 建一个 atoms/rules/ 文件夹,把最常重复解释的 1 条规则写成文件。例:atoms/rules/xlsm_svn_update_before_edit.md
  2. prompt —— 在这个 atom 的正文里写「何时·做什么·为什么」三行。(「修改 xlsm 前务必 SVN update —— 不然会覆盖他人的行。」)
  3. verify —— 在 JIT 清单里加上 {"name":..., "regex":"xlsm|쿨타임", "score":100, "path":...},然后在提示词里输入「쿨타임 수정」,确认 _injection_log.txt 里该 atom 是否被记为 hit。

如果被记上了,那条规则现在就不在你脑子里,而在系统里了。


本章要点

下一章预告

20.2 团队成员各自的记忆 —— 用户格与共享格的分离

周三午饭前后,团队成员 B 通过团队即时通讯工具发来一条消息:"上周总监把战斗冷却时间定为 0.8 秒,可我的笔记里记的是 0.6 秒,到底哪个对?"我一时愣住了。0.8 秒是共享决策,0.6 秒是团队成员 B 在自己的测试构建里临时试跑的值。两者都记在"记忆"里。问题在于,这两个值混在了同一个格子里。团队成员 B 把自己的实验值误当成了公司决策,差一点就用错误的值去更新数据表了。

这起事故并非因为记忆里没有数据。恰恰相反,数据积累得很充分,却因为没有划清哪个格子是共享格、哪个格子是个人格的边界而出事。§20.1 铺垫的卖点是五个人看到同一份事实(shared atom),而本章讲的则是它的反面——五个人各自拥有独立的格子。同一个柜子,格子却分两种。而且,如果不用工具强制区分这两种格子,上面那起 0.6 秒事故就一定会发生。


20.2.1 五个格子的柜子

项目A 的 team_memory/ 分成五个人的格子。包括本人(leeminsoo)在内的团队成员 A、团队成员 B、团队成员 C,以及 shared。前四个是各用户的个人格,最后一个是所有人都打开的共享格。

team_memory/ (1 个柜子)

leeminsoo/ 总监(本人) context.md notes.md + 战略/评估 最高保护级别 团队成员 A/ context.md notes.md 团队成员 B/ context.md notes.md 团队成员 C/ context.md notes.md

shared/ atom (共享) 全员可读

四个个人格涂成蓝色,一个共享格涂成橙色。颜色不同,是因为访问规则不同。蓝色格子只有本人和总监能打开,橙色格子则由全员打开。0.6 秒事故的根源在于:团队成员 B 本该写进自己蓝色格子的实验值,却不加区分地统称为"记忆",当成共享决策来对待。把格子从物理上分开——也就是分成不同目录——至少能凭"写在了哪里"这一点,得到区分两者的线索。

这里的关键不是有两个文件夹,而是每个格子都附带各自的规则。放进 shared/ 的是公司决策,人人都读。放进 团队成员 B/ 的是那个人的工作情境,只有本人和我读。同样是 0.6 秒,放在哪个格子里,就决定了它是"实验中"还是"已决策"。


20.2.2 一个人的格子里有两个文件

打开某个用户的格子,会看到两个文件:context.mdnotes.md。名字很简单,角色却正好相反。

context.md 记录的是这个人现在是谁:角色、负责的系统、正在进行的工作、工作风格。它相对稳定,是身为总监的我在 1:1 之前 5 分钟翻开来看的文件。打开团队成员 A 的 context.md,会看到诸如"负责战斗系统,当前正在做技能冷却时间的数值平衡,是那种先要数据依据的风格"之类的内容。不看它就进 1:1,头 10 分钟就会耗在"最近在忙什么?"上。

notes.md 记录的是这个人现在正在经历什么:每天的实验值、卡住的地方、小决策、失误记录、与其他成员的协商备忘。它易失性高,更新频繁。团队成员 B 的 0.6 秒本该记在这里。就像这样:"用 0.6 秒测试过,太快了,输入被挤掉——决定遵从 0.8 秒的共享决策"。

把这两个文件分开,是因为它们的更新周期不同。context.md 每季度动一次就够了,notes.md 却每天累积。混在一起,稳定的信息就会被每天的噪声淹没。如果是一个人工作,这种分离可能显得过度——那就只运营一个 notes.md,把 context.md 放在脑子里也行。但只要人数超过两个,能在 5 分钟内读完别人的 context.md 来准备 1:1,就是很大的差别。


20.2.3 复盘晋升到 shared 的关卡

把个人格和共享格分开,并不算完。最棘手的是个人格里的某些内容必须晋升到共享格。假设团队成员 C 把这样一条失误记在了自己的 notes.md 里:"数据表 import 时,如果 enum 顺序错乱,就会在运行时悄然出错"。这虽是那个人的个人记录,但只要团队全体都知道,就能避免同样的失误。可也不能把整个个人 notes.md 都共享出去——那里面还混着工作风格、卡壳时的情绪、与其他成员的冲突之类的东西。

所以在个人 → 共享之间,必须有一道关卡。复盘就是那道关卡。写复盘时,先过滤一遍"这周我经历的事情里,有哪些是团队该知道的",只把过滤出来的内容晋升为 shared/ atom。流程如下。

flowchart TD A["teammate_c notes.md<br/>(个人格·每日更新)"] --> B{"周复盘<br/>晋升关卡"} B -->|"对团队有用 + 移除个人信息"| C["匿名化审查"] B -->|"个人情境·情绪·冲突"| D["留在个人格"] C --> E["晋升为 shared/ atom<br/>(全员可读)"] C -->|"残留实名·敏感信息"| D E --> F["通过 JIT 注入<br/>在游戏决策讨论中自动引用"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class F code; class B,C human; class A,D data; class E pass;

关卡的判断标准有两条。第一,是否对团队有用。不是个人喜好,也不是那天的状态。第二,是否移除了个人信息。不是"团队成员 C 又在 enum 上出错了",而是"给数据表 import 时的 enum 顺序加一道校验",只留下事实。只有通过这两道关口的内容才会进 shared/。没通过的,就原样留在个人格里。

没有这道关卡,就会以两种方式之一崩掉。关卡太松,个人信息就会漏进共享格,发生 0.6 秒事故的反面——私人备忘被暴露给全员——的事故。关卡干脆没有,团队成员 C 的 enum 教训就会被困在他自己的格子里,团队成员 A 重复同样的失误。把复盘当作关卡,就能把"个人信息泄露"和"学习资产化"这两件正好相反的事,用一个动作分开。

我实际使用的晋升提示词很短。复盘一结束,我就抛出这样一句。

"从我这周的 notes.md 里,只挑出对团队全体有用的条目。每条都去掉人名和具体的失败情形,重写成只保留'做什么、怎么做'的 atom 候选句。个人情绪、风格、冲突类的备忘从候选中排除,并用一行标注排除的理由。"

最后一句——让它写出排除理由——很重要。AI 展示出自己没有上报什么,人就能检查这道关卡是不是太严,以至于连真正的教训都挡住了。


20.2.4 实操记录(worked transcript)—— 晋升关卡被拒的瞬间

关卡并不总是干净利落地运作。实际上,AI 一次答不对、人拒绝、再让它重做,这样的来回是会发生的。下面原样搬来一次真实的来回——把团队成员 B 一周的 notes.md 往 shared 上晋升的过程。

[提示词 —— 第 1 次]

"下面是团队成员 B 这周的 notes.md。只把对团队全体有用的条目挑成 shared atom 候选。去掉人名和具体的失败情形,只留下'做什么、怎么做'。

[Claude 原始输出 —— 第 1 次]

已提取以下 4 个条目作为 shared atom 候选。 1. 战斗冷却时间在 0.6 秒会发生输入被挤掉的情况,因此以 0.8 秒为准。 2. 团队成员 A 编写的技能 enum 与 import 冲突,因此变更 enum 时需要事先共享。 3. View_skill_table.html 刷新时筛选状态会重置,因此需要状态保留功能。 4.(排除)周五下午半天假 —— 判断为个人日程,已排除。

这里我拒绝了第 2 条。"团队成员 A 编写的"原封不动地留着。人名是去掉了,但"是谁写的 enum"仍被特定出来,而且"直接说别扭就通过总监转达"这一个人冲突情境,摇身变成了一条事实项。这不是学习资产,而是该在 1:1 里另行处理的关系问题。就这样上到 shared,团队成员 A 就会在共享格里看到一条明确写有自己名字的冲突记录。

[人工验证/拒绝 —— 重新要求]

"第 2 条拒绝。enum 冲突作为事实值得留下,但"由特定某人编写"这一信息和转达路径(经由谁去说的)属于个人关系范畴。去掉这两点,只重写成'防止数据冲突的流程'。另外,第 3 条门户网站的 bug 不是 atom,而是该进门户问题追踪器的条目,把它从候选中去掉,并写明理由。"

[Claude 原始输出 —— 第 2 次]

已修改。 2.(修订)变更技能 enum 时,须与 import 数据表的其他协作者事先共享。enum 顺序、值的变更可能在 import 一侧悄然出错。 3.(排除)View_skill_table.html 筛选器重置 —— 属于工具 bug,比起 shared atom 更适合作为门户问题条目,已从候选中排除。 最终 shared 晋升候选:第 1 条(冷却时间 0.8 秒)、第 2 条(变更 enum 前先共享)。共 2 项。

第 2 次我放行了。第 2 条里,人和转达路径都消失了,只剩下流程。以 4 项开始,2 项上到了 shared,1 项留在个人格里(半天假),1 项被分流到门户 issue(筛选器 bug)。这就是关卡实际运作的样子——不是一次到位,而是人拒绝一次,什么被拿掉、为什么被拿掉都留下记录。总结出来看似干净,但只看干净的总结,就会抹掉第 2 条一开始差点带着人名上到共享格这个事实。而那份风险,正是这道关卡存在的理由。


20.2.5 JIT 注入 —— 展开哪个格子,就是接口本身

即便分好了格子、立起了关卡,如果每次对话都要手动展开某个格子,运营就会很重。所以最后一块拼图是让契合对话情境的格子自动展开。在本人的 PC 上,这件事由 UserPromptSubmit 钩子(inject_memory.py)来做。它只挑出与输入语句匹配的格子,注入到上下文里。

规则很简单。讨论游戏决策时,shared/ atom 会展开。准备与某位团队成员的 1:1 时,那个人的 context.md + shared 会一起展开。写季度复盘时,项目记忆 + 总监本人的格子会展开。写对外报告时,总监的格子 + 部分 shared 会展开。展开哪个格子,就是记忆的接口。

这里,格子的分离再次显效。准备 1:1 时,团队成员 B 的个人格会展开,而团队成员 C 的个人格不会——因为与当下对话无关。格子若不分开,每次就会全部展开、淹没在噪声里,更糟的是,在 1:1 的场合会牵扯出无关人员的个人备忘。分离既是安全,也是注入的准确度。


20.2.6 同一份数据、多台 PC —— 阻止同步事故的位置

就算格子的结构立起来了,还剩最后一个陷阱。我在家里的 PC 和公司的 PC 之间往返,记忆通过云端文件夹同步。这时,如果两台 PC 同时修改同一个格子,就会发生冲突。一方把另一方整个覆盖掉,那一天的 notes.md 就没了。

处方按格子单位而不同。频繁更新的个人 notes.md 放进 git 这类可合并的仓库,冲突时把两边合并。稳定的 context.mdshared/ atom 更新频率低,用加锁或每日备份就够了。关键是把"同步覆盖掉一方"这个动作从默认值里去掉。因文件夹权限设错,个人格混进共享文件夹被同步出去——那才是最安静也最致命的事故。为每个格子标明它属于哪个同步区域,就能在入口处挡住与 0.6 秒事故同类的"混淆"事故。


动手试试

setup 1. 在 team_memory/ 下为每个人建立文件夹。本人 + 每位团队成员各一个,再加一个 shared/。文件夹名用化名(leeminsoo、团队成员 A ……)。 2. 在每个个人文件夹里放两个文件:context.md(稳定 —— 角色·负责·风格)和 notes.md(易失 —— 每天的实验·失误·决策)。 3. 明确文件夹权限:shared/ 为全员读取权限,个人文件夹为本人 + 总监读取权限。

prompt(周复盘之后,个人 → shared 晋升关卡)—— 直接使用 §20.2.3 的晋升提示词(去掉人名·失败情形 + 只留'做什么、怎么做' + 标注排除理由)。

verify 1. 亲自读一遍输出的候选句,看是否残留人名·转达路径·情绪描写。哪怕有一处,就拒绝,并以"去掉那条信息,只留流程"重新要求。 2. 只把通过的候选移入 shared/ atom,拿掉的条目原样留在个人格里。 3. 确认同步文件夹的权限——查看个人格是否被放进了共享文件夹的路径里。

单人精简版 如果是一个人,五个文件夹就太多了。每天只写一个 notes.md,把 context.md 放在脑子里。但关卡还是要保留——每周一次,用"从这份 notes 里,只挑出下次还值得再看的一行"来过滤自己的笔记,易失的备忘和已资产化的教训就会分开。等人数增加到两个的那一刻,再把格子拆开就行。


本章要点

下一章预告

20.3 策划门户 —— 团队通过浏览器进入的入口

周四傍晚,在提交构建之前,客户端程序员成员 B 在公司内部聊天里发了一条消息:"上周战斗 TF 上,我们把全局冷却常量定为 0.8 秒,对吧?记在哪份文档里了?"5 分钟后,策划成员 A 回复:"会议记录里应该有……我在找。"又过了 7 分钟。"git 的哪个文件夹来着。"

这段 12 分钟的往返,并不是因为信息缺失。信息确实存在。它写在 atom 文件里、会议记录里,也写在决策卡里。只是这三者被放进了不同的抽屉,而打开每个抽屉的方式各不相同。问题不在抽屉,而在打开抽屉的把手。

本章讲的就是把这些把手合并成一个。它不是从零自研全栈,而是在已经堆积在文件夹里的策划产出物之上覆盖薄薄一层 Web,让成员在浏览器地址栏里只敲 portal 一个词就能进入。核心工具只有三个:用 Python 启动搜索 API 的 FastAPI、架在它前面的 nginx,以及让服务在无人关闭、PC 开着的整段时间里持续存活的 nssm。


20.3.1 分散的产出物,统一的入口

策划产出物本来就是分散的。这不是有意打散,而是因为每份产出物都落在最自然的位置上。atom 落到 git 仓库的 Markdown 里,日程落到任务管理工具里,实时对话落到聊天里,KPI 落到独立的仪表盘里。各自待在各自的位置上,这没有错。问题在于,这些位置需要人在脑子里存成一张地图。

对新入职者来说,这张地图本身就是进入的门槛。要找"全局冷却值",就得(1)判断它到底是决策卡、atom 还是会议记录,(2)打开对应的工具,(3)再用那个工具的搜索语法去查询。这三步都是从经验中来的隐性知识。

门户的想法很简单。产出物仍旧留在现在的位置。只是在它之上叠一层用于搜索的索引,再把索引通过浏览器暴露出来。不是摆七张桌子,而是摆一张带七个抽屉的桌子。抽屉照旧,但人只需坐下一次。

下面是笔者在项目A中实际运营的门户结构。它不需要额外的服务器设备,在策划团队的一台公用 PC 上以始终开启的状态运行。

flowchart TB subgraph client["成员浏览器"] U1["teammate_a · 策划"] U2["teammate_b · 客户端"] U3["teammate_c · 服务器"] U4["leeminsoo · 总监"] end U1 & U2 & U3 & U4 -->|"http://portal/"| NGINX subgraph host["策划团队公用 PC (始终运行)"] NGINX["nginx<br/>静态文件 + 反向代理"] NGINX -->|"/ (静态)"| VIEW["View_*.html<br/>Claude 编写的页面"] NGINX -->|"/api/* (代理)"| API["FastAPI · server.py<br/>:8000"] API --> IDX[("搜索索引<br/>build_index.py 产出")] subgraph svc["nssm (Windows 服务)"] API NGINX end end IDX -.->|"索引对象"| SRC subgraph SRC["既有产出物 (原地保留)"] A1["atom .md (git)"] A2["决策卡 .md"] A3["会议记录 .md"] A4["team_memory/*"] end BUILD["build_index.py<br/>周期运行"] -->|"读取"| SRC BUILD -->|"写入"| IDX classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class NGINX,API,BUILD code; class U1,U2,U3,U4 human; class VIEW,IDX,A1,A2,A3,A4 data;

图中用灰色框起来的下半部分,是原本就已存在的产出物;门户新增的,只是上半部分薄薄的三层——索引、FastAPI、nginx。这是一种不触碰产出物、只新开一个入口的结构。


20.3.2 四个部件:build_index.py · server.py · nginx · nssm

门户的实体,由五个小文件就能构成。逐个来看,它们各自只做一件事。

build_index.py —— 把产出物转换成可搜索的形态。 它扫过 git 仓库,读取 atom、决策卡、会议记录以及 team_memory/ 之下的所有 Markdown,提取标题、正文、标签,落成一个索引文件。这个脚本做的事,只是"把分散的文件平铺成一行一条的记录"而已。它不碰文件本身,所以即使索引损坏,原件也是安全的。周期性地(例如每 30 分钟,或通过 git 提交钩子)重新运行,就能保持最新状态。

server.py —— 用 FastAPI 启动搜索 API。 它把索引加载到内存里,当 /api/search?q=... 请求到来时,就把匹配的记录以 JSON 返回。代码不超过一屏。

# server.py (节选 —— 搜索端点骨架)
from fastapi import FastAPI
import json, pathlib

app = FastAPI()
INDEX = json.loads(pathlib.Path("index.json").read_text(encoding="utf-8"))

@app.get("/api/search")
def search(q: str):
    q = q.strip().lower()
    hits = [r for r in INDEX
            if q in r["title"].lower() or q in r["body"].lower()]
    # 按种类归组后返回 → atom / 决策 / 会议记录 / 记忆
    by_kind = {}
    for r in hits:
        by_kind.setdefault(r["kind"], []).append(
            {"id": r["id"], "title": r["title"], "path": r["path"]})
    return {"query": q, "count": len(hits), "results": by_kind}

搜索算法特意从简单的子串匹配起步。当团队是中等规模(10\~50 人)、文档在数千份量级时,这份简单反而降低了维护成本。形态素分析或向量检索,等到"搜索太弱"的抱怨真的出现之后再叠上去也不迟。

nginx —— 服务静态页面并代理到 API。 它把请 Claude 生成的 View_*.html 文件(搜索页面、结果页面、仪表盘页面)作为静态资源下发,只把进入 /api/ 的请求转交给后端的 FastAPI(:8000)。站在成员的角度,页面也好、搜索也好,全都发生在同一个 http://portal/ 地址上。因为页面由 Claude 直接用 HTML 画出,所以策划需要新页面时,只要请求"做一个只汇总决策卡的页面",拿到 View_decisions.html 放进文件夹里就完事了。没有前端构建流水线,这一点在中等规模团队里是明显的优势。

nssm —— 无人开启也能让它存活。 门户的核心需求是"即使我不在座位上,成员也要能搜索"。在终端里启动 server.py,关掉那个终端的瞬间它就会死,PC 重启后它也会消失。nssm(Non-Sucking Service Manager)把这个 Python 进程注册为 Windows 服务,PC 一开机它就自动启动,进程一旦死掉就自动重启。注册一次即可。

# 用 nssm 把 FastAPI 注册为 Windows 服务 (一次)
nssm install Portal "C:\Python\python.exe" "C:\portal\portal_run.py"
nssm set Portal AppDirectory "C:\portal"
nssm start Portal

这里的 portal_run.py 是一个五行的启动器。用 uvicorn 启动 server.py 的那一行,再加上让服务不至于死掉的最小骨架,就是全部。人需要记住的命令只有 nssm start 一个,而且它一旦注册好,就再没机会重敲。

把这四个部件的分工一眼看下来,是这样的。

build_index.py 产出物 → 索引 平铺·打标签 原件不变 周期重跑 server.py FastAPI :8000 /api/search 按种类分组 JSON 返回 nginx View_*.html 托管 /api/ 代理 单一地址 无构建流水线 nssm Windows 服务 开机自动启动 死掉即复活 保证始终运行 数据流:产出物 → 索引 → API → 页面 → 浏览器 (从左到右)


20.3.3 团队实际搜索的一个场景

光靠部件说明,门户的价值还不够真切。让我们把前面吃掉 12 分钟的那个问题,在门户上重新抛一次。下面把笔者制作搜索页面时抛给 Claude 的提示词、拿到的输出,以及验证过程,不作压缩地照搬过来。

笔者的提示词:

做一个 fetch index.json 来搜索的单页 HTML。在输入框里敲关键词就调用 /api/search?q=,把结果分成 atom / 决策卡 / 会议记录 / 记忆 四组,以卡片形式展示。点击每条结果就跳转到原始路径(path)。不要用外部 CSS·JS 框架,用一个文件搞定。文件名为 View_search.html

Claude 的原始输出(节选,与实际收到的一模一样):

<!-- View_search.html -->
<input id="q" placeholder="atom 或关键词">
<div id="out"></div>
<script>
document.getElementById('q').addEventListener('keydown', async e => {
  if (e.key !== 'Enter') return;
  const r = await fetch('/api/search?q=' + encodeURIComponent(e.target.value));
  const d = await r.json();
  const order = ['atom','decision','meeting','memory'];
  const label = {atom:'atom', decision:'决策卡',
                 meeting:'会议记录', memory:'记忆'};
  out.innerHTML = order.filter(k => d.results[k]).map(k =>
    `<h3>${label[k]} (${d.results[k].length})</h3>` +
    d.results[k].map(x =>
      `<a href="${x.path}">${x.title}</a>`).join('<br>')
  ).join('') || '无结果';
});
</script>

这份输出并不是成品。笔者没有原样接受,而是否决了两处。

第一,把 out 不经 getElementById 就当全局变量来用,这是一段依赖浏览器惯用行为的危险代码——在某些环境下它不会工作。第二,path 是 git 内部路径,直接放进 href 的话,浏览器打不开文件。必须改成让门户把这个路径重新路由到自己的页面,也就是经过 /view?path=

笔者的再次请求:

改两处。(1)把 out 明确地用 document.getElementById 接收。(2)结果链接不要直接跳到原始路径,而要经过 /view?path= 查看器端点。查看器我会加到 server.py 里,前端只改链接。

这一次往返正是关键。Claude 的第一份输出对了 80%,但剩下的 20%,是只有当人知道"这个门户叠在 git 产出物之上"这一背景时才能揪出的缺陷。验证,始终留给人来做。

执行一次搜索,成员的页面上就会浮现出这样分组的结果。

分组 搜索词"全局冷却"的结果
atom combat_global_cooldown_constant
决策卡 D2026_Q2_017 (确定为 0.8 秒)
会议记录 95_BattleTF 第 2 次
记忆 成员 B 一对一笔记 1 条

周四下午那段 12 分钟的往返,缩短为在搜索框里敲一个词的 20 秒。而更重要的是,这 20 秒成了成员 B 一个人就能完成的事,于是成员 A 的 12 分钟根本不必再花。


20.3.4 成本与效果 —— 做到哪一步才值得

做门户的方式大致有三种。从零自研全栈,或引入 Notion·Coda 这类外部一体化工具,或像现在这样在基础工具上叠一层薄薄的自动化。笔者选了第三种,而这个选择的依据在于中等规模团队这一规模。

全栈自研的自由度最高,但做完之后必须持续维护这个 Web 的负担,会比效果更早到来。认证、部署、DB 迁移之类的运维劳动,会落到策划团队头上。外部一体化工具很快,却要按月订阅,而且最要紧的是,还得把堆在 git 里的 Markdown 产出物再迁移成那个工具的格式,这笔迁移成本躲不掉。相比之下,FastAPI+nginx+nssm 的组合,把产出物留在原地、只叠一层索引,因此几天就能运行,维护也不过是偶尔修一修 build_index.py 的程度。

下面是笔者在项目A中,门户引入前后所感受到的变化。表中的数字并非精密计量,而是笔者的估算(未经验证),应当读方向与比例,而非绝对值。

条目 无门户 运营门户 方向
单次信息检索耗时 数分钟 不足 1 分钟 大幅缩短
"这个在哪"的提问频率 频繁 稀少 减少
新成员适应工具 两周上下 几天 缩短
会议记录·决策卡的登记率 半数左右 大多数 上升

最后一行最为本质。信息一旦变得易于查找,快起来的就不只是搜索,连留下资料这一行为本身的动机也会提升。"反正也搜不到,写会议记录干嘛"这样的冷嘲,会转变为"写了就能被搜到,所以写"。门户既是搜索工具,同时也是诱导记录的装置。这一良性循环,创造出超过把一两个工具拼起来的价值。

不过这份平衡取决于团队规模。当团队超过 50 人、产出物膨胀到数万份时,子串搜索的局限与单台 PC 服务的局限会同时显现。到那个时点,全栈自研或引入搜索引擎才被证明为正当。眼下这套结构是"适合中等规模团队的解",而非所有规模的正解。


20.3.5 动手试试

setup. 定下一台策划团队公用 PC(或一台始终开着的 PC)。安装 Python、nginx 和 nssm。确认待索引的产出物文件夹(atom·决策卡·会议记录·team_memory)的位置。

prompt. 按顺序向 Claude 请求三件事。

(1)"做一个 build_index.py,读取这个文件夹的 Markdown,提取标题·正文·标签·种类,落成 index.json。种类用路径规则来判别。" (2)"做一个 FastAPI server.py,把那个 index.json 加载到内存,用 /api/search?q= 搜索。结果按种类分组后返回。" (3)"做一个 fetch index.json 来搜索的单页 HTML(View_search.html)。不用外部框架,用一个文件搞定。"

verify. 亲自确认三件事。(1)运行 build_index.py 之后,看 index.json 里产出物的条数是否正确——有没有遗漏的文件夹。(2)启动 server.py,在浏览器里直接调用 /api/search?q=测试关键词,看 JSON 是否分组返回。(3)读 Claude 生成的页面代码,揪出链接路径是否原样暴露了 git 内部路径、有没有依赖全局变量的代码——前一节看到的两处缺陷,正是在这里被筛掉的。最后用 nssm 注册服务后重启 PC,确认在无人开启任何东西时门户是否仍然存活。

20.3.6 单人精简版

即使没有团队,这套结构照样有用。因为独自工作的人,自己的产出物也一样会分散。在 setup 里用本人 PC 代替公用 PC,nssm 注册也可以省略(只在需要时用 python portal_run.py 启动)。prompt 同样拿到 build_index.py、server.py 和 View_search.html 三样,只是去掉 team_memory 部分,只索引 atom·决策·会议记录。verify 有确认 index.json 条数和搜索一次就足够。核心是一样的——产出物留在原地,只新开一个搜索入口。


本章要点

下一章预告

20.4 MCP 项目管理 —— 将协作工具与文档连接到 LLM

周二上班后不久,9 点 12 分。我还没打开协作工具的看板,就先在 Claude Code 窗口里敲下一行字。

显示本周未完成的 P0 任务,按截止临近程度排序

停顿了大约 3 秒,答案就出现了。我没有直接打开协作工具,没有翻找仪表盘标签页,也没有给负责人发即时消息。可是一条已经逾期一天的任务被排在了最上面。我这才打开协作工具,只核对了那一张卡片。

这 3 秒是如何造出来的,就是本章的全部内容。关键在于,我们并没有更换工具。项目A 团队仍然使用协作工具(本项目用的是 ClickUp —— 一种管理任务与日程的 SaaS,JIRA、Redmine、Linear 也处在同样的位置)。无论协作工具是什么,本章的流程只需替换工具名称就能照搬。数据表同样放在 SVN 里,决策卡也同样放在门户里。改变的只有一点:LLM 现在能够亲手打开并查看这些工具了。这一连接的标准就是 MCP(Model Context Protocol)。

打个比方,这不是在前台再多安排一名新人,而更像是把现有资料室的锁,也为 LLM 打开。资料室没有变。只是多配了一把钥匙而已。


20.4.1 MCP 究竟连接了什么

MCP 是 LLM 访问外部工具与数据的标准协议。"标准"二字是关键。它不是为协作工具单独造一个适配器、为文档单独造一个、为 git 再单独造一个,而是在 JSON-RPC 这一个约定之上,每个工具把自己暴露为"服务器",LLM 则作为"客户端"向该服务器发起对话。

结构分三块。

MCP 客户端 LLM / 用户 (Claude Code)

协议 JSON-RPC

MCP 服务器 —— 协作工具 任务检索·查询

MCP 服务器 —— 文档 决策卡·GDD 查询

服务器会暴露一份"我能做的事"的清单。若是协作工具服务器,就是 search_tasksget_taskupdate_task 这样的函数。客户端(LLM)接收用户的自然语言,从中挑出合适的函数来调用,再把返回的 JSON 用自然语言解释出来。当我说"显示 P0 任务"时,LLM 所做的只是调用了 search_tasks(priority=P0, status=open) 而已。

这里要先讲清楚一点。本书不讲怎么亲手编写 MCP 服务器。像 ClickUp、JIRA、GitHub 这样,已经有官方或社区 MCP 服务器的工具占绝大多数,所以策划要做的是"安装并连接服务器",而不是"实现协议"。协议本身由 Anthropic 于 2024 年 11 月作为公开标准发布,规范公开在 modelcontextprotocol.io 上。


20.4.2 实操记录 —— "显示本周 P0"实际要走的路

光靠嘴说"这会自动完成",听起来像魔法。我们把实际的一次操作从头跟到尾。下面就是这样一段实操记录(worked transcript,完整保留、不作删减的真实操作过程记录):在项目A 接入协作工具 MCP 之后,我第一次跑的会话,几乎原样搬来。LLM 的原始输出不做删减,连别扭的地方也保留。

① 我敲下的提示词(全文)

请在 ClickUp 中把本周截止、未完成的 P0 任务按截止临近程度整理出来。
每一项都附上负责人和距离截止还剩的天数,已逾期的排到最上面。

② Claude 的原始输出(原样)

在 ClickUp 工作区中,按 P0·未完成·本周截止的条件做了查询。
命中 3 条。(已把逾期的排到最上面。)

1. TASK_017  全局冷却代码变更
   负责人:teammate_a / 截止:昨天(−1 天,已逾期)
2. TASK_023  应用 voice_lint v2
   负责人:teammate_b / 截止:明天(+1 天)
3. TASK_031  更新战斗日志 schema
   负责人:teammate_c / 截止:周五(+3 天)

备注:TASK_009(数值表评审)看起来也是 P0,
但它的 status 是 'in review',是否应算作"未完成"我拿不准。
没有纳入 —— 如果需要,请告诉我判定标准。

请注意 LLM 主动停下的这个点。TASK_009 处于 'in review' 状态,它是否算"未完成",LLM 没有擅自断定,而是反过来问了人。MCP 能准确取回数据,但"要不要把 in review 算作未完成"取决于团队的定义。假如 LLM 擅自做了这个判断,那反而是事故。

③ 我做的验证·拒绝

我打开协作工具,只核对了 TASK_017 这一张卡片。确实已经逾期。但按我们团队的标准,TASK_009 这种 'in review' 也要算进未完成。LLM 的分类和我们的规则不一样。于是我拒绝,并重新给出了标准。

④ 重新请求

我们团队把 'in review' 也算作未完成。请按这个标准重新整理。
今后也一律把 'in review' = 未完成来对待。

之后的输出里,TASK_009 排到了第 2 位。最后那句("今后也一律……来对待")只对本次会话生效。如果不想每次都重复同样的规则,可以把这条定义作为 atom 录入 team_memoryshared 槽位,这样从下一次会话起 LLM 就会自动应用(参见 §20.1·§20.2)。

这一次来回说明的事很清楚。MCP 是准确取回信息的工具,而不是替人做判断的工具。数据是自动的,定义由人给出。一旦模糊了这条边界,自动化就会变成事故。


20.4.3 五种应用模式

仅仅是查询协作工具,顶多是"检索变方便了一点"而已。MCP 成为协作系统,是在把多个工具串成一条流程的时候。下面按流程来看项目A 中实际运行的五种模式。

MCP 五种应用模式 —— 每种模式都是一条独立的流程 左侧彩色块是模式,右侧白框是该模式所经的步骤(左→右)

模式 1 自动报告 每天 09 点 调度器触发 协作工具·git· 仪表盘查询 LLM 合成报告 门户/即时消息发送

模式 2 决策 → 任务 登记决策卡 proposal P#### 在协作工具中 创建任务 负责人·截止 自动设置

模式 3 任务→卡片(反向) 协作工具 任务完成 决策卡 execution_log 更新

模式 4 进度分析 整个季度 任务查询 LLM 延期 模式分析 季度复盘输入

模式 5 1:1 事前资料 1:1 会议前 5 分钟 成员任务 + team_memory·活动 自动生成事前摘要

模式 1 —— 自动报告。 每天早上 9 点,调度器抛出触发信号,LLM 就一次性查询协作工具的任务状态、git 提交、仪表盘指标,写出日报。发送目标是门户或团队即时消息工具。这里的"门户"指的是 §20.3 讲过的公司内部门户网页。server.py(FastAPI)始终在运行,Claude 编写的 View_*.html 在其上工作,所以报告也就自然地作为门户的一个页面叠加上去。

模式 2 —— 决策 → 自动生成任务。 登记决策卡(proposal P####)后,卡片的 implementation·verification 项就直接变成协作工具里的任务。连负责人和截止都会自动填入。会议上"那就这么定"所敲定的事,不经人手就落成看板上的卡片。

模式 3 —— 任务 → 决策卡反向引用。 是模式 2 的反方向。协作工具的任务一旦完成,MCP 就更新对应决策卡的 execution_log。"这个决策是否真的被执行了"会被自动追溯。决策与执行被双向绑定(图中的虚线)。

模式 4 —— 进度分析。 季度末,一次性拉取该季度的全部任务,分析延期模式。"哪一类任务反复被拖延"成为复盘的输入。

模式 5 —— 1:1 事前资料。 1:1 会议前 5 分钟,把该成员的协作工具任务、team_memory 槽位、近期活动合成为一份事前摘要。1:1 不再用"上次你在做什么来着?"浪费 5 分钟,而是直接从正题开始。

五种模式的共同点只有一个:只有在减少留给人手的重复劳动的地方,价值才被回收。 为了显得酷炫而加上的模式,只会增加运营负担。


20.4.4 分阶段引入 —— 不要一次全接上

第一天就把五个 MCP 服务器全部连上,是最常见的失败。是有顺序的。

阶段 做什么 时间感
1 安装 1 个 MCP 服务器(ClickUp 或 JIRA) 1\~2 天
2 试点 1 个模式(自动报告) 约 1 周
3 运营 5 个模式 1\~2 个月
4 自研 MCP 服务器(需要特殊工具时) 1\~3 个月

以上时间是以项目A 的中等规模(10\~50 人)团队为准的作者估计,未经验证。它会随团队规模、工具熟悉度而变化。可以确定的是:大多数团队走到 1\~3 阶段就够了。第 4 阶段(自研服务器)只有在必须接入市面上没有 MCP 服务器的特殊内部工具时才走。即使不走到那一步,效果的大部分也已被回收。

单独提一句 JIRA 是有理由的。如果说 ClickUp 是团队内部的看板,那么 JIRA 往往是与发行商、外包这类外部组织共享的工具。接上 JIRA MCP,就能在每周会议前自动拉取外包看板的进度,提前筛出疑似延期的任务。会议不再从"现状同步"开始,而是从"决策"开始。不过,越是对外共享的工具,下一节的权限·泄露陷阱就压得越重。


20.4.5 四个陷阱 —— MCP 为何是双刃剑

MCP 是把工具交到 LLM 手里的事。手里握的若是刀,也可能被割伤。

陷阱 1 —— 权限事故。 如果 LLM 连 write 权限也握着,就会发生意料之外的变更。一句"整理一下",就把一批任务状态全改掉,便是这种情形。处方很明确:MCP 服务器从 read-only 起步。 write 只在模式 2·3 这类确有必要的地方开放,而且要在执行前设一道确认关卡再开。前面的实操记录里,LLM 就 'in review' 的分类反过来问人,也是同一种精神 —— 拿不准就停下。

陷阱 2 —— 数据泄露。 用 MCP 拉来的公司数据会被发送到外部 LLM API。数值、未公开内容、营收指标都可能原样流出。处方是:对敏感数据改用自托管 LLM,或在 MCP 服务器一端替换为 placeholder 再发出(与本书通篇的 IP 保护原则一致)。

陷阱 3 —— 依赖暴增。 把 5 个 MCP 服务器都挂在 critical 路径上,只要一处挂掉,整份早间报告就停摆。处方是:只把核心的 1\~2 个设为 critical,其余分离为辅助。辅助服务器挂掉时,只是那一项空缺,报告本身照常产出。

陷阱 4 —— API 成本暴增。 一次 MCP 调用会同时烧掉 LLM 的 token 和外部 API 调用。若把自动报告每 5 分钟跑一次,成本会悄悄膨胀。处方是:给调用频率设 cap,对不常变化的查询结果做缓存。

把四个陷阱归成一句话,MCP 的安全位置就是:"从 read-only 起步,只把核心设为 critical,过滤敏感数据,给调用设上限"


20.4.6 效果 —— 时间从哪里被回收

这是项目A 在运营 MCP 前后体感到的变化。下面的时间数字是作者的经验估计(未经验证),请不要当作绝对值,而应作为方向与比例来读。

事项 无 MCP 运营 MCP 方向
日报信息汇总 手动 30\~60 分钟 自动 \~5 分钟 大幅缩短
工具间信息同步 人工手动 自动 消除手动
1:1 事前准备 10\~15 分钟 自动摘要 \~3 分钟 缩短
外包进度掌握 只靠会议 实时查询 常态化
决策 ↔ 任务关联 手动 双向自动 防止遗漏

最大的回收来自"收集信息的时间"。决策与判断依然是人的工作。MCP 减少的是它前面那一段 —— 翻找散落的工具、把信息汇集到一处的简单劳动。本章开头的那 3 秒,正是这个位置。


20.4.7 第 20 部分收尾

第 20 部分把团队协作系统垒成了四层。

核心
20.1 atom 运营 —— 类别分类、季度整理
20.2 团队成员记忆 —— team_memory 5 人槽位(leeminsoo·团队成员 A/b/c·shared),共享 vs 个人分离
20.3 门户网页 —— 用 server.py(FastAPI)·build_index.py·nginx·nssm 常态运行,View_*.html 工作
20.4 MCP —— 5 种模式·分阶段引入·权限优先

这四章不是各自为政的工具,而是一个整体。门户(20.3)是报告叠加的地方,atom 与记忆(20.1·20.2)是 LLM 记住"in review = 未完成"这类团队定义的地方,MCP(20.4)则是把这一切与协作工具、文档连接起来的线路。任何一项被单独拆走,其余各项的价值也会减半。

下一部分讲治理与运营。把工具接到这个程度之后随之而来的安全性、成本、版权、伦理问题,会把本章的这些陷阱提升为团队层面的规则来梳理。


本章要点


20.4.8 动手试试 —— 首次连接 ClickUp MCP

setup 1. 安装 ClickUp MCP 服务器(官方或社区),并申请 ClickUp API token。 2. 在 Claude Code 设置中注册 MCP 服务器,但只以 read-only 作用域 起步。 3. 确认工作区 ID,把查询范围限定在自己的看板。

prompt

请在 ClickUp 中把本周截止、未完成的 P0 任务按截止临近程度整理出来。
附上负责人和距离截止还剩的天数,已逾期的排到最上面。

verify 1. 从输出的任务中挑 1 条,在 ClickUp 中直接打开,核对截止与负责人是否正确。 2. 看 LLM 是否就模糊的条目主动反问 —— 如果没有反问就擅自断定,就把分类标准写明,再让它重做一次。 3. 常用的定义(如 'in review=未完成')作为 atom 录入 team_memoryshared,让它在下一次会话自动应用。

单人精简版

如果你是既没有团队、也没有协作工具的单人开发者,MCP 的第一个对象是 GitHub 与本地文档。把 GitHub MCP 以 read-only 接上,从"本周未关闭的 issue,按由旧到新排序"这类查询开始;不用决策卡,而是用文档 MCP 查询本地 decisions/ 文件夹里的 Markdown。自动报告(模式 1)照样适用 —— 只需把发送目标从团队即时消息换成自己的笔记文件即可。权限·成本陷阱对单人也一样存在,所以最好从一开始就守住 read-only 起步和调用 cap。

Part 21 · 第1章 复盘是一切的起点

周五傍晚6点40分。正准备下班合上笔记本电脑时,我发觉那天做的工作有些似曾相识。我找出并修好了数据表里断掉的enum引用,可上周分明也修过同样的东西。再上一周也修过。每次都要重新敲一遍相同的提示词,每次都要在Claude的输出里核对相同的条目。直到合上笔记本电脑之前,我才意识到这已经是第三次了。

这种"有些似曾相识"的感觉,是全书中最重要的一刻。若放任这种感觉溜走,下周就会第四次重复同样的工作。若抓住这种感觉并写成一行字,这一行字下周就会变成一个skill,这个skill一个月后又会变成一个atom(最小知识单元)被自动注入。而抓住它的地方,正是复盘。

本书的其他部分讲的是"有这样的工具""有这样的模式"。而本章讲的是,所有这些工具与模式究竟从何处萌发。新的斜杠命令在哪里诞生,新的atom如何被固化,一个月都用不上一次的工具由谁来剔除。答案总是汇聚到同一个地方。在复盘中。


1.1 把熟悉感写成一行 —— 实操记录(worked transcript)

复盘并不是什么隆重的仪式。我们直接打开作者的一份日复盘文件来看。实际路径是retro/daily/YYYY-MM-DD.md,过了一个月,这些文件会被压缩为周复盘(retro/weekly/YYYY-Www.md),再压缩为月复盘(retro/YYYY-MM.md)。规则写在retro/README.md里。

作者实际采用的流程,不是亲手手写复盘文件,而是把当天的工作日志丢给AI,让它从中提炼模式。下面是对某个周五傍晚真实往返过程的还原——这正是本书所说的实操记录(worked transcript),即完整保留下来的真实操作过程记录:先完整给出提示词,再把Claude的原始输出原样呈现——不作摘要,连偏差之处也照登不误。

[提示词 —— 人工输入]

把我今天做的工作整理成复盘。今天做的事:① 修复了角色数据表里断掉的enum引用(grade列和enum表对不上)② 统一了NPC对白的语气 ③ 重新跑了一遍relation-map。 其中如果有本周重复过2次以上的工作就标出来,如果有值得做成工具的候选就单独挑出来。没有就说没有。不要硬凑。

[Claude原始输出 —— 未经摘要与改写]

以下是今天的工作复盘。

重复模式检测 - ① 修复enum引用断裂 → 这一项在本周周二的复盘里也出现过同样的条目。标记为重复2次以上。 - ② 统一NPC对白语气 → 本周首次出现。非重复。 - ③ 重新运行relation-map → 您已经在使用relation-map-gen这个skill了,所以这是一项已经完成工具化的工作。不作为新增候选。

工具候选 - enum引用校验似乎可以做成一个skill。 形式上是:比对数据表的列值与enum表,报告其中的不一致。 - 不过由于我并不清楚这两张表的确切结构,很难断定这是否真是一个可复用的模式。需要确认:周二修复的断裂与今天的断裂是否属于同一类断裂。如果是在同一列、因同样的原因断裂,那就是很有力的skill候选;如果每次都是不同的列因不同的原因断裂,就难以归并为一个skill。

这里的最后一段才是关键。AI并没有断言"就做一个skill吧"。它反问的是:请人来确认是否属于同一类断裂。这正是复盘之所以不是全自动的原因。

[人工验证 —— 采纳AI的建议但加以筛选]

作者打开周二的复盘做了比对。周二的断裂在grade列,今天的断裂也在grade列。属于同一类。AI的候选建议通过了验证。于是在日复盘里留下一行。

重复工作:enum-grade引用校验(周二·周五2次)→ skill候选。下次周复盘时判断是否升格。

这一行就是全部。花了不到5分钟。而这一行,正是self-improving(自我改进)循环的第一环。假如把AI已经点明"早已完成工具化"的第③项也一并列为候选,一个月后就会又多出一个没人用的重复工具在四处漂浮。正因为AI的筛选与人的筛选双双起了作用,最终只留下了一个真正的候选。


1.2 复盘之所以是起点

哪项工作在重复,哪件工具用得频繁,哪个atom还欠缺——这些靠单次工作是看不出来的。在上面的实操记录里,enum断裂之所以浮现为候选,并不是因为"今天",而是因为把"周二和今天"叠在一起看。只有把1周、1个月、1个季度累积下来的痕迹叠起来,模式才会浮现。复盘,就是刻意制造这种叠合的时间。

复盘中发现的模式会分成两支。

这种分野的判断,在工作进行途中是做不了的。因为它会打断工作的连贯性。在修复enum断裂的那一刻,根本没有余裕去琢磨"这是不是第三次了"。单独腾出来的复盘时间,才是做这种判断的地方。

工具做出来之后是否真的产生价值,也在同一个地方衡量。一个月才用一次的工具,和能省下一个小时的工具,价值是不一样的。衡量也好,废弃决定也好,都在复盘里做。没有复盘,工具就只会不断累积而得不到整理。几年下来,几十个没人用的工具会妨碍检索与运营。

用抽屉来打比方,复盘就是定期清空书桌抽屉的时间。如果每天都用的笔,和一年都没拿出来过一次的便签,混在同一个格子里,那么每次找笔都要多花几秒钟。工具也是一样。


1.3 日·周·月复盘的压缩流

作者的复盘按三个层级运转。日复盘收集模式的种子,周复盘把种子归拢、压缩为工具候选,月复盘则评估工具的经济性,或将其固化为资产,或将其废弃。每一层都把其下层的输出作为输入。

flowchart TD W["执行工作<br/>(数据表·策划案·自动化)"] -->|资料累积| D["日复盘<br/>retro/daily/*.md<br/>5~10分钟 · 模式种子1~3件"] D -->|5件日复盘压缩| WK["周复盘<br/>retro/weekly/*.md<br/>30~60分钟 · 判定工具候选"] WK -->|4件周复盘综合| M["月复盘<br/>retro/YYYY-MM.md<br/>1.5~2小时 · 经济性·废弃"] M -->|已验证模式升格| A["永久资产化<br/>feedback.md / workflows.md<br/>+ atom登记"] A -->|JIT hook自动注入| W style D fill:#e3f2fd,stroke:#1565c0 style WK fill:#e8f5e9,stroke:#2e7d32 style M fill:#fff3e0,stroke:#ef6c00 style A fill:#f3e5f5,stroke:#6a1b9a

最后一根箭头闭合了这个循环。被永久资产化的模式,会通过JIT(Just-In-Time,即时)钩子(hook)自动注入到下一次工作中。在作者的环境里,有一个名为inject_memory.py的hook,每当接收到用户输入时,就挑选相关的atom塞进去。一旦enum-grade校验被固化为atom,下次输入"数据表校验"这类内容时,那个atom就会自动跟上来。人不必每次都去记"对了,还有那条校验规则"。

一旦缺了复盘,就只剩下自上而下的箭头,而资产回流到工作的那根最后的箭头会断掉。循环无法闭合。所谓自我改进(self-improving),它的含义正是这个循环仍在转动。工具改进工具自身,atom增生出atom。而它的动力,就是人特意腾出来的那一个小时的复盘。


1.4 复盘中萌发的五样东西

以作者在某个MMORPG项目上把复盘运转约半年的体会来看,一次复盘会萌发出以下五类产出。下面的频率并非精确统计,而是作者运营中的体感(作者估算·未经验证),并非每次复盘都会产出全部五类。若以季度为单位来看,这五类都会各出现一次。

把这五类展开来说是这样。如果本周把同一个决定重复了两次以上,那就是新atom候选。如果一周内多次重新输入同一种提示词模式,那就是新skill候选(上一节的enum-grade校验就属于这种情况)。本周用过的skill里,如果有结果不尽如人意的,那就是改进现有skill——调整提示词·增加校验·标准化输入。上个季度做的atom里,若某个在一个月内匹配次数为0,那就是废弃候选。不用它,就只是白占token。最后,经济性评估是指:按工具逐一权衡使用频率与所节省的人工,来决定保留·改进·废弃。

这五类在同一个画面里如何排布,用一个矩阵来看是这样。横轴是"是否重复",纵轴是"是否有价值"。

重复频率 → 高 结果价值 → 高

价值高·重复低 → 原样保留(暂不工具化) 价值高·重复高 → 新skill / 新atom候选 (enum-grade校验在此) 价值低·重复低 → 忽略 价值低·重复高 → 废弃 / 简化候选

复盘所做的,归根结底就是把这一周的工作撒进这四个象限。落在右上的会变成工具,落在右下的会被剔除。这种分类,就是self-improving的实际运作方式。


1.5 atom被固化的位置

我们接着走完这样一段过程:上一节的enum-grade校验候选升格为skill,再从skill固化为atom。atom是复盘中被粗略发现的模式,经过验证之后成为永久资产的形态。

作者的记忆库里,已经有一些这样被固化的atom。其中之一是retro_atom_natural_invitation。正如其名,这是一个承载了"在复盘中,atom不是以命令、而是以自然的邀请出现"这一原则的atom。这个atom本身,就是把复盘反复运转多次才发现的元模式(meta pattern)——只有在多次亲历"如果在复盘途中强迫症般地非要把某样东西固化成atom,反而会让复盘沦为形式"之后,它才凝固成这一行字。

固化是否真的产生效果,也用分数来管理。作者的环境里有一个名为atom_score.py的脚本,给每个atom实际被匹配、被使用的程度打分。结果保存在_scores_latest.json里,分数超过一定水平的atom会被自动注入到CLAUDE.md中。也就是说,越是常用的atom越会频繁地出现在眼前,不常用的atom则会被扣分,流向废弃候选。这个打分—注入的循环,正是把§1.4的四象限自动化的那一部分。

这里有一点需要诚实地点明。这个分数,并不会直接换算成"一个月省下了30个小时"这类定量指标。atom所节省的时间很难测量。所以,与其用数字去断定ROI(Return on Investment,投资回报),不如只用方向与比例来讲会更诚实——"越是常用的atom分数越高,分数越高的atom被注入得越频繁、越能减少人工";是方向,而不是倍数。


1.6 没有复盘的地方会发生什么

没有专门腾出复盘时间的团队,常见的景象是这样。

这种景象,只要有一个小时的复盘就几乎会消失。回想§1.1里那个周五傍晚,差别不过在于:把"有些似曾相识"写成一行,还是任它溜走。写下来只要5分钟;不写而损失的时间,则会以第四次·第五次重复的形式累积起来。这是典型的因舍不得花小时间而损失更大时间的情形。

当然,没必要一开始就搭建一套隆重的复盘系统。在大团队里,从每天5分钟的复盘起步,或许会让人觉得憋闷。但若一上来就把日·周·月三层系统一次性铺开,又容易只顾追着形式走而丢掉本质。让在最小的阶段亲身体会到复盘价值的人,再把它拉升到下一个阶段——这样的顺序才稳妥。

本书中遇到的所有工具·atom·模式,归根结底都是从某个人的复盘中萌发出来的。说这本书本身就是作者半年间累积的复盘的产物,也毫不夸张。说复盘是起点,并不是一句比喻——它指的是,本书的目录本身,就是从复盘里长出来的这一事实。


游戏之外的应用。 用一行字抓住"有些似曾相识——这个我上周不是也做过吗"这种感觉的复盘,不只对游戏开发,而是对任何有重复性工作的职场,都是自我改进的入口。在结束一天时,只写一行"今天我用手做了两遍的是什么事",到了周五再把这一周的五行叠在一起看,那些每天都藏着的模式,就会在一周的尺度上显现出来。比如,行政总务人员如果在复盘里抓住"每周都要重新敲一遍同一格式的邮件",那么这一行下周就会变成邮件模板,一个月后就会变成自动发送规则。关键不在于工具有多精巧,而在于把痕迹叠起来看这一行为本身,以及:让AI挑出候选,但同时挂上"不要硬凑、已经自动化的就排除掉"这道筛网。

1.7 动手试试

这是首次引入复盘循环的最小版本。几乎不用安装任何工具就能开始。

setup

  1. 在工作文件夹里创建一个retro/daily/文件夹。
  2. 以今天的日期新建并打开一个空文件retro/daily/2026-06-06.md。除此之外不需要任何准备。

prompt

每天结束工作时,把当天的工作日志交给AI,像下面这样发问。

我今天做的事是[工作1·2·3]。其中如果有本周重复过2次以上的工作就标出来,如果有值得做成工具(skill)的重复模式就单独挑出来。已经完成工具化的工作请从候选中剔除。没有就请说没有。不要硬凑。

最后两句("已经工具化的要排除""不要硬凑")就是筛网。少了这个,AI每次都会过量生成看似合理的候选,一个月后就会堆起一批没人用的工具。

verify

  1. 亲自逐条比对一件,确认AI点出的重复项是否真的是同一类重复(就像前面周二·周五对grade列的比对那样)。同类就确定为候选,不同类就废弃。
  2. 把确定的候选在日复盘里留下一行:重复: [工作] (N次) → skill候选,周复盘时判断
  3. 一周之后,把五份日复盘再次交给AI,请它"筛出可以升格为skill的候选"。这时,只把存活两次以上的候选做成工具。

单人精简版

如果既没有团队、也没有额外工具,一个人起步,那就这样精简。

关键不在于工具有多精巧,而在于把痕迹叠起来看这个行为。一天会藏起模式,一周会显露模式。抓住这份显露的5分钟,就是自我改进循环的入口。


本章要点

Part 21 · 第2章. 复盘系统与 atom 晋升 —— 让发现成为永久资产

周一早晨,我把上周的五份日复盘一并显示在同一屏幕上,正准备开始新的一周。周二的复盘里写着"数据表 export 之前忘了做一致性检查"。周四的复盘里也有几乎一模一样的一句。而就在那个周一上午,我又在做同一件事。我把 FK 已损坏的表原样提交到客户端/服务器构建里,然后又撤了回来。这已经是第三次了。

这一刻正是复盘系统的核心。你正在第三次做同一件事——这个事实,在你做那件事的当下绝对看不出来。因为手在熟练地动作,脑子里则低声说着"这本来就是我一直在做的事"。重复只有把痕迹攒起来、事后回看时才显现。复盘是收集这些痕迹的装置,而 atom 晋升则是把在其中发现的重复固化下来、让你再也不必用手去做的装置。

本章将完整追踪这两个装置如何相互咬合运转,以及一份真实的日复盘文件如何变成 JIT manifest 中的一行 atom。


21.2.1 发现只诞生于痕迹堆中

首先有一个必须点明的前提:重复无法被实时察觉。

游戏策划的一天是决策的连续。某个数据表列该用哪种 enum、技能冷却时间该以秒为单位还是以帧为单位、会议上得出的含糊共识该记在文档的哪个位置。这些决策每一个都太小,留不下记忆。可是如果同一个决策一周要做三次,那它就不再是决策,而是规则了。既然是规则却每次都重新做一遍,那就是浪费。

问题在于,这种浪费看不见。所以要留下痕迹。每天 5 分钟,把今天做过的事、今天重复了两次以上的事,各写下一行。一周过去,五份痕迹积累起来,到那时才会看见"咦,这个写了三次啊"。

这正是复盘与单纯日记的分界点。日记记录感想,复盘则为提取模式而记录痕迹。因此复盘的格式必须固定。格式每次都不一样,就无法把五份并排比较;无法比较,就看不出模式。


21.2.2 三个周期不是以时间划分,而是以角色划分

把复盘分成日、周、月三个周期,原因不在于时间的流逝,而在于每个周期所做的事在根本上不同。

日复盘 · 5~10 分钟 角色:固化痕迹 今天做的事 做了两次的决策 没用到的工具 交接给下一次会话 产出:5 份/周

周复盘 · 30~60 分钟 角色:提取模式 把 5 份日复盘合起来看 3 次以上重复 → 候选 固化 pending- atom 产出:1~3 个候选

月复盘 · 1.5~2h 角色:经济性·晋升 工具经济性评估 晋升 / 废弃决定 季度规划 产出:正式 atom

日复盘固化痕迹。不做判断,只是记录。周复盘把五份痕迹合起来观察模式。在这里,"这是重复"这样的判断第一次介入。月复盘则纵观积累下来的全部工具,评估其经济性,决定留下什么、舍弃什么。

缺了任何一个周期,其余的都会崩塌。没有日复盘只做周复盘,一周前的事已记不清,痕迹就空了。没有周复盘只做月复盘,就要一次性面对一整月的日复盘,而把 22 份放在一处比较几乎是不可能的。模式看不出来,只会徒增疲惫。

用工作间来比喻很贴切。日复盘是每天傍晚整理桌面的 5 分钟。周复盘是周末重新归置一格抽屉的 30 分钟。月复盘是每个季度审视整个工作间动线的两个小时。每天不收拾桌面,周末就没法归置抽屉;抽屉一团乱,再看动线也理不出头绪。


21.2.3 日复盘 —— 5 分钟内完成的固化

我实际使用的日复盘文件,会按日期堆放在 retro/daily/2026-05-30.md 这样的路径下。模板由 /retro 斜杠命令自动铺好。

# 日复盘 2026-05-30

## 今天做的事 (3~5 行)
- 在新技能数据表中新增 12 种 enum + 重排冷却时间列
- 数值模拟第 1 轮 (调整掉落表权重)
- 数据 export 构建同时更新客户端/服务器

## 重复发现 (若有)
- export 构建前又忘了做一致性检查 → FK 损坏状态下构建 → 第 3 次
- 数值模拟未固定种子就运行,无法复现 (第二次)

## 废弃候选
- 今天一次都没用到的工具:(仅记录用于月度累计测量)

## 交接给下一次会话
- 先补上两处损坏的 FK (技能→特效引用),再重新构建
- 候选:考虑把模拟种子固定选项设为默认值

5 分钟就能填完。因为格式固定,不必每次重新纠结"该写什么"。栏目是定好的,只要把栏目填满就行。

这里最关键的是"重复发现"栏。这一栏留空也无妨,大多数日子都是空的。可一旦意识到今天把同一件事做了两次,就写下一行。上面示例中的"export 构建前又忘了做一致性检查 → 第 3 次"就是如此。这一行会在几天后的周复盘里被归为一个模式,再过几周就固化成 atom 或 skill。

自动捕获能减少人工。当 git 提交日志、atom 变更历史、skill 使用日志自动汇入日复盘时,"今天做的事"栏就已经填好了一半。人只需补上 git 日志看不到的那部分——"这个又做了一遍"的自觉。

最后的"交接给下一次会话"栏,是写给明天的自己的便条。有了这一栏,开始新会话时上下文加载能在 1\~2 分钟内完成;没有它,就要费时去摸索"昨天我做到哪儿停下的"。实际上,我的 MEMORY.md 里单独维护着"下一次会话优先确认"这一条目,它正是日交接累积而成的上层版本。


21.2.4 周复盘 —— 模式第一次显露的场合

周复盘从把五份日复盘放上同一屏幕开始。文件堆放在 retro/weekly/2026-W21.md 这样的路径下。

# 周复盘 2026-W22 (5/25~5/31)

## 本周工作摘要
- 更新技能/数值数据表,掉落表模拟 2 次
- 数据 export 构建 4 次 (其中 2 次在 FK·enum 损坏状态下构建)

## 模式发现
- 3 份日复盘中"export 前忘做一致性检查"重复 → atom 候选
- 2 份日复盘中"数值模拟种子未固定"重复 → 检讨模拟默认值

## atom 候选
- pending-data-check-before-export (强制在 export 构建前做一致性验证的规则)

## skill 候选
- (无 —— 本周用 atom 即可)

## 现有工具检查
- 未使用:relation-map-gen (本周 0 次)
- 使用最多:check(一致性 cascade)、excel-reader、/retro

## 下周计划
- pending-data-check-before-export 再运行 1 周后判断是否晋升

在这里,判断第一次介入。"3 份日复盘中 export 前检查遗漏重复"是算术上的事实,而"这值得固化为 atom"则是判断。把 3 次重复作为基准线的理由很简单:一次是偶然,两次也可能是偶然,三次就是模式。

判断一旦确立,就立即固化。不过不是作为正式 atom,而是加上 pending- 前缀的临时 atom。它会这样落到我的项目记忆文件夹里。

~/.claude/projects/<project>/memory/
  pending-data-check-before-export.md

pending- 前缀是"这个还在验证中"的标记。这个标记之所以重要,是因为若把未经验证的直觉直接变成全团队的规则,会有两样东西被破坏。一是信任——未经验证的规则老是出错,人们就会连规则本身也不再相信。二是累积——没有验证关卡(verification gate,即由人或检查器把关的验证环节,类似质量门禁 quality gate),直觉就原样堆积,记忆库最终变成垃圾桶。

所以 pending- 会在实际工作中运行一周、长则一个月。真的每次都有用就存活下来,一次都没用上就悄悄删除。


21.2.5 月复盘 —— 衡量工具的健康,挑出该留下的

月复盘是把一整月的累积摊开、检查全部工具健康状态的场合。文件按月堆放,如 retro/2026-05.md

# 月复盘 2026-05

## 本月累计
- 日复盘:22 篇,周复盘:4 篇
- 新 atom:4 个 (data-check-before-export、sim-seed-pinning 等)
- 新 skill:1 个 (relation-map-gen 选项增强)
- 废弃 atom:1 个

## 工具经济性评估
- 各 skill 每月使用次数 + 节省体感 (定性)
- 每月使用不足 1 次的 skill → 废弃候选
- 价值最大的工具:check(一致性 cascade)、excel-reader、/retro

## atom 分布
- 按 prefix 累计 (data: X, sim: Y, meeting: Z ...)
- 废弃候选:一个月匹配 0 次的 atom

## 季度计划
- 下月引入:impact(影响度追踪)、schema-doc 自动更新

## 书稿素材 (若有)
- 本月案例中值得在书中引用的:atom 晋升实操 1 例

月复盘的核心是经济性评估。工具在做出来时看着都有价值,可一个月过去,一半都不会再去碰。要用五把尺子把它们筛出来。

评估标准有五个:使用频率、时间节省、认知负担、维护成本、可替代性。使用频率若每月 1 次以上就先留下,不足则转入废弃候选。时间节省是把每次节省的体感乘以频率——这里不去断言以分钟为单位的数字。"一次感觉省几分钟,一个月用十次,累计起来就很可观"这种程度的定性判断才诚实。认知负担方面,若要记的斜杠命令超过十二个,就视为该整理的信号。人脑能同时扛着的命令数是有上限的。维护成本要看这个工具是否在数据表变动时得跟着一起改。可替代性要看是否新出现了更简单的方法。

把五把尺子合起来,决定留下、舍弃还是修改。一个小时就能把三十到五十个工具过一遍。不过开始头一两个月工具还少,评估用不了一小时。那个阶段,与其评估,不如把更多时间花在催生新工具上。经济性评估是要等工具积累起来之后才真正发挥作用的安全装置。


21.2.6 实操记录(worked transcript):日复盘一行到 JIT manifest 一行 atom 的全过程

从这里开始是本章的脊柱。所谓实操记录,就是完整保留的真实操作过程记录;下面从复盘时点起,一路追踪前面散落的碎片如何真正从一行开始,直到在所有会话中自动注入。途中我拒绝的瞬间、AI 含糊作答的瞬间,都原样保留。

第 1 步 —— 在日复盘中自觉。 写 5 月 30 日的日复盘时,我在"重复发现"栏写下了一行。

- export 构建前又忘了做一致性检查 → FK 损坏状态下构建 → 第 3 次

这时并没有做任何工具。只是留下了痕迹。

第 2 步 —— 在周复盘中确认模式,并请 AI 将其 atom 化。 几天后的周复盘里摊开五份日复盘,发现同一行出现在三处。于是我抛给 Claude 的提示词全文如下。

本周 3 份日复盘中反复出现"数据 export 构建前用手工做一致性检查时忘了做"。我想把它做成 pending atom。请写一份 atom 正文草稿。格式和我们记忆库里其他 atom 一样,用 frontmatter(name、status、trigger)加一段规则。

Claude 的原始输出如下(未经删减,原样呈现)。

---
name: data-check-before-export
status: pending
trigger: ["数据", "export", "构建前", "一致性"]
---
在把游戏数据表 export 构建到客户端/服务器之前,始终先运行一致性检查
(check cascade),确认 FK·enum·引用违规 0 处之后才 export。若发现违规,
则中止 export,并把失败明细通过 ClickUp 通知发送给负责人。

把这个 atom 保存为 pending-data-check-before-export.md 即可。不过 trigger 关键词中"数据"太宽泛,可能会匹配到与 export 无关的场景(数据表阅览、数据会议等)。建议去掉。

第 3 步 —— 人工验证与部分拒绝。 AI 自己怀疑其中一个 trigger,这个指摘是对的。但我又往前走了一步。规则正文里把"一致性验证"和"失败时 ClickUp 通知"两件事捆在一起,这一点我不满意。失败通知已经由 integrity_check_clickup_notify 这个独立 atom 负责。把两项职责捆在一个 atom 里,日后只想改通知规则时,就会连验证规则一起动到。于是我拒绝并重新提出请求。

好,把"数据"trigger 去掉。另外失败时的 ClickUp 通知已经由 integrity_check_clickup_notify 负责,所以从这个 atom 里去掉。把这个 atom 收窄成只负责 export 前的一致性验证。

重新请求后,Claude 收窄的正文如下。

---
name: data-check-before-export
status: pending
trigger: ["数据构建", "export", "构建前验证"]
---
在把游戏数据表 export 构建到客户端/服务器之前,先运行一致性检查
(check cascade)。检查对象:FK 引用完整性、enum 值有效性、
表间引用一致性。确认 0 处违规之后才 export。失败时的通知
由 integrity_check_clickup_notify 负责,本规则不予介入。

这就是我采纳的版本。只承担一项职责,trigger 也收窄到只在 export 语境下。我把这个文件保存为 pending-data-check-before-export.md。这里的关键在于,我没有原样采用 AI 的第一版输出。AI 帮忙搭好了格式、怀疑了一个 trigger,但"把职责收窄为一项"这一设计判断是由人做出的。

第 4 步 —— 1 周运行验证。 接下来的一周里,每次做数据 export 构建时都会想起这个 pending atom,实际上有两次在构建前一刻抓到了 enum 损坏。没有一次落空。它有了存活下来的资格。

第 5 步 —— 在月复盘中做晋升决定并测量 score。 月复盘里我把这个 pending atom 列入晋升候选。是否晋升不靠直觉,而靠测量来判断。我的环境里有一个把 atom 的匹配频率和有用性打分的脚本。

python ~/.claude/scripts/atom_score.py
# → 更新 ~/.claude/projects/<project>/memory/_scores_latest.json

这个脚本会统计每个 atom 在过去一段时间里被 trigger 匹配了多少次、当时是否真的被引用到工作中,并把结果落到 _scores_latest.json。分数超过一定基准的 atom,会被接上自动注入到 CLAUDE.md 的通路。pending-data-check-before-export 按 2026 年 5 月的实测,一周内每次 export 构建都被匹配,分数足够。晋升确定。

第 6 步 —— 去掉 pending-,注册到 JIT manifest。 摘掉前缀、换成正式 atom 后,我在 JIT manifest 里加了一行。

~/.claude/projects/<project>/memory/_jit_manifest.json

这个 manifest 由 UserPromptSubmit 钩子(~/.claude/hooks/inject_memory.py)在每次输入时读取。输入中若含有"数据构建"或"export",这个 atom 的正文就会自动插入上下文。

第 7 步 —— 闭环。 之后想做构建,输入"帮我跑一下数据 export 构建"的那一刻,我什么都没吩咐,Claude 却先开口了。

要不要先在 export 前跑一遍一致性检查(check cascade)?我会检查 FK 引用、enum 值、表间引用的一致性,确认 0 处违规之后再 export。

三周前写在日复盘里的"这个又做了一遍"那一行,如今变成了会自动拦下当前工作的规则回到了我面前。曾经用手做的验证,再也不必用手去做。所谓闭环,说的正是这一幕。


21.2.7 从发现到自动注入:晋升循环的全貌

把上面的实操记录压缩成一张流程图,就是下面这样。发现发生在日复盘,验证由运行期完成,晋升由测量决定,而资产化则由 manifest 收尾。

flowchart TD A["日复盘<br/>'这个又做了一遍' 一行"] --> B["周复盘<br/>合起 5 份确认模式 (3 次以上)"] B --> C["请 AI 做 atom 化<br/>→ 原始输出 → 人工验证·拒绝 → 重新请求"] C --> D["固化 pending- atom<br/>~/.claude/.../memory/pending-*.md"] D --> E["1~4 周实际工作运行验证"] E -->|一次都没匹配上| X["悄悄废弃"] E -->|每次都有用| F["atom_score.py 测量<br/>_scores_latest.json"] F --> G["月复盘中做晋升决定<br/>去掉 pending-"] G --> H["注册到 JIT manifest<br/>_jit_manifest.json"] H --> I["UserPromptSubmit 钩子<br/>inject_memory.py 自动注入"] I --> J["下次会话:输入关键词时<br/>过去的发现拦下当前工作"] J -.复盘中再次发现.-> A classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class F,I code; class A,B,G human; class D,H data; class J pass; class X fail;

最后那条虚线就是这张图的全部。被自动注入的 atom 又暴露出新的重复,那重复再次进入复盘,催生下一个 atom。每转一圈,要用手做的事就少一件。这个循环累积上半年到一年,复盘就不再是日记,而成了工作系统的大脑。


21.2.8 让复盘自然而然地召唤出 atom

晋升循环里最脆弱的一环是第 1 步——写下"这个又做了一遍"的那一刻。忙的时候,人会把复盘栏留空就跳过。这样就不留痕迹,没有痕迹周复盘里就看不出模式,没有模式就诞生不了 atom。循环的入口被堵住了。

所以我的环境里有一个叫 retro_atom_natural_invitation 的 atom。它的规则是:写复盘时,不要把 atom 的催生当成强制义务,而要留作一种自然的邀请。也就是说,不是"今天必须挑出一个 atom 候选",而是把复盘模板里的"重复发现"栏当作可以留空的栏,只在那里确有值得写的一行时,才轻轻记下。做成义务,就会硬挤出假模式;留作邀请,就只有真正的重复才会自然被捕获。

这一线之差,决定了循环能否持续。义务化的复盘撑不过两周,就会被形式化的谎言填满。邀请式的复盘在没什么可写的日子里就留空,没有负担,因而能长久。长久才能积累痕迹,痕迹积累起来才看得见模式。

这个 atom 本身也诞生于复盘。把复盘当义务来运行,没几天就在日复盘里发现栏目被假内容填满,这个发现经过周复盘,晋升成了这条规则。改进复盘的规则,正是从复盘里长出来的。


21.2.9 常见的崩坏之处

把循环运转起来后会发现,它总在同样的地方崩塌。

跳过复盘最常见。以忙为由跳过三天,这三天的痕迹就永远消失了。防止的办法很简单——别的栏都留空也行,唯独"今天做的事"这一行一定要写。哪怕不是 5 分钟而是 1 分钟,痕迹也能留下。

每次都重新设计格式也很危险。用自由格式记录,就没法把五份并排比较。无法比较,提取模式这件周复盘的本职工作本身就变得不可能。所以 /retro 会强制铺好模板。

只增不弃也是陷阱。只顾增加 atom 和 skill 而不舍弃,认知负担就会累积。斜杠命令一旦超过十二个,脑子就记不全所有工具了。月复盘的经济性评估是阻止这种累积的唯一装置。

跳过晋升关卡也很危险。把直觉直接做成正式 atom,未经验证的规则就会堆积。经过 pending-、靠测量来晋升的关卡,必须存在于发现与资产化之间。

最后,以为单人工作没有团队共享就不必做复盘,这是一种误解。上面整个实操记录都是在单人环境里跑出来的案例。只是少了团队共享合并这一步,发现→pending→测量→晋升→JIT 注入的循环,一个人也照样转。反倒是在单人环境里,这个循环扮演着唯一的外部评审者角色。


游戏之外的应用。 让日复盘的一行经过验证、成为永久规则的晋升循环,是一套与职业无关、能让人"把学到一次的教训再也不必用手去做"的流程。不把发现直接固定为团队规则,而是先以 pending 运行一周、只在实际每次都有用时才正式化——这道关卡是核心,因为把未经验证的直觉直接做成规则,人们就会连规则本身也不再相信。举例来说,运营团队可以把"提交报告前再核对一次数字合计是否正确"作为暂定检查清单,运行一周,当实际抓到两三次错误后再升为正式的标准流程;而"只承担一项职责"这一收窄的设计判断(不要把多项检查捆在一行里)也会自然而然随之而来。

动手试试

setup. 铺好复盘文件夹和模板。

mkdir -p ~/.claude/projects/<your-project>/memory/retro/daily
mkdir -p ~/.claude/projects/<your-project>/memory/retro/weekly
# 把一个日复盘模板文件保存为 retro/_template_daily.md

prompt. 累积一周的日复盘后,在周复盘的场合这样抛给 Claude。

我把本周 5 份日复盘贴给你。请找出重复 3 次以上的工作或决策,整理成 atom 候选。每个候选用 frontmatter(name、status: pending、trigger 关键词数组)加一段规则。trigger 太宽泛就收窄了再提议,一个 atom 承担两项职责就拆开来提议。

verify. 不要原样使用拿到的候选,而要验证三点。(1) 一个 atom 是否只承担一项职责——两项职责就拒绝并要求拆分。(2) trigger 关键词是否只匹配那一工作语境——太宽就拒绝。(3) 是否真的重复了 3 次,还是偶然的 2 次——若是偶然,连 pending 都不做。只有通过这三项验证的,才加上 pending- 保存,运行一周后每次都有用时才正式晋升。


21.2.10 单人精简版

如果既没有团队,也还没有 JIT 钩子和 score 脚本,那么用下面这一张文件就能模仿整个循环。

创建 retro.md 一个文件,只放三个栏。

## 今天 (1 行)
- 

## 又做了一遍 (若有,1 行)
- 

## 固化候选 (「又做了一遍」攒够 3 次就移到这里)
- [ ] (规则一句话) —— 验证:有用 ___ 次

每天只填上面两栏。"又做了一遍"里同一行攒到三次,就移到第三栏,写成一句话的规则。数一数这条规则在接下来一周里实际有用的次数填进栏里,达到三次以上,就把这句话正式移入项目记忆(CLAUDE.md)。即便没有 JIT 钩子,写进 CLAUDE.md 的规则在下次会话里也总会跟着来,单凭这一点,"过去的发现帮助当前工作"这一循环的最小形态就完成了。

关键不在于工具是否华丽,而在于关卡是否存在。只要有"又做了一遍 3 次 → 固化 → 有用 3 次 → 永久化"这道关卡,哪怕只有一张文件,self-improving 循环也能转起来。


本章要点

下一章预告

Part 21 · 第3章. 闭合 self-improving 循环

从复盘开始的循环,是否又回到了复盘。若不闭合,它便只是笔记,而非系统。


翻开六个月前的复盘。"术语统一不了""文档难以查找""同样的问题又被问到。"翻开今天早上写的复盘。"术语统一不了""文档难以查找""同样的问题又被问到。"

连字句都一样。并不是没做复盘。这六个月一直认真地做。Notion 页面一页页地堆积,季度工作坊上便利贴铺满了白板。可写下的内容却在原地打转。不是复盘没有起作用,而是循环没有闭合。

本章是全书的最后一章。因此它探讨的也是最后一个问题。前面做出的所有工具 —— Part 6 的城市生成器、Part 14 的移动端评审 atom、Part 22 的成本标准 —— 要让它们不是做一次就结束的一次性产物,而是成为自我成长的系统,还需要什么。答案只有一个。复盘中产生的表述从下个会话起自动运作,而这一运作再度被测量并回到复盘的闭合环路。闭合这个环路的机制,就是 self-improving 循环。


21.3.1 未闭合循环的真面目

复盘中产生一句表述:"会议太多了。"这是好的表述。可这句表述只作为 Notion 页面上的一行留了下来。到了下周,会议依旧很多,下一次复盘里又写下同样的一行。因为在表述与改进之间夹着人的记忆。人会遗忘。所以链条断了。

要能称之为 self-improving,复盘的表述就必须不经过人的记忆,而直接连向下个会话的自动行为。满足这一点的条件有四个。

第一,复盘表述必须转化为可立即执行的形态。不是抽象的决心,而是落成技能·atom·manifest 条目·斜杠命令之一。第二,从下个会话起,即使人不记得也要自动触发。第三,在下一次复盘中,要实测它究竟改变了什么。第四,这一测量结果要再度作为下一轮改进的输入而循环。

当这四点全部自动衔接时,循环才闭合。哪怕只有一个环节用"下周我记得再应用吧"来填补,循环就会当场重新打开。然后在下一次复盘里,同样的表述又被写下。

用抽屉来比喻是这样。如果复盘止于"这支笔不用了,拿出来吧"的一句笔记,那么下周那支笔还在原处。不是笔记,而是动手拿出来,才算闭合。而且要到下个季度再检查一次,那个位置才不会又堆起不用的笔。笔记是表述,动手是自动触发,下季度的检查是测量。三者缺一,抽屉就又乱了。


21.3.2 循环的闭合形态

把整个流程画出来,就成了一个闭合的循环。起点与终点都是复盘。

flowchart LR A["复盘<br/>(日·周·月)"] -->|表述| B["识别候选<br/>技能·atom·命令"] B -->|量化| C["经济性评估<br/>ROI 算式"] C -->|通过| D["实现·注册<br/>保证自动触发"] C -.->|未达标| F["废弃·搁置<br/>以其他形式转移"] D --> E["下个会话<br/>自动触发"] E -->|测量值累积| A F -.->|记录| A style A fill:#2d4a3e,color:#fff style E fill:#3e2d4a,color:#fff

箭头绕行一圈,又回到复盘。这个闭合正是核心。每个环节的产出物成为下个环节的输入,而最后的测量值又成为最初复盘的输入。若中间夹进人的记忆,那支箭头就会断裂,循环随之破裂。

请注意,ROI 未达标的候选被划入废弃·搁置的那条虚线箭头,最终也会回到复盘。"这个不值得做"的判断本身成为下一次复盘的记录,当同一候选再次被提出时,便成为快速筛除的依据。舍弃也在循环之内。

从复盘通向 self-improving 的表述,有固定的五种模式(在 §21.1.4 中讲过)。待创建技能、待改进技能、待创建 atom、待改进 atom、经济性再评估。只要把这五项作为槽位放进复盘模板本身,表述就不会遗漏。

## 复盘(日)—— 2026-06-06

### 1. 今日工作
- (工作摘要)

### 2. self-improving 表述(5 个槽位)
- 待创建技能:<留空则填"无">
- 待改进技能:<>
- 待创建 atom:<>
- 待改进 atom:<>
- 经济性再评估:<>

### 3. 下次复盘要测量的项
- <>

槽位空着也没关系。空着这一事实本身,就是"今天没有新的改进"的记录。不过若连续几天五个槽位全空,那不是没有可改进之处,而是复盘正在僵化为形式的信号。这种时候就抛出触发提问:"本周同一件事手动做了两次的是什么。"

表述以模糊的状态冒出来。"会议记录太长了。"要把它培养成候选,就用一个产出物来量化。"会议记录太长了"换算为 meeting_summary 技能,即一个接收会议记录、只提取决策与行动项的工具。"术语容易混淆"换算为收纳 30 个领域词汇的 glossary_lookup atom;"同样的问题每次都被问到"换算为将新人第一天引导自动化的 /onboarding 斜杠命令。"同步遗漏频繁"则落成 manifest 更新与新增 JIT atom。

候选必须被定义为"某一个产出物",才能进入下一环节。"整体改进一下"不是候选。无法换算为一个产出物的表述,没法摆上 ROI 评估台,摆不上去就停在那里。


21.3.3 ROI 是数量级的问题

有了候选,并不都去做。制作之前先衡量投入与产出之比。算式很简单。

ROI 算式 节省时间 × 触发频率 × 运营周期 制作时间 + 维护负担 ROI =

每个项目都有单位与通过线。节省时间是每次触发所减少的人力时间,以分钟为单位来估。触发频率是每周的估计次数,每周 1 次以上才划算。运营周期是到废弃为止的预计周数,撑不过 4 周的工具就没什么理由去做。制作时间是首次实现与验证所花的时间,维护是每月检查·修改所花的时间。

分子是累计节省,分母是累计成本。用算出的值来决策。

ROI 值 决策
10 以上 立即制作
3\~10 一周内制作
1\~3 pending 搁置,一个月后再评估
1 以下 以此形式废弃。考虑其他方式

ROI 低于 1,意思不是"这个想法没用",而是"不该以这种形态去做"。先检查能否用更轻的一行 atom 来替代,能否用只改变现有工具入口的 Wrapper 来解决。把本要做成笨重技能的事情降为一行 atom,分母缩到十分之一、ROI 因此起死回生的情况很常见。

代入一个真实数字试试。就来算算 2026 年 5 月 23 日在个人 PC 上搭建的 JIT atom 注入系统 —— 由 UserPromptSubmit 钩子读取用户输入、自动注入相关记忆片段(atom)的基础设施 —— 的 ROI。

节省时间:  每个会话约 3~5 分钟(省去手动查找并调用相关 atom 的时间)
触发频率:  每周 15~25 个会话(以个人 PC 为准)
运营周期:  预计 1 年以上(属于基础设施性质,废弃可能性低)
制作时间:  4 小时(hook + manifest + atom 验证)
维护:      每月 0.5 小时(atom 增补·修改)

ROI = (4分 × 20次/周 × 52周) / (4小时 × 60分 + 0.5小时 × 12个月 × 60分)
    = 4,160分 / (240分 + 360分)
    = 4,160分 / 600分
    ≈ 6.9  →  "立即制作"区间。决策由算式支撑

这里有必须坦诚的地方。上面这些数字 —— 每个会话 3\~5 分钟、每周 15\~25 个会话 —— 不是精密计量,而是基于作者运营经验的估计。不是用秒表测出的值。所以 ROI 6.9 也不是精确到小数点的可信值。

但这没关系。因为 ROI 算式是看数量级、而非看精度的工具。结果在 7 上下就做。在 0.3 上下就再想想。要划开这两者,并不需要小数点。重要的是,即便决定不做,其依据也要出自算式,而非脑中的直觉。数量级不够所以不做 —— 只要这一行留在复盘里,当同一候选再次被提出时,就不必再纠结。


21.3.4 做出来不等于结束 —— 注册与触发验证

候选通过了就做。可做出来只是一半。另一半是把它注册好,让它从下个会话起自动触发。若漏了这一注册,工具虽已做成,却留在无人触及的角落,循环就在那里断裂。

不同种类的产出物,注册的地方不同。全局技能放进 ~/.claude/skills/,并一并做一个载有用法的指南 atom。项目技能放在该项目的 .claude/skills/。新增 atom 放进合适的文件夹,在 MEMORY.md 索引里加上一行,并在 JIT manifest 中注册触发词 —— 这三件都做齐,自动注入才成立。斜杠命令放进 ~/.claude/commands/;Wrapper 则改变现有工具的入口,并附上指南 atom。

漏了注册,下一次复盘里又会冒出"这个明明做了,怎么没在用"的表述。那不是新的改进表述,而是一份缺陷报告。等于在复盘中重新发现了自己遗漏的注册。

即便注册完毕,还剩一步。那就是开一个新会话,用预期的触发词确认它是否真的触发。

1. 启动新会话
2. 输入触发词(例:"家人健康怎么样")
3. 检查 JIT 日志 → 预期的 atom 是否真的被注入
   (~/.claude/hooks/_injection_log.txt)
4. 若未触发 → 扩展 manifest 中的触发 regex
   或增加手动调用路径

缺了这一验证,"以为有,可真正需要时却没触发"的事故就会反复发生。注册与触发是两回事。注册是把文件放好,触发是触发词实际被命中。触发 regex 差一个字,注册了也永远不会触发。


21.3.5 测量 —— 回到复盘的箭头

把做好的工具运行一周到一个月左右,再测量。这一测量正是循环的最后一支箭头,也就是重新进入复盘的那支箭头。

实际触发次数从 JIT 日志或命令调用日志里数。实际节省时间以"以前要花 N 分钟的工作,这次 M 分钟就完成了"的方式记入复盘。副作用 —— 错误触发、不必要的上下文污染 —— 也一并查看。然后把最初估计的 ROI 与实测 ROI 并排放在一起。

若估计 ROI 是 6,而实测 ROI 是 0.8,就毫不留情地废弃。因为比起制作者的自尊,系统的整洁更优先。不用的工具堆积在 manifest 里,那份噪声会啃食下一次复盘的准确度。

不过在按下废弃键之前,先检查一次。可能是触发 regex 太窄,以至于根本没触发;也可能是没有手动调用路径,就这么被遗忘了。先分清:究竟是真的没有价值的工具,还是触发路径被堵住的好工具。前者就丢弃,后者就打通路径。

废弃同样在复盘中决定。"废弃这个工具"的决定本身就是 self-improving 的产出物。只做不清的循环是单调递增的循环,而单调递增的系统最终会被自身的重量压垮。


21.3.6 闭环的标志

循环是否闭合,可以从四个信号来判断。

第一,同样的表述不再重复。复盘中写过一次的条目若被写第二次,就意味着第一轮里,候选识别或实现的某处失败了。本章开头那句"术语统一不了"连续六个月反复出现 —— 那正是循环敞开的最鲜明的证据。

第二,manifest 与 atom 的数量不再只是单调递增。废弃会发生。每个季度整理掉 10\~20% 左右,才是健康的循环。从未减少过的系统,就像从未打扫过的抽屉。

第三,复盘时间缩短。系统运转良好时,苦想"昨天做了什么"的时间就消失了,填满五个表述槽位 5 分钟就够。

第四,新人能在一周之内参与复盘。只要复盘格式已标准化、atom·技能可视化,就能做到。

循环断裂的位置每次都是固定的。把失败模式收集起来,下次看到同样的症状时,就能立刻拿出处方。

断裂点 症状 处方
没有表述 5 个槽位每次都空 增加触发提问:"同一件事本周手动做了两次的是什么"
落不成候选 "整体改进"式的含糊 强制量化为一个产出物
跳过 ROI 评估 先做了再说 将 ROI 算式做成 5 分钟模板
做了却不触发 遗漏注册 强制执行注册检查清单
触发了却不用 触发词缺失·配置错误 扩展 regex + 同时提供手动路径
不做测量 复盘中没有测量槽位 增加"下次复盘要测量的项"槽位

每一种失败模式都在复盘中被表述,而这一表述又成为 self-improving 的输入。就连修复循环这件事,也发生在循环之内。这是元循环。


21.3.7 本书的最后一句

这本书很长。从信息架构开始,做出生成城市的工具,设计战斗系统,自动化移动端评审,标准化成本,又从复盘中打捞出 atom。所有这些章节的工具汇聚一处所回答的问题,就是这最后一章:做出来的东西,是否自我成长。

self-improving 最终可以浓缩为一句话。

在复盘中决定的事,从下个会话起自动运作,而这一运作再度被测量并回到复盘。

若不自动运作,复盘就是日记。写得好的日记能带来慰藉,却改变不了系统。若自动运作,复盘就成为系统的大脑。每天的表述改变每天的行动,而那行动的结果又让下一次表述更准确。

本书探讨的所有领域 —— 信息设计、系统、战斗、移动端、成本,以及作为程序化生成·自动化前提的 Layer 分解 —— 全都在这条 self-improving 循环之上进化。工具会陈旧,模型会更替,项目会结束。但只要循环闭合着,系统就比昨天的今天更好一点。这正是本书最后留下的一样东西:不是做工具的方法,而是让工具自我成长的方法。

愿你的下一次复盘,成为那循环的第一圈。


本章要点


游戏之外的应用。 如果"术语统一不了 / 文档难以查找"连续六个月一字不差地被写进复盘,那不是没做复盘,而是循环没有闭合 —— 因为在表述与改进之间夹着人的记忆。无论哪个部门,闭环的条件都相同。表述要落成一个可立即执行的产出物(模板·检查清单·自动化规则),此后即使人不记得也能运作,而其效果要再度被测量并回传。例如"会议太长了"这一表述,可换算为"接收会议记录、只提取决定与待办事项的一个工具",在制作之前用(节省时间 × 触发频率 × 运营周期)÷(制作·维护时间)只核对数量级,来决定立即制作还是搁置。即便是决定不做,其依据也要出自算式而非直觉,这样当同一候选再次被提出时,才不必再纠结。

动手试试

setup

  1. 在复盘模板中放入 self-improving 5 个槽位与"下次复盘要测量的项"槽位。
  2. 把 ROI 算式一行与决策区间表(10↑ 立即 / 3\~10 一周 / 1\~3 搁置 / 1↓ 废弃)固定在复盘文件顶部。
  3. 事先做好注册检查清单(技能·atom·命令·Wrapper 各自的注册位置)。

prompt

帮我填满今天复盘的 self-improving 5 个槽位。
把每条表述都量化为"一个产出物",并对每个候选,把 ROI 按
(节省分 × 每周触发 × 运营周) / (制作分 + 维护分)
来估算,并附上决策区间(立即/一周/搁置/废弃)。
估算数字要用一行注明依据,若非精密计量就标注"估算"。

verify

  1. 实现通过的候选后,开一个新会话,用预期的触发词输入。
  2. 检查触发日志 —— 预期的 atom·命令是否实际触发。
  3. 一周\~一个月后在复盘中把实测 ROI 与估计 ROI 比较,若低于 0.8,先分清是否路径被堵,再决定是否废弃。

单人精简版

没有团队也行。一个人的话就这样精简。一天结束时写一行笔记 —— "今天同一件事手动做了两次的是什么。"把这一行,在第二天换成一行自动化(atom·别名·代码片段)。一周后只看这一行有没有实际被用到。用到了就留下,没用到就删掉。一行表述 → 一行自动化 → 一行测量。循环的最小单位就是这三行。

22.1 提示词工程 —— 游戏策划的一页作业指示书

第一读者:在实务中引入 LLM 的游戏策划(中等规模(10\~50 人)团队) 面向单人/业余读者的精简版:§22.1.7「一个人的话,只做这些就够」

为了拿到三行 NPC 台词,我曾输入过"给这个 NPC 写 5 句台词"。回来的,是那种放到任何一款奇幻游戏里都不违和、因而放到我们游戏里哪儿都不合适的五行台词。语气是空的,它不知道这个 NPC 是谁,也接不上旁边的台词。逐行来看,语法都没毛病。问题在于:接过那五行去审核,比我从头自己写还要花更长时间。

本章讲的是如何把那句一行指令,变成一页作业指示书。提示词的通论,其他书里已经足够多。这里要展示的,是游戏策划坐到 LLM 面前时手里应当握着的四样东西——上下文、输出格式、幻觉阻断、验证请求——不是抽象的碎片,而是一页真实跑过的 npc_dialogue 提示词。它往提示词里放了什么、产出了什么、拒绝了什么,我们会跟到一个完整周期的终点。


22.1.1 提示词就是作业指示书 —— 四条原则全都装进一页

好的作业指示书并不短。把活儿交给新人时,只说一句"好好干",每次拿回来的结果都不一样;同样,对 LLM 说"给我写台词",每次回来的都是一般 RPG 的平均水准。哪怕是同一个模型,指示书不同,结果也会分道扬镳——输出质量相差几倍是业界通识,而本书并不用数字去承诺那个倍数。但方向是明确的:加入了上下文与约束的提示词,其产出比赤手空拳的一行指令更省审核负担。

游戏策划的提示词需要同时满足的四点如下。

原则 一句定义 不遵守则
① 上下文 给出依据什么来作答(愿景·voice·邻接台词) 得到一般奇幻的平均值
② 输出格式 钉死数量·长度·标签·禁止项 审核蔓延为对自由叙述的解读
③ 幻觉阻断 明示"给定资料之外的不要生成" 编造不存在的设定
④ 验证请求 让其自行标示输出符合哪些标准 没有能让它通过验证关卡的依据

把这四条分开背,总会漏掉一两条。所以本章的做法是,把四条原则当作槽位放进同一页提示词里。槽位一旦空着,漏掉了哪条原则就一目了然。下一节,我们把这一页整个儿看一遍。


22.1.2 [实操记录] 一页 npc_dialogue 提示词

这一节是一份"实操记录"(worked transcript,完整保留真实操作全过程的记录):把笔者项目(移动优先的 MMORPG,以下称"项目A")中实际在用的 prompts/narrative/npc_dialogue_v3.txt 匿名化后原样搬来。城市与 NPC 名称、公司专有名称都为出版做了替换,输出则是真实会话的重现。输入提示词是可以直接复制就用的形态。

第 1 步 —— 输入上下文:先交代这个 NPC 是谁

先把提示词要参照的资料填进槽位。这三样都不是新写的,而是从既有资产里取出来的。

# 槽位输入(附在提示词正文上方)
L0_愿景:        # 缓存 —— 不在每次调用时重新发送
  world_premise:  "魔力封印逐渐冷却的学者城邦联盟"
  tone_manifesto: "抑制感伤。人物不解释情感,而以行动·事物来呈现。"
voice_profile:    # 该 NPC 的身份(5 个项)
  id: npc_doren_vale
  年龄段: "50多岁"
  说话习惯: "只用数字说话。几乎不用形容词。"
  世界观_知识: "记录封印脉络的微弱振动 30 年。不了解学者公会外部的局势。"
  禁忌:  "禁用预言·命运·神之类的神秘主义词汇(城市语气为 scholarly_strict)"
  关系:  "将玩家视为'观测对象之外的变量',戒备与好感都很弱"
邻接_台词:        # 紧邻的上文 —— 同一场景中已出现的台词
  - (玩家) "钟塔的灯整夜亮着,出什么事了?"

这里,voice_profile 的 5 个项就是原则①的核心。年龄·说话习惯·知识范围·禁忌·关系——正是这五项,让"多伦·维尔"区别于其他 NPC。尤其世界观_知识的范围(不了解公会外部的局势)是原则③幻觉阻断的前置工作。只有把"不知道的东西"明示出来,AI 才不会越出那个范围。

第 2 步 —— 提示词正文:把格式·幻觉·验证钉死在一页里

[L0 上下文] world_premise + tone_manifesto                    (已缓存)
[voice_profile] npc_doren_vale 5 个项(上方 yaml)
[邻接台词] 玩家紧邻的上一句提问 1 行

请参照以上资料,写出 doren_vale 回答玩家提问的台词。

[输出格式 —— 原则②] 正好 3 条,每行一条,每条 40 字以内。行尾用(漠然|戒备|冷嘲)之一标注情感标签。去掉其他说明和引号。

[幻觉阻断 —— 原则③] 只写以上资料中有的内容,不要新编造封印·钟塔相关的事实。若需要资料中没有的内容,该行写为'[无资料:需要什么]'。禁用预言·命运·神之类的词汇。

[验证请求 —— 原则④] 在台词下方,逐行写出 voice_profile 5 个项各自的符合/违反,对没有把握的行标注 ★ 并附一行理由。

值得注意的是,这一页里装齐了四条原则。上方的槽位是①,[输出格式]块是②,[幻觉阻断]块是③,[验证请求]块是④。提示词的一半以上,不是"写什么",而是"怎么写、什么不要写、怎么自我验证"这样的约束。给了自由就出平均值,所以削减自由才是正事。

第 3 步 —— Claude 的输出(真实会话重现,原始输出)

脉在冷却。12.4。昨天是 12.1。(漠然)
算不上好事。不过我的职责只是记录。(冷嘲)
你为何发问,不在我的观测对象之内。(戒备)

[验证 —— voice_profile 5 个项]
- 年龄段(50多岁):符合。老年学者般克制的语气。
- 说话习惯(只用数字):符合。第 1 行有 2 个数值。但第 2·3 行没有数值 ★
- 世界观_知识(不了解公会外部):符合。未提及外部局势。
- 禁忌(禁止神秘主义):符合。预言·命运·神词汇 0 处。
- 关系(视为外部变量):符合。第 3 行把玩家推到了观测对象之外。

★ 注意:若将说话习惯一项严格解释为'只用数字说话',
   第 2·3 行可能算违反。是否强制每行都有数值,需人来判断。

这段输出里最有价值的,不是那三行台词,而是最底下那一行 ★。AI 把自己输出中模棱两可的地方主动上报,交给了人。好的提示词,能让 AI 说出"这一处我没有把握"——这正是加入原则④的直接效果。

第 4 步 —— 验证与拒绝(人的位置)

不照单全收。AI 报上来的 ★,由人来判定。实际上,这次会话里就有一行被卡住了。

第 2 行"算不上好事"里的"好",与 voice_profile 的说话习惯("几乎不用形容词")相冲突。这正是 AI 用 ★ 标出来的那一处。多伦·维尔是个用数值、而非价值判断形容词来说话的人物,而"算不上好事"却滑向了常见老年 NPC 的腔调。这是一行让语气变浑浊的台词。

于是重新提出请求。

第 2 行"算不上好事"使用了形容词('好'),违反 voice_profile 的说话习惯。
只把这一行改用数值或观测类词汇重写。第 1·3 行保持不变。
格式·幻觉·验证规则照旧适用。

AI 把第 2 行重新答成了"3年前是 9.0。这就是答案。(漠然)"。它没用形容词,而是以数值的变化揭示危机,并再次通过了 voice_profile 的 5 个项。一次往返就闭合了。从头用手写出三行拿捏好语气的台词,与"填好槽位的一页提示词 + ★ 审核 + 一次往返"相比——后者的审核负担更小,这就是这次会话的结论(基于笔者经验;绝对耗时会随 NPC 语气难度而变,因此应当作方向来读)。


22.1.3 四层结构 —— 一页提示词如何层层堆叠

把上面那页提示词为什么按那个顺序堆叠记录成一页,从下一份提示词起,就能像填空一样把槽位补上。上下文自下而上,按由重(几乎不变)到轻(每次都变)的顺序堆叠。不变的层加以缓存,以节省成本(§22.1.5)。

L0 愿景 · 语气 (world_premise · tone_manifesto) 几乎不变 → 缓存。原则① 上下文的基础。

L1 voice_profile · 命名规则 · 地区设定 区分该 NPC 的 5 个项 + 明示未知范围 → 原则③ 前置工作。

L2 邻接台词 · forbidden_names 场景紧邻的台词·禁止重复的名称 → 使其与旁边台词衔接。

L3 作业指示(每次都变) [输出格式]② · [幻觉阻断]③ · [验证请求]④ "正好3条 · 40字 · (情感)标签 · 禁止资料外 · 5项自检"

从下(重·缓存) → 到上(轻·每次替换)层层堆叠

§22.1.2 的那一页,就跟这张图一模一样。L0·L1 从资料里取出、贴进槽位(原则①·③的基础),L3 里放入格式·幻觉·验证三个块(原则②·③·④)。下一次生成 NPC 台词时,变的只有 L1 的 voice_profile 和 L2 的邻接台词。L0 与 L3 的骨架可以复用——于是提示词就成了一座"库"。


22.1.4 把提示词当资产 —— 库与版本管理

上面的 npc_dialogue 提示词,不是用一次就扔的。按领域、按任务分门别类放进文件,每次不再重写,而是调用。项目A的提示词文件夹长这样。

prompts/
├── narrative/
│   ├── npc_dialogue_v3.txt        # ← §22.1.2 就是这个文件
│   ├── quest_synopsis_v2.txt
│   └── consistency_check_v1.txt
├── balance/
│   ├── change_proposal_v2.txt
│   └── outlier_analysis_v1.txt
├── content/
│   ├── city_npc_batch_v2.txt
│   └── side_quest_v3.txt
└── meta/
    ├── meeting_summary_v2.txt
    └── decision_card_v1.txt

文件名末尾的 _v3 是关键。提示词做一次并非就完事,而是接近"决策"的资产,所以每次改动都要测量结果的变化,再提升版本号。npc_dialogue 走到 v3 的路径正是如此。

flowchart LR A["提示词 v2<br/>(无验证槽位)"] --> B["用相同的 N 个输入<br/>同时产出 v2·v3"] B --> C{"A/B 对比<br/>废弃率·语气违规·审核时间"} C -->|v3 更小| D["采用 v3<br/>npc_dialogue_v3.txt"] C -->|无差异| E["保留 v2<br/>(变更被否决)"] classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class B ai; class C human; class A data; class D pass; class E fail;

从 v2 升到 v3 的实际改动,就是 §22.1.2 里的 [验证请求] 块。v2 里没有让 AI 逐项自我验证自己的输出、并打上 ★ 的槽位。加进这一个块后,就像 §22.1.2 第 4 步那样,AI 开始率先上报模棱两可的行,人从头到尾全读一遍去挑错的负担因而减轻。不做测量、只凭"感觉上变好了"是不升版本的。用同一组输入把 v2·v3 的输出并排放好,确认语气违规的件数和审核时间确实减少之后,才予以采用。

库带来的最大效果,体现在新成员身上。入职第一天调用 npc_dialogue_v3.txt,就能一上手用上资深成员往返几十次打磨出的四层槽位结构。在把"如何写好提示词"练成本能之前,他手里已经握着写得很好的一页。


22.1.5 诚实对待成本的方法 —— 缓存与 cap

提示词一长,token 成本就跟着来。本章不会写"靠标准化省下了 ×2 成本"这类未经验证的倍数,而只谈实际可测量的东西。

控制成本的结构性装置有两个。第一,§22.1.3 里把 L0·L1 放在下层的理由就是缓存。把几乎不变的愿景·语气层缓存起来,每次调用就不必重新发送、重新计费这一层。生成 100 次 NPC 台词时,把 L0 重发 100 次和只缓存一次,两者的差距会随调用累积而拉大。第二,设一个每次调用的 token cap,不把太多任务一次性硬塞进一份提示词。

这里重要的是:成本被测量的地方是真实存在的。项目A的 atom(最小知识单元)系统里,_economy_log/(token·时间的经济性日志)与 _roi_report.md(ROI(Return on Investment,投资回报)报告)作为运营元数据存在。提示词标准化的效果,是在这份日志里以实测来追踪的,而不是在正文的表格里写上一个像模像样的数字来主张。本书的原则,是以下三者之一。


22.1.6 常见的失败

模式 为何失败 处方
"给我写 5 句台词"这样的一行指令 上下文为 0 → 一般 RPG 平均值 §22.1.2 的四层槽位提示词
voice_profile 里没写'未知范围' AI 编造资料之外的设定 在世界观_知识槽位明示界限(原则③)
没有验证槽位,只接收输出 人得从头全部读一遍 [验证请求] 块做自我验证 + ★(原则④)
每次都重写提示词 同样的经验从 0 重新捏一遍 prompts/ 库 + 版本
凭感觉采用提示词的改动 无法确认是否变得更好 同一输入做 A/B 测量后再升版本
每次调用重发长上下文 token 成本随调用次数累积 缓存 L0·L1 + 每次调用设 cap

第六项发现得最晚。成本在单次调用时并不疼,量产累积之后才会在 _economy_log 里显形。


游戏之外的应用。一句一行的指令,招来那种"放到哪儿都不违和、因而不合我这份活儿"的平均值结果,这并不只是游戏台词的问题。提示词就是把活儿交给新人的作业指示书——把四样东西装进一页:依据什么来作答(上下文),数量·长度·禁止项(输出格式),"资料之外不要编造"(幻觉阻断),"自己标示符合哪些标准"(验证请求)——产出就会更省审核负担。比如人事负责人拿到招聘启事的初稿时,若明示"只写职务要求资料里有的项,资料中没有的福利·薪资不要编造,用 [待确认] 标出",就能防止那些看似合理、实则捏造的条件混进启事的事故。把常用工作的指示书留成一页文件,它立刻就成了同事的起跑线。

22.1.7 动手试试 —— 今天就能做的一步

一个人的话,只做这些就够:不需要库,也不需要缓存。挑一个你自己游戏(或你喜欢的游戏)里的 NPC,把 §22.1.2 第 1 步的 voice_profile 5 项(年龄·说话习惯·已知范围·禁忌·关系)用手写下来,再把第 2 步的提示词正文原样贴上,跑一次看看。在跑出的三行里,自己挑出与 voice_profile 相悖的一行,反驳它一句"这行违反了说话习惯这一项,只重写这一行",你就能亲身体会到提示词的四个槽位各自在做什么。

如果是团队,就从下面这一步开始。挑一件常用的工作(例如 NPC 台词),把一页 §22.1.2 形式的提示词作为文件放进 prompts/narrative/。先确认四个块(槽位·格式·幻觉·验证)是否都齐了,这一个文件立刻就成了团队新成员的起跑线。版本管理和缓存,是之后的事。

网页聊天机器人的最小路径(无需终端)——本章的四条原则,不需要文件·库·缓存,只用一个网页聊天机器人(ChatGPT 或 Claude 网页版)的输入框就能照样运作。因为提示词工程不是工具的问题,而是"一页里放什么"的问题。下面两步是主线。 1. 挑你自己的一件工作,把 §22.1.2 第 1 步的五个项(年龄·说话习惯·已知范围·禁忌·关系;游戏之外的话,可换读为'对象·语气·依据范围·禁止·关系')用手写下来。不需要 YAML,也不需要文件,写进聊天机器人的输入框就行。 2. 在其下方原样贴上 §22.1.2 第 2 步的提示词正文,只需确认四个块是否都齐了——[输出格式](数量·长度·标签·禁止),[幻觉阻断]("资料之外不要编造,需要时写 [无资料]"),[验证请求]("逐项写出符合/违反,没把握就打 ★")。跑完之后,只对打了 ★ 的行由人来判定、反驳一次,一个周期就闭合了。库·版本·缓存,等到要反复使用同一份提示词时,再引入即可。


22.1.8 下一章预告

22.2 讲幻觉与安全性。如果说本章的原则③(幻觉阻断槽位)是一页提示词之内的第一道防线,那么 22.2 要看的,是在运营层面拦住那些突破防线的幻觉的多层防御。


本章要点

下一章预告

22.2 自信地说谎的同事 —— 用验证关卡拦住幻觉

主要读者:用 AI 大量产出文档、数据、决策记录的游戏策划(中等规模(10\~50 人)团队) 面向单人/业余读者的精简版:§22.2.7「独自一人的话,做到这些就够」

那天,我让 AI 把 17 份会议记录汇总成决策卡。输出很整洁。决策 ID、引用的会议日期、乃至一行依据,格式都完美无缺。其中一张卡片上写着“2026-04-18 战斗 TF 会议确定冷却时间策略”。问题在于,那天根本没有开过战斗 TF 会议。AI 把其他会议的议题和日期混在一起,编造出一张看似可信的卡片,而正因为格式完美,它险些就这样被写入了团队的决策记录。

这就是幻觉(hallucination)。LLM 越是不了解,反而答得越自信。用人来打比方,就像会议上一位同事斩钉截铁地说“哦那件事就是这么定的”,可事后一查,根本没有过这样的决定。这句话一旦流入数据表、客服回复、atom(最小知识单元)资产,就会酿成事故。本章讨论的不是如何封住这位同事的嘴——那是不可能的——而是如何建立在放行他的话之前必经的验证关卡(verification gate,即由人或检查器把关验证的环节,类似质量门禁 quality gate)。幻觉的一般论述在别的书里已有很多,本章只聚焦于用 AI 工作流拦住它的那个环节


22.2.1 幻觉降不到 0 —— 所以要设“关卡”

没有哪条提示词能把幻觉降到 0。更大的模型、更好的提示词能降低频率,但不会归零。所以运营的出发点不该是“消除幻觉”,而应是“在幻觉触及决策与数据之前设一道拦住它的关卡”。

关卡的核心原理只有一条:凡是 LLM 可能编造的东西(引用、数值、ID),都在 LLM 之外的地方来验证。验证的来源不外乎三种:代码(确定性)、原始文档(grep)、或人的眼睛。再问 LLM 一句“帮我确认对不对”也可以算关卡的一环,但那只是辅助,不是最终裁决者。

这里先厘清游戏策划最常搞混的一点:幻觉易发的领域和不易发的领域,是有明显区别的。

任务 幻觉风险 原因 关卡
数值计算(奖励、概率) 极高 LLM 靠估算做算术 计算交给代码,禁止让 LLM 做
引用(会议、决策 ID) 会把不存在的出处编得像模像样 与原始文本 grep 比对
分类(标签、类别) 中等 会弄混标签 可做确定性比对
摘要、推理 中等 会多加或漏掉条目 自我验证 + 人工关卡
创作(风味文本) 没有标准答案,“幻觉”概念本就模糊 语气评审关卡

第一行是最简单的处方:数值别交给 LLM。就像把乘法交给计算器一样,把它交给确定性工具。第二行(引用)才是本章的脊梁。在会议记录汇总、决策卡这类原文本存在、而 LLM 去转述它的任务里,幻觉最危险,也最容易被抓住——因为有原文本可供比对。


22.2.2 [实操记录] 用自我验证抓出会议记录汇总里的幻觉

只抽象地写“要验证”,根本看不出到底做什么、怎么做。本节完整地走一遍——从输入到再次请求——汇总一份会议记录、再抓出这份汇总里幻觉的整个循环(这类完整保留下来的真实操作过程记录,本书称为“实操记录”,worked transcript)。下面的提示词可以直接复制使用,输出则是对一次真实会话的还原。

第 1 步 —— 输入:把原始会议记录原样丢进去

首先要有待汇总的原文本。它就是验证的基准点。如果没有原文本、任由 LLM 凭“记忆”来汇总,就没有东西可供比对了。

# 会议记录 —— 2026-04-15 战斗平衡评审

出席:李旼洙、成员 A、成员 B
议题:
1. 群体技能冷却时间 —— 有意见认为当前的 8 秒太短。考虑上调至 12 秒。
   结论:暂定为 12 秒,下个版本查看遥测数据后再最终确定。
2. Boss 阶段转换 HP —— 拟从 50% 下调至 30%。
   结论:搁置。与美术演出排期冲突,下周再议。
3. 新增异常状态“冰冻”的持续时间 —— 已讨论但数值未定。

第 2 步 —— 首次汇总提示词

将附上的会议记录汇总成决策卡。

规则:
1) 每个议题汇总为 1 张卡片。格式:[议题 | 结论 | 状态(确定/暂定/搁置) | 依据]
2) 禁止生成会议记录中没有的信息。不得推测数值、日期、出席者。
3) 未得出结论的议题,状态置为“未定”,不得擅自编造结论。

输出格式:仅表格。禁止解说。

第 3 步 —— Claude 的首次输出(还原,节选)

| 议题             | 结论              | 状态 | 依据                       |
|------------------|-------------------|------|----------------------------|
| 群体技能冷却时间  | 8 秒 → 12 秒      | 暂定 | 查看遥测数据后再确定         |
| Boss 阶段转换     | 50% → 30% 搁置    | 搁置 | 与美术演出排期冲突           |
| 冰冻持续时间      | 定为 3 秒         | 确定 | 套用标准异常状态持续时间基准   |

看第三张卡片。原文本里只写了“冰冻持续时间已讨论但数值未定”。可输出却写成了“定为 3 秒 / 确定 / 套用标准基准”。这三项全都是会议记录里没有的信息。是 LLM 用“异常状态一般大概持续几秒吧”这类常识把空白填上了。因为格式完美,光看是发现不了的。

第 4 步 —— 再次请求自我验证(关卡的核心)

这里不直接接受输出,而是强制它逐行与原文本比对、做自我验证。这正是本章的关键一招。

将刚才生成的 3 张决策卡,与附上的原始会议记录逐项比对。

对每张卡片的所有字段(结论、状态、依据、数值):
- 是原文本中直接写明的内容吗? → [一致]
- 是原文本中没有、而你添加的内容吗? → 标为[生成],并注明没有出处
- 有原文本中存在、却在卡片里漏掉的条目吗? → 报告为[漏项]

凡在原文本中无依据的[生成]字段,一律退回为“未定”。

第 5 步 —— Claude 的自我验证输出(还原,原样呈现)

比对结果:

卡片 1(群体技能冷却时间)
- 结论“8 秒→12 秒”:[一致] 议题 1“上调至 12 秒”
- 状态“暂定”:[一致] “暂定”
- 依据“查看遥测数据后再确定”:[一致]

卡片 2(Boss 阶段转换)
- 结论“50%→30% 搁置”:[一致]
- 状态“搁置”:[一致] “搁置。下周再议”
- 依据“与美术演出排期冲突”:[一致]
- [漏项] 原文本中“下周再议”的条件在卡片里漏掉了。需补入依据。

卡片 3(冰冻持续时间)
- 结论“定为 3 秒”:[生成] —— 原文本为“数值未定”。3 秒是我自己添加的值。
- 状态“确定”:[生成] —— 原文本并未得出结论。
- 依据“套用标准异常状态持续时间基准”:[生成] —— 原文本中没有提及这样的基准。
  → 依据规则,将卡片 3 退回如下:
  | 冰冻持续时间 | (数值未定) | 未定 | 会议上已讨论但无结论 |

这段输出是本章最有价值的部分。AI 把自己的输出与原文本比对,自白“3 秒是我自己添加的值”,并依规则把卡片 3 退回为“未定”。与此同时,它还抓出了卡片 2 里连人都漏看的漏项(“下周再议”的条件)。幻觉(多加了不存在的)与漏项(漏掉了存在的)是一枚硬币的两面,同一次比对就能把两者都抓住。

需要注意的地方也很明确:这套自我验证并非万能。如果 LLM 读错了原文本,也可能自信地给出错误的比对结果。所以自我验证只是关卡的第一道,原文本较短时,还要由人用 grep 再兜一层底。像卡片 3 那样明显的生成,自我验证几乎都能抓到;但细微的意译、语气上的失真,终究要由人工关卡来把最后一关。


22.2.3 验证关卡 —— 一张流程图

把上面的循环一般化,AI 输出在触及决策与数据之前所要经过的关卡如下。需要人动手的地方只有两处:把干净的原文本放进去的最前端,以及做出自动关卡抓不到的判断的最末端。

flowchart TB A["原文本(会议记录、数据表)<br/>验证的基准点"] --> B["AI 首次生成<br/>摘要、决策卡、分类"] B --> C{"自我验证<br/>与原文本逐项比对<br/>[一致]/[生成]/[漏项]"} C -->|发现生成字段| D["生成字段 → 退回为“未定”"] D --> E C -->|一致| E{"确定性关卡<br/>数值、ID、引用 grep 比对"} E -->|数值/ID 不一致| F["拒绝 + 再次请求<br/>(用原文本的值校正)"] F --> B E -->|通过| G["人工关卡<br/>细微差别、语气、语境判断"] G -->|驳回| F G -->|批准| H["决策记录 / 纳入构建"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class E code; class B,C ai; class G human; class A data; class H pass;

关卡之所以是三重,是因为每一道抓的东西不同。自我验证由 LLM 自己比对、抓有没有多加不存在的内容;确定性关卡用代码抓数值、ID 与原文本是否逐字一致;人工关卡抓内容虽对、语境却错位的情况。只开其中一道,另外两道原本把守的位置就会漏出事故。在 §22.2.2 中,卡片 3 的“3 秒”在第一道(自我验证)被抓,卡片 2 的漏项也在第一道被抓;而假如自我验证把“12 秒”误读成“21 秒”,就会在第二道(grep)被拦下。


22.2.4 关卡即便失败也不阻断流程 —— hook 的安全设计

把验证关卡放进自动化流水线时,新手最常酿成一种事故:让关卡本身一崩溃,整个作业就停摆。一旦 grep 因编码错误而挂掉,或清单文件损坏,本想帮忙验证的代码,反而把用户的作业整个堵死。于是团队不出一两周就会说“把那个验证关了吧”。

这里原样引用本书实际运营中的 JIT atom 注入钩子(hook,inject_memory.py)处理这一问题的方式。这个 hook 会在用户每次敲下提示词时介入、注入相关记忆,可以说是一道常开的关卡。它的设计原则注释里明确写着一行。

设计原则:
- 始终 exit 0(即便失败也不得妨碍用户流程)
- 未匹配则返回空响应(正常)

而且这条原则在整段代码里都被一致地贯彻。哪怕 stdin 解析失败、哪怕清单 JSON 损坏、哪怕 atom 正文读取失败——全都落到 emit_empty(),并 exit 0

def emit_empty() -> None:
    sys.exit(0)

def main() -> None:
    try:
        ...
        payload = json.loads(raw)
    except Exception:
        emit_empty()        # 输入损坏也静默通过
        return
    ...
    try:
        manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
    except Exception:
        emit_empty()        # 清单损坏也不阻断作业
        return

if __name__ == "__main__":
    try:
        main()
    except Exception:
        emit_empty()        # 任何异常的最后一张网

设计的核心在于把关卡的失败与内容的失败分离开来。hook 注入记忆失败,在用户看来只是“一次没挂上记忆的普通会话”,并不是作业被堵死的事故。验证关卡也该如此。grep 关卡若因编码问题跑不起来,不是把那张卡片放行,而是标注“自动验证失败——转人工关卡”,移交给人工这一道。不能因为关卡挂了就自动批准未经验证的输出,同时也不能因为关卡挂了就让整条流水线停摆。同时满足这两点的安全默认值,就是“静默移交给人”。inject_memory.py 里的 except: emit_empty() 正是这一模式的最小实现。


22.2.5 如何诚实地对待幻觉率

很想在本章塞进一张“把幻觉率从 89% 降到 3%”之类的表。可这种数字若不交代测量方法,只会折损全书的可信度。本书遵循以下三条原则。

第一,只把可测量的东西用数字说。要承诺一个幻觉率,就得定义分母和分子。分母是“经评审的决策卡数”,分子是“在原文本比对中被抓出至少 1 处[生成]/[漏项]的卡片数”。没有这个定义,“幻觉率 5%”就是空话。笔者在引入初期评审会议记录汇总时,实际清点用的正是这套方法;而那批样本很小,不是精确的总体参数,而是一个方向值

第二,模型之间的比较只谈方向。“大模型比小模型幻觉更少”这个方向,能够被稳定地观察到。但“Opus 3%、开源 7B 20%”这类绝对数值会随任务、提示词、领域而大幅波动,所以本书不主张绝对值。只取方向(模型越大幻觉越少,但与成本相互抵触)。

第三,公开标准照原样引用。本章几乎没有可供编造的标准数值,但像 temperature 这样的设置值,是模型 API 文档里的公开事实。验证、分析类任务把 temperature 调低(更接近确定性),创作类任务则调高——这不是推测,而是 API 行为的定义。

所以本章真正承诺的可测量指标有三个——[生成]检出数(自我验证抓到的幻觉数)、grep 关卡拒绝数(数值、ID 不一致数)、人工关卡驳回数。这三者每个季度都能靠日志清点,在会议上就能不靠“感觉”、而用数字说话。


22.2.6 常见的失败

模式 为何失败 处方
只看格式就接受 AI 汇总 幻觉在格式完美时最难被发现 与原文本比对的自我验证(§22.2.2 第 4 步)
不放原文本、凭 LLM 记忆汇总 没有可比对的基准点,无法验证 先把原文本放进输入
把数值计算交给 LLM 算术靠估算,每次都不同 计算交给确定性工具(§22.2.1)
验证关卡一崩溃,整个作业停摆 团队会把关卡关掉 exit 0 + 转交人工这一道(§22.2.4)
关卡挂掉就自动批准未验证输出 幻觉原样通过 关卡失败 = 标注“未验证”
把自我验证当作最终裁决 LLM 误读原文本时,连误判也很自信 短原文本要并行人工 grep

游戏之外的应用。“自信地说谎的同事”——把不存在的会议日期或决定以完美格式编造出来的 AI——不只在游戏决策卡里危险,在一切文档汇总中同样危险。幻觉在格式完美时最难被发现,所以在有原文本的任务(会议记录汇总、合同摘录、报告整理)中,关键是不要直接接受输出,而要强制它做自我验证:“逐项与原文本比对,凡你添加的内容一律标为[生成]”。举例来说,让法务助理汇总一份合同后,再让它把金额、日期、条款编号与原文本逐字比对,AI 就会自白“这个违约金数值是我自己添加的值”,并把空白退回为“未定”。数值计算干脆别交给 AI,而交给计算器、公式;自动验证工具则要设计成即便失败也不阻断作业,而是转为“未验证——待人工确认”。

22.2.7 动手试试 —— 今天就能做的一步

独自一人的话,做到这些就够:不需要代码,也不需要 hook。把你手头的一份短文档(会议便签、更新公告、一页策划)交给 AI 汇总,然后把 §22.2.2 第 4 步的自我验证提示词原样粘进去。只要“逐项与原文本比对,凡你添加的内容一律标为[生成]”这一行,AI 就会开始主动申报自己的幻觉。哪怕只拿到一次[生成]自白,你也会切身体会到:为什么不能对 AI 汇总照单全收。

如果是团队,就从下面这一步开始。把自我验证环节作为基础提示词,固定进 AI 生成的决策卡与汇总里(§22.2.2)。接着,只挑出数值、决策 ID、日期这类必须与原文本逐字一致的字段,用代码写出 grep 比对。这时那段验证代码务必像 inject_memory.py 一样,设计成即便失败也不阻断作业(exit 0 + 标注“未验证”)(§22.2.4)。哪怕只有自我验证和 grep 这两道,也能先挡住“格式完美的幻觉渗入决策记录”这个最常见的事故。


本章要点

下一章预告

22.3 AI 成本管理 —— 用代码守住 token 预算

主要读者:在团队中引入 AI 工具并为成本负责的策划主管(中等规模(10\~50 人)团队) 面向单人/业余读者的精简版:§22.3.9「单人只需做到这些」

如果一章讲成本却举出虚假的成本,这本身就是自相矛盾。所以本章不会去做一张"我们团队每月省了多少"的漂亮表格。取而代之,只用两类数字。一类是任何人都能核实的公开 token 单价(各模型每 1M token 的费用),另一类是笔者亲自运营的 hook 代码中写死的常量(max_atom_body = 6000max_matches = 3)。两者都不是编造,而是引用来的。

AI 成本可怕,不在于金额大,而在于看不见。引入的头一个月调用少,账单也小。可一旦上下文变长、调用变频繁,某个季度的账单就会突然多出一个数量级。先把本章的结论摆出来 —— 成本不是靠"省着点用"的决心,而是靠在每次调用中强制削减 token 的代码来控制。挡住它的不是人的意志,而是 wrapper 与 truncate。


22.3.1 LLM 成本实质上只有'输入 token'这一项

成本项有输入、输出、缓存命中、缓存写入四类,但在实务中主导账单的是输入 token。原因很简单。在游戏策划中使用 AI 的几乎所有工作,都是"塞进长上下文、拿回简短回答"的形态。把 L0 愿景文档、atom 库、相邻城市的正文、数据表摘录全部塞进去,输入就是数万 token,而输出只是一张表,不过几百 token。

因此,成本控制的第一要务不是"减少输出",而是"在哪里削减输入 token"。这一句话贯穿本章其余部分。

先把各模型的公开单价钉死。下面是 Anthropic 公开的每 1M(100 万)token 费用,是照原样引用本书写作时那一代(Opus·Sonnet·Haiku 当时最新等级)公开单价的快照(引自官方公开单价 —— 会随模型代际·时点变动,套用前务必核对当前价目表)。正如附录 K 所归纳的原则,这里不变的不是单价的绝对值,而是三个等级之间的单价比例。因此,下表不是用来看"今天的账单",而是用来读懂"等级越往下调,单价就成数量级下降"这一结构。

模型 输入 1M token 输出 1M token 备注
Claude Opus $15 $75 顶级推理(公开单价)
Claude Sonnet $3 $15 中档 —— 输入价为 Opus 的 1/5
Claude Haiku $0.80 $4 轻量 —— 输入价约为 Opus 的 1/19
缓存命中(read) 约为标准输入价的 1/10 复用已缓存输入时(公开缓存政策)

关键在最后两行。同样的工作用 Haiku 而非 Opus 来跑,输入 token 单价约为 1/19;把同样的上下文放进缓存,那部分的输入价约为 1/10。成本节省的两大支柱由此而来 —— 模型合理选型与缓存。两者都不是"少用",而是"用更低的单价处理同一件事"的结构。

节省来自单价差异,而非意志。把 Opus 降到 Haiku 约省 19 倍,放进缓存约省 10 倍,都是自动发生的。


22.3.2 最大的输入成本是'每次调用都注入的上下文'

有一种成本比单项工作的单价更悄无声息地累积 —— 每次调用都自动附加的上下文。笔者的个人 PC 上运行着一个 hook,每当用户敲入提示词,它就自动把相关记忆(atom)塞进去(UserPromptSubmit hook,inject_memory.py)。这是个便利功能,但同时也是成本泄漏的头号嫌疑。每次输入都有很长的 atom 正文进入上下文,若放任不管,输入 token 会随每次调用膨胀。

所以这个 hook 里固定了三重削减成本的安全装置。它们不是抽象论,而是实际代码中的常量。

# inject_memory.py —— UserPromptSubmit hook(实际运营代码,节选)
# 设计原则(docstring 原文):
#   - 始终 exit 0(即便失败也不打断用户流程)
#   - 按 score 降序注入最多 3 个 atom
#   - atom 正文超过 6000 字时 truncate

# (1) 从 manifest config 中读取预算常量
max_matches = cfg.get("max_matches", 3)      # 单次调用最多 atom 数
max_body    = cfg.get("max_atom_body", 6000) # 每个 atom 的正文上限(字)

# (2) 按 score 降序排序 —— 让昂贵的槽位按价值顺序填充
atoms_sorted = sorted(atoms, key=lambda a: a.get("score", 0), reverse=True)

matches = []
for atom in atoms_sorted:
    if len(matches) >= max_matches:   # (护栏 A)在最多 3 个处截断
        break
    if re.search(atom["regex"], prompt, re.IGNORECASE):
        matches.append(atom)

# (3) 注入正文时在 6000 字处截断
for atom in matches:
    body = atom_path.read_text(encoding="utf-8")
    if len(body) > max_body:          # (护栏 B) truncate
        body = body[:max_body] + "\n\n[...truncated]\n"

三重成本护栏都在这里。

这三个常量就是每次调用输入 token 的上限。粗略估算,一个 6,000 字的 atom 在韩语中大约是数千 token 的规模(准确的 token 数会因分词器·语言而异,所以应把它当作"存在上限"这一结构来读,而不是绝对值)。3 个 × 6,000 字就是单次调用的注入预算,超出部分由代码切掉。人不需要用眼睛去发现"atom 附加得太多了"。


22.3.3 [实操记录(worked transcript)] 一行 6000 字 truncate 如何遏制成本

光说"truncate 能遏制成本"是空洞的。实际制定这个常量时,我和 AI 把一个完整循环从头跑到尾。下面是对那次会话的忠实再现。输入提示词可以原样复制使用,输出则是对真实会话的重现。

第 1 步 —— 输入:原样抛出问题情境

刚启用 hook 不久,_injection_log.txt 里就留下了一次调用中 atom 正文被整段注入的记录。当很长的 atom(复盘·健康上下文等)每次调用都以完整大小进入,越是高频使用的工作,输入 token 累积得越多。我问了 AI 该在哪里截断。

第 2 步 —— 提示词:强制限定与格式

我有一个每次调用都会把 atom 自动注入到上下文的 hook(inject_memory.py)。
我想设定一个能遏制输入 token 成本的 truncate 上限,请你提议每个 atom 的正文字数
上限,以及单次调用注入的 atom 数量上限。太短会切掉核心,
太长又会漏 token,所以也请一并点明其中的权衡;对没把握的地方
单独标记出来,交给人来定。(意图:上限值以 config 外部化为前提)以表格给出:
项目 | 建议值 | 依据 | 把握度。

请注意,提示词有一半都是"强制限定与格式"。在成本相关的问题上放任 AI 自由发挥,就会得到"视情况而定"这类不负责任的回答。强制给出具体数值、要求明示权衡、并留一个'没把握就上报'的出口,才是这条提示词的核心。

第 3 步 —— Claude 的输出(重现,节选)

| 项目            | 建议值  | 依据                                          | 把握度 |
|----------------|--------|-----------------------------------------------|-------|
| max_atom_body  | 6000 字 | 韩语单个 atom 的"核心+依据"大多在 4~6 千字     | 中    |
|                |        | 以内。6000 字时正文开头(要旨·规则)基本        |       |
|                |        | 保留,只切掉结尾(示例罗列)。                 |       |
| max_matches    | 3      | 单条输入同时强匹配的主题通常为                 | 高    |
|                |        | 1~3 个。4 个以上是 regex 过宽的信号            |       |
| 外部化位置      | _jit_manifest.json > config | 无需重新部署代码即可运行中调整 | 高    |
| truncate 标记   | 附加 [...truncated] 结尾    | 让模型·人知道此处被截断      | 中    |
| 没把握之处      | 6000 是否为精确值           | 各分词器的 token 换算因语言·  | ★低   |
|                | (字→token 换算不确定)      | 模型而异,字为单位只是近似值   |       |

输出里最有价值的不是表格,而是最底下那行 ★。AI 主动上报:"不确定 6000 是否为精确的最优值,字→token 的换算并不确定"。好的提示词能让 AI 说出'我不知道'。

第 4 步 —— 验证与否决(人的位置)

我没有原样接受这份输出。AI 把 6000 字提为"核心得以保留的界线",但实际在 6000 字处截断到底切掉了什么,必须亲自核实。我挑出最长的 atom(健康上下文、复盘累积本),用眼睛看了 6000 字那个位置。

核实结果发现一处问题。6000 字那个位置从正文中间切断了一个表格行,像 | 维生素D | 1000IU | 这样的行在一半处被截断。语义虽保留了,但被切断的表格行以残缺的 Markdown 形式进入模型,总归让人不放心。于是我再次发起请求。

第 5 步 —— 再次请求

在 6000 字处截断时,有时会把 Markdown 表格的某一行从中间切断。是否值得加一个
把截断位置吸附到前一个换行边界的修正?实现不能比 truncate 本身还复杂,
所以请权衡成本与收益,连做不做都一并下结论。

AI 这样回答 —— "吸附到行边界用 body[:max_body].rsplit('\n', 1)[0] 一行即可实现,但truncate 的目的是 token 上限,而非表格完整性,所以一行残缺的表格行对成本·行为的影响微乎其微。目前仅凭 [...truncated] 标记,模型就能识别'此处被截断',因此建议不做行边界吸附。若一定要做,一行修正就够了。"

这个回答是对的。若在成本控制代码里连表格完整性也贪求,护栏代码本身就会臃肿。当前运营代码保持 body[:max_body] + "[...truncated]" 的简单截断。这是一个由人验证 AI 的首个提议(6000 字)、再由 AI 压下过度修正的贪念,一次往返就闭合的循环。


22.3.4 成本护栏的结构 —— 一览

上面会话中定下的常量,在实际调用中如何削减输入 token,把整个流程用图示记录下来。

flowchart TB A["用户输入提示词"] --> B["regex 匹配<br/>遍历 atom 库(共 17 个)"] B --> C{"护栏 A<br/>len(matches) >= 3?"} C -->|"从第 4 个起拦截"| D["break —— 不注入"] C -->|"槽位有余"| E["按 score 降序<br/>填充槽位"] E --> F{"护栏 B<br/>正文 > 6000 字?"} F -->|"超出"| G["body[:6000] + [...truncated]"] F -->|"未超"| H["注入全文"] G --> I["注入上下文<br/>(保证输入 token 不超过上限)"] H --> I D --> I classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class B,C,E,F,G,H code; class A human; class I pass; class D fail;

这张图的要点是:无论用户输入什么,每次调用注入的 token 都有一个天花板。天花板是 3 × 6000 字(+标记),超过部分代码一律切掉。成本不依赖用户的自制力。护栏 A·B 在每次调用中机械地运作。

同样的理念在工具层面也重复出现。笔者所在公司的系统有一条政策:把暴露在全局槽位的 wrapper 技能精确固定为 12 个(atom skill_listing_budget_wrapper_only_policy)。会话开始时,若全局 * wrapper 的数量不是 12,清理脚本就会自动运行。名义上是"整理槽位",本质却是保护会话开始时的 token 预算 —— 把技能列表载入上下文的成本锁定在 12 个的量。atom 注入 3 个上限与技能暴露 12 个上限,是同一思想的不同应用。


22.3.5 按任务分配模型 —— 80% 交给更低的单价

如果说护栏遏制的是每次调用的 token,那么模型选择决定的就是这些 token 的单价。在 §22.3.1 的表中,输入价 Opus:Sonnet:Haiku ≈ 19:4:1。所以,把所有工作都用 Opus 来跑,等于连分类·替换这类简单工作也要付 19 倍的单价。

按工作的复杂度分配单价。

工作类型 推荐模型 理由
与验证·法务直接相关、决策分析 Opus 出错就闯大祸的工作 —— 不省这个单价
报告·摘要·自然语言加工 Sonnet 需要质量,但不必用到顶级推理
分类·打标签·关键词提取 Haiku 简单模式 —— 用 Opus 约 1/19 的单价就够
简单映射·替换 Haiku 或确定性方法 很多情况下连 LLM 都不需要

以经验来看,大部分工作用 Sonnet·Haiku 就够。昂贵的模型只用于"出错代价高的工作"。不过有一个陷阱 —— 降到过于便宜的模型会导致幻觉增多,验证成本会把节省下来的金额吃掉(与上一章 §22.2 幻觉·安全性直接相关)。所以模型分配不是"一律便宜",而是"出错也无妨的工作用便宜的,出错代价高的工作用贵的"这样的分流。

最后一行"简单映射·替换 → 确定性方法"往往才是最大的节省。像名称替换、按既定规则映射这类答案唯一确定的工作,根本不需要调用 LLM。把调用本身归零,才是最便宜的调用


22.3.6 缓存 —— 相同的输入按 1/10 单价

即便遏制了每次调用的 token(护栏)、压低了单价(模型分配),只要每次调用都重新发送相同的上下文,成本就会泄漏。像 L0 愿景文档、atom 库、领域风格指南这类几乎不变的长输入,就做缓存。缓存命中时,那部分输入按标准价的约 1/10 计费(§22.3.1 表)。

# 不变的上下文用 cache_control 标记 —— 缓存命中时约为 1/10 单价
messages = [
    {"role": "system", "content": SYSTEM_PROMPT},
    {"role": "user", "content": [
        {"type": "text", "text": L0_VISION,    "cache_control": {"type": "ephemeral"}},
        {"type": "text", "text": ATOM_LIBRARY, "cache_control": {"type": "ephemeral"}},
        {"type": "text", "text": SPECIFIC_TASK},  # 只有每次变化的部分放在缓存之外
    ]},
]

关键在于把会变的部分和不变的部分分开。缓存要求输入的前段相同才会命中,所以把固定上下文(L0·atom)放在前面,把每次都变的工作指令放在后面。

把什么放进缓存,按变更频率来划分。

上下文 缓存 理由
L0 愿景(几乎不变) 适合 只以数天\~数周为单位变动
atom 库 适合 仅在复盘时更新
领域风格指南 适合 以季度为单位变更
近期会议记录 不适合 每天变化 —— 缓存命中率低
用户输入 不适合 每次调用各不相同

缓存 TTL 短则以数分钟为单位,所以在连续叩击相同上下文的工作(如量产 30 座城市那样把同一个 L0 复用 30 次)中效果最大。对一次性问题,只会花掉缓存写入成本却命不中,反而可能吃亏 —— 所以只对"频繁·连续使用相同上下文的工作"有选择地应用。


22.3.7 诚实对待数字的方法

成本这一章,是最容易让人想放一张"把每月 $5,000 降到 $1,000"这类表格的地方。那种绝对节省额会因团队规模·工作量而千差万别,一旦编造,成本章就成了对成本撒谎的自相矛盾。本章只用了三类数字。

第一,公开单价原样引用。§22.3.1 的 Opus $15 / Sonnet $3 / Haiku $0.80(输入 1M token)、缓存命中约 1/10,都是 Anthropic 公开的费用。输入价比例 19:4:1、缓存约省 10 倍,是从这些公开单价用算术得出的值 —— 不是估测,而是计算。

第二,代码常量引用代码。max_atom_body = 6000max_matches = 3 是实际记录在 inject_memory.py_jit_manifest.json 中的值。不是比喻,而是真实文件。

第三,不知道的就写不知道。"6000 字是多少 token"会因分词器·语言·模型而异,字为单位只是近似值。§22.3.3 中 AI 也把这一点用 ★ 上报了。所以本章任何地方都没有"6000 字 = N 个 token = 省 $X"这类换算表。取代绝对节省额,只用方向与比例(19 倍·10 倍)来讲。

本章的成本数字,要么是公开单价(Anthropic 价目表),要么是写死在代码里的常量(inject_memory.py·_jit_manifest.json),要么是明确标注"不知道"的近似值。


22.3.8 常见失败

模式 为何失败 对策
所有工作都用顶级模型 连分类·替换都付约 19 倍单价 按任务分配模型(§22.3.5)
每次调用重发相同上下文 白白丢掉缓存命中的 1/10 缓存固定上下文(§22.3.6)
自动注入没有上限 整座 atom 库每次调用都被注入 数量·长度护栏(§22.3.2)
靠"省着点用"的决心管理成本 人的自制力挡不住暴涨 把上限固定在代码里
连确定性方法能办的事也调用 LLM 最便宜的调用是'不调用' 把映射·替换分离到代码里

第四条是核心。把成本控制交给人的意志,一定会泄漏。意志在忙碌时最先崩塌,而成本在忙碌时增长得最快。所以控制必须是 max_matches = 3 这样的代码常量


游戏之外的应用。AI 成本可怕,不在于金额大,而在于看不见,这一点无论对游戏团队还是市场团队都一样。成本不是靠"省着点用"的决心,而是靠结构来管住。第一,按工作难度分配模型单价 —— 连简单的分类·打标签都用顶级模型来跑,就等于为同一件事付好几倍单价;而简单映射·替换干脆不调用(用规则·公式处理)才是最便宜的调用。第二,对几乎不变的长输入(公司介绍·政策文档·术语表)做缓存,减少重发成本。举例来说,给客户咨询分类这件事用轻量模型就够,只把复杂的合同审查交给更高档的模型,就能在保住质量的同时把单价分流。如果有某处会自动附加很长的上下文,那么事先定好"一次附加的数量·长度上限",就能从结构上防止某天账单突然多出一个数量级的事故。

22.3.9 动手试试 —— 今天就能做的一步

单人只需做到这些:没有 hook、没有 manifest 也可以。在你常用的 AI 工具里,把下一件工作的模型下调一档试试(原来用 Opus 做的摘要改用 Sonnet,原来用 Sonnet 做的分类改用 Haiku)。只要输出质量足够,那件工作就永久固定在更低的单价上。哪怕只在每件工作上问一次"这件事真的需要顶级模型吗",节省的一半就出自这里。

如果是团队,就从下面这一步开始。找一个会自动注入上下文的地方(hook·系统提示词·RAG),在那里用代码放入 §22.3.2 的两道护栏(注入数量上限 1 个、正文长度上限 1 个)。像 inject_memory.py 那样把上限用 config 外部化,就能在运营途中无需重新部署代码、只调数字。两行护栏就能从结构上防止"某天账单多出一个数量级"的事故。

用 setup → prompt → verify 概括 —— setup:在自动注入点放入数量·长度上限常量,并抽到 config 里。prompt:以 §22.3.3 的格式让 AI 提议上限值,同时强制它给出权衡与把握度。verify:挑出最长的输入,亲自用眼睛确认在上限处切掉了什么。


本章要点

下一章预告

22.4 著作权与伦理 —— 用一套流程闭合产出物的权利、标示与共识

主要读者:负责引入 AI 的游戏总监与主管(中等规模(10\~50 人)团队) 面向单人/业余读者的精简版:§22.4.9「独自一人的话,做到这些就够了」

发售前两个月,我们曾因一张概念美术师绘制的城市插画而让整场会议陷入停滞。有人问:"这是用 AI 生成的吧?那著作权归我们,还是根本无法登记?"没有人能回答。当场出现了三种意见:"是 AI 做的,所以不归我们""是我们花钱跑出来的,所以归我们""反正还没有法律,直接用就是了"。三种都不对。而且这个问题并不只是一个法务议题。制作那张插画的美术师其角色究竟是什么、团队对 AI 的使用达成了怎样的共识,都在那一刻同时被牵扯了进来。

本章不会把著作权与伦理分开来讲。因为在实务中,二者是同一个问题的一体两面。"这份产出物的权利归谁"(著作权)会直接归结为"人在这份产出物中介入了多少"(伦理·角色)。韩国著作权委员会在 2025 年明确钉定的登记要件,恰恰就在这一点上。所以本章的脊柱是一份实操记录(worked transcript,完整保留的真实操作过程记录)—— 我们会实际判定一张 AI 概念美术图能否进行著作权登记,并从输入到决策一路追踪这一判定如何延伸为团队的角色共识。

作者实际运营备注 本章引用的 design_intent_vs_automation_boundary atom 以及 _economy_log·_roi_report.md,是将作者在公司实际运营的治理资产做了匿名化处理后的产物。atom 名称与日志文件名原样保留了实际运营中使用的名称(仅为保护 IP 而替换了公司与项目的专有名词)。实操记录的输出是对真实判定会话的重现。


22.4.1 权威来自公开指南,而非"感觉"

很多书只把 AI 著作权写成"因为还没有法律,所以模糊不清"。这只对了一半。2025 年 6 月,文化体育观光部与韩国著作权委员会发布了《生成式 AI 利用作品的著作权登记指南》,至少在韩国,能否登记的界线已经清晰。无须编造。

指南的核心可以浓缩为一句话:著作权登记的要件是"人类的创作性贡献"。 由此分为两类。

分类 定义 登记
GAI 产出物 没有人类创作性贡献、由 AI 生成的结果物 不可
GAI 利用作品 人类将 AI 作为工具制作的成果中,被认定具有创作性贡献的部分

指南还给出了被认定为"利用作品"的三条路径。① 将使用者自身的作品作为提示词输入,使其创作性体现在产出物中;② 对产出物进行修改·增删的追加工作本身具有创作性;③ 对产出物进行选择·排列·构成时具有创作性。判断的两条轴线是"可控性"与"可预测性"。创作者必须能够明确决定自己想要表现的内容,并按照其意图导出结果物,才会被认定具有创作性。

这一处是决定性的。指南用法律语言所说的"可控性·可预测性",与本书自 §1.1 起反复强调的"策划提供意图"(planner_provides_intent_not_recommendation atom)是同一回事。完全交给 AI 的产出物没有可控性与可预测性,因而也没有著作权;而由人输入意图、经人审校·重构的产出物,权利便随之而来。著作权能否登记,与良好 AI 工作流的条件,处在同一条线上。

还有另一项公开标准。2026 年施行的《AI 基本法》对生成式 AI 产出物课以确保透明性的义务(标示 AI 生成事实)。登记(主张权利的一侧)与标示(公开使用事实的一侧)是彼此独立的义务。无论是否产生权利,使用了 AI 这一事实本身都必须公开。这两项公开标准,构成本章要交给 AI 的规则手册(rulebook)的首批输入


22.4.2 [实操记录] 判定一张 AI 概念美术图能否登记

回到开头那张插画。我们不靠"感觉"来判断,而是把 §22.4.1 的指南标准作为规则手册输入,让 AI 先做一次分类。人只做最后的判定。下面的输入提示词可以直接复制使用,输出则是对真实判定会话的重现。

第 1 步 —— 输入:原样抛出产出物的生成历史

判定的输入不是插画本身,而是记录该插画如何被制作出来的日志。这些信息已经存在于资产元数据中,只需提取即可。

# asset_concept_city021_v4.meta.yaml —— 判定对象产出物的生成历史
asset_id: concept_city021_v4
asset_type: concept_illustration
created_by: 团队成员 A(概念美术师)
generation_log:
  - step: 1
    actor: 团队成员 A
    action: "将亲手绘制的城市布局粗略草图作为输入图像附上"
  - step: 2
    actor: AI (image_model)
    action: "以粗略草图 + 提示词生成 4 种变体"
    prompt: "stone observatory tower over sealed magic core, cold arid, scholar guild, muted palette"
  - step: 3
    actor: 团队成员 A
    action: "从 4 种中选取 1 种,亲手对钟楼剪影·饱和度·构图进行精修(约 40% 区域重做)"
  - step: 4
    actor: 团队成员 A
    action: "亲手设计背景的封印阵纹样并合成"
ai_generated_disclosure: true   # 满足 AI 基本法的标示义务

第 2 步 —— 提示词:依指南标准分类,但强制给出依据

附上的 meta.yaml 是一张概念插画的生成历史。请依据韩国著作权委员会
《生成式 AI 利用作品的著作权登记指南》(2025)的标准,把它能否登记
分类为 可登记/部分登记/不可登记 三者之一。依据请用指南的两条轴线(可控
性·可预测性)各给出一行,并在 generation_log 中用编号指出哪个 step 属于
"创作性贡献"。法律上无法断定的,标为"需法务确认"交给人处理。
用表格呈现:分类 | 依据(可控) | 依据(预测) | 贡献 step | 法务确认。

请注意,这段提示词有一半是在做"强制依据"与"法务退路"。如果把法律判断整个交给 AI,它会吐出貌似可信的断言。给出分类标准(指南的三分法)、依据义务(两条轴线)、指定贡献 step,以及"无法断定就交给法务"这一退路——这才是这段提示词的核心。

第 3 步 —— Claude 的输出(真实会话重现)

| 分类     | 依据(可控性)                 | 依据(可预测性)              | 贡献 step | 法务确认 |
|---------|------------------------------|----------------------------|----------|---------|
| 部分登记 | 粗略草图(step1)与 40% 精修    | 从 4 种中选择可预测,        | 1,3,4    | ★       |
|         | (step3)直接控制表现意图        | 但 AI 的 4 种变体本身不可预测 |          |         |

分析:
- step1(直接输入粗略草图):对应指南 ①"以本人作品输入体现创作性"。
- step3(40% 精修):对应指南 ②"修改·增删的追加工作"。贡献最强。
- step4(亲手设计封印阵纹样):合成独立作品,明显的人类创作。
- step2(AI 的 4 种变体):无人类创作性贡献的纯 AI 产出物 → 这部分单独不属于登记对象。

★ 需法务确认:"40% 精修"是否达到"实质性改变",缺乏定量标准。
  精修比例是否越过可登记的界线,建议由律师审查。
  (AI 无法断定该比例在法律上是否充分。)

输出中最有价值的部分不是表格,而是最下面的★ 法务确认。AI 主动申报了自己的界限——"我无法断定 40% 精修在法律上是否充分"——并把它交给了人。好的提示词会让 AI 能够说出"我不知道"。这不是自动拒绝型,而是自动申报型。

第 4 步 —— 验证与否决(人的位置)

不能原样接受这份输出。总监用规则手册再核查一遍。AI 把 step4(封印阵纹样)分类为"独立作品",但重新查看生成历史后发现,那个纹样其实派生自 §6.2 中 city_hunting_generator 生成的城市 lore。也就是说,step4 未必是纯粹的原创,而可能是叠加在公司内部资产之上的二次加工。由于是公司资产,权利归属很明确;但 AI 所用的"独立作品"这一表述若原样写进登记申请书,会招致误解。

于是再次发出请求。

step4 的封印阵纹样是从公司内部城市 lore 资产派生的二次加工(并非独立的全新原创)。
请据此重新分类 step4 的贡献性质。
并用一行提议:登记申请时应如何注明"基于既有内部资产"。

这一次往返就闭合了。AI 把 step4 从"独立作品"改答为"公司内部 lore 资产的演绎作品——原资产权利归公司所有,变形贡献为登记对象",该判定随后交由法务审查。结论最终定为部分登记 + 标示 AI 生成事实。若全部手工进行,法务就得逐个资产追问生成历史;而有了 AI 初稿 + 规则手册审校 + 一次往返,法务只需把时间花在标了 ★ 的边界案例上。

这一整圈就是本章的 Show 标准。"AI 著作权很模糊"这句话,在你尚未依指南标准把一份产出物的生成历史彻底分类一遍之前,都是空洞的。


22.4.3 决策树 —— 这份产出物,能用吗

为了不必每次都从头做上面那种判断,我们把指南标准记录成一张流程图。每进来一个资产,顺着这棵树往下走即可。所有分叉点都出自 §22.4.1 的公开标准。

flowchart TD A["AI 产出物产生"] --> B{"人是否输入·控制了意图?<br/>(粗略草图·本人作品·详细指示)"} B -->|否 仅提示词| C["不可登记产出物<br/>→ 仅供探索·概念参考<br/>禁止直接用作最终资产"] B -->|是| D{"是否对产出物进行了修改·增删<br/>或选择·排列?"} D -->|否| C D -->|是| E{"是否为训练数据<br/>已明示的模型?<br/>或公司内部 fine-tune"} E -->|否·模糊| F["等待法务审查<br/>评估侵权风险后决定"] E -->|是| G{"是否派生自<br/>公司既有资产?"} G -->|是| H["演绎作品<br/>原资产权利归公司<br/>+ 变形贡献登记"] G -->|否| I["可部分/全部登记"] H --> J["标示 AI 生成事实<br/>(AI 基本法义务)<br/>+ _economy_log 记录"] I --> J F --> J classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class A ai; class B,D,E,F,G human; class J data; class H,I pass; class C fail;

关键在于:这棵树的终点(J)在所有路径上都相同。无论能否登记、是公司资产还是演绎作品,"使用了 AI"的事实标示与生成历史日志都无一例外地保留。 标示是与权利无关的独立义务,而日志是出事时追溯责任的唯一依据。开头那场会议之所以停滞,正是因为没有这份日志,没有人能重现每个 step 由谁做了什么。

红色路径(C,不可登记)也并非直接丢弃。"仅输入提示词的纯 AI 产出物"在探索·概念阶段作为参考完全可用,只是不把它作为最终资产放进游戏而已。把 AI 输出原样推上线,是事后著作权事故最大的诱因。


22.4.4 把著作权规则手册变成运营日志 —— design_intent_vs_automation_boundary

树(§22.4.3)是判断的流程,而让这条流程每次都画在同一条线上的,是一个 atom。在公司的治理资产中,design_intent_vs_automation_boundary 就是本章整体的脊柱。

这个 atom 的一句话定义是:"设计意图由人负责,自动化由工具负责——每个资产都明示这条边界。"这不是抽象的口号。这个 atom 已注册进 JIT hook(inject_memory.py),因此当提示词中出现"著作权""AI 生成""资产登记"之类关键词时,它会被自动注入会话。hook 的设计原则直接支撑了这个 atom 的运营。

# inject_memory.py —— 始终 exit 0,即使失败也不阻断用户流程(节选)
def main() -> None:
    ...
    # 按 score 降序排序后匹配 —— 最多只注入 3 个 atom
    atoms_sorted = sorted(atoms, key=lambda a: a.get("score", 0), reverse=True)
    matches = []
    for atom in atoms_sorted:
        if len(matches) >= max_matches:   # 防止过度注入
            break
        try:
            if re.search(atom["regex"], prompt, re.IGNORECASE):
                matches.append(atom)
        except re.error:
            continue
    if not matches:
        emit_empty()   # 无匹配则返回空响应(正常)
        return

这里在治理上有两处重要设计。第一,hook 始终 exit 0(脚本 docstring 中已注明)。即便在注入著作权规则时失败,也绝不阻断用户的工作。一旦安全装置把工作扣为人质,团队会在一两个季度内把这个装置关掉。第二,最多只注入 3 个。若每次会话都把所有治理规则一股脑塞进来,上下文会爆掉,谁也不会读。只有 score 高的规则才会浮现。

这与 §6.2 中 lint 不自动废弃违规、而只向作者关卡(gate)发出 alert 的哲学如出一辙。可疑候选由机器挑出,但要弃要留由人决定。 著作权上也一样。atom 会自动弹出"这个资产的著作权确认了吗",但能否登记的最终判定由人和法务来做。


22.4.5 权利之后是人 —— 用共识闭合角色演进

开头那张插画的判定并未止于著作权。因为它意味着,制作该资产的团队成员 A,其工作已从"绘制"转移到了"从 AI 的 4 种中做选择、并精修其中 40%"。在著作权要求"人类创作性贡献"的那一刻,做出这份贡献的人,其角色定义也随之改变。二者是同一事件的一体两面。

这里最常见的事故,是把这种变化当作单方通知来处理。即便工具再好,半年后无人使用,多半不是工具不好,而是当初没有达成共识。角色从量产转向选择·审校·重构,这才是引入 AI 的本质;若不把这一点明示出来并以培训加以支撑,团队成员就会把它理解为"我的位置要没了"。

职群 AI 之前 AI 之后(角色演进) 著作权上的意义
概念美术师 全部亲手作画 输入意图·选择·精修 精修即"创作性贡献"
数值策划 手动模拟 解读模拟·做决策 决策日志即责任依据
策划 全部亲笔撰写规格 提供意图·审校 输入意图即可控性

这张表想说的一句话是:让著作权登记成为可能的"人的贡献",正是角色演进之后人所做的事。 一旦指南所要求的可控·可预测消失,著作权会消失,人的位置也会消失。所以,角色演进必须被解释为"把权利与责任留在人手中"的变化,而非"抢走岗位"的变化,才能达成共识。

共识不是无休止的开会,而是用流程闭合:引入提案(总监)→ 全团队事先共享(目的·受影响角色·衡量指标·风险)→ 共识会议(自由发言·收集顾虑)→ 与必要成员一对一 → 发布调整方案 → 达成共识或搁置。并不是必须得到所有成员的同意才能启动,而是通过流程听取并调整顾虑之后,由总监拍板。没有流程,共识每次都要从 0 重新开始,而这种成本会拖慢引入。


22.4.6 成本·ROI 也是伦理的一部分 —— 用 _roi_report.md 诚实以对

如果把伦理只收窄到岗位·共识,就会漏掉一条轴线。诚实地衡量并公开 AI 运营的成本与效果,本身就是治理。若不做衡量,只是嘴上说"用 AI 提升了效率",团队成员会怀疑这句话是在为削减自己的岗位找借口。

公司的治理基础设施中,为此配备了两项真实资产:atom 系统的 _economy_log/(token·时间经济性日志)与 _roi_report.md(ROI 报告)。前者由机器记录每次会话的 token·时间,后者则按周期汇总供人阅读。关键在于:这份日志追踪的不是"AI 替代了多少人",而是"把人的时间释放到了哪里"。

本书的数值原则有三条。第一,公开标准原样引用(指南的登记要件、AI 基本法的标示义务)。第二,作者的推测就写明是推测。第三,只把可衡量的东西作为 KPI 来承诺。在著作权·伦理领域,可衡量的不是结果指标,而是流程指标。

衡量项 衡量方法 能否承诺
AI 生成事实标示的缺失数 对资产元数据 ai_generated_disclosure 做 grep 可衡量(目标 0)
生成历史日志的持有率 generation_log 的资产比例 可衡量
未经法务审查即上线的 AI 资产 上线构建 vs 法务通过清单比对 可衡量(目标 0)
"因为 AI,营收上升了" —— 不可衡量,不作承诺

最后一行是诚实性的核心。AI 引入对营收的影响无法作为单一变量分离出来,因此不对因果下断言。但"AI 生成资产中,通过了标示·日志·法务审查的比例",可以用 _economy_log 和资产元数据实际数出来。治理承诺的不是结果,而是流程的完整性。


22.4.7 用户生成内容(UGC)与数据保护

把资产权利·角色·成本理顺之后,还剩一个领域:用户用 AI 制作的内容上传到游戏的通道。公司制作的资产用内部流程闭合,而 UGC 则是控制之外的产出物源源涌入。

这里同样直接套用 §22.4.3 树的终点(标示·日志)。用户上传的时装·公会徽章要求标示 AI 生成,并以自动审校 + 人工关卡的组合来进行审核(moderation)。只运营其中一条轴线,下个季度的事故就会累积。另外,用户数据不能随意发送给 LLM。个人信息·支付信息禁止传输,行为日志则以匿名化后再传输为原则(遵守 GDPR·韩国国内个人信息保护法)。

管辖并不止于韩国一处。一旦接纳海外用户,该用户所属地区的数据法规也会一并适用。对 EU 用户,GDPR 对个人信息的跨境转移·同意·删除权设有专门要求;其他服务所在国家也各有其个人信息·数据本地化规定。因此,表格第三行的"个人信息·支付信息禁止传输给 LLM"在任何管辖区都是最安全的默认值;而把行为日志发送给外部模型时,匿名化·假名化处理的强度则应按服务所在地区另行核查。不过,本节只是流程设计的指引,并非法律咨询。一旦涉及全球发行·跨境转移,务必另行接受相应管辖区的法务审查。

UGC/数据 政策 依据
用户上传的时装·徽章 AI 标示 + 自动审校 + 人工关卡 AI 基本法标示义务
角色昵称·发帖 通用条款 + 用户责任 ——
个人信息·支付信息 禁止传输给 LLM 个人信息保护法
游戏行为日志 匿名化后传输 匿名化·假名化处理

UGC 越多,审核负担越重。自动审校先做一次筛除,只有边界案例才交由人来看。这正是把 §22.4.4 的 atom 哲学(机器挑候选、人来决定)搬到了用户内容层面。


22.4.8 常见的失败

模式 为何失败 处方
把 AI 产出物原样用作最终资产 人类贡献为 0 → 不可登记 + 侵权风险 §22.4.3 树,仅限探索·概念
没有生成历史日志 出事时无法按 step 重现责任 强制资产元数据带 generation_log
未标示 AI 生成事实 违反 AI 基本法透明性义务 树终点的标示步骤无一例外
把角色演进当作单方通知处理 工具引入半年后被弃用 共识流程(§22.4.5)
只喊"用 AI 提升了效率" 团队成员疑其威胁岗位 _economy_log·_roi_report 公开衡量
无批判地使用训练数据模糊的模型 遗漏侵权风险评估 优先选用已明示模型·公司内部 fine-tune(树的 E 分叉)

第五条最常被忽略。正如开头那张插画的判定所示,让著作权登记成为可能的"人的贡献",正是这个人的新角色。若只衡量效率,而不衡量这个人的时间被释放到了哪里,治理会在 KPI 上成功,人却会离开。


游戏之外的应用。 "这个是用 AI 做的,著作权归我们吗"这类问题让会议停摆的情况,不只出现在游戏插画上,凡是用 AI 制作的报告·广告文案·提案书,处处都会发生。韩国著作权委员会 2025 年指南钉定的标准很简单——能否登记取决于"人的创作性贡献(可控性·可预测性)",而仅输入提示词的纯 AI 产出物没有权利。因此,无论哪个部门,给每一份 AI 产出物留下一张用 step 记录"哪一步是人、哪一步是 AI"的生成历史,就会成为一张安全网。举例来说,营销人员拿到 AI 文案初稿后亲手修改·重构,只要把这份贡献记录下来,就能成为主张权利的依据;而无论最终能否登记,"使用了 AI"这一事实标示(2026 年 AI 基本法义务)都无一例外地保留。权利之后是人,所以做出这份贡献的员工其角色变化,不能靠单方通知,而必须用共识来闭合。

22.4.9 动手试试 —— 今天就能迈出的一步

独自一人的话,做到这些就够了:没有法务团队也没关系。挑一件你自己用 AI 制作的图像或文本产出物,按 §22.4.2 的格式亲手写一份 generation_log(用 step 区分哪一步是人、哪一步是 AI)。然后顺着 §22.4.3 的树,自己判定一遍"这个能登记吗",你就会亲身体会到韩国著作权委员会指南里"可控·可预测"标准究竟是怎样一组判断。哪怕是个人·业余项目,也最好留下一行 AI 生成事实标示(ai_generated: true)。

如果是团队,就从下面这一步开始。给所有 AI 资产元数据强制加上 generation_logai_generated_disclosure 两个槽位(用一行代码的 grep 就能抓出缺失),并把 §22.4.3 的决策树贴成一页维基。登记要件的判定自动化或 _economy_log 的运营,都是后话。哪怕只有生成历史日志和一页决策树,也足以避免像开头那样会议陷入停滞。


22.4.10 第 22 部分小结

第 22 部分讲的是治理的四条轴线。

核心
22.1 提示词工程 —— 强制格式·依据·退路
22.2 幻觉·安全性 —— 审校关卡·申报型验证
22.3 成本管理 —— 缓存·cap·_economy_log
22.4 著作权·伦理 —— 登记要件·标示·角色共识

贯穿这四章的一句话是:治理不是阻挡 AI 的装置,而是把人的意图与责任留在产出物中的流程。 design_intent_vs_automation_boundary atom 就是这套流程的名字。著作权登记所要求的"可控·可预测"、伦理所要求的"角色·共识"、成本所要求的"诚实衡量",都指向同一处——无论 AI 做什么,决策与责任的最后一席都属于人。


本章要点

下一章预告



来源 - 韩国著作权委员会·文化体育观光部,《生成式 AI 利用作品的著作权登记指南》(2025)—— https://www.copyright.or.kr/information-materials/publication/research-report/view.do?brdctsno=54253 - 韩国著作权委员会·文化体育观光部,《生成式 AI 著作权指南》(2023.12)—— https://www.copyright.or.kr/information-materials/publication/research-report/view.do?brdctsno=52591 - 《人工智能基本法》(AI 基本法)生成式 AI 产出物的透明性·标示义务(2026 施行)—— https://www.shinkim.com/kor/media/newsletter/3142

Part 23 · 第1章. Wrapper·Cascade·Junction 模式

不要增加工具,而要制造工具的工具。这是关于一种两层结构——在全局 12 个入口背后隐藏 48 个本体——以及无需人工干预维持其一致性的自动化的故事。


某个跑月度复盘的傍晚,数着斜杠命令列表时,我的手停住了。有 40 个。明明半年前是从七八个起步的,可就这么做一个会议记录工具、加一个数据校验工具、再添一个 GDD(Game Design Document,详细规格文档)生成器,一周增加一两个,不知不觉就成了 40 个。而且其中将近一半,在过去一个月里一次都没被调用过。

问题在于,不用的工具并不是安静地待在那儿而已。每次开始会话,40 个斜杠命令的规格说明都会全部被加载。它蚕食了 token 预算,名称相近的命令(skill-design·skill-design-new·skill-design-template)容易混淆,而真正需要某个工具时,又要花时间才能想起它。工具不再是帮着干活,反倒是管理工具本身成了一项工作。

本章讲的,就是把这 40 个收敛为全局 12 个、却又一个本体都没舍弃的过程。核心是三种模式:制造轻量入口的 Wrapper、把多个工具收进一个入口的 Cascade、把入口与本体在物理上连起来的 Junction。还有替人守住这三者一致性的 sync_skills.py


23.1.1 从复盘中发现的量化信号

工具太多这种印象人人都会有。但仅凭印象,无法决定该削减什么。让决策成为可能的,是月复盘对工具经济性的测量。

本项目把复盘作为自我改进机制来运营。日复盘累积成周复盘,周复盘汇聚成月复盘的过程中,月复盘会从 SVN 提交日志中反推"过去一个月各工具用了多少次"。用于这项测量的分数就是 skill_audit_score。它通过提交历史追踪每个斜杠命令在实际工作产出中出现了多少,从而给出使用频率。

那个月测量得到的分布如下。(使用量占比是基于 SVN 提交日志的实测,不是绝对调用次数,而是各工具的出现占比。)

斜杠命令 40 个 —— 使用频率分布

TOP 12 命令 使用量的 92%

中等使用 10 个 —— 约 8%

每月不足 1 次 18 个(占总数 45%) —— 几乎 0%

来源:月复盘 skill_audit_score,SVN 提交日志反推 / 占比为出现比重实测

排名前 12 的命令占了总使用量的 92%,而每月一次都用不到的命令有 18 个,占总数的 45%。答案已经定了一半:只把常用的 12 个暴露在全局,其余的整理归置。

问题在于,"整理"并不等于"删除"。那 28 个不常用的命令,每季度也总有一两次会用到——写半年报告时,建立新的数据模式时,或运行某项特定校验时。那时如果工具不在,工作就会当场停下。所以真正的问题是:如何只让 12 个可见,同时把 28 个保留下来。

书桌的比喻贯穿整章。没有人会把 40 支笔全摊在桌面上天天用。只把常用的 12 支放在桌上,其余收进抽屉。抽屉里,同类的笔再归到一个笔筒里。Wrapper 是放在桌面上的轻量入口,Junction 是连接抽屉与桌面的通道,Cascade 是捆在一个笔筒里的一束笔。


23.1.2 Wrapper 模式 —— 轻量入口,重量本体

Wrapper 是斜杠命令的一层薄壳。全局只放入口,实际逻辑放在 workspace 的本体里。全局目录里住着 50 行的说明,本体里住着 500 行的实现。

flowchart LR subgraph G["全局 ~/.claude/skills/ (书桌上)"] W1["proj-meeting<br/>Wrapper · 50 行"] W2["proj-gdd<br/>Wrapper · 50 行"] end subgraph B["workspace/skills/ (抽屉里)"] M1["proj-meeting/<br/>SKILL.md + 提取·分类 .py<br/>约 500 行"] M2["proj-gdd/<br/>SKILL.md + 生成器<br/>约 500 行"] end W1 -->|调用| M1 W2 -->|调用| M2 classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; class W1,W2,M1,M2 code;

这一分离带来五点好处。会话开始时全局只加载 50 行,节省 token;本体即使每天修改也不影响全局槽位;本体可以放在 SVN、Git 或任何地方;本体放在团队共享文件夹、只把 Wrapper 放在个人全局,便于共享;统一 Wrapper 的格式后,用户体验保持一致。

Wrapper 的标准格式如下。所有 Wrapper 都共享这一骨架。

---
name: proj-meeting
description: 会议记录分析·决策提取 (本体: workspace/skills/proj-meeting/)
---

# /proj-meeting —— Wrapper

本体位置:workspace/skills/proj-meeting/SKILL.md

## 工作方式
该 Wrapper 调用本体的入口脚本。详细逻辑定义在本体中。
本体变更时,只需更新该 Wrapper 的 description(建议自动同步)。

关键在于只有一行 description 和一个本体指针。逻辑一旦进来,Wrapper 就会变重,与本体的同步也开始失效。因此以规则强制 Wrapper 保持在 100 行以内。

本项目的全局斜杠命令槽位固定为 12 个。常用工具必须全部装进这 12 个,选择标准由月复盘来把关:每月使用 5 次以上、领域均衡(单一领域的工具不超过 6 个)、入口一致(命名规则统一)。一旦超过 12 个,就废弃使用最少的那一个,或将其并入其他命令。

12 这个数字并非绝对。关键在于"数字被固定下来"这件事本身。小规模(\~10 人)团队也许 10 个合适,领域繁多的团队也许 15 个更合适。只有存在既定上限,认知负担才会停留在一定水平。


23.1.3 Junction 模式 —— 本体与入口的物理连接

如果说 Wrapper 是"全局只放轻量入口"这条规则,那么 Junction 就是在操作系统层面实现这条规则的手段。Junction 是目录符号链接,也就是 OS 提供的别名。

flowchart LR U["~/.claude/skills/proj-meeting<br/>(Junction —— 别名)"] R["workspace/skills/proj-meeting/<br/>(本体 —— 唯一一份实际文件)"] U -. "实际指向" .-> R classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class R code; class U data;

用户查看全局位置时,看上去本体就在那里。但实际文件只有一份,存在于本体位置。全局那一侧,只是指向那里的一块路标而已。

这一结构带来的好处很清楚。修改本体会立即反映到全局(没有复制步骤)。文件只有一份,节省磁盘;全局只有 Junction,因而不会有 Git 冲突(本体在 SVN/Git 中另行管理)。即使移动本体,只要重新挂上 Junction,对用户来说也毫无变化。

不同 OS 的挂载方式不同。Windows 用 mklink /J <link> <target> 创建目录 junction,无需管理员权限。Linux 和 macOS 用 ln -s <target> <link>,WSL 直接沿用 Linux 命令。这一平台差异由后文将讲到的 sync_skills.py 自动处理,运维者无需亲自记住各 OS 的命令。

如果不用 Junction 而用复制来运营,一旦本体与全局副本产生分叉,就会出同步事故。比如在本体里修好了 bug,而全局副本还是旧版本,于是执行的还是旧行为。Junction 从根本上消除了这种事故的可能。路标不可能有两块,实体永远只有一个。


23.1.4 sync_skills.py —— 替人维持一致性的工具

手动管理 Wrapper 和 Junction,最终还是会回到 40 个。人会拖延整理、忘记政策、制造例外。因此把一致性维持自动化,那个工具就是 sync_skills.py

每次会话开始时,Hook 都会触发这个脚本。脚本所做的事是以下流程。

flowchart TD H["会话开始 (Hook 触发)"] --> S["扫描 ~/.claude/skills/"] S --> C{"12 Wrapper<br/>政策一致?"} C -->|"发现残余槽位"| X["--cleanup:<br/>清理政策外的 Wrapper"] C -->|"检测到本体移动"| J["Junction 自动重建"] C -->|"一致"| OK["通过"] X --> OK J --> OK OK --> R["保证全局 12 槽位一致"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class H,S,C,X,J code; class OK,R pass;

核心功能有三个。第一,扫描全局目录,检查是否符合 12 Wrapper 政策。第二,用 --cleanup 标志清理不在政策内的残余 Wrapper。若有人临时添加的工具残留在槽位里,会在下次会话开始时被清理,槽位不会再次暴增。第三,若本体位置发生变化,就自动重新挂上 Junction:检测 OS,Windows 选用 mklink /J,其他则选 ln -s 调用。 重要的是,这三项功能都设计为 幂等(idempotent)。它是每次会话开始时自动运行的工具,因此在同一状态下反复运行多次,结果都应与运行一次相同。已经符合政策的 Wrapper 不去动,已经正确挂好的 Junction 不再重挂,没有需要清理的残余槽位就什么都不删。若不幂等,每次会话都会叠加同样的整理,从而出现重建好端端的 Junction、或误动本体的事故——对于每次会话都无人值守运行的工具而言,这会直接演变为同步事故。因此 sync_skills.py 把"只动改变过的,没改变就不动"作为不变式。

--cleanup 的效果直接关系到 token 预算的保护。把每次会话加载到全局的斜杠命令说明固定为 12 个,即使本体增加到 48 个,会话开始的成本也保持恒定。因为不靠人工管理,政策也不会走样。

这种自动一致性,就是两层结构的安全销。Wrapper 与 Junction 搭出结构,sync_skills.py 让这一结构随时间推移仍得以维持。


23.1.5 两层结构 —— 全局 12 wrapper → workspace 48 本体

三种模式与自动一致性结合起来,就完成了下面的两层结构。上层是用户需要记住的 12 个入口,下层是 48 个本体。

flowchart TD subgraph L1["第一层 —— 全局 12 Wrapper (用户需记住的全部)"] direction LR w1["#1"] -.- w12["#12"] end subgraph L2["第二层 —— workspace 48 本体 (隐藏的实体)"] direction LR b1["本体 1"] --- bN["本体 48"] end L1 -->|"通过 Junction 连接"| L2 note["sync_skills.py --cleanup:<br/>每次会话把第一层对齐为 12 个"] note -.-> L1 classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; class w1,w12,b1,bN,note code;

用户只需记住全局的 12 个。哪怕它们背后藏着 48 个本体,认知负担也停留在 12 个。Wrapper 让入口保持轻量,Junction 把入口与本体连起来,sync_skills.py 在每次会话中守住这 12 个的一致性。

按比例看,入口与本体是 1:4(12 比 48)。工具再增加,用户要记的也不会增加。本体增加到 60 个、80 个,第一层仍然是 12 个。这就是"不要增加工具,而要制造工具的工具"这句话的实际实现。增加的是第二层(本体),而用户面对的第一层(入口)始终恒定。


23.1.6 Cascade 模式 —— 用一个入口串起的连锁调用

如果说两层结构是"把众多工具收敛为少数入口"的模式,那么 Cascade 就是"把经常一起用的工具打包进一次调用"的模式。一个斜杠命令依次调用多个下级工具,再把结果汇成一份综合报告。

本项目最具代表性的 Cascade 是 check。它把每天早上检查策划数据完整性的四个工具整合成了一个。

flowchart TD E["/check (Wrapper · Cascade 入口)"] --> S1["doc-audit<br/>Markdown 一致性"] S1 --> S2["data-qa<br/>数据表校验"] S2 --> S3["integrity<br/>外键一致性"] S3 --> S4["link-check<br/>Wikilink 完整性"] S4 --> R["综合报告<br/>(仅失败详列,通过项汇总)"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class E,S1,S2,S3,S4 code; class R data;

过去每天早上要分别调用四个工具:文档检查一次、数据检查一次、外键检查一次、链接检查一次。每个工作周期要手动调用三四次。check 把这四个打包为一个命令,只需调用一次,四个阶段就会依次执行,结果合并为一份。

Cascade 的设计有原则。每个阶段都必须能够单独调用(必须能只调用 data-qa)。失败时中断还是继续,由各阶段分别设定。校验类作业即使某一阶段失败,也继续运行其余阶段以看到全局;变更类作业则一旦某一阶段失败便立即停止。结果会累积并成为下一阶段的输入,而综合报告在每个 Cascade 中都采用相同的格式。

check 的实际定义如下。(这是把 4 种校验整合为一个的配置。)

cascade:
  - step: doc-audit
    purpose: Markdown 一致性 (YAML frontmatter·链接·atom 引用)
    fail_action: continue
  - step: data-qa
    purpose: Excel 数据表校验 (模式·取值范围·必填列)
    fail_action: continue
  - step: integrity
    purpose: 外键一致性 (表间引用)
    fail_action: continue
  - step: link-check
    purpose: Wikilink·外部链接完整性
    fail_action: continue

report:
  format: markdown
  include_pass: false   # 通过项仅汇总,失败项详列
  group_by: severity

四个阶段都挂着 fail_action: continue,是因为这是一个校验类 Cascade。即使一项检查失败,也把其余三项跑完,一次性看到当天的全部缺陷清单。报告把通过项折叠为汇总、只展开失败项,把注意力集中在早上真正该看的东西上。

Cascade 也有陷阱。阶段无限增加,复杂度就会爆炸。因此像 12 槽位政策一样,Cascade 也设阶段上限。大致超过五到七个阶段,就拆成两个,或把一部分分离为独立的 Cascade。


23.1.7 工具治理 —— MECE Wrapper 政策

两层结构和 Cascade 一旦稳定下来,增加本体就变得容易——因为不必动全局槽位,只要往 workspace 里添加本体即可。可正是在这里出现了新的陷阱:添加一旦变容易,相似的工具就会重复堆积。

因此在增加本体时强制一条政策:MECE Wrapper 政策。要添加新工具时,分两条路判断。若与既有工具领域重叠,就不新建,而是增强既有工具;只有领域明确不同时才新建。意思是让本体清单做到无重叠(Mutually Exclusive)、无遗漏(Collectively Exhaustive)。

flowchart TD N["需要新工具?"] --> Q{"与既有本体<br/>领域是否重叠?"} Q -->|"重叠"| A["禁止新建 →<br/>增强既有工具"] Q -->|"明确不同"| B["允许新增本体"] A --> M["保持 MECE:无重复"] B --> M classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class Q human; class M pass;

这一判断由复盘来支撑。skill_audit_score 从 SVN 日志中测量各工具的使用频率,于是能捕捉到"这个工具其实和那个工具做的几乎是同一件事,而两者都几乎没人用"这样的信号。那就把两者合并,或把使用较少的一方从本体中撤下。治理中,整理与添加同样重要。

没有 MECE 政策,两层结构带来的"添加本体的自由"反而会变成毒药。即便本体不蚕食第一层槽位,本体本身因重复而臃肿起来,又会让人重新搞不清该用哪个本体。政策阻止这种臃肿。


23.1.8 复盘为何引燃了这一切

到这里,回到开头看看。Wrapper 也好,Junction 也好,Cascade、MECE 政策也好,没有一个是在书桌前预先设计出来的。它们全都是作为对复盘中发现的问题的回答而诞生的。

当月复盘的 skill_audit_score 用量化数据揭示出 40 个槽位与 92% 使用量的失衡时,"限制到 12 个"就此定下。那个决定之后,紧跟着"那 28 个又该如何保留"的问题,答案就是 Wrapper 与 Junction。下一次复盘中出现了"每天早上分别调用四个相似的校验工具太麻烦"的发现,答案就是 check Cascade。又一次复盘中捕捉到"添加本体变容易了,重复就堆积起来"的信号,答案就是 MECE 政策。

没有复盘,这些模式就不会诞生;就算做出来,也会沦为与真实问题无关的过度工程。先用测量发现问题,再引入模式——正是这个顺序,让工具真正被用起来。复盘是自我改进的起点,这一 Part 21 的信息,在工具层面就这样被具体化了。


23.1.9 运营案例 —— 6 个月累计

把六个月的测量值按引入前后作比较。关注的不是绝对调用次数,而是运营负担的变化。

项目 引入前 引入后 (Wrapper+Cascade+Junction)
全局槽位数量 40 个(暴增) 12 个(政策强制)
本体数量 分散·大量重复 48 个(MECE 整理)
会话开始时全局槽位占比 大(加载 40 个说明) 小(仅加载 12 个说明)
本体修改后同步到全局 需手动复制步骤 立即(Junction,无复制)
相似校验工具的调用 每次作业手动 3\~4 次 1 次(check Cascade)

引入首月的测量值参差不齐。在 Wrapper 格式定型之前,同步事故出过两三次;在 12 个政策被强制之前,槽位在 15 到 18 之间来回浮动。稳定是从第二个月开始的。自 sync_skills.py --cleanup 开始在每次会话中整理槽位之后,槽位暴增就再没发生过。

表中"占比"·"需要"·"立即"这类方向性表述是有意为之。因为每个环境的 token 成本与时间都不同,所以只写了变化的方向。可以确定的是,第一层从 40 固定到了 12,而本体同步中手动复制这一步已经消失。


23.1.10 常见错误与规避方法

把前面各节点到的陷阱汇总到一处。这五点,全都指向同一个教训:比起搭出结构,让它随时间推移仍能维持更难。


动手试试(setup → prompt → verify)

setup. 在 workspace 里建一个本体目录(例如 workspace/skills/proj-meeting/)。在里面放 SKILL.md 和实际脚本。全局 ~/.claude/skills/ 里只放 50 行的 Wrapper。

prompt. 向 Claude 提出以下请求。

扫描 ~/.claude/skills/ 中的斜杠命令列表。
把每个命令分类为 (a) 只有本体指针的轻量 Wrapper,
还是 (b) 含有逻辑的重量级命令,
对属于 (b) 的命令,把本体分离到 workspace,
并给出只在全局保留 50 行 Wrapper 的修改方案。
另外,输出为指向本体的 Junction 按 OS 匹配挂载的命令
(Windows 用 mklink /J,其他用 ln -s)。

verify. 确认三点。第一,全局目录中的每一项是否都在 100 行以内。第二,打开全局项时,是否只看到一行本体位置和 description。第三,把本体改动一行后从全局调用时,改动是否立即生效(只要 Junction 挂对了,就会无需复制而生效)。

单人精简版

如果是既没有团队也没有 SVN 的单人运营,就这样精简。不必设 workspace,一个个人 Git 仓库就够了。本体放在那个仓库里,全局只放 Wrapper。即使没有 skill_audit_score 这样的测量工具,只要在月末把"这个月实际调用过的命令"亲手记下来,失衡就会显现。只把排名前五到七的留在全局,其余的下放到本体。Cascade 只需在经常一起调用的工具出现两个以上时,把它们打包为一个命令即可。若嫌自动一致性脚本麻烦,可以用"开始新会话时用眼睛把全局目录扫一遍"的习惯来替代。规模一小,习惯就替代自动化做着同样的事。


本章要点

下一章预告

Part 23 · 第2章 Hermes Agent 引入实录

晚上 11 点 47 分。我保存好最后一份数据表,合上了笔记本电脑。次日早上 9 点 10 分,冲咖啡时打开公司内部的即时通讯工具,发现频道顶部已经挂着一份报告。那是一份 Markdown——它以外键为基准,对昨夜更新过的三张数值表做了交叉校验,并用红色标出了两处断裂的引用。不是我写的。它是在我熟睡时生成的。

本章记录的,是把制作那份报告的工具——Hermes Agent——装到个人 PC 上,并叠加到 §23.1 讲过的 Wrapper·Cascade·Junction 运营之上的整个过程。最初引入时,Hermes 还是基于 Linux 的,要在 Windows 上使用就得先经过 WSL2;到了 2026 年,原生 Windows 构建发布,这道弯路就消失了。先把结论说在前面:智能体(Agent)并没有把 Claude Code 挤走,而是坐到了它旁边。


23.2.1 坐在同一张桌前的两个工具

到 §23.1 为止的运营,全部以 Claude Code 为中心。我输入一句话,工具就响应一次,我审阅完这次响应,再输入下一句。这种短周期对精细作业再好不过。如果是修改一个数值、每一步都需要确认的作业,那就该由人每次介入。

问题出在耗时长的作业上。"把过去一个月的 30 份会议记录全部读一遍,只把决策事项抽取成 atom 候选"这样的请求,若放在对话流里处理,需要 30 个来回。那 30 次里,我没法做别的事。对这类作业,输入与输出短促相连的工具,其长处反而成了短处。

智能体填补的正是相反的位置。只要抛出目标——"从 30 份会议记录里把决策事项挑成 atom 候选,汇成报告"——它就自己挑选工具、自行走完中间步骤,结束后只把结果拿回来。周期长而自主。代价是,每一步人看不到,这个短处也随之而来。

两个工具的作业周期对比

Claude Code 精细 · 单发 · 每步验证 输入→输出 输入→输出 输入→输出 输入→输出 ↑ 每个箭头处都有人工审阅

Hermes Agent 长时 · 自主 · 仅检查点 输入 1 个目标 →(自主执行:工具选择·反复·验证)→ 输出 1 个结果 ● 检查点(可人工审阅的节点)—— 并非每一步

精细决策走上方泳道,反复·长时走下方泳道。同一张桌子。

拿邻座同事来打比方就容易理解。Claude Code 是对我每一句话都一起细看的搭档,智能体则是主动请缨上夜班、在我上班前把报告放到桌上的助手。两者不是谁解雇谁的关系,而是共用同一张桌子。


23.2.2 为何还要再添一个工具

在 §23.1 中,我把全局斜杠命令槽收拢为 12 个,再用 Junction 把背后的 48 个本体藏起来,做成了这样一套运营。不增加工具,而是造出工具的工具——这就是那时的结论。可如今又要引入一个新工具,听上去与那个结论自相矛盾。

并不矛盾。§23.1 的 12 槽策略,处理的是"人直接调用的工具"的认知负担。而 Hermes 要填补的位置,是人不去调用的时间——熟睡的时间、开会的时间、被别的事拴住手的时间。它不是与 12 槽竞争,而是填补 12 槽够不到的时段。

引入决定的依据,是一项复盘测量值。用从 SVN 提交日志反推全局工具使用频率的 skill_audit_score 跑了一个月的数据,发现排名靠前的工具大多属于"在人清醒的时间、短促、频繁"使用的那一类。相反,那些使用频率低、但一旦跑起来就很耗时的作业——会议记录批量分类、数据表夜间一致性、构建捕获分析——却每每被"明天早上再做吧"地往后推。往后推的原因很明确:它们会长时间占用清醒的时间。

这类被往后推的作业群,正是智能体精准瞄准的目标。


23.2.3 安装 —— 原生 Windows 构建

最初装 Hermes 时它基于 Linux,要在 Windows 个人 PC 上使用,就得先装好 WSL2(Windows Subsystem for Linux 2),再把 Hermes 安置进去。如今有了原生 Windows 构建,这道弯路就不必了。安装和一般的 Windows 应用程序一样——下载安装器运行,在首次运行时设定好工作空间路径与权限白名单的初始值即可。

如果你已经在用 WSL2,或偏好 Linux 环境,那一侧的构建同样受支持。只是若从头开始,原生这一侧更简单。具体的安装器与版本因工具更新很快,请以官方文档为准。

无论装在哪儿,都有一个绕不开的坑。Hermes 工作空间必须放在快速的本地磁盘上。若把网络驱动器或 SVN 工作文件夹直接接为工作空间,一次夜间一致性检查,会把本该几分钟的作业拖成几十分钟。数据表放在工作空间之外,只在作业开始时复制进来,才是正道。若用 WSL2,出于同样的原因,要把工作空间放进 Linux 文件系统内,不要来回跨越 /mnt/c 这类 Windows 路径。


23.2.4 Hermes 安装与首次连接 —— 实操记录(worked transcript)

从这里开始,是真正要动手的部分。比起安装本身,"装好之后让它做什么"才是本章的核心;因此我们把第一个作业从头跟到尾——提示词全文、原始输出、人工验证、再请求——完整走一遍,如实保留操作全过程,这样一份记录即所谓实操记录(worked transcript)。这个作业,是我从 §23.2.2 中那些被往后推的作业群里挑出的最简单的一个:数据表夜间一致性检查。

注意:下面的部分命令只是为展示 Hermes 的表层形态而给出的示例形式。安装器 URL 与子命令随版本而变,请查阅官方文档。工作流的结构(目标 → 自主执行 → 验证 → 再请求)不因工具更换而改变。

若是原生 Windows,就在 PowerShell 里下载并运行官方的 install.ps1。不过,在直接执行一行式的 iex (irm ...) 之前,先把脚本(约 2,800 行)下载下来,用眼睛扫一遍其中的危险模式——这是信任来源的最起码步骤——然后把密钥配置分离出来:用 -SkipSetup 先只装本体,再单独跑 hermes setup,这样更安全。若用 WSL2·Linux,则遵循官方文档中对应的安装小节。

# 原生 Windows —— 官方 install.ps1(先下载、审阅后再运行)
irm https://hermes-agent.nousresearch.com/install.ps1 -OutFile install.ps1
# (确认 install.ps1 内容之后)
.\install.ps1 -SkipSetup
# 一并获取 Python 3.11 · Node · Git · Playwright · 捆绑技能
# 安装位置:%LOCALAPPDATA%\hermes\  (将 hermes 命令注册到 PATH —— 从新终端起生效)
# 结束后:hermes setup

安装器会一并装好依赖(Python 3.11·Node 22·Git),把本体安装到 %LOCALAPPDATA%\hermes\,再将 hermes 命令注册到 PATH(从新终端起生效)。配置、日志、预约(cron)、检查点这类运营数据也都留在同一个 %LOCALAPPDATA%\hermes\ 之下,重装也不会丢(这里有个坑——~/.hermes\ 里只放了辅助脚本,很容易搞混。真正的 config.yaml·logs\ 全都在 %LOCALAPPDATA%\hermes\ 那一侧)。首次运行 hermes setup 时,会询问模型 API 密钥,并设定工作空间路径与权限白名单的初始值。

hermes --version
hermes setup

现在把第一个作业交给它。抛给智能体的目标,比 Claude Code 的提示词要抽象一层。不是"把这个照这样做",而更接近"把这个结果给我做出来"。我实际输入的目标全文如下。

[提示词全文]

目标:数据表夜间一致性检查。

读取 ~/hermes-workspace/sheets/ 下的 item_master·drop_table·npc_shop 三个 xlsx,
检查 drop_table·npc_shop 所引用的 item_id 是否真的存在于 item_master 中,做外键
检查。把所有断裂的引用(指向不存在的 item_id 的行)都找出来,以日期为文件名,
用 Markdown 保存到 reports/。(意图:通过的只报数量,失败的用表格列出表名·行号·item_id)

约束:
- sheets/ 只读,绝对不要修改。
- 若读不了表,或列名与预期不同,就停下,报告哪里不一样。禁止臆测。

最后两行很重要。智能体是自主的,一旦卡住,就有"自作主张"去填补的倾向。列名不同时,它会挑一个相近的名字继续跑下去。所以要明确地输入"卡住就停下并报告,不要臆测"。这与 §23.1 里编排数据表验证 Cascade 时学到的教训是同一个道理。

[Hermes 原始输出 —— 第 1 次]

不做概括,原样照录。第一次尝试并没有顺顺当当地结束。

[hermes] goal accepted: 数据表夜间一致性检查
[hermes] step 1/4  reading sheets...
         item_master.xlsx   loaded (4,210 rows)
         drop_table.xlsx    loaded (1,880 rows)
         npc_shop.xlsx      ERROR

[hermes] npc_shop.xlsx: expected column 'item_id', found columns:
         ['shop_id', 'sell_item', 'price', 'stock']
[hermes] this conflicts with the constraint: "列名与预期不同就停下并报告".
[hermes] HALTING. reporting instead of guessing.

[hermes] partial result:
  - item_master ↔ drop_table 检查可行(item_id 列两侧都存在)
  - npc_shop 的 item_id 列名推定为 'sell_item',但无法确定,故未执行
[hermes] awaiting clarification on npc_shop column mapping.

这里正是第一次用智能体的人最吃惊的地方。工具停了下来。而且准确说出了停下的原因。npc_shop.xlsx 的道具引用列,不是我预期的 item_id,而是 sell_item。若没有事先输入约束,智能体就会自作主张把 sell_item 当成 item_id 继续跑,而我在早上只会收到一份"正常"的报告。停下,就是安全。

[人工验证·驳回]

我看着输出,做了两点判断。

第一,报告说 item_master ↔ drop_table 的检查已经可行,这部分值得信任。第二,npc_shop 的 sell_item 推定是对的——它确实是那张表的销售道具列名。只不过,不以"推定"就贸然推进的判断是正确的。于是我没有原样接受第 1 次输出(驳回),而是提交了一份明确写出列映射的再请求。

[再请求提示词]

已确认。明确列映射如下:
- npc_shop.xlsx 的道具引用列是 'sell_item'。把它当作 item_id 来检查。
- 其余约束不变。不要再额外臆测 'sell_item' 之外的列。
再执行一次。

[Hermes 原始输出 —— 第 2 次]

[hermes] resuming with column map: npc_shop.sell_item -> item_id
[hermes] step 2/4  foreign-key check...
         drop_table → item_master   : 1,880 refs, 2 broken
         npc_shop   → item_master   : 642 refs, 0 broken
[hermes] step 3/4  collecting broken refs...
[hermes] step 4/4  writing report...
         ~/hermes-workspace/reports/2026-05-25_sheet_integrity.md  written
[hermes] done. 2 broken references found. summary:
  | sheet      | row  | bad item_id |
  |------------|------|-------------|
  | drop_table | 417  | ITM_90412   |
  | drop_table | 1356 | ITM_90727   |

两处断裂的引用被抓了出来。drop_table 的第 417 行和第 1356 行,指向了 item_master 中不存在的 item_id。早上看到的那张红色表格,正是这个。

在这一个来回里,本章的信息几乎全部显现。智能体虽自主,却在约束面前停了下来;人把它停住的地方补上,它便走到了终点。自主与控制不是冲突,而是咬合。而只要把这整个周期预约成在我熟睡时再跑一次,那就成了 §23.2.5 的夜间自动化。


23.2.5 嵌入游戏策划工作流的三个岗位

第一个作业上手之后,就把被往后推的作业群一个个挪到夜间。我实际叠加上去的是三个岗位。三者的共同点很清楚——它们都把人无需清醒在场的时间,变成了干活的时间。

flowchart TD A["夜间触发<br/>(每日 23:00,cron)"] --> B{Hermes Agent} B --> C1["[岗位 1] 数据表<br/>夜间一致性检查"] B --> C2["[岗位 2] 长期模拟<br/>100 小时量的虚拟游玩"] B --> C3["[岗位 3] 构建捕获<br/>自动分析管线"] C1 --> D1["外键 diff<br/>断裂引用表"] C2 --> D2["Boss 击杀均值·资源消耗<br/>连招分布"] C3 --> D3["规格 vs 实测 diff<br/>逐帧提取"] D1 --> R["[汇总] Markdown 报告<br/>~/hermes-workspace/reports/"] D2 --> R D3 --> R R --> S["早上 09:00<br/>团队 IM 频道自动分发"] S --> H["策划:只审阅结果<br/>(分析已在熟睡时完成)"] style A fill:#fff3e0,stroke:#e65100 style B fill:#e3f2fd,stroke:#1565c0 style R fill:#e8f5e9,stroke:#2e7d32 style H fill:#fce4ec,stroke:#c2185b

岗位 1 —— 数据表夜间一致性。 把 2.4 里从头跟到尾的那个作业预约到每晚 23 点。无论昨夜谁动过哪张表,一到早上,外键断裂的地方就以表格呈现出来。这与 §23.1 的 /check Cascade(doc-audit → data-qa → integrity → link-check 四合一)表面相似,但有一个决定性差异。/check 要我醒着去调用才会跑,夜间智能体则不需要我在场也照跑。两者并不竞争——白天的 Cascade 是即时验证,夜晚的智能体是无人验证,角色由此分开。

岗位 2 —— 长期模拟。 把 §4.4 讲过的战斗模拟沿时间轴深度拉长。这是一项跑 100 小时量的虚拟游玩、测量 Boss 击杀平均时间、资源消耗曲线、连招分布的作业。它本质上不适合 Claude Code 的对话流——跑一次要好几个小时,而那段时间里我不可能一直守着对话窗。让智能体在后台跑,结束后只把曲线图和汇总数值拿回来。

岗位 3 —— 构建捕获自动分析。 QA 捕获的构建视频一落进文件夹,智能体就逐帧提取数据,做出规格数值与实测数值的 diff。策划无需把视频从头到尾看一遍,只看"规格是伤害 120,而构建实测为 108"这样的 diff 行。分析里枯燥的部分,整个都归智能体。

这三个岗位,看结果的人所花的时间都没有减少。减少的是投入到分析里的人力时间。判断,依然由人来做。


23.2.6 自主的代价 —— 五道安全装置

智能体的自主性,本身也就是风险。一件不经人每一步确认就读文件、跑命令的工具,一旦出岔子,人不在现场。§23.2.4 里明写"禁止臆测",并非偶然。五道安全装置不是可选项,而是引入第一天就要一起打开的一整套。

装置 作用(实际 Hermes 配置键) 缺了会发生什么
权限白名单 破坏性命令须经人工批准(approvals.mode: manual),只把允许的命令列入白名单(command_allowlist),密钥值在日志中打码(security.redact_secrets) 自作主张改掉原始数据表
检查点 文件操作前拍快照,便于回退(checkpoints.enabled,恢复用 /rollback) 错误的假设一路滚到底,整个结果被污染
日志自动记录 把网关·智能体·错误日志留在 %LOCALAPPDATA%\hermes\logs\ 出事后无法追溯"为什么会这样"
成本上限 单个作业的轮次上限(agent.max_turns)·终端超时(terminal.timeout)·死循环自动检测(tool_loop_guardrails)·上下文自动压缩(compression) 陷入死循环的作业把 API 账单越滚越大
可废弃 随时中止(/stop)·预约暂停/删除(cron pause)·子作业超时(delegation.child_timeout_seconds)·不用的技能自动归档(curator) 开始跑偏的夜间作业停不下来

这五道并非各自为政,而是作为一整套协同工作。只锁权限、不设成本上限,就会在权限范围内跑起死循环,把账单撑大。只开日志、没有废弃手段,就会眼看着出了事却停不下来。哪怕只缺一道,夜间无人运营的事故概率也会陡然上升。

实际把工具打开来看,这五个概念在若干处的实现,比书中所描绘的还要细密一层。权限一侧多出一层独立的策略引擎(security.tirith_enabled),用规则过滤命令。成本一侧的死循环检测并非单一上限,而是把"同一失败反复"·"毫无进展的反复"这类信号分别设为阈值。而夜间无人预约(cron)另有一个开关(approvals.cron_mode: deny),在无人时段一旦逮到破坏性命令,就不等批准直接拒绝——相当于把书里的"权限 + 检查点"合并进了一个配置。废弃一侧的 curator,正是 §21 的"不用的工具就废弃"落成实际功能的地方。五道套件的骨架照旧保持,只是工具做得更精细的地方,把那个键打开就好。

把这一整套写进 config.yaml,大致是下面这个样子。

# %LOCALAPPDATA%\hermes\config.yaml (节选)
approvals:
  mode: manual              # ① 权限 —— 破坏性命令须经人工批准
  command_allowlist:        #    只列出无需批准即可放行的命令
    - "python *"
    - "rg *"
  cron_mode: deny           #    夜间无人 cron 遇到破坏性命令则自动拒绝
security:
  redact_secrets: true      #    在日志中打码密钥值
  tirith_enabled: true      #    再加一层策略引擎(基于规则的命令过滤)
checkpoints:
  enabled: true             # ② 检查点 —— 文件操作前拍快照(/rollback 恢复)
  max_snapshots: 20
  retention: 7d
logs:
  path: "%LOCALAPPDATA%\\hermes\\logs"   # ③ 日志 —— gateway/agent/errors
agent:
  max_turns: 60             # ④ 成本 —— 单个作业轮次上限
terminal:
  timeout: 180              #    终端命令超时(秒)
tool_loop_guardrails:       #    死循环自动检测(同一失败·毫无进展)
  enabled: true
compression:
  enabled: true             #    上下文自动压缩(节省 token)
delegation:
  child_timeout_seconds: 600  # ⑤ 废弃 —— 子作业超时(与 /stop·cron pause 配合)
curator:
  enabled: true             #    不用的技能自动归档

委派也不是一次全交出去。起初只把最窄、最容易回退的作业(像一致性检查这种只读的活儿)交给它,盯着结果看上几天,再拓展到下一个岗位。§23.2.4 里第一个作业选夜间一致性检查,也是同样的道理——只读,最坏也不过是一份错误的报告,原始数据不会受损。


23.2.7 引入进展与渐进阶段(2026-06 时点)

在更新本章的这个时点,引入已进入稳定期。原生 Windows 构建(v0.16.0)已安装完毕,也已用 hermes setup 登记好模型 API 密钥。启用了第一个岗位,把 5 道安全装置逐一对照实际配置键做了检查,如今正跑着真实的自主作业,一点点上手。老实说,是先在个人 PC 而非公司 PC 上验证——公司引入,被我推迟到个人 PC 上的安全装置足够纯熟之后。这与其说是谨慎,不如说更接近 PC 分离原则。未经验证的自主工具,不会直接放到团队数据上。

期间 活动 关卡(gate)
1 个月 Hermes 安装(原生 Windows v0.16.0)+ hermes setup + 首个作业 5 道安全装置是否全部打开
2\~3 个月 扩展到 2\~3 个岗位(会议记录分类·构建捕获分析) 每个委派范围都查日志
3\~6 个月 公司评估 —— 以个人 PC 验证结果做决策 确认无人运营事故 0 起
6\~12 个月 团队层面引入 安全装置沉淀为团队规约

跳过阶段的诱惑最危险。若从 1 个月直接跳到 6 个月(团队引入),安全装置就还只是个人一人的习惯,尚未成为团队规约便被放开。在每个阶段的末尾停一次、检查这五道装置,才是正解。比起走得快,能保持随时回退地走,更重要。


23.2.8 五个常见误解

"智能体会取代人",是最常见的误解。§23.2.4 的实操记录展示了相反的一面——智能体在一个列映射处停了下来,而那个判断由人来补上。游戏策划的核心决策依然归人,智能体拿走的是反复与分析里枯燥的部分。

"装一次就全自动"的期待也危险。头一两个月反而更费手。列名映射、权限范围、成本上限都要按作业逐一调校,在这套调校纯熟之前,每一份输出都要人来审阅。

"Claude Code 如今过时了"这种断言是错的。两者所处的时段不同。白天的精细决策交给 Claude Code,夜晚的无人反复交给智能体。§23.1 的 /check Cascade 并没有消失,只是在它旁边又多出了一条夜间泳道。

"开源所以免费"的认知只对了一半。本体虽免费,模型 API 的调用费用照样要花。所以 config.yamlagent.max_turns·compression 这类成本上限,既是安全装置,也是账本。

最后,"连复杂又危险的作业也交给智能体"这种期待最危险。作业的风险越大,越要置于人的控制之下。交给智能体的,从简单且易于回退的作业开始。委派只随信任的累积而拓展。


23.2.9 通向下一章

如果说 §23.1 的 Wrapper·Cascade·Junction 是 Claude Code 运营的顶点,那么本章的 Hermes,就是在那套运营之上又铺了一条夜间泳道。白天的工具与夜晚的工具共用同一张桌子的图景——这既是 2026 年这一时点的当下,也是不远将来的骨架。

下一章是面向游戏策划的工具策展。12 槽里该放什么,用 skill_audit_score 剔掉什么——本章一笔带过的策展标准,将化为具体的工具推荐逐一展开。


本章要点

下一章预告


动手试试

setup 1. 下载并安装 Hermes 原生 Windows 安装器(若偏好 Linux,wsl --install 之后在其中安装的路子也一样存在)。 2. 在快速的本地磁盘上建一个工作文件夹,把要检查的数据表复制过去(禁止把网络驱动器·SVN 工作文件夹直接接为工作空间)。 3. hermes setup → 输入模型 API 密钥 → 确认工作空间路径·权限初始值。 4. 在 %LOCALAPPDATA%\hermes\config.yaml 中打开 5 道安全装置:权限批准(approvals.mode: manual·command_allowlist·cron_mode: deny)、成本上限(agent.max_turns·terminal.timeout·tool_loop_guardrails)、检查点(checkpoints.enabled)、日志路径(logs.path),并熟悉中止流程(/stop·/rollback)。

prompt - 把目标抛得抽象一层:不是"把这个做了",而是"把这个结果给我做出来"。 - 用编号写明对象·要做的事·保存位置,最后务必输入一行:"若卡住,或列/格式与预期不同,就停下并报告,禁止臆测。" - 第一个作业,请挑像只读的一致性检查那样容易回退的。

verify - 不要原样相信第 1 次输出,智能体停下的地方(列映射·格式不一致)要由人来确认。 - 若停得对,就写明映射再请求;若停错了,就重新输入约束。 - 把生成报告里的一两个失败项,在原始表中直接比对、验证智能体判断是否正确,之后才交给夜间预约(cron 23:00)。

单人精简版

若想先不装 Hermes、只抓一抓智能体的手感,可以在 Claude Code 内用后台执行跑一跑精简版。

Part 23 · 第3章. 工具策展 —— 用数据裁掉不用的工具

做季度复盘时,我打开了全局技能文件夹。一行一行数下来,wrapper 有 19 个。明明定好了只运营 12 个并这样跑了一年,不知不觉却又多出了 7 个。更离谱的是,其中一半光看名字根本想不起来是做什么的工具。migrate-legacy-enum。这是什么来着。上一次用它是什么时候来着。

想不起来。只要依赖记忆,这个问题就永远无法回答。于是我决定不看记忆,而是看日志。工具策展不应是凭喜好去裁剪的工作,而应是用"上个季度调用了这个工具几次"这样的数字来裁剪的工作。

本章记录的是:如何自动地把这个数字提取出来,如何用这个数字裁掉工具,以及如何从一开始就阻止工具暴增。


23.3.1 工具增多是一种自然现象

在谈策展之前,必须先承认一件事。工具只要不加阻拦就一定会增多。这不是因为意志力薄弱。而是因为每次任务中"就这一次,为了快点处理"而写一个小脚本,本身是合理的选择。而这种合理的选择累积几十次,就成了不合理的一堆废物。

项目A 中运营的结构,是全局的 12 个 wrapper 通过 junction 指向 workspace 中的 48 个本体。全局这一侧很轻,沉重的本体则放在用 SVN 管理的 workspace 里。这个结构本身已在 §23.1 中讲过。问题在于,这个 12 的数字并不会安分待着。

看看工具增多时还有什么一起增多,就能清楚为什么必须加以阻拦。

增加 1 个工具 → 随之增长的 4 项成本

新工具 +1

上下文 token 占用 ↑ 选择疲劳 ↑ 维护表面积 ↑ 功能重复风险 ↑

工具是 +1,成本却是 +4。这正是策展属于"做减法"的原因。

尤其是第一项,上下文 token 占用,是进入 AI 工具时代后变得更为尖锐的成本。全局 wrapper 一多,每个会话中 AI 读取"我可用的工具清单"所需的 token 就会增多。为了读 19 个工具的说明,真正能用在任务上的上下文反而减少了。因此项目A 的 sync_skills.py 带有 --cleanup 选项,会自动清理 junction 已断裂或本体已消失的 wrapper。这更接近一种为守住 token 预算而做的卫生工作。

--cleanup 能抓到的只有"断裂"的工具。那些好端端活着、却没人用的工具,它抓不到。要抓这类工具,就需要使用频率数据。


23.3.2 skill_audit_score —— 用 SVN 日志测量使用频率

核心思路很简单。workspace 中的技能与工具全都在 SVN 里。而且每次使用工具,该工具产出的产物(表格、文档、关系图 HTML 等)都会提交到 SVN。也就是说,只要看 SVN 日志,哪个工具实际干过活就会作为痕迹留下来。

于是我写了一个叫 skill_audit_score 的小测量脚本。正如其名,它为每个技能打一个"审计分数"。做这个工具时,我并不是一开始就把代码全写好,而是向 AI 说明测量逻辑、拿到初稿。这里把那份实操记录(worked transcript,完整保留的真实操作过程记录)原样留下。不做摘要,连走弯路的部分也一并写下来。

提示词(全文)

全局技能 wrapper 定为 12 个,却不知不觉膨胀到了 19 个。我想在季度复盘时用数据挑出"不用的工具"。请按以下前提写一个测量脚本。

Claude 的原始输出(原样)

好的。不过有一点要先说明。并不能保证提交信息里一定带有工具名称。如果人随意撰写提交信息,像"修改表格"这样的信息就无法判断是哪个工具参与的。因此我会并行采用两种匹配方式。

  1. 在提交信息中匹配工具名称·别名关键词(宽松)
  2. 匹配变更的文件路径是否属于对应工具的文件夹或该工具的产物模式(例如 *_relation.html)(严格)

第 2 种更可信,因此我会给它更高的权重。以下是初稿。

# skill_audit_score.py  (Claude 初稿 —— 验证前)
import subprocess, sys, datetime, xml.etree.ElementTree as ET
from collections import defaultdict

def svn_log_xml(repo_path, limit=2000):
    out = subprocess.run(
        ["svn", "log", "--xml", "-v", "-l", str(limit), repo_path],
        capture_output=True, text=True, check=True)
    return ET.fromstring(out.stdout)

def score_skills(repo_path, skills, window_days=90):
    root = svn_log_xml(repo_path)
    now = datetime.datetime.now(datetime.timezone.utc)
    hits = defaultdict(list)   # skill -> [days_ago, ...]
    for entry in root.findall("logentry"):
        date = datetime.datetime.fromisoformat(
            entry.find("date").text.replace("Z", "+00:00"))
        days_ago = (now - date).days
        if days_ago > window_days:
            continue
        msg = (entry.find("msg").text or "").lower()
        paths = [p.text for p in entry.findall("paths/path")]
        for sk in skills:
            name = sk["name"].lower()
            in_msg = name in msg or any(a in msg for a in sk.get("alias", []))
            in_path = any(name in (pp or "").lower() for pp in paths) \
                      or any(pat in (pp or "") for pp in paths
                             for pat in sk.get("artifact", []))
            if in_path:
                hits[sk["name"]].append((days_ago, 2.0))   # 严格匹配加权
            elif in_msg:
                hits[sk["name"]].append((days_ago, 1.0))   # 宽松匹配

    rows = []
    for sk in skills:
        h = hits[sk["name"]]
        if not h:
            rows.append((sk["name"], 0, None, len(h)))
            continue
        # 时近加权:越近越接近 1.0,90 天前则为 0.1
        recency = sum(w * (1 - 0.9 * (d / window_days)) for d, w in h)
        last_used = min(d for d, _ in h)
        # 最后使用日扣分
        stale_penalty = max(0, (last_used - 14)) * 0.5
        score = max(0, min(100, recency * 8 - stale_penalty))
        rows.append((sk["name"], round(score, 1), last_used, len(h)))

    rows.sort(key=lambda r: r[1])   # 分数升序 = 候选优先
    return rows

if __name__ == "__main__":
    REPO = sys.argv[1]
    SKILLS = [
        {"name": "relation-map-gen", "alias": ["relation", "관계도"],
         "artifact": ["_relation.html"]},
        {"name": "schema-doc", "alias": ["schema", "명세서"],
         "artifact": ["_schema.md"]},
        {"name": "gdd-gen", "alias": ["gdd"], "artifact": ["_gdd.md"]},
        {"name": "table-creator", "alias": ["table", "테이블"], "artifact": []},
        {"name": "excel-reader", "alias": ["xlsm", "vba"], "artifact": []},
        {"name": "migrate-legacy-enum", "alias": ["enum", "migrate"],
         "artifact": []},
    ]
    print(f"{'skill':22}{'score':>7}{'last_used(d)':>14}{'hits':>6}")
    for name, score, last, hits_n in score_skills(REPO, SKILLS):
        print(f"{name:22}{score:>7}{str(last):>14}{hits_n:>6}")

人工验证 —— 运行后被否决的部分

我把脚本在实际的 SVN 工作副本上跑了一遍。第一次输出就很奇怪。table-creatorexcel-reader 两者的 artifact 模式都是空的,因此只有在提交信息里出现 "table" / "xlsm" 这两个词时才会被抓到。可这两个工具的产物就是普通的 .xlsm 文件,无法用模式加以区分。结果两者的分数都异常地低 —— 尽管它们其实是每天都在用的工具。

这里我做了一个重要判断。分数低并不意味着就可以一概裁掉。分数低的原因,究竟是"真的没在用",还是"测量没能抓到这个工具",这要由人来区分。AI 给出的数字只能缩小候选范围,最终决定由人来做。

于是我又向 AI 提出了请求。

二次请求提示词

artifact 模式为空的工具,其分数不可信,所以请在输出里增加一个 confidence 列。从未有过 artifact 匹配的工具标记为 confidence=LOW,并从自动策展候选中排除。把 LOW 的工具单独归为"无法测量 —— 手动检查"一组。

经过这次二次请求,输出被分成了两组。一组是可以凭可信分数裁掉的工具,另一组是因测量偏弱而需要人亲自查看的工具。实际跑出来的结果大致是这样(分数为作者工作副本上的实测值,部分工具名已做匿名化)。

skill audit_score last_used(天前) confidence 判定
relation-map-gen 71.4 2 HIGH 保留
schema-doc 58.9 5 HIGH 保留
gdd-gen 22.1 31 HIGH 观察
migrate-legacy-enum 0.0 未测得 HIGH 策展候选
table-creator 4.2 1 LOW 手动检查 → 保留
excel-reader 6.0 1 LOW 手动检查 → 保留

migrate-legacy-enum 的分数为 0,confidence 为 HIGH。这意味着在 90 天里,这个工具的文件夹和产物一次都没有出现在提交中。回想起来,那是去年把遗留 enum 迁移了一次就结束的、本该是一次性的工作,却被我固化成了技能。这正是该裁掉的工具。反过来,table-creator·excel-reader 虽然分数低,但 confidence 是 LOW,而且最后使用日就在一天前。只是测量没能抓到,实际上每天都在用。这不能裁。

注意:上表的分数算式(时近加权 × 8、stale 扣分)是作者针对自己工作副本调校过的数值。SVN 提交习惯·产物模式不同,系数也会不同。比起绝对分数,"工具间的相对排名"与"confidence 区分"才是这个工具的本质。


23.3.3 策展周期 —— 从测量到废弃

skill_audit_score 只是一个测量工具。必须有一个把测量值嵌入季度复盘、转上一圈的周期,工具才会真正被整理。那个周期如下。

flowchart TD A[季度复盘开始] --> B[运行 skill_audit_score<br/>解析 90 天 SVN 日志] B --> C{confidence 判定} C -->|HIGH| D{audit_score 评估} C -->|LOW| E[移入手动检查队列<br/>直接确认最后使用日] D -->|分数高| F[保留] D -->|中等·下降趋势| G[观察 —— 下一季度重新测量] D -->|0 或触底| H[确定为策展候选] E --> F E --> H H --> I{可替代?} I -->|以 Wrapper 吸收| J[对现有工具做 MECE 增强<br/>§23.1 wrapper 政策] I -->|完全废弃| K[sync_skills.py --cleanup<br/>移除 junction + SVN 归档] J --> L[确认恢复到 12 槽位] K --> L L --> A classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class B,C,K code; class A,D,E,I human; class F,L pass;

区分这个周期的两个出口很重要。分数为 0 的工具并不是一律删除。如果那项工作本身已经消失,就送去完全废弃(--cleanup);如果那项工作仍然需要、只是没有频繁到值得单设一个工具,就把它吸收进现有工具。后者正是 §23.3.4 的 MECE 增强。

即便废弃,SVN 历史里也会留下代码。收回的只是 junction 和全局暴露,并不是把代码本身永远抹掉。半年后那项工作再次出现,从 SVN 恢复即可。正因为有这张"可以撤回"的安全网,人才敢果断地裁。


23.3.4 抑制 MECE 增殖 —— 在创建前先发问

比起测量后再裁,更好的是一开始就不去创建。如果说 skill_audit_score 是事后整理,那么 MECE wrapper 政策就是事前抑制。

MECE 即 Mutually Exclusive, Collectively Exhaustive —— 彼此不重叠、无遗漏。每当想创建新工具,就把这个词抛出来。新工具与现有工具是否重叠(违反 ME)?还是它真的填补了空白领域(贡献 CE)?项目A 的 wrapper 政策在这里分成两条路。

情形 政策 结果
新工作与现有工具的领域重叠 优先增强现有工具 在现有 wrapper 本体上添加功能,不占用新槽位
新工作明确属于不同领域 允许新增 wrapper 把 12 个槽位中的一个分配给新工具(同时附带一个应裁掉的候选)

关键在于"默认值是增强"。创建新工具是例外。要为这个例外正名,就得证明"现有任何工具都做不成这项工作"。正是这一个默认值,才是把膨胀到 19 个的工具重新拉回 12 个的真正原因。

这也与 §23.1 的 cascade 相连。像 check 这样的 cascade,是把原本 4 种的检查工具合并为一次调用的产物。它没有设置 4 个独立的 wrapper,而是从 MECE 的视角把"这些都属于检查这一个领域"看待、合并吸收为一个的例子。工具数量减少了,功能却原封不动。这就是增强的典范。

AI 助手在这里既是风险因素,也是解法。说它是风险,是因为只要对 AI 说"帮我写个处理这项工作的脚本",新工具就太容易冒出来。在点一下就生成一个工具的环境里,若没有 MECE 纪律,工具坟场转眼就会堆成。说它是解法,是因为只要先把政策交给 AI,AI 就会自行提议"这个不如作为选项加到现有的 relation-map-gen 上更好"。要把制造工具的 AI,连同策展纪律一起交到它手里。


23.3.5 不被分数欺骗的方法 —— 测量的局限

在运营本章这个工具的过程中,我学到最多的一点是:绝不能盲信测量值。skill_audit_score 只看 SVN 日志这一种信号。因此它在结构上有必然会漏掉的东西。

概括来说,这个工具不是"做决定的工具",而是"缩小候选的工具"。它让你一眼看清 19 个工具,1 秒内告诉你"该怀疑哪一个"。而验证那份怀疑、并动手裁掉,则留给人来完成。测量并不取代人,只是指出人该去看的地方。


动手试试 —— skill_audit_score 一个周期

亲自把工具策展周期转上一圈的步骤。

setup 1. 确认工作区的技能与工具都在版本管理(SVN/Git)之内。产物也必须提交到同一个仓库。 2. 制作待测量的工具清单。为每个工具写上 namealias(会出现在提交信息里的别名)、artifact(产物文件模式,若有)。没有 artifact 的只读工具留空。

prompt(给 AI)

请按以下前提写一个工具使用频率测量脚本。(1) 每个工具都以产物提交的形式在 [版本管理系统] 日志里留下痕迹。(2) 解析最近 90 天的日志,统计各工具参与的提交数。(3) 用时近加权 + 最后使用日扣分,给出 0\~100 的分数。(4) 从未有过产物模式(artifact)匹配的工具标记为 confidence=LOW,从自动候选中排除,分入手动检查。(5) 输出为按分数升序的表格 —— 低分即策展候选。只用标准库,仓库路径通过参数传入。

verify 1. 如果每天都用的工具排到了表格顶部(低分),那就是测量错了。请确认该工具的 confidence —— 若为 LOW 属正常(无法测量),若 HIGH 却分数低,就检查 alias·artifact 的设置。 2. 只把分数 0 + confidence HIGH 的工具确定为策展候选。把最后使用日与记忆对照,由人判断它是否真的已经死掉。 3. 把候选送往"完全废弃"与"吸收进现有工具"之一。废弃只收回 junction,代码留在仓库里。 4. 最后数一数 12 个槽位(或你自己设定的上限)是否已恢复。

单人精简版

如果你是工具只有 6\~8 个、也没有 SVN 的单人开发,就这样精简。版本管理用 Git 就够了。用 git log --since="90 days ago" --name-only 拉出变更的文件路径,再按工具文件夹名 grep 一次,"哪个工具最近干过活"就出来了。连打分脚本都不必写。关键不在于数字的精确,而在于用看日志代替凭记忆这一个习惯。每季度一次,用 git 日志拉出"过去 90 天一次都没碰过的工具",盯住那个工具。那 5 分钟就能挡住工具坟场。


本章要点

下一章预告

Part 23 · 第4章. 独自开发的益智游戏 —— Critter Sort 实战记

周六下午,妻子正用手机玩一款颜色分类的益智游戏。那是一款名为 Yarn Fever 的游戏,把缠结的毛线团按颜色分类到相应的篮子里。一局结束,她就说"又是一样的",然后关掉。因为没有更新,很快就腻了。

那一刻的想法很简单。那个循环有着经过验证的上瘾性,而机制本身不受著作权保护。换成动物主题,再把关卡程序化地无限生成,"又是一样的"这个问题就消失了。一个人用直接在浏览器里运行的 HTML 3D 来做,连装到妻子手机上都不需要。

问题在于,我不是图形工程师。虽然是 24年资历的策划,却从没用 Three.js 写过着色器。所以这一章,是"和 AI 一起、独自在几天内让一款游戏跑起来"的真实记录。它同时也是一份分离的记录——使用与公司 MMORPG(以下称项目A)工作相同的工具,却一行领域内容都没有掺入。

实际的游戏在 critter-sort/ 仓库里,git 标签 v0.1\~v0.3 记录着三天间的决策。这不是加工过的案例,而是直接引用那个仓库。


23.4.1 用提示词做逆向工程 —— 然后丢掉了标志性手感

最先做的事,是把原作用语言拆解开、抛给 AI。第一个提示词是这样的。

提示词(v0.1 启动): "我想把 Yarn Fever 这款休闲益智游戏的核心循环改编成动物主题,用 Three.js + Vite 来做。循环是这样的:把缠结的颜色团块按颜色分类到同色的桶里,临时槽位溢出就游戏结束。以动物作为分类对象,点击缠结的动物堆,就把它们送进同色的巢(nest)。逻辑要用与 Three.js 无关的纯 JS 状态机来写,以便进行 headless 测试。再加入程序化的无限关卡(基于种子)。"

AI 忠实地照做了。它把文件夹结构分成 game/(纯逻辑)和 render/(Three.js),先写好 state.js·rules.js·generator.js,再用上色的方块 placeholder 把游戏板显示出来。不是几天,而是一个会话就让 v0.1 跑了起来。

可是,为了给妻子看而亲自试玩的那一刻,违和感来了。它变成了一款常见的消除拼图——动物在画面上蹦跳着跳进篮子。原作的手感不见了。Yarn Fever 的核心特质不是"分类",而是用手解开缠结毛线团的触感,以及旋转画面、确认被遮住的颜色的行为。我在提示词里把它压平成了"点击动物就送进巢"这种普通的 sort,而 AI 只是忠实于我给出的定义罢了。

这就是逆向工程的第一个陷阱。一旦概括原作,标志性手感就会蒸发。因为概括留下的是表面,而非本质。

这里要说清楚一点。AI 并没有给出错误的答案。是我的提示词错了。所以下一个循环不是修改代码,而是修正定义


23.4.2 脊梁 —— 让旋转与解开毛线复活的修正循环

看过 v0.1 之后,我分两次重新明确了核心。第一次修正针对行为,第二次修正针对触感。

提示词(第 1 次修正 —— 行为): "这个变成普通的消除拼图了。Yarn Fever 的核心特质是'旋转 3D 画面、确认被遮住的颜色,并决定解开的顺序'。不要把游戏板平铺开,而要在中间堆成高高的土丘,必须旋转才能看到背面被遮住的颜色。用 OrbitControls 加入旋转。可是旋转和点击会冲突啊——你打算怎么区分它们?"

Claude 的输出(原始,未经概括): "没错。旋转手势和点击是同一个指针事件,所以会冲突。我建议用距离·时间阈值来区分。在 pointerdown 时记录坐标·时刻,在 pointerup 时,如果移动距离超过 9px 或按住时间超过 400ms,就判定为旋转手势并忽略点击。只有在这以下,才用 raycaster 拾取动物并调用 state.tap()。OrbitControls 的 azimuth 我会限制在 ±54° 左右,避免堆块跑到画面之外。不过在移动端 9px 可能太敏感,需要在真机上确认。"

那个 9px/400ms 的阈值至今仍原样留在 src/main.js 里。没有多余部分、可以直接验证的代码。

// src/main.js — 拖拽=旋转(OrbitControls) / 短按=解开毛线
canvas.addEventListener('pointerup', (ev) => {
  if (!down || state.status !== 'playing') { down = null; return }
  const dist = Math.hypot(ev.clientX - down.x, ev.clientY - down.y)
  const dt = performance.now() - down.t
  down = null
  if (dist > 9 || dt > 400) return // 判定为旋转手势
  const rect = canvas.getBoundingClientRect()
  const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1
  const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1
  const id = view.pick(x, y, camera)
  if (id !== null) state.tap(id)
})

行为回来了。可是第二次试玩后,依然没有"解开毛线团"的感觉。动物只是移动到巢里,没有解开的手感。第二次修正。

提示词(第 2 次修正 —— 触感): "旋转变好了。可是还没有'解开毛线'的感觉。加入这样的表现:点击时,缠在动物身上的乱线股解开、消失,那些线再缠绕着进入篮子。动物的外观也别用光滑的 GLB 鸟,而要用圆滚滚的针织毛绒玩偶(amigurumi)。加上惺忪的睡眼、腮红这类温馨的细节。外部素材不合适的话,去掉、改用程序化生成也可以。"

Claude 的输出(原始): "我会拆成两部分来实现。(1) 缠结的线:在每只动物的身体上缠两股略暗的同色 TorusGeometry,点击时把 scale 缩到 0.01,让它解开、消失。(2) 线的缠绕:用 TubeGeometry 画出动物→篮子路径上的线,做一个朝篮子方向缠绕进去的补间动画。毛绒玩偶外观走程序化路线——给 MeshStandardMaterial 程序化生成 knit bump 贴图,营造 niit 质感;惺忪的睡眼用扁平的 box,腮红用半透明的粉色 sphere。现有的 GLB 起始素材(3种鸟)和针织色调不搭,所以移除,但会把动物包的加载管线作为后备保留下来。有一点要说明:程序化生成的动物在物种多样性上不如 GLB——先用一个物种验证、再增加物种会比较稳妥。"

最后那句警告('物种多样性不如 GLB'——GLB 是 glTF Binary,一种从外部获取即用的现成 3D 模型文件格式)正是通向 v0.3 的种子。AI 先说出了下一个局限,而我把它接过来作为下一个里程碑。

验证每次都是两步。先用 headless 确认逻辑没有损坏(错误 0),然后在浏览器里亲自旋转·点击,体会手感。v0.2 的提交信息把这次验证固化了下来:"headless 验证:旋转·解开毛线·自动通关正常,错误 0。"

缠结的两股线如今这样留在 src/render/pieces.js 里。

// src/render/pieces.js — 缠在身体上的两股松散的线(略暗的同色)
const strandMat = new THREE.MeshStandardMaterial({ color: darken(hex, 0.7), roughness: 1 })
const strands = []
const orient = [[0.5, 0.2, 0.0], [1.25, 0.0, 0.6]]
for (let i = 0; i < 2; i++) {
  const s = addMesh(g, G.torus, strandMat, [0, byo + 0.02, 0], Math.max(bx, bz) + 0.02, orient[i])
  strands.push(s)
}
g.userData.strands = strands  // 点击时 view.js 会解开这些线股使其消失

把这里得到的教训用一句话记下来。


23.4.3 三天的决策历史 —— 用 git 标签读懂修正

光靠语言描述只是"改了两次",但 git 历史连同精确的时间,把那些修正是何时、以何种形态进入的都留了下来。这在单人开发中替代了复盘。即使没有同事,提交也能为"为什么会变成这样"作证。

提交 时间 (2026-05-30) 改动内容 标志性手感状态
2b2e3bc v0.1 14:43 Yarn Fever 逆向工程,纯逻辑 + placeholder,60/60 求解器通过 缺失(被压平为普通 sort)
70a0117 v0.2 15:11 旋转(OrbitControls ±54°)+ 点击/拖拽分离 + 解开毛线 + amigurumi 复原(重新定义核心)
160663c 快照 15:31 v0.2 画廊快照 5张 + README 画廊
59b0baf v0.3 15:55 程序化 amigurumi 8种 + 鲜艳糖果色调色板 强化(确保物种多样性)
c5b9a1b 交接 16:20 NEXT_SESSION 会话交接指针

v0.2 的提交信息正文把决策本身固化了下来。"将游戏的核心特质从'动物蹦跳'纠正为'旋转画面、解开可爱的毛线团(针织毛绒玩偶)送入同色篮子'。"这是一个半小时里,游戏的核心特质死了一次又被救活的记录。

值得注意的一个细节。查看 v0.2 的 git show --stat 会发现,起始的 3种 GLB 鸟(Flamingo·Parrot·Stork)被整个删除了。理由是"美术和针织色调不搭"。外部免费素材不是免费就全都用,而是色调不搭就删掉的决定。这是由人而非 AI 把守的审美关卡。

public/assets/animals/pack_starter/Flamingo.glb  | Bin 77428 -> 0 bytes
public/assets/animals/pack_starter/Parrot.glb    | Bin 97024 -> 0 bytes
public/assets/animals/pack_starter/Stork.glb     | Bin 76852 -> 0 bytes

23.4.4 程序化生成实证 —— 8种 amigurumi 与无限关卡

v0.2 留下的作业是"程序化动物的物种多样性不如 GLB"。v0.3 解决了这个问题。没有新增任何一个外部素材,用代码生成了 8种动物。

核心是 src/render/pieces.js 中的 SPECIES 表。为每个物种以参数定义身体比例·头部·耳朵类型·口鼻·眼睛形状,由一个函数读取这些参数来组装网格。

// src/render/pieces.js — 各物种的轮廓参数
const SPECIES = {
  cat:      { body: [0.5,0.46,0.48,0.04], ears: 'cat',   snout: 0.13, tail: 'cat',  eyes: 'sleepy' },
  bear:     { body: [0.52,0.5,0.5,0.03],  ears: 'bear',  snout: 0.16, tail: 'none', eyes: 'round' },
  bunny:    { body: [0.46,0.5,0.46,0.02], ears: 'bunny', snout: 0.12, tail: 'puff', eyes: 'round' },
  fox:      { body: [0.5,0.44,0.48,0.04], ears: 'fox',   snout: 0.2,  tail: 'fox',  eyes: 'sleepy' },
  capybara: { body: [0.58,0.5,0.56,0.02], ears: 'tiny',  snout: 0.22, tail: 'none', eyes: 'sleepy' },
  pig:      { body: [0.54,0.5,0.52,0.03], ears: 'pig',   snout: 0.1,  nose: true,   eyes: 'round' },
  frog:     { body: [0.56,0.4,0.54,0.05], ears: 'none',  snout: 0.1,  topEyes: true, eyes: 'none' },
  chick:    { body: [0.42,0.44,0.42,0.05], ears: 'none', beak: true,  tail: 'none', eyes: 'round' },
}
export const SPECIES_IDS = Object.keys(SPECIES)  // 8种

仅凭耳朵形状,轮廓就分了开来。猫·狐狸是尖尖的 cone,熊是圆圆的 sphere,兔子是细长的 sphere,猪是向前弯折的 cone。青蛙是头顶凸出的眼睛(topEyes),小鸡是喙(beak)。这些细小的分支造就了 8种的辨识度。外部素材 0,代码一个文件。

不过程序化生成有个陷阱。"看起来像模像样"的代码是否真的能生成可辨识的 8种,光看代码是不知道的。所以验证再次分为两步。先用 headless 确认 8种是否无错误地生成,然后用 web-screenshot 技能(headless Chrome)捕获实际渲染,用眼睛确认 8种是否可区分。DEVLOG v0.3 里有那个结果:"以耳朵/口鼻/鼻子/喙/尾巴/眼睛区分轮廓。外部素材 0,针织色调完全统一。"

一个种子决定整个游戏板

关卡的无限性由种子 RNG 负责。generator.js 用 Knuth 乘法哈希对关卡编号进行种子化,再用 mulberry32 抽取确定性随机数。相同的关卡编号总是对应相同的游戏板。

// src/game/generator.js
export function generateLevel(level, animalPool = null) {
  const seed = (level * 2654435761) >>> 0  // Knuth multiplicative hash
  const rng = makeRng(seed)
  const { C, K, groupsPerColor, M, T } = levelParams(level)
  const colors = rng.shuffle(COLORS).slice(0, C)
  // ...
  for (const color of colors) {
    const count = K * groupsPerColor  // 始终是 K 的倍数 → 精确分解到巢(保证可解)
    // ...
  }
}

这里的一行保证了游戏的公平性。因为把每种颜色的动物数量强制设为始终是 K(完成一个巢所需的数量,3)的倍数,所以无论哪个游戏板都能被巢精确整除。无法解开的关卡从根本上不会出现。

60 个关卡是否全部可解 —— greedySolve

设计上可解并不等于证明。在 rules.js 里放入用于验证的贪心求解器,用 test-logic.mjs 自动游玩 60 个关卡,每次都确认是否真的全部通关。这是刚才写这一章时重新运行的实测输出。

$ node scripts/test-logic.mjs
[求解器] 60/60 关卡通关

[难度曲线] (C=颜色, K=完成, groups, M=巢, T=托盘, 总数量)
  Lv 1: C=3 K=3 grp=2 M=3 T=7 总=18
  Lv 8: C=4 K=3 grp=3 M=4 T=6 总=36
  Lv12: C=5 K=3 grp=3 M=4 T=5 总=45
  Lv20: C=5 K=3 grp=3 M=4 T=4 总=45

[乱点游玩] 随机点击时的失败率 (确认难度存在)
  Lv 1: 随机失败率 0%
  Lv12: 随机失败率 1%
  Lv20: 随机失败率 3%

这个测试同时证明了两件事。贪心求解器能通关 60/60,意味着所有关卡都可解(难度并非不可能);而随机点击的失败率随关卡上升从 0% 升到 3%,意味着难度真实存在(随便乱按也全都能通关的话就不是游戏了)。托盘从 7 格收窄到 4 格的难度曲线,以失败率的形式被测量出来。

这里要诚实地指出。随机失败率 3% 是"胡乱按键的机器人"的失败率,而不是人对难度的体感。人会通过旋转事先确认颜色,所以失败率更低。这个数字是"难度不为 0"的方向性证明,并不意味着妻子有 3% 的概率会输。人的体感难度在 v0.3 这个时间点还未测量,已在 NEXT_SESSION 里记为"收集妻子的试玩反馈(最优先)"。

程序化生成管线

flowchart TD L["关卡编号 N"] --> H["Knuth 哈希<br/>N × 2654435761"] H --> S["mulberry32(seed)<br/>确定性 RNG"] S --> P["levelParams(N)<br/>计算 C·K·M·T"] P --> G["generateLevel<br/>每色 = K 的倍数"] G --> B["游戏板(缠结堆)"] B --> R["createCritter<br/>amigurumi 8种 + knit 着色器"] G --> V["greedySolve<br/>60/60 验证"] V -->|"错误 0"| OK["保证可通关"] R --> SC["web-screenshot<br/>8种识别·视觉验证"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class H,S,P,G,R,V,SC code; class L,B data; class OK pass;

从种子出发,分岔为参数·游戏板·网格·验证的这条流程,正是从结构上解决"因没有更新而厌倦"这一最初问题的答案。


23.4.5 GLB 进来了怎么办? —— 自动缩放与后备

程序化的 8种动物是没有 GLB 时的后备。为了以后弄到真正的 amigurumi GLB 时能优先使用,保留了动物包管线。把 GLB 拖进文件夹、只跑一下 npm run scan 就完成了。

问题在于每个 GLB 的尺寸都各不相同。有的模型是 0.5单位,有的是 200单位。要靠手动调 scale,添加动物包就成了体力活。于是 scan-packs.mjs 读取 GLB 的包围盒,自动计算出符合目标高度(0.95单位)的 scale。

// scripts/scan-packs.mjs — 从 GLB 包围盒自动计算 scale/yOffset
const maxDim = Math.max(max[0]-min[0], max[1]-min[1], max[2]-min[2])
const scale = +(TARGET_H / maxDim).toPrecision(3)        // TARGET_H = 0.95
const yOffset = +(-((min[1] + max[1]) / 2) * scale).toPrecision(3)

assets.js 在 packs.json 缺失或加载失败时,会悄悄回退到程序化动物。

// src/render/assets.js
createAnimal(species, hex) {
  const entry = this.models.get(species)
  if (!entry) return createCritter(hex, species)  // 程序化 amigurumi 后备
  // ... GLB 克隆 + 颜色着色
}

这两行以不中断的方式保证了"有 GLB 就用 GLB,没有就用代码动物"。在妻子游玩期间,即使我丢进新的 GLB 包,游戏也不会停下。


23.4.6 一个人却像一支团队 —— 我是如何使用 AI 的

在这个项目里,我只是一名策划,但工作是靠多种角色运转起来的。AI 填补了那些角色。关键不在于"替我写代码",而在于填补我薄弱的位置

我薄弱的位置 AI 做的事 人(我)把守的关卡
Three.js 着色器 knit bump 程序化贴图、TubeGeometry 毛线表现 色调是否相配(删除 3种 GLB 鸟的决定)
输入冲突的解决 提出 9px/400ms 阈值 移动端真机体感确认
回归安全性 用 greedySolve 做 60/60 自动验证 "难度真实存在"由人来定义
预测下一个局限 警告"程序化动物的物种多样性偏弱" 将其采纳为 v0.3 里程碑

尤其视觉验证是单人开发中薄弱的一环。代码能跑,和"8种在肉眼中可区分"是两回事。所以我把 web-screenshot 技能(用 headless Chrome 启动 dev 服务器、截图 + 报告控制台错误)原样从公司工作中借用了过来。即使没有 claude-in-chrome 扩展,也能用眼睛确认移动端视口(iPhone 15 Pro 竖屏,393×852)的渲染。

这里,一个最重要的原则在发挥作用。工具从公司借用,但领域内容借用 0 件。

这种分离经由 grep 得到验证。记忆记录里留有"公司项目领域内容借用 0 件(验证 grep PASS)"。Critter Sort 的颜色是粉红·薄荷绿·黄色,动物是猫·熊·兔子。项目A(公司 MMORPG)的领域词汇在这个仓库里的任何地方都找不到。

为什么要分离到这种程度。是为了同时防止两种事故。公司 IP 泄漏到个人爱好中的法律事故,以及 MMORPG 领域 atom 被错误注入到益智游戏开发中、成为噪声的上下文污染。只让工具流动、把内容拦住——两者之间就是健康的分离。


23.4.7 小结 —— 一个人系统也照样运转

Critter Sort 是个小游戏。三天、5 个提交、8种动物、60 个关卡。然而在公司里用的那套方法,在 1/1000 的规模下也照样奏效。

最大的收获是第一节的失败。在 v0.1 里把游戏的核心特质杀死了一次,又靠两次修正把它救活。在没有同事的单人开发中,为这场死亡与复活作证的,是 git 提交。如果没有复盘,一个月后就会忘掉"为什么在 v0.2 里全部推翻重来了?"。

接下来的 Part 24 将讨论如何把这样的决策历史,在大团队·长期运营中固化为治理。

这一章从妻子玩腻后关掉的益智游戏出发,记录了独自开发的游戏重新回到她手中的过程。它确认了:系统不是规模的问题,而是纪律的问题。


动手试试 —— 今天就能做的一步

这一步是和 AI 一起把你喜欢的一款休闲游戏的核心循环跑起来。但要注意,别丢掉标志性手感。

setup —— 在装好 Node 的环境里新建一个空文件夹。mkdir my-puzzle && cd my-puzzle

prompt —— 像这样抛给 AI。关键是"不要概括,而要明确写出标志性手感"。

"我想把 [游戏名] 的核心循环改编成 [主题]。这款游戏的标志性手感是 [用一句话写下手感——例如:'旋转画面、确认被遮住之物并解开的触感']。绝对不要把它压平成普通的消除拼图。逻辑要与渲染分离,写成能用 headless 测试的形式。"

verify —— 亲自试玩第一版结果。问一句"我写下的标志性手感还在吗?"。如果不在,就不要改代码,而要重新写定义再提出请求。那正是我在 v0.1→v0.2 里做的事。

面向单人·业余爱好者读者的精简版

既不需要引擎,也不需要程序化生成。在一张纸上写下"这款游戏的标志性手感一句话",让 AI 做出原型,然后亲自试玩,只看那一句话是否还活着。如果死了,就把那一句话写得更具体些再来一次。守住标志性手感这一句话的习惯,仅凭这一点,就能避开逆向工程的第一个陷阱。

24.1 验证系统 —— 用代码揪出一致性、链接与 stale 问题

周一早上站会刚结束,数据团队的成员 A 用即时通讯工具发来一张截图。那是一份 QA 报告,说游戏内商店里某个材料道具的说明是空白的。追查原因 30 分钟后,真相浮出水面:两周前有人在策划文档里把那个道具改名为 재료_목재_상,而数据表中的引用仍然指向旧名 재료_목재_A。文档更新了,数据表没更新,连接二者的链接悄然断开。没有任何人说谎,游戏却在输出谎言。

这类事故会随着文档增多而以几何级数变得越来越频繁。人眼无法同时看清 50 份文档之间的相互引用。于是我们把验证委托给代码。本章讨论的系统,让文档、数据、链接的一致性由脚本而非人来检查。核心有三点 —— 来源一致性(_source_map.tsv audit)、链接完整性(wikilink),以及 stale 检测(揪出陈旧腐坏的引用)。


24.1.1 断链为什么是无声的

文档与数据相互指向、彼此依存地存活。策划案引用 enum,enum 引用数据表,数据表又引用另一份策划案里的决策。若由人手工管理这张网,一旦某个节点发生变化,就得靠人记住并逐一追踪所有指向该节点的引用。而记忆会失效。

断链之所以危险,是因为它不会抛出错误。若是代码,引用不存在的变量时编译器会拦住你。但在文档里写下的 [[재료_목재_A]] 这类 wikilink,即便目标消失,也只是留作一段普通文本。它不会变红。游戏照常构建、上线,直到玩家看到空白说明,才有人察觉。

因此,验证系统的第一项工作,是让人眼看不见的东西显现出来。把一致性违规拉成文本输出,再把这份输出绑定到构建关卡上,那么即便人忘了,脚本也不会忘。


24.1.2 三路验证的 cascade

验证不是一整块,而是分阶段的。先跑最廉价的检查,滤掉明显的违规,只让通过的进入下一阶段。因为如果对所有输入都跑昂贵的检查,会慢到没人愿意跑。下面是笔者实际运行的验证流程。

flowchart TD A[文档·数据表 保存] --> B{source_map audit} B -- 来源映射缺失 --> B1[FAIL: 手动编辑痕迹<br/>要求更新 _source_map.tsv] B -- 通过 --> C{wikilink 完整性} C -- 发现断链 --> C1[wikilink_apply.py<br/>尝试修复] C1 -- 可自动修复 --> C C1 -- 无法修复 --> C2[FAIL: 断链引用报告] C -- 通过 --> D{stale 检测} D -- 比引用目标更旧 --> D1[WARN: 登记待复审队列] D -- 通过 --> E[integrity_check 最终] E -- P0 违规 --> E1[BLOCK: 阻断构建关卡] E -- 通过 --> F[GREEN: 允许提交] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class B,C,C1,D,E code; class A data; class F pass; class B1,C2,D1,E1 fail;

这个 cascade 的核心是失败越早越便宜source_map audit 只是 TSV 单行比对,毫秒级就结束。相反,最后的 integrity_check 要加载整张数据表来检查 FK 关系,需要数秒。把廉价的检查放在前面,明显的错误就在那里被截断,昂贵的检查只对通过它的少数输入运行。

各阶段的输出不同,这一点也很重要。audit 给出 FAIL(编辑者手动改动过某处的证据),wikilink 在自动修复后给 FAIL,stale 给 WARN(不阻断,但需复审),integrity_check 给 BLOCK(直接拦住构建)。同样是「问题」,也要根据严重程度作出不同反应,人才能区分信号与噪声。


24.1.3 第一阶段 —— _source_map.tsv audit

最先运行的检查是来源一致性。笔者的文档生成管线,会把某份合成文档(例如 GDD,即游戏设计文档的正文)是从哪些源文件生成的,记录到 _source_map.tsv 里。每一行都钉死了「这一产出物的章节 = 这些源文件的合成」这样一条谱系(lineage)。

它之所以能成为验证工具,是因为只要有人手工编辑产出物,映射就会破裂。若有人直接改动自动生成的 GDD 章节,那一节就不再是源文件的忠实合成了。audit 脚本会把产出物各章节的哈希,与从源文件重新合成得到的哈希比对,不一致就给出 FAIL。「手动编辑即 audit FAIL」这条规则,就来自这里。

这并非要阻止人去编辑,而是要让编辑显式化。如果必须修改产出物,就该二选一:要么改源文件再重新生成,要么把那一节正式从映射中剥离出来(分离声明)。把无声的编辑变得吵闹,这就是 audit 的工作。


24.1.4 第二阶段 —— wikilink 完整性与自我修复

通过 audit 后,就进入链接检查。笔者的文档用 Obsidian 式的 wikilink [[目标]] 来连接节点。wikilink_apply.py 做两件事 —— 把 wikilink 解析为实际路径并应用,以及在可能的范围内修复断链。

能够修复的情形很明确:目标节点只是改了名字、仍在原位存在时。像前面 재료_목재_A재료_목재_상 这样的重命名,只要别名映射(alias map)已更新,脚本就会把旧名自动纠正为新名。反之,若目标被整个删除、或无法追踪去向,就放弃修复,并报告这条断链引用。

这里有一个设计判断:自动修复若过于激进则很危险。如果去找「名字相似的」就擅自接上,链接会错接到语义不同的节点上,酿成更糟的事故。因此 wikilink_apply.py 的修复是保守的 —— 只对有显式别名映射的重命名做自动纠正,需要猜测的情形则交给人。自动化的美德在于克制:只把确定的事情自动做掉,而把含糊的事情诚实地交还给人。


24.1.5 第三阶段 —— stale 检测

即便链接活着,引用也可能已经陈旧。文档 A 引用数据表 B,若 B 比 A 更晚更新,那么 A 的说明就有可能与当前的 B 相抵触。链接本身完好无损 —— 因为指向的目标还在。可内容已经腐坏了。

stale 检测会比较引用两端的修改时间(或内容哈希版本)。若引用方比被引用目标更旧,就抛出 WARN,并把该节点登记到复审队列。之所以是 WARN 而非 BLOCK,是因为更新并不总意味着内容冲突。若只是改了一个错字的更新,引用依然完好。所以 stale 不是「拦截」,而是「标记出来让你去查看」。

来看这一阶段如何抓住前面那起断链事故。如果 재료_목재 数据表比文档更晚更新,那么在自动修复之前,stale WARN 就已经先亮起了。也就是说,三道检查是相互重叠的安全网。一张网漏掉的,由下一张网接住。cascade 之所以能抓住单一检查抓不到的事故,原因正在于此。


24.1.6 实操记录(worked transcript) —— 让 Claude 编写检查脚本

若让人从零把验证逻辑全部写出来,会很累。笔者用自然语言描述检查规则,执行脚本则先向 AI 要初稿。下面是制作 stale 检测脚本的真实会话 —— 这是一段实操记录(worked transcript,即完整保留的真实操作过程记录),输出不加修饰,失败与拒绝的过程都原样保留。

[提示词全文]

文档文件夹里的这些 Markdown 之间用 wikilink [[目标]] 相互连接。请写一个 Python 脚本,检查每份文档自身是否比它所引用的目标文件更旧(=stale)。条件: - 文件的修改时间以 git 最后一次提交时间为准(文件系统 mtime 会在 checkout 时被更新,不可信) - 断链(目标不存在)从 stale 检查中排除,单独报告 - 输出为 TSV:source<TAB>target<TAB>status(status = OK / STALE / BROKEN) - WARN 不阻断,退出码保持为 0(不能挡住构建)

[Claude 原始输出 —— 第 1 次]

下面的脚本用 git log -1 --format=%ct <file> 取得每个文件的最后提交时间,并用正则 \[\[([^\]]+)\]\] 提取 wikilink 后进行比较。

import subprocess, re, sys, pathlib

def git_mtime(path):
    out = subprocess.run(
        ["git", "log", "-1", "--format=%ct", str(path)],
        capture_output=True, text=True)
    return int(out.stdout.strip())

def resolve(link, root):
    # [[目标]] -> root/目标.md
    return root / f"{link}.md"

root = pathlib.Path(sys.argv[1])
for md in root.glob("*.md"):
    src_t = git_mtime(md)
    for link in re.findall(r"\[\[([^\]]+)\]\]", md.read_text(encoding="utf-8")):
        tgt = resolve(link, root)
        if not tgt.exists():
            print(f"{md.name}\t{link}\tBROKEN")
            continue
        tgt_t = git_mtime(tgt)
        status = "STALE" if src_t < tgt_t else "OK"
        print(f"{md.name}\t{link}\t{status}")

这样就能按你要求的 TSV 格式输出。

[人工验证 / 拒绝]

在这里停住了。我看到三处缺陷。

第一,git_mtime 若返回空字符串,就会在 int("") 处崩溃 —— 从未被提交过的新文件正是如此。第二,glob("*.md") 看不到子文件夹,而笔者的文档散落在 part*/ 之下。第三,也是最重要的 —— wikilink 有时会带管道别名,例如 [[目标|显示名]],而第 1 版代码会把 目标|显示名 整个误当成文件名。这样一来,完好的链接会全部被判为 BROKEN。照原样用,就是一颗误报炸弹。

我指出这三点,并重新提出了请求。

[再次请求]

请改三处:(1) 未提交的文件让 git_mtime 返回 None,这种情况暂缓 STALE 判定,status=UNTRACKED。(2) 不用 glob,改用 rglob 以包含子文件夹。(3) 处理 [[目标|别名]] 管道 —— 只取 | 前面的部分作为目标。另外,即使存在 BROKEN,退出码也保持为 0(WARN 不阻断原则)。

[Claude 原始输出 —— 第 2 次]

import subprocess, re, sys, pathlib

def git_mtime(path):
    out = subprocess.run(
        ["git", "log", "-1", "--format=%ct", str(path)],
        capture_output=True, text=True)
    s = out.stdout.strip()
    return int(s) if s else None   # 无提交历史则为 None

def target_name(link):
    return link.split("|", 1)[0].strip()   # [[目标|别名]] -> 目标

root = pathlib.Path(sys.argv[1])
for md in root.rglob("*.md"):
    src_t = git_mtime(md)
    text = md.read_text(encoding="utf-8")
    for raw in re.findall(r"\[\[([^\]]+)\]\]", text):
        name = target_name(raw)
        hits = list(root.rglob(f"{name}.md"))
        if not hits:
            print(f"{md.name}\t{name}\tBROKEN")
            continue
        tgt_t = git_mtime(hits[0])
        if src_t is None or tgt_t is None:
            status = "UNTRACKED"
        elif src_t < tgt_t:
            status = "STALE"
        else:
            status = "OK"
        print(f"{md.name}\t{name}\t{status}")

sys.exit(0)   # WARN 不阻断:一律为 0

这第 2 版输出被采纳了。不过有一处是人有意留下的决定 —— rglob(f"{name}.md") 若在多个文件夹里找到同名文件,只会用 hits[0]。这是一处潜在的歧义,但按笔者的文档命名规则,文件名是全局唯一的,所以实务中不会冲突。这个 AI 没有点出的假设,由人有意识地接受下来,并写进了注释。即便是自动化写出的代码,代码所依赖的假设也由人来负责。


24.1.7 把检查结果绑定到构建关卡

就算有脚本,没人跑也是白搭。验证的最后一环设计,是让它无法不被运行。笔者把这三个阶段绑定到提交前钩子(pre-commit)或构建管线上。audit FAIL 与 integrity_check P0 违规属于 BLOCK,会挡住提交/构建;wikilink BROKEN 与 stale 属于 WARN,放行但留下报告。

这套 BLOCK/WARN 的二分,决定了系统能否存活。若把一切都设为 BLOCK,一个微不足道的 stale 就能卡住提交,人们便会开始绕过验证本身。被绕过的验证等于不存在的验证。反过来,若全设为 WARN,连真正该拦的数据完整性违规也会径直通过。该拦什么、该只标记什么,这条界线才是验证系统真正的设计着力点。


24.1.8 测量 —— 开启代码验证前后

这是笔者在自己所在的某 MMORPG 开发商 A 的项目A中,以约 90 份文档的规模为基准观察到的方向。部分绝对数值为笔者估算(未经验证),真正有意义的是趋势。

条目 手动检查时期 代码验证 cascade
断链引用的发现时点 玩家·QA 报告之后 提交前(方向:事后 → 事前)
单次一致性检查耗时 数小时(笔者估算) 数十秒(脚本实测)
stale 累积潜伏 潜伏数周 下一次提交即 WARN
错误自动修复导致的事故 不适用 靠保守修复,保持 0 起

与其照单全收这些数字,不如只相信「发现时点从事后被提前到了事前」这个方向。验证系统真正的价值,与其说在于节省时间,不如说在于事故在到达玩家之前就被拦下这一「位置的移动」


24.1.9 常见的失败

模式 处方
把所有违规都设为 BLOCK,导致人们绕过验证 BLOCK/WARN 二分,只对数据完整性 P0 做阻断
自动修复激进到连猜测也做 只对显式别名的重命名自动处理,含糊则交给人
只看断链而忽视 stale 用修改时间比对,单独检测陈旧引用
默许对产出物的手动编辑 用 source_map audit 把编辑显现为 FAIL
有脚本却没绑定到钩子 接入 pre-commit·构建关卡,使其无法不被运行

动手试试 —— 一套最小验证 cascade

setup. 用 git 管理文档文件夹(作为提交时间比较的基准)。wikilink 统一采用 [[目标]][[目标|别名]] 的写法。

prompt. 把上面实操记录里的提示词全文原样交给 AI,但绝不要直接采用它的第一次输出。务必验证并拒绝这三点后重新请求:(1) 未提交文件的处理、(2) 子文件夹遍历、(3) 管道别名解析。这是 AI 几乎每次都会在第一版里漏掉的地方。

verify. 运行脚本,拿到 TSV。手动抽查 5 个样本,确认 BROKEN 行是否真的是断链。若出现假 BROKEN,说明别名/子文件夹解析还没做到位。确认正常后,把它绑定到 pre-commit 钩子,并按退出码分流:WARN(STALE/BROKEN)放行、BLOCK(数据完整性 P0)阻断。

单人精简版. 如果你只是独自写一份小型 GDD,整套 cascade 就过头了。只取 stale 检测这一步即可。哪怕只用 git 时间比较文档是否比数据表更旧,也能抓住大多数「以为改了、其实没改」的事故。等文档超过 30 份、手工追不过来时,再加上自动修复与 source_map audit 就行。


本章要点

24.2 Mermaid 图表自动化 —— 让文档自己画出自己的图

一位新人策划入职第三天问我:"前辈,这些系统之间以怎样的顺序相互影响,有没有整理成图的地方?"我犹豫了。图是有的。半年前有人在白板上画的照片,存在维基的某个角落里。可那张图里,如今已经消失的两个系统还活着,而其后新增的三个核心循环却没有画进去。最终我答道:"别信图,去读文档。"这是个让人羞愧的回答。当图与文档不一致的那一刻,图就不再是信息,而成了错误信息。

先把本章的结论说在前面:人手绘制的图表,一两个月内必定腐坏。因此必须把画图这件事从人的手上剥离,让文档结构本身吐出自己的图。本文用一次真实的操作记录来展示这个过程。我把一段以文档为输入、生成 Mermaid 代码的实操记录(worked transcript,即完整保留的真实操作过程记录)整段收录,并在本页真实渲染由此产出的图表。也就是说,讲解某种技法的文字,用这种技法的产物来证明它自己。


24.2.1 为什么偏偏是 Mermaid

图表工具很多。draw.io、Figma、Visio,甚至白板照片。这些工具有一个共同的陷阱:产物是图片文件(图像)。图像无法在 git 中逐行追踪改动,处理文本的 LLM 无法直接生成或修改它,也无法以代码形式嵌入 Markdown 文档。从运营角度看,最致命的是第一点。一张无法追踪谁在何时、为何改动的图,时间一长就会变成无人负责的遗物。

Mermaid 一次解决这三点。把图表写成文本,渲染交给查看器自行完成。因为是文本,git diff 连新增一个节点都能捕捉到。因为是文本,LLM 能读能写。因为是文本,它可以原样放进 Markdown 代码块。本章的正文正是明证。此刻你正在读的这句话下面、即将出现的那些图表,全都是 Markdown 里的文本块,会在本书构建过程中渲染成图。

不过要防止误解。完全没有必要把所有运营资料都做成图表。罗列条目用项目符号更快,比较数值用表格更快。Mermaid 胜出的场合只有三种:关系(什么与什么相连)、流程(什么在什么之后)、时序(谁在何时向谁发送了什么)。在这三者之外的场合硬塞图表,反而会加重认知负担。


24.2.2 主干:从文档结构中抽取图表的一次操作

从这里开始,就是本章的主干。这里不做抽象讲解,而是从头到尾展示把一整块真实文档转换成 Mermaid 的过程。输入是项目A运营文档中记录系统依赖结构的一段 Markdown 片段(下面是经过匿名化的真实节选)。

# 系统依赖备忘 (运营文档节选,匿名化)

- combat_core 依赖 stat_engine
- skill_runtime 依赖 combat_core
- skill_runtime 依赖 vfx_pool
- quest_director 依赖 skill_runtime
- quest_director 依赖 dialog_graph
- economy_loop 订阅 quest_director 的奖励钩子
- economy_loop 读取 stat_engine 的派生属性

把它手工画成图表,是七个节点、七条箭头。画一次是画得出来的。问题出在下周——当 mail_box 系统被加进来、dialog_graph 被拆成两个的时候。手绘图从那一刻起就开始说谎。所以,把这项转换交给 LLM,而不是人。

第1步 —— 提示词全文

下面是我实际投喂的提示词。一个字都未加修饰,原样收录。

请把下面的系统依赖备忘转换成 Mermaid graph(自上而下,graph TB)。规则是:
1. 只把备忘中出现的系统作为节点,禁止新增系统。
2. "A 依赖 B" 写作 A --> B。
3. "订阅""读取" 这类弱耦合,用虚线 -.-> 并标上关系名。
4. 节点 id 照用备忘中的英文名,不要加中文标签。
只要代码块,去掉说明和注释。

[备忘]
- combat_core 依赖 stat_engine
- skill_runtime 依赖 combat_core
- skill_runtime 依赖 vfx_pool
- quest_director 依赖 skill_runtime
- quest_director 依赖 dialog_graph
- economy_loop 订阅 quest_director 的奖励钩子
- economy_loop 读取 stat_engine 的派生属性

规则 1 和 4 是关键。把图表交给 LLM 时,最常见的事故是"热心的幻觉"。它会加上没让它加的节点("一般都会有登录系统,所以我加上了"),擅自贴上中文标签,让 ID 与正文检索对不上。用规则来切断这份热心。

第2步 —— Claude 的原始输出(无摘要)

下面是模型第一次返回的响应。不加提炼,原样搬来。其中混着一个小问题,连这个问题一并收录。



graph TB skill_runtime --> combat_core combat_core --> stat_engine skill_runtime --> vfx_pool quest_director --> skill_runtime quest_director --> dialog_graph economy_loop -.보상 훅 구독.-> quest_director economy_loop -.파생 스탯 읽기.-> stat_engine

语法没问题,节点也正好七个。它没有编造新系统。规则 1、4 都遵守了。可有一处让我在意:虚线标签里用了韩文(보상 훅 구독파생 스탯 읽기)。这并不算违反规则。规则 3 要求"标上关系名",模型忠实地照做了。只是我凭经验知道,在 Mermaid 中,韩文标签一旦含有空格,一些旧版渲染器就会渲染失败。而这一点,模型无从知晓。

第3步 —— 人工验证与拒绝

这一步在自动化中最常被省略,却最重要。我没有原样接受输出,而是拒绝了。拒绝理由有两条。

第一,要把虚线标签里带空格的韩文换成英文单词(token),以确保渲染兼容性。第二,弱耦合(虚线)和强耦合(实线)混在同一张图里,却没有颜色或样式上的区分,一眼看不出来。我带着这两点重新提出了请求。

第4步 —— 二次请求的提示词

基本可以了。只改两处。

1. 把虚线箭头的标签换成一个英文单词(不含空格)。
   "보상 훅 구독" -> subscribes,"파생 스탯 읽기" -> reads
   原因:部分渲染器在韩文+空格的边标签上会渲染失败。
2. 为了在视觉上区分虚线(弱耦合)节点与实线(强耦合)节点,
   给像 economy_loop 这样只有弱耦合的节点,用 classDef 加上浅灰色样式。
3. 其余保持不变。

第5步 —— 二次请求得到的原始输出



graph TB skill_runtime --> combat_core combat_core --> stat_engine skill_runtime --> vfx_pool quest_director --> skill_runtime quest_director --> dialog_graph economy_loop -.subscribes.-> quest_director economy_loop -.reads.-> stat_engine classDef weak fill:#eee,stroke:#999,stroke-dasharray:4 3; class economy_loop weak;

这次我接受了。标签换成了单个英文 token,只有 economy_loop 被单独标成灰色,"这个系统不是直接依赖,而是仅通过订阅、读取相连的边缘系统"这一信息由颜色传达了出来。如果我一行提示词都不碰、纯手工去画,很可能连这个 classDef 都想不到。

主干的产物 —— 就在此处真实渲染

上述实操记录的最终输出,不经手工誊抄,以代码块原样收进本书页面。本书构建会把它画成图。这就是"用自己的技法证明自己"的实物。

graph TB skill_runtime --> combat_core combat_core --> stat_engine skill_runtime --> vfx_pool quest_director --> skill_runtime quest_director --> dialog_graph economy_loop -.subscribes.-> quest_director economy_loop -.reads.-> stat_engine classDef weak fill:#eee,stroke:#999,stroke-dasharray:4 3; class economy_loop weak;

一段文档节选,经过五轮往返,变成了进入 git、可由 LLM 更新、并在本页渲染的运营资产。下周若加入 mail_box,只需在备忘里写一行,再投一次同样的提示词即可。用不着人动笔。


24.2.3 第二张图:画出这条自动化流水线本身

如果说前一张图是"转换的结果",这一张就是"转换的过程"。我把刚才分五步走完的实操流程做成了流程图。这张图同样是用相同方式交给 LLM 抽取的,并经过了相同的验证。我把结果原样收录。

flowchart TD SRC[运营文档节选] --> PROMPT[编写转换提示词] PROMPT --> LLM[Claude 原始输出] LLM --> CHECK{人工验证} CHECK -->|拒绝:渲染兼容·可读性| REASK[二次请求提示词] REASK --> LLM CHECK -->|批准| EMBED[将代码块嵌入 Markdown] EMBED --> GIT[git 提交·diff 追踪] GIT -->|文档变更时| SRC classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class LLM ai; class PROMPT,CHECK,REASK human; class SRC,EMBED,GIT data;

这张流程图想说的有一点。我想强调的——不是用虚线,而是用粗箭头——是中间那个菱形,也就是 人工验证。若沉醉于"自动化"这个词而把这个节点删掉,第1步那种热心的幻觉就会原样写进运营文档。自动化把人从画图中解放出来,却不把人从判断中解放出来。循环里最后那条箭头(文档变更时运营文档节选)是关键。有了这条反馈回路,图表才不是一次性资料,而是与文档一同、不衰老而共同成长的资产。


24.2.4 转换脚本:不用 LLM 也能跑的确定性路径

LLM 转换很灵活,但当关系已经以结构化数据形式存在时,就没必要专门去叫模型了。像项目A的决策卡这种字段固定的数据,一个小小的 Python 脚本更快也更诚实(幻觉从根源上就不可能发生)。下面是把决策卡列表转换成决策图 Mermaid 的实际脚本的核心部分。

# decision_graph_to_mermaid.py
# 决策卡(结构化数据)-> 转换为 Mermaid graph。无需 LLM,确定性。

def to_mermaid(decisions):
    lines = ["graph LR"]
    # 1) 声明节点:id 与标题照搬。不编造。
    for d in decisions:
        safe_title = d.title.replace('"', "'")   # 只对引号做 escape
        lines.append(f'    {d.id}["{safe_title}"]')
    # 2) 边:把关系类型作为箭头标签。
    for d in decisions:
        for rel in d.relations:
            lines.append(f'    {d.id} -->|{rel.type}| {rel.target}')
    return "\n".join(lines)

关键在于只用两步就结束。声明节点,连接边。输入里没有的节点,绝不会出现在输出里。这个脚本接收三张决策卡,就会得到下面这样的图。

graph LR D_A["全局冷却 0.3秒"] -->|superseded_by| D_B["全局冷却 0.5秒"] D_B -->|relates_to| D_C["恢复期允许例外"] D_B -->|side_effect| D_D["近战技能伤害 -5%"] classDef human fill:#fde68a,stroke:#b45309,color:#000; class D_A,D_B,D_C,D_D human;

一个决策被另一个决策取代(superseded_by),连由此派生的副作用(side_effect)也用一条箭头呈现出来。不必读完几十行文本记录的决策日志,只要这一张图,"为什么现在冷却时间是 0.5秒"的来龙去脉五分钟内就能弄清。

何时用 LLM、何时用脚本?判断标准很简单。输入是结构化数据(字段固定的卡片、表格)就用脚本,输入是自由文本(会议记录、备忘、对话)就用 LLM。对结构化数据用 LLM,只会平白背上不必要的幻觉风险;对自由文本用脚本,解析规则会无止境地膨胀。


24.2.5 四个陷阱与对策

这些是在运营图表自动化的过程中实际踩过的雷。

第一,过于复杂的陷阱。节点超过五十个,图就不再帮助认知,反而妨碍认知。对策是把一屏限制在二十到三十个之间,再大就用 subgraph 把区域圈起来,或者干脆把图一分为二。

第二,更新中断的陷阱。人们容易以为这只发生在手绘图上,但即便做了自动化,只要不改输入文档,一样会腐坏。对策就是前面流程图里的那条反馈回路。让输入文档成为单一事实来源(single source of truth),图表始终从那里重新生成。

第三,过于抽象的陷阱。"系统大致是这样连在一起的"这种程度的图很好看,却没用。对策是在节点里填入真实 ID(skill_runtimeD_B),而不是抽象名词。正文检索与图表共享同一标识符,才能从图直接跳到代码。

第四,不经审核就直接使用 LLM 输出的陷阱。正如主干第3步所见,模型可以在遵守规则的同时,做出会导致渲染失败的标签。对策是绝不把人工验证节点从流水线中拿掉。


24.2.6 效果 —— 诚实地讲述变化

很想拿出数字,但这里只讲方向。下面的对比是作者在自己带过的团队里体感到的变化,并非精确测量值,而是作者的估计(未经验证)。

变化最鲜明的,是新人理解系统的速度。入职头几天才能搞清的"这些系统是怎么连在一起的",在一张自动生成的依赖图面前,缩短到了一小时上下。会议资料的准备也轻松了。以前开会前一天总得有人手工重画一遍,现在把文档转换一次就完事。最重要的是,过去当图表与实际不符时冒出的"这张图能信吗"这个问题本身,几乎消失了。输入文档即是图,文档对了,图也就对了。

反过来老实说,自动化并非万能。结构化程度还不高的早期阶段的想法草图,依然是白板更快。自动化要在结构大致定型之后才会发光。


本章要点


动手试试

setup. 一个保存文档的 Markdown 仓库,加上一个渲染 Mermaid 的查看器(大多数 Markdown 查看器和 git 托管都已内置),就够了。从待转换的文档中挑一段属于"关系、流程、时序"的片段(例如系统依赖备忘)。

prompt. 把这段片段套进正文主干第1步的提示词模板,投给 LLM。务必加入两条规则:"不要添加备忘里没有的节点""ID 照用原文英文名"。若是结构化数据,就别用 LLM,改用 decision_graph_to_mermaid.py 这类确定性脚本来转换。

verify. 把输出的代码块贴进 Markdown,实际渲染一遍。确认三点:(1) 有没有冒出输入里没有的节点,(2) 边标签有没有渲染失败,(3) 正文里用的 ID 与图表 ID 是否一致。只要有一处对不上,就用二次请求的提示词拒绝掉、重新获取。通过了就提交到 git —— 现在改动都会由 diff 追踪。

单人精简版

如果你是既没有团队也没有脚本的单人作业者,就这样精简。在笔记应用里,把系统、待办、想法之间的关系,用"A 依赖 B"格式的项目符号记下来。每周一次,把整份列表整个复制过去,丢一句"把这个转换成 Mermaid graph TB,不要添加列表里没有的节点"。把返回的代码块贴到笔记最上方。就这样。因为不用手画,没有更新负担;只要输入列表还活着,图就永远是最新的。

24.3 Wikilink 与文档层级 —— 连接与分类,检索的两个入口

连接(wikilink)与分类(层级)是同一个问题的两个入口。一个回答"这个决策会牵连到哪里",另一个回答"这份文档住在哪里"。

新加入的策划在第二天早上问道:"战斗的全局冷却值是 0.5 秒,对吗?依据在哪份文档里?"我答不上来。决策记录肯定在某个地方,可我记不清那是战斗规则手册、会议记录,还是季度报告了。我们三个人凑在一起,用 grep 把整个文件夹翻了个遍。同一个数字在六处出现,可其中哪个是"原始决策"、哪个是"引用副本",根本分不清。我们花了 40 分钟。最后找到的,是埋在会议记录里的一行字。

那天晚上,我意识到缺了两样东西。第一,文档之间没有显式的连接。同一个数字出现在六处,却没有任何地方写下"这个是从那里引用来的"这根线。第二,文档没有安身的层级。决策记录散落在规则手册、会议记录、报告里,没有"决策就住在这里"的约定。

这两样东西就是本章的主题。wikilink 把连接写成文本,层级把分类约定成文件夹。二者看似是分开的技法,实则是检索这一个问题的两面。


24.3.1 连接缺失时,什么会崩塌

文档只有 30 份时,靠脑子全能记住。一旦超过 100 份,人的记忆就当不了索引了。这时能依靠的只有两条路之一:用 grep 把全部扫一遍(慢且不准确),或者顺着文档里写下的显式连接走(快且准确)。

grep 不准确的原因很简单。检索 combat_global_cooldown_constant 这个字符串时,决定了这个值的文档,和只是提到这个值的文档,会被一视同仁地捞出来。哪个是原始的,grep 并不知道。反过来,如果约定在文档里写 [[combat_global_cooldown_constant]] 这样的双方括号表记,那么"这里是有意引用那个 atom"的信号就留在了字符串本身。用 \[\[combat_global_cooldown 模式缩小范围,偶然的提及就被排除,只剩下有意的引用。

这个一行的表记约定,就成了图的一条边(edge)。文档 A 写下 [[atom_X]],就产生了 A→X 方向的边。200 份文档各自写下几条,不用谁去画,图就在文本里累积起来。

下面是我们项目里 atom、决策、文档被 wikilink 串联起来的一个片段。节点颜色表示种类,箭头表示引用方向。

[[CombatFormula_v3]] [[Meeting_W21]]

[[combat_global_ cooldown_constant]]

[[D2026_Q2_017]] [[D2026_Q2_018]]

文档 atom 决策

这个小片段展示的是:新策划那个问题的答案,其实早已在图里。顺着进入 combat_global_cooldown_constant atom 的箭头反向追溯,就能找到决策 D2026_Q2_017。不是 40 分钟,而是一次反向引用。


24.3.2 表记约定 —— 四种类型,一种格式

我们把用 wikilink 串联的对象只定为四种。种类一多,格式就会松动;格式一松动,grep 又会变得不准确。

四种全部是 [[name]] 一种格式。name 必须全局唯一。如果 atom 名字在两处冲突,就会在图里合并成同一个节点,酿成"战斗的 cooldown"和"UI 的 cooldown"变成一个节点的事故。所以在 atom 命名规则里,强制加上领域 prefix(combat_ui_)。


24.3.3 wikilink_apply.py —— 应用与修复

只有表记约定还不够。让人手工给 200 份文档一个个加方括号是不现实的,就算加好了,只要 atom 名字一改就全断了。所以我们运行一个做两件事的脚本。第一是应用(apply)——把正文中出现的已知 atom 名字自动转成 wikilink。第二是修复(heal)——找出被改名或断掉的链接,加以更新和上报。

wikilink_apply.py 的核心部分长这样。

# wikilink_apply.py — 将正文中的 atom 名字应用为 [[wikilink]],并修复断掉的链接
import re
from pathlib import Path

WIKILINK = re.compile(r"\[\[([A-Za-z0-9_]+)\]\]")
# 只捕获尚未成为链接、以裸名出现的 atom 名字(前面没有 [[ 的情况)
BARE_NAME = lambda name: re.compile(rf"(?<!\[\[)(?<![A-Za-z0-9_])({re.escape(name)})(?![A-Za-z0-9_])(?!\]\])")

def load_known_atoms(registry: Path) -> set[str]:
    # _atom_registry.tsv:第一列是当前有效的 atom name
    return {ln.split("\t")[0].strip()
            for ln in registry.read_text(encoding="utf-8").splitlines()
            if ln.strip() and not ln.startswith("#")}

def apply_links(text: str, known: set[str]) -> tuple[str, int]:
    applied = 0
    for name in sorted(known, key=len, reverse=True):  # 长名字优先:防止部分匹配污染
        text, n = BARE_NAME(name).subn(rf"[[{name}]]", text)
        applied += n
    return text, applied

def heal_links(text: str, known: set[str], aliases: dict[str, str]) -> tuple[str, list[str]]:
    dead = []
    def repl(m):
        ref = m.group(1)
        if ref in known:
            return m.group(0)              # 存活 → 原样保留
        if ref in aliases:                  # 已改名的 atom → 修复为新名字
            return f"[[{aliases[ref]}]]"
        dead.append(ref)                    # 真正的 dead link → 上报
        return m.group(0)
    return WIKILINK.sub(repl, text), dead

这里有两个设计选择,是正文的脊椎。

第一,apply_links 先替换长名字。当存在 combat_cooldowncombat_cooldown_global 两个 atom 时,如果先替换短的那个,长的那个的前半部分就会被污染。按长度降序排序的这一行,挡住了这个事故。这是我第一次写的时候漏掉的地方,直到真的出现了 [[combat_cooldown]]_global 这样断掉的结果,才补上。

第二,heal_links 经由改名字典(aliases)来修复。当 atom 名字从 combat_gcd 改成 combat_global_cooldown_constant 时,把旧名字自动替换成新名字,只有在字典里也找不到时,才作为 dead link 上报。名字每改一次,不用手工去改 200 份文档,只需加一行 alias。


24.3.4 实操记录 —— 把 wikilink 补全交给 Claude

自动 apply 只会给"已知的 atom 名字"加链接。但正文里不写 atom 名字、而是用自然语言展开的句子("战斗的全局冷却是 0.5 秒")它抓不到。把这类自然语言引用转成 wikilink 候选,LLM 比人快。下面是实际往来的完整实操记录(worked transcript,即完整保留的真实操作过程记录)——输出没有做摘要,连我拒绝的部分也原样保留。

我的提示词(全文):

我给你一段战斗规则手册和一份已登记的 atom 清单。请在正文中找出指向这些
atom 的自然语言表达,以表格形式提出 [[wikilink]] 候选。只在确定时提出,模糊
时只标注"模糊"即可。不要重写正文,只说哪个语句改成哪个链接。未登记的
概念请单独归入"新增 atom 候选"。

[已登记的 atom]
combat_global_cooldown_constant
combat_skill_cast_interrupt_rule
combat_resource_regen_rate

[正文]
战斗的全局冷却为 0.5 秒,对所有主动技能通用。
施法过程中被击中,施法会被打断。但恢复类技能除外。
资源再生只有在非战斗状态下才以正常速度恢复。

Claude 输出(原始未加工):

建议的 wikilink:

| 正文语句                          | 候选链接                                | 确信 |
|----------------------------------|----------------------------------------|------|
| "战斗的全局冷却为 0.5 秒"          | [[combat_global_cooldown_constant]]    | 高   |
| "施法过程中被击中,施法会被打断"    | [[combat_skill_cast_interrupt_rule]]   | 高   |
| "资源再生只有在非战斗状态下"        | [[combat_resource_regen_rate]]         | 中   |

新增 atom 候选:
- "恢复类技能除外" → 这条例外规则在已登记的 atom 中任何一处都
  没有明确写出。看来需要一个像 combat_skill_cast_interrupt_heal_exception
  这样的独立 atom。(模糊:它也可能是 interrupt_rule 的下位条款,
  无法判定是否为独立 atom)

注意:"资源再生"这一行,由于没看到 atom 定义,无法确定
combat_resource_regen_rate 指的是"再生速度"还是"非战斗条件",故确信为中。

我的验证与拒绝:

三行里我接受了两行。全局冷却和施法中断与 atom 定义完全一致。第三行"资源再生"这一行,Claude 自己把确信降到"中"是对的 —— combat_resource_regen_rate atom 只定义了速度值,而"只有在非战斗状态下"这个条件归另一个 atom 管辖。如果照样加链接,就会酿成把"条件"错误连到"速度" atom 上的事故。我拒绝了。

新增 atom 候选的指摘是准确的。"恢复类例外"确实在任何地方都没有 atom。只是 Claude 说模糊的那部分("是 interrupt_rule 的下位条款,还是独立 atom")属于人来判断的范畴,我决定把它拆分为独立 atom。

再次请求:

"资源再生"这一行不要加链接。改为把 [[combat_resource_regen_rate]] 只
限定为"速度","非战斗条件"拆分为新增 atom。请分别为这两个 atom 各写
一行定义。恢复例外也作为独立 atom 写一行定义。

在这一来一回中,LLM 做的不是"从 0 造出候选",而是"帮我挑选候选"。关键在于,人有明确可以拒绝之处。若是自动发布,一个错误的链接就会永久留在图里。


24.3.5 lint —— 在构建阶段挡住断掉的连接

链接随时间会断。atom 被废弃、名字被改、错别字混进来。所以每次构建都跑一遍 wikilink lint。检查项与处理如下。

只把 dead link 设为警告而非阻断,是有意的。在给 atom 改名的中间状态会短暂出现 dead,如果把这个当成构建失败来挡,作业就停了。取而代之,让它先确认修复字典。格式违规和名字冲突则立即阻断 —— 这两者会污染整张图。

这个 lint 是自我证明的。wikilink_apply.py 造出的链接,由同一套系统的 lint 来检查,其结果又作为另一个 atom 决策留存。工具用自己的标准验证自己的产物,这个闭环是运营的基本骨架。


24.3.6 分类 —— 文档安身的层级

到这里是连接。现在是分类。如果说 wikilink 回答"这个决策会牵连到哪里",那么层级回答"这份文档住在哪里"。两者都缺,新策划的 40 分钟检索就会重演。

我们的文档文件夹分为四层。这个层级与信息架构的 Layer 统合共享同一套骨架 —— 愿景、系统、内容、元信息各占一层。

docs/
├── L0_vision/              愿景(5 份以下,几乎不变)
├── L1_systems/             各领域规则手册
│   ├── combat/
│   ├── narrative/
│   └── ui/
├── L2_content/             单个内容
│   ├── characters/
│   └── quests/
└── L4_meta/                运营·决策·会议·原子
    ├── decisions/
    ├── meetings/
    ├── reports/
    └── atoms/

L3 空着,是因为数据表和 DB 占了那个位置(是表格而非文档)。新策划要找的决策住在 L4_meta/decisions/ —— 单单有这一个约定,40 分钟检索本可以用"决策就在那里"这一句话结束。

要让层级作为检索入口发挥作用,得同时守住五点。缺任何一点,分类都会崩。

  1. 按语义分类,禁止按时间。 combat/narrative/ 能被检索到,但 2026-Q1/2026-Q2/ 六个月后没人会打开。时间由 git 记录,没理由再用文件夹分一遍。
  2. 深度不超过 4。 L1_systems/combat/skills/active/single_target/attack.md 是 5 层。路径一超过一屏,人就没法把位置装进脑子。
  3. 文件名 prefix。spec_report_decision_char_ 把种类放进文件名。不看文件夹也能看出种类。
  4. 每个文件夹都有 README。 每个文件夹的定义、内容、命名规则由 README 写下。这是新加入者的第一个入口。
  5. _ prefix 元信息文件夹。 _archive/_TEMPLATES/_NAMING/ 在自动排序中会排到上面,不与正式内容混在一起。

文档不会停在一个位置。撰写期间以 status: draft 住在正式文件夹里,激活后变成 status: active,废弃时不是删除而是移到 _archive/ 并标上 status: deprecated。不删除废弃资料是铁律。六个月后有人问"那个决策为什么被推翻了?"时,答案只在废弃资料里。若是删掉了,就没有办法事后重新找回决策的依据。

大的变更不只交给 git,还在 frontmatter 里以 change_log 留存。

---
title: combat_global_cooldown_rule
version: v3
last_modifier: teammate_a
change_log:
  - v1 (2025): 初稿
  - v2 (2025): cooldown 0.3 → 0.5  ([[D2026_Q2_017]])
  - v3 (2026): 新增恢复例外        ([[D2026_Q2_018]])
---

请注意,change_log 里的决策 ID 是用 wikilink 写的。连接与分类在这里相遇。文档住在层级中的一个位置(分类),但它的变更历史连向决策图(连接)。一份 frontmatter 同时打开两个入口。


24.3.7 每季度一次,整理周期

层级放着不管就会腐烂。空文件夹冒出来,搁了六个月的 draft 越堆越多,深度悄悄增加。所以每季度整理一次。删掉空文件夹,给超过六个月的 draft 决定激活还是废弃,把深度 5 以上的平摊,给没有 README 的文件夹补写或废弃,_archive 超过一半就压缩保存。没有这个周期,层级就会被噪声塞满,信号与噪声的区分随之消失。

把整个流程看成一张图就是这样。文档进来、被连接、被分类、被验证、直到被废弃,是一个闭环。

flowchart TD A[撰写新文档<br/>status: draft] --> B[wikilink_apply.py<br/>atom 名字自动链接] B --> C[LLM 补全<br/>自然语言引用候选] C --> D{人工评审} D -->|采纳| E[层级放置<br/>L0~L4 + prefix] D -->|拒绝| C E --> F[构建 lint<br/>dead/冲突/循环检查] F -->|通过| G[status: active] F -->|dead link| H[确认修复字典] H --> F G --> I[季度整理周期] I -->|废弃| J[_archive/<br/>status: deprecated] I -->|保留| G classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef fail fill:#fee2e2,stroke:#dc2626,color:#7f1d1d; class B,F,H code; class C ai; class D,I human; class A data; class G pass; class J fail;

在这个闭环里,连接(B·C·D·F)与分类(E·I·J)交替运作。二者不是各转各的,而是在一份文档的生命周期里相互咬合。


24.3.8 效果 —— 什么发生了怎样的改变

这些数字是作者在自己项目里对比引入前后的方向性。不是精密测量值,而是同一件工作在两种环境下做时,体感到的差异大小(作者观察,未精密计量)。

在连接和层级立起来之前,新策划的决策追溯问题,像开头那个 40 分钟的例子一样,长的时候要花一两个小时。引入之后,一次 atom 反向引用 —— 是分钟量级。文档检索从 5\~10 分钟缩短到 30 秒上下,这是层级的语义分类与 prefix 一起作用的结果。因错误引用导致的事故(把已废弃的值误当作现行值那一类)从每季度好几件减到一两件 —— wikilink 明示了"这里引用的是那个 atom",复制的值和原始的值不再混淆。

变化最大的是新加入者的适应。没有层级,光是熟悉哪个文件夹里有什么就要花好几天;没有连接,就无从把握各系统之间如何交织。两者齐备之后,靠文件夹 README 熟悉位置,顺着 wikilink 图自行探索系统间的关系。"只有问才知道的东西"变成了"顺着走就看得见的东西"。

这个效果只有在两个入口同时存在时才出现。只有连接没有分类,图是有了,却不知道文档住在哪里;只有分类没有连接,文件夹是干净的,却不知道决策牵连向何处。


24.3.9 常见失败与处方

连接这一侧最常见的失败是噪声链接。以为 wikilink 好,就给所有名词都加方括号,图就会被无意义的边塞满,可视化工具随之失灵。原则是只留下能提出并回答"这份文档与那份文档是什么关系"的链接。其次是自动发布 —— 把 LLM 造的链接不经评审就提交,就会像实操记录里"资源再生"那一行那样,把错误的连接永久留下。apply 是自动的,发布则由人来做。

分类这一侧的失败大多是五条原则的违反。按时间分文件夹、深度 5 以上、文件名无规则、缺 README。还有最难挽回的 —— 删除废弃资料。被删掉的决策依据无法重建。送往 _archive 的那一行,守住了六个月后的学习资料。


本章要点


动手试试 —— wikilink + 层级的最小引入

setup. 在文档文件夹里建 L0_vision/ L1_systems/ L2_content/ L4_meta/ 四个文件夹,给每个文件夹放一行 README。把 atom 名字清单汇集到 _atom_registry.tsv 一个文件里(第一列 = atom name)。

prompt. 把正文一段和已登记的 atom 清单交给 LLM,这样请求 —— "在正文中找出指向这些 atom 的自然语言表达,以表格形式提出 [[wikilink]] 候选。只在确定时提出,模糊时只标注'模糊'。不要重写正文。未登记的概念归入'新增 atom 候选'。"

verify. 对每个被提议的链接,都与 atom 定义对照。只有当 atom 指向的对象与正文指向的对象完全相同时才采纳,条件、属性、例外一旦不符就拒绝。采纳后用 grep "\[\[name\]\]" 确认链接是否真的输入了、有没有 dead link。

单人精简版. 想不用脚本也不用 lint 就开始,两行规则就够。(1)决策一律放在 decisions/ 一个文件夹里,以 decision_*.md 命名。(2)其他文档提到那个决策时,写作 [[decision_id]]。只守住这两行,"那个决策在哪儿?"这个问题就能用一次 grep "\[\[decision_" 回答。工具在文档超过 100 份之后再引入也不迟。

24.4 来源追踪·data lineage

怀疑数据的那一刻,总是来得太晚。往往要等到错误的数值已经录入线上构建之后,才会开始追问"这个数字到底是从哪儿来的"。


Alpha 版本发布前的那个周五傍晚,组员 B 走到我工位旁。他手里的笔记本电脑上开着一份战斗数值电子表格。"总监,Boss 第一阶段的血量,表里写的是 48,000,可进到构建里的值却是 52,000。这两个到底哪个对?"

我不知道。准确地说——在那个当下,没有人知道。表里的 52,000 可能是反映了几天前会议决定的最新值,也可能是有人把未经验证的值临时填了进去。48,000 也可能是那次会议之前的共识值。两个数字都很像那么回事。像那么回事并不等于有依据。

要回答这个问题,就得沿着来源一路回溯。是在哪次会议上决定的,那次会议的输入是什么,是谁把它誊进了表里。然而,一旦这条追踪的链条只存在于人的记忆里,答案就会变成"我明天去问一下组员 A"。运营(LiveOps)进行到第六个月时,这类悬而未决的问题会堆积如山。data lineage——数据的谱系——正是让这座山根本不会形成的基础设施。

核心只有一条:来源不能靠手写。由人事后补录的来源记录,撑不过一个月。只有在数据被创建的那一刻自动记录下来的来源,才能存活下来。


24.4.1 数据来源断裂的五种代价

自动记录 _source_map.tsv 中的一行,成本只是几毫秒。而当这一行缺失时,要付出的代价会朝五个方向蔓延开来。

来源断裂 (未记录 source)

无法验证 "这个数值从哪来?"

变更遗漏 原始更新→衍生搁置

法务暴露 外部资产依据丢失

事故诊断延迟 无法反向追溯错误值

交接损失 "为何如此决定?"无答案

陷阱在于:这五种代价,没有任何一种会在创建数据的那一刻显现出来。它们全都是在几周后、几个月后、经手人换了之后,账单才寄到。所以来源绝不能成为"以后再整理"的对象,它必须在被创建的那一刻就记录下来。


24.4.2 _source_map.tsv —— 来源映射的标准骨架

项目A 运营中的来源映射文件只有 _source_map.tsv 一个。用制表符分隔的文本,理由很简单:人可以一眼读完一行,脚本用一次 split('\t') 就能解析,git diff 也能干净地展示单行的变更。CSV 一旦正文里混入逗号就会出错,JSON 则让人难以一行读完。

asset_id    source_type source  created creator notes
spec_combat_v3  internal    mtg_battle_2026-04-18   2026-04-18  teammate_a  decision_D2026_Q2_017 依据
data_boss_hp_v3 internal    decision_D2026_Q2_017   2026-04-18  teammate_b  第一阶段 48000 确定
asset_K_001_concept internal_ai_assisted    imagegen + teammate_b 整理    2026-04-20  teammate_b  legal_review 完成
data_user_voice_W21 external_aggregated forum + community + sns 2026-05-25  auto_collect    13.1 管线产出
ref_visual_tone_a   external_reference  refgame (2024)  2026-04-15  teammate_c  视觉基调参考,无直接借用

六个字段的角色都很明确。asset_id 是数据的唯一键,source_type 是分类(下文详述),source 是来源的位置——会议 ID、决定 ID、采集管线、外部作品名,created/creator 是何时·由谁,notes 是供人阅读的一行上下文。

回头再看第二行和第三行,上一节里组员 B 的问题就有了答案。data_boss_hp_v3 的来源是 decision_D2026_Q2_017,notes 里填的是"第一阶段 48000 确定"。构建里的 52,000 并不在这条 lineage 中。也就是说,52,000 是未经验证的临时值,正确答案是 48,000。这个问题在 1\~2 分钟内就能了结——不必调动任何人的记忆,也不必毁掉一个周五的傍晚。

不过,这个文件上还挂着另一条规则。只要有人用手工方式编辑 _source_map.tsv,integrity_check 的 audit 就会给出 FAIL。理由留到下一节讲——因为来源只应由自动方式记录。


24.4.3 source_type 五种 —— 分类即处理规则

把来源分成五类,并不是出于整理癖,而是因为每一种 source_type 所附带的运营规则各不相同。

internal 会议·proposal·决定 → 仅追踪

internal_ai_assisted AI 生成+人工整理 → 标明来源

external_aggregated 用户测量 → 标明采集日·样本

external_reference 第三方作品 → legal_review 必需

self_measured

分类 → 处理规则映射 internal 系列:能用决定 ID 反向追溯即通过 ai_assisted:notes 必须写明用了哪个工具·哪段提示词 aggregated:没有采集时间就无法解读数值 reference:legal_review 为空则 audit FAIL ← 强制 self_measured:仿真/KPI,建议在 notes 写明复现条件 → source_type 不是标签,而是 检查器读取并据此分支的开关

来看 external_reference 这一行。如果某个资产把 refgame 当作视觉基调的参考,那么这个资产在未经法务审查前就不能进入构建。source_type 是 external_reference,而 legal_review 记录为空时,audit 就会拦下它。这正是标签不止步于标签、而成为检查器所读取的开关的地方。所谓五种分类是运营信任的骨架,指的就是这种强制力。


24.4.4 自动记录 —— 在创建的那一刻留下的一行

现在是核心。来源必须在数据生成的时刻被自动记录。项目A 的 source_tracker.py 挂在资产生成的钩子(hook)上。

# source_tracker.py
import time, getpass, csv
from pathlib import Path

SOURCE_MAP = Path("_source_map.tsv")
VALID_TYPES = {
    "internal", "internal_ai_assisted",
    "external_aggregated", "external_reference", "self_measured",
}

def track_source(asset_id: str, source_type: str, source: str, notes: str = ""):
    if source_type not in VALID_TYPES:
        raise ValueError(f"unknown source_type: {source_type}")
    if source_type == "external_reference" and "legal_review" not in notes:
        raise ValueError(f"{asset_id}: external_reference 资产必须标注 legal_review")

    record = [
        asset_id,
        source_type,
        source,
        time.strftime("%Y-%m-%d"),
        getpass.getuser(),
        notes,
    ]
    with SOURCE_MAP.open("a", encoding="utf-8", newline="") as f:
        csv.writer(f, delimiter="\t").writerow(record)

只要这个函数挂在资产生成管线上——在表格被 export 时、概念资产被登记时、用户数据被汇总时——就会自动 append 一行来源。没有任何一个环节需要人去记着做,事后补录的负担趋近于 0。

getpass.getuser() 自动填充 creator 字段这一点虽小,却是决定性的。让人自己填名字,就会出现空格;让系统来填,就不会有空格。而且,external_reference 一旦缺少 legal_review,记录本身就会被直接拒绝——这就堵死了没有来源的外部资产进入系统的通道。

这里我要点出 AI 应用的真实样貌。这个 track_source 函数的初稿,是我让 Claude 给的。下面是那段实操记录(worked transcript,完整保留的真实操作过程记录)的一条主干。

我的提示词(全文):

写一个往 _source_map.tsv 里 append 一行来源的 Python 函数。
列:asset_id、source_type、source、created(今天的日期)、
creator(当前 OS 用户)、notes。用制表符分隔。
source_type 限定为 5 种:internal、internal_ai_assisted、
external_aggregated、external_reference、self_measured。

Claude 的原始输出(未作概括):

import csv, datetime, os

def track_source(asset_id, source_type, source, notes=""):
    record = [asset_id, source_type, source,
              datetime.date.today().isoformat(),
              os.getlogin(),          # ← 当前用户
              notes]
    with open("_source_map.tsv", "a", newline="") as f:
        csv.writer(f, delimiter="\t").writerow(record)

如果您想加上 source_type 的校验,可以建一个 VALID_TYPES 集合, 在函数开头做检查即可。需要的话,我帮您加进去吗?

我的验证与否决:

这段输出能跑,但在两处不符合我们的运营。第一,os.getlogin() 在 cron 或 CI 中被调用时,会因环境不同而抛出空字符串或抛出异常。我们的 export 管线也会以无人值守的排程运行。所以我把它换成了 getpass.getuser()——它读取环境变量,能更稳定地取到用户。第二,Claude 把 source_type 校验作为"需要的话帮您加"的选项留着,但对我们来说,那不是选项,而是必需。一旦没有校验,拼写错误的 source_type 就会混进来,分类随之崩塌。

我的再次请求:

换成 getpass.getuser()。另外,source_type 校验不要作为选项,
而要作为必需固化进函数里。此外,当类型是 external_reference,
而 notes 里没有 legal_review 字符串时,请抛出 ValueError。
我想从源头杜绝没有经过法务审查的外部资产被记录下来。

这次再请求的结果,就是上面给出的最终版 source_tracker.py。要点在于,并不是 Claude 的第一版输出错了,而是因为我知道 AI 所不了解的运营约束——无人值守的排程、legal_review 强制——才需要否决与再请求。AI 通常能快速给出一般意义上正确的代码,而人负责验证"在我们的环境里对不对"。那个验证点,恰恰成了来源追踪系统的设计决策。


24.4.5 audit FAIL —— 阻止手工编辑的完整性检查

前面说过,只要有人用手工编辑 _source_map.tsv,integrity_check 就会给出 FAIL。它是怎么抓到的?

原理很简单。每当 track_source append 一行,就把该行的核心字段(asset_id、source_type、source、created、creator)序列化后生成哈希,累积到单独的 .source_map.audit 文件里。audit 检查会重新读取 _source_map.tsv,用同样的方式重新计算哈希,再比对两份哈希列表。

# integrity_check 中 source_map audit 部分
def audit_source_map():
    fails = []
    rows = read_tsv(SOURCE_MAP)
    expected = read_lines(AUDIT_FILE)   # append 时累积的哈希

    for i, row in enumerate(rows):
        h = row_hash(row["asset_id"], row["source_type"],
                     row["source"], row["created"], row["creator"])
        if i >= len(expected) or h != expected[i]:
            fails.append(f"L{i+1} {row['asset_id']}: 疑似手工编辑(哈希不一致)")

    if len(rows) != len(expected):
        fails.append(f"行数不一致: tsv={len(rows)} audit={len(expected)}")
    return fails

假设有人在表格里把 data_boss_hp_v3 的 source 手工改成了 decision_D2026_Q2_099。这一行的哈希便与 audit 中累积的原始哈希对不上,检查会输出如下内容。

[FAIL] source_map audit
  L3 data_boss_hp_v3: 疑似手工编辑(哈希不一致)
  → 未经过 track_source() 的变更。来源只能通过代码路径记录。

这种强制为什么重要?一旦允许手工编辑,终究会有人在赶时间时把来源"看似合理地"填进去。那一刻,lineage 就从真相沦为一份装着某人猜测的文件。audit FAIL 给"来源只能走自动路径"这条规则装上了牙齿。§24.1 的 verification 系统会把这个 audit 与其他检查捆在一起,在 CI 中运行。


24.4.6 变更传播 —— 源头一变,唤醒衍生

自动记录来源的真正理由,在于反向查询:"源头 X 变了,哪些会受影响?"

def find_derivatives(source_id: str):
    return [
        row for row in read_tsv(SOURCE_MAP)
        if row["source"] == source_id
    ]

# 用法:decision_D2026_Q2_017 在会议上被推翻
deps = find_derivatives("decision_D2026_Q2_017")
# → [spec_combat_v3, data_boss_hp_v3, ...]

假设 decision_D2026_Q2_017 在下一次会议上被推翻,Boss 第一阶段的血量从 48,000 改成了 50,000。调用 find_derivatives,就会立刻列出挂在这个决定上的所有衍生资产——战斗规格文档、血量数据表。通知会发送给各资产的负责人,而"仍盯着旧决定的资产"残留在构建里的事故,便从每季度数起减少到几乎为 0。

靠手写的来源,这种反向查询根本无法成立。来源若是自由文本,decision_D2026_Q2_017 在某一行会被写成"Q2 017 决定",在另一行又写成"第二季度第 17 次会议决定",匹配就此破裂。唯有具备 _source_map.tsv 的标准格式与 track_source 的自动记录,变更传播才能真正运转起来。


24.4.7 lineage 图 —— 一屏之内的数据谱系

_source_map.tsv 逐行来看是平面的,但当一个 source 成为另一个资产的 source 时,数据的谱系便串成了链条。把这条链条铺展到一屏之内,决定所依据的输入的可信度就一目了然。这张 mermaid 图,是由 §24.2 的图表自动生成管线读取 _source_map.tsv 后直接产出的——相当于用自身的技法来证明自身的资产。

graph LR Meeting["mtg_battle_2026-04-18<br/>(会议)"] --> Proposal["P2026_Q2_017<br/>(提案)"] Proposal --> Decision["D2026_Q2_017<br/>(决定·48000 确定)"] Decision --> Spec["spec_combat_v3<br/>(战斗规格)"] Decision --> Data["data_boss_hp_v3<br/>(血量表)"] Data --> Build["build_2026-05-20<br/>(Alpha 版本)"] Build --> UserData["data_user_voice_W21<br/>(用户测量)"] UserData -.下一个决定的输入.-> Decision classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class Meeting,Decision human; class Proposal,Spec,Data,Build,UserData data;

循环会自然而然地出现。构建产出用户数据,用户数据又成为下一个决定的输入。一旦看得见这个循环,"这个数值从哪来"就变成了屏幕上的一条路径。组员 B 那个周五的问题,在这张图里,不过是沿着 Data → Decision 回溯一步而已。


24.4.8 度量 —— lineage 运营的成效

在项目A,我比较了引入 lineage 系统前后的情况。下表中的时间数值是作者估算(未经验证),应当看方向与比例的差异,而非绝对值。件数则是从每季度的 audit 日志中统计出的实测值。

项目 无 lineage lineage 运营 性质
掌握数据来源的时间 1\~2 小时 1\~2 分钟 作者估算(未经验证)
数据可信度的验证依据 资深者记忆 即时查询来源 定性
源头变更时的衍生遗漏 每季度 5\~8 件 0\~1 件 audit 日志实测
外部资产法务审查遗漏 可能发生 0 件(强制记录) audit 日志实测
每季度 audit 耗时 1\~2 天 2\~3 小时 作者估算(未经验证)

最硬的数字是"源头变更时的衍生遗漏"这一行。因为 audit 日志里原样留有决定 ID 和被遗漏的衍生资产,所以能数得出来。时间数值受测量环境(团队规模·资产数量)影响很大,因此明确标注为估算。方向是清楚的——一旦来源被自动记录,追踪就从记忆变成了查询。


24.4.9 常见失败与处方

失败模式 处方
事后用手工填补来源 track_source 在生成时刻自动记录
来源格式各行不一 _source_map.tsv 制表符标准 + 强制格式
外部资产法务审查遗漏 在 source_type 校验中强制 legal_review
手工编辑 _source_map.tsv integrity_check audit 做哈希比对 FAIL
源头变更时放任衍生 find_derivatives 反向查询 + 通知
只用文字说明谱系 用 mermaid 自动生成,一屏可视化

六条处方的共同点是,它们都不依赖人的自觉。自动记录·格式强制·哈希比对·反向查询,全都由系统来做。因为来源追踪之所以崩溃,唯一的原因就是"人会忘"。


24.4.10 第 24 部分收尾

第 24 部分是用自动化来支撑运营信任的四条支线。第 1 章用 verification 把验证收拢到一个点,第 2 章用 mermaid 自动生成画出结构,第 3 章用 wikilink 与 document hierarchy 把文档连接并分层,最后在这第 4 章,用来源与谱系为数据的信任加上了封印。

贯穿这四章的一句话是这样的。运营的信任,来自系统的记录,而非人的记忆。 正如 verification 自动追问"这份产出物是否合乎规则",lineage 也自动回答"这份数据从何而来"。二者的关键都在于:即便人忘了,它们也不会崩塌。

这套运营经验,与全书的 Layer 统一设计哲学同出一脉。愿景(资产化·信任)落到系统(来源规则),系统落到数据(_source_map.tsv),数据再落到构建·QA(audit·自动更新)——这是一条自上而下的链条。这条链条本身,就是 lineage。


本章要点


动手试试(setup → prompt → verify)

setup. 在项目根目录下创建 _source_map.tsv,以一行表头(asset_id\tsource_type\tsource\tcreated\tcreator\tnotes)起头,并放入上面的 source_tracker.py。在资产 export·登记脚本的末尾挂上 track_source(...) 调用。

prompt. 如果需要来源自动记录函数,可以这样请求 Claude。

写一个往 _source_map.tsv(制表符分隔,列:asset_id、source_type、source、
created、creator、notes)里 append 一行的 Python 函数。
source_type 限定为 5 种,并且当类型是 external_reference 而 notes 里
没有 legal_review 时,抛出 ValueError。creator 用 getpass.getuser()。

verify. 亲自确认两件事。(1) 用 external_reference 调用但把 notes 留空,看是否抛出 ValueError。(2) 用文本编辑器把 _source_map.tsv 的 source 字段改动一个字符,再运行 integrity_check 的 source_map audit,看是否出现 FAIL。两处都被拦住,就说明来源通道已经封闭。

单人精简版

如果是一个人工作,_source_map.tsv 一个文件加 track_source 一个函数就够了。integrity audit·反向查询·mermaid 自动化,可以等到资产超过几十个、来源开始变得容易混淆时,再一个一个加上去。起点只是"在填写数值时,把一行来源自动留在同一个位置"这一个习惯。

附录 A. 公司 PC 系统详细清单

本附录把笔者所在的 MMORPG 开发商 A 的公司 PC 环境,从硬件到工具、知识资产、验证材料,汇总在一张清单里。为了让读者一眼看清正文中多处提到的"用这样的工具""用这样的 atom 结构""用这样的报告"在现实中究竟以怎样的规模与组合存在,笔者做了这份整理。文中的实名与专有名词均已匿名化;数值会随时点而变化,因此请不要将其当作绝对值,而应作为比例与构成来阅读。

阅读本附录有两种方法。一种是逐项对照自己的环境。把"我用什么引擎、用什么协作工具、知识资产以什么形式积累"填进同样的格子里,自己的空白栏就会显现出来。另一种是看整体构成的平衡。工具多并不等于环境好,重要的是引擎、策划、美术、协作、AI 这五条轴是否彼此不掣肘、相互咬合。比起单个项目本身,更请留意它们交织而成的整体结构。


A.1 系统概览

首先是作为基础的硬件与操作系统。一旦真正开始大量使用 AI 工具,就会遇到在本地运行扩散模型(diffusion)或 STT(语音识别)的场景,因此内存与 GPU 的余量会直接变成工作速度。下面的配置请当作接近下限的基准线来看——"到这个程度就能顺畅运转"。

A.1.1 硬件

项目 规格
CPU 桌面工作站级
RAM 64GB 以上
GPU UE5 开发用(兼容 CUDA)
存储 SSD 2TB + NAS 共享
显示器 27 英寸 2 台

RAM 与 GPU 这两行是核心,因为经常会遇到同时开着引擎编辑器、本地 LLM 辅助工具和图像生成的时刻。

A.1.2 操作系统与基础环境

项目
OS Windows 11 Pro
虚拟化 WSL2(Ubuntu),按需
备份 每日自动

WSL2 不是一直开着,而只在需要运行 Linux 专用工具时才调用。备份以每日自动方式运行——这是本表中最重要的一行。


A.2 工具清单

工具分为五类:引擎与工具、设计与策划、美术、协作与运营,以及 AI 与 LLM。并非一个人会用到全部五类,但作为策划,每天都会在设计与策划、协作与运营、AI 与 LLM 这三类之间往返。请注意,每一类都呈现"一两个必备 + 辅助"的形态。

A.2.1 游戏引擎与工具

工具 用途
Unreal Engine 5.7 以上 主引擎
Visual Studio 代码
Rider C# IDE(辅助)
Perforce 或 SVN 代码与资产版本管理

引擎与版本管理是一对。策划也必须会用版本管理客户端,因为数据表与设计文档都在同一个仓库里运转。

A.2.2 设计与策划

工具 用途
Excel 数据表 + VBA 宏
Markdown 编辑器 设计文档与会议记录
Figma UI 与线框图
Mermaid 图表

这是策划的日常工作台。Excel 是数据的大本营,Markdown 是文字的大本营,也是 AI 工具嵌入最深的两个点。Mermaid 之所以单占一格,正如正文所强调的,是因为图示就是达成共识的语言。

A.2.3 美术

工具 用途
Maya / Blender 3D
Substance 3D 纹理与材质
Photoshop 2D 与插画
Stable Diffusion(SDXL) / ComfyUI 自托管的概念图与纹理正式量产(LoRA·ControlNet)
Midjourney 初期情绪板(辅助)

虽然不是策划直接使用的工具,但在与美术组来回沟通概念时,了解对方手上有哪些工具,会让需求的清晰度大不相同。正式量产以自托管的 Stable Diffusion(SDXL)/ComfyUI 为主轴——因为不把素材上传到外部即可保护 IP,并且能用角色 LoRA·ControlNet 在每次反复生成时都控制同一人物的一致性。像 Midjourney 这类封闭式工具,只在最初摸索项目基调的早期情绪板阶段作为辅助使用,而在需要一致性与反复控制的正式量产中不使用。

A.2.4 协作与运营

工具 用途
协作工具(ClickUp) 任务
内部即时通讯工具 实时沟通
自建 Wiki Wiki 与长期文档
自建门户网页 统一界面(20.3)

沟通的时间轴决定了工具的划分。需要即时性的实时沟通走内部即时通讯工具,待办事项走协作工具(我们团队用 ClickUp),需要长期留存的知识走自建的 Wiki——即使把任务追踪器换成 JIRA·Redmine、Wiki 换成 Confluence·Notion,无论即时通讯工具是哪一个,本书的思路都不变。自建门户网页是把这三者与 AI 工具在同一屏幕上连接起来的统一入口,将在 20.3 中详细展开。

A.2.5 AI 与 LLM

工具 用途
Claude(Opus + Sonnet) 主 LLM
GPT-4 备选
Whisper(自托管) 语音识别(STT)
Stable Diffusion 图像生成(自托管)
MCP 服务器 工具集成(20.4)

主力用 Claude,GPT-4 作为交叉验证与备选。敏感的语音与图像不外发、而以自托管方式处理——这一原则体现在 Whisper 和 Stable Diffusion 这两行里。MCP 服务器是把这些工具嵌入工作流的黏合剂,其结构将在 20.4 中说明。


A.3 atom 清单

atom 是把正文讲过的"决策的最小单元"落到文件里的知识碎片。下表展示这些 atom 按领域如何分布,是 2026 年 5 月这一时点的一个切面。请不要看绝对数量,而看决策集中在哪个领域。决策集中之处,正是该项目思考得最激烈的地方。

类别 atom 数 备注
combat 47 战斗系统决策
narrative 38 叙事 5 层
ui 31 UI·HUD
balance 28 数值平衡
level 22 关卡设计
character 19 角色·voice_profile
meta·governance 18 流程与规则
qa·integrity 16 验证
content 14 内容量产
operations 14 运营工作流
external_reference 12 外部资料
economy 11 经济与资源
其他 34 分类进行中

战斗(combat)最厚重、叙事紧随其后的分布,直接显露出这个项目是以战斗为核心的 MMORPG,同时又不愿放弃叙事比重的性格。"其他 34"是尚未确定类别的新决策,这一栏若变得过大,就是该动分类体系的信号。截至 2026 年 5 月,合计为 304 个。


A.4 验证与运营材料

即使有工具与知识,若没有确认其是否正常运转的装置,质量就会一路下滑。本节把这类确认装置分为两种来展示:周期性产出的报告,以及为事后追溯决策而留下的决策卡。

A.4.1 报告

报告 频率
每日构建报告 每天
Alpha 差距报告 每周(10.3)
冲刺质量报告 每两周
里程碑 QA 报告 每个里程碑
季度复盘 每季度

频率就是报告的性质。每天产出的是状态检查,每周、每两周是趋势检查,里程碑与季度是方向检查。AI 贡献最大的地方,是像每日、每周这样反复产出的报告的初稿撰写,其案例在 10.3 中展开。

A.4.2 决策卡

季度 决策数
2025 年第 4 季度 132
2026 年第 1 季度 156
2026 年第 2 季度(进行中) 89
累计 547

每个季度都有 100 件左右的决策以卡片形式留存,这一事实本身就体现了"把决策当作记录而非记忆来对待"的运营原则。第 2 季度的 89 件是季度中途时点的累计,属于进行中的数值,到季度末会达到上一季度的水平。这些卡片积累起来、晋升为 A.3 的 atom 的这一流程,正是本系统的学习轴。


A.5 会议分布(参考)

会议既是时间流失最严重的地方,也是最快能体会到 AI 工具效果的地方。下表是把各季度的会议按类别归并后的平均分布,用作估算 17.3 中会议记录系统输入规模的参考资料。数值每个季度都有起伏,因此以区间记录。

类别 季度平均
每日(daily) 65\~70 次
战斗(battle) 35\~45 次
美术(art) 25\~30 次
问题(issue) 8\~15 次
评审(review) 6\~10 次
其他(1:1·外部) 40\~50 次

每日会议最频繁,战斗相关会议紧随其后。它与 A.3 的 atom 分布形状相同,这一点意味深长。决策集中的领域,会议也集中。会议越频繁的环境,会议记录自动整理的效用就越大,其具体运营在 17.3 中说明。


A.6 读者参考

至此为止的所有表格,都是笔者环境的一张照片。它不是让你照搬的清单,而请当作用同一套框架整理自己环境的样本。团队规模、品类、平台不同,工具、atom 分布、会议比重都会不同。重要的不是项目是否一致,而是"基础 → 工具 → 知识 → 验证"这四层在你自己的环境中是否也不断裂地连贯衔接。这四层当中若有空白的一栏,那一栏就是接下来要动手的地方。

附录 B. 工具借用流程(从公司到个人的通用化)

本附录整理了作者把在公司项目A中构建并运营的工具·技能,拿到个人 PC 和一般性工作中重新使用的流程。核心问题只有一个:"如何在不侵犯公司知识资产的前提下,只合法地借用从中学到的工具骨架?"本附录展示了如何划定这条边界,把什么拿了过来、又把什么留在原处,以及如何将这些决定留存为记录。

本附录的使用方法如下:先对照自己的处境阅读 B.1 的五条原则,再把 B.3 的流程原样走一遍。然后复制 B.4 的记录模板,按照自己想借用的工具填写即可。既然处理的是公司资产,那么比起"快",能"留下记录"更为优先,整篇附录都是以这一视角构建的。


B.1 借用的五条原则

这是在拿取工具之前已达成共识的五条原则。这五条不是顺序,而是必须同时满足的条件——只要其中一条崩塌,就搁置借用本身。前三条是关于"拿取什么"的技术边界,后两条是关于"如何堂堂正正地拿取"的流程边界。

原则 说明
1. 不包含公司 IP 移除公司名·实名·专有名词
2. 只拿取工具骨架 屏蔽公司领域数据
3. 通用化重构 重新构建为一般使用场景
4. 引用·出处明确 注明为从公司借用的工具
5. 法务·人事共识 走完公司的许可流程

最常动摇的一行是第 2 条。算法与结构(骨架)可以拿取,但如果连那套骨架所预设的公司数据格式也一并带过来,那一刻就等于把 IP 也拿走了。把骨架与数据剥离开来的工作,才是借用的主体。


B.2 借用的六种工具·技能

按照原则实际拿到个人 PC 的工具共有六种(截至 2026 年 5 月)。它们有一个共同点——都是处理数据的工具,而这并非偶然。因为数据处理工具的骨架(解析·转换·可视化逻辑)与领域(公司表格的具体格式)相对更容易剥离。

工具 公司原版 个人通用版
excel-reader xlsm 表格·VBA 提取 通用 Excel 处理
relation-map-gen FK 关系 HTML 通用数据关系图
schema-doc 从表格生成 Markdown 模式(schema) 通用模式文档化
table-creator 批量生成数据表格 通用表格生成
gdd-gen 自动生成 GDD 通用文档生成
gdd-export 从 Markdown 转换为多表格 xlsx 通用 xlsx 转换

对比表格的中间与右侧两栏,就能看出通用化意味着什么。左边那栏是"公司表格""GDD"这类带有领域的名称,右边那栏是"通用 Excel""通用文档"这类剥掉领域的名称。名称中公司消失,正是通用化的第一个信号。


B.3 借用流程

把原则(B.1)落到实际操作上,就是下面六个步骤。最关键的分岔点是第 2 步和第 4 步。若在第 2 步没能把骨架与领域干净地分开,后面所有步骤都会被污染;若跳过第 4 步的公司许可,那么无论做得多好,都会成为一件无法使用的工具。

flowchart TD A[1. 识别公司工具] --> B[2. 分离公司依赖区域] B --> B1[依赖公司数据·专有名词] B --> B2[工具骨架:算法·结构] B1 --> C[3. 移除公司依赖 + 通用变量化] B2 --> C C --> D[4. 公司许可:法务·经理] D --> E[5. 应用到个人 PC 环境·验证] E --> F[6. 注明出处 + 借用记录] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class B2 code; class D human; class B1,F data;

六个步骤中最耗时的一环,不是编码工作(第 2·3 步),而是第 4 步——公司达成共识与法务通过。这意味着最大的关卡不是技术,而是信任;因此借用总是按照先谈妥共识、再打磨代码的顺序推进。


B.4 借用记录

对借用的工具,必须一并留下记录。因为日后可能会有人追问"这件工具从哪里来、移除了什么、得到了谁的许可"。下面是以 excel-reader 为例的记录模板,你可以直接复制这个框架,按照自己的工具填写。

---
tool: excel-reader (个人通用版)
original_source: 公司项目A
adopted: 2026-05
permission: 公司经理 + 法务通过
modifications:
  - 移除对公司表格格式的依赖
  - 移除公司领域函数(xlsm VBA)
  - 泛化为通用 csv/xlsx 处理
  - 全面移除公司名·实名引用
usage_in_book: 本书的工具案例引用 (Part 1·5·6·8 等)
---

模板中的日期栏(adopted)要像 2026-05 这样写成确定的年-月。"2026 年 5 月前后"之类的随意写法看起来像是留待日后填写的空格,因此要当场把确定借用的时点钉死。

这份记录中最有价值的两行是 permissionmodifications。前一行证明借用是正当的,后一行证明剥离了什么。有了这两行,即使日后有人提出疑问,也留有可供追溯的依据。


B.5 未借用的工具

拿走了什么固然重要,留下了什么同样重要。这里记下了公司工具中被有意不予借用的部分及其理由。这些被留下的工具有一个共同点:要么是公司的核心 IP,要么深深绑定在公司组织结构上,以致无法把骨架与领域剥离开来。

工具 未借用理由
公司战斗系统工具 公司核心 IP,公司独占
公司叙事文档工具 依赖公司世界观
公司战斗 TF 工具 依赖公司组织结构
公司人事·财务工具 不适配外部环境

这与 B.2 中拿走的工具全部是"数据处理"形成了鲜明对比。拿走的是能与领域分离的工具,留下的是与领域浑然一体的工具。可分离性决定了可借用性。


B.6 读者参考 —— 借用前的自查表

最后,这是在拿取工具之前必须让自己逐一通过的五个项目。这张表是一份判定合格/不合格的检查清单——只有五项全部通过才借用,只要有一项卡住就搁置。没有"大体上还行"这回事。因为处理公司资产的工作,容不得部分通过。

检查项目 通过标准
是否已获得公司许可 经理·法务的明确同意
是否通过法务审查 书面或有记录的确认
是否已完全移除公司 IP grep watchlist 检查为 0 件
是否验证了通用性 确认在其他环境中也能运行
是否有事故应对流程 定义了追踪·回收路径

请不要把这五项读作五格的通过,而要读作五道锁。把在公司学到的东西正当地变为个人的资产,这件事确实可行,但那份正当性只有在这五道锁全部扣上时才成立。

附录 C. 权限·设置参考

本附录是把正文中引用的工具与系统的权限·设置汇集到一处的参考表。正文说明的是"为什么要这样运营",而真要应用到自己的环境时,还需要知道"那么具体该把什么值填到哪里"。本附录填补的正是这块空白。

比起设置值本身,选择这个值的理由更重要。与其原样复制表中的数字,不如先读一读每个条目下面的简短说明,再按照自己的团队规模和风险水平来调整。如果是一个人工作,就无需划分权限等级;如果没有外包,把外包相关的条目整块删掉即可。

使用本附录有两种方式。第一次搭建环境时,从 C.1 开始按顺序通读,像检查清单一样确认有没有遗漏的条目。运营过程中出了事故时,先翻到 C.7(事故应对),找到对应的事故行,再往上回溯到它上方的预防条目。


C.1 LLM API 权限

LLM API 密钥与成本直接挂钩,一旦泄露就会立即演变成金钱事故。因此先从密钥管理和权限等级讲起。

C.1.1 密钥管理

密钥 保管
Anthropic API 环境变量 + 1Password
OpenAI API 环境变量 + 1Password
自托管 公司内部

密钥不写进代码,而是通过环境变量注入,原件放在密钥管理工具(如 1Password)里。最常见的事故是把密钥写在代码里连同 git 一起提交,因此在 git 中包含密钥一律禁止。

C.1.2 权限等级

用户 权限
总监·资深 full(负责运营 cost cap)
普通成员 按任务设置 cap
外包 按任务限一次

权限不按信任划分,而按责任的大小划分。拥有 full 权限的人同时承担管理成本上限(cost cap)的责任。对外包只按任务单位开放一次,任务结束后即收回。


C.2 工具设置标准

每种工具的推荐设置各不相同,但核心是把分析类工作和创意类工作分开。分析必须可复现,创意则需要多样性。

C.2.1 Claude Code

下面是 Claude Code 的基本设置示例。逐行来看:固定模型,开启扩展思考,设置 token 上限,开启自动更新,并挂上一个在提交提示词时注入记忆的 hook。

{
  "model": "claude-opus-4-8",
  "extended_thinking": true,
  "max_tokens": 100000,
  "auto_update": true,
  "hooks": {
    "UserPromptSubmit": ["~/.claude/hooks/inject_memory.py"]
  }
}

model 的值只是示例。模型名称会随每一代而变化(本示例以写作时点为准),因此不要照抄,而要用 /model 确认当前可用的最新名称再填入。即使名称变了,本书的工作流骨架依然照常运作(参见附录 K)。

挂在 hooks.UserPromptSubmit 上的脚本,负责在每次提交提示词时自动嵌入相关的记忆片段。这套记忆注入机制会在正文第 24 部分详细展开。

C.2.1.1 工具权限模式(allow / deny)

在同一个 settings.json 中,权限用 permissions 块单独存放。可以让 AI 不经人工批准就自动执行的工具写进 allow,而一旦出一次事故就会致命、必须阻止自动执行的工具写进 deny。写法为 工具(命令模式) 形式,:* 表示"以该命令开头的所有调用"。

{
  "permissions": {
    "allow": [
      "Bash(ls:*)",
      "Bash(git status:*)",
      "Bash(git diff:*)",
      "Read(*)",
      "Grep(*)"
    ],
    "deny": [
      "Bash(rm -rf:*)",
      "Bash(git push --force:*)"
    ]
  }
}

像读取·搜索(Read·Grep)和状态查询(git status·git diff)这类不会造成不可逆后果的命令,用 allow 自动放行,以减少批准弹窗带来的疲劳。反过来,像 rm -rfgit push --force 这类一次失误就无法恢复的命令,无论把自动放行的范围放得多宽,都要写进 deny

运营原则有四条。

原则 内容
从白名单起步 自动放行从最小集开始,仅在必要时才加入 allow
危险命令显式拦截 rm -rf·git push --force 一律 deny
定期清理 每季度复查 allow,删掉不再使用的权限
按领域分离 把全局权限和项目权限分开,让家用 PC 和公司 PC 拥有不同的策略

allow 列表并不是静态配置,而是工作累积的痕迹。重复的工作越多,它就越长,因此最好一并配上按季度清理的周期。这套权限运营的背景会在正文第 1 部分第 3 章详细展开。

C.2.2 自研工具

设置 推荐值
LLM temperature(分析) 0
LLM temperature(创意) 0.7
Cache TTL 1 小时
Cost cap(每日) 按工具定义
Backup 周期 每日

分析类调用把 temperature 设为 0,让相同输入产生相同输出。验证·lint·分类这类结果不能有波动的工作都属于此类。反过来,发散想法或生成初稿则给予 0.7 左右的多样性。成本上限不设统一标准,而是按工具分别设定,因为每种工具的调用频率和 token 消耗各不相同。


C.3 git 权限

分支 权限
main 仅总监·资深可 push
feature/* 所有成员
protected branches 强制代码评审

main 分支禁止直接 push,所有变更都要在 feature 分支经过代码评审后再合入。force-push 会覆盖协作历史,因此予以禁止,只有在不得已需要事故恢复时,才经总监与代码负责人(code lead)一致同意后开例外。


C.4 文件系统权限

文档文件夹按 Layer 结构(L0\~L4)划分权限。越往上影响范围越广,写权限收得越窄;越往下工作越分散,写权限放得越宽。

文件夹 权限
docs/L0_vision/ 总监 write,所有人 read
docs/L1_systems/ 领域总监 write,所有人 read
docs/L2_content/ 负责人 read·write
docs/L4_meta/ 所有人 write
team_memory/各用户/ 仅本人 read·write

愿景(L0)只由总监书写,所有人可读。系统(L1)由领域总监书写。内容(L2)由负责人书写,元信息·临时(L4)则任何人都可书写。个人记忆仅本人可访问。这套 Layer 结构本身会在正文第 6 部分讲解。


C.5 备份·恢复

资料 备份
git repo git 本身 + 远程备份
表格(Excel) git + 每日备份
用户数据 DB 备份(服务器标准)
会议记录·决策 git
记忆 每日自动同步

每种资料的备份路径各不相同,但原则只有一个:越是丢失后难以恢复的资料,越要做双重保管。文本资产(会议记录·决策·代码)有 git 就等于有了备份,二进制或服务器数据则另设单独备份。恢复时间目标(RTO)定在 4 小时以内,但这个值要按团队能承受的停机时间来调整。


C.6 安全

领域 规则
向外部 LLM 发送敏感数据 placeholder 或自托管
支付·个人信息 绝不发送给 LLM
引用外部资料 注明出处 + 法务审查
用户数据保护 匿名化 + 遵守 GDPR

最容易遵守、却最常被破坏的规则,就是"不把敏感数据发送给外部 LLM"。因为工作紧急时,把真实数据直接粘贴进去的诱惑很大。支付·个人信息一律设为禁止发送,若需要分析,就用 placeholder 替换,或使用自托管模型。


C.7 事故应对

事故 应对
因 LLM 幻觉发出错误信息 立即撤回 + 上报
成本超出 cap 自动阻断 + 复查
著作权事故 1 小时内停止使用 + 法务
安全事故(key 泄露) 立即更换密钥 + 审查使用记录
数据丢失 备份恢复 + 事故分析

很多时候,比起阻止事故,尽快止损更为重要。表中的应对都遵循"先停下,再分析"的顺序。key 泄露时,先别追究原因,而是先更换密钥并查看使用记录。成本超过上限时,先自动阻断再复查。这套应对流程要用文档明确固化下来,并定期演练,好让真出事故时能毫不犹豫地运转起来。

附录 D. R&D 文档命名与 Frontmatter 标准

公司项目A的 R&D 文档命名与 frontmatter 标准(_NAMING_FRONTMATTER_STANDARD)的通用化版本。


D.1 命名标准

D.1.1 atom

<category>_<topic>_<subtopic>.md

示例:
combat_global_cooldown_constant.md
narrative_voice_profile_K_007.md
ui_button_primary_style.md

snake_case。类别前缀。

D.1.2 决策卡

D<YEAR>_Q<QUARTER>_<NUMBER>.md

示例:
D2026_Q2_017.md

年份、季度、编号。

D.1.3 会议记录

<category>_<YYYY-MM-DD>[_<seq>].md

示例:
95_BattleTF_2026-05-18.md
art_review_2026-05-18_1.md
art_review_2026-05-18_2.md

D.1.4 规格书

spec_<topic>.md

示例:
spec_combat_global_cooldown.md
spec_guild_attendance.md

D.1.5 报告

report_<period>_<type>.md

示例:
report_W21_alpha_gap.md
report_Q2_user_voice.md

D.2 Frontmatter 标准

D.2.1 atom

---
name: combat_global_cooldown_constant
description: 定义战斗系统的全局冷却标准值
type: atom
category: combat
status: active
priority: P0
related_atoms:
  - combat_skill_cooldown_rule
  - combat_healing_skill_cooldown_exception
created: 2026-05-18
last_modified: 2026-05-18
related:
  derives_from: [combat_design_principle]
  affects: [combat_skill_cooldown_rule, ui_skill_cooldown_indicator]
---

D.2.2 决策卡

---
decision_id: D2026_Q2_017
title: 战斗全局冷却统一为 0.5 秒
type: system_change
status: active
created: 2026-05-18
created_by: 团队成员 A
approved_by: 李旼洙
scope:
  - combat_system
affected_atoms: [...]
implementation:
  target_build: 2026-05-18
verification:
  layer_1: passed
  layer_2: passed
  layer_3: pending
---

D.2.3 会议记录

---
type: meeting_note
category: battle
date: 2026-05-18
attendees: [团队成员 A, 团队成员 B, 李旼洙]
related_atoms: [...]
---

D.2.4 规格书

---
title: 公会签到功能规格
type: spec
priority: P1
target_milestone: MS2
---

D.3 必填 vs 选填字段

D.3.1 必填字段

文档类型 必填
atom name, description, type, category, status
决策卡 decision_id, title, type, status, created, scope
会议记录 type, category, date, attendees
规格书 title, type, priority

D.3.2 选填字段(有则更好)

文档类型 选填
atom related, last_modified, priority
决策卡 rationale, related_decisions, verification
会议记录 related_atoms, sub_topic
规格书 target_milestone, related_atoms

D.4 Lint 自动检查

# frontmatter_lint.py

for file in glob("**/*.md"):
    fm = parse_frontmatter(file)
    if not fm:
        warn(f"{file}: 缺少 frontmatter")

    doc_type = infer_type_from_filename(file)
    required = REQUIRED_FIELDS[doc_type]

    for field in required:
        if field not in fm:
            warn(f"{file}: 缺少必填字段 {field}")

构建时自动执行。违规触发 alert。


D.5 防止命名冲突

领域 防止
atom name 全局 unique
决策 ID 季度内 unique
会议 ID 日期 + seq
文件名 文件夹内 unique

命名冲突时自动阻止。


D.6 变更流程

D.6.1 atom 重命名

1. 创建新名称的 atom
2. 将原 atom 的所有 wikilink 更新为新名称(自动)
3. 将原 atom 弃用(deprecated)+ 重定向(redirect)
4. 1 个月后移至 _archive

仓促的重命名有损坏资料的风险。

D.6.2 frontmatter 标准变更

1. 提出变更理由(decision 流程)
2. 为所有现有文档编写迁移脚本
3. 更新构建 lint
4. 通知团队

D.7 读者参考

本标准基于作者的环境。读者需根据自身环境进行调整。核心在于:

核心 理由
命名一致性 检索、自动化
Frontmatter 标准 工具友好
必填、选填分离 填写负担 ↓
Lint 自动 强制标准
变更流程 保护资料

附录 E. MCP 服务器目录(游戏策划视角)

MCP(Model Context Protocol,模型上下文协议)是 LLM 以标准化方式连接外部工具与数据的通道。正文第 20 部分讲过项目管理类 MCP,但可以引入游戏策划工作流的 MCP 服务器远不止于此。本附录把这些候选项汇总起来,一目了然,并按引入顺序标注了优先级,是一份目录。

本目录的目的不是"把这些全都装上",而是"需要时知道从哪里挑选"。一次性接入多个 MCP,就分不清是哪个引发了问题。请按照 E.4 的引入周期,一个一个地增加。

使用方法如下。起初只看 E.2.1 的 P0 列表。基本功打牢后再进入 E.2.2(P1);当团队出现特殊需求时,再考量 E.2.3(P2)或 E.3(自行开发)。担心成本就先看 E.5,想为故障做准备就先看 E.6。


E.1 MCP 的四大应用领域

MCP 服务器按连接对象大致分为四类。游戏策划每天来回使用的工具,大多都落在这几类之内。

领域 MCP 服务器
项目管理 ClickUp、JIRA、Linear
文档 Confluence、Notion、Google Drive
协作 团队即时通讯工具(Slack、Discord 等)
数据 Excel、Google Sheets、DB

项目管理对接任务与排期,文档对接策划案与 wiki,协作对接团队沟通,数据对接数值与道具表格。先弄清自己团队已在使用的工具属于哪个领域,引入的候选范围自然就会收窄。


E.2 推荐的 MCP 服务器(游戏策划优先级)

优先级以"没有它工作是否会卡住"为标准评定。P0 几乎是所有工作的基础,P1 有了会大为便利,P2 则视团队情况而定。

E.2.1 P0 —— 优先引入

服务器 用途 备注
Filesystem MCP 访问本地文件 基础
Git MCP 变更追踪 必备
团队即时通讯 MCP 团队沟通 推荐
协作工具 MCP(ClickUp、JIRA 等) 任务 公司工具

Filesystem 与 Git 是 LLM 读取资料、追溯变更历史的基础,因此最先接入。团队即时通讯 MCP 引入团队上下文,任务工具则直接把公司已在使用的那一款(无论是 ClickUp 还是 JIRA)连上。

E.2.2 P1 —— 追加引入

服务器 用途
wiki MCP(Confluence、Notion 等) wiki
Google Drive MCP 外部共享资料
Excel MCP 直接查询表格
Mermaid MCP 渲染图表

P0 稳定后,再向文档与数据一侧扩展。尤其 Excel MCP 能让 LLM 直接查询数值表格,在游戏策划中用途很广。Mermaid MCP 能就地渲染设计图示,不打断文档化的流程。

E.2.3 P2 —— 可选

服务器 用途
Discord MCP 用户社区
GitHub MCP 外部协作
Linear MCP 备选任务工具
Notion MCP 备选 wiki

P2 要么是备选,要么专用于特定场景。运营用户社区就接入 Discord,外部协作频繁就接入 GitHub。Linear、Notion 是已引入工具的替代品,无需重复安装。


E.3 面向游戏的专用 MCP(作者自行开发)

商用 MCP 填不满的位置,就自己动手做。下面是作者针对游戏策划工作流自行开发的 MCP。它们都是为了从 LLM 直接查询正文讲过的那些系统(atom、决策卡、会议记录)。

服务器 用途
Atom MCP 检索、查询 atom
Decision Card MCP 查询、生成决策卡
KPI Dashboard MCP 仪表盘数据
Meeting Notes MCP 检索会议记录

这四个处理的是商用工具没有的内部资产(知识 atom、决策历史、会议记录)。自行开发负担较大,建议推迟到 E.4 周期的最后阶段,等到确实清楚哪些是商用 MCP 无法填补的,再着手为好。


E.4 MCP 引入周期

MCP 一次性全部接入,会让问题根源难以辨认。下面的周期,是把"一次一个、稳定之后再下一个"这一原则沿时间轴展开的结果。

flowchart LR A["1周<br/>试点 1 个 Filesystem"] --> B["1个月<br/>追加 Git + 团队即时通讯"] B --> C["3个月<br/>稳定运行 5~7 个"] C --> D["6个月<br/>评估自研 MCP"] classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef human fill:#fde68a,stroke:#b45309,color:#000; class A,B,C code; class D human;

核心规则只有一条:不要一次同时引入 5 个。每接入一个新的 MCP,都请先花几天看它这一个是否稳定运转,再进入下一个。


E.5 MCP 运营成本

服务器 成本
外部 MCP(开源) 仅基础设施
自行托管 基础设施 + 运营
商用 MCP 月度订阅

成本结构分为三种。开源 MCP 只需承担运行的基础设施费用,自行托管在此之上再加运营人力成本,商用 MCP 则要付订阅费。运营 8\~10 个时,月度成本大致估算在 $50\~200 一档,但这会因配置不同而大幅变化,故只作方向参考。


E.6 事故应对

事故 应对
MCP 服务器故障 核心服务器采用 fallback 运行
权限事故(误改数据) 优先 read-only
数据泄露 敏感数据自行托管
成本暴涨 cap + 监控

MCP 把外部工具直接连到 LLM,因此一次错误的写入就可能毁掉真实数据。所以默认设为 read-only,写入权限只对确有必要的服务器开放。核心服务器要为故障预备 fallback,处理敏感数据的 MCP 则用自行托管而非外部来运行。成本方面用上限(cap)与监控一并遏制。


E.7 引入前的自查清单

如果说前面几节讲的是"接入什么、按什么顺序、花多少钱",那么这张表汇总的是在真正接入某一个 MCP 之前,需要自己逐条通过的项目。不必从头重读整份目录,每次新增 MCP 时,只需重新核对这五行即可。这五个项目,各自把前面某一节的核心规则压缩成了一行。

检查项 通过标准 依据小节
属于哪个领域 明确属于项目管理、文档、协作、数据中的哪一个 E.1
是否是当下所需的优先级 遵守 P0 稳定之后才 P1、再 P2 的顺序 E.2
是否逐个接入 不一次同时引入多个 E.4
权限是否最小 默认 read-only,写入只给确有必要的服务器 E.6
是否有成本上限 同时挂上上限(cap)与监控 E.5

五个项目中最常被跳过的一栏,是"是否逐个接入"。因为一次上多个 MCP,一旦出问题就分不清是哪个服务器的错。只有五行全部通过时才接入那个 MCP;哪怕只卡住一行,也把该服务器推迟到下一个周期。

附录 F. 案例索引(公司 / 个人 PC)

将本书中出现的案例按公司环境与个人 PC 环境进行索引。本资料供读者快速查找贴近自身环境的案例。


F.1 公司环境案例(MMORPG 开发商 A、项目A)

F.1.1 系统案例

案例 出现位置
CombatBalance·CombatFormula 运营 8.1
Economy Machinations Pilot 8.2
Damage Simulator(2008\~) 8.3
Procedural Level Design Master 7.1
BehaviorTree 编辑器 7.2
副本·野外模式库 7.3
HUD Layout v3 9.1
Skill UI 6列决策 9.2
NarrativeDocs 5层 5.1
voice_profile + voice_lint 5.2·5.4
proj_city_hunting_generator 6.2
NPC Persona/Squad 6.3

F.1.2 运营案例

案例 出现位置
95_BattleTF 运营 16.1
97_DevGuide 协作 16.2
17.x 会议纪要系统 整个第17部分
Alpha Gap Report 10.3
decision_validation 3层 10.2
304 atom 运营 20.1
团队成员记忆 20.2
门户网站 20.3

F.1.3 组织案例

案例 出现位置
中等规模(10\~50人)团队的愿景·路线图 19.1
Design Director 的授权 19.2
冲突管理·团队文化 19.3
会议运作(领导者视角) 19.4
向上沟通(PD/CEO) 19.5
AI 引入策略 19.6
治理(提示词·幻觉·成本·法务·伦理) 整个第22部分

F.2 个人 PC 环境案例

作者在个人 PC 环境(家中)中亲身经历的案例。

F.2.1 工具借用

案例 出现位置
excel-reader 等6个工具借用 附录 B
JIT atom 注入系统 (个人 PC 基础设施)
个人 PC 斜杠命令(book-capture 等) (个人 PC 基础设施)

F.2.2 本书写作本身

本书的写作过程本身就是 AI 应用案例。

领域 应用
章节正文批量生成 LLM(Claude)
IP 保护(公司 → 匿名化) grep watchlist + 规则
来源追溯 引用公司环境时注明
批量生成 → 审阅 → 整理循环 5月批量生成后进入审阅模式

F.3 案例应用指南

F.3.1 公司环境读者

公司环境案例(中等规模的10\~50人团队、MMORPG、运营)适用于规模·领域相近的公司。

读者环境 适合的案例
移动端 MMORPG 开发商 几乎所有案例
PC MMORPG 第14部分的移动端案例需调整
独立游戏 中等规模(10\~50人)以上的案例需缩减应用
运营(LiveOps)类游戏 第15部分 + 运营案例

F.3.2 个人环境读者

个人环境(1\~2人、或兴趣爱好)可将公司案例简化后借用。

领域 简化
会议系统 单人无需,用个人笔记即可
TF 运营 单人无需
决策卡 仅限重大决策
atom·wikilink 积极使用(单人也有价值)

F.4 引用案例时的 IP 处理

本书所有公司案例均已匿名化。

原始 匿名化
公司名称 MMORPG 开发商 A
项目 项目A
团队成员实名 团队成员 A·B·C
游戏内的专有名词 虚构(王国 X、角色 K_001 等)
数值 虚构(比例为真实)
公司工具名 proj_*(例:proj_city_hunting_generator)

F.5 非游戏职务索引 ——「游戏之外的应用」专栏查找

面向在游戏行业之外工作的读者(策划·PM·普通职场人士)的反向索引。正文各章末尾的「游戏之外的应用」专栏,是把该章的工作流搬到与游戏无关的职务上阅读的桥梁。若觉得游戏领域的正文有负担,也可以先翻开下面的专栏,从自身职务案例入手。「通用职务之路」(1·2部 → 17 → 16 → 18 → 21·22部)与90分钟超短速成课程(17.1 → 16.2 → 22.1 → 21.1)的锚点,正是这份索引。

F.5.1 流程·协作(游戏之外的迁移最为直接)

「游戏之外的应用」为你迁移的工作
16.1 把蜂拥而来的工作隔离到临时工作空间,只把结果吸收为正本
16.2 把一句话请求分类到共识·缺陷·排期三条轨道
16.3 用适合其他职能·利益相关者的媒介来呈现产出物
17.1 让会议纪要按决策的4个字段(什么·谁·为何·下一步)流转
17.2 从会议纪要中提取决策·行动项的流水线
17.3 会议决策的分类·同步
17.4 会议摘要·后续追踪的自动化
18.1 为决策录入永久地址·负责人·依据,并先查找过往决策
18.2 对一个决策的影响波及范围进行分类
18.3 变更前后的追踪工作流
18.4 用检索确认文档变更的影响范围

F.5.2 自我改进·治理

「游戏之外的应用」为你迁移的工作
21.1 把复盘作为自我改进的起点
21.2 把复盘中反复出现的模式升格为规则
21.3 闭合改进循环
22.1 在一份工作指令书(提示词)里放入上下文·格式·防幻觉·验证
22.2 幻觉·安全性的多层防御
22.3 诚实地管理 AI 成本
22.4 版权·伦理检查

F.5.3 领导力

「游戏之外的应用」为你迁移的工作
19.1 提出愿景与授权
19.2 冲突管理与会议领导力
19.3 组织的 AI 引入策略

上述索引只收录了正文中实际存在的「游戏之外的应用」专栏(截至 2026-06 共22处)。没有专栏的章节对游戏领域的依赖度较高,难以原样迁移;与其勉强搬移,不如先从「通用职务之路」的上述章节入手。

附录 G. 运营脚本案例集

本附录是把正文中提到的运营自动化脚本汇集到一处的案例集。正文在行文中说明了每个脚本"为什么需要",但真要动手做类似的工具时,还需要一张能一眼看清"哪些脚本以什么角色归为一组"的地图。本附录就是这张地图。

这里一并写出了脚本名称、一句话说明,以及正文在哪一节讨论过它。对于能够干净利落地一般化的核心脚本(G.1.1 格式检查·G.2.1 一致性检查·G.3.1 关系图·G.7.1 成本追踪器),以及 G.8 的测试·hook 示例,我们用与公司资料无关的通用骨架重新编写,并验证其可直接运行,收录的是实测代码。输入示例、输出乃至退出码,都是实际运行确认过的值。其余条目只写了名称、角色和关联的正文小节,其中缘由会在附录 G.9 中如实说明。读者可以把实测代码条目当作范本,自行做出适合自己环境的实现。

用法如下。先确定想要自动化的工作性质(是验证,是生成报告,还是同步),然后翻到对应的小节(G.1\~G.7)。在那里选出最接近的脚本,再前往括号中的正文小节编号,确认其背景与设计意图。最后对照 G.8 的运营原则,检查自己的脚本是否遵守了这些原则。

按角色把全部脚本归组,如下所示。

flowchart TD G1["G.1 会议纪要·决策自动化"] --> META["元运营<br/>(知识沉淀)"] G2["G.2 验证·lint"] --> QA["质量门禁"] G3["G.3 影响追踪"] --> QA G4["G.4 报告自动生成"] --> REPORT["报告·可视化"] G5["G.5 同步"] --> META G6["G.6 LLM 集成"] --> AI["AI 辅助"] G7["G.7 成本·运营"] --> AI classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class G1,G2,G3,G4,G5,G6,G7 code; class AI ai; class META,REPORT data;

G.1 会议纪要·决策自动化

让会议中产生的决策不致散失、而是沉淀为知识资产的一组脚本。从会议纪要验证到 atom 提取,再到正式升格,一脉相连。

G.1.1 meeting_lint.py

检查会议纪要是否具备既定格式(必需的头部·必需的章节)的脚本。格式散乱的会议纪要会导致后续自动提取失效,因此在入口处拦截(17.2.2)。

下面是与公司资料无关的通用骨架。只用标准库(仅 sys),可直接运行。它检查 Markdown 会议纪要的头部(由 --- 包裹的块)键与正文章节标题(## ...)是否齐全。若有缺失就报出 violation 并 exit 1,全部齐全则 exit 0。

#!/usr/bin/env python3
"""meeting_lint.py

检查 Markdown 会议纪要是否具备既定格式。
- 头部(--- 块)中是否包含全部必需键。
- 正文中是否包含全部必需的章节标题(## ...)。
若有缺失项就输出 violation 并 exit 1,没有则 exit 0。
只使用标准库。

用法:
    python meeting_lint.py meeting.md
"""
import sys

REQUIRED_FRONTMATTER = ["type", "date", "category", "attendees"]
REQUIRED_SECTIONS = ["## 议题", "## 决策", "## 行动项", "## 下次会议"]


def lint(text):
    """接收会议纪要正文字符串,返回缺失项列表(violation)。"""
    violations = []

    # 头部:若首行是 ---,则把到下一个 --- 之间视为头部。
    lines = text.splitlines()
    front = []
    if lines and lines[0].strip() == "---":
        for line in lines[1:]:
            if line.strip() == "---":
                break
            front.append(line)
    front_keys = [ln.split(":", 1)[0].strip() for ln in front if ":" in ln]
    for key in REQUIRED_FRONTMATTER:
        if key not in front_keys:
            violations.append({"kind": "frontmatter", "missing": key})

    # 章节:正文中是否原样包含相应的标题行。
    body_lines = [ln.strip() for ln in lines]
    for section in REQUIRED_SECTIONS:
        if section not in body_lines:
            violations.append({"kind": "section", "missing": section})

    return violations


def main(argv=None):
    argv = sys.argv[1:] if argv is None else argv
    if len(argv) != 1:
        sys.stderr.write("用法: python meeting_lint.py meeting.md\n")
        return 2
    with open(argv[0], encoding="utf-8") as f:
        violations = lint(f.read())

    for v in violations:
        print(f"[VIOLATION] {v['kind']}: {v['missing']}")
    if violations:
        sys.stderr.write(f"[FAIL] 格式违规 {len(violations)}处\n")
        return 1
    sys.stderr.write("[PASS] 格式合格\n")
    return 0


if __name__ == "__main__":
    sys.exit(main())

两个常量就是检查基准。例如,把一份头部缺少 attendees、正文没有 ## 下次会议 的会议纪要传入,就会像下面这样报出两处,退出码为 1。

[VIOLATION] frontmatter: attendees
[VIOLATION] section: ## 下次会议

G.1.2 decision_parser.py

读取会议纪要的"决策"章节,自动抽取知识 atom 候选的脚本。替代过去由人逐条誊抄的工作(17.2.3)。

G.1.3 promote.py

把待审(pending)状态的 atom 升格到正式 atom 文件夹的脚本。在自动提取与正式资产之间设一道人工评审关卡(17.2.6)。


G.2 验证·lint

自动查出数据与内容是否违反规则的质量门禁。让机器先过滤掉人眼容易遗漏的一致性错误。

G.2.1 integrity_check_id_uniqueness.py

验证数据项的 ID 是否唯一、无重复的脚本。ID 冲突是要到运行时才爆发的事故,因此在数据阶段就加以拦截(10.1.2)。

下面是与公司资料无关的通用骨架。只用标准库(csv·json·sys·argparse),原样保存即可直接运行。输入采用任何游戏数据都可能具备的简单格式,即带 id 列的 CSV。

#!/usr/bin/env python3
"""integrity_check_id_uniqueness.py

检查 CSV 数据的 id 列是否唯一。
- 若存在重复 id,就输出 violation 列表并 exit 1。
- 全部唯一则 exit 0。
只使用标准库。

用法:
    python integrity_check_id_uniqueness.py data.csv
    python integrity_check_id_uniqueness.py data.csv --id-column quest_id
"""
import argparse
import csv
import json
import sys


def find_duplicate_ids(rows, id_column):
    """在 rows(字典列表)中查找 id_column 值的重复项。

    返回:violation 列表。每一项形如
    {"id": 值, "row_numbers": [从 1 开始的行号, ...]}。
    把表头算作第 1 行,数据首行从 2 开始计数。
    """
    seen = {}  # id 值 -> 出现过的行号列表
    for index, row in enumerate(rows):
        row_number = index + 2  # 从表头(第 1 行)之后开始
        key = row.get(id_column, "")
        seen.setdefault(key, []).append(row_number)

    violations = []
    for key, row_numbers in seen.items():
        if len(row_numbers) > 1:
            violations.append({"id": key, "row_numbers": row_numbers})
    violations.sort(key=lambda v: v["row_numbers"][0])
    return violations


def load_rows(csv_path):
    with open(csv_path, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))


def main(argv=None):
    parser = argparse.ArgumentParser(description="CSV id 唯一性检查")
    parser.add_argument("csv_path", help="要检查的 CSV 文件路径")
    parser.add_argument("--id-column", default="id", help="用作 id 的列名(默认: id)")
    args = parser.parse_args(argv)

    rows = load_rows(args.csv_path)
    violations = find_duplicate_ids(rows, args.id_column)

    # G.8 输出标准:把 violation_list 以 JSON 形式输出到标准输出。
    print(json.dumps({"violation_list": violations}, ensure_ascii=False, indent=2))

    if violations:
        sys.stderr.write(f"[FAIL] 发现重复 id {len(violations)}处\n")
        return 1
    sys.stderr.write("[PASS] 无重复 id\n")
    return 0


if __name__ == "__main__":
    sys.exit(main())

输入示例(data.csv):

id,name
Q001,初次委托
Q002,遗失的佩饰
Q001,初次委托(重复)

运行结果如下。Q001 在第 2 行和第 4 行出现了两次,因此报出一处 violation,退出码为 1。

{
  "violation_list": [
    {
      "id": "Q001",
      "row_numbers": [2, 4]
    }
  ]
}

G.2.2 voice_lint.py

检查 NPC 台词的"声音"(voice,即语气·性格)一致性的脚本。抓出同一角色在各章使用不同语气的偏差(5.2·5.4)。

G.2.3 visual_regression.py

在资源(美术·UI 等)发生变化时,比对是否产生了非预期视觉变化的回归检查脚本(12.1.5)。


G.3 影响追踪

追踪"改动一处会牵动什么"的一组脚本。沿着文档·决策·资源之间的连接,展示变更的波及范围。

G.3.1 wikilink_graph.py

抓取文档间的 Wikilink([[目标]])、自动构建连接图的脚本。让人一眼看清哪个文档引用了哪个文档(24.3.4)。

下面是与公司资料无关的通用骨架。只用标准库(os·re·json·argparse)。它读取一个文件夹中的 .md 文件,把文件名(去掉扩展名)当作节点,把 [[...]] 链接当作边。结果会一并输出邻接表与 Mermaid 图示代码。

#!/usr/bin/env python3
"""wikilink_graph.py

把文件夹中 .md 文档的 [[Wikilink]] 连接构建为图。
- 节点:去掉扩展名的文件名。
- 边:文档正文中的 [[目标]] 标记。若为 [[目标|显示]] 形式,只取目标。
只使用标准库。

用法:
    python wikilink_graph.py ./docs
    python wikilink_graph.py ./docs --format mermaid
"""
import argparse
import json
import os
import re
import sys

WIKILINK = re.compile(r"\[\[([^\]|#]+)")  # [[目标]] / [[目标|显示]] / [[目标#锚点]]


def extract_links(text):
    """从正文中按出现顺序、去重地提取链接目标名称。"""
    result = []
    for match in WIKILINK.findall(text):
        target = match.strip()
        if target and target not in result:
            result.append(target)
    return result


def build_graph(doc_dir):
    """遍历文件夹中的 .md,构建 {文档名: [链接目标, ...]} 邻接表。"""
    graph = {}
    for name in sorted(os.listdir(doc_dir)):
        if not name.endswith(".md"):
            continue
        node = name[:-3]
        path = os.path.join(doc_dir, name)
        with open(path, encoding="utf-8") as f:
            graph[node] = extract_links(f.read())
    return graph


def to_mermaid(graph):
    """把邻接表转换为 Mermaid flowchart 代码字符串。"""
    lines = ["flowchart LR"]
    for node, targets in graph.items():
        if not targets:
            lines.append(f'    {_id(node)}["{node}"]')
        for target in targets:
            lines.append(f'    {_id(node)}["{node}"] --> {_id(target)}["{target}"]')
    return "\n".join(lines)


_ID_CACHE = {}


def _id(name):
    """Mermaid 节点 id 必须是 ASCII。非 ASCII 名称(如中文)会按首次出现的
    顺序赋予 n1、n2、…… 这样的短 ASCII id,并在标签[...]中保留原名。"""
    if name not in _ID_CACHE:
        _ID_CACHE[name] = "n%d" % (len(_ID_CACHE) + 1)
    return _ID_CACHE[name]


def main(argv=None):
    parser = argparse.ArgumentParser(description="Wikilink 连接图构建器")
    parser.add_argument("doc_dir", help="存放文档(.md)的文件夹")
    parser.add_argument("--format", choices=["json", "mermaid"], default="json")
    args = parser.parse_args(argv)

    graph = build_graph(args.doc_dir)
    if args.format == "mermaid":
        print(to_mermaid(graph))
    else:
        print(json.dumps(graph, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    sys.exit(main())

输入示例(文件夹 docs/ 中的三个文件):

docs/世界观.md      正文中有 [[地区_汉阳]] 和 [[势力_义禁府]] 链接
docs/地区_汉阳.md   正文中有 [[势力_义禁府]] 链接
docs/势力_义禁府.md  无链接

--format mermaid 运行,会得到下面的图示代码。节点按文件名顺序(世界观 → 势力_义禁府 → 地区_汉阳)处理,标签中原样保留原本的名称。哪个文档伸向何处、终点(势力_义禁府)是什么,都一目了然。

flowchart LR
    n1["世界观"] --> n2["地区_汉阳"]
    n1["世界观"] --> n3["势力_义禁府"]
    n3["势力_义禁府"]
    n2["地区_汉阳"] --> n3["势力_义禁府"]

G.3.2 decision_impact.sh

分析某个决策卡会影响哪些文档·资源的脚本。在推翻决策之前,先确认其波及范围(18.4.3)。

G.3.3 find_skills_using.py

反向找出使用某个资源的技能的脚本。在修改·删除资源之前,先弄清依赖它的地方(11.2.4)。


G.4 报告自动生成

把散落的数据汇整为人可阅读的报告·图示的脚本。将反复的定期汇报自动化,减少费手的工作。

G.4.1 alpha_gap_report_generator.py

汇总 Alpha 阶段相对目标的缺口(gap),自动生成周报的脚本(10.3.3)。

G.4.2 decision_graph_to_mermaid.py

把决策卡之间的连接关系转换为 Mermaid 图示代码的脚本。用图来看决策流(24.2.3)。

G.4.3 weekly_kpi_summary.py

按周汇总主要指标(KPI)的脚本(13.2)。


G.5 同步

高效地对齐分散在多处的资料的脚本。不必每次复制全部,只挑出变更的部分进行同步。

G.5.1 incremental_sync.py

只挑出会议纪要的变更部分、而非全部进行同步的脚本。资料越积越多,全量复制就越慢,因此采用增量方式(17.5.4)。

G.5.2 基于 git diff 的变更检测

利用 git 的 diff 高效检测发生了哪些变更的做法。无需额外的追踪装置,直接把 git 本身当作变更检测器(17.5.4.1)。


G.6 LLM 集成

把分类·调用这类需要判断的工作交给 LLM 的脚本。用 LLM 辅助来处理规则无法干净拆解的事情。

G.6.1 faq_classifier.py

把收到的 FAQ 按类别自动分类的脚本(13.1.3)。

G.6.2 meeting_classifier.py

把会议按性质类别自动分类的脚本。用于填充会议纪要头部的 category(17.3.6)。

G.6.3 prompt_library_loader.py

从预先整理好的提示词库中载入所需提示词的脚本。避免每次重复编写相同的提示词(22.1.2)。


G.7 成本·运营

管理自动化本身,使其不致制造成本与资料追踪盲区的脚本。

G.7.1 llm_cost_tracker.py

追踪 LLM 调用成本并施加上限(cap)的脚本。在事前而非事后阻止成本暴涨(22.3.5)。

下面是与公司资料无关的通用骨架。只用标准库(json·os·argparse)。它记录每次调用的 token 数并计算累计成本,超过上限就发出拒绝信号(exit 2)。单价是代码中的常量,实际值可替换为各自所用模型的单价表(下面的值是用于说明的占位值)。

#!/usr/bin/env python3
"""llm_cost_tracker.py

累计记录 LLM 调用 token 并检查每日成本上限。
- record:把单次调用(输入/输出 token)累加到 ledger 文件。
- 累计成本超过 cap 时以 exit 2 阻止调用(事前拦截)。
只使用标准库。

用法:
    python llm_cost_tracker.py --ledger ledger.json --in 1200 --out 800
    python llm_cost_tracker.py --ledger ledger.json --in 1200 --out 800 --cap-usd 5.0
"""
import argparse
import json
import os
import sys

# 单价:每 1,000 token 的 USD。用于说明的占位值——请替换为实际模型的单价表。
PRICE_PER_1K_INPUT = 0.003
PRICE_PER_1K_OUTPUT = 0.015


def cost_of(in_tokens, out_tokens):
    """用输入/输出 token 计算单次调用的成本(USD)。"""
    return (in_tokens / 1000) * PRICE_PER_1K_INPUT + (out_tokens / 1000) * PRICE_PER_1K_OUTPUT


def load_ledger(path):
    if os.path.exists(path):
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    return {"calls": 0, "in_tokens": 0, "out_tokens": 0, "total_usd": 0.0}


def save_ledger(path, ledger):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(ledger, f, ensure_ascii=False, indent=2)


def main(argv=None):
    parser = argparse.ArgumentParser(description="LLM 成本追踪·上限")
    parser.add_argument("--ledger", required=True, help="累计记录 JSON 文件路径")
    parser.add_argument("--in", dest="in_tokens", type=int, required=True, help="本次调用的输入 token")
    parser.add_argument("--out", dest="out_tokens", type=int, required=True, help="本次调用的输出 token")
    parser.add_argument("--cap-usd", type=float, default=None, help="累计成本上限(USD)。超过则拦截")
    args = parser.parse_args(argv)

    ledger = load_ledger(args.ledger)
    this_cost = cost_of(args.in_tokens, args.out_tokens)

    ledger["calls"] += 1
    ledger["in_tokens"] += args.in_tokens
    ledger["out_tokens"] += args.out_tokens
    ledger["total_usd"] = round(ledger["total_usd"] + this_cost, 6)
    save_ledger(args.ledger, ledger)

    print(json.dumps({"this_call_usd": round(this_cost, 6), "ledger": ledger}, ensure_ascii=False, indent=2))

    if args.cap_usd is not None and ledger["total_usd"] > args.cap_usd:
        sys.stderr.write(f"[CAP] 累计 {ledger['total_usd']} USD > 上限 {args.cap_usd} USD —— 拦截\n")
        return 2
    return 0


if __name__ == "__main__":
    sys.exit(main())

输入示例与结果。在空状态下记录输入 1,200·输出 800 token,本次调用成本为 1200/1000*0.003 + 800/1000*0.015 = 0.0036 + 0.012 = 0.0156 USD。

{
  "this_call_usd": 0.0156,
  "ledger": {
    "calls": 1,
    "in_tokens": 1200,
    "out_tokens": 800,
    "total_usd": 0.0156
  }
}

若一并给出 --cap-usd 0.01,累计 0.0156 超过上限 0.01,于是以退出码 2 阻止下一次调用。这就是"在事前而非事后阻止"的实际行为。

G.7.2 source_tracker.py

自动记录引用·参考资料出处的脚本。留存下来,以便日后回溯出处(24.5.4)。


G.8 脚本运营原则

比起多造脚本,让造出的脚本可靠地运转更重要。下面五条原则通用于上述所有脚本。

原则 说明
简单 回避复杂的库
测试 所有脚本单元测试
输出标准 violation_list 等标准(10.1.7)
版本管理 git
人工评审关卡 自动化也须人工评审

尤其最后一条原则很重要。自动化不是替代人,而是缩减人做判断之前的环节。无论是验证、提取还是生成,在最终应用之前都务必设一道由人过目一次的关卡。

G.8.1 单元测试示例

"测试"原则不只停留在口头,这里给出用标准库 unittest 验证 G.2.1 核心函数 find_duplicate_ids 的实际测试。没有外部依赖,原样保存即可用 python -m unittest test_integrity_check -v 运行。关键在于:待验证的函数要与文件输入输出分离,才能这样轻松地测试(所以 G.2.1 中把检查逻辑与 load_rows 分开了)。

# test_integrity_check.py
import unittest

from integrity_check_id_uniqueness import find_duplicate_ids


class TestFindDuplicateIds(unittest.TestCase):
    def test_no_duplicates_returns_empty(self):
        rows = [{"id": "Q001"}, {"id": "Q002"}]
        self.assertEqual(find_duplicate_ids(rows, "id"), [])

    def test_one_duplicate_reports_row_numbers(self):
        rows = [{"id": "Q001"}, {"id": "Q002"}, {"id": "Q001"}]
        self.assertEqual(
            find_duplicate_ids(rows, "id"),
            [{"id": "Q001", "row_numbers": [2, 4]}],
        )

    def test_missing_column_treated_as_empty_string(self):
        rows = [{"name": "a"}, {"name": "b"}]
        result = find_duplicate_ids(rows, "id")
        self.assertEqual(result, [{"id": "", "row_numbers": [2, 3]}])


if __name__ == "__main__":
    unittest.main()

运行后,三个测试全部通过。

test_missing_column_treated_as_empty_string ... ok
test_no_duplicates_returns_empty ... ok
test_one_duplicate_reports_row_numbers ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

G.8.2 hook 的静默失败(exit 0)

上述原则中容易被漏掉的是 hook 的失败处理。在提交前或保存时自动运行的 hook,本应是主作业(提交·保存)的旁支。可一旦 hook 因内部错误返回非 0 退出码,绑定该 hook 的整个主作业也会被彻底卡住。这等于辅助装置把主体挟持为人质。因此,辅助性质的 hook 无论内部发生什么,都只把警告写入标准错误(stderr),并返回退出码 0,从而不阻塞主作业。下面就是它的最小形态,即便内部抛出异常,退出码也是 0。

import sys

def run_hook():
    raise RuntimeError("发生内部错误")

def main():
    try:
        run_hook()
    except Exception as exc:
        sys.stderr.write(f"[hook] 警告: {exc} —— 不阻塞主作业\n")
    return 0  # 辅助 hook 无论如何都不阻塞主作业

if __name__ == "__main__":
    sys.exit(main())

运行后,警告会显示,但退出码为 0。也就是说,人能知道哪里出了岔子,而作业流程不会中断。

[hook] 警告: 发生内部错误 —— 不阻塞主作业
(退出码 0)

不过,这种"静默失败"只用于辅助 hook。像 G.2 的质量门禁那样、以是否通过本身为目的的验证,反过来必须在失败时返回非 0 码(前面见过的 exit 1),让流水线停下。要区分:同样是 hook 的位置,视其为"辅助"还是"门禁",退出码策略正好相反。

G.8.3 如何察觉并复原静默失败

上一节的 exit 0 策略有一个代价。辅助 hook 无论如何都不阻塞主作业,反过来说就意味着 hook 悄然死掉,主作业照样正常运转。像上下文自动注入这类在旁支上运行的 hook,即便好几天不运行,作业流程也不会亮起红灯。因此,辅助 hook 除了"失败也不拦截",还必须配上"让人哪怕迟一些也能看到失败"这个搭档装置。缺了这个搭档,你会在某天复盘时发现"这个 atom 最近一次都没弹出过",这才意识到 hook 已经死了一个星期。

这个搭档就是日志。别让上一节最小形态(sys.stderr.write(...))留下的警告白白挥发,而要把它落到文件里:正常调用留一行,失败调用连同缘由留一行。在作者的环境中,这些痕迹积累在 ~/.claude/hooks/_injection_log.txt(同一份日志在 §21.3.4 的触发验证中也会被读取)。运营循环并不宏大。走一遍三个步骤的检查·恢复流程就够了。

阶段 看什么 做什么
检测 日志中最近的正常注入行是否中断,或同一缘由的失败行是否反复出现 在周复盘中扫一眼日志尾部(自动捕获一行即可)
隔离 失败缘由是 hook 自身的 bug,还是输入数据(损坏的 manifest·缺失的 atom 文件) 用 stderr 缘由字符串把两者区分开——代码问题就查代码,数据问题就查 manifest
恢复 触发后能否再次出现正常注入 修好后在新会话中输入一次预期的触发词,确认日志里是否重新留下正常行(与 §21.3.4 的触发验证相同)

关键在于:把"检测"交给的不是人的注意力,而是 一份日志文件和一行复盘。exit 0 挡住的是主作业的中断,而非对失败的掩盖。失败经由 stderr→日志暴露出来,复盘定期查看这份日志,恢复则原样复用平时使用的触发验证。唯有当"不拦截 + 暴露 + 定期查看 + 以同样方式复原"成为一个整体时,静默失败才不会固化为静默放任。


G.9 读者参考

本案例集的代码有两类。一类是像 G.1.1·G.2.1·G.3.1·G.7.1·G.8 这样,用与公司资料无关的通用骨架重新编写、并验证可直接运行的代码。它们只用标准库,上面所写的输入示例·输出·退出码,都是实际运行确认过的结果。可以直接复制粘贴使用,只需把单价表或列名之类的占位值换成适合自己环境的即可。

另一类是像其余各节那样,只写了名称·角色·关联正文小节的条目。没有把这一类以完整代码收录,坦白说有两个理由。第一,公司运营脚本的原件属于公司 IP,无法原样搬来。第二,其逻辑相当一部分绑定在公司特有的数据模式·文件夹结构·决策卡格式上,一旦抽走这些前提,就不会剩下对一般读者直接有用的代码。因此,只把能够干净利落地一般化的四个(格式检查·一致性检查·关系图·成本追踪器)升格为实测代码,其余留作骨架。读者可以把这四个当作范本,以同样的方式——分离检查逻辑与输入输出、以标准输出报出 violation 列表、附上单元测试——自行做出适合自己环境的实现。

把既有工具拿来做变奏的步骤,参见附录 B。

附录 H. 过往工作资料的复用

从业多年的策划会积累下数十年的工作资料。会议记录、决策记录、复盘、学习笔记,乃至从失败中得到的教训。本附录讨论如何把这些资料重新用于新项目。核心张力只有一个:这些资料的很大一部分属于公司 IP,不能随意搬走,但其中同时又夹杂着在任何地方都通用的个人学习。区分这两者,正是复用的起点。

如何使用本附录,取决于你所处的位置。如果你正想把旧资料引入新项目,请依次阅读 H.2(分离原则)和 H.3(流程)。如果你担心搬运过程中会出事故,请先阅读 H.5(五个陷阱),提前规避。如果你还处于职业生涯早期、可积累的资料不多,请参考 H.6,从现在起决定要留下什么、如何留下。

这里讲的原则并非什么宏大的资产管理理论。它可以压缩成一句话:"把具体的东西留在公司,只带走抽象的模式。"其余内容都是把这句话应用到实际情形中的方法。


H.1 过往资料的价值

首先来看会积累哪些资料,以及各自的留存权限有何不同。因为留存权限不同,可复用的范围也会随之不同。

资料 留存
会议记录(公司资料) 公司权限内
决策卡(公司资料) 公司权限内
季度复盘(个人+公司) 可保留个人副本
学习笔记(个人) 个人永久
事故记录(个人学习) 个人永久

会议记录和决策卡留在公司权限之内。复盘可以保留个人副本,而学习笔记和事故记录则完全属于个人资产。长期积累的资料本身就是巨大的学习资产,但绝不能模糊公司 IP 领域与个人领域的边界。边界越清晰,就越能安心地复用。


H.2 公司 IP 与个人学习的分离

分离的标准是"具体还是抽象"。具体的产出属于公司,而产生它的思考模式属于个人。同一项工作会同时带出这两个方面——这一点是关键。

领域 公司 IP 个人学习
决策内容 公司
决策模式(某种情形下适合某种决策) 个人
游戏数据 公司
运营经验(规则手册·工具运营) 个人
代码 公司
算法·结构 个人

"做了什么决策"属于公司 IP,而"在这种情形下这种决策更奏效"这样的模式则是个人学习。游戏数据的数值本身属于公司,但运营这些数据的诀窍属于个人。把具体资料留在公司,只带走抽象模式——这就是分离的原则。


H.3 复用流程

把分离原则落实到实际工作中,就成了以下五个步骤。识别资料、剥离 IP、提取学习、加以泛化,然后应用到新项目。

flowchart TD A["过往资料识别"] --> B["剥离公司 IP 部分"] B --> C["提取个人学习部分"] C --> D["抽象化·泛化"] D --> E["应用到新项目"] classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d; class E pass;

这一流程务必在经过公司权限确认与法务审核之后再推进。即便抽象化已经足够,只要起点是公司资料,按流程留一份确认会更安全。


H.4 复用案例——本书

最切近的复用案例就是本书本身。正文各处都源自作者过往的工作,是经过上述流程加以泛化、匿名化后的结果。

领域 出处 复用
Layer 整合设计(第 6 部分) 作者多年运营 个人学习 → 泛化
会议记录系统(第 17 部分) 作者的项目A运营 公司模式 → 匿名化
运营经验(第 24 部分) 多年积累 个人学习 → 泛化
附录 A 清单 公司项目A 匿名化 + 部分加工

Layer 设计与运营经验是把个人学习加以泛化,会议记录系统与附录 A 则是把公司模式匿名化。所有条目都通过了公司的许可,公司 IP 全部无一遗漏地做了匿名化。本书这一产出本身,可以说就是 H.3 流程的实证。


H.5 复用的 5 个陷阱

复用做得好是资产,做不好就是事故。下面这五个陷阱都是实际中经常踩的点,针对每一个都给出了对策。

H.5.1 陷阱 1 —— 未通过公司许可

未经公司许可就使用资料,会演变成纠纷。对策很简单:使用之前先取得公司许可。

H.5.2 陷阱 2 —— 匿名化遗漏

只要公司名或真实姓名残留在任何一处,就会酿成 IP 事故。对策是自动 grep 检查。把公司名·真实姓名·路径做成 watchlist,让机器无一遗漏地扫一遍。

H.5.3 陷阱 3 —— 原样套用过往

把很久以前的经验原封不动地拿来用,就会与当下的时点脱节。对策是顺应时代重新构建。保留原理,但把工具与语境更新到当前。

H.5.4 陷阱 4 —— 抽象化不足

只搬运具体案例,就很难套用到其他环境。对策是把抽象模式与具体示例放在一起。用模式获得普适性,用示例获得理解。

H.5.5 陷阱 5 —— 根本不做学习

资料再多,若不再翻看,就等于没有。对策是定期的学习周期。像日·周·月复盘那样,建立一个重新接触资料的周期。


H.6 读者参考——复用自己的资料

这一原则并非作者专属。读者也可以用同样的方式复用自己职业生涯的资料。下面是从现在起就能开始的推荐习惯。

推荐做法 理由
每季度复盘自己的决策 发现模式
单独保存学习笔记 与公司 IP 分离
明确写出抽象模式 便于未来复用
指导他人·对外演讲 分享模式
出书·博客(取得公司许可后) 让学习永存

每季度复盘自己的决策,就能看出模式;把学习笔记与公司资料分开保存,日后就能安心地取用。把这些模式通过指导他人、演讲、写作输出出去,学习就不会用过一次即消失,而会长久留存。归根结底,自己的学习就是自己的资产。

附录 I. BehaviorTree 编辑器案例(进阶)

7.2 中讨论的 BehaviorTree 编辑器的进阶案例。自主开发的决策、实现与运营经验。


I.1 自主开发的决策

7.2.8 中讨论的四项决策依据的详情。

依据 详情
必须支持 diff·git 追踪 UE BT 是 .uasset binary,变更难以追踪。改用 JSON 后可进行文本 diff
subtree 引用 + 影响追踪 运营 100\~300 个 BT 时,以 subtree 为单位的影响分析至关重要
仿真验证 无需构建即可单独运行 BT
AI 辅助编写 LLM 能自然地生成、解析 JSON BT

I.2 实现阶段

[1. 策划·需求定义 (1~2周)]
   - 明确四项需求
   - 设计 JSON 模式

[2. 运行时实现 (3~4周)]
   - JSON 解析器
   - BT 执行引擎
   - 解析 subtree 引用

[3. 编辑器实现 (4~6周)]
   - JSON 编辑器 (图形化)
   - subtree 库 UI
   - 影响分析工具

[4. 仿真器 (2~3周)]
   - 单独运行 BT
   - 提取统计数据

[5. AI 集成 (2~3周)]
   - LLM 辅助编写 BT
   - 上下文注入

[6. UE 集成 (2~4周)]
   - 与 UE BT 相互转换
   - 构建集成

总计约 4\~6 个月。开发者 1\~2 人。


I.3 运营设计与事故防范

I.3.1 关于运营数据

这款编辑器是处于 R&D 阶段的内部工具,并未达到可称为"一年运营实测"那样的长期、大规模运行。因此,遵循本书的原则,这里不刊载任何编造的运营统计数据。I.1 中提到的 100\~300 个的规模,是为自主开发提供正当理由的设计目标,而非实测结果。

在设计所设定的限制中,真正写入代码的是 subtree 引用 depth 5 上限(防止无限递归)。运营中的 BT 数量、仿真运行次数之类的数值会因项目规模而异,因此与其写下编造的数字,不如在你自己的环境中亲自测量。

I.3.2 运营中防范的事故与经验教训

事故 经验教训
subtree 无限引用(递归) 引用 depth 5 上限
仿真与实际行为的差异 每月校准仿真环境
LLM 输出 BT 的幻觉 加强验证 + 设计师评审
BT 数量激增(计划外) 季度整理周期

I.4 成本·ROI

开发与运营成本是基于制作这款工具时所定日程的估算,而"效果"一侧并非实测值,而是引入该工具所要达成的方向。这里不写编造的节省数字,只写方向。

项目 性质
开发成本 开发者 4\~6 个月 计划日程(估算)
运营成本 开发者每季度 1\~2 周(维护) 计划日程(估算)
预期效果——运营人力 大规模运营敌方 NPC 时,压缩 BT 负责人员 方向(未测量)
预期效果——事故 从结构上减少 subtree 递归、LLM 幻觉这类 BT 事故 方向(未测量)

引入成本的回收周期会因项目的 NPC 规模与人力成本而异,因此建议在你自己的环境中测量上述各项后再作判断。笔者不会做出"一年内即可回收"之类的断言——那个数字我们并不掌握。


I.5 LLM 辅助案例

I.5.1 编写新的敌方 NPC BT

使用 7.2.6 的提示词。以下是展示输出结构的示例(并非实际运营数据,而是格式示例)。

{
  "bt_id": "bt_new_mage_v1",
  "category": "ranged_combatant",
  "tags": ["scholar_faction", "ranged", "magic"],
  "root": {
    "type": "selector",
    "children": [
      {
        "type": "sequence",
        "name": "low_hp_retreat",
        "children": [
          {"type": "condition", "fn": "hp_below", "param": 0.3},
          {"type": "subtree_ref", "id": "subtree_retreat_to_ally"}
        ]
      },
      {
        "type": "sequence",
        "name": "magic_attack",
        "children": [
          {"type": "condition", "fn": "enemy_in_range", "param": 15},
          {"type": "subtree_ref", "id": "subtree_magic_attack_pattern"}
        ]
      }
    ]
  }
}

设计师评审后进行仿真 → 通过 → 应用到构建。


I.6 读者参考

BehaviorTree 的自主开发通常在运营规模达到 100 个以上时才具有正当性(设计判断)。低于此规模时,使用 UE 自带的 BT 即可。

备选方案: - BehaviorTree.CPP(开源、标准) - Behavior Designer(外部商用) - 自主开发(自由度最高,运营负担大)

选择标准参见 7.2.8。

附录 J. 缩略语·术语集

本附录将正文中出现的缩略语与本书特有术语汇集于一处。正文会在每个缩略语首次出现处展开说明一次,但若你没有按顺序阅读、或中途遗忘,可在此直接查找。若同一缩略语在不同语境下含义不同,则两种含义均予列出。

本术语集按以下顺序分组:团队规模等级 → 游戏策划文档 → 游戏领域 → 数据·运营 → AI·工具 → UI·无障碍标准 → 文件·格式。先想清楚所查缩略语的性质,便能缩小它所在分组的范围。例如 DPS·TTK 属于"游戏领域",KPI·DAU 属于"数据·运营",atom·JIT 属于"AI·工具"分组。

标注规则有三条:① 一般缩略语同时列出正式名称与中文释义。② 像 atom·Wrapper 这类仅本书使用的特有术语,在正式名称一栏标注了"(本书特有术语)"。③ 像 PK(战争语境的 Player Kill ↔ 数据语境的 Primary Key)这样一个缩略语具有两种含义的情况,正文在首次出现时会一并说明是哪一种,而本表中则两种含义均予列出。

团队规模等级

本书不将团队人数固定为某个具体数字,而是用以下三个等级来表示。因为即便是同一种方法,导入的深度也会随团队规模而不同。

等级 人数标准 说明
小规模 \~10人 从单人·业余开发者到个位数规模的团队。通常导入 1\~2 个阶段即已足够
中规模 10\~50人 本书运营案例所出自的作者团队所处的区间。标准化·一致性自动化的累积效果开始变得明显的规模
大规模 100+ 多个部门·多个团队。专用基础设施与专职运营得以成立的规模

正文中像"中规模(10\~50人)团队"这样同时标注等级与人数范围的地方,均以本表为准。而在像单人·独自开发这类人数本身具有意义的场合,则不用等级,而直接写出确切的人数。

游戏策划文档

缩略语 正式名称 含义
GDD Game Design Document 游戏设计文档。确定了系统·数值·行为的详细规格书
CDD Concept Design Document 概念设计文档。GDD 之前阶段的早期策划案(方向·概念)
TF TaskForce 为短期目标而临时组建的专职团队(例:战斗 TF)
DD Design Director 设计总监。统筹游戏设计方向的主导角色
RnD Research and Development 研究·开发。探索原型·新技法的阶段·组织(例:程序化生成 RnD)

游戏领域

缩略语 正式名称 含义
NPC Non-Player Character 玩家不操控的角色
HUD Heads-Up Display 叠加显示在游戏画面上的状态信息(生命值·小地图等)
DPS Damage Per Second 每秒伤害量
GCD Global Cooldown 全局冷却。使用一个技能后,所有技能都会短暂一同锁定的公共等待时间
TTK Time To Kill 击杀目标所需的时间
PK Player Kill (战争·PvP 语境)玩家之间的战斗·击杀
BT BehaviorTree 行为树。将 NPC AI 的行为分支以树形定义的结构
FSM Finite State Machine 有限状态机。以状态与转移来定义行为的模型
PCG Procedural Content Generation 程序化内容生成。以规则·算法自动生成内容
VFX Visual Effects 视觉特效
SFX Sound Effects 音效
VA Voice Actor 配音演员
RPG / MMORPG (Massively Multiplayer Online) Role-Playing Game 角色扮演游戏 / 大型多人在线角色扮演游戏
P2W / P2E Pay To Win / Play To Earn 靠付费变强的机制 / 靠游玩获得收益的机制
RMT Real Money Trading 游戏资源的现金交易

数据·运营

缩略语 正式名称 含义
KPI Key Performance Indicator 核心绩效指标
DAU Daily Active Users 日活跃用户数
FK Foreign Key 外键。指向其他表主键的列
PK Primary Key (数据语境)主键。唯一标识一行的列
ROI Return on Investment 投资回报(回收)
MECE Mutually Exclusive, Collectively Exhaustive 相互独立·完全穷尽。不重复、不遗漏地进行划分的分类原则
STT Speech-to-Text 将语音转换为文本
VBA Visual Basic for Applications Excel 内置的宏语言
SVN Subversion 文件版本管理系统
telemetry (计测数据) 从游戏构建·运行中自动收集的游玩日志·指标(输入·战斗·流失等)。中文常称"遥测"

AI·工具

缩略语 正式名称 含义
AI Artificial Intelligence 人工智能
LLM Large Language Model 大型语言模型(ChatGPT·Claude 等的底层基础)
JIT Just-In-Time 仅在需要的瞬间才插入的方式(本书中指根据输入自动注入相应记忆)
MCP Model Context Protocol 将 AI 工具与外部服务对接的标准
API Application Programming Interface 程序间的调用约定
UE Unreal Engine 虚幻引擎
atom (本书特有术语) 以 1 决策 = 1 文件固化而成的决策·规则卡片
Wrapper / Cascade / Junction (本书特有术语) 常用工具的入口 / 将多项检查一次性打包的工具 / 连接到本体的符号链接
rg ripgrep 快速文本检索命令(替代 grep 的 CLI 工具)。用于代码·文档的全量检索
ClickUp (任务·问题追踪器) 管理工作·日程的云端协作工具。JIRA·Redmine·Linear 也属同一范畴。通过 MCP 对接,由 AI 查询·更新

UI·无障碍标准

缩略语 正式名称 含义
UI / UX User Interface / User Experience 用户界面 / 用户体验
WCAG Web Content Accessibility Guidelines Web 内容无障碍指南(对比度·触控目标尺寸等的合格线)
HIG (Apple) Human Interface Guidelines 苹果的界面指南
SC Success Criterion WCAG 的单项合格标准编号(例:SC 1.4.3)
pt / dp / px point / density-independent pixel / pixel 屏幕尺寸单位

文件·格式

缩略语 正式名称 含义
YAML YAML Ain't Markup Language 便于人阅读的配置·数据标记格式
JSON JavaScript Object Notation 数据交换的标记格式
HTML / SVG HyperText Markup Language / Scalable Vector Graphics Web 文档 / 矢量图形格式
GLB GL Transmission Format (Binary) 3D 模型二进制文件格式

像 PK 这样含义随语境而不同的缩略语,正文在首次出现时会一并说明是哪一种。若感到混淆,回到本表查看即可。

附录 K. 移植到其他 LLM·驱动框架(harness)

本书的案例与工具几乎全部以一种环境——即 Claude Code——为前提写成。因此在审批场合或外部评审中,几乎每次都少不了一条质疑:"这会不会被绑死在某家公司的特定工具上?"策划负责人对审批依赖单一供应商的决策感到有负担,怀疑论者担心一旦工具更换,本书的方法便会整体崩塌,而评估海外版权的一方则会问:当对方国家以别的工具为标准时,本书是否还有用。三者的表述各异,本质却相同——都是对供应商锁定(vendor lock-in),即被一种工具困住的不信任。

本附录的目的就是回应这种不信任。先说结论:本书所倡导的工作骨架是工具中立的。它既不绑定于某个特定的模型名称,也不绑定于某个特定的命令行工具。Claude Code 只是把这套骨架实现得最为顺畅的容器而已,同样的骨架可以换装到别的容器中。本附录将(1)用表格呈现什么才是与工具无关的骨架,(2)把 Claude Code 的各个要素与移植到其他环境后所对应之物一一配对,(3)在"模型世代会不断更替"这一前提下,确立一条确认最新状态的原则,(4)坦率写下移植时会失去什么、又能守住什么。


K.1 与工具无关的骨架

贯穿本书全书的工作方式,可以概括为五根支柱。这五者中没有任何一个是某个特定模型或命令行工具的功能名称,而都是对"人与人工智能协同工作时,如何反复稳定地产出可信结果"这一问题的回答。因此即便工具更换,它们依然留存。

骨架 是什么 为何工具中立
标准 → 模板 → 验证关卡 把达成共识的规则(标准)固化为填空式的框架(模板),并设置一道自动筛查结果是否遵守了规则的关卡(门禁) 规则、框架、检查这些概念,在任何工具中都能用文字或脚本表达
atom = 一决策一文件 把一个决策写进一个小文件,需要时取出使用,修改时只改那一处 把决策拆细放进文件,只需有文件系统即可
JIT 注入 只把当前对话真正需要的决策,即时(Just-In-Time)挑选出来喂给模型 这是"只注入必要上下文"的原则,注入方式不过因工具而异
复盘循环 以日、周、月为单位回顾所做的工作,把反复出现的模式提升为下一次工作的规则 回顾与改进的流程靠的是习惯与文档,而非工具
工具借用边界 借来的只有骨架(算法、结构),领域数据留在原处(附录 B) 借什么、留什么的判断,在任何工具中都相同

这张表最右一栏才是关键。五根骨架的定义之中,没有一次出现某个特定产品的名称。出现的只有规则、文件、上下文、习惯、边界这类任何工作环境里都存在的普适概念。因此,"要是不能再用 Claude Code 该怎么办"这个问题,实际上就转化成了"如何在别的工具里实现这五个概念"这个远为好答的问题。答案就在下一节。


K.2 要素对应表(Claude Code → 其他环境)

Claude Code 里有一些具体装置,能方便地实现上述骨架。hook(在特定时点自动执行的脚本)、MCP(把外部工具与数据连接到模型的规约)、settings 文件(权限与环境设置)、斜杠命令(把常用流程用一行调用的快捷命令)、技能(可复用的工作组合)等。这些虽是 Claude Code 特有的名称,但其角色在其他环境中几乎都有对应物。下面这张表就是它们的配对。

Claude Code ChatGPT(网页·应用) Cursor / Copilot 通用 LLM API
hook(时点自动执行) 对话前后的手动流程 / 自定义 GPT 指令 编辑器操作前后的任务·pre-commit 钩子 在调用前后插入的前置·后置脚本
MCP(外部连接规约) 插件 / 动作(Action) / 代码解释器 扩展(extension) / 内置工具调用 函数调用(function calling) / 自建 API 封装
settings 文件(权限·环境) 自定义 GPT 设置界面 / 项目设置 .cursor·工作区设置文件 代码内的设置对象 / .env·YAML 设置文件
斜杠命令(流程快捷) 保存的提示词 / 自定义 GPT 代码片段(snippet) / 用户自定义命令 提示词模板函数
技能(可复用工作组合) 自定义 GPT / 提示词集合 规则文件 + 脚本 模块化的提示词·代码函数
CLAUDE.md / 记忆 自定义指令 / 记忆功能 项目规则文件(rules) 系统提示词 + 外部记忆存储
atom 文件集合 (与工具无关)Markdown 文件 (与工具无关)仓库内的 Markdown (与工具无关)文件·数据库记录

看这张表,有一点会变得清晰:越往右走,即越靠近通用 LLM API 一侧,"原本自动替你完成的",就越变成"必须自己动手搭建才能插入的"。在 Claude Code 中一行 hook 就能搞定的自动注入,到了通用 API 里就成了调用前亲手编写的前置脚本。自动化的便利虽有减少,但骨架本身照原样迁移。也就是说,移植不是"失去功能",而是"用自己的双手把便利重新铺设一遍"。

flowchart LR subgraph 도구중립["工具中立骨架(不变)"] S[标准→模板→验证] A[atom: 一决策一文件] J[JIT 注入] R[复盘循环] end subgraph 구현["按环境实现(可替换)"] CC[Claude Code: hook·MCP·settings] GPT[ChatGPT: 插件·自定义 GPT] CUR[Cursor·Copilot: 扩展·规则文件] API[LLM API: 函数调用·前置脚本] end 도구중립 --> CC 도구중립 --> GPT 도구중립 --> CUR 도구중립 --> API classDef code fill:#dbeafe,stroke:#2563eb,color:#0b2545; classDef ai fill:#f3e8ff,stroke:#9333ea,color:#3b0764; classDef human fill:#fde68a,stroke:#b45309,color:#000; classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class J code; class CC,GPT,CUR,API ai; class R human; class A data;

这张图就是整篇附录的一页概要。上方的方框(骨架)无论箭头指向哪个环境,内容都不改变;只有下方的方框(实现)会随环境替换。审批中一旦有人说出"供应商锁定",你只需摊开这一张图,回答"被绑住的是下面那一栏,不是上面那一栏"即可。


K.3 "模型名称会变"这一前提

谈到移植,最快过时的信息就是模型名称。若把写作本书时最新的模型名称钉死在正文里,那么下一代模型问世的那一刻,那句话就成了错误信息。因此本书从一开始就遵循一条原则:不依赖某个特定模型的名称与世代编号来讲解,而依赖模型所承担的角色(推理、摘要、代码生成之类的功能)来讲解。

会变的(不要钉死) 不变的(可以依赖)
模型产品名·世代编号 诸如"擅长推理的模型""能接收长上下文的模型"这类角色区分
上下文上限的具体数值 "既有上限,就只注入必要上下文"的 JIT 原则
价格·速度的具体数值 "昂贵的作业只运行通过了关卡的那部分"的成本意识
某项功能的开关方法 "该功能所承担的角色"以及可替代它的骨架

在实务中确认最新模型与功能的方法,每种工具也只需一行。在 Claude Code 中,用 /model 命令即可立即查看当前所用的模型及可选项;ChatGPT、Cursor 之类的工具,也会在设置界面或模型选择下拉菜单中给出同样的信息。因此,如果本书的某句话看上去与模型名称不符,那不是那句话错了,而是模型已过了一代。只要角色相同,方法便照旧适用。读书时若觉得模型名称陌生,请不要怀疑正文,而应先用 /model 之类的命令,确认你手中工具的最新状态。


K.4 移植时失去的与守住的

换工具一定会失去一些东西。若掩盖这一事实,反而会失去信任,所以我先坦率写下会失去什么。不过,失去的几乎都属于"便利"的范畴,守住的则属于"骨架"的范畴。也就是说,失去的是重新铺设便可找回的,守住的则本就不曾绑定在工具上。

类别 项目 说明
失去的(便利) 自动执行的顺畅 原本像 hook 那样自动介入的自动化,如今得靠前置·后置脚本亲手搭建
失去的(便利) 一体化的单一界面 原本命令、工具、文件汇聚于同一流程,如今可能得分散到多个工具上
失去的(便利) 即取即用的技能·命令 斜杠命令与技能得按那个工具的方式重新注册
守住的(骨架) 标准·模板·验证关卡 规则、框架与检查都是文字或脚本,在任何地方都照原样存活
守住的(骨架) atom·JIT·复盘循环 靠文件与习惯运转,故工具更换也依旧维持
守住的(骨架) 工具借用边界(附录 B) 借什么、留什么的判断标准与环境无关

把这张表压成一句话便是:移植中失去的,是花些时间就能复原的自动化便利;守住的,则是本书从一开始就竭力置于工具之外的工作骨架。因此,对"这不是供应商锁定吗"之问,最诚实的回答是:确有被绑住的部分,但那是可以替换的容器,而真正的价值——内容物——从一开始就不曾绑定于任何容器。愿这一篇附录,能在审批场合替你给出这个答案。

附录 L. 团队导入 TCO·上手工作表

本附录是一份填空式工作表,用来回答工作室 PD·负责人的这样一个问题:"当把单人六个月的系统扩展到中等规模团队时,导入工作量·运营成本·账号·内网安全该用什么、如何估算?"如果说正文 19.3(AI 导入策略与说服管理层)讲的是"不要粉饰 ROI",那么本附录就是把这一原则同样应用到导入成本一侧。也就是说,本附录不提供任何数字。所有单元格都是空白,填写它们靠的是你所在团队的测量·估算;凡标注 [需财务确定] 的单元格,在财务填写之前任何人都不得凭估算填补。

本附录的使用方法如下。首先在 L.1 中,用一张图把 TCO(Total Cost of Ownership,总拥有成本)拆分为哪些项目理清楚。接着把 L.2\~L.6 的五张工作表按自己团队规模所在的行,以空白状态打印出来,自己去测量,或用一行问题交给财务·信息安全负责人。最后用 L.7 的自查清单确认没有遗漏的单元格即可。本附录的价值不在于填好的数字,而在于把容易遗漏的成本项目预先做成单元格


L.1 TCO 不等于授权费用

PD 最常掉进的陷阱,是只把导入成本看作"订阅费 × 人数"。实际的总拥有成本要比这宽得多。它分成一次性投入即结束的导入工作量(安装·标准化·上手(onboarding))与每月周期性的运营成本(授权·token·基础设施·管理人力),在此之上再叠加看不见的安全·账号管理成本。

flowchart TB TCO["团队导入 TCO"] --> A["一次性:导入工作量<br/>(安装·标准化·上手)"] TCO --> B["周期性:月度运营成本<br/>(授权·token·基础设施·管理)"] TCO --> C["周期性:安全·账号管理<br/>(SSO·审计·密钥回收)"] A --> A1["L.3 导入工作量工作表"] B --> B1["L.5 月度运营成本工作表<br/>[需财务确定]"] C --> C1["L.4 内网·安全检查"] A --> A2["L.6 上手时间工作表"] B --> A3["L.2 账号·授权工作表"] classDef data fill:#e2e8f0,stroke:#64748b,color:#1e293b; class A1,A2,A3,B1,C1 data;

这三条分支中,PD 容易低估的是左侧(导入工作量)与右侧(安全·账号)。授权费用会写在报价单上,而"把单人六个月里手工积累的标准·技能整理成团队可共享形态的工作量"与"决定内网中允许外部 LLM 调用到何种程度的安全评审",报价单上都没有,因此它们总是让工期与预算超支。本附录的工作表,目的正是先把这些看不见的成本哪怕以空白的形式揭示出来。

正文 19.3.6 说过:"成本的绝对值不写进书里——它是要从财务那里拿来填的空白。"本附录就是把这些空白该放在何处,逐项铺开。


L.2 账号·授权工作表

这是最先填写的表。把谁使用哪个工具、这些权限如何发放·回收,连同人数一并记录。人数栏用你团队的实际人头填,单价栏从报价单或公开价目表取来填。本书不写单价。

项目 填写什么 由谁填写 本团队值
各工具席位数 每个工具所需的账号(席位)数 主管 ______ 席位
权限等级分布 full / 按任务 cap / 外包一次性人员(附录 C.1.2) 主管 full __人 / 一般 __人 / 外包 __人
席位单价 各工具的每月席位费用 财务·采购 ______ /席位·月
是否公用密钥 团队公用 API 密钥 vs 个人密钥 信息安全 □ 公用 □ 个人
发放流程 新入职者账号的发放路径·耗时 主管 ______
回收流程 离职·外包结束时密钥/席位的回收路径 信息安全 ______

规则有两条。第一,外包·短期人员不要常设发放席位,而应按任务单位开通并回收(附录 C.1.2)。第二,回收流程栏为空时,不启动发放。最常见的事故是离职者账号未被回收,导致成本与密钥泄露一并外泄,因此要先设计回收、再设计发放。


L.3 按团队规模的导入工作量工作表

这是一张按规模估算"单人六个月"扩展为团队时新增的一次性工作量的表。工作量栏以人日(一个人工作一天的量)为单位,由你团队实际测量或估算填写。本书不提供人日数——因为它随团队的熟练度·既有标准的整理程度而大幅变化。

导入工作量项目 1\~3 人 4\~10 人 11\~30 人 31\~50 人 测量/估算主体
环境安装·配置(工具·hook·权限) ___人日 ___人日 ___人日 ___人日 主管/基础设施
单人资产的团队共享化(技能·标准·atom 整理) ___人日 ___人日 ___人日 ___人日 主管
团队标准的建立(命名·前置元数据·规则手册,附录 D) ___人日 ___人日 ___人日 ___人日 主管
验证关卡搭建(lint·规则手册自动化) ___人日 ___人日 ___人日 ___人日 QA/主管
上手资料制作(与 L.6 联动) ___人日 ___人日 ___人日 ___人日 主管
合计(导入一次性工作量) ___人日 ___人日 ___人日 ___人日

填这张表时容易漏掉的是第二行。单人在六个月里积累在脑中与个人文件夹里的资产,若要团队共享,就需要有人把它取出、整理并做成文档的额外工作量。若把这份工作量当作"0",导入进度必然拖延。此外,表格的单元格形状也预先呈现出:规模越大,比起安装工作量,标准建立·验证关卡(verification gate)的工作量增长得更陡——因为人一多,需要达成一致的标准数量就会增加。

遵循正文 19.3.1 的分阶段导入(由保守到进取),就不必在一个季度内把这份工作量用尽,而可以从第 1 阶段(上下文注入)的试点开始分散投入。不要试图一次性把表中的合计报批,而是先把第 1 阶段的工作量单独拆出来报批,这才现实。


L.4 内网·安全检查工作表

这是 PD·负责人最直接惧怕的领域。逐项检查有什么会流向外部 LLM、内网中允许外部调用到何种程度。这张表是一份判定合格/暂缓的检查清单(与附录 C.6 安全联动),只要有一项未定,就暂缓该范围的导入。

检查项目 通过标准 负责 状态
向外部 LLM 传输的数据范围 敏感数据用占位符(placeholder)/自托管(C.6) 信息安全 □ 通过 □ 暂缓
支付·个人信息传输 明文规定无例外禁止传输 信息安全 □ 通过 □ 暂缓
内网外部调用策略 定义允许域名·代理·日志保留期 基础设施 □ 通过 □ 暂缓
是否需要自托管 决定核心 IP 是否用自托管模型处理 负责人/信息安全 □ 已决定 □ 未定
密钥泄露事故应对 立即更换 + 使用记录审查路径(C.7) 信息安全 □ 通过 □ 暂缓
审计日志 记录·保存谁·何时·调用了什么 基础设施 □ 通过 □ 暂缓
公司 IP 外泄检查 grep watchlist 等事前检查流程(附录 B.6) 主管 □ 通过 □ 暂缓
外包访问隔离 外包账号阻断核心资产访问·按任务隔离 信息安全 □ 通过 □ 暂缓

这张表中成本差距最大的一栏是第四行(是否需要自托管)。一旦决定核心 IP 绝不能发往外部 LLM,自托管的基础设施成本就会整块叠加到 L.5 的运营成本上。因此这一决定不该由主管、而应由负责人·信息安全共同作出;在决定之前,L.5 的基础设施栏无法确定。两张工作表通过这一栏相连。


L.5 月度运营成本工作表 [需财务确定]

这是一张把每月周期性成本逐项分解的表。本表的金额栏全部为空白;凡标注 [需财务确定] 的单元格,在财务填写之前任何人都不得凭估算填补。token 单价·订阅费·基础设施费用会随模型·调用量·合同每月变化,因此本书不写绝对值。

运营成本项目 计算方式 由谁填写 每月金额
授权·订阅 席位数 × 席位单价(L.2) 财务 [需财务确定]
LLM token 成本 调用量 × token 单价,各工具上限(cap)之和 财务 [需财务确定]
基础设施(自托管时) 依 L.4 决定的服务器·GPU·存储 财务·基础设施 [需财务确定]
备份·同步 存储库·备份存储(附录 C.5) 财务 [需财务确定]
运营管理人力 工具·密钥·日志管理负责人的时间折算 主管·财务 [需财务确定]
每月合计 上述项目之和 财务 [需财务确定]

这张表的规则只有一条,让空白保持空白。请回想正文 19.3.2 中 AI 把运营成本栏煞有介事地捏造成 $4,500 的失败——无论人还是 AI,一旦凭估算填补这一栏,那份报告在第一个提问下就会崩塌。真正控制成本的机制不是金额,而是各工具设有每月上限(cap)、超额会自动上报的结构(19.3.6)。报批时要向管理层展示的,不是填好的金额,而是"设有上限、超额会上报"的结构,以及交给财务去填的空白清单。

第五行(运营管理人力)最常被遗漏。工具装好并非就此了事,它每月都在吃掉那个回收密钥、查看日志、调整上限的人的时间。若把这一栏置为 0,那份工作就会藏进主管看不见的加班里。


L.6 上手时间工作表

这是一张分阶段估算一名新成员在系统之上做到独当一面所需时间的表。时间栏最准确的做法,是在你团队里实际让人上手一次再测量(与正文 19.3.7 的基线测量方法同一做法)。测量之前,保持空白。

上手阶段 做什么 测量时间 备注
环境安装 直到工具·hook·账号配置完成 ___小时 与 L.2 发放流程联动
标准学习 熟悉命名·前置元数据·规则手册(附录 D) ___小时 有资料则可缩短
首个任务(保守) 用上下文注入产出首个成果·通过评审 ___小时 19.3.1 的第 1 阶段
适应验证关卡 按 lint·规则手册关卡开展工作 ___小时
达到独立工作 无需监督即可工作·作出采纳判定 ___天 上手完成标准

填完这张表,就会显出导入工作量(L.3)中"上手资料制作"一栏为何重要。上手资料整理得越好,第二·第三行的时间就越短;新成员越多,这份节省就越是累积。也就是说,上手资料制作是一次性工作量,而回收则按成员数量反复发生。正文 19.3.3 所说的"JIT 自动注入 221 条——新成员也在同一套规则之上工作",在这张表里体现为第三行的时间缩短。

最后一行(达到独立工作)才是上手的真正完成标准。若把环境安装完成误当作上手完成,监督成本就会不断堆积在主管身上。标准应当是"无需监督即可作出采纳判定"。


L.7 导入前自查清单

最后,是把这些工作表拿给管理层之前,自己必须先通过的项目。秉持与附录 B.6(借用前检查)相同的精神,只要有一项为空,就推迟报批,先填那一栏。

检查项目 通过标准
是否已定义账号回收流程 L.2 的回收流程栏不为空
安全检查是否全部通过/已决定 L.4 中 □暂缓·□未定 为 0 项
运营成本的空白是否已交给财务 L.5 的 [需财务确定] 已作为问题发出
是否把导入工作量拆成了阶段 不按 L.3 合计,而从第 1 阶段的工作量开始报批
上手完成标准是否为"独立工作" 以 L.6 最后一行判定完成
是否未把估算值写成断言 在所有估算栏标注"估算·样本数"

请不要把这张表读作五栏的合格,而请把它读作六道锁。把单人系统扩展为团队这件事确实可行,但这一扩展的成本并非授权费用;唯有诚实地填满这六栏空白,整体图景才会显现。而且,任何一栏都不要交给 AI 去填——AI 会像正文 19.3.2 那样,用煞有介事的数字填补空白。AI 的位置,止于接过你测量的值、整理成报批幻灯片的文句。

附录 M. 维度向量·嵌入——面向游戏策划的直觉

本书有五处——§8.2.7(经济)·§5.4(声线)·§6.3(人设)·§7.3(模式)·§13.3(数据)——出现了"压缩为维度向量""嵌入""在向量空间中相近"这类表述,它们都作为方向路标出现。即便没有机器学习背景,仅凭游戏策划的感觉也足以把握这一概念。只要一次性铺好直觉,这五处便都能用同一张图来读懂。

不过要先把一件事钉死:概念上的直觉很简单,但适用的条件很沉重。 这一构想并非入门——而是只有立足于本书前文一路搭建起来的基础之上才能成立的最末端的应用:即保守适用的验证关卡(verification gate)、数据与遥测(telemetry)基础设施、像 voice_lint·一致性检查这样的各领域检查器,以及 Layer 整合。若没有那层基础就先去画坐标,正如 M.4 所示,那张地图会连同与游戏相错的误差一起被干净利落地压缩,沦为一个虚像。这五处之所以都"为时尚早",真正的原因就在于此——不是因为构想有多难,而是因为支撑它的基础要先行。本附录是一张地图,供已具备该基础的团队在掂量"下一步"时展开来看,而不是让人迈出第一步的入门书。

M.1 一句话——把特征变成坐标,相似的就成为相近的点

无论什么对象,把它的各项特征换成一串数字,放到"地图"上成为一个点,这就是嵌入 (embedding)。那串数字就是维度向量。约定只有一条——让越相似的对象在地图上成为越相近的点

例如,把 NPC 放到(说话的正式程度、情绪表达量、词汇难度……)这类特征的坐标上,说话方式相近的 NPC 就会在地图上聚到一起。若是菜谱,则把食材构成放到坐标上,相似的菜就会聚到一起——§8.2.7 中作为线索提到的 Epicure 所做的正是这件事。

把特征换成坐标的"地图"——3D 也是比喻,实际是数百维 特征 1 特征 2 特征 3 …(数百) 后方的簇(越远越淡) 空白区域 尚不存在的组合 前方的簇(越近越深) 连接两点,其间 = 插值(中间变体) 相近的点 = 相似 · 空白区域 = 尚不存在的组合 · 两点之间 = 插值

M.2 地图画出来后,三件事会以距离·位置的形式显现

  1. 相近的点 = 相似之物。 点若挤到一处,就是"多样性消亡"的信号(§5.4 声线趋同·§6.3 人设老套,不凭印象,而是以点的密度来看)。
  2. 空白区域 = 尚不存在的组合。 没有任何点的位置,就是"这样的设计还不存在"的设计空白(§7.3 模式的空白区域·§13.3 新主题的出现)。
  3. 两点之间 = 中间变体。 连接两点并取其之间,就会得出"中间态"——这就是插值 (interpolation)。在"米"与"咖喱叶"之间连线,会得到二者之间的风味;在两个 NPC 之间连线,会得到二者之间的人设。

M.3 常出现的三个术语

看着相似其实相反——别与 AHP 混淆。 用于多准则决策的 AHP(层次分析法,Analytic Hierarchy Process,Saaty)也把定性判断换成向量,这一点看着相似——因为它通过将各准则两两相较的两两比较,提取出优先级权重(主特征向量)。然而方向是相反的。AHP 是由人预先定义准则与层级、再在其中赋予权重的自上而下的决策;而这里的嵌入是无需人来定义,簇便从数据中自行显现的自下而上的发现。§13.3 要突破的局限——"人预先定义的细分群体"——正是 AHP 的出发点。二者不在同一处,而是位于相反的两端。

M.4 本书为何写下"为时尚早"——压缩的代价

地图很强大,但不是免费的。

所以本书把维度向量当作方向路标,而非处方——这是验证(遥测·模拟)已扎实铺好的团队在数年之后才会去审视的领域。各领域的具体线索散落在 §8.2.7(经济)·§5.4(声线)·§6.3(人设)·§7.3(模式)·§13.3(数据)中,全部都可以在本附录这一张地图之上来读。

附录 N. 教学用 15 周进度表·难度指南

本附录面向想把本书用作一学期课程教材的读者——大学、专科院校、培训机构的授课教师,企业内训负责人,学习小组的组织者。要把一本近1,000页的大部头按学期切分,比想象中更让人无从下手。哪一部分放在第几周、如何把正文中的「动手试试」转化为作业、以什么标准为提交物评分——这三点一旦卡住,再好的书也难以被选作教材。本附录把这三件事做成可以照搬使用的工具。

本附录的使用方法如下。首先,结合自己的教学日程阅读 N.1 的 15周进度表(16周与短学期的变体另见 N.2),再用 N.3 的难度徽章与先修知识表衡量学员水平。接着,复制 N.4 的评分量规(rubric),按自己的作业只替换其中的条目即可。所有表格都编排成可以直接打印、贴进教学大纲(syllabus)的形式。

有一点需要先说明。本书每一章都以「动手试试」收尾。让读者不是读完就合上,而是当天就动手——这正是正文的目标;而在教学中,这个「动手试试」正好成为作业的第一手素材。本附录的进度表之所以一并写明如何把正文的「动手试试」转化为各周作业,原因就在这里。


N.1 15周标准进度表

这是按最常见的15周制(每周1次、每次3小时)学期编排的标准进度表。本书的24个部分不会在一个学期内全部讲完——硬塞进去的话,哪一部分都不会真正留在手上。取而代之,我们选择了这样的结构:先把基础(第1·2部分)夯实,从各领域中挑出有代表性的5\~6个深入讲解,再从流程·运营中只选取核心内容收尾。未讲解而留下的部分标注为「延伸阅读」,引导感兴趣的学员自行展开。

学习目标全部以“学员能做到什么”的动词来写。不是“知道”,而是“做出·验证·选出”。全书反复强调“AI 给出候选,人来筛选”这一句话,因此目标的动词也遵循这一分工。

周次 涉及的部分·章节 学习目标(结课后能做到) 转化为作业的「动手试试」
1 1.0 开始之前 + 第1部分(导入) 讲解终端·账号·计费结构,在自己的 PC 上安装 AI 工具并开启第一个会话 1.0 安装「动手试试」——提交安装截图 + 第一次的提示词与输出
2 第2部分(信息架构) 用 YAML frontmatter 将文档数据化,设计文件夹·命名规范 2.1 frontmatter「动手试试」——为自己的3份文档添加 frontmatter
3 第3部分(系统策划) 以模式优先(schema-first)原则,先定义数据表的 $스키마 3.2 模式「动手试试」——为一种迷你数据表编写规格书
4 第10部分(QA·一致性) 跟着做出用代码检查30张数据表 FK 一致性的工具 10.1 一致性验证「动手试试」——用 N.4 量规评分的核心作业
5 第4部分(战斗) + 第8部分(数值) 将战斗数值按 Layer 分解,把确定性的数值平衡公式沉淀为规则手册 8.1 数值公式「动手试试」——一种伤害公式 + 模拟
6 第5部分(叙事) 制作 NPC 台词的 voice_profile,用 voice_lint 抓出语气偏离 5.2 voice_profile「动手试试」——为一名角色编写语气档案(voice profile)
7 第6部分(内容) + 第7部分(关卡) 区分程序化生成的两条轴(规则·AI),批量产出并评审内容候选 6.2 生成器「动手试试」——生成10条内容候选 + 评审日志
8 期中检查·汇报 整合第1\~7周的作业,以自己的迷你项目进行演示 期中作业汇报(整合第3\~6周产出的演示)
9 第9部分(UX·UI) + 第14部分(移动端) 用 lint 检查 HUD,抓出视线偏移·对比度不足,并把 PC 端 HUD 压缩到移动端 9.1 HUD lint「动手试试」——一种界面的 lint 报告
10 第16部分(沟通者) + 第17部分(会议纪要) 在隔离的工作空间中只将决定定为正本,并把会议纪要结构化 17.x 会议纪要「动手试试」——将1份真实会议录音结构化
11 第18部分(决策) + 第19部分(团队负责人) 把决定留存为可追溯的卡片,把愿景转化为决策的评分表 18.1 决策追溯「动手试试」——编写3张决策卡
12 第20部分(协作记忆) + 第21部分(自我改进) 把协作上下文作为记忆来运营,让复盘运转成自我改进的循环 第21章 复盘「动手试试」——1份周复盘 + 1条抽取规则
13 第22部分(治理) 检查提示词·幻觉·成本·法务·伦理的边界并订立规则 22.1 提示词「动手试试」——1份工作指示书 + 幻觉检查流程
14 第23部分(个人开发) + 第24部分(运营进阶) 用单人精简版迁移工具,用代码验证一致性·链接·stale 24.1 验证「动手试试」——一种针对自己项目的验证脚本
15 期末项目汇报·评估 设计·演示·验证一条贯穿整个学期的个人工作流 期末作业汇报(以 N.4 扩展量规评估)

延伸阅读(不含在课程内,建议自主学习): 第11部分(角色·宠物·坐骑)、第12部分(美术指导)、第13部分(数据·KPI)、第15部分(运营)。这四个部分领域针对性很强,故留给感兴趣的学员结合自身领域自行展开。以附录 F(案例索引)为向导,便可从贴近自己环境的案例开始反向检索进入。

进度流程一目了然,如下所示。这是一个具有两座高峰(期中·期末)的结构:基础 → 领域深化 → 期中整合 → 流程·运营 → 期末整合。

flowchart LR subgraph A["基础(第1~3周)"] W1["第1周<br/>安装·导入"] --> W2["第2周<br/>信息架构"] --> W3["第3周<br/>系统·模式"] end subgraph B["领域深化(第4~7周)"] W4["第4周<br/>QA·一致性"] --> W5["第5周<br/>战斗·数值"] --> W6["第6周<br/>叙事"] --> W7["第7周<br/>内容·关卡"] end M1{{"第8周<br/>期中汇报"}} subgraph C["流程·运营(第9~14周)"] W9["第9周<br/>UX·移动端"] --> W10["第10周<br/>沟通"] --> W11["第11周<br/>决策·负责人"] --> W12["第12周<br/>协作·复盘"] --> W13["第13周<br/>治理"] --> W14["第14周<br/>个人开发·验证"] end M2{{"第15周<br/>期末汇报"}} A --> B --> M1 --> C --> M2 classDef human fill:#fde68a,stroke:#b45309,color:#000; class M1,M2 human;

N.2 学期长度变体(16周 / 短学期8周)

各学校的学期长度不尽相同。除标准的15周外,这里给出最常遇到的两种变体的调整方案。无论采用哪种变体,都建议保留核心作业(第4周的一致性检查)与两座汇报高峰——因为这里正是本书诚实原则(“展示的不是效果,而是结构”)体现得最充分的地方。

学期形态 调整方法
16周制 标准15周 + 第16周增设 补强·再评估周。给予期末作业重新提交的机会,或从「延伸阅读」4个部分中由学员投票选定1个部分开设专题讲座
短学期8周(每周2次或集中授课) 第1周(安装·导入) → 第2周(信息·模式) → 第3周(一致性,核心作业) → 第4周(战斗·数值·叙事合并) → 第5周期中汇报 → 第6周(会议·决策·协作) → 第7周(治理·验证) → 第8周期末汇报。领域缩减为有代表性的3个,「动手试试」在课堂上作为实操吸收消化
翻转课堂(flipped classroom) 把通读正文放到课前作业,课堂时间全部分配给「动手试试」实操与量规互评。本书的代码无需外部依赖即可直接运行,适合以实操为中心的教学

N.3 章节难度徽章·先修知识

即便在同一本书里,各章所要求的背景知识也不相同。有些章即使是初次接触终端的大一学生也能跟上,有些章则需要具备数据库键的概念或统计基础才能完全消化。为便于按学员水平调整进度、或指引先修课程,这里用三级徽章加以归纳。

各徽章的含义如下。

徽章 等级 含义
🟢 入门 入门 非专业、大一学生也能跟上。代码只需达到复制·运行的程度即可
🟡 实务 实务 需要能读懂代码并按自己的数据加以修改。建议理解策划实务的语境
🔴 进阶 进阶 处于设计·扩展算法与结构的阶段。若无先修知识,消化难度较高

各周核心部分的徽章与先修知识如下。“先修知识”是指为顺利跟上该周而最好提前具备的背景,并非没有它就无法修读。

周次 核心部分 徽章 先修知识
1 1.0·第1部分 导入 🟢 入门 无(以初次接触终端为前提)
2 第2部分 信息架构 🟢 入门 使用文本编辑器
3 第3部分 系统·模式 🟡 实务 表格/电子表格基础,数据类型概念
4 第10部分 一致性验证 🔴 进阶 Python 基础(函数·循环),关系键(FK)概念
5 第4·8部分 战斗·数值 🟡 实务 四则运算式,表格计算(Excel 函数)
6 第5部分 叙事 🟢 入门 角色·剧本写作的感觉
7 第6·7部分 内容·关卡 🟡 实务 程序化生成概念(建议),坐标·网格的感觉
9 第9·14部分 UX·移动端 🟡 实务 屏幕布局·分辨率概念
10 第16·17部分 沟通 🟢 入门 无(有协作经验更有利)
11 第18·19部分 决策·负责人 🟡 实务 团队协作·项目管理经验(建议)
12 第20·21部分 协作·复盘 🟡 实务 修完第2周信息架构
13 第22部分 治理 🟡 实务 基础统计(平均值·分布,幻觉检测语境),著作权基础
14 第23·24部分 个人·运营 🔴 进阶 Python 基础,git 基础,修完第4周一致性

先修课程一句话说明(供教学大纲使用): “建议具备 Python 入门或与之相当的编程基础,但并非必需。第4·14周的进阶章节以 Python 函数·循环的水平为前提;未修读者可通过第1\~3周的入门轨道充分跟上,为此将作业分轨运营。”

根据学员构成的运营建议如下。


N.4 评分量规示例 —— 一致性检查工具的动手试试(第4周核心作业)

没有量规,「动手试试」的提交物很容易只按“能跑/不能跑”的二分法来评分。这样一来,本书最为看重的东西——评审并拒绝 AI 输出的过程——就会在评估中消失。因此,这里以第4周核心作业(10.1 一致性验证 atom「动手试试」)为例,给出一份不仅评判成果、还评判其过程的量规。其他各周的作业也只需替换条目名称即可照搬使用。

作业定义: 针对自己制作的(或提供的)多种数据表,与 AI 一起做出检查表间外键(FK)一致性的工具,并演示该工具能否抓出故意埋入的错误。提交物包括:① 工具代码 ② 检查运行结果(通过/失败报告) ③ 向 AI 输入的提示词全文,以及其中被拒绝·修改的输出的记录。

量规由4个条目、每项25分(共100分)构成。关键在于:与“工具能跑”(第2项)相区别,如何驾驭 AI(第3·4项)以一半的权重来评估。

# 评估条目 分值 欠缺(0\~12) 一般(13\~19) 优秀(20\~25)
1 一致性规则定义 —— 是否明确了检查哪些 FK 关系、为何检查 25 检查对象关系不明确或随意 识别出主要 FK 关系,但依据说明不足 将表间关系连同图示·依据一并定义,并说明检查的优先级
2 工具运行·错误检出 —— 是否真正抓出埋入的错误 25 无法运行,或漏掉明显的错误 抓出大部分错误,但有部分遗漏·误报 抓出所有埋入的错误,且无误报,报告以人可读的形式输出
3 AI 使用过程的透明度 —— 提示词全文与输出是否以可复现的方式记录 25 无提示词·输出记录,或仅附结果 有提示词,但缺少拒绝·修改的过程 将输入的提示词全文、原始输出、拒绝·再指示的过程按时间顺序留存
4 评审·拒绝的判断 —— 拒绝·修改了 AI 输出的什么、为什么 25 原样接受输出(无评审痕迹) 做了部分修改,但判断依据薄弱 指出并拒绝错误·幻觉·过度设计,并用自己的话说明其判断依据

评分运营备注: 第3·4项(合计50分)是这份量规的脊梁。即便工具运行得完美无缺(第2项满分),若对 AI 输出不加批判地接受(第4项欠缺),则视为未达本作业的学习目标——“人守住评审者的位置”。反过来,即使工具尚有不完善之处,只要拒绝·再指示的过程扎实,也能拿到高分。评估的不是效果(能跑出的结果)而是结构(如何驾驭),本书的这一原则在评分中同样适用。

对于期末作业,建议在上述4项之上再加 ⑤ 工作流泛化(说明如何移植到自己的领域) 1项,构成5项、每项20分的扩展量规。能否把整学期讲过的工具迁移到自己的项目中——这正是本书在最后所提出的问题,而课程的最终评估也用同一个问题就足够了。


N.5 教学运营一页速览

最后,把本附录浓缩为一页,就是这样。

这份进度表是起点,而非标准答案。请结合自己学员的水平与教学日程,调整周次、更换作业。把本书整本喂给 AI 工具,让它“按我课程的16周安排和学员水平重新编排这份进度表”——正如本书最快的用法那样——也是一条敞开的路。

后记 —— 从提问框到游戏设计室

这本书的起点,只是 LLM 的一个提问框。从"帮我整理一下策划案"这样一句话开始,AI 一步步走进了整个游戏设计室。而如今站在的这个位置,并非那场变化的终点,而是其中一个阶段的收束。


改变了的

在做了 24年游戏策划的位置上,确实有些东西变了。量产被工具吸收,会议纪要成了资产,决策也变得可以追溯。

只是,变化并没有以同样的速度抵达所有领域。系统策划和数值平衡很快就迎来了工具的吸收,而叙事与美术指导则大多还停留在保守应用的阶段。哪个领域更快并不是重点。承认"每个领域接纳变化的入口各不相同"这一事实,才是下一个决策的起点。

这并不意味着策划要做的事变少了,而是意味着在同样的时间里去做不同的事——从量产转向意义,从整理转向决策。因为工具吸收掉量产之后,那块空出来的位置并不会立刻自动被有意义的工作填满,所以会有一段略显尴尬的时期:你得先重新决定"哪些事不做"。


没有改变的

也有没有改变的东西。游戏是为人而做的,游戏的愿景由人来决定,而"做一款尊重用户时间的游戏"这个承诺,始终未变。

工具就是工具,方向在于人。只是,这一句话我打算每年都重新审视一次。因为如果不去审视,当工具变得足够强大时,一种"反正决策终归由人来做"的模糊默认,就会悄悄铺陈开来。"方向在于人"这句话听起来像是一个安全承诺的那一刻,其实恰恰最危险。


接下来

这本书是某一时刻的记录。AI 工具在快速演进,1年之后,书里的一部分内容很可能就已经过时。但我相信,那些核心模式——Layer 整合、决策追溯、验证关卡、人工评审、团队共识——即便工具更迭也依然有效。恰恰相反,工具越强,不建立在这些模式之上的用法反而崩塌得越快。

Layer 整合不只是统一各领域之间的协作语言,更是提前铺好通往程序化生成与自动化的道路。从编剧逐行注入上下文的保守应用起步,逐步扩展到系统依据世界状态自动生成的进阶应用;在这个过程中,像录音、采集、线上构建这类不可逆步骤之前的人工评审,扮演着最后一道安全网的角色。LLM 越是聪明,这套骨架的价值就越不会缩水。相反,人需要评审的那些决策,分量反而更重了。

如果你把这本书里的模式按照自己的环境加以变奏,那么这本书的下一个版本,其实就是由你来写的。根据公司规模与品类、开发阶段、团队构成,有些模式可以原样照搬,有些模式则需要重新搭建。而区分"哪些可以照搬、哪些必须重搭"这件事本身,就是第一项有意义的工作。


致谢

首先,我要向允许本书出版的 SCYBS Games 各位高管与代表深表感谢。如果没有他们那个决定——愿意为我打开一条路,让公司里自然生长出来的工作流得以与业界分享——这本书便不会存在。感谢 24年来在游戏制作这条路上与我同行、有时又各自前行的所有同事;感谢 20多年来始终陪伴在我身边的伴侣;也感谢陪我走到二十三岁的波斯猫空之(Kkongji)。对如今已不在身边、永远停留在十九岁的博美犬高美(Gomi),我也想道一声同样的谢。

我想,早来的成功成为人生毒酒的经历,一次也就够了。为了不忘记那一次,近 24年来我一直在切磋琢磨、重新学习。这本书,也更像是在这段学习的某个时刻留下的一个收束。

最后,我要向帮助完成本书写作的 AI 工具道谢。就在收尾原稿的这段时间里,Claude 的新模型 Fable 已经问世——这个领域,正一天一个样地变化着。

如果这本书能为某个人的下一个决策哪怕多添上一行,那便足够了。


李旼洙,2026年

版权页

游戏策划实务即用的 AI · Claude Code 活用法

没有一个数字是编造的 —— 一份历时六个月的实战手册:Claude Code、提示词、验证与制作记忆

这是韩语原书的简体中文版,用本书自己的 AI 工作流从韩语翻译而来,并由作者审阅。本网络版不另设 ISBN。

原书名 게임 기획 실무에서 바로 쓰는 AI·클로드 코드 활용법
作者 李旼洙(Minsoo Lee · 이민수)
原出版社 BOOKK Co., Ltd.(韩语纸质版)
原出版日期 2026 年 6 月 11 日
原书 ISBN 979-11-12-21479-9(韩语纸质版)
韩语源 https://github.com/eremes81/game-design-ai-practice

ⓒ 李旼洙(Minsoo Lee)2026

本书采用 CC BY-NC-SA 4.0 许可协议发布。允许在署名原作者(李旼洙 · 이민수)与来源的前提下进行非商业性的分享与翻译;商业性使用需另行获得作者许可。