沒有一個數字是編造的 —— 運行了六個月的 AI 工作流,連同提示詞、程式碼、驗證全部公開
24年資歷的總監在中規模(10\~50人)團隊中實際運營的304條規則·48個工具·自動化系統,原樣呈現
著 李旼洙 · 2026
本書正文的全部內容均以2026年上半年為基準。AI 工具的費用、模型與功能變化迅速,具體數值與安裝方法請在各官方頁面確認最新資訊。
本書希望被廣泛閱讀,因而免費公開。但無論是韓語原版,還是英語、日語譯本,本書的原作者是李旼洙(Minsoo Lee)這一事實,在任何地方都必須保持不變。
https://eremes81.github.io/game-design-ai-practice(作者的 GitHub Pages —— 編輯權歸作者所有的正本)您可以自由地這樣做。 個人學習、非商業性的共享與引用、非商業性的翻譯、用於公司內部學習小組 —— 但請同時標註原作者和正本連結,並在修改內容或轉譯為其他語言時如實說明這一點。二次作品也請以相同條件共享。
請先另行取得許可。 商業性的出版與銷售、用作付費課程的教材、納入公司的商品與服務,以及抹去原作者署名後的再分發。
本書所承載的知識在一冊之中即告完結,其本身是免費的。倘若它對您有所幫助、您願意支援作者,以正式電子書或付費工具包來表達心意,我將不勝感激。
這本書寫了三遍。
第一遍並不是書,而是一份公司內部手冊。在公司執行 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.1 是"第一次見面"。是坐在閃爍的游標前、敲點什麼試試的環節。但要坐到那個位置上,先得準備好一些東西。工具已經裝好,登入已經完成,大致知道費用是怎麼產生的,在黑屏前會敲上幾個字。本章比 1.1 還要靠前一步。
很多入門書會跳過這一步。只寫一行"開啟終端"就略過去了。可入門者恰恰就卡在那一行。終端在哪裡、要裝什麼、裝的過程中冒出紅字該怎麼辦——在第一行就停下的人,根本到不了 1.1。本章的目標只有一個,就是讓你不要卡在第一行。
本章分為五個部分。安裝、賬號·登入、費用方案的概念、終端生存包,以及"5 分鐘首次執行"清單。按順序跟下來,坐到 1.1 那個位置上的準備就完成了。
安裝的原則是遵循官方指引。工具經常變化,通過非官方渠道下載的安裝檔案有風險。所以本書不附下載連結,而是教你如何找到官方渠道。在搜尋框裡輸入"Claude Code 官方文件"或"Claude Code install",Anthropic 的官方文件頁面會排在最前面。安裝命令直接照搬那個頁面上的,是最安全的做法。
大方向最好先了解一下。Claude Code(本書統一用英文寫法)是一個在終端裡執行的工具,通常用一行命令安裝。各作業系統的流程略有不同。
| 作業系統 | 準備物 | 安裝流程(概念) |
|---|---|---|
| Windows | PowerShell(系統自帶) | 把官方文件裡的安裝命令一行貼上到 PowerShell |
| macOS | 終端(系統自帶) | 把官方文件裡的安裝命令一行貼上到終端 |
| Linux | 終端 | 把官方文件裡的安裝命令一行貼上到終端 |
三個作業系統的流程都一樣。"開啟終端 → 貼上官方文件裡的那一行 → 回車"。命令不需要背。從官方文件裡複製貼上才是正道。
安裝過程中即便冒出紅字(報錯)也不必慌張。入門者遇到的安裝錯誤大多是兩種之一。要麼是許可權問題,要麼是缺少前置工具(例如 Node.js 這類執行時)。如果出現了紅字,把整句話原樣複製去搜索或者問 AI,十有八九能解決。報錯資訊不是敵人,而是線索。
確認是否安裝成功的方法:在終端裡敲
claude --version並回車。如果出現一行版本號,就是安裝成功了。如果出現"找不到命令"之類的提示,說明要麼還沒裝好,要麼需要重新開啟終端。把終端徹底關掉再重新開啟,然後再確認一次吧。
裝完不等於馬上就能用。Claude Code 是一個借用 Anthropic 的 AI 模型來工作的工具,所以需要一個確認"是誰在用"的登入環節。
流程很簡單。在終端裡第一次執行 claude,就會出現登入指引。通常網頁瀏覽器會自動開啟,在那裡用 Anthropic 賬號登入即可(如果沒有賬號,可以在那個介面新建)。登入完成後,瀏覽器會顯示"現在可以回到終端了"之類的提示,終端那邊也會出現完成標誌。
入門者在這裡經常卡住的地方有兩處。
第一,瀏覽器沒有自動開啟的情況。這時終端裡會顯示一行很長的網址(URL)。把那個網址複製到瀏覽器位址列裡開啟就行。這不是卡住了,只是要手動多做一步而已。
第二,弄不清賬號型別的情況。在網頁聊天(Claude.ai)裡用的賬號,和 Claude Code 的賬號·費用是怎麼關聯的,這一點的政策可能因時期而異。遵循登入介面的指引和官方文件是最準確的。按照首次執行介面的提示一步步走,大多都能順利登入。
登入一次之後,在那臺 PC 上就會一直保持。不需要每次都重新登入。
入門者最不安的部分就是"會花多少錢?"。總有一種模糊的擔憂:是不是每敲一個字就會產生費用。先把大方向搞清楚,這種不安就會減輕。費用方式大致分兩條路。
| 方式 | 計費形態 | 類比 | 適合誰 |
|---|---|---|---|
| 包月訂閱 | 每月固定金額 | 通訊包月套餐 | 入門者·日常使用 |
| API 按量 | 用多少算多少(按 token) | 電錶 | 大量·自動化·開發整合 |
包月訂閱是按月支付一筆固定金額、在額度內隨意使用的方式。和手機包月套餐類似。每月花費相同,容易預測,不需要在意"每敲一行多少錢"。所以入門者通常以包月訂閱起步,心裡會更踏實(作者推測——具體的方案構成與額度因時期而變,請在官方費用頁面確認)。超過額度後,要麼等到下個週期,要麼升到更高的方案。
API 按量是按實際使用量(token)成比例計費的方式。像電錶一樣,用多少就計費多少。適合大量處理、自動化流水線,或者與其他程式整合的情況。用得精細就高效,但在入門階段,在對用量沒有感覺之前,成本可能難以預測。
token 是什麼、為什麼用它來計費,會在 1.2(AI 模型·token·驅動框架)裡詳細講。這裡只需記住一點。入門者通常以包月訂閱起步。因為每月金額固定,可以在沒有"用著用著會不會被天價賬單砸中"這種擔憂的情況下練習。方案名稱·價格·額度經常變化,所以本書不附具體數字。本書內容以 2026 年中為基準撰寫,費用方案·模型·功能在那之後仍會持續變化。當前的數值,在官方費用頁面確認最準確。
一句話總結:對"是不是每用一次就要花錢"的擔憂 → 包月訂閱就是每月固定。入門以包月起步,心裡會踏實。
現在來到最大的那堵牆,黑屏。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 裡雙擊資料夾或者後退是一樣的動作。
黑屏可怕的真正原因,是"敲錯了好像會弄壞什麼"的那種感覺。但上面這六個裡沒有哪個命令會弄壞東西。pwd·ls·cd 只是檢視或移動,不會刪除或修改檔案。Enter 只是執行,Ctrl + C 只是中斷。所以這六個命令隨時都可以放心敲。
有時螢幕看起來像是卡住了。敲了命令卻半天沒反應,或者游標在另一行閃爍、好像在等什麼的時候。這種時候按一下 Ctrl + C,大多就會回到原來的游標。光是知道有這個"停止按鈕",黑屏就會變得沒那麼可怕。卡住了就用 Ctrl + C 退出來,重新開始就行。
最後,當敲過的字堆了一大堆、看著亂糟糟的時候,可以把螢幕清空。Windows PowerShell·macOS·Linux 都用 clear 命令清屏。清空了也不會讓做過的事消失,只是把看得到的字整理一下而已。
走到這裡,準備就完成了。如果能在 5 分鐘內通過下面這五格,就有資格坐到 1.1 那個位置上了。哪怕只卡在一格,回到對應的那一節(1.0.1\~1.0.4)就行。
claude --version 會出現一行版本號(確認安裝)claude 時已經處於登入狀態(或按指引完成登入)pwd 看當前位置、用 ls 看資料夾內容Ctrl + C 退出來五格都填滿了的話,黑屏就不再是一堵未知的牆了。工具裝好了,登入完成了,知道了費用方式的大方向,也會在螢幕裡移動和停下。1.1 就在這個準備之上開始。坐到閃爍的游標前,第一次敲下"幫我總結一下這個資料夾裡有什麼",到那個位置上就行了。
本書前半部分(第 1·2 部分)只用自然語言提示詞就能跟下來。不過第 4 部分之後的部分章節會直接執行小的 Python 指令碼(例如 pip install pyyaml、pip 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 分鐘內整理清楚。費用先以包月訂閱起步,就能不用擔心成本、盡情練習。
黑色的畫面亮了起來。游標在閃爍。一位有著 24 年資歷的遊戲策劃坐在它面前。用 PPT 和 Excel、Wiki 和 Figma 工作了 24 年的手,在鍵盤上停頓了片刻。那有點像從前還有電腦培訓班的年代裡的 DOS。在那之後,終端這個東西只在程式設計師的桌面上才見得到。不知道該敲什麼,而且彷彿一敲錯就會弄壞什麼。這一份猶疑,正是本書的出發點。
大多數人會在這裡關掉視窗。然後在會議上一遍遍重複"我們也得做點什麼才行"。本章要做的,是不關掉那扇視窗,陪你一起坐在熬過頭 30 分鐘的那個位置上。目標不是什麼宏大的匯入戰略,而是把"在閃爍的游標面前敲點什麼、距離感就會消解"這件事,實打實地放到你手裡。
遊戲策劃第一次坐到 Claude Code 這類 AI 程式設計工具面前時,新奇與不適會在同一個位置同時湧起。這兩種情緒相互衝突這件事本身,就是匯入的第一條線索。
新奇的理由很明確。原本要花上半天的資料表一致性檢查,幾分鐘就完成了;拖沓冗長的會議記錄被彙總成決策事項的表格;一年前被埋沒的策劃案,用一句自然語言就能重新調取出來。
不適的理由也同樣明確。黑色的畫面、閃爍的游標、英文命令,跟日常的工作景象太不一樣了。遊戲策劃的一天是在 GUI 之上流動的,而往黑色終端裡敲字這件事,跟職業身份很難貼合。不過這份不適並不是工具的缺陷,而是習慣了 GUI 的人所需付出的適應成本。光是承認這一點,距離感就已經消解了一半。
本書就是一本想要縮短這份距離感的書。1.1 陪你一起坐在第一次相遇的位置上,梳理該看什麼、該嘗試什麼、又有什麼可以先放一放。
遊戲策劃比其他職能更晚才匯入 AI 的浪潮。處理程式碼的人先進去,設計師、美術接著進去。策劃則常常陷入一遍遍重複"我們也得做點什麼才行"、卻一拖再拖的模式。
拖延的理由是合理的。策劃的產出不像程式碼那樣定型化,而是文本、表格、圖示、會議、口頭共識混雜在一起。AI 輸出的可信度看起來偏低,煞有介事的謊言又很危險,而且 AI 是否真的理解遊戲系統也令人懷疑。
然而在 2024\~2026 年之間,有三件事發生了變化。
第一,AI 模型的推理能力越過了臨界點。它不再停留在簡單的句子生成,而是能處理複雜的系統設計、一致性驗證、影響分析。最新的 Claude 系列能輔助遊戲策劃工作流的相當一部分。不過這並不意味著可以把全部都交給它。驗證與責任仍然在人這一邊。(輔助的幅度因工作型別和團隊成熟度而大相徑庭——作者推測,未經驗證。)
第二,驅動框架(harness)成熟了。Claude Code 這類工具不是單純的聊天。它會直接讀寫檔案、執行命令,再把結果作為輸入接回來。這跟人工作的方式很像。
第三,記憶、atom、skill 這類運營技法已經落地。AI 不再是用一次就完事,而是有了一套方法論:把團隊的知識累積起來,讓它隨著時間推移越來越聰明。本書後半部分要講的核心,正是這種累積。
這三件事一旦匯合,對遊戲策劃來說,匯入 AI 也就成了一個合理的時間點。趁還沒更晚之前開始,是划算的。
策劃常接觸的 AI 工具有兩類。一類是往聊天框裡拋問題的聊天機器人型(ChatGPT、Claude 網頁應用),另一類是在程式碼編輯器裡做自動補全的編輯器結合型(Cursor、Copilot)。
Claude Code 屬於第三類。它在 CLI(終端)裡執行,能訪問人的整個工作環境。三類工具在哪裡分道揚鑣,用一張圖來看是這樣的。
遊戲策劃的工作不是程式碼,而是文件、表格、關係。Claude Code 的強項在於:它能看到、理解並操作人工作的整個資料夾。不必像聊天機器人那樣每次都把資料複製貼上進去。
用辦公室來打比方,聊天機器人型是前臺諮詢臺。問一次答一次,資料每次都得重新拿出來。Claude Code 則更接近鄰座的同事。它知道資料在哪裡,會用自己的手開啟檔案,把結果整理好再放回桌上。同樣是 Claude,讓它坐到哪張桌子上,擅長的事情就會隨之不同。
安裝和配置在 1.0 裡講。1.1 專注於在頭 30 分鐘裡體驗什麼能減少距離感。頭 30 分鐘分為四個區段,每個區段都會讓一部分距離感分階段地消失。
開啟終端、執行 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 個 Markdown 檔案",或者"含有'獎勵'這個詞的檔案在哪裡?"這類請求。策劃每天都要開啟"在檔案中查詢"、敲入關鍵詞、滾動結果的活兒,Claude Code 用自然語言接過來,整理好再還給你。這個工具替你幹掉每天的雜活——這種感覺會在第二個 5 分鐘裡站穩腳跟。
讓它做一點小轉換。比如"從這份會議記錄裡只提取決策事項,做成表格",或者"把這張 Excel 表的第 1\~10 行轉成 Markdown 表格"這類請求。原本用手要花 5\~10 分鐘的轉換,在很短的時間裡就被處理掉——這種體驗會帶來一種感覺:工具不止於替你搜索,更是在幫你騰出時間。
止步於此之所以危險,是因為 AI 會說煞有介事的謊話。讓它自己去質疑剛剛做出來的輸出。
幫我再確認一下剛才做的表格和原文是否一致。有沒有漏掉的條目?
於是 Claude Code 會重新讀一遍原文,把它跟自己的輸出逐行對照,報告出不一致之處。實際上在這個區段裡,經常會出現這樣的畫面。
幫我再確認一下剛才做的表格和原文是否一致。有沒有漏掉的條目?
我把原文(meeting_0603.md)和剛才做的表格重新對照了一遍。
找到了 3 處不一致。
⚠ 遺漏 "教程獎勵下調 50%" —— 在原文第 12 行,但表格裡漏掉了
⚠ 筆誤 負責人 "金某某" → 表格裡誤寫成了 "李某某"
⚠ 模糊 "獎勵再議" —— 是決策還是擱置,原文表述不清。
我擅自把它歸入了'決策',但需要確認。
要我重新做一張修正後的表格嗎?模糊的那條要怎麼處理,
告訴我後我會照著改。
工具懂得質疑自己的輸出,並且這份質疑是和人一起來做的——這是最後 10 分鐘的核心。像第三條那樣,反過來問"我是擅自判斷的,請你確認一下"的這種態度,正是把驗證留在人手裡的安全閥。
30 分鐘過去,距離感的 80% 已經消失了。剩下的 20% 會在接下來的各章裡慢慢減少。
作者作為設計總監運營的某個 MMORPG 專案(以下稱"專案A"),已與策劃團隊(4\~5 人)用以 Claude Code 為中心的工作流運營了約 6 個月(專案A 的整個開發團隊為中等規模,10\~50 人)。這裡轉述幾個景象。
只把最抓得住手的一件拿出來,按實測來算。這是橫跨 30 多張資料表的 FK(外部索引鍵)一致性檢查。它是用人眼去追蹤"一張表的 ID 在另一張表裡是否被正確引用"的工作,表越多,組合就以幾何級數膨脹。
從半天到 5 分鐘。這一條我不會去做一般化。其他工作的節省幅度更小,或者會新添出審閱時間。同樣這 6 個月裡看到的其他景象,只用方向和比例來記。
每個工具一旦做成,6 個月裡累積下來節省的時間,按作者的體感不是以人-周(person-week)計,而是以人-月(person-month)計(精確的合計未測量,為估算值)。靠那些時間,得以專注於更深的策劃。
這類工具一旦做成,就能長久地幹活。不過'長久'並不等於'無人'。要運營它的人和驗證結構一起配齊,才能長久;只留下工具而人走了,兩個季度內就會腐壞。本書後面的各部分,會講上述每個工具是如何被做出來、又如何運營的。
來坦誠地點一點遊戲策劃在 AI 工具面前常有的恐懼。不迴避而是去面對,是匯入的第一步。
"AI 會取代我的工作"這份恐懼,對了一半,也錯了一半。單純的雜活(一致性檢查、文件轉換、搜尋)會被 AI 取代,但決策、優先順序、玩家情緒的設計,它取代不了。反倒是善用 AI 的策劃,從雜活中解放出來,專注於本質。不妨自問:"我的工作裡,雜活和本質的比例是多少?"如果雜活佔 70%,那本質的 30% 原封不動還是你自己的,而那 30% 變得更重要,才是關鍵。
"AI 錯了,責任誰來擔"這個問題也經常出現。對策劃的決策所負的責任,永遠是策劃自己的。不經驗證就照搬 AI 的輸出,那是策劃的失誤,而不是 AI 的失誤。把驗證流程一併設計進去,是匯入的一部分。1.1.3 最後 10 分鐘裡看到的'讓它質疑自己的輸出',就是這套流程裡最小的那粒種子。
"不太懂程式碼所以用不了"這份恐懼很快就會解開。Claude Code 用自然語言執行,不懂程式碼就以不懂的樣子起步即可。因為會和 AI 一起讀它寫出來的指令碼,幾個月下來,簡單的指令碼就能讀懂並修改了。學習會自動地跟上來。
"工具變得太快"也是常見的擔憂。要把模型、功能、趨勢全都追上,人會累垮。只把對自己工作流有幫助的 1\~2 個功能深入學透,其餘的等需要時再看。
先打個預防針。即便 1.1.3 的回應畫面看起來很光滑,實際的頭 30 分鐘裡,會混著答偏的回答、張冠李戴的檔案概括、卡頓的輸出。那是正常的。本書講的不是光滑的成功故事,而是更多地講:當輸出答偏了,要怎麼重新請求把它糾正過來。
本書由 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 是一章縮短距離感的章節。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 分鐘內也能切身體會到。
這是某次做完一件任務、檢視用量時的事。這周的五份會議記錄堆在資料夾裡,我得在週一上午站會之前把"已確定的內容"整理成一頁。我在 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 就是一例。
上面那行灰色字不是隨機日誌,而是一個固定迴圈中的一格。驅動框架(harness)所做的事歸根結底就是快速地轉同一個圈:把檔案讀出來餵給模型,模型說"執行這條命令"就去執行,再把結果重新餵給模型。這個圈一直轉到任務結束為止。
在這張圖裡,人動手的格子只有最上面(指令)和最下面(確認驗證結果)兩處,中間的圈由驅動框架自主運轉。讀五份會議記錄時灰色字閃了五次,就是把 Read → Inject 這格轉了五圈。換成網頁聊天,你得親自開啟五個檔案複製·貼上。驅動框架替你幹掉這份勞動——這正是把聊天機器人和 CLI 型驅動框架分成兩種不同工具的決定性差別。
迴圈每轉一圈,就在 Inject 格里把累計 token 求和。所以不先理解 token,這個迴圈的成本和上限都看不見。先從 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 注入,是本書工作環境裡實際運轉的裝置。一旦輸入一行字,inject_memory.py 鉤子就按分數高低匹配記憶體 atom,只挑出排名前幾條來注入,即便失敗也不阻斷工作流(實現細節在 1.3 詳述)。"只取需要的資料、只取前幾條、即便失敗也安靜"這條 token 節約原則,原樣寫進了一個程式碼檔案裡。
模型就像汽車引擎,可以在 Claude Code 這同一車體上換裝 Opus·Sonnet·Haiku 這些不同的引擎。換了引擎,任務的性格就變了。
對照策劃工作來看,大致這樣劃分。像系統設計評審、整合多份資料合成 GDD(Game Design Document,遊戲設計文件,即詳細規格書)初稿這類需要深度推理和一致性的活,交給 Opus;像會議記錄決策抽取、每日摘要這類大多數日常工作,交給 Sonnet;像資料表的簡單格式轉換這類幾乎不需要判斷的活,交給 Haiku。前一節的會議記錄任務跑在 Sonnet 上,也是按這個標準——挑出決策搬進表格,比起深度推理,更講究均衡與速度。
引入初期人人都會掉進一個陷阱:想把所有任務都跑在最好的引擎,也就是 Opus 上的衝動。順著這股衝動走,成本·速度的負擔很快會轉成運營負擔,而"讓模型去匹配任務"的手感也立不起來。運營真正的本事不是每次都在腦子裡挑模型,而是在模式成型後用自動化把它固化下來。
這類固化,就把模型明確寫進 settings.json 或斜槓命令裡(1.3 詳述)。一旦固化,每次挑選的工夫就沒了。
模型大約每半年出一個新版本,即便名字相同,4.5 和 4.6 也不一樣。新版本出來時,只拿工作流中最核心的五項任務、用同樣的輸入做對比。要全部測試會累垮。光看這五項的結果差異,就足以判斷要不要切換。
前面說過,迴圈每轉一圈,就在 Inject 格里累積 token。那份累積撞上的天花板就是上下文視窗,也就是模型一次能處理的 token 最大量。拿人來比,就是工作記憶(working memory)。
前面那個會議記錄任務,累計輸入是 1 萬 1 千 token 級,相對 200K 天花板只佔 6% 出頭,很寬裕。但若不切換任務、在同一窗口裡把會話拖得很長,就會逼近天花板。一旦裝滿,舊內容就被截掉,模型開始丟失前半部分的"記憶",自動壓縮隨之觸發,先前的對話被替換成摘要本。
治理這個天花板的習慣有四條。
| 模式 | 何時 |
|---|---|
| 會話分離 | 轉向別的主題時,新開一個會話 |
| 顯式壓縮 | 一件任務結束後,只留核心再壓縮 |
| 記憶體外接 | 常用資料拆成 atom,用 JIT 隨時注入 |
| 上下文視覺化 | 用 /context 用眼睛確認當前用量 |
遊戲策劃常碰到的吃重場景,是會議資料·策劃案·資料表同時需要的任務,這時 1M 選項就有用。不過 1M 伴隨成本·速度負擔,所以平時 200K 就夠,只有資料包真的很大時才動用。
回到迴圈圖最下面的 驗證通過? 格。沒有這一格,模型那套煞有介事的謊話就會原樣存進檔案。模型有時會自信滿滿地把錯誤答案當成正確答案丟擲來(幻覺,hallucination),這種頻率隨代際上升而下降,但不會歸零。所以把驗證設為常設的一格。
策劃中危險的幻覺很具體:引用一個根本不存在的資料表列,用錯誤的公式算數值,或者把會議上沒敲定的事當成已確定的來摘要。會議記錄任務裡最可怕的是第三種——"只討論、暫時擱置的事項"悄悄爬上了決策表。
驗證有五種模式。
不必每次五個全做,而是按任務的風險度挑 1\~3 個。會議記錄任務裡,把第 4 項 Linter("決策是否齊全包含主體·內容·期限")和第 3 項抽樣評審做一次捆在一起。前面 token 表最後一行"驗證再輸入 320 token",正是 Linter 揪出遺漏、向模型回問的那一來一回,換成迴圈圖就是 驗證失敗 → Inject 又多轉了一圈。
每次都靠人全檢,引入效果就減半,所以驗證本身也是自動化物件。會議記錄決策抽取由 Linter 檢查格式遺漏,資料錶轉換檢查行數·合計·外部索引鍵一致性,GDD 自動生成檢查核心章節遺漏。通過的人就不用看,只看沒通過的。這就像在塞滿櫃子的檔案裡,只把貼了紅標的資料夾拿到手中的畫面。讓人的視線只落在危險的地方——這正是驗證自動化的目的。
現在把那次會議記錄任務從頭到尾捋一遍,看四個詞如何被串進一行。
| 格 | 幹什麼 | 對應哪個概念 |
|---|---|---|
| 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 分鐘,而是那道視線落點變了。
"Opus 總是更好"最常見。若無視成本·速度確實如此,但對簡單任務而言 Opus 是浪費。按任務匹配才是答案。
"1M 上下文總是必需的"也常出現。大多數 200K 就夠,1M 伴隨負擔,所以只用在資料包真的很大時。
"驗證是人來做的"只對一半。可自動驗證的部分佔多數,人則專注剩下的。
"token 不用操心"在個人工作裡某種程度上行得通,但多人一起用時,累計成本會迅速變大。從一開始就把視覺化·節約模式固定下來更穩妥。
"驅動框架沒有差別"也意外地多。即便是同一個模型,是聊天機器人還是 CLI,也會分成不同的工具。前面看到的有沒有複製·貼上勞動,就是那個差別。
用一件小任務,親手把本章的四個詞跑一遍。
setup
prompt
從這個資料夾的筆記裡,只挑出"已確定的內容",
做成主體·內容·期限 3 列的表格。
擱置·討論中的事項剔除,併為表中每一行
標上它來自哪個檔案的檔名。
verify
/context,看看這個會話用了多少 token(你會發現輸入比想象中大)。單人精簡版
如果你剛開始用工具,上面只需抓住兩件事。第一,把筆記整個資料夾交出去,別親自複製·貼上(交給驅動框架迴圈)。第二,輸出表一律連同檔名一起拿到,只對可疑的行開啟原文看。模型選擇或 token 視覺化,等熟練了再加也不遲。不用手搬資料、把輸出與原文對照,光這兩個習慣,引入就立穩了一半。
我開啟一個新會話,輸入"來看一下技能冷卻時間的數值平衡吧"。在按下回車之前,螢幕下方有一行灰色小字一閃而過:[memory injected: 2 atoms, 1,842 chars]。我並沒有開啟任何檔案,這意味著上週固化下來的冷卻時間規則文件,已經被附加到了模型輸入的前面。這就是搭好基礎設施的工作環境發出的第一個訊號。開啟工具的那一刻,工具已經記得我。
要讓這個畫面成立,需要三樣東西提前各就各位。AI 記住什麼(記憶)、AI 在沒有人工批准的情況下能做什麼(許可權),以及開關這兩者的中央開關(settings.json)。第一次安裝最多花一個小時,而這一個小時會化作此後 6 個月裡每天省下的時間回到你身邊。這是一筆幾乎能全額收回的投資。
本章是一段實地走查(walkthrough):依次展開作者在個人 PC 上實際執行的那一行 settings.json、它所呼叫的 inject_memory.py,以及那個檔案讀取的 _jit_manifest.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 所有行為的中央檔案,分為兩層。
~/.claude/settings.json —— 全域性。對所有會話生效。可與團隊共享的配置。~/.claude/settings.local.json —— 本地。僅在這臺 PC 上生效。個人 PC 的特殊配置。兩者會合並後生效。所以作者把團隊需要共享的 hook、許可權放在 settings.json,把僅在這臺家用 PC 上使用的絕對路徑或個人工具路徑分開放在 settings.local.json。這樣的分離既能在協作時避免 git 衝突,也能防止個人配置洩漏到團隊倉庫裡。
除了 hook 之外,還有幾個經常會用到的條目。
effortLevel —— 模型的推理深度。low / medium / high。像策劃案設計這種需要深度判斷的工作,就設為 high。permissions —— AI 在沒有批准的情況下可以執行的命令範圍(1.3.4 詳述)。enabledPlugins —— 已啟用的外掛列表。這裡有一個最重要的運營習慣。settings.json 哪怕只有一個小小的筆誤,都會讓工具本身起不來。JSON 裡少一個逗號,解析就會崩。所以修改前備份是必須的。作者的 PC 上實際就留有這樣的備份檔案。
settings.json.bak_2026-05
settings.local.json.bak_2026-05
用日期字尾備一份,回滾只要 1 秒。用 git 管理就更好了。就像抽屜裡的一把舊鑰匙,平時用不上,但在鎖著的門前,總會有非用一次不可的那一刻。
現在進入 settings.json 呼叫的指令碼內部。這是走查的脊柱。程式碼只有一百來行,但核心是五個動作。
把這五個動作展開來說就是這樣。
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.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。
name —— atom 的唯一名稱。path —— 一旦匹配就去讀取正文的檔案路徑。regex —— 定義哪些關鍵詞出現在輸入裡時會喚醒這個 atom 的模式。score —— 排序優先順序。越高就越先被匹配、越先佔到位置。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 在需要時喚醒。
接下來是基礎設施的第三根支柱,許可權。Claude Code 可以刪檔案、執行命令、呼叫外部 API。強大與危險結伴而來。許可權系統管理著這份危險。
許可權分兩類。無需人工批准就自動執行的,和每次都要拿到批准的。哪一類放什麼,由 settings.json 的 permissions 塊定義。
{
"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)文件化——常用的工具,正是常常被允許的許可權。
許可權運營中會沉澱下四種模式。
rm -rf、git push --force 這種一次事故就致命的命令,寫進 deny。哪怕自動允許越擴越廣,這兩個也絕不去碰。allow,把已經不用的許可權去掉。痕跡只堆不清,就成了噪聲。每次都彈出批准彈窗,人會累。也有減輕疲勞的裝置。可以用 fewer-permission-prompts 之類的斜槓命令把頻繁出現的模式批次登記,或在一個會話內給出臨時允許,或僅在個人工作中使用全權自動允許模式。不過最後這個選項,在團隊環境裡不推薦。
疲勞感與安全之間的平衡,由本人來調。太嚴格工作轉不動,太寬鬆會出事故。哪怕寬鬆地起步,只要備好季度清理的週期,平衡自然會找到位置。
到目前為止看到的三根支柱(settings·記憶·許可權)在一個會話裡如何同時運作,以一行輸入為基準展開來看。下面是輸入"來看技能冷卻時間的數值平衡"時實際發生之事的一個剖面。
三條泳道在同一次輸入上交會。settings.json 喚醒 hook,hook 挑出記憶附到輸入上,由此生成的響應在呼叫工具時,許可權作為最後一道關卡發揮作用。使用者明明只敲了"來看冷卻時間"這一行,三套基礎設施卻在看不見的地方依次幹活。這就是工具讓人感覺成了"我的工具"那一刻的內部結構。
理論看過了,就動手。第一次裝好 Claude Code 之後,一個小時就能把上面那整張圖鋪到你自己的 PC 上。分成五個區間。
0\~10 分鐘,確認安裝·執行。 安裝後在終端裡啟動 Claude Code。在某個資料夾裡問一句"這個資料夾裡有什麼?",確認能得到響應。先看工具是不是活著。
10\~25 分鐘,寫好三個全域性記憶。 自動載入的全域性,三個檔案就夠了。
MEMORY.md(5 行)—— 本人身份一行 + 通向其他檔案的指標。user-profile.md(20\~30 行)—— 姓名·角色·專長領域·聯絡方式。feedback-collaboration-style.md(20\~30 行)—— 語言·語氣·執行優先·說明簡潔這類協作規則。照著作者的樣例原樣抄來起步也好。在運營中慢慢打磨即可。
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 鋪上去就行。
引入初期反覆出現的錯誤歸為五類,而每一類都站在同一個事故成因之上。
| 錯誤 | 事故成因 | 規避法 |
|---|---|---|
| 往全域性塞太多 | 所有會話都變重、浪費 token | 全域性控制在 5KB 以內,細節轉移到專案記憶 |
| 把所有許可權設為自動允許 | 便利遮蔽了危險的第一現場 | 只把讀取·檢索設為自動,寫入·刪除需批准(含季度清理) |
| 不備份就改 settings | 損壞的 settings 讓工具本身起不來 | 改動前自動儲存 settings.json.bak_<日期> |
| 把 atom 無限堆進記憶資料夾 | 自動載入逼近 token 上限 | 約從 50 個起引入 JIT manifest |
| 團隊·個人配置混在一個檔案裡 | git 衝突·個人配置洩漏 | 團隊用 settings.json,本人用 settings.local.json |
這五條沒必要從第一天起就全部規避。全域性膨脹和漏備份,最好在頭一個小時內就把規避模式立住;其餘三條,則在運營一個月左右、於本人最可能出事故的位置上裝上規避裝置,會更自然。
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 個。
里程碑構建前一天的晚上,系統策劃團隊成員 A 通過即時通訊工具問我:"這周改過獎勵曲線的文件有幾個?評審到哪一步了?"我答不上來。文件散落在某個資料夾裡,誰最後改過它、它屬於哪個里程碑,這些資訊分散在各自的記憶和檔名約定裡。那天晚上我們做的事,就是定下一條約定——在文件第一行寫上六行字。正是那六行字,讓從下一個里程碑起,團隊成員 A 的問題不必有人開啟資料夾就能回答。
文件最上方 --- 之間寫的幾行 YAML,就叫前置後設資料(frontmatter)。這條約定讓人和機器無需讀正文一個字,就同時知道"這份文件是什麼"。本章會用一段真實執行的指令碼,跟蹤這一行字如何成為整個資訊架構的入口座標。
先只交代一個術語。本書把策劃文件分成五個 Layer(第 6 章正式展開):L0=世界觀·概念,L1=系統規則,L2=內容,L3=資料,L4=實現座標。自上而下依賴是正常方向。下文出現的 layer: 2,就是"這份文件屬於內容 Layer"的座標宣告。
傳統的策劃文件一直活在 Word、PPT、Google Docs 上。正文是為人閱讀而最佳化的。可是文件的型別、責任、狀態、位置這類元資訊,要麼融在正文裡,要麼依賴資料夾結構和檔名約定。於是要知道"這份文件屬於哪個里程碑、誰是責任人、上次評審是什麼時候",就得開啟正文看。
這裡疊加了兩重侷限。第一,文件不會自己說明身份。身份藏在人的記憶和資料夾約定裡,而那套約定隨時間腐化。第二,AI 沒有線索去推斷上下文。你對 Claude Code 說"幫我評審這份文件",它會把正文從頭讀到尾、浪費 token,也不知道責任邊界到哪裡。
YAML 前置後設資料一次解決這兩點。在文件第一行明確地寫入後設資料,人和機器都不必開啟正文就能識別文件。這就像櫃子抽屜正面貼了標籤,不拉開抽屜也知道里面裝什麼。而且這枚標籤不止是分類工具。後文會看到,僅 layer 這一個欄位,就成了程式化生成與自動驗收的入口座標。
不用抽象示例,我們直接看專案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: 2 和 affects: [L3_BalanceSheet_v2] 這兩行。它宣告的是"這份內容(L2)文件會影響資料 Layer(L3)的配置表"。僅憑這一點,工具就能不讀正文、把 L2→L3 的依賴關係畫成圖。反過來,如果一份 L3 資料文件用 depends_on 去引用 L1 系統規則(自下而上的反向依賴),那就是設計上的壞味道。工具會自動檢出這種反向引用。
YAML 比 JSON 更易於手寫,原因很簡單:用縮排表達結構,幾乎不需要引號,還能寫 # 註釋。適合策劃親自填寫。
_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)歸在同一序列裡管理。
這裡發生了一個重要的轉變。如果標準只是一份給人讀的文件,人就會違反它。而把標準做成機器讀取的資料,機器就會強制執行它。下一節就是這一轉變的真實程式碼。
實操記錄(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 只是每天早晨把這個事實擺出來而已。
把上面的實操記錄壓縮成一張流程圖,就是下面這樣。它展示了人寫下的一行字,如何一路流向機器的檢查關卡。
關鍵有兩點。第一,標準(E)與指令碼(D)是分離的。改了標準,不改程式碼,檢查基準也隨之改變。第二,違例(H)不是死路,而是回到撰寫階段(B)的迴圈。這不是責怪人,而是把本人文件退回去讓本人自己改。
作者作為總監運營的專案A,約在 6 個月前把前置後設資料引入了整個策劃團隊(4\~5 人)。引入不是一蹴而就,而是經過了四個節點。
引入第 1 周最大的牴觸是"這玩意兒每次都要手寫?"。每份新文件都要揹著寫六行,確實麻煩。解法是模板自動插入。VSCode 程式碼片段、Obsidian 模板、策劃門戶的"新建文件"按鈕,會自動塞進一個空的 YAML 塊。作者只填空值。牴觸在一週內消失了。
第 1 個月爆發了標準衝突。多人各自隨意新增欄位,owner·responsible·author 同時出現。明明是同一個概念,卻有三種寫法,搜尋和自動化都崩了。解法是用 _NAMING_FRONTMATTER_STANDARD.md 一份文件整理所有欄位的正式名稱、允許值與示例,並把新欄位的新增規則化為必經此文件。標準在一個月內穩定下來。
第 3 個月,2.1.4 裡那個 Linter 進來了。哪怕有標準,人也會違反。所以讓每天早晨自動生成一份一致性報告,落到團隊即時通訊工具的公共頻道里。責任人只看自己的文件就行。自動化之後,標準違例明顯減少(作者估測,非精確測量值——體感大致降到一半以下)。
第 6 個月,與 AI 的結合大放異彩。標準穩定後,下面這類提問都能即答返回。
related·affects 圖自動生成最終,前置後設資料成了人與 AI 之間的公共詞彙。人寫,AI 理解;AI 寫,人驗證。兩邊看同一批 key。只不過第 1 周的牴觸、第 1 個月的衝突、第 3 個月的 Linter、第 6 個月的結合——這是累積 6 個月才形成的結果,不是一蹴而就的。
匯入初期反覆出現的錯誤,可歸為五類。它們都站在同一個根上——"把標準只交給人的意志的那個地方"。
| 錯誤 | 事故原因 | 規避法 |
|---|---|---|
| 一開始就定義太多欄位 | 作者填空值填到累,質量下降 | 從核心 6 個起步,1\~2 個月後只補常用的 |
欄位名不停改(tag→tags→category) |
舊名殘留在累積文件裡,搜尋·自動化崩壞 | 改名時同步遷移指令碼。發現舊名時自動轉換或告警 |
| 每次都人手寫 | 拼寫錯誤·欄位缺失·日期寫法分歧(2.1.4 那個 bug)成了家常便飯 | 優先模板·程式碼片段·"新建文件"自動化。人手只用在有意義的值上 |
| 隻立標準、不驗證就放任 | 哪怕有標準也不知道誰違反了,自然腐化 | 用 Linter + 每日自動報告,讓違規者本人去改 |
忘了 layer 欄位 |
沒有 Layer 座標,跨領域可見性與驗收關卡都建不起來 | 把 layer 強制為必填欄位。Linter 檢出缺失 |
五個錯誤不必從第一天就全堵上。第 1·3 項在匯入第 1 周就把規避模式立好,第 2·4·5 項則邊運營、邊從本團隊最常撞上的地方開始依次嵌入,這樣更自然。
前置後設資料的匯入,意外地是項輕活。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 天的文件"。僅憑這一點,"我評審到一半就忘掉的文件"每週就會浮上水面一次。標準-模板-檢查的三角形,在單人規模下也照樣運轉。
layer 一個欄位,就成了程式化生成與自動驗收的入口座標新人入職的第一週,他在聊天裡問我:"戰鬥冷卻時間是 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。
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 讀作"獨自也說得通的單元"。
檔名是 combat_cooldown_rule_v2。這不是隨手起的名字,而是有三段式結構。
combat_ cooldown_rule _v2
└ prefix └ 決策正文 └ 版本
(哪個領域) (關於什麼的決策) (第幾次修訂)
prefix combat_ 是"這是戰鬥領域的決策"這一座標。專案A的規則 atom 以 prefix 區分領域:quest_(任務)、data_(資料運營)、docs_(文件運營)、meeting_(會議記錄)、portal_(策劃檢視器)。光看 prefix,就能抓住這個決策屬於誰的責任範圍、會從哪裡受到影響。
命名一旦動搖,一切都跟著動搖。同一個決策若以 skill-cooldown.md 和 cooldown_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 的內容"。名字,就是那套自動化的定址體系。
正文上方 --- 之間的 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%,就讀作廢棄工作流沒有運轉的訊號。
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,會變成誰也不敢動的化石。
atom 最下方的 Relations 小節,把這份檢材從一張孤立的便籤,變成圖中的一個節點。關鍵不在於只寫"相關文件",而在於明示關係的種類。
六種關係各自做著不同的事。
derives_from:這個決策派生自哪條上層原則。冷卻 0.6 秒,是"決策負擔減輕"這條原則的具體化。affects:這個 atom 一旦改動,什麼會受影響。把 0.6 秒改成 0.5 秒,DPS 計算和平衡曲線就會動搖。改動前就能自動拉出影響範圍。requires:這個決策要成立,什麼必須先存在。沒有輸入緩衝系統,全域性冷卻就會把輸入吃掉。conflicts_with:與什麼矛盾。它與舊版技能取消規則衝突,這條 link 就是"兩者必須廢其一"的訊號。is_a / part_of:分類(rule)與歸屬(combat_system_master)。圖的骨架。若只是簡單的 "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),這才成為可能。
五個部位都齊備的 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 稱作"資產"而非"整理"的理由。
至此,我們對一個 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
}
實際注入是這樣流轉的。
關鍵在最後一格。AI 不會只答"曾經是 0.6 秒"。它讀了 atom 的 Why,就給得出依據;讀了 Relations 的 affects,就連改動時會動搖的物件(DPS 計算、平衡曲線)也提前點到。切細、寫明理由、標註關係的五個部位,全都在回答裡活了起來。
到這裡,單一決策原則是自動化前提這一點就顯露了。假如這個 atom 是 220 行的整合 GDD,"冷卻"一詞被匹配的那一刻,職業設計、傷害曲線、UI 就會被整塊注入,token 預算被削減,AI 也會在五個決策中迷失該答哪一個的焦點。atom 越小越清晰,JIT 準確度越高。切得細,不是整理的美德,而是自動注入的前提條件。
score 是守住上下文預算的裝置。一次輸入匹配到多個 atom 時,只注入 score 排名前 N 個(預設 3 個)。打分標準在運營中定。
解剖過的檢材 combat_cooldown_rule_v2,是拿到 status: approved 的團隊共享 atom。並非所有 atom 一開始就坐到這個位置。專案A把 atom 分為兩層。
分層的理由是心理上的。個人 atom 自由,才能毫無負擔地寫下驗證前的假設,並在一週後廢棄。如果一開始就對團隊公開,就會因"這要是錯了怎麼辦"而乾脆不寫。反過來,團隊共享 atom 嚴格,全員才會信任並引用。
combat_cooldown_rule_v2 起初恐怕也只是個人 atom 裡"冷卻 0.6 秒,試試看吧"的一行備忘。在 Alpha 版本中驗證過後,以變更請求的形式晉升為團隊共享,再經另一位策劃評審,成了 approved。這套個人→團隊的晉升流程本身,就是 atom 系統隨時間變聰明的 self-improving 迴圈的一條主軸。
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 種,在運營第三個月時跑一次季度排查,自然就會對齊。
本章把一個 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)一路延伸的資訊架構骨架中,本章是其第二個關節。
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、沒有構建流水線的單人開發者,可以把本章整章壓縮成一個筆記應用的資料夾。
domain_decision_v1 這一條規則。Linter 由你自己的眼睛來代替。→ 影響:、↑ 依據:、✕ 衝突: 三個標記,影響追溯的九成就保住了。核心不是工具,而是五個部位的習慣。最初的 10 條筆記最難,熬過那個坎,接下來的 100 條,手會自己造出來。
那是分工從三個增長到八個的時段。戰鬥策劃把技能攻擊距離定為 8m。同一周,關卡設計師把副本通道寬度鎖定為 6m。兩者在各自分工內都是完全合理的決定。問題在三週後的版本里暴露出來。範圍技能穿過通道牆壁打出去,敵人在玩家根本看不見的地方被打死。這不是任何人的失誤。只是兩個人沒有一扇窗戶能看見彼此的決定而已。
本章講的就是造出這扇窗戶的故事。讓每個分工保留自己的房間,同時只憑一個座標就能知道隔壁房間裡在發生什麼。這個座標系,就叫作 Layer。
遊戲策劃的分工被切得很細。系統、戰鬥、敘事、內容、關卡、數值、UX、QA。每個分工都有自己的工具、產出物、會議。規模越大,各自越是深入自己的領域,陷入不知道別的分工在做什麼的狀態。這就叫作孤島(silo)化。
孤島化的代價要等時間過去才會顯現。
原因不是能力不足。各自在自己的分工裡做了合理的決定,只是沒有一條通道能讓人察覺別的分工的決定而已。用會議來填補,會議就會暴增;用群聊來填補,訊號就會被噪聲淹沒。這並不是說會議和群聊毫無價值,關鍵在於把"能填補的部分"和"不能填補的部分"之間的邊界劃清楚。
解決辦法是:既不收窄各自的領域(保持分工分化),又能讓彼此看見對方的流程(統一可見性)。這兩個看似衝突的訴求,只要對齊到同一個座標系上,就能同時達成。那個座標系就是 Layer。拿辦公室來打比方,就相當於每個人都有自己的桌子,同時看著同一面掛鐘和日曆。
本書使用的 Layer 是 0\~4 的 5 層抽象。越往上越抽象、越少變更;越往下越具體、變更越頻繁。
五個層各自在程式化生成·自動化管線中所承擔的角色,寫在上圖右側的標籤裡。這個對映是本章的脊柱。如果只把 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。
分工橫向鋪開,Layer 縱向堆疊。一個分工的工作橫跨多個 Layer。下面的矩陣用單元格的顏色深淺,表現 11 個分工(橫軸)× Layer 0\~4(縱軸)的分佈重心。深色格就是那個分工的重心 Layer。
縱向讀,能看出一個分工橫跨哪些 Layer;橫向讀,能看出一個 Layer 聚集了哪些分工。L0(願景)這一行,敘事和美術指導最深 —— 這是離願景最近的兩個分工。L3(資料)這一行,系統·戰鬥·關卡·數值·角色聚得很深 —— 這是它們在配置表裡彼此碰撞的訊號。
只要明確地擁有這份分佈,別的分工就能立刻知道"得去看戰鬥的 Layer 2"的位置。這不是孤島的牆被推倒,而是在牆上鑿出了一扇窗。
把整個矩陣壓成一句話就是這樣:縱軸 Layer 是為了把生成自動化而分,橫軸分工是為了發揮專業性而分。兩者在網格的某一格里相遇。
筆者作為設計總監運營的 MMORPG 專案A,與策劃團隊(4\~5 人)一起把 Layer 系統運營了約 6 個月(整個開發團隊屬於中等規模,10\~50 人)。看看具體案例。
先看敘事 5 層。敘事策劃資料夾本身就按 Layer 做了分割。
敘事作者在 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.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、判定方向就要花好幾分鐘。當文件增加到數百個,實際上就不可能了。反向引用總是一兩個一兩個地悄悄混進來,過了很久才在版本里爆掉。
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 的發展抬高了程式化生成·自動化的可實現性 —— 貫穿本書全書的這條元資訊,就在這裡。
本書的各分工部分,會在匯入處明確各分工主要分佈在哪個 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)最近的兩個分工是敘事和美術指導。這些相鄰關係在座標系上自然地顯現出來。
要想一開始就完美地引入 Layer 系統,就連開始都做不到。漸進地引入才是正解。
L1_·L2_·L3_ prefix),把關係圖·一致性檢查自動化。每個階段至少一個月,長則以季度為單位。硬推,人就會累。把運營負擔控制在不超過引入價值的範圍內、調節速度,這是總監的工作。
小規模(\~10 人)走 1\~2 階段,中規模(10\~50 人)走第 3 階段,大規模(100+)要走到第 4 階段才會出效果。這並不是說小團隊用不了。只是深度不同,核心價值在第 1 階段就已經開始了。
Layer 不是單純的資料夾整理技巧。它是把分化的遊戲策劃捆進一個座標系、讓 AI 能夠推理的元原則,進而是各分工程式化生成·自動化的共通前提條件。
本書其餘所有部分都以本章為前提。各分工部分會在匯入處明確各分工在 Layer 中佔據的座標,流程部分講橫跨 Layer 的運營系統,運營部分講 Layer 系統本身的 self-improving 迴圈。
下一章(遊戲本體論與知識圖譜)在 Layer 之上疊加語義關係。如果說 Layer 是座標,那麼本體論就是座標之上的語義箭頭。兩者合在一起,AI 才終於能自主推理"這個文件會影響那個文件"。
有一點要說清楚。本章的任何自動化都沒有代替決定。在反向引用檢出中,機器只是把違規候選鋪開,選擇接受什麼、接受到哪一步的,是人的手。Layer 是幫助人更快做出更好決定的座標系,不是把決定甩出去的裝置。
layer_unified_design_philosophy —— 本章的母體 atomdocs_layer_numeric_prefix_naming —— 檔名 prefix 強制規則dead_table_5layer_cleanup —— 5 層之外的表清理規則portal_layer_change_impact_check —— 變更影響自動檢查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 層也行。核心不是數字,而是"明確標出層級"這一行為。
週一上午,一條變更請求被提了上來。戰鬥團隊的成員 A 在團隊即時通訊工具裡寫下一行字:"我想把全域性冷卻從 0.5 秒改成 0.3 秒。有誰受影響嗎?"換作平時,從這裡開始就是一場半小時的會議。負責傷害計算公式的人舉手,負責連招取消規則的人插話,還有人問"Boss 的招式會不會也受影響"。沒有人把整張圖都裝在腦子裡,於是會議被翻找記憶這件事填滿。
可這一次不一樣。請求提上來 1 秒後,機器人自動加了一條評論:"修改這個 atom 會影響 4 個 atom。skill_dps_calculation、combat_combo_cancel_v3、refgame_boss_pattern_phase2、balance_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.1 裡,我們用 YAML 前言(frontmatter)附上了後設資料,又在 atom 正文裡撒下了 wikilink。僅憑這些,文件就已經像網一樣連線起來了。問題在於,這些連線沒有寫明它意味著什麼。
這個決策成立於 [[skill_cooldown_rule_v2]] 之上。
這一行只說到"提及了 skill_cooldown_rule_v2"。為什麼提及?是這個決策需要那條規則(requires),是從那條規則派生而來(derives_from),還是與那條規則衝突(conflicts_with)?人讀句子就明白,機器卻不知道。就算問 AI"開啟這個決策會不會有什麼東西被破壞",僅憑沒有語義的連結也答不出來。
所以要給 wikilink 套上關係型別。遊戲策劃中實際用到的關係,出乎意料地少。下面這六種就覆蓋了 90% 以上。
把這六種用 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,同步負擔就會翻倍,而一旦只改一邊、忘了另一邊,圖譜就開始說謊。一個方向靠人手,反方向靠工具,這是貫穿本章始終的原則。
從這裡開始,我把實際發生的一次會話整段照搬過來。"實操記錄"是本書的專有詞,指完整保留的真實操作過程記錄。情形是這樣的:把連招取消規則從 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 存活
我讀取了 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_v1以conflicts_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 做到"這裡斷了"為止,人則做到"為什麼斷、怎麼修"為止。
前一節的檢查也可以每次都用提示詞來跑,但把同樣的檢查用程式碼固化下來,就能在圖譜上一目瞭然。專案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 建立關係"。
用虛線畫出的兩條邊,正是 2.4.2 裡靠人工驗證抓到的問題。qa → legacy 是以廢棄 atom 為基準的 stale link,balance_curve_v3 → skill_cancel_rule_legacy 是指向不存在節點的斷鏈。畫成圖譜後,這兩條虛線就在實線之間凸顯出來。要是隻用文本來運營,它們就會埋沒在 47 個檔案的某處,永遠看不見。
在驗證關卡(verification gate,由人或檢查器把關驗證的環節,類似質量門禁 quality gate)(Layer 4)上自動執行的規則有四條。
requires 鏈條若繞回自己,就告警。用 simple_cycles 檢出。conflicts_with 繫結的兩個 atom 若都是 status: approved,就告警。(2.4.2 的案例因一方為 deprecated 而通過。)graph_orphan_detection_quarterly 明確規定了這個週期。docs_layer_numeric_prefix_naming 強制在檔名上加 Layer 編號字首,逆行只看檔名就能先做一輪篩除。這四條規則一旦用程式碼固化,就不必像 2.4.2 那樣每次都編寫提示詞。變更請求一提上來,機器人就重新構建圖譜,把受影響的 atom 清單與斷鏈、迴圈、衝突自動寫成評論。章首那條"有 4 個 atom 受影響"的評論,正是這個 —— 這個機器人就是前面預告過的那個機器人。
這裡得說清楚 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 讀取圖譜、找出約束違反(斷鏈、衝突),人在關卡上判定。
成為關係箭頭起點、終點的節點,不只有決策 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,正是"約好要收進詞典、卻還沒建立的空條目"。
讀到這裡,會有人問"這不就是 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 個月的覆盤中排查,自然就會對齊。
頭一個月是虧本的。只是關係書寫的負擔在累積,看得見的效果卻沒有。所以要從小處起步。第一週只用 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 做好寫關係鍵的準備。一開始只用 affects、derives_from、requires 三個。正文連結統一為 [[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)中只展示了核心的兩個函式。把整個資料夾構建成圖譜並掛上四項檢查的全文如下。
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 里加 requires、affects 兩行,每當發生廢棄、改名時就把上面的 prompt 跑一遍。圖譜視覺化是以後的事 —— 在變更前把斷鏈和衝突過一遍的習慣,這一次,就擋住了單人運營中最大的虧損。
週四下午 4 點 50 分。數值策劃填好的技能表剛剛上傳。技能 312 個。每個技能都要在 effect_id 這一欄裡填上效果編號,而那個編號指向另一張效果表裡的某一行。兩者對上了,遊戲才能跑起來。對不上,客戶端就會呼叫一個空效果,或者悄無聲息地崩潰。
從前的我是用手一行行核對的。技能表點一格,跳到效果表,確認編號,再跳回來。312 遍。再快也要兩個小時。在眼睛發花的最後 50 個裡,總會漏掉一兩個,而正是這一兩個,在 QA 版本里炸了。
本章要講的,是那兩個小時去了哪裡,以及那道一致性檢查在系統策劃的工作地圖上究竟落在哪個座標。座標不先定好,你就只能憑感覺去判斷該把 AI 嵌在哪裡,而且永遠只能憑感覺。
系統策劃是在抽象度與具體度之間遊走範圍最廣的人。他接過名為願景的那團霧,一直把它拽到資料表最後一格那個堅硬的數字為止。在這段旅程中產出的成果,共有四類。
(1) 把願景翻譯成結構。 當總監說"打擊感十足的動作戰鬥"時,系統策劃就把它轉化成技能、連招、取消、頓幀這套骨架。"成長的自主決定權"就變成職業、技能樹、裝備系統。這是霧凝結成構築物的第一瞬間。
(2) 為系統之間的介面編寫規格。 戰鬥、移動、背包、商店、任務、公會同時在跑。戰鬥中開啟背包會不會附加無敵?強化途中收到 PvP 申請怎麼辦?這些情形的答案匯聚起來,造就了所謂"做得好"的那種手感。每一處缺了答案的地方,玩家都會感到煩躁。
(3) 對資料表及其結構(schema)負責。 312 個技能的係數,數百件道具的效果,數十種怪物的行為。數值要麼自己填,要麼交給數值策劃或內容策劃。但表的列定義(結構,schema)這一塊,得攥在系統策劃手裡。這是替別人做好一隻只貼了標籤的抽屜的活兒。抽屜做得潦草,每個人填法就各不相同,一致性也就破了。
(4) 設計行為邏輯。 角色、怪物的 AI 會以狀態機(FSM,Finite State Machine,有限狀態機)、行為樹(Behavior Tree,以下簡稱 BT)、決策表、程式化規則等形態產出。這些資料交到程式設計師手中,變成程式碼。
四樣東西全都匯到同一個人的桌上,這一點是關鍵。所以"今天把時間花在什麼上"就成了系統策劃最大的運營決策。
在 2.3 裡,我們把整個遊戲製作物從 L0(願景)到 L4(版本)放上了一根座標軸。現在,就把 3.1.1 的四樣產出原樣標在那根軸上。像系統策劃這樣,一個領域的產出物橫跨多個 Layer、鋪得這麼開的情形,是很罕見的。
下面這張圖,把產出物落在 Layer 上的哪個位置、又在每個座標上與誰相遇,都畫在了一張紙上。
這張圖說明的有兩點。第一,系統策劃負責的是從 L0接過來、一直送達到 L4 的這段長路。第二,親手製作的區段是 L1\~L3,而這三格里每一格協作物件都不一樣。每換一格,協作語言就變,所以不把座標放在心上,會議就會一次次空轉。
不過這並不是說一個人要把 L1\~L3 全都包了。團隊大,L1\~L2 負責人和 L3 負責人就會分開;團隊小,一個人全看。座標是分工的地圖,不是把活兒全推給一個人的命令。
圖畫好了,現在該上色了。哪個座標引入 AI 的效果大?不是悶頭"全部自動化",而是看座標的性質來挑。
關鍵在於座標越往下走,AI 專責的比重越大。L1 的"職業要做幾個"關乎遊戲特質,得攥在人手裡。反過來,L3 的"312 行外部索引鍵是不是都對得上"是定式、重複,該整個交給 AI。L2 在中間——決策由人來做,但"改了這條規則會牽連到哪裡"這樣的影響範圍提取,由 AI 來託著。
這張圖解釋了為什麼 3.1.4 之後的所有實操都從 L3 附近起步:因為那是效果最大、風險最小的位置。結構工具就算出故障也不會闖禍,關係圖只是畫畫圖而已,一致性檢查則可以由人否決。
理論到此為止。現在回到 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_id、name、effect_id欄位。 2)effect.json—— 每個效果物件都有effect_id、effect_type。請檢查以下各項。 (A)
skill.json裡所有effect_id是否都存在於effect.json中(外部索引鍵一致性)。 (B) 存在於effect.json、卻沒有任何技能引用的孤立(orphan)效果。 (C)skill.json中effect_id為空或為 0 的行。規則:不要猜測。JSON 中沒有的欄位,就報告"欄位缺失"。 輸出分為 (A)(B)(C) 三個小節,每條違規用一行寫出
skill_id(或 effect_id)和原因。 若沒有違規,就在對應小節裡只寫"無違規"。
沒有收到兩個輸入 JSON。當前訊息中沒有附帶檔案內容。 請用以下任意一種方式重新發來。 -
skill.json和effect.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"}
]
(A) 外部索引鍵一致性 —— 違規 1 件 -
skill_id: 1004→effect_id: 5099在effect.json中不存在(dangling reference)(B) 孤立效果 —— 違規 1 件 -
effect_id: 5003(BUFF) → 沒有任何技能引用(C) 空/0 effect_id —— 違規 1 件 -
skill_id: 1003→effect_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 行那種一格一格跳著看的兩個小時,消失了。被自動化掉的是"跳轉加比對"這種勞動,而剩下的那三行判斷,才是核心。
上面那段記錄裡的檢查,固然可以每次都用手去讓它跑,但 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 是定下"嵌在哪裡"的地圖,接下來三章是動手嵌進去的活兒。
三樣工具一次性全開,運營負擔會比效果先到。按筆者的經驗,安全的順序是從風險小的座標(下面那頭)開始。
| 時點(建議) | 引入 | 座標 | 出錯時會發生什麼 |
|---|---|---|---|
| 1 個月 | 結構優先(3.2) | L3 | 不過是文件少更新了一次 |
| 2\~3 個月 | 關係圖視覺化(3.3) | L2\~L3 | 不過是圖不準確 |
| 3\~6 個月 | AI 輔助提示詞(3.4) | L1\~L3 | 要經過驗證,人可以否決 |
時間不是絕對標準。視團隊規模、既有基礎設施而定,可能要花兩倍,也可能減半就完事(筆者推測,未經驗證)。不變的是順序。把風險大的決策輔助放在最後,等前兩樣工具已讓團隊養成驗證習慣之後,再去碰最敏感的那個位置。
不美化數字,如實記下。下面是筆者作為總監運營的 MMORPG 專案(以下簡稱"專案A")的策劃團隊(4\~5 人,整個開發團隊為中等規模 10\~50 人,運營約 6 個月)裡觀察到的情況。這不是精確的自動計量,而是基於工作日誌和覆盤記錄的筆者的觀察,建議只當作方向和大致的比例來讀。
關鍵在於:省下來的時間,不是不做遊戲的時間。那段時間會迴流到像 L1 骨架那樣、沒法交給 AI 的深層決策上。減掉勞動,用在判斷上——這就是本章想給的一句話。
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 起步,上手之後再用關係圖和影響範圍一格一格往上爬就行。
週一上午,新來的策劃填好了技能表的 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):同一資訊只在一處管理,讓所有人都看向那一處的運營原則。
如果把模式優先只理解為"提前定義好列",那隻抓住了一半。核心在於先輸入什麼的順序。填資料的手按什麼順序移動,決定了一致性是被守住還是被打破。
本書推薦的輸入順序,是一條四格的管線。
從左向右流動的實線就是強制的輸入順序。先定義 $模式,再從那裡用 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 行的地方去修列規則,規則就會反過來跟著資料走,那一刻,事實來源就立反了。
$模式 到 csv 一次走通與其用嘴解釋,不如真的把一張表從頭到尾走一遍。假設要新建一張技能表。下面是帶著 AI 輔助進行的全程記錄。不做刪減,把出錯的地方和人否決的地方原樣留下。
$模式工具和 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 - 是否被動
把 $模式 的 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
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;
}
到這裡,人否決了一處。AI 把 none 放成了 SKILL_ELEMENT_NONE = 4,置於 enum 的末尾,可在 $模式 裡,none 是"無屬性"這一預設狀態。爭議點在 proto 的 0 號值。在 proto3 中,enum 的 0 號是"當值沒被填時自動落入的位置",所以 0 號到底是空著留給 _UNSPECIFIED(未填值),還是用 none(有意為之的無屬性)這類語義值填上,就會分道揚鑣。一旦把兩者合在同一個位置,把列留空的行(失誤)和特意選了無屬性的行(有意)就都以 0 落進來,從此再也分不清。AI 雖然把 _UNSPECIFIED = 0 與 none 分開放了,卻把 none 挪到了 4 號末位,讓最常見的預設狀態在檢索、除錯時離得更遠了。
人做出的決定:
- _UNSPECIFIED = 0 保留(用於檢出缺失)。
- none 照舊保留,但在 csv 編寫規則裡追加一條:"無屬性必須顯式寫 none,禁止留空"。空白=0=UNSPECIFIED 一律視為輸入錯誤。
- 把這個決定在 $模式 的 element 行說明裡追加一行:"無屬性也要顯式寫明(none)。空白即錯誤。"
沒有照搬 AI 的輸出。格式接受了,語義的邊界由人來劃。
驗證過的 proto 定義,由 Excel 的 Export 宏($模式 表上的按鈕)一致地輸出。人不在 IDE 裡重新打字。表才是事實來源,所以 proto 由表裡產出。這個順序,是 3.3 將要講的"輸出即正本"(json_over_schema_doc_as_source_of_truth)原則的延伸——不是文件去解釋程式碼,而是表去產出程式碼。
提示詞(全文): 請生成 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=true 時 damage_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 就能快速把格式填好,人只檢查語義和違規。一旦順序崩了,人就得從格式到語義全部一肩扛起。
把 $模式 放在 Excel 裡,對策劃來說方便,但對 AI、git 和外部工具而言,那是一處封閉的位置。所以我們運營一個把 $模式 自動轉換成 Markdown 的工具。斜槓技能 schema-doc 做的就是這件事。
它的動作分四步。
$模式 表(python-calamine,Rust 加速)<表名>_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_
請看 element 與 is_passive 說明列裡,3.2.2 第 4、6 步中人劃下的邊界原樣跟了進來。人在 $模式 裡寫了一行,文件、proto、驗證就都共享了同一條規則。這就是單一事實來源真正運轉起來的樣子。
落到 Markdown 上的模式,會立刻被三處直接用上。
如果說模式是表內部的規則,那麼 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.2 的記錄一般化,就成了五個步驟。把每一步的主體與產出分開來看,什麼由人攥著、什麼交給工具,就一目瞭然了。
五個步驟不必在頭一個月就全部備齊。哪怕只跑第 1、2 步(模式設計 + 自動文件化),也能抓住一半的價值。第 3\~5 步等運營熟練之後再逐步接上。一開始就強推五步,編寫者的負擔會在落地之前就把運營拖停。
筆者作為總監運營的某 MMORPG 專案(以下稱"專案A")裡,把這套工作流跑了約 6 個月。下面的數字中,資料表列一致性、新表初稿時間是從工具日誌和工作記錄裡彙總的實測,FK 斷裂頻次則是從構建失敗的 issue 反推得來的筆者估算(未驗證)。
| 專案 | 引入前 | 引入後 | 依據 |
|---|---|---|---|
| 列名一致性 | 約 60% | 約 95% | schema-doc 比對實測 |
| FK 斷裂頻次 | 每週 2\~3 起 | 每月 1 起以下 | 構建 issue 反推(筆者估算) |
| 新表初稿時間 | 4\~8 小時 | 1\~2 小時 | 工作記錄實測 |
| 新策劃理解表 | 開會 3 次 | 文件 1 次 + 開會 1 次 | 入職案例(僅方向) |
引入成本是工具初期開發約 3 天 + 運營落地約 1 個月。引入成本相對 6 個月的累計效果而言很小,這是運營得出的結論。不過上述比例只是一個團隊、一個專案的單一案例,並不保證能原樣搬到別的團隊。
鋪好模式,AI 的資料生成可靠度會飛躍式上升。原因在於,模式會預先把那種成為幻覺溫床的模糊輸入範圍關掉。面對"給我做 20 個技能"這樣的請求,若沒有模式,AI 就會發明出貌似合理的列,填進與本表不相容的值。有了模式,同樣的請求就會以遵守了所定義的 7 個列、各項約束、FK 的行返回。即便像 3.2.2 第 1003 行那樣冒出違規,點出一行再請求一次就完事。
代價是,邊界很分明。數值不交給 AI。damage_coef 若讓 AI"隨手"定,就會與遊戲的意圖衝突。把格式正確的候選快速鋪出來,到這裡為止是 AI 的份內事,而"這個技能的係數取 2.4 對不對",由人來回答。話雖如此,並不是說 AI 對數值毫無用處——曲線是否平滑、離群值、範圍統計,AI 能迅速抓出來。測量數字交給工具,而那個數字對不對則由人來甄別。
| 錯誤 | 規避 |
|---|---|
| 堆了 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 檔案、一個文本編輯器就夠了。
$模式,把列規則按 5 要素寫好(15 分鐘)。$模式 文本以 skill_schema.md 存進記事本。這就是你自己的第一個單一事實來源。轉到下一張表時,重複同樣的 4 步。當一個季度內有 5\~10 張核心表按同樣的順序排齊,那時才真正值得接上 schema-doc 這類自動化。
$模式→Enum/proto→csv,違規就會在填資料之前被關掉一位新來的策劃入職第一週走到我的座位前。"我想改一下任務獎勵表,可這東西一動,會把哪裡弄崩呢?"我正要指著顯示器作答,卻停了下來。我腦子裡有一幅圖。RewardTable 咬著 ItemTable,ItemTable 咬著 ItemEffectTable,在它們之上 QuestTable 又引用著獎勵……可一旦把那幅圖用話說出口,聽者腦中的形體就崩塌了。我在白板上畫了七個方框。箭頭開始纏成一團。30 分鐘後,他點點頭回到了座位,第二天又帶著同一個問題回來了。
正是這一幕讓我寫下了這一章。系統策劃的腦子裡有一張依賴關係圖。問題在於,它只存在於腦子裡。人一換,圖也就消失了。我需要一件把這幅圖外化出來的工具,於是做出了 gen_relation_map.py。
資料表只有 5\~10 張時,靠腦子就夠了。一旦超過 30 張,人的工作記憶就應付不來了。一個專案的表格資料夾,通常很早就越過了那條線。把哪裡依賴哪裡用文字寫成的表,即便讀了也畫不出圖來。這一章會從頭到尾跟著走一遍:把外部索引鍵關係自動生成為可互動的 HTML 關係圖的完整操作過程(worked transcript,完整保留的真實操作過程記錄)。
在做工具之前,先理清沒有關係圖時實際上會卡在哪裡。有四個場景反覆出現。
新策劃入職引導。 新策劃為了熟悉系統結構而約了會。就是上面那一幕。用話傳達的依賴關係,在聽者腦中撐不了幾天。如果一起點開一張關係圖,第一次會議就能畫出一半以上。它與白板上的手繪圖有一個決定性的不同:圖不會被擦掉,而是留在原處。
變更影響範圍討論。 系統變更請求提了上來。"這個會影響到哪裡?"約了會,討論了半天,還是漏掉了一兩個區域。如果有關係圖,只要點選要變更的節點、沿著入邊(inbound edge)追下去,影響範圍就一目瞭然。討論只需確定"這個影響是不是真的成立"以及優先順序即可。
檢出依賴逆行。 L3 資料表引用 L1 系統文件是正常的。反方向(上層 Layer 直接引用下層資料表)則幾乎總是設計缺陷。在用文字羅列的 FK 清單裡,人是抓不住這種逆行的。在圖裡,它會立刻以一根 Layer 顏色錯亂的箭頭顯現出來。
發現孤立的表。 偶爾會發現一張哪裡都沒有引用的表。要麼是舊策劃留下的殘跡,要麼是決定廢棄卻只剩檔案沒刪的情況。這就像辦公室角落裡滾著一個沒貼標籤的箱子。要有圖,才能發現那座孤島。
這四個問題的共同點是:都屬於"必須用眼睛看清結構才能解決"的範疇。靠文字和表格是行不通的。
現在真正跟著走一遍。輸入是一個裝著資料表的資料夾,輸出是在瀏覽器中開啟的一張可互動 HTML。我會把這中間 AI 做了什麼、人在哪裡做了驗證/否決,毫無遺漏地記下來。
核心是第 3 步和第 5 步之間的人工驗證迴路。FK 候選提取由機器鋪好初稿,人在其中剔除誤報。一旦省掉這個迴路,關係圖看上去煞有介事,卻是一幅錯的圖。
這件工具的準確度,取決於你從哪裡把輸入拉過來。3.2 中定下的 schema-first 原則原樣適用。FK 資訊的正本順序如下。
$스키마 표 —— 每張資料表的第一份正本。按列明確標註了型別、Enum、FK 目標。這裡若寫明瞭 FK,那它就是第一優先。*.proto / Enum 定義 —— 由 VBA(Excel 巨集語言)Export 匯出的 schema。當規格書為空時,用它補全型別。csv 輸出 —— 表格匯出的實際資料。規格書裡沒有的關係,也會在資料中以模式顯現(例如:若 npc_id 列的值全部落在 NPCTable 的鍵範圍之內,那它實際上就是 FK)。這裡要明確一條原則。正本不是 schema 文件,而是實際的 JSON/csv 輸出。 即便規格書裡寫著 reward_id 是 FK,只要實際資料中該列為空或指向了無關的值,那就是規格書錯了。工具在兩者不一致時信任資料這一側,並把不一致本身記進報告。這就是不把 schema-doc 當正本的理由。
工具的第一個動作,是開啟資料夾內所有 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_idNPCTable -
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(自引用,地區層級結構)注意:以上推斷基於列名規則,並未核對實際資料值。特別是
count、value這類不以_id結尾的列,我已從 FK 檢查範圍中排除;若存在隱藏引用,則會被漏掉。
在這份輸出上,人做的事有三件。
faction_id →AI 因為沒有 FactionTable 而做了不確定處理。這是對的。不過實際上 faction_id 是 Enum。陣營固定為 6 種,所以不另建表,直接用 Enum 管理。多虧 AI 沒有捏造一張不存在的表、而是停住了判斷,人才能照著規格書把它確定為 Enum。從 FK 中排除。currency_id →AI 把兩種可能性都留著。一看實際資料,CurrencyTable 是存在的(是我在輸入清單裡漏掉了)。確定為 FK。 這怪不得 AI,是人的輸入遺漏。prev_quest_id 與 parent_region_id 的自引用檢出。這一點若是單純的正則規則,本會漏掉。AI 還附上了"前置任務""地區層級"這樣的語義,讓驗證更快了。這裡得到的教訓很明確。AI 最有用的地方,不是快速推斷,而是把不知道的位置留作"不確定"的那份剋制。要是它硬把空格填滿,faction_id 就會被連到無關的表上,而那個誤報會作為一根假箭頭留在關係圖裡,把新策劃引向歧途。
得到經過驗證的 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),就判為逆行並把邊塗成紅色。當人開啟圖、看到紅色箭頭時,那幾乎總是該動手處理的地方。
最後一步是 pyvis 吐出可互動 HTML。點選節點時,該表的列、Layer、入邊數會以工具提示(tooltip)彈出,還能在搜尋框裡按表名過濾。之所以必須是 HTML 而非靜態 PNG,原因就在這裡 —— 一旦節點數超過幾十個,靜態圖裡箭頭就會纏成一團,什麼都看不見。要用滑鼠拖開鋪展,再點選把關注區域收窄,才抓得住模式。
把用上述示例資料生成的關係圖結構轉成 SVG,大致如下。顏色代表 Layer,紅色箭頭表示(本例中沒有)逆行的位置。
看節點大小就知道,RegionTable 被引用得最多(Quest·NPC 都指向它)。這就是樞紐。ItemEffectTable 是葉子節點,所以小。新策劃問"要理解這個系統該從哪兒看起",答案已按節點大小的順序寫在圖裡了。
3.1 中定義了 Layer 座標。當這一章的關係圖把那套座標提升到視覺層面時,用文字或表格無法實現的四種診斷,就能在同一屏內完成。
不過這並不意味著圖能抓住所有問題。圖抓的是結構性缺陷。這個 FK 在語義上是否真的是對的關係(例如 npc_id 究竟是"釋出任務的 NPC"還是"任務中出現的 NPC"),圖是解不開的。那是人的領域判斷之責。工具只不過是為人的判斷鋪好施展的舞臺。
關係圖不是做一次就完事的。表每週都在新增、變更。交給手動更新的關係圖,一兩個月就會與實際結構錯位,而錯位的地圖會指錯路,那還不如沒有。被一張錯圖坑過的團隊成員,從此就不再看圖了 —— 這是最昂貴的失敗。
所以,把更新掛到自動觸發上。
/relation-map 斜槓命令即時生成。會議中臨時要展示時用。生成的 HTML 會自動部署到內部靜態託管(策劃門戶)上。無需另裝工具,只要有瀏覽器,人人都能看到同一張地圖。這就像桌邊總是攤開著的那張地圖。無論誰來問,都指著同一幅圖一起作答。
| 錯誤 | 為何發生 | 規避法 |
|---|---|---|
| 節點超過 100 個、圖纏成一團 | 把所有領域硬塞進一屏 | 按領域過濾、按分組拆分檢視 |
| Layer 顏色每件工具都不同 | 調色盤在每段程式碼裡重新定義 | 用 1 個 atom 把調色盤標準化(LAYER_COLORS) |
FK 檢出只抓 *_id,導致漏報·誤報 |
依賴一行正則 | 規格書 FK 標註 + 實際資料值驗證並行 |
| 圖是做了,可沒人看 | 沒接入工作流 | 強制在變更請求·會議中附上圖 |
| 做了卻不更新而腐爛 | 依賴手動更新 | 必須自動觸發,手動一個月就失效 |
在 gen_relation_map.py 的運營裡,最常被坑的是第三行。只信 *_id 規則,就會漏掉 count 或 value 這類隱藏引用(3.3.2.3 中的 AI 也自行警示了這一侷限),並把 Enum 的 grade 誤判為 FK。規格書和實際資料兩者都看的驗證迴路,才是這一行的答案。
想一口氣處理整個公司的資料表,既沉重,又會在展示出價值之前就把人累垮。從自己分管的一個資料夾起步,小規模開始。
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 的列(像 grade、effect_type 這種沒有 _id 卻看著像 FK 的)要從 FK 中剔除。
3. 確認自引用(prev_*_id、parent_*_id)是否抓對了。
4. 用驗證過的清單畫圖,在瀏覽器中開啟,用眼睛找出紅色箭頭(逆行)和孤島(孤立)。
如果沒時間做工具,第一週用一張手繪的 mermaid 起步也行。把 5 張表的 FK 按 3.3.2.3 的格式直接寫進 mermaid。帶著這一張去開會,展示"這就是我們系統的依賴關係",價值就在那一刻當場得到證明。價值一旦顯現,自動化工具自然會隨後跟上。"必須一開始就拿出能跑的工具"這份負擔,你大可放下。
擴充套件會按這個順序自然流動 —— 第 1 周自己表格的 mermaid 手繪圖 → 第 2 周加上 Layer 顏色和點選 → 1 個月自動更新(git hook 或夜間批處理)→ 3 個月部署到內部門戶 → 6 個月全部表格的整合關係圖。
3.2 講了表的內側(schema),3.3 講了表的外側(關係)。3.4 會在這之上疊加 AI 輔助提示詞模式。在 schema 與關係都已就位的系統之上,AI 如何輔助一致性檢查與影響範圍提取,接下來會以一系列實用模式展開。
那是 Alpha 版本即將到來的一週。我在技能表裡新增了一個職業的一行並儲存。可那一行所引用的 buff ID,其實是前一天有人刪掉的那一行——這一點我直到第二天早上構建崩了之後才發現。為了順著崩掉的構建回溯找出原因,花了我兩個小時。要是在刪掉那一行之前能先問一句"這個刪了沒關係嗎?",這兩個小時本可以不必花。
本章講的就是如何讓 AI 替你問出那個問題。重點不在於把提示詞寫得多漂亮,而在於把同一個問題固化下來,不必每次都從 0 重寫一遍。在 3.2 裡鋪好了模式(schema),在 3.3 裡鋪好了關係圖。兩者都是資料的骨架。本章要在這副骨架之上,把人拋給 AI 的問題本身固化為資產。
先釘死一點。AI 造出來的不是答案,而是候選。本章出現的所有模式裡,最終拍板的那隻手始終留在人這一側。
剛開始用 AI 時,每次都用自然語言即興敲。大致是這樣:
幫我看下技能表。確認有沒有外部索引鍵斷掉的,
有奇怪的就告訴我。哦對,還有冷卻時間為負的。
這條提示詞在兩處漏掉東西。
第一,檢查項每次都不一樣。今天想起了"冷卻時間為負",明天就忘了。昨天篩過一次的"重複 PK(Primary Key,主鍵)",今天的提示詞裡沒了。依賴人記憶的檢查,會隨人的狀態而漏。
第二,結果格式每次都不一樣。同一個意圖,寫成"幫我確認""檢查下""掃一遍"這些不同說法,AI 有些天用表格回,有些天用大段文字回。格式參差不齊,結果就沒法再拿去自動處理。
解法是把提示詞從手上拿下來,放進抽屜。把每次手寫的便條,換成貼了標籤的卡片,從同一個抽屜裡取。那張卡片就是本書所說的斜槓命令(skill)和 atom。
固化有三種容器。把什麼裝進哪一個,取決於呼叫頻率與穩定性。
高頻且定義已定型的作業用斜槓命令。高頻但"不能忘的約束"用 atom JIT 自動注入。偶爾做但分量大的作業用模板檔案。而定義仍在搖擺的作業,先不固化,留作即興。三者不必一開始就備齊。從一兩個斜槓命令起步,看到價值再增加。
與其用嘴解釋,不如把一個模式從頭到尾走一遍。這是一個在刪掉一個空行之前,自動先問一句"這個刪了沒關係嗎?"的模式。它的名字叫 /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)。一致性檢查就是把這兩條原則原樣搬成檢查規則。
展開 /check-sheet,裡面裝著這樣一段提示詞正文。這就是不必每次手敲的那部分。
角色:你是遊戲資料表的一致性檢查器。
要檢查的表:{{sheet_name}}
可參考的模式:$스키마 表(各列的型別·範圍·FK 物件)
可參考的正本:同一張表的 export JSON(優先於模式文件)
檢查項(按此順序):
1. 外部索引鍵有效性 —— FK 列的值是否實際存在於被引用的表中
2. 範圍越界 —— 超出 $스키마 中定義的 min/max 的值
3. 必填列缺失 —— required=true 的列為空的行
4. 重複 PK —— 同一 PK 出現兩次以上
輸出格式(務必照此表):
| # | 行 | 列 | 違規型別 | 當前值 | 修正候選 | 置信度 |
規則:
- 修正候選只提建議。絕不直接改表。
- 置信度為高/中/低三者之一。若屬猜測則為低。
- 沒有違規時,只輸出"違規 0 件"並結束。
這段正文裡要緊的是最後三行規則。"不直接改"、"標註置信度"、"猜測要坦白為低"。這三行把 AI 拴在推候選的位子上,而不是給答案的位子上。
/check-sheet skill_table
結束了。只遞一個表名。四個檢查項也好、輸出表格式也好,都不再重寫。這就是固化的價值。
下面是實際跑了一次後返回的輸出。不做摘要,把含糊之處與 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",那很可能是會毀掉連招收招技能的錯答。
接著由人逐行裁定。驗證自動伴隨,意味著這張表會自動生成,並不意味著人不看。
最後第 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,傳播給下一個人、下一次檢查。這就是固化的複利。同一個失誤不會被篩第二次。
第二個模式,是要消掉開頭那個場景裡的那兩個小時。在改一行、改一個決定之前,先問"這會波及到哪裡"。它的名字叫 /impact。3.3 裡做的關係圖(gen_relation_map.py 輸出的 FK 關係 HTML)就是這個模式的燃料。
/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 為"直接",正文提及為"間接",推定為"弱"。
- 若擔心遺漏,哪怕標為弱也要列上。漏掉比列上更糟。
- 不做優先順序或工作指派。只生成清單。
/impact buff_id=9012
用一行問出"想把猛毒 buff(9012)換成別的效果,哪裡會動搖?"。
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。
抽掉猛毒,這些套裝的概念就會變。
這裡 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 造出來的始終只是備忘草稿,按下發送的手指是人的。
上面兩份記錄遵循同一副骨架。其餘所有模式也都搭在這副骨架上。
關鍵是右下角的虛線。否決不是終點,而是作為修正命令本身的輸入折返回來。一致性檢查裡被否決的第 4 條成了 finisher 例外規則,那條規則被記錄進 atom,傳播給下一次檢查。沒有這個反饋,同一個錯答每週都會被重新篩一遍。
三處留著人的手。呼叫(選物件)、驗證(採納·修正·否決)、規則改進(否決的迴流)。AI 只在這之間做推候選的活兒。
把同一副骨架搬到別的作業上,模式就會增多。不附記錄,只點位置。它們都原樣遵循 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 卻以"離群值"為由把它削平的事是有的。所以數值計算即便自動附帶了曲線驗證,最後一行也要在人將其與意圖對照之後才閉合。
模式一多,就需要運營紀律。下面五項原則不是要背的規則,而是要嵌進工具本身的設計原則。
| 原則 | 為什麼 |
|---|---|
| 一命令 = 一作業 | 越小越易於複用·除錯。不把檢查·修改·共享都塞進 /check |
| 命令自動伴隨驗證 | 在輸出表本身加上置信度·依據列,減輕人工驗證的負擔 |
| 命令定義化為 atom | 像 3.4.3 的否決→規則迴流那樣,把緣由·示例·變更歷史留在 atom 裡 |
| 度量使用頻率 | 月呼叫不足 1 次的命令是淘汰候選。用資料來砍 |
| 人的手只在決定上 | 命令只到生成候選為止。禁止自動決定 |
最後一個收斂點。在作者運營過的某個 MMORPG 專案裡,能在系統策劃中穩定留存的斜槓命令,隨時間推移收斂到 12 個上下。這不是公開標準,而是一個專案的觀察值(作者經驗,未經驗證)。但方向是明確的。命令不是無限地增,而是每月加減 1~2 個,停在腦子裡裝得下的數目上。貼了 100 個標籤的抽屜,和沒貼標籤的抽屜一樣。
引入不必一次到位。第一個月,把每週重複的一件作業固化為斜槓命令就夠了。那一件顯出價值,下個月自然就蔓延到兩件、三件。
在 3.1 鋪好系統策劃的 Layer 座標,在 3.2 鋪好模式,在 3.3 鋪好關係圖,再在 3.4 於其上疊加 AI 輔助提示詞。走過四章的系統策劃,一週會這樣改變。
雜活時間減少,那些時間又回到深入的設計與對玩家體驗的思考上。不把省下的時間重新填滿雜活——這才是引入工具的真正理由。
下一個 Part 4 是戰鬥策劃。它是系統策劃最近的兄弟,3.1~3.4 的工具與模式可原樣過渡過去。
setup. 挑一件每週重複的檢查作業(例如表的一致性)。把該作業的 4 個檢查項與輸出表格式寫下來,固化為一個斜槓命令。務必把三行規則("不直接改 / 標註置信度 / 猜測要坦白")放進命令正文。
prompt. 只把物件用一行遞過去來呼叫。
/check-sheet skill_table
verify. 把返回的表逐行以採納·修正·否決裁定。一旦出現否決,那不是運氣,而是規則的缺陷。把那條例外追加進命令定義的那一行再發一次,讓下一次呼叫不會把同一個失誤篩第二次。
如果既沒有團隊也沒有 atom 系統,就用備忘錄應用裡的一個文本塊代替斜槓命令。標題寫"表檢查提示詞"。內容是上面 setup 的 4 個檢查項 + 3 行規則。每次檢查時複製這個文本塊,只改表名再粘給 AI。一旦碰上要否決的事,就在那個備忘塊裡直接加一行例外。工具無論是斜槓命令還是一張備忘,迴圈(固化 → 呼叫 → 驗證·否決 → 規則更新)都照樣轉。
本章學習目標(難度 🟡 實務 · 前置:四則運算·表格計算):學會把"打擊感"這類抽象形容詞拆解為可測量的訊號,並用座標指定戰鬥策劃的五項產出各自落在 Layer 的哪一格。
構建評審會議室。程式設計師把剛接入的新技能投到顯示器上。角色揮劍,敵人被向後擊退。五個人在看。有人開口。
"嗯……總覺得打擊感有點弱。"
旁邊的人點頭。"對,有點平。"
程式設計師問:"該改哪裡、怎麼改呢?"
沉默。會議室裡的五個人沒有一個能用數字回答這個問題。"打擊感弱"是五個人都感受到的,但沒有人能說出"把頓幀從 3 幀改到 5 幀"。會議花了 40 分鐘來回拋"再厚重一點""衝擊力不夠"之類的形容詞,最後以"先放到下個構建裡再看吧"收場。
這一幕濃縮了戰鬥策劃的全部問題。這是玩家最直接感受到的領域,可一旦要把這種感受落成語言,剩下的就只有形容詞。形容詞無法測量,無法測量就無法調整。戰鬥策劃的第一項工作,就是把這些形容詞拉下來變成數字。
本章要定的,是這些數字落在哪一格。戰鬥策劃做的五項產出各自落在 Layer 的何處,以及這套座標為何成為自動化的前提條件。4.2、4.3、4.4 的實戰工具都在這套座標之上運轉。
給非本專業讀者的一句話。 在本部分,你不必記住戰鬥數值或幀單位。你只需帶走這一條 —— "以形容詞來回傳遞的請求,既測不了也調不了。" 把"再厚重一點"拉成"把什麼改成幾"的那一刻,協作才能轉起來 —— 這個思路對遊戲之外任何崗位的模糊反饋都同樣適用。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(第一)的體感重量。哪樣產出是哪樣產出的輸入,如果沒有寫明,一次改動就會牽動五處。所以需要座標。
在 2.3 立下的 L0\~L4 座標之上,把五項戰鬥產出擺上去。這套對映是本章的脊椎。
用表格再整理一遍是這樣。
| Layer | 戰鬥策劃的產出 | 變更頻率 |
|---|---|---|
| L0 | (接收 —— 願景:"打擊感鮮活的動作戰鬥") | 幾乎固定 |
| L1 | 連招·取消結構 / Look & Feel 規格書 / 職業骨架 | 慢 |
| L2 | 各章節敵人群落推進曲線 / 技能解鎖流程 | 中等 |
| L3 | 技能傷害係數表、冷卻時間值、資源消耗 | 快 |
| L4 | 構建實測 DPS、真實可行連招路徑、玩家反饋 | 每個構建 |
戰鬥策劃的特點是:L4 的比重比其他領域都大。劇情策劃的 L1 規格書幾乎就等於最終成品,而戰鬥不同。"打擊感好"屬於必須在構建裡親手打、看著畫面才能知道的領域。規格書裡寫了"頓幀 5 幀",它實際是否體感厚重,只能在 L4 裡確認。所以模擬與自動測量工具在本領域創造的價值最大(4.4)。
不過,L4 大並不意味著 L1 不重要。看那些虛線箭頭。L4 的測量值會反饋回 L1 規格書。沒有規格書,測量值就失去了"比對的基準"。要有 5 幀的規格書,才會得出"實測 4 幀,少了 1 幀"這樣的診斷。規格書 → 構建 → 測量 → 修正規格書的迴圈,貫穿全部五個 Layer。戰鬥策劃把手按在整個迴圈上,是同一張桌子上同時轉動著最多 Layer 的位置。
抽象論到此為止。現在回到會議室那一幕,用實操記錄(worked transcript,完整保留的真實操作過程記錄)來看"打擊感弱"實際被拉成數字的過程。工具只有一個 LLM。輸出不加修飾,連人否決掉的部分也原樣保留。
筆者作為總監運營的某 MMORPG 專案(以下稱"專案A")的戰鬥 TF 中,將某個新近戰技能的 Look & Feel 規格化的真實流程,在此還原。
把這個技能的 Look & Feel 不要用形容詞,改成 ms·幀數值。
命中時序、頓幀、鏡頭震動、特效同步、後搖,共五項。
沒把握的就寫"需在本遊戲中驗證"。
技能:大劍縱劈 (heavy_overhead_slash)
意圖:"沉重地劈下,敵人踉蹌的感覺"
幀率:60fps
大劍縱劈的"沉重感"主要由頓幀與後搖的長度,以及鏡頭震動的衰減塑造。按專案逐條給出。
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 的人手上,這一點連工具都知道。
上面那段實操記錄只展示了一個位置(規格化)。在整個戰鬥策劃中,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 報告 —— 全是供人憑手感做決定的素材。
到這裡為止,都是"把產出按 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 分解的直接產物。
最後是可逆性。戰鬥策劃的驗收迴圈裡,混著可回退和不可回退的步驟,知道這道邊界很重要。
動作捕獲是戰鬥中最厚的不可逆步驟。捕獲工作室的檔期、演員的邀約、重拍成本都很大。所以標誌性動作的動作捕獲,只在模擬·捕獲自動分析充分運轉、序列已定下來之後才進行。無論保守還是進階,都把動作捕獲和線上構建之前設為決策門。戰鬥策劃的所有驗收,都要在這道門左側的可逆步驟內結束,才安全。
這是專案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 咬定"需驗證"的專案,絕不要原樣照單全收。
如果是獨自做的遊戲,五項產出·五個 Layer 不必全備。最少做兩樣。一,Look & Feel 規格書一頁 —— 對核心動作 3\~5 個,只把頓幀·後搖·同步用數字寫下來。用形容詞寫的備註,半年後連你自己都認不出。二,把連招·取消從程式碼裡分離成一個檔案 —— 把可取消組合抽成資料,日後就能讓 LLM"用這些組合提出 5 條連招"。這兩樣,是獨自開發也能為自動化敞開門的最小座標。
會議室的顯示器前圍著五個人。同一個版本、同一個技能、同一段 30 秒的影片正在螢幕上第三次迴圈播放。客戶端程式設計師先開口:"我覺得還行。"美術抱著胳膊說:"太弱了。感覺缺了點什麼。"旁邊的策劃插話:"特效是不錯,可就是不'跟手'。"總監盯著看了好一會兒,做出決定:"嗯……再稍微做得'厚重'一點吧。"
然後會議就結束了。"再稍微厚重一點"到底是多少 ms、多少幀,沒有一個人記下來。下一個版本里,程式設計師實現的是他自己理解的"厚重",美術疊加的是他自己理解的"厚重"。然後下一週,在同一個會議室裡看著同一段影片,同樣的對話又重複一遍。
打擊感、手感、Look & Feel。這是戰鬥策劃中用得最頻繁、卻又最缺乏定義的詞。所有人都自以為懂,可腦子裡的定義各不相同,於是討論結束後什麼也沒留下。本章要做的,就是把那種"感覺"分解成可測量的數值。這是把手感從抽象拉回資料的環節。
先把邊界劃清楚。戰鬥策劃大致分為兩條線。
本章只講後者。傷害是 100 還是 120,與手感沒有直接關係。那 100 點傷害"打進去的那一瞬間"玩家如何體感,才是手感。即便是同一個傷害公式,只要命中時機與頓幀不同,就會讓人感覺像是完全不同的遊戲。
先得老實指出一點:打擊感不是僅憑三個數值就能完成的。攻擊動作(動畫)的加速·減速曲線、被擊方的反應(受擊反饋·硬直)、80—90 年代日本動作遊戲慣用的誇張變形(deformation,即命中瞬間把角色拉伸、壓扁的殘影·糊化表現),這些全湊齊了,"打中了"這一整塊的感覺才立得起來。本章集中、並能拉回到可測量數值的,是其中的三條軸。動作·受擊反應·誇張變形是動畫師·美術師著手更多的領域,留到後面幾章和美術部分講;這裡把重心放在策劃可以用規格固定、並能在版本中驗證的三條軸上。這三條軸可以這樣拆分。
當會議室裡有人說"弱"時,那種弱來自三者之一。是反應慢(時機)?是沒有命中感(頓幀)?還是各自為政(同步)?用這三條軸分解去追問,"弱"才終於變成一句可以修正的話。
不過"弱"的成因並不總是隻在這三條軸上。把構成手感的要素一個不漏地列出來,劃清本章負責到哪裡。
| Look & Feel 構成要素 | 是什麼 | 本章中 |
|---|---|---|
| 命中時機 | 輸入 → 首次反應的 ms。最先被懷疑的要素 | 測量·規格(軸 1) |
| 頓幀 | 命中瞬間讓時間停頓、賦予重量的長度 | 測量·規格(軸 2) |
| 攝像機抖動 | 配合打擊、畫面晃動的反饋 | 測量·規格(含於軸 3) |
| VFX·SFX 時機 | 特效與聲音是否同步到命中幀 | 測量·規格(含於軸 3) |
| 攻擊動作(動畫) | 揮擊的加速·減速,預備動作與後續動作的曲線 | 提及(美術·動畫領域) |
| 受擊反應·硬直 | 被擊方一震並陷入硬直的反應 | 提及(下一章·美術部分) |
| 誇張變形 | 命中瞬間拉伸、壓扁的誇張(殘影·糊化) | 提及(美術部分) |
| 手柄震動 | 傳到手上的物理反饋 | 測量·規格(含於軸 3) |
上面四項歸入本章的三條軸,成為測量·規格的物件;中間三項(動作·反應·誇張變形)不可或缺,但屬於策劃一人難以用數值收口的美術·動畫領域,因此只明確"它們存在"這一點。要是動作僵硬、或者被擊方穩穩站著不動,那麼三條軸全對了打擊感也立不起來。
三條軸裡最先講時機是有原因的。玩家懷疑手感時,最先卡住的是"反應慢"這種感覺;別的東西再華麗,只要輸入遲鈍,那一瞬間一切都會垮掉。所以從時機抓起。
手感的第一條軸是時間。從按下按鈕的那一瞬間(0ms)到畫面首次反應的瞬間,要花多少 ms。人對這種延遲敏感得驚人。60ms 和 120ms 的差別,"嘴上說不清楚,手卻知道"。
一次攻擊不是單純的一個點,而是鋪展在時間上的多個事件。把一次普攻放到時間軸上看,長這個樣子。
這張圖裡最重要的數字是"攻擊判定框首次開啟的 100ms"。意思是按下按鈕 100ms 之後,攻擊判定開始。這個值決定了手感的體感速度。
推薦區間因型別·角色而異,但大致的基準線是有的。
| 種類 | 推薦 輸入→反應 | 備註 |
|---|---|---|
| 即時反應(輕攻擊) | 60\~120ms | "跟手"感的核心區間 |
| 厚重反應(大型技能) | 200\~400ms | 為厚重感而刻意設定的前搖 |
| 蓄力(長時間充能) | 500\~2000ms | 刻意的等待,另行處理 |
這個區間不是絕對標準。這是作者估算(未經驗證):休閒手遊往輸入寬鬆的一側浮動約 ±50ms,格鬥主機端則傾向於收得更嚴。比數字本身更關鍵的,是讓全隊共享一條基準線——"我們遊戲的輕攻擊約定為 90ms"。有了基準線,才能看著版本說出"對/錯"。
但這裡有一個陷阱。人眼分不清 90ms 和 110ms。在 60fps 下 1 幀約為 16.67ms,而這 20ms 的差距不過一幀出頭。會議室裡"好像有點慢?"這句話到底對不對,光靠眼睛終究判不出來。所以才需要測量。
規格里寫了"攻擊判定框 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 不會疲憊。下一節實際讓它做一遍。
把規格 yaml 和版本 telemetry 日誌兩份都貼進去,讓 AI 對照兩者、找出對不上的地方。下面是提示詞全文、Claude 的原始輸出,以及人工驗證的全過程。不做摘要,原樣照搬。(實操記錄指完整保留的真實操作過程記錄。)
下面是一個戰鬥技能的"命中時機規格",以及從實際版本中提取的
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。
先把規格的 expect_ms 換算為幀。在 60fps 下,frame = round(ms / 16.67)。
- hitbox_on: 100 / 16.67 ≈ 6.0 → frame 6
- vfx_trigger: 100 → frame 6
- sfx_trigger: 100 → frame 6
- damage_apply: 110 / 16.67 ≈ 6.6 → frame 7(四捨五入)
- ui_dmgnum: 100 → frame 6
- cam_shake: 100 → frame 6
與日誌中實際 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 卻把它當成"需要確認"反問回來。既然給了規格,就該在規格之內把答案收口,它卻製造了不必要的疑問。這句話沒有采納。
帶著否決理由重新發起請求。
damage_apply 在規格里已用 110 釘死意圖,所以去掉"需要確認"的註釋。
// 意思是:不要去反問規格之內已收口的專案
只保留兩條 FAIL(sfx、ui_dmgnum),不要斷定成因,
改成"待確認的程式碼/資源位置"檢查清單。
針對兩條 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 去斷定成因,它會編出聽起來煞有介事的假話,因此只讓它做到"待確認之處"為止才安全。
第二條軸是停頓。命中那一瞬間把遊戲時間極短地停住、或放慢的效果。它決定了"打中了"這種感覺的強度。這是格鬥遊戲與動作 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 保證規格一致,人則看那個規格值本身是否合適。
第三條軸是同時性。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 抓出規格對不上,與人去抓"規格全對、可手感還是不立"的領域,這兩件事仍然是分開的。
把至此為止的碎片接成一條流程。這個迴圈一旦轉起來,會議室裡的"再稍微厚重一點"就被翻譯成"頓幀 victim 4→6 幀"。
在這個迴圈裡,AI 負責的格(D、F)和人負責的格(A、G、H)清晰地分開。AI 擅長機械對照與生成檢查清單,人擅長設定基準線·確定成因·最終手感判定。自動化不是要把人剔出去,而是讓人從每次都要花半天的"用眼睛數幀"中解脫出來,只專注於"手感"。
迴圈的最後一格(I)很重要。這些測量資料不是用一次就丟,而是再次作為下次里程碑覆盤的輸入進來。一旦"上個季度的手感 FAIL 集中在 sfx 同步上"這類規律以資料留存,下個季度就先動音訊流水線。
在作者以總監身份參與的某個手遊 MMORPG 專案(以下稱"專案A")中,把上面的迴圈跑了約 6 個月,觀察到的變化如下。下面的數值不是精密計量,而是基於會議記錄的時間戳與版本驗證記錄的作者運營觀察(含估算),請按方向與比例來讀。不要把絕對值當作可引用的基準來用。
| 專案 | 引入前 | 引入後 | 性質 |
|---|---|---|---|
| 單次 Look & Feel 會議耗時 | 拖得很長 | 縮短到一半以下 | 以會議記錄為準,體感 |
| 版本驗證(多個技能) | 幾乎一整天 | 大幅縮短 | telemetry 自動對照的效果 |
| "打擊感弱"反饋的消解 | 多個版本週期 | 1\~2 個週期 | 樣本少,只看方向 |
| 規格 vs 版本 一致率 | 一半上下 | 大多一致 | 引入 telemetry 後變得可測量 |
比數字本身更本質的是定性變化。會議室裡一冒出"好弱啊",馬上就會反問"哪條軸?時機?頓幀?同步?";答不上來就一起把 telemetry 調出來。可測量的客觀為討論造出了終點,這是 6 個月裡最大的變化。"再厚重一點"走出會議室之外的情況變少了。
| 錯誤 | 規避 |
|---|---|
| 沒有規格就先驗證版本 | 規格優先。沒有基準,"對/錯"就無從談起 |
| 想先鋪影片分析 | 我們自己的版本從 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 種同步是難的,但哪怕只抓住"輸入→反應"這一條軸,手感討論的一半也已經搬到客觀這邊來了。
下一章從一次擊中轉入擊中的連續。連招·取消·輸入佇列——講那些讓一次擊中自然銜接到下一次擊中的規則。
戰鬥設計師組員 B 站在會議室白板前,用馬克筆畫著方框。基礎1、基礎2、基礎3,還有向旁邊岔出去的重擊分支。當箭頭增加到七條左右時,有人問道:"那麼重擊挑空之後用閃避取消,還能再回到基礎1嗎?"組員 B 停下了筆。白板上的圖裡並沒有畫出那條路徑。是能畫卻沒畫,還是規則上根本不可能,他自己也無法當場回答。
這就是連招設計真正的難題。連招在腦海裡看上去像"1-2-3 連起來再分支到重擊"這樣簡單的一條主幹。可一旦摻入取消和輸入佇列,主幹就變成了圖。在六個節點上只要再加幾條取消邊,實際能踩到的路徑就會膨脹到幾十條。人無法在腦海裡把這幾十條全部展開。於是,"這條路徑太強了"這樣的數值問題,往往要等它進了構建版本之後才被發現。
本章的目標只有一個:建立一套不靠手畫、而是自動枚舉出全部連招路徑,並對每條路徑加以驗證的工作流。把用自然語言寫下的規則轉成規格,從規格中列舉路徑,再把枚舉出的路徑送進模擬。在這個過程中,AI 能幫到哪一步、又會在哪裡撒謊,我會原原本本地展示出來。
把連招寫成表格,會是這樣:"基礎1 之後是基礎2,基礎2 之後是基礎3。"行與列一目瞭然。可這張表會撒謊。因為表格假定了一條直線。在真實戰鬥中,玩家會從基礎2 岔向重擊,把重擊用閃避取消,閃避剛結束又按下基礎1。這些分支和迴圈都藏在了表格的行與行之間。
所以連招真正的形態是有向圖。動作是節點,連線是邊。每條邊上掛著輸入視窗(何時接受輸入)和輸入鍵。節點上掛著持續幀數,部分節點上還掛著獎勵條件(必須經過特定節點才會附加傷害倍率)。
把戰士角色的一套基礎連招畫成圖,如下所示。六個節點,含取消分支。
與白板有兩處決定性的不同。第一,每條邊上都標明瞭輸入視窗的幀數範圍。"重擊 6\~24f"的意思是:從基礎2 開始之後的第 6 幀到第 24 幀之間接受重擊輸入。第二,有一條用虛線畫出的閃避→基礎1 重新進入的邊。這正是組員 B 在會議室裡無法當場回答的那條路徑。用圖明確標出後,"有/沒有"就一清二楚了。
這張圖若由人手畫,六個節點加七八條邊。如果有二十個角色、每個角色又有三四套連招,圖就會變成幾百張。手是跟不過來的。所以要把圖寫成文本規格,再從中自動生成圖示與驗證。
把上面的圖轉寫成 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 中段開始接受的";機器解析同一行,用於生成圖示和列舉路徑。一個來源同時產出人的理解和機器的驗證。
上面的幀數(21、24、[6, 24])不是實測值,而是為了本章說明、由作者構造的示例值(未經驗證)。在真實專案中,這些值來自動畫師製作的蒙太奇(montage)長度和構建版本里的通知(notify)時機。第一次寫規格時填入設計師的意圖值,等構建版本出來後再捕獲、用實測值加以校正 —— 這條校正迴圈在 4.4 中講解。
把組員 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 條(數值要誠實)。
已轉換為規格。幀數值未提供,故留為 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 選了一邊,又如實報告說它模糊,這是誠實的;但這屬於設計決策,得由人來定。
回答三個問題,並否決其中一部分。
window: [8, 18]。cancels_recovery: true 屬性,以明示它是"打斷後搖的動作"。→ 部分採納 + 追加屬性。另有一處要否決。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 標出模糊之處,人憑領域知識來決斷 —— 只靠其中一方都行不通。
既然規格是圖,那麼連招路徑的列舉就變成了圖搜尋問題。從起點節點出發,尋找通往終點節點(或終結技)的所有路徑,採用深度優先搜尋(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)引用。
光有路徑列表,還不知道"哪條路徑太強"。得把每條路徑放進 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 壓下去。在構建版本出來之前,就在規格階段做出這個判斷。
把整條工作流壓成一張圖,如下所示。
規格(D)處在中心,列舉(E)·驗證(F)·模擬(G)從那裡岔出。一旦檢出矛盾或數值失衡,就回到規格去修。白板上沒有這條迴圈 —— 所以白板上的連招,要等進了構建版本之後才知道它錯了。
到目前為止,我們看了連招圖和路徑列舉。取消和輸入佇列,是調節這張圖的兩個旋鈕,方向恰好相反。
取消會拉長邊。 每追加一條取消規則,圖上就多一條邊,枚舉出的路徑數會以乘法暴增。所以取消並不是"越寬鬆越好"。把取消放得越開,路徑就越爆炸,其中混入非預期的強力路徑(就像上一節那種迴圈路徑)的機率也隨之上升。格鬥遊戲傳統上把取消管得很嚴,動作 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。如果對二十個角色的佇列值各管各的,就分不清哪個是有意的差異、哪個是失誤。
最後還有一點要點明。到目前為止,我們只把邊當作"有/沒有"來處理,但即便邊存在,在那個節點上究竟如何切換到下一個節點,又是另一個決策。連招之間的連線方式有三個核心分支,雖然超出本書的深度、不用程式碼展開,但若不提及,規格就只畫了一半。
window: [10, 21])正是這個通知的表現。把通知提前,連招就變快(在這一擊結束之前就接下一擊);推後,則每一擊都更有分量。也就是說,視窗的起始值不是單純的數字,而是"這一擊要完整展示出來,還是快速切到下一招"這樣的手感決策。這三者面對同一條邊,卻能把遊戲手感做成截然相反。在規格階段只定邊的存在與視窗,連線方式(混合 vs 跳幀)通常留到構建版本時,與動畫師一起定。不過若在規格里預先留一行 transition: blend / transition: skip 這樣的欄位,就能在構建階段省去再問一遍"這條邊當初是定的怎麼切來著"。連線方式是連招圖裡那條隱藏的第三根軸。
到這裡為止的所有驗證,都發生在規格之上。路徑列舉、DPS 模擬、矛盾檢測,統統以 YAML 為物件。可規格里的幀數值是設計師的意圖值,不是構建版本的實測值。動畫師做出的蒙太奇的實際長度、構建版本里通知實際觸發的幀、輸入佇列在引擎裡實際生效的視窗 —— 這些得捕獲構建版本來測量。
要從構建版本的影片裡自動抽取 5 個訊號(打擊發生幀、後搖、取消視窗、輸入佇列、頓幀)實現難度很高,現實中最可信的是遊戲內遙測(telemetry)—— 讓構建版本內部打出"這個動作在這一幀接受了這個輸入"這樣的日誌,再把日誌與規格對照(捕獲方法的比較參見 4.4)。這條對照迴圈就是 4.4 的主題。如果規格上是 [12, 24] 的視窗,在構建版本里被測成 [14, 26],那就是用構建實測來校正規格。
| 錯誤 | 為何危險 | 迴避法 |
|---|---|---|
| 把連招寫成直線表格 | 分支·迴圈藏在行間被遺漏 | 用圖(節點+邊)做規格,不手畫 |
| 把取消籠統成"什麼都行" | 列舉路徑暴增,混入強力路徑 | 分別明示動作·閃避·防禦取消 |
| 列舉器沒有防迴圈護欄 | 閃避重新進入處陷入無限迴圈 | 每條路徑同一節點只走 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 模擬則把節點傷害以表格一併給出,用"計算每條路徑的總傷害與幀數,做成表給我"這樣的後續請求來收。精度會差一些,但比白板看得遠多了。核心始終不變 —— 連招別在腦海裡展開,讓它枚舉出來看。
戰鬥 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)。前面幾節(模擬器、連招列舉)可以讀作讓這種對照成為可能的準備步驟。
戰鬥策劃要驗證一個新技能,會發生什麼事?
策劃寫規格。程式設計師錄入資料,美術接上動作與特效,構建跑一遍,QA 過一輪,這才輪到策劃親手上手試。快則兩天,通常三四天。如果在迴圈末尾才發現「DPS 太高了」,這個發現就成了一道「回到起點重來」的命令。三四天再來一次。
模擬就是在這個迴圈的第一步給出答案的工具。僅憑規格就算一遍。答案不好就改規格,再算一遍。在進入「構建」這個昂貴的階段之前,規格本身先被過濾一次。這就像先把模型車放進風洞裡試一試。在把真車開上道路之前,可疑的設計在桌面上就被淘汰了。
當然,風洞並不能 100% 預測道路。所以才需要第二條軸線——捕獲分析。如果說模擬給出的是理想的答案,那麼捕獲給出的就是構建裡實際發生的答案。把兩者並排放在一起、讀出差異——這就是本章的全部。
抽象的虛擬碼什麼也驗證不了。所以從一開始就寫能跑的程式碼。下面是筆者在戰鬥 TF 中所用 simulate_dps.py 的核心骨架,為了載入本書而剝離了公司資料並加以重構。它不依賴任何外部庫,僅靠 Python 標準庫即可執行(完整檔案參見「動手試試」)。
輸入很簡單。一個技能是帶有 damage、cast_sec(施法佔用時間)、cooldown_sec、resource_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_dash、basic_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%,過頭了。無需等待構建,現在就可以去調 damage 或 cooldown_sec。
侷限也要誠實地寫明。這個模擬器不反映玩家的輸入失誤、因移動與閃避產生的空檔、敵人的干擾。所以測量值總是比實際構建偏高。這不是 bug,而是「上限線」這一模擬器定義本身。與實測之間的差距,在 4.4.5 中用捕獲來補上。
AI 運用筆記。 上面的骨架是筆者親手寫的,但要接入新的資源模型(例如怒氣槽在受到傷害時累積的結構)時,要像「請給這個 simulate_dps 加一條受擊時怒氣 +5 的規則。在 tick 迴圈內,作為獨立於現有資源恢復的變數」這樣引用現有程式碼來提出請求。如果讓它從白紙生成整個模擬器,只會得到無法驗證的程式碼。脊柱由人來定,AI 來添枝。
僅憑 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 個,人也只需讀那張排好序的表。
現在輪到第二條軸線。這是測量構建中實際發生了什麼的階段。人們常會聯想到「AI 看構建錄影自動分析」,但這裡要誠實地劃分支路。獲取測量值的路徑有三條,而三者的成本與精度差別很大。
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}
input 與 hit 的時刻差,就是測得的命中時機。12.560 − 12.340 = 0.220 秒 = 220ms。這不是畫素分析的 ±33ms,而是引擎時刻原樣的值。把這份日誌取出來與規格對照,就是下一節的內容。
現在把兩條軸線匯到一處。規格承諾了 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 位置。
對於 +70ms 超出,我推測以下四種原因。
- 網路延遲。 輸入往返伺服器時可能多出了 70ms。如果是多人構建,請確認 RTT。
- 幀率下降。 如果構建以 30fps 執行,1 幀為 33ms,通知時機會被四捨五入到幀邊界,延遲隨之累積。
- 施法動作時長變更。 cast_thunder 片段可能比規格變長,導致 HitWindow 通知被往後推。
- 輸入緩衝延遲。 輸入佇列可能晚一個 tick 才被處理。
可能性最高的原因是第 1 項網路延遲。70ms 是典型的 RTT 範圍。
在這裡停下,重新看日誌。AI 的第一推測(網路)在這份日誌裡已被排除。input 與 anim_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。僅憑這一事實縮小原因,並以行動為單位寫明該向程式設計師、動畫師請求什麼。 // 核心是「以行動為單位」——不是推測,而是以「向誰請求什麼」收尾。
根據所給依據,原因可縮小為動畫片段 cast_thunder 的 HitWindow 通知被放在了 0.220s 而非 0.150s。剩下兩條分支。
- (甲)通知對準 0.150,但整個片段變慢了 —— 如果播放速度(play rate)低於規格或片段時長變長,那麼 0.150 位置就會被拉伸到即時的 0.220。
- (乙)通知位置本身被挪到了 0.220 —— 片段時長不變,只是標記移動了。
區分兩條分支的確認方法: 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.2\~4.4.3)與捕獲分析(4.4.4\~4.4.5)若各跑各的,只值一半。當合為一體時,就形成了下面的迴圈。
由於模擬在構建之前給出答案,進入構建的規格已經被過濾過一次。所以構建之後發現的問題,性質就從「規格錯了」收窄為「規格與實現相左」。4.4.5 的 +70ms 正是後者——規格 150 是合理的,只是實現偏成了 220。這一區分消除了會議上的責任歸屬之爭。
不過這個迴圈並不能覆蓋所有戰鬥內容。像主 Boss 的招牌演出這種感覺即內容的領域,無法被還原成 DPS 數字。模擬擅長基於數值的內容,而演出依然得靠人眼的錄影評審來定奪。迴圈在數值之河裡打轉,演出之河則另自流淌。
這是筆者運營過的某款 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 兩者只要有一個跑起來就夠了。兩者扣成一個迴圈,是從第二個季度才開始的。
有五種反覆出現。
第一,絕對信任模擬值。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.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}")
GetTimeSeconds 等)原樣記下來是關鍵。prompt. 請從構建 telemetry 中挑一個與規格相左的項,按 4.4.5 的格式向 AI 提問——貼上日誌摘錄,並務必包含「不要把推測當定論,要寫明各原因的驗證方法,不確定就標為未確認」。
verify. 不要原樣採納 AI 的第一次輸出。請親自讀日誌,用手抹掉可排除的候選(像 4.4.5 排除網路、幀率那樣),附上依據後再次請求。當最終報告以「原因推測 + 向誰請求什麼」的行動為單位收尾時,再把它帶進會議。
單人精簡版。 如果全域性安裝 telemetry logger 有負擔,就只在你要驗證的那一個技能上打 input、hit 兩行。simulate_dps 也只看那一個技能的 DPS。不要鋪開整套工具,先用最可疑的一個技能把迴圈轉一圈,再擴充套件。
走進會議室,白板上有一個角色名字被紅筆圈了起來。是個叫金某的 NPC。在一位策劃的支線任務裡,他是"在主角小時候收養並把他養大的養父";而在另一位策劃的主線任務第 3 章裡,他卻是"背叛主角後離去的舊日同伴"。兩份文件都在一個月前通過了審批,兩者都已進入了構建。連配音錄製的報價都拿到了。
誰都沒有錯。兩位策劃都讀過世界觀文件,也都參考了角色設定。問題在於,同一個角色的設定散落在三個不同的檔案裡,而其中哪一個才是"真的",誰也無法斷言。世界觀文件是一整塊 70 頁的 Word 檔案,一搜索,金某出現在十一個地方。哪一行是決定、哪一行是備註,沒有任何區分。
那天會議結束後定下來的事情,就是把 NarrativeDocs 拆成五層。本章講的就是這五層的故事。
把按領域的 Layer 分解先從敘事開始,是有理由的。
敘事最抽象。世界觀、情感、基調這類東西落不到數字上。它不像美術或系統那樣有"精靈圖數量""傷害係數"這種明確的單位。如果連這樣抽象的領域都能被幹淨地分解為 Layer,那麼更具體的其他領域自然會用同樣的模式解開。這相當於先看難的地方能不能走通。
再者,敘事的介面最多。角色與美術相接,任務與內容、關卡相接,臺詞與 UX、本地化相接,獎勵與系統相接。它處在橫跨領域最多的位置,所以 Layer 統合的價值會立刻顯現出來。
最後,自然語言產出物的佔比最高。因此它也是 AI 輔助作用最大的領域。不過並非所有遊戲都以敘事為中心。如果是休閒、街機型別,本章的深度可能會顯得過頭。即便如此,"把一整塊文件拆成 Layer、收窄介面"這一骨架本身,可以原封不動地搬到任何領域。
把 NarrativeDocs 分解為五層後的樣子如下。願景(L0)在上,構建·QA(L4)在下,連線各層之間的通道有意收得很窄。
落到資料夾上就是下面這個形態。每個檔名都不是書中抽象出來的名字,而是實際存在於那個資料夾中的檔名。
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 之間的通道,故意畫得最窄(粗紅箭頭),其中的理由後面再看。
用抽屜來比喻,就是一個五格抽屜。第一格里放絕不挪動的世界觀一行,第二格里放規則手冊,第三格里放正文,第四格里放表格,第五格里放驗收日誌。格與格之間的通道很窄,通道之上掛著變更通知的鈴鐺。
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 中被修改時,若沒有主敘事的批准就無法合併。
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 是與遊戲程式碼最接近的敘事產出物,所以要與系統策劃結對作業。
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 表格。兩者若放在同一個檔案裡,每改一行表格都得動到正文,而那樣一來翻譯鍵就會錯位。
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 分離的核心。通道一寬,兩側就會對彼此知道得太多,改動一側時另一側就會跟著崩壞。
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 情感支柱契合 | 主敘事 |
| 角色配音一致性 | 角色負責人 + 敘事 |
| 本地化可行性 | 本地化人員 |
自動與人工的邊界一旦清晰,驗收時間就會變得可預測。"這一章的驗收要幾天?"也就能答得上來了。
到這裡為止是結構。然而這個結構真正發揮作用的地方,是在製作新內容時。在一個已經鋪好五層的專案裡,接到一條支線任務的一句話想法,把它撒進各 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 頁重新翻一遍。
把五層分開的真正理由,是為了收窄介面。而每一處窄介面都貼上變更檢測的自動化。
| 介面 | 流動的是什麼 |
|---|---|
| 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 分離本身,介面的自動化才是本質。
遷移到五層、執行 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,貼自動化花的時間更長。
到這裡為止是表面的理由。"統一各領域之間的協作語言。"然而還有一個更本質的理由。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 如何自動驗證世界觀→角色→任務的一致性,也就是從結構上攔下金某事故的檢查器。
假設你從已經有一整塊世界觀文件的狀態開始。
setup. 在 NarrativeDocs 資料夾下建立五個空資料夾。從 Layer0_Vision 到 Layer4_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)。
如果你是沒有團隊、獨自制作的獨立開發者,五層可能看起來過頭。兩格就夠了。
放一張 vision.md(L0+L1 合併)、一個 content/ 資料夾(L2),以及一張電子表格(L3)。QA 不另設一層,而是用在 content 檔案 frontmatter 裡只寫 requires: 一行的習慣來代替。每次寫新任務時,只把 requires 裡寫的檔案重新攤開對照,就能不翻遍 70 頁也攔下金某事故。核心不在層的數量,而在"把絕對不變的那一行單獨抽出來,每次都先給 AI 看"的習慣。那一行就是上下文錨點,無論是一個人還是中等規模的團隊,都從那裡開始。
臨近 Beta,QA 提交了一份 Bug 報告。標題是"國王說話不用敬語"。正文很短:"在 3.4 的開場過場動畫裡,K_001(國王)對玩家說了'喂,等一下'。這個角色從 1.1 到 3.3 一直用的是'閣下'。"
我去問編劇,得到的答覆出乎意料。"那句臺詞不是我寫的。"追查之後才發現,是一位外包編劇在趕工填補一條過場動畫分支時隨手加進去的。我們的角色聖經(character bible)裡本來就有 voice_profile,但那位外包編劇從沒看過那份文件。規則在文件裡,臺詞卻從文件外進來了。
這就是一致性事故的本質。它不是因為沒有規則,而是因為規則沒能一路跟進到正文裡。而如果這一句話出現在過場動畫裡,那就更可怕了。過場動畫通常會配上配音錄製。如果只是文本,改一次就完事;可一旦錄完音才發現,就會帶來重新召集配音演員、重新錄音、重新混音這種不可逆的成本。一致性驗證真正的目的,是"在錄音之前"把問題揪出來。
本章講的就是如何用規則手冊和檢查器、而不是人眼,來揪出這類事故的工作流。我們會原樣看真實產出:lore_consistency_rule 規則手冊如何成為檢查器的輸入,voice_lint 如何把語氣波動挑成可疑候選,以及為什麼最終判定無論如何都要留在人的位置上。
把上線 RPG·MMORPG 的使用者評論彙總起來看,敘事一致性事故會收斂成幾種固定模式。種類看似不同,原因卻幾乎只有一個。
這五種看上去像是五種不同的事故,但追查下去,它們都從同一個地方漏出去。是 Layer 0(世界前提)或 Layer 1(規則)變了,而這個變更沒有傳播到 Layer 2(正文)和 Layer 3(資料表)。規則更新了,正文卻還停留在舊規則之上。
想靠人工評審來堵住它是不現實的。一個章節裡牽連著 50 名 NPC、2,000 行臺詞、30 個任務,當你改動一行規則時,要讓人去 100% 追蹤它的影響波及到哪裡,這是不可能的。漏掉的那一行在評審階段不會被抓到,會在上線後的評論區裡被抓到。
可話說回來,自動檢查也並不能保證 100%。關鍵在於職責分工。自動檢查負責快速挑出可疑候選,判定由人來做。 自動化的目的是減少人工評審的時間,而不是取消人。一旦這個前提被模糊掉,後面要講的所有失敗都會接踵而至。
專案A 的 L1 文件之一就是 lore_consistency_rule.md。這份文件既是供人閱讀的指南,同時也是檢查器解析的輸入。frontmatter 裡的 atoms 和 affects 把這兩個角色綁在了同一份文件上。
---
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 是"閣下",臺詞裡卻出現了"喂",那它就不是意見,而是規則違反的候選。
變更發生的那一刻,檢查器就被觸發。流程如下。
最後那個分支才是本章隱藏的脊柱。所有一致性判定都必須在文本階段、也就是可逆階段就結束。一旦評審推到錄音·選角之後,修改就變成不可逆的了。所以 voice_lint·timeline_lint 這類檢查器,關鍵不在於跑得"快",而在於跑得"早"。過場動畫臺詞在進入錄音佇列之前,至少要通過一次。
檢查器共有四種,各自與規則手冊的一個章節一一對應。
world_rule_lint.py —— L1 世界規則 + 所有 L2 正文 → 魔法使用·神回應等違反候選voice_lint.py —— voice_profile + dialogue_id_table → 語氣波動的可疑臺詞timeline_lint.py —— npc status_timeline + 所有臺詞·登場時點 → 死亡 NPC 重新登場等衝突faction_lint.py —— faction_relation_matrix + dialogue tone → 關係矛盾的臺詞四個檢查器都不是 100% 準確。所以輸出的名字不叫"違反",而叫"可疑候選"。
光說"有檢查器"這種抽象的話,沒什麼感覺。我們實際跑一遍。下面是復現前面"國王不用敬語"事故的輸入。
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 —— 編劇在變更請求裡原樣收到這條評論。判定由編劇來做。
emotion_peak_exception 標記,把 217 登記為例外兩個候選是同一個檢查器挑出來的,結局卻完全相反。一個是改正文,一個是改規則。機器無法自動完成這個分叉,正是下一節的要點。
檢查器只挑到可疑為止、把判定交出去,有三個理由。
第一,存在有意為之的違反。 在角色崩潰或轉變的章節裡,語氣會被有意地動搖。上面的 217 就是如此。自動拒絕型的檢查器會把編劇的演出意圖給擋掉。
第二,規則本身在進化。 如果同一類可疑總是被判定為"有意為之的變化",那就是規則跟不上現實的訊號。檢查結果不只是讓人去改正文,也讓人去改規則手冊。
第三,新角色·新勢力需要一個學習區間。 voice_profile 才填了兩三個專案的新 NPC,冒出很多可疑是正常的。在這個時期如果上了自動拒絕,編劇就會把檢查器當成敵人。
自動檢查和人工判定的邊界足夠清晰,檢查器才能活下來。要是做成自動拒絕型,一個月之內編劇們就會說"把這玩意兒關了吧"。這就像給公司大門裝一個太過靈敏的自動感應器,每次有人經過門就關上,最後總會有人把感應器拆掉。檢查器不該是關門的裝置,而該是"有人從這裡經過了"的提示裝置。
再補充一個註腳。評審必須在文本階段結束這條原則(前面流程圖裡的阻斷線),同樣適用於人工判定。編劇的"有意為之的違反"判定也得在錄音之前完成。錄音之後再翻案,問題就不再是檢查器的問題,而變成了工序成本的問題(可逆/不可逆邊界的全貌見 5.4.5)。
在專案A 中,我們分階段引入了 4 種檢查器,並測量了 6 個月。下面是基於實測日誌,但用方向·比例代替絕對值來呈現的(公司內部測量,非作者推測)。
最後一項最有意思。有了檢查器,規則就算頻繁修改也很安全。改一行規則,它的影響會被自動視覺化,於是變更的恐懼減少了,規則進化得更快了。一致性工具真正的效果,與其說是"減少了事故",不如說更接近於"能夠無所畏懼地修改規則了"。
不過,上面這些數字是 4 種檢查器全部運轉時的數字。更重要的一點是,在引入初期,光是 voice_lint 一個就已經產生了可見的效果。沒必要一開始就把 4 種全都開啟。
自動檢查器的本體,基於規則更高效。同樣的輸入要得到同樣的結果,信任才會積累,而 LLM 是非確定性的,不適合放在那個位置。AI 放進另外四個位置。
規則快速且確定,LLM 擅長說明與生成。把這兩者的角色混在一起,兩邊都會壞掉。把檢查交給 LLM,就會出現同一句臺詞昨天通過、今天卻被攔下的情況;把說明交給正則,就只能得到"honorific 專案違反"這種機器語。
一開始就把 4 種檢查器全做出來,負擔會先於效果到來。推薦的順序是從最便宜·效果最大的開始。
要強調的一點是,光是第 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 負責說明這個核心結構照樣活著。只要守住一條就行——在交給錄音·語音合成之前,讓這道審查走一遍。不越過可逆階段這條原則,與團隊規模無關。
那天我在為一個新的支線 NPC 草擬第一句臺詞。我在空白的聊天框裡敲下"幫我寫 5 句村莊鐵匠 NPC 的臺詞"。5 秒後,螢幕上跳出"勇士啊,把你的武器交給我吧"。那不是一種似曾相識,而是一種我幾乎能說出確切在哪兒見過的句子。把同樣的提示詞放進別的團隊的別的遊戲裡,也會得到一模一樣的回答。那一刻我意識到的並不是模型弱,而是我什麼都沒告訴模型——我沒讓它瞭解我們的遊戲。
AI 很會寫一般化的奇幻句子。可它寫不出我們這個世界的句子。差別只有一個,就是上下文注入。每次請求時把 L0 基調和 L1 規則一併送進去,AI 吐出的那一行就會從"似曾相識的句子"變成"這個遊戲的句子"。本章講的是把這種注入按四層來運營的實務,最後把同一原理拔高到世界模擬的規模,把更進取的應用(世界 BT(BehaviorTree,行為樹)+ 任務雲)作為研發最前沿來點明。
在敘事領域,AI 輔助是匯入最快、也最快失去信任的環節。因為它的失敗模式幾乎一模一樣。
你扔一句"給我 5 句任務開場臺詞",回來的就是 5 種以"勇士啊,我們村子……"開頭的一般化奇幻句。你說一句"幫我改改這個角色的臺詞",聲音就被抹平,所有 NPC 都收斂成相似的語氣。你說一句"幫我寫第 1 章梗概",拿到的就是你見過的那些 RPG 梗概的平均值。
問題不在模型,而在上下文是空的。模型輸出的是訓練資料的平均值。不想要平均值,就得給它一個能遠離平均值的線索。本章的主題就是這個線索怎麼造、怎麼注入。
在筆者運營的 MMORPG 專案(以下稱專案A)裡,敘事 AI 輔助會按順序堆疊四層上下文。5.1 中把 NarrativeDocs 拆解為 Layer 0\~4 的那套結構,在這裡被原樣複用為注入單元。
四層並不是每次都全部塞進去。按任務型別只取出需要的層。如果是一個角色的下一句臺詞草稿,A + B(只要基調)+ D(那個角色最近的 10 行臺詞)就夠了。如果是新支線任務的梗概,就追加 C(quest 結構規則)。如果是分支結果 4 案,C(分支規則)+ D(分支前的整段正文)就會變重。這就好比從書桌上的檔案櫃裡,按任務大小挑出人設表、世界觀一行、規則手冊某頁、鄰近正文一沓,再發出去。
實操記錄(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 沒有最終決定哪怕一行。
鋪在最上面的是人設定義。定一次,幾乎不再改。上面那段實操記錄裡的系統區塊就是它的實物。五行裡最後一行("寫草稿,決定由作者來做")最重要。這行一缺,AI 就會自信地端出裝作"最終版"的句子,而作者就從驗收變成了打分。其次重要的是第三行("不知道的設定別編,就回答無法判斷")。沒有這行,模型就會用貌似合理的謊言去填空。在敘事裡,貌似合理的謊言會在幾天後以世界設定衝突的形式回來。
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 部分(成本)章節裡講。這裡只要記住"把會變的趕到後面"這條原理就夠了。
L1 規則手冊篇幅大,全塞進去上下文會撐爆,更糟的是模型會抓不住要點。只挑與任務相關的規則,而且只挑 _summary 節。
抽取主線任務分支結果時,挑 dialogue_branching_rule 和 faction_relation_matrix。新 NPC 臺詞,就挑對應 NPC 的 voice_profile 和 tone_manifesto。世界設定詞典新條目,就挑 lore_consistency_rule 和 world_premise。支線任務骨架,就挑 quest_template 和 reputation_model。選擇由人親手做,或沿著 wikilink 圖(第 7 部分)自動抽取;自動抽取時,召回率優先於精確率。因為漏掉一條規則的損失,遠大於多放一條規則的損失。
不把規則手冊正文全塞進去,而是在規則手冊檔案開頭放一個 _summary 節,只注入它。
---
title: 分支規則
layer: L1
---
## _summary
- 分支只在章節末尾發生
- 分支為 2~3 案。禁止 4 案及以上
- 分支選擇對聲望產生 +/-1 影響,結局分支為 +/-3
- 所有分支結果必須在 24 小時內呈現結果
- 分支不可撤銷(建議分離存檔,UI 提示)
## 1. 分支發生時點規則
(詳細說明,供運營者參考 —— 不注入 LLM)
...
_summary 的 5 行,比正文 50 行對 LLM 輸出質量更有效。模型更能遵守簡短而斬釘截鐵的規則。冗長的說明會分散模型的注意力,而被分散的注意力會以違反規則的形式回來。
上一句臺詞、鄰近任務、同一章梗概。這是變動最大的上下文。同一角色的新臺詞,就按時間順序放那個角色的上 10 行臺詞;章節中段的任務,就放章節梗概和同一章任務的 1 行摘要;分支結果結局,就放分支前的整段正文和選項文本。放太多,LLM 就輸出平均值;放太少,就給出一般化的輸出。合適的區間在 1,500\~3,000 token 之間(以筆者觀察為準,未經驗證)。
一條核心規則:鄰近正文不加工、不摘要,原樣不動地放進去。上面那段實操記錄裡,學者的上 5 句臺詞原樣不動地放了進去,所以模型才能在重新請求階段精準地抓住"記錄"這個詞,把它再利用為自責的媒介。如果當初把那 5 句摘要成"學者謹慎而冷淡"再放進去,作者那些細微的選擇就會全部消失,模型也就重新回到平均值。摘要不是在減少資訊,而是在抹掉作者已經做出的決定。
AI 輸出永遠是草稿。驗收要通過既定的關卡。
作者從 N 個裡挑出 0 個(全部廢棄)也是正常的。正如上面那段實操記錄裡 3 案被否決,否決不是失敗,而是關卡起了作用的證據。所以要按作者、按角色測量廢棄率,把它作為上下文注入質量的指標。
廢棄率在 0\~20% 是上下文充分的穩定運營,就照原樣放著。20\~50% 是一般運營區間,只做監控。升到 50\~80% 就重新檢查是不是漏選了 L1 規則。超過 80% 就不是個別規則的問題,而是系統提示詞、人設本身錯位了,於是要重寫 Layer A。廢棄率每週按作者彙總一次,在覆盤裡共享。
不過廢棄率不是絕對指標。變化快的角色(例如上面實操記錄裡的 K_007,處於第一次流露情感的轉折點)即使廢棄率高也正常。數字是對話的起點,不是判決。
L0\~L1 是遊戲的核心 IP。要是把它們原樣發給外部 LLM API 讓人放心不下,選項就出現分岔。把外部 API 在"不用於訓練"的合同下原樣使用最快,但需要法務審查。把公司名、專有名詞替換成 placeholder 再發,會增加額外處理成本,也會損傷自然度。自託管(開源模型)資料安全,但質量與運營負擔大。把 L0 留在內部、只把草稿送往外部的混合方案,運營複雜。
筆者的專案A 採用第一種方案(外部 API + 不用於訓練合同)。第二種方案試過後放棄了。因為 placeholder 替換會把正文抹平成"○○ 王國的 ○○ 學者就 ○○ 說了……"這種形態,把輸出質量整個搞垮。匿名化扼殺質量,是貫穿全書反覆出現的取捨(參見第 1 部分匿名化章節)。在敘事裡這種損傷尤其大。因為專有名詞本身就是基調。
不給系統提示詞、只扔任務指令,出來的就是平均值。先鋪好人設與禁忌。每次都整段注入 L0 卻不用快取,成本就會漏。把快取塊聚到前面。把 L1 規則手冊整個塞進去,模型就抓不住要點。只抽 _summary 節。把鄰近正文摘要後放進去,作者的選擇就被抹掉。原樣引用。不定輸出格式,相當一部分回答就會以"這裡是 3 個候選:"開頭,甚至會跟來作者把那段開場白誤當正文的事故。明確標出數量、長度、標籤。把 AI 輸出當最終版用,驗收關卡就垮了。永遠讓它通過作者關卡。不測廢棄率,工具的健康度就依賴人的印象。按周彙總,在覆盤裡共享。
到此為止都是保守的應用。作者用心地注入上下文,AI 只做一行一行的草稿。單位是"這個角色的下一句臺詞 3 案""這個任務的梗概"這類小活兒。穩定,但在量產規模和動態反應性上有侷限。
這裡先點明一件事。程式化生成、世界模擬、動態任務,是策劃們二三十年前就在紙上畫出的願景。基於確定性規則手冊的 PCG 處理過副本房間、武器屬性、刷怪分佈這類數值領域,卻碰不到自然語言正文、角色人設、敘事分支、NPC 對話。也就是說,策劃的相當一部分一直停留在紙上。2024\~2026 年 LLM 與影像模型的進步,把那片領域拉到了可實現的位置。AI 進步的核心意義不在於模型分數,而在於那些長久停留在紙上的策劃,其可實現性被打開了。只不過,可能性被開啟,和落地為可運營的系統,是兩回事。
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 上。
觸發要經過五個階段。
打個比方,任務不是插在圖書館書架上,而是飄在空中的雲。NPC 一旦到達某個狀態,適合它的那朵雲就會降下來,落到手裡。在這個結構裡,AI 不是寫一行的助手,而是同時處理三件事:程式生成的 Persona、任務的自然語言正文(說明·臺詞),世界 BT 狀態反映進 NPC 時的詞彙變動(保持同一個 voice_profile,但話題隨世界狀態而變動),以及驗證階段的可疑分類器(這個任務可不可以在這個 NPC 身上觸發)。
進取應用裡,可逆/不可逆邊界(5.4.5)依然成立。雲中觸發的任務臺詞若未通過驗收就流向語音流水線,程式碼回滾也救不回來。所以把所有驗收關卡(voice_lint、可疑分類器、作者關卡)放在不可逆邊界之前,是進取應用的安全裝置。自動量產疊加得越多,這道關卡就要比保守應用更硬。
本書不涉及進取應用的完成形態。那要另寫一本書的篇幅,而且每家公司、每個專案的基礎設施前提都不同。只要記住兩點。首先,做不好保守應用的團隊,進取應用也做不了。保守應用裡驗收關卡轉不起來,進取應用裡雲就會失控。要讓運營存活,程式化生成基礎設施、世界 BT 節點定義與測試工具、觸發任務的自動可疑分類 + 作者關卡、觸發率·廢棄率·玩家滿意度的同步追蹤、誤觸發任務的自動回收與替換,這些必須一併具備。五者缺一,就會在一個季度內被廢棄。因為一致性事故暴增,人工驗收跟不上。
其次,進取應用不是增加量產的工具,而是增加動態反應性的工具。只盯著量產,雲裡就會塞滿一般 RPG 的平均值,玩家遇到的是一個"看上去更多樣、卻更空"的世界。話雖如此,這條路也並非只有風險。這片領域是遊戲策劃研發的最前沿。在程式化生成、模擬、LLM 相遇的地方,最有可能誕生真正全新的遊戲形態。即便眼下很少會在公司匯入,也值得把它當作未來 5\~10 年的方向之一來留意。
下面是把本章的保守應用原樣再現的步驟。
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"這樣用手數著記就夠了。工具再小,四層注入與驗收關卡這套骨架,對單人作者也同樣有效。
voice_profile,並對聲音做驗收錄音棚裡。導演戴著耳機,抬手給出提示。配音演員讀出學者 NPC 的臺詞。"哇,真是太厲害了。"導演的手停住了。這個角色在整整 5 個章節裡,一次都沒用過"哇"這樣的感嘆詞。他是個連髒話、連現代感嘆詞都不會掛在嘴邊、說話總是把話尾壓低的學者。可臺詞稿上卻原樣寫著這一句。
這裡同時產生了兩筆成本。第一,要改這一句,配音演員就得重讀。出場費和錄音棚的時間,鐘錶早已在轉動。第二,更可怕的是導演沒能抓住這一句、就這麼放過去的情形。錄音結束、音源進入構建之後,那位學者在遊戲裡就永遠那樣說話了。錄下來的音源不像文本那樣可以靠一行改動撤回。必須重新約同一位配音演員、同樣的狀態、同一個錄音棚。
本章討論的就是那個錄音棚門前的虛線。虛線之上是文本,可以無限次修改;虛線之下是音源,改不了。所有臺詞的審校都必須在虛線之上結束。要做到這一點,每個角色"這個人是這樣說話的"就必須不是記在腦子裡,而是記錄成檔案,並且每當有新臺詞上來,都要自動與那份檔案比對。那份檔案稱為 voice_profile,比對工具稱為 voice_lint。
敘事一致性事故中,最常爆發的就是角色語音的動搖。往往要等到上線之後,才會收到"這個 NPC 怎麼突然語氣變了"的反饋。原因每次都幾乎相同。
作者換人,同一個 NPC 就變成了另一個人。即便作者不換,過了 6 個月,本人也會忘掉自己定下的語氣。寫新臺詞時若不翻開那個角色之前的臺詞,上下文就斷了。語音規則只在作者腦子裡、沒有落成文件,就傳不到下一個人手中。還有最近兩年裡增長最快的第五個原因——不帶任何角色資訊就把"幫我寫這個 NPC 的臺詞"丟給 LLM,AI 會返回一段平均而無可挑剔、因而不屬於任何人的臺詞。
第五個原因是引入 AI 帶來的副作用。上一章(5.3)的上下文注入是處方,但要是注入的上下文本身就單薄,那就沒什麼可注入的。那個上下文正是 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 才會看到同一套標準。
若用翻譯·本地化的眼光重新審視這五個專案,會分成兩類。語言相關屬性(詞彙範圍·句子長度·敬稱體系的表層形態)必須隨翻譯的每種語言重新設定;而語言無關屬性(是剋制情感還是直接表達、什麼話始終不說出口這樣的態度)則無論譯成哪種語言都必須照樣守住。把本地化工作交付出去時若一併把這個區分傳過去,就能避免譯者只改表層詞彙、卻連角色態度都動搖了的情況。
一開始就從空白開始寫 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 鋪出初稿、作者來固定的。
只要 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 是標示可疑之處的手電筒,不是鎖門的鎖。
本章的脊樑是這一張圖。一句臺詞從作者手中出發、走到配音演員口中的途中,審校關卡逐級鋪開。而在那條流程正中央,有一條粗虛線。
綠色是可逆階段,紅色是不可逆階段。虛線之上(綠色)全是文本。一句臺詞不滿意,用鍵盤改就行。成本是作者的幾分鐘。虛線之下(紅色)是音源。配音演員在錄音棚裡讀了那一句、音源進入構建的那一刻,那句臺詞就作為資產固定下來了。要改的話,必須重新約同一位配音演員、同樣的狀態、同一個錄音棚,出場費·錄音棚·導演時間會和第一次一模一樣地再花一遍。日程緊的話,同一位配音演員的追加場次本身可能都約不上。
所以只有一條規則統轄著所有工作流——所有審校關卡都在虛線之上結束。錄音不是審校階段。它是把審校全部完成的結果固定為資產的階段。若在虛線之下冒出"這句臺詞不太對勁"的疑問,那不是還要再審校的位置,而是上一級審校有遺漏的訊號。手電筒必須在虛線之上全部照完。錄音棚不是可以漆黑的地方,而是不可漆黑的地方。
圖正中央,主敘事抽樣審校定為"每章 10% 樣本"。這個比例是審校時間與準確度的平衡點(作者運營值,未驗證的估計)。降到 5% 以下事故會漏出,提到 20% 以上則單個主敘事會成為瓶頸。因為 lint 已經預先篩掉了違規·可疑,樣本就從 lint 通過的部分裡抽——人眼專注於工具抓不到的語境錯誤(例如:看似正當的"命運"引用其實是角色崩塌)。
角色若從頭到尾一個樣地說話,劇情就停滯了。經歷了同伴之死的學者,若還用之前一模一樣的語氣說話,反倒是假的。若變化是有意為之,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 裡用一張地圖講清——它是方向標,不是處方)。
臺詞被譯成多種語言、再配上配音演員的語音之後,管理單位會成倍增加。一行韓文分岔成英文·東南亞各語言,每一種又都承載著語氣。
翻譯一致性裡最常漏的地方,是同一個表達在每一章被翻得不一樣(用翻譯記憶一致性檢查來抓)。角色 voice_profile 未反映到翻譯裡(另附按角色的翻譯指南)、新詞彙未登記進術語集(術語集 lint)緊隨其後。翻譯指南從 voice_profile 自動生成——把"這個角色格式體 100%、無感嘆詞、神秘主義詞彙為禁忌"自動附在翻譯指示書的開頭。譯者把那位學者譯成英文時,看到的是同一道邊界。
VA(Voice Actor,配音演員)審校,是觸及虛線之下前、最後一道文本可逆階段的審校。語氣一致性(憤怒·悲傷表達的強度)由導演和敘事來看,發音準確性(專有名詞)由術語集負責人來看,氣息·停頓(profile 中"常見短促停頓"之類的指示)由導演來看。審校結果記錄在 voice_review_log.md(L4)裡,以通過·駁回標註,下次角色選角時參考。
駁回儘可能在選角·錄音之前結束。在錄音棚裡發現的臺詞稿錯誤,會把那天的整場錄製整個搞垮,連帶動搖下一場的日程。但靠推遲錄音來關掉審校並不是答案。如果審校頻頻卡在錄音棚前,那是上一級(作者·主敘事)工作流晚了,而不是錄音日程的問題。
在專案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 天。運營成本小,系統才能存活。運營沉重的工具會在一個季度內悄悄被廢棄。
| 模式 | 處方 |
|---|---|
| profile 裡只有抽象描述("沉穩的") | 每個 5 專案強制附實際臺詞示例 |
| 一開始就想著完整寫滿 50 名 | 主要 7 名完整 + 其餘從正文累積反向提取 |
| voice_lint 自動駁回型 | 違規·可疑 + 作者判定。駁回只能由人 |
| 機械地擴大禁忌 | 在禁忌處一併寫明例外語境("可作反駁引用") |
| 角色變化時 profile 未更新 | 版本管理(v1·v2·v3),按章節編號套用 |
| 翻譯中未傳達 profile | 從 profile 自動生成翻譯指南 |
| 錄音後試圖修改臺詞 | 錄音不可逆。審校在虛線之上收尾 |
| 把審校壓縮排錄音日程 | 以改進上一級工作流來解決 |
| profile 只存在腦子裡 | 一律落成檔案。腦子裡的東西會隨作者換人而消失 |
新章節臺詞上來時,用一份 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)之前,只要讓它過這一遍,錄音棚門前的虛線就守住了。
週一早晨的策劃會議。白板上寫著一行字:"上線前要做 1,000 個支線任務。"有人按起了計算器。一名作者每個任務花一天,就是四年;哪怕五個人一起上,也接近一年。房間裡的空氣沉了下來。在這個房間裡坐了 24 年的我知道,面對這個數字,人們總是分成同樣的兩派:一派說"砍掉一些量",另一派說"用工具量產"。而幾乎每一次,結論都是兩者都要。
程式化內容生成(Procedural Content Generation,以下簡稱 PCG)正是"用工具量產"那一派的老答案。副本房間的佈局、武器詞條的組合、敵人重新整理池,早在 20 年前就靠規則手冊和機率表實現了自動化。新出現的並不是 PCG 本身,而是在自然語言、影像、敘事進入的位置上,換成了 LLM 與生成模型。
不過,本書想說的並不是"把 AI 接到 PCG 上"。那誰都會做。問題在於接在哪裡。面對一整塊內容,如果不把它落在自動化的哪一檔強度、結構的哪一層交匯的那"一格"上釘死,就會陷入有工具卻沒有位置的狀態。本章要看的,是如何把那"一格"畫成座標,以及在那一格之上,一塊內容如何真正地在流水線上跑完一圈。
傳統 PCG 擅長確定性。相同的輸入產生相同的輸出,並且可以驗證。副本房間圖、武器詞條的 prefix·suffix、敵人重新整理分佈因此很早就站穩了腳跟。"烈焰之劍 +5"在 20 年前就能自動生成。
問題總是出在緊接著的那個位置。房間佈置好了,但房間裡 NPC 的名字、外形、簡短背景仍留在作者手裡。"烈焰之劍 +5"能生成,可"國王遺失的最後一柄劍"這一句卻生成不出來。哪怕任務 generator 拼出了目標與獎勵的組合,"為什麼要做這個任務"仍要由人來寫。
在體量大的遊戲裡,這個位置一直是瓶頸。可量產的部分與需要人工的部分,比例大約是 4 比 6,而那需要人工的 6 成吃掉了排期的大半。哪怕量產線快速產出那 4 成,只要 6 成跟不上,整個週期就被拖到那個速度上。
LLM 和影像模型進入的位置恰好就在那裡。規則手冊處理不了的自然語言、敘事、視覺領域,也被納入可量產的範圍。但這並不意味著把這個位置整個交給 AI 就是答案。AI 每次給出的答案都略有不同,一旦上下文為空,就會吐出一般 RPG 的平均值。因此需要設計結合點。結合點由兩條座標軸來定義。
縱軸講的是,把人、規則手冊和 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 個還要貴。
只靠縱軸,量產線是轉不起來的。內容本身必須被拆解成層,自動化才有可進入的位置。這就是第 5 部分講過的 Layer 拆解,也是內容領域裡的橫軸。五個層各自對應程式化生成的一種角色(錨點·規則手冊·正文·數值·關卡)這一通用說明已在 §2.3.6 講過,這裡直接套用到內容量產線上。Layer 0 願景是基調與世界觀錨點(每次生成都注入),Layer 1 系統是生成規則手冊(規則·機率表·標籤體系),Layer 2 內容是生成結果堆積的正文位置(支線任務·NPC 背景·城市介紹文),Layer 3 資料是數值·ID·關係(獎勵·重新整理·曲線),Layer 4 構建·QA 是驗證關卡(lint·一致性檢查·作者評審)。
這兩條軸講的是不同的事。縱軸說的是"人插手多少",橫軸說的是"內容的哪個部位"。而兩者只有相乘才有意義。只有把一塊內容釘在兩軸的交點,也就是一格上,"這由誰、在哪裡、怎麼做"才算定下來。
把一路用文字講下來的兩條軸,疊成一張格子看看。橫向是內容的 Layer,縱向是自動化強度。每一格里的標籤,是專案A中實際佔據那一格的內容。顏色越深的格子,越接近量產線的重心。
這張格子是本章的核心。原本散落在文字裡的"主線是 L0""支線是 L2""獎勵歸規則手冊"之類的判斷,匯聚到一個座標上。會議上有新內容進入議題時,只需問一句"這是哪一格"就夠了。格子一旦定下,那格的縱座標告訴你誰來插手,橫座標告訴你是哪個部位。
讀這張格子,有兩點會映入眼簾。第一,重心(深色格)在 L2 行 × Layer 2 列。支線任務骨架、NPC 背景就在那一格,是量產線的心臟。第二,一塊內容並不只待在一格里。支線任務的正文(Layer 2)在 L2 格,但它的獎勵數值(Layer 3)則下沉到 L1 格。同一個任務,不同部位住在不同格子。這正是把兩軸分開的理由。
在重心格 —— L2 行 × Layer 2 列,支線任務骨架 —— 之上,看一塊內容如何真正跑完一圈。流程如下。
我們用一份實操記錄(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 個內容全都跑這個迴圈。格子既已定下,就不必每次再為"這由誰來做"爭論一遍。
要決定把新內容放進格子的哪一格,五個問題會有幫助。每當會議上出現量產議題時,把它們記下來一起作答,格子分配的一致性會在一個季度內穩定下來。
一、量產負擔有多大。上線前需要 N 個?N 一旦超過 100,L0 行幾乎不可能。
二、一致性要求有多高。若內容之間的一致性是體驗的核心,驗證關卡(Layer 4)就得夠強;若多樣性才是核心,就有往更上面的行走的餘地。
三、能否容忍非確定性。這是每次略有不同的結果會帶來豐富度的領域,還是相同結果才是信任核心的領域。
四、評審成本是多少。每塊內容是 5 分鐘還是 30 分鐘,決定了運作週期的長短。
五、出事時的成本是多少。是可以自由廢棄、重寫,還是一旦放出去就直接釀成使用者事故。
把這五問拋給支線任務,答案會朝一個方向匯聚。1,000 個以上(L0 不可能)、一致性低於主線、容許非確定、評審 5\~10 分鐘、事故成本低(可單獨廢棄)。五個答案一匯合,L2 行 × Layer 2 格便順理成章。把同樣的五問拋給主線任務,則朝相反方向匯聚。50 個、一致性與敘事深度最高、不容許非確定、評審成本大、事故成本極大 —— 就是 L0 格。
即便畫好了格子,會掉進去的陷阱也大同小異,反覆出現的有四個。
第一,從 L3 行起步。 抱著"AI 自動搞定 100 個"的期待出發,評審就會暴增。先讓 L1 格落地,再往上升到 L2,L3 只在一部分內容上謹慎使用。上面那條迷你流水線裡,人刪掉獎勵欄位的那一個動作,小小地展示了 L3 行為何危險。
第二,不要規則手冊,把整件事整個交給 AI。 "幫我做 100 個支線任務"招來的是一般 RPG 的平均值。必須先由 Layer 1 規則手冊搭好骨架,AI 在其上填血肉,才能產出屬於我們這款遊戲的內容。寫一本規則手冊,是 PCG 裡最費工、最無趣的活兒,但一旦跳過它,其上的所有量產都會塌回平均值。
第三,驗證關卡(Layer 4)空缺。 AI 輸出自動進入構建,就會直接引發一致性事故。無論哪一格,人工關卡都是必需的。
第四,只看成本就定工具。 LLM API 成本每個季度都在下降,但一致性事故的成本不會下降。定工具時,要在 API 成本之上,再加上一致性與評審時間的總和來看。
在專案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。
從這次度量裡要帶走一點。遷格子的決定,必須讓吞吐量、作者時間的分配、滿意度一起跟上。只看吞吐量,量產是成功了,人卻走了。
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 資料始終是一整塊,下一章的具體工具也站不住腳。
下一章會解剖一個佔據這張格子重心格的具體工具:量產各城市狩獵場的 proj_city_hunting_generator。看輸入後設資料、規則手冊骨架、AI 正文、驗證關卡如何被綁成一個迴圈,以及本章的迷你流水線在真實工具的規模上如何放大。
setup. 選一種量產候選內容(例如支線任務)。把 Layer 0 的一句基調和 Layer 1 的規則手冊骨架(槽位定義)拆成單獨的檔案。獎勵數值槽在規則手冊一側留空。
prompt. 先把願景作為上下文輸入,再給出骨架,並明確寫上"不要編造獎勵數值,輸出為 JSON"。把上面第 2 步的提示詞直接改一改用就行。
verify. 看三點。(1)AI 若擅自加了獎勵欄位,就刪掉(L3 是規則手冊的位置)。(2)有世界觀之外的詞,就打回到骨架步驟。(3)只有通過的部分,才由規則手冊填上獎勵數值並納入構建。
單人精簡版. 沒有團隊也行。你自己用文本檔案做出一本規則手冊(5 個槽位)和一句基調就夠了。把 10 個任務用上面的迴圈跑一遍,數一數評審時打回了幾個。打回率若超過 30%,說明格子選錯了 —— 要麼把規則手冊骨架做得更細密,要麼下降一行(L1)重新看。打回率一旦穩定,那就是在你自身的規模上、這一格能運轉的訊號。
主要讀者:負責內容量產的 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 等)、公司專有名稱已替換為書中用名。輸出正文是對真實會話的重現。
工具的整體流程分為四個階段。關鍵在於:第 1 階段和第 3 階段是確定性的(規則手冊),只有第 2 階段是 AI。規則手冊從兩端把住骨架與驗證,即便夾在中間的 AI 每次給出略有不同的答案,城市之間的一致性也不會動搖。人只介入最初的輸入(後設資料)和最後的關卡(稽核)。
在這張圖裡,人工觸及的地方只有兩處:最上方把一頁後設資料乾淨地填進去的位置,以及最下方對 lint 抓不到的語氣、敘事作出判斷的位置。中間那些枯燥的骨架生成與正文量產,交給規則手冊和 AI 去跑。有一處關鍵設計:lint(第 3 階段)即便發現違規也不自動廢棄,只把 alert 上報給文案關卡(第 4 階段)——理由將在 §6.2.5 中說明。
文案為每座城市寫一頁後設資料。撰寫時間 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 重名」。
規則手冊接收後設資料,生成城市的結構骨架。程式碼很簡單。
# 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 的工作。
規則手冊搭好骨架之後,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 中選取」的約束。
那麼實際會產出什麼?我們完整地跑一遍。
如果只是抽象地寫「AI 填充正文」,就無法知道這個工具究竟吐出了什麼。我們把 silvermark 這一座城市的迴圈,從輸入一直跟到廢棄,完整地走一遍。
西爾弗馬克(Silvermark)是一座封印之城。100 年前,七位學者為阻止西境魔力的泛濫,在這裡的地下立起了封印陣,而城市便在那座陣上,一塊石頭一塊石頭地壘砌、生長起來。學者公會總部的鐘樓是研讀封印脈絡的觀測所,而非朝向神明的尖塔。最近 3 年,觀測記錄裡反覆寫著同一行字:「脈在變冷。」封印減弱的最初征兆,不在別處,正是在這座城市的儀表盤上第一次顫動。街上的學者們不會大聲談論此事,只是鐘樓的火整夜不熄的日子,多了起來。
這裡,3 個 lore_seeds(封印中心地、減弱的最初征兆、學者公會總部)都寫進了正文,沒有「歡迎冒險者的和平村莊」這類套話,scholarly_strict 語氣(剋制抒情,使用觀測、記錄類詞彙)也得到了保持。這就是一份能夠通過的輸出該有的樣子。
接著,同一個迴圈填充狩獵場 hg_0 的 6 格 NPC 槽位中的 3 格。
1)多倫·維爾(Doren Vale) —— 鐘樓觀測助理。50 多歲,一隻眼上永久裝著黃銅放大鏡。記錄封印脈絡細微震動的工作,已經做了 30 年。話很少,只用數字交談。「今天 12.4。昨天 12.1。在往上走。不是好事。」
2)米拉·科斯特(Mira Kost) —— 公會檔案庫管理員。30 多歲,墨漬再也擦不掉地留在手指上。她守護著封印陣的設計原本,卻堅信能讀懂那張圖紙的學者早已全部離世。對外來者戒心很重。
3)格雷姆(Grem) —— 鐘樓下的守爐人。身份不明,年齡不詳。他唯一的工作就是不讓鐘樓的火熄滅,對詢問封印之事的人,只答一句「看著火就行」。(標記為存疑 —— AI 自行申報)
值得注意的是,第三名 NPC「格雷姆」是 AI 自己給它打上了 存疑標記。好的提示詞能讓 AI 說出「這一個我沒有把握」。現在,第 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。
- Mira Kost → 保留。雖與 riverhold 的 Mira Veldt 同名,但在不同城市、不同姓氏,不存在同時登場的可能。作為有意變體予以通過。(不過,是否要把 forbidden_names 規則從「名+姓完全一致」改為「僅名字衝突也報 WARN」,另記一筆待議。)
- 格雷姆 → 廢棄。 voice_lint 置信度偏低就是訊號。重讀之後發現,「看著火就行」這樣的守爐人角色,與 scholarly_strict 的城市語氣相牴觸。當學者公會主導的城市裡出現走神秘主義語氣的 NPC,城市的身份認同就會被沖淡。廢棄後重新請求。
文案一旦決定廢棄,就會再跑一次重新請求。「廢棄格雷姆的槽位。請重新生成一個符合同一狩獵場學者公會語氣(觀測、記錄、嚴格)的守爐人 NPC。停用神秘主義詞彙。」AI 這次回以一位記錄鐘樓爐溫、連火都當作資料來看的老人,該輸出以 voice_lint 0.81 通過。輸入 → 骨架 → 正文 → 驗證 → 廢棄 → 重新生成的一個迴圈,在這裡閉合。
這一圈就是本書貫穿始終的「Show(展示)」標準。工具吐出什麼、什麼被攔下、人又親手斃掉什麼——若不曾完整地看過哪怕一次,「用 AI 量產」這句話就是空洞的。
在上面的迴圈裡,有 1 名 NPC 被廢棄。放到整座城市來看,廢棄會更多。稽核時間平均每座城市 5\~10 分鐘,廢棄率 NPC 約 20%,支線任務約 33%。
這裡如實交代這些比例的計算依據。廢棄率是在匯入初期,親自稽核包括 silvermark 在內的 5 座城市並逐一計數得出的值。NPC 是稽核的 30 名中廢棄 6 名(20%),支線任務是稽核的 15 件中廢棄 5 件(33%)。由於樣本僅 5 座城市、規模很小,它不是精確的總體比例,而應當作「五中取一、三中取一」量級的方向值來讀。等 30 座城市全部稽核完之後的累計比例,可能比這更低,也可能因狩獵場性質不同而更高。
重要的是,廢棄率 0% 並不是目標。廢棄 0% 更像是稽核走了形式的訊號。當五名 NPC 中有一名因語氣不合而被廢棄、三個支線任務中有一個因與 lore_seeds 貼合不上而被重新生成時,稽核關卡才是在真正運轉。
我們對比工具匯入前後。下面的時間數值是包括 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 那句警告——一旦文案覺得自己成了「稽核機器」,工具就會被抵制——在這裡同樣適用)。
即便自動化的範圍變寬了,以下內容仍留在工具之外。
| 內容 | 留在工具之外的理由 |
|---|---|
| 主線任務正文 | 一致性、敘事深度直接關係到遊戲的身份認同 |
| Boss 招式、演出 | 視覺、互動細節繁多,交給設計師上手更快 |
| 標誌性主要角色 | 需要完整撰寫 voice_profile,無法量產 |
| 分支結局 | 屬於文案親自決策的領域 |
| 每座城市 1\~2 個標誌性支線任務 | 由文案挑選並親自制作 |
「能夠量產」這一事實,不該自動導向「就應該量產」的決定。正如在 silvermark 迴圈中看到的,工具能把 6 名 NPC 中的 5 名量產得很好。然而,要扛起那座城市「封印正在變冷」這一核心張力的那一名標誌性 NPC,由文案親手雕琢。只要自動化的邊界足夠清晰,量產工具反而會成為守護那塊核心領域的工具。
| 失敗模式 | 為何失敗 | 處方 |
|---|---|---|
| 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.2 的格式手寫一份後設資料(3 個 lore_seeds 是關鍵),再把 §6.2.4 的介紹文提示詞原樣貼上去,跑一遍看看。從產出的 NPC 裡挑一個語氣不合的,親自對它反駁:「這個 NPC 與城市語氣相牴觸,廢棄、重來」——這樣一來,你就能親身體會到稽核關卡究竟是怎樣一組判斷的集合。
如果是團隊,就從下面這一步開始。先做出一份後設資料 yaml 表單和 forbidden_names 自動附加指令碼。規則手冊骨架(generate_skeleton)和 lint 放在之後。哪怕只有輸入表單和重名檢查這兩樣,也能先擋住 AI 正文量產崩成「30 座雷同村莊」的兩種常見失敗。
6.3 將討論 NPC Persona/Squad 流水線。如果說 6.2 的 generator 是把多倫、米拉這樣的 NPC 逐個量產,那麼 Persona/Squad 則把這些 NPC 以組為單位捆在一起。這是一套讓同一狩獵場的五名 NPC 不再是彼此無關的木偶集合,而是作為一個小型社會來運轉的方法。
主要讀者:負責 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 一樣替換為書中用名。輸出正文是對真實會話的重現。
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 的數值,而這些數值又成為任務觸發條件,一路走到動態反應性。本章只點到那種進階應用的入口,正面處理的部分只到"由人評審的保守量產"為止。
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 趨同而被攔下。
規則手冊先定下 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 迴圈相同。本章真正的活兒在其後——驗證捆好的群體是否真的像一個群體那樣運轉的那個環節。
如果只抽象地寫一句"AI 補充關係",就看不出這條流水線到底吐出什麼。下面把 silvermark hg_3 這一個 Squad 的後半段迴圈,從關係文本生成一直跟到廢棄·重新請求,完整走一遍。
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 關係上重複一遍。
NPC 若整天站在一個地方,就又成了玩偶。規則手冊來填動線模式。stationary 是固定一處(衛兵·Boss),stationary_with_shifts 是每 8 小時微調位置(普通),routine_loop 是基於時間表(居民),event_driven 是僅在觸發時移動(任務 NPC)。這一步是確定性的,不呼叫 AI。
現在來查捆好的五個人是否真的像一個群體那樣運轉。如果說 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。
- voice 趨同 (WARN) → 廢棄 bystander_2。即便是學者之城,五個人都是學者口吻也會讓狩獵場單調。請求把 bystander_2 重新生成為
practical, dry語氣的雜役工。(quest_giver·lore_keeper 兩人都是學者,是城市身份使然,予以保留。)- 動線衝突 (WARN) → 修正規則。把 merchant 的 shift 起始偏移推後 +2 小時,化解 14:00 的重疊。不呼叫 AI,只調整動線引數。
- 關係暴露 0 (FAIL) → 最重要的一件。定義了整整兩件關係,卻在遊戲裡一次都不露面,那份資料就是死資料。編劇決定挑選一件核心關係(多倫–米拉),把它埋進臺詞。
三件裡有兩件靠規則·重新生成合上了,最後那個 FAIL 才是這條流水線的核心。編劇追加請求了多倫對話分支的一行。
請在多倫的臺詞裡,插入僅僅一行、像不經意間流露出與米拉關係的話。
不要說明腔,要像順帶一提。學者風語氣,只要一行臺詞。
[重新請求的輸出]
"那張圖紙在檔案庫裡。去問米拉吧。……二十年前是我教那傢伙怎麼讀的,如今反過來了。"
這一行加進去的一刻,兩個 NPC 的關係就從資料表搬到了遊戲畫面上。輸入(Squad 後設資料)→ 骨架 → Persona 填充 → 關係補充 → 動線 → 一致性驗證 → 廢棄·暴露決策,這一個迴圈在此合上。
這一圈就是本章的 Show 標準。"用 Squad 把 NPC 捆成社會"這句話,若沒有哪怕一次看到人把關係暴露 0 件的 FAIL 用一行臺詞合上的場景,就是空洞的。
把上面的迴圈用一張圖放在這裡。要點是:第 1·2·4·5 步是確定性的(規則手冊·lint),只有第 3 步是 AI;而且人的手只觸及最上面的輸入和最下面的關卡。
人的手觸及的地方只有兩處。最上面定下職能·關係意圖的環節,最下面判定 lint 抓不到的語氣·敘事的環節。中間的骨架·動線·驗證由規則手冊來跑,關係正文由 AI 來跑。
第 5 步 lint 的 關係暴露 項最常出現 FAIL。因為關係只在資料裡活著。把關係從資料裡拽進遊戲的裝置有三個。
第一,臺詞引用。像 §6.3.4 裡多倫那句臺詞那樣,NPC 用一行提及其他成員。最便宜,效果也最大。
第二,動線交叉。每週二多倫與米拉一同待在檔案庫的場景,能在遊戲裡被觀察到。玩家偶然看到,就會察覺"那兩個人是不是有牽連"。只要第 4 步的動線與關係一致,它自然就會出現。
第三,分支條件。拒絕 quest_giver 的請求,lore_keeper 的好感度也會一起下降。這第三個裝置正是 §6.3.1 所說進階應用的入口——關係越過單純的描寫,開始對遊戲狀態產生影響的那個環節。
三個裝置不必都用。在 5 人 Squad 裡,哪怕只把 2\~3 件核心關係用第一·第二個裝置暴露出來,狩獵場的體感也會大不相同。過多的話,玩家要記的東西就多了。編劇在評審階段挑選要暴露的關係插進去,其餘的只留作資料。
比較工具引入前後。不使用加工過的數字。時間·比例是親手評審 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 吸收了捆綁工作的大部分,但像多倫最後那一行臺詞那樣的核心,必須給編劇留出親手雕琢的時間。
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%)不是精確值,而是運營指南,一旦把它換算成嵌入距離,鬆散就有可能裝成精確的樣子藏起來——距離值是幫助編劇判定的訊號,而不是判定本身。
| 失敗模式 | 為何失敗 | 處方 |
|---|---|---|
| 只量產 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 擺出來,多倫和米拉就只會在資料表裡是師徒關係。
一個人的話,做到這些就夠:沒有規則手冊、沒有 lint 也行。從你自己的遊戲(或喜歡的遊戲)裡某一處的 NPC 中挑 3\~5 名,按 §6.3.2 的格式,用手寫下職能和 2 件關係。接著把 §6.3.4 的關係補充提示詞原樣貼上,拿到一行描寫,最後親自問一句——"這段關係,現在在遊戲臺詞的哪裡看得見?"一處都沒有的話,那就正是 lint 的
關係暴露 0FAIL。在一名 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,把一件核心關係埋進臺詞,親手合上。
關係暴露 0 FAIL,才是這條流水線的心臟。三個工具各自完工的那一週,我一口氣跑了城市 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 串成一條生產線來運轉的故事。工具有三個,陷阱卻不是三個——它們會在工具之間的縫隙裡重新冒出來。
如果把城市·NPC·道具 generator 各自單獨去跑,每個工具的輸出就會和另一個工具的輸入對不上。解法不是把工具做得更聰明,而是在上游一次性固定好共享的後設資料,再讓各個工具在它下面排成一隊。如果說 6.2 ch2 的城市 generator 是單個工具的範本,那麼本章要做的,就是把那個工具降格為產線上的一個工站。
整條產線是這樣流動的。
關鍵是 city_manifest.json 這條箭頭。城市 generator 在造出一座城市的同時,把這座城市的身份(是沒落的漁村,還是繁榮的貿易港)產出成一份 manifest,NPC generator 和道具 generator 把這份 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_seeds 和 tone,造出“沒落漁村裡那群堅韌的人”。道具 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 小時。
單個 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 那樣,把“看似錯誤、其實是好故事”的東西分辨出來,是規則手冊做不到的。
把工具串起來之後,就需要節奏。產線以一週為單位運轉最為穩定。一週,短到不會讓評審暴增,又長到不會讓回收變慢,正好卡在我桌上日曆的一格里。
| 星期 | 產線工站 | 作者時間 |
|---|---|---|
| 週一 | 編寫 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 周就達成了。
產線是否健康,不靠印象,而靠數字來看。這是每週自動彙總的四個指標。
| 指標 | 正常範圍 | 偏離時的訊號 |
|---|---|---|
| 整合 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 座,去查原因。
把多個工具串起來後,會出現單個工具時沒有的事故。這裡記下我常見的三種。
第一,契約不一致事故。 城市 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。與系統強化並行,事故模式要在覆盤中共享。
把產線串起來的真正目的,不是取消作者,而是讓作者能專注於招牌內容。下面用一張圖畫出:工具各自為戰時作者的時間是怎麼散掉的,串起來之後又是怎麼聚攏的。
主線和招牌內容上聚起了作者時間的 80%。但這個分配不會自動維持。引入產線後,作者時間往往會全流向評審。所以要每月測量時間分配,一旦主線跌破 50%,就減少量產城市數,把主線時間奪回來。時間分配得靠制度來守。
城市·NPC·道具產線穩定之後,就把同一套骨架擴充套件到副本·圖鑑·線上活動。核心是不製造新的模式。“副本和城市不一樣,得用別的結構”這種誘惑總會冒出來。但產線的骨架(共享 manifest → generator 連鎖 → 整合 lint → 人工評審)是完全一樣的。要替換的只有輸入後設資料的格式和領域規則手冊。
如果是副本,dungeon_manifest.json 裡會加上 boss_pattern·encounter_flow 這類欄位,Boss 走位之類的領域規則會給整合 lint 再添一行。骨架相同,只有規則不同。保持同一套骨架,作者就不必再學一個新工具,整合 lint 的基礎設施也能原樣複用。不過這並不是說可以忽視領域特殊性。副本里顯然需要城市所沒有的走位規則。
這是在我的專案裡,把這條整合產線運轉六個月的結果,與把城市·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 個”這樣的具體對比之上做決定。
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 一致性這一行,也能擋住那個陷阱。沒有宏大的基礎設施,只要守住“在工具之間放進一行契約”這一條原則,產線就能開始運轉。
副本第47號房間的出口被堵死了。構建通過了,QA 也通過了。玩家在 Boss 房間前對著牆站著的截圖被髮到社群,是在上線第三天。那個房間是兩個季度前把手工製作的房間複製貼上過來的,複製的過程中,東側的一條通路沒有連線資訊,只留下了視覺外觀。沒有人驗證過它。當時也沒有能驗證它的工具。
本章講的是如何構建一種結構,讓這類事故在構建階段被自動攔截。關鍵不在於繪製空間的手上功夫,而在於用規則來運營附著於空間的資料的方式。
關卡設計的工作現場更接近製圖室。圖紙一張一張出自人手,但圖紙之間的一致性、複用與驗證,由圖紙櫃的運營規則決定。手工畫一個副本誰都會,而把100個副本用一致的難度曲線和沒有死路的圖運營起來,靠的不是手藝,而是系統的問題。
在筆者擔任策劃總監的專案A(面向國內 + 東南亞的 MMORPG,中等規模(10\~50人)團隊,移動端優先)中,這套系統的名字就是一份名為 Procedural_Level_Design_Master 的文件。本章講的是這份文件整合了什麼、AI 介入到哪一步、又在哪裡停手。筆者曾主導一款每一局都會重新生成副本的移動端 Roguelite RPG 的策劃,以規則運營程式化空間的這段經驗,構成了本章的底色。
關卡自動化分為兩個方向。一個是對空間本身進行程式化生成。BSP 分割(Binary Space Partitioning,把空間遞迴地二等分來佈置房間的經典手法)、wave function collapse、drunken walk 網格這類傳統 PCG(Procedural Content Generation,程式化內容生成)都屬於這一類。另一個是運營空間的後設資料 —— 房間標籤、連線性、難度標籤、事件槽位。
傳統 PCG 擅長第一種。在 Roguelike 或沙盒這類以"每局都是新地圖"為遊戲性核心的品類裡,第一種才是正解。但 MMORPG 不同。玩家會把同一個副本刷上幾十遍,刷到動線都背下來。所以副本必須是手工打磨的固定空間,而自動化能切入的位置不是空間本身,而是讓這個空間可被運營的後設資料。
後設資料為什麼是運營的脊柱,按產出物逐項來看就一目瞭然。
| 產出物 | 若沒有後設資料 |
|---|---|
| 數十個副本池 | 無法檢索哪個房間在哪裡,無法複用 |
| 難度曲線驗證 | 沒有各房間的難度標籤,無法繪製曲線 |
| 任務·Boss 位置自動佈置 | 沒有事件槽位後設資料,只能手動輸入座標 |
| 美術團隊同步 | 沒有房間型別 → 美術資源集的對映,視覺不一致 |
| 玩家動線·停留時間測量 | 無法進行基於房間 ID 的遙測 |
沒有後設資料的副本能構建出來,卻無法運營。就像藏書滿架卻沒有索引的圖書館。這正是本章聚焦於"空間後設資料運營"的原因。
Procedural_Level_Design_Master 把四項標準捆到一份文件裡:房間後設資料格式、房間標籤詞典、連線性規則、驗證檢查清單。先看看這四項散落各處時會發生什麼。五名設計師各自從不同檔案裡參照格式,type 欄位就有人寫 combat、有人寫 Combat、有人寫 battle_room。檢索壞掉,統計壞掉,最終自動化也壞掉。
把這四項標準按 Layer 歸位,各自的位置就很清晰。格式、詞典、規則位於支配生成的規則手冊(L1);生成出的房間正文位於內容層(L2);表格值位於資料層(L3);驗證位於構建·QA 門禁(L4)。
所謂總綱文件整合四項標準,並不是"把正文全塞進一個檔案",而是"把規則匯聚到 L1 這個位置"。正因如此,後文將出現的自動化才能架設在 Layer 邊界之上(分離一旦崩塌會發生什麼,7.1.11 會講)。
一個房間遵循以下格式。這份格式就是自動化的輸入介面。
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 自動佈置。沒有消費方的欄位就不放進格式裡 —— 只會增加輸入成本,卻沒有價值。
標籤是後設資料的檢索鍵。一旦無限增殖,檢索就會壞掉。抽屜上貼了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個房間。最後一條是關鍵。臨時造出來的標籤只用一次就被棄置,詞典就會被汙染。
到目前為止的這些標準如何串成一條流程,這條連線線正是本章所支撐的骨架。它是一條從規則手冊出發、經過 AI 輔助變奏、以護欄驗證收尾的管線。
這條管線有三個特點值得點明。第一,規則手冊(L1)位於一切生成的上游。第二,AI 只在規則手冊所定義的詞典內變奏 —— F 門禁會把詞典外的輸出退回。第三,驗證(H·I·J)被固定為構建前的門禁,違規由程式碼攔截,而不依賴人的注意力。第47號房間的事故,正是因為沒有 H 門禁才發生的。
房間後設資料的 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),
}
五個指標會以可與其他副本比較的形式輸出,用作副本池的多樣性指標。不過指標多樣並不意味著副本有趣。指標是用來攔截事故的,不是用來保證趣味的。零死路並不能保證趣味。趣味來自設計師的洞察,而圖驗證只是托住底,別讓那份洞察被事故淹沒。
自動化裡,人最常想撒手不管的部分就是 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_layout 和 two_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格全填滿。這兩種失敗都很常見,而兩條處方都不能靠提示詞,得由規則手冊(詞典 + 上限)來強制,才穩定。
設計師手工填一個房間的後設資料要花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 的最終審批。其餘由工具來填,人來評審。自動化的目的,是把設計師從輸入中解放出來,讓他回到節奏、標誌性房間、複用策略這些判斷上。
總綱標準最大的效用是房間複用。有30個能用標籤檢索的房間,就能組合出5\~10個副本。但複用比例一高,副本就會變得乏味。所以複用要連帶配上護欄。
| 護欄 | 定義 |
|---|---|
| 一個房間最多出現在5個副本中 | 自動追蹤出現頻次 |
| 第二次出現時強制視覺變奏 | 更換燈光·道具 |
| Boss 房間·標誌性房間禁止複用 | 用 flag 強制 |
| 追蹤複用房間的負面反饋 | 玩家遙測 |
複用是降低成本的手段,不是目的。一旦把複用率本身當成 KPI,玩家體驗就會變得單調。0%(所有房間都是新的)會讓量產成本暴漲,超過70%則各個副本彼此難以區分。從經驗看,30\~40% 這個區間是成本與多樣性的平衡點(方向性觀察,精確閾值因專案而異)。
| 模式 | 處方 |
|---|---|
| 後設資料格式被5個人解讀出5種 | 用總綱文件在 L1 統一 |
| 標籤增殖到50\~100個 | 30個詞典 + 四道門禁 |
| 不做死路檢查就構建 | 把圖驗證設為構建門禁 |
| 設計師手工處理所有後設資料 | 編輯器提取 + AI 輔助 |
| AI 生成詞典外標籤 | 用詞典強制門禁自動拒絕 |
| 複用0%或70%+ | 30\~40% 區間 + 變奏護欄 |
到目前為止,以規則手冊·生成·驗證展開的 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個、手工檢查變得吃力時,再引入即可。
一隻見習法師貼著玩家揮刀砍殺。這是一個按遠端法術施法者設計的 NPC。它的 HP 薄如紙片,近戰中只要挨一下就會死,可它卻毫無拉開距離的打算。構建日誌裡沒有任何報錯。在編輯器裡重新開啟 BehaviorTree,節點也都好好地連著。盯著看了一個小時,才找到原因:後退分支的距離條件填的不是 5,而是 0.5。本該在敵人進入 5 米以內時就逃跑,可現在只有對方逼近到 0.5 米——也就是幾乎貼臉——後退分支才會觸發。
問題只是一個數字。在圖形化節點編輯器裡,這個數字要展開節點內部的面板才看得到,而且不會留在變更歷史裡。沒有辦法追溯是誰在什麼時候改了這個值。從那天起,筆者的專案A 就不再用圖形方式,而是改用 json 來處理 BehaviorTree。本章記錄的,正是人與 AI 共同編輯這份 json、再由機器自動驗證的一個完整週期。
行為樹是定義敵方 NPC 戰鬥、移動、反應的事實標準結構。選擇器(selector)按優先順序依次嘗試各分支,序列(sequence)則把條件與動作按順序串起來。結構本身很簡單。問題在於規模。
在筆者的專案A 中,一個敵方 NPC 的 BT 大約由 50\~200 個節點構成,而需要運營的 NPC 超過了 100 個。相乘之後,BT 節點總量就達到數萬量級。在這個規模下,總會有那麼一刻——面對"改動這個後退模式會影響到哪些 NPC?"這樣的問題,人已經無法回答了。這就像桌上攤開著一百本筆記,改了第一本里的一行,卻要用眼睛去追剩下九十九本中哪裡會受到波及。
筆者從圖形化 BT 轉向 json 時,提出了四點要求。
商用遊戲引擎自帶的 BT 編輯器整合方便,視覺化除錯能力強。只是它往往以二進位制(binary)資產的形式儲存,文本 diff 與變更影響追蹤較弱。筆者的專案A 面向的是運營 BT 超過 100 個的長期運營(LiveOps)遊戲,因此選擇了自行開發獨立的 json BT 格式與編輯器。有一點要講清楚:這並非所有團隊的正確答案。如果運營 BT 不足 50 個,直接沿用引擎自帶的編輯器幾乎總是更划算。自研的正當性,本章末尾會再談。
先看看成品的樣子。下面是學者公會遠端支援型 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_range 的 param 是 5 還是 0.5——在 json 裡變成了一眼就能看見的一行。
| 元素 | 作用 |
|---|---|
bt_id |
git diff、變更追蹤鍵 |
category、tags |
檢索、複用單位 |
subtree_ref |
引用公共模式(改一處 → 更新多個 BT) |
description |
供策劃、劇情作者共享 |
標紅的 enemy_in_close_range 5 就是開頭那個讓人耗掉一個小時的節點。在 json 裡,一次程式碼評審就能揪出來。
超過 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 個遠端敵人全部發生了變化"這一事實。
從這裡開始,是本章分量最重的部分。所謂實操記錄(worked transcript),就是完整保留的真實操作過程記錄:本節把新敵方 NPC"見習法師"的 BT 初稿交給 AI,再由人對其輸出進行驗證、否決、重新請求——這一整個週期不加修飾、原樣呈現。之所以不做順滑刪減,是有原因的:AI 在第一次輸出裡錯在哪裡、怎麼錯的,這種失敗的紋理正是本章想要傳達的全部。
[敵方 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。
{
"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"}
]
}
}
拿到輸出後,先過一遍 lint(下一節會講),再用眼睛讀。有三處出了問題。
最後一個分支錯了。 subtree_ranged_attack_pattern 是基於弓箭的攻擊模式。儘管 enum 註釋裡寫了"不用於法術",AI 還是拿來了名字相似的弓箭模式。見習法師應該用 subtree_magic_attack_pattern。—— 否決。
添加了概念中沒有的行為。 mana_recover 分支(mana_below → wait 120frame)是概念裡隻字未提的行為。儘管提示詞裡明確寫了"禁止新增未指定的行為",AI 似乎還是判斷"既然是法師,管理法力就很自然",於是把它塞了進來。看著合理,卻違反了規格說明。—— 否決。
後退閾值可疑。 hp_below 0.4 比參考 BT(0.3)更早開始後退。見習法師確實更脆弱,所以這可能是有意為之。但 AI 從未給出依據,因此這是該由策劃決定的事項。—— 保留,待策劃確認。
只改這三處。最後一個分支 subtree_ranged_attack_pattern 是弓箭模式,
換成 subtree_magic_attack_pattern;mana_recover 分支概念裡沒有,整段刪除;
hp_below 0.4 改回參考 BT 的 0.3——如果有理由用 0.4,就在 description 裡寫一行。
其餘保持不變,只要 json。
{
"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 的輸出是初稿,而非最終稿。
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 化的實際收益。
下面用表格列出筆者的專案A 引入前後的對比。絕對數值會隨團隊規模、遊戲型別而變化,因此屬於筆者的推測(未經驗證)。但方向與比例,是實際運營中觀察到的原樣結果。
| 專案 | 引入前(直接用引擎自帶 BT) | 引入後(json + 編輯器) |
|---|---|---|
| 編寫一個新敵人的 BT | 1\~2 天 | 2\~4 小時 |
| 掌握 BT 變更影響 | 依賴推測與經驗 | 自動(subtree 影響列表) |
| 變更後驗證 | 需要實際構建 | 模擬 5\~10 分鐘 |
| 運營 100 個敵方 NPC | 3 名策劃全職 | 1\~2 名策劃 |
| 上線後 BT 事故(異常行為) | 每季度 10\~15 起(筆者推測) | 每季度 2\~4 起(筆者推測) |
最有意義的是,最後兩行同時發生了變化。通常削減人手會導致質量下降。而這裡,策劃人數減少的同時,事故也減少了。因為原本由人手動追蹤的變更影響與驗證,被機器接管了。自動化的價值,與其說在於"變快",不如說在於這種"人減少的同時質量反而變好"。
讀完本章就下結論"我們也來做一個 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 + 評審關卡"——而工具則應當搭在自帶編輯器或外部庫之上使用。
| 模式 | 處方 |
|---|---|
| 只把 BT 當作二進位制資產來管理 | 儲存為 json,讓 git 追蹤重新可用 |
| 不用 subtree,在每個 BT 裡複製貼上相同模式 | 拆到 subtree 庫裡去引用 |
| 手工做 BT 影響追蹤 | 把影響分析指令碼繫結到 PR 上 |
| 不用模擬,只在實際構建裡驗證 | 執行一個與構建分離的模擬器 |
| 不經評審就使用 AI 輸出的 BT | 讓它通過 lint + 策劃 + 模擬三重關卡 |
| 不衡量自研 ROI | 只在 100 個以上、且長期運營時才自研 |
這是小團隊今天就能嘗試的最小週期。
setup —— 把一個正在運營的敵方 NPC 的 BT 親手寫成 json(bt_id、category、tags、root)。把一塊公共的後退/攻擊模式拆到 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)上,哪怕是一個人,也能把"改動這個後退模式會讓哪個敵人發生變化"從推測變成輸出。僅憑這一個習慣,開頭那個"一個數字耗掉一個小時",就縮短成了程式碼評審裡的一行。
在一次副本評審會上,一位新人關卡設計師把自己做的一個副本投到了螢幕上。狹窄的走廊、從後方緊追的快速敵人、在岔路口的閃避抉擇。這是個做得不錯的副本。問題在於,它和我們此前已經在十一個不同副本里做過的東西有著微妙的差異。敵人的追擊速度、陷阱觸發的時機、岔路口出現的時點,沒有一處是相同的。這位新人相信自己做出的是名為"追擊副本"的同一種體驗,但使用者實際感受到的手感卻副本各異。
那天我們做的決定很簡單:把"走廊追擊"這種體驗精確地定義一次,然後把這個定義固化下來。下次無論誰做追擊副本,都不再從零開始搭建,而是取出這個固化好的定義來用。這就是模式庫的開端。
如果說房間是空間單元、BehaviorTree(行為樹)是行為單元,那麼模式就是把空間、行為與事件捆在一起的運營單元。一個模式被多個副本複用後,量產負擔會減輕;更重要的是,使用者獲得的體驗能在各副本之間保持一致。
拿菜譜裡的食譜來打比方就很準確。一份食譜上,食材、烹飪步驟、火候、成品照片一併列出。哪怕換了餐廳,只要照同一份食譜來做,味道就一樣。只不過每家餐廳允許略作變奏。模式也一樣:空間(房間)、行為(BT 子樹)、事件(event)、結果(獎勵·難度),再加上設計師的意圖說明,一併打包在內。
一個模式一旦定義好,就能讓每個副本都穩定地產出同一種體驗——就像一份經過驗證的食譜能在多家餐廳做出同一種味道。只不過同一份食譜,每家餐廳仍會留一點變奏。如何管理這些變奏,佔了模式運營的一半分量。後文要講的 overrides 就是這一環。
模式庫的核心在於:先把模式固化為規則手冊,再把它們組合起來生成副本。設計師不是在空白螢幕上從頭搭建副本,而是挑選經過驗證的模式加以佈置,只對一部分作變奏。
這條流程的左半段(觀察→分解→對映→驗證→登記)是製造模式的過程,右半段(呼叫→佈置·變奏→完成→追蹤)是消費模式的過程。製造很少發生,消費則頻繁發生。庫運營得當時,這種不對稱就會轉化為量產效率。
筆者的專案A屬於動作RPG一類,因此把模式分為五個類別。這套分類依賴於品類。若是恐怖遊戲,埋伏與敘事節拍的比重會不同;若是解謎遊戲,利用環境的戰鬥會成為核心。不要把分類本身當作絕對,應先確定自己遊戲的核心體驗是什麼,再來劃定類別。
| 類別 | 核心體驗 | 示例 |
|---|---|---|
| pursuit | 追擊·逃脫 | 走廊追擊、峽谷逃脫 |
| ambush | 埋伏·突襲 | 進入房間時埋伏、視野死角埋伏 |
| puzzle_combat | 利用環境的戰鬥 | 拉桿·陷阱 + 戰鬥 |
| boss_phase | Boss 階段 | Boss 階段 1\~3 模式 |
| narrative_beat | 敘事節拍 | 回憶觸發、同伴登場 |
在這五個類別裡,模式總數大致維持在三十到五十個之間。這個數字是有理由的。一旦模式超過一百個,設計師就無法把整個庫裝進腦子裡。那一刻起,庫就變成了檢索起來費時的倉庫,設計師寧可從頭去搭。庫一旦開始被冷落,一致性這個初衷就會崩塌。因此,有意識地管理模式數量的上限,和類別設計一樣重要。
一個模式由一份 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_chase、subtree_ranged_support 這樣的引用,直接指向 7.2 中在 BehaviorTree 編輯器裡定義的 subtree。關鍵在於:模式並不直接把 BT 收納進來,而只是引用它。改動 BT,所有引用了該 BT 的模式都會自動跟進。空間(房間模板)與行為(subtree)各自在自己的庫中管理,模式只承擔把這兩者編織起來的組合表角色。clear_time_estimate_sec、difficulty_modifier 這類數值只是筆者環境下的運營值,並非普適常數。你必須用自己遊戲的模擬與使用者測試親自測量後填入。
設計副本時,不從頭搭建模式。而是從庫中呼叫,指定它放在哪裡,再用 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 超過模式欄位的一半時,那就不是變奏,而是新模式的訊號,該把它拆分為獨立模式了。
修改 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 個有變奏,需要親自檢視"。減輕改動模式時的恐懼,才是這個工具真正的價值。看不見影響範圍時,設計師就乾脆不去動模式,庫便成了一潭死水。
這裡正面回應一個最常被問到的問題:"模式的編寫不也可以交給 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——留作根基足夠紮實的團隊幾年後再去審視的領域。
庫,填滿容易,清空卻難。運營大約一年後,那些建好卻幾乎無人使用的模式會堆積起來。放著不管,庫的檢索成本就會上升,設計師挑選模式時還得連那些已死的選項一併翻看。因此要定期收攏。
| 條件 | 處理 |
|---|---|
| 6 個月內 usage_count 零增長 | 歸為 deprecated 候選 |
| 在評審會上決定廢棄 | 標記 deprecated: true |
| 已在使用的副本 | 原樣保留(歷史性保留) |
| 新建副本 | 禁止使用該模式 |
關鍵在於,廢棄並不等於刪除。已經在使用該模式的副本照舊保留。因為動一個正在線上運營中執行的副本,比攔住一個新模式更危險。deprecated: true 只是"從現在起不要再新用"的標記,而非抹去過去的命令。
就像每個季度把書桌抽屜裡不用的工具拿出來整理一次那樣,庫也要排定每季度收攏一次的日程。沒有這份日程,庫就只會朝一個方向膨脹,某一刻便淪為被設計師冷落的倉庫。
這是筆者在專案A中運營模式庫一年所觀察到的變化。下表中的時間數值是筆者環境下的估計(未經驗證),只有方向和相對比例是真實觀察到的。
| 專案 | 引入前 | 引入後 | 備註 |
|---|---|---|---|
| 單個副本的設計時間 | 約 2 周 | 約 1 周 | 筆者估計,方向明確 |
| 副本間的體驗一致性 | 離散度大 | 穩定 | 基於使用者評價,定性 |
| 每個模式的平均使用副本數 | — | 約 8 個 | 量產效率的核心指標 |
| 新設計師上手 | 約 2 個月 | 約 3 周 | 筆者估計,體感最明顯 |
| 摸清模式改動的影響 | 手工 1\~2 天 | 自動 5 分鐘報告 | pattern_impact.py 的引入成效 |
最令人印象深刻的變化,是倒數第二行——新人上手。模式庫無意間充當了設計教科書的角色。新人只要讀一份模式檔案,就能理解"這款遊戲的追擊體驗是這樣做出來的",於是前輩守在旁邊講解的時間大幅減少。當初新人拿來的那個各不相同的副本問題,竟由庫本身化解了。
"每個模式的平均使用副本數約 8 個"這個數字,意味著同一個模式被複用了八次,這是量產效率的誠實度量。只不過 8 這個值依賴於筆者遊戲的副本規模與模式設計。在副本數量少、或每次都要求不同概念的遊戲裡,這個值會小得多。
最後,得講一個足以推翻整章的觀點才算誠實。模式庫並非萬能。確實存在一些環境,建庫與運營的成本收不回來。
| 條件 | 建議 |
|---|---|
| 副本少於 5 個 | 手工即可,無需建庫 |
| 只有 1 名設計師 | 腦子裡就是庫 |
| 只發行一次,無線上運營 | 複用機會本身就少 |
| 每次都是完全不同的概念 | 複用比例低,ROI 收不回 |
庫的 ROI(Return on Investment,投資回報率)只有在三個條件同時具備時才收得回:有線上運營、設計師在三人以上、副本超過二十個。長線運營的 MMORPG 之所以是典型的適用物件,原因就在這裡。如果自己的專案落在上表的某一行,那就該在動手建庫之前停下來重新考慮。工具只有在存在問題時才有價值,而對一個只有五個副本的專案來說,模式庫的成本大於它要解決的問題。
| 症狀 | 對策 |
|---|---|
| 模式超過 100 個,設計師記不住 | 精簡到 30\~50 個,每季度用 deprecated 收攏 |
| 用人手追蹤模式影響(會有遺漏) | 使用 pattern_impact.py 之類的自動追蹤工具 |
| overrides 達 80% 以上(實質上不算複用) | 變奏過大 → 拆分為獨立模式 |
| 把模式編寫整個交給 AI | 編寫靠設計師洞察,AI 只輔助第 3·4 階段 |
| 不測量 usage_count | 自動彙總 + 在季度覆盤中審查 |
| 不向新人講解庫 | 在上手資料中加入庫的導覽 |
這張表的第二行和第四行最常絆住人。不把影響追蹤自動化,設計師就會懼怕修改模式,庫隨之僵化;把編寫交給 AI,庫則會收斂為平均。這兩種失敗都會扼殺庫的生命——也就是"經過驗證的體驗的複用"。
第 7 部分把關卡這一領域壘成了三個層次。7.1 確立了房間後設資料、標籤與連通性的標準(空間),7.2 講了基於 JSON 的 BehaviorTree 編輯器、subtree 與模擬(行為),而本章走到了把這兩者連同事件一起打包複用的模式庫(運營單元)。把空間與行為分開處理的運營中,同一處的決定每週以不同形態搖擺的那個問題,靠模式這一捆包固化下來加以解決——這正是整個第 7 部分的主幹。
這條脈絡與 Layer 整合設計嚴絲合縫地咬合在一起。遊戲整體空間基調這一願景在最上層,其下是關卡生成規則與 BT 規則這一系統層,房間、BT 與模式庫構成內容層,副本例項與模式使用統計沉澱為資料,lint、模擬與使用者遙測在構建·QA 環節對其加以驗證。模式庫既是這五層中內容層的脊柱,又是向上遵循系統規則、向下生成資料統計的連線環。
patterns/ 和 dungeons/ 兩個目錄。pattern_impact.py 原樣儲存下來。由設計師親自寫模式初稿 yaml(這部分是人的活兒)。然後只把對映交給 AI。照用正文裡的對映提示詞,只是在輸入中附上自己的初稿和兩份庫清單。別漏掉兩行關鍵約束。
- 不要編造庫裡沒有的新模板。只做推薦。
- 若適合度全都在 3 以下,請明確說明需要新建。
find_dungeons_using("pattern_..."),確認這兩個副本被準確捕捉到。如果你一個人做小遊戲,庫這套系統就過頭了。你只需挑一段自己最中意的副本片段,把那份體驗寫成一份 yaml 就夠了。做下一個副本時,開啟那一份複製過來,只改 20%。模式庫的本質——經過驗證的體驗的複用——在一份檔案上同樣成立。等規模變大,那時再加上類別與追蹤工具即可。
本章學習目標(難度 🟡 實務 · 前置:四則運算·表格計算):把戰鬥平衡拆分為公式的位置與數值的位置,並以確定性·可追溯性這兩項性質為依據,區分哪些環節可以交給 AI、從哪裡開始必須由人用規則手冊鎖定。
凌晨兩點,一條告警彈了出來:線上伺服器的坦克職業生存率衝到了 89%。沒有一個坦克打不到 Boss 收尾,而不會死的坦克又太多了。為了找出是誰動過手腳的痕跡,我打開了資料表。一行防禦係數映入眼簾——DEF / (DEF + 1000)。這個 1000 究竟是在什麼時候、經誰的手、以什麼依據從 1200 降到了 1000,表裡哪兒都沒寫。於是一場追查開始了:翻聊天記錄、翻構建歷史,最後不得不追溯到三年前已經離職的那位數值策劃的記憶裡,才算到頭。
只要運營過戰鬥平衡的人,都會撞見這樣的場景一兩回。而這一幕的真正病根,並不在於那個 1000 是錯的。而在於:這個數字住在公式的位置上,可公式變更的歷史卻哪兒都找不到。戰鬥平衡公式是遊戲裡最應當具備確定性的領域,也是最應當可追溯的領域。這兩項性質為何會成為不該把 AI 放進這個位置的理由,正是本章的脊樑。
給非專業讀者的一句話。 本部分的 z-score·模擬·曲線即便讓你感到陌生,也沒關係。你只需帶走這一條——"對於同樣的輸入必須始終給出同樣輸出的規則(公式),不要把 AI 放進來。" 區分哪裡需要確定性、哪裡需要探索的這一判斷,同樣適用於會計規定·結算邏輯·合同條款這類處理"不能出錯的規則"的所有崗位。公式本身,從 8.1.2 起再慢慢看也不遲。
做遊戲設計做久了,手裡會攥著兩類文件:經常變的,和幾乎不變的。在戰鬥平衡裡,幾乎不變的那一類就是公式。"傷害如何計算"一個季度改上一兩次,"這個角色的攻擊力是多少"一週就要改上五六次。把頻率不同的兩股流放進同一個檔案,經常翻動的手,就會把偶爾才翻的那張紙撕破。
在筆者運營的專案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。
防禦係數 DEF / (DEF + 1000) 這一行裡,裝著這款遊戲整套平衡哲學。把這一行畫成圖,就能看出緣由。橫軸是防禦力,縱軸是所受傷害被削減的比例。
這條曲線會緩緩貼向漸近線(asymptote)。它在防禦力 1000 處正好把傷害削去一半,再往後無論怎麼堆,都夠不到 100%。無敵之所以不可能,就藏在這一行裡。若像灰色虛線那樣是線性的,那麼防禦力到 1000 時傷害就被全部擋住,再往上就越過界,進入負數傷害(捱打反而回血)這種說不通的區域。所以線性沒有被採用。
這裡回到凌晨兩點的那場事故。假設有人把這個 1000 上調到 1200。整條曲線會向右平移。同樣的防禦力擋下的傷害變少了,於是全遊戲的坦克都被削弱,輸出職業的單位時間傷害則上升。公式裡的一個常數,就撼動了整個遊戲。 這跟改動一個數值(某個角色的攻擊力)相比,影響的量級截然不同。這一差別,正是必須把公式和數值放在不同位置的理由,也是公式變更必須附帶歷史的理由。
凌晨兩點的追查之所以是地獄,原因只有一個:沒有變更歷史。在專案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%)是為了展示運營流程而給出的筆者估算值(未經驗證)。每款遊戲的曲線和目標範圍都不一樣。但"變更之後跟著嘗試,嘗試之後跟著模擬依據,採用之後跟著事後測量"這個結構,與真實運營別無二致。這個結構裡只要空掉一格,空掉的那一格就會以凌晨兩點的追查回到你身邊。
把公式、數值、歷史這三個位置放在一起看,就是下面這樣。
現在從頭跟一遍 D17 是怎麼定下來的。這就是確定性規則手冊在實務中運轉的方式。
在這個流程裡,要準確看清模擬器的角色。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 年間,工具的外殼換了又換,可裡頭裝的規則手冊,從來沒有一次是機率性的。這才是關鍵。
現在來到本章最想說的一句。在 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 的位置。整個平衡領域,都以同一副骨架運轉。
到目前為止,都是人來做候選、模擬來驗證的保守式應用。再往前一步,連做候選這件事也可以交給工具代勞。只是,規則手冊依舊屬於人和確定性。
起點是異常模式檢測。從 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 去計算這件事,無論規模大小,都別做。
主要讀者:負責線上經濟的 MMORPG 數值/系統策劃(中等規模(10\~50 人)團隊) 面向單人/業餘讀者的精簡版:§8.2.10「一個人的話,只需做到這些」
最早察覺金幣開始洩漏的,不是賬單,而是交易行。上線第二個月,強化石的行情悄悄上漲,一個月後翻了一倍。為了查明原因我召集了一場會議,可會議室裡冒出來的全是"感覺"。有人說新副本的獎勵給多了,有人說是刷怪點的效率變高了,還有人說不過是高等級玩家變多了而已。每種說法都像那麼回事,於是什麼也沒能定下來。一個小時耗在了猜測上,最後以"先等下週再多看看資料"收場。
問題在於資源不止一種。金幣、強化石、聲望、榮譽、靈魂石各自都有 source(流入的路徑)和 sink(流出的路徑),而這些路徑又彼此供養。強化石 Boss 也會掉金幣。用金幣買來的裝備又會消耗強化石。當 5 種資源與數十條流動糾纏在一起時,光靠腦子裡的心算,連一種資源一週的收支都算不出一個誠實的數字。本章講的,是把這種纏繞搬進 Machinations 節點模型,並讓經濟變更的決策不再靠會議上的猜測、而是靠 模擬關卡(simulation gate) 來通過。經濟設計的一般理論,別的書裡已經講得夠多,本章只聚焦於把那套理論 放進 AI 工作流去跑的那一環。
筆者實際運營筆記 本章的案例,是把筆者在公司 R&D 資料夾中運營的經濟試點文件(
Economy_Machinations_Pilot)與經濟調研工作區做了匿名化處理。資源種類、source/sink 結構、Pilot 四個階段都忠實地照搬了實際運營,而公司專有名稱、真實數值則替換為書用版本,或只以比例、方向來表述。AI 輸出正文是對真實會話的重現。
把經濟資源列成表,只有五行,看上去很簡單。陷阱不在資源,而在連線資源的 流動 的數量上。
| 資源 | source(流入) | sink(流出) |
|---|---|---|
| 金幣 | 刷怪、任務獎勵、交易行出售 | 購買裝備、強化、修理、稅金 |
| 強化石 | 副本 Boss、活動 | 裝備強化、合成 |
| 聲望 | 支線任務 | 陣營商店、轉職 |
| 榮譽 | PvP、公會戰 | PvP 商店、公會設施 |
| 靈魂石 | 擊殺 Boss | 角色復活、學習技能 |
資源雖只有 5 種,但 source 與 sink 加起來有二十幾個,而且資源之間還會相互轉換(用金幣購買強化石的交易行,既是金幣的 sink,又是強化石的 source)。一旦這些流動開始彼此供養,"金幣多投放 5%,強化石行情會怎樣"這類問題,只盯著一種資源就答不出來。這正是角色平衡(8.1)與經濟平衡決定性不同的地方。角色平衡用一行公式就能收口,而經濟是 隨時間累積的動態系統,即便一週收支接近 0,累積 26 周也會把交易行壓垮。
所以經濟工作的本質,不是"把數字挑好",而是 "用模擬去看流動如何隨時間累積"。而手工搭建、修改這套模擬模型既枯燥,每做一次又都會有遺漏。反覆而容易漏項的初稿工作,可評審又必須牢牢握在人手裡 —— 這種性質的工作,正是 AI 與人的分工線畫得最乾淨的地方。
先用一張圖,放上本章要處理的經濟迴圈的骨架。
虛線才是本章的核心。強化 sink 拉高強化石需求,從而推高強化石行情(UP -.-> S);而當金幣淨流入超過淨流出時,那部分超額每週都會堆進池(pool),累積成通貨膨脹。這兩條虛線,靠手算根本無法追蹤,所以才需要模型。
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,收益還沒到,運營負擔先到了。
光看工具說明,還是不知道它實際會吐出什麼。這裡把"僅金幣一種資源搬進 Machinations 模型"的一個完整週期,從輸入提示詞一直跟到人的駁回,走到底。輸入提示詞可以原樣複製使用,輸出則是對真實會話的重現。
先把金幣的 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 若做假設就標示出來
附件 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 自由發揮,它會把假設藏起來、只吐出一堆看似合理的數字。那樣一來,即便模型錯了,也指不出 到底哪裡錯。把玩家行為假設從正文裡揪出來、單獨立成一行 —— 這項請求的分量全壓在這裡。假設露出來,才能驗證。
| 節點 | 型別 | 一週流量算式 | 假設 | 把握度 |
|--------------|-----------|----------------------------------------|------------------|-------|
| 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 說出"這個假設我信不過"。
這份輸出不能直接塞進模型。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 上報 ★、人來判定"這一結構,像重複計算這樣的缺陷,在進入模型之前就被攔下了(筆者估計 —— 節省的時間因團隊、資源數而異,與其看絕對值,不如把它當作"從頭手工"與"起草+評審"之間的結構差異來讀)。
金幣模型收口了,並不意味著可以把全部資源一次性建模。筆者的運營也沒有把整體一股腦塞進去,而是走了從單一資源起步、經過驗證·校準再擴充套件的四個階段。
| 階段 | 範圍 | 核心關卡 |
|---|---|---|
| 1. 單一資源(金幣)建模 | source 3·sink 4,§8.2.3 會話 | 節點分類·標示假設 |
| 2. 模擬 vs 實際對比 | 模擬一週 net vs telemetry 一週 | 是否通過誤差閾值 |
| 3. 模型精度校準 | 反映玩家行為分佈(低/中/高活躍) | 按 segment 重新測量誤差 |
| 4. 資源擴充套件(5 種) | 逐步加入強化石·聲望·榮譽·靈魂石 | 驗證轉換流動(交易行) |
第 2 階段的對比驗證,是這四個階段的心臟。模擬與實際若對不上,錯的不是遊戲,而是模型。用一個對不上的模型去做決策,那決策就會在線上以事故的形式回來。所以擴充套件(第 4 階段)永遠只在通過第 2·3 階段的驗證之後才做。這個順序一旦被打破,也就是跳過單一資源驗證、把 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 分別去跑。只看平均,就會漏掉由高活躍玩家引發的行情崩塌。
要讓模擬關卡被信任,模型就不能與實際遊戲對不上。遊戲每週都在變,模型也得跟著校準。上線後,用真實 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.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)足夠紮實時才有意義 —— 若壓縮前的模型已與遊戲對不上,壓縮只會把那份誤差也一併乾淨利落地壓進去。所以這一節不是處方,而是 方向標。眼下要做的,是把保守式應用誠實地跑起來;維度向量,則留給那些根基積累得足夠厚的團隊,在幾年後再去審視的研究領域。
對比工具引入前後。下表的時間·頻率,承載的是引入初期運營中體感到的方向,與其當作精確的絕對值來讀,不如讀作它朝哪個方向移動了才對。
| 專案 | 引入前(會議·手算) | 引入後(模擬關卡) |
|---|---|---|
| 經濟變更決策 → 落地 | 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 起"不是精確測量值,而是運營體感的方向。通貨膨脹事故的計數會隨定義(把行情 ±百分之幾視為事故)而變,所以與其看絕對起數,不如讀作"從事後發現轉向事前攔截"這一結構變化才對。
| 模式 | 為何會失敗 | 對策 |
|---|---|---|
| 把全部資源一次性建模 | 無法分離出哪種資源的模型錯了 | 從單一資源 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 會自信地吐出一個看似合理的模型,卻只用 ★ 上報自己假設的弱點、把缺陷留在正文裡。那些 ★ 若不由人來判定,錯的模型就會被放行,其上的所有模擬決策也就一起錯了。
一個人的話,只需做到這些:沒有 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 新增產出)。
2008 年的一個凌晨,我對著一張 Excel 表,把同一個數字驗算了三遍。規格書裡寫著某個劍士角色的每秒傷害是 847。可那天我第一次跑出來的模擬器,輸入的是同一個角色、同樣的引數,卻吐出了 612。相差 27%。兩個數字裡必有一個在說謊,而我還不知道是哪一個。
規格書上的 DPS 是紙面上的承諾。一次技能的傷害乘以發動頻率的算術。模擬器的 DPS 則是把這份承諾實際揮動 1,000 次之後的結果。冷卻時間相互重疊、施法動作吃掉時間、暴擊沒有按期望值那樣打出——這些紙面不知道的摩擦摻了進來。這 27% 的縫隙,正是數值策劃安身立命的地方。信了紙面,上線之後就要哭。
本章講的是這一件工具的故事。2008 年做出來、至今沒有離手的 Damage Simulator。它如何追蹤規格與產出分岔的確切位置,以及 18 年後我又如何給這套追蹤接上了 AI——本章用一段真實的實操記錄(worked transcript,完整保留的真實操作過程記錄)一步步走過。
先來拆解一下這個 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 是上限,而不是實測值。而玩家玩的不是上限,是實測值。
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 的原始輸出是這樣的(未作潤色)。
從日誌中可以確認的事實:
state=BLOCKED reason=GCD事件反覆出現。在[0.9]處橫斬、在[1.9]處普攻,都因為 GCD(全域性冷卻)被擋住了。- 在突刺施法(0.0→0.9,0.9 秒)結束之前,其他技能無法發動。看起來是施法時間佔用了 GCD 的結構。
- 因此,規格上"每個技能一到冷卻就發動"的假設被打破了。實際上施法動作彼此衝突,導致一部分發動被遺漏。
不過所提供的日誌只有 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 的第二次輸出。
用匯總來驗證。
- 規格期望累計:847,000
- 模擬實測累計:612,300
- 差距:234,700(27.7%)
這個差距的構成: - 因 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 自己也不知道的這個區分,是靠日誌彙總與規格中隱藏假設的對照——也就是人的驗證——才做出來的。
把剛才那次會話所審視的這件工具的輸入輸出,作為一整套攤開來看。模擬器歸根結底是一個誠實的函式。同樣的輸入,同樣的輸出。輸入從三條路匯聚而來。
這張圖的關鍵在於:箭頭是單向的。遊戲資料表只會被模擬器讀取,模擬器絕不會去改寫資料。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。把這個數字握在手裡之前的全部功夫,就是數值策劃的工作。
這件工具活了 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 一旦擠進那個位置,迴歸驗證就變得不可能,工具也就死了。
如果在一件數值工具裡只能劃一條線,我會把它劃在確定性核心的邊界上。裡側,必須像鋼鐵一樣保證同樣的輸入得到同樣的輸出;外側,人和 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,讓它"分成結構原因和方差原因"。工具雖小,衣架——對比規格做差距分解、結構與方差的分離、復現驗證——照樣立得住。
週五下午4點,Alpha版本的5:5 PvP自動模擬跑完了1,200局。結果JSON有4兆位元組。其中某處記著"隊伍A勝率92%"這一行,而平均勝率是52%。我花了40分鐘去找那一行,最終沒弄清緣由就下班了。
平衡屬於確定性的領域。相同的輸入代入相同的公式,總會得出相同的傷害。所以傷害模擬器必須是程式碼,獎勵曲線必須由人親手繪製——這裡是AI不該踏入的位置。然而在那個確定性核心的周邊——也就是從1,200局結果中找出異常的一行、為其成因建立假設、篩選出該改動什麼的候選、再把這些候選重新投入模擬——正是這些周邊勞動吃掉了數值策劃一天的大半。本章講的就是把AI附著到這一週邊的事,而核心原封不動。
8.3中見過的那個2008年的傷害模擬器——歷經三次更換引擎與公司,確定性核心依舊存活了下來——正是本章的起點。輸入相同則輸出相同這一性質,就是平衡工具全部的信任所在。同一版本跑兩次卻得出不同勝率,那這個工具就該被丟棄。
於是把平衡工作的骨架畫出來,便是這樣一副模樣:中間有一團確定性,而它的入口與出口上掛著人的手工勞動。下面是把這副骨架拆解開——用顏色把確定性區域(藍色)與人·AI介入的區域(橙色)分開。
中間的藍色方框只有一個是程式碼。其餘五個橙色方框全是人的判斷·解讀·撰寫這類勞動,而AI能進入的席位只有這五處。一旦讓LLM"幫我算算這個角色的DPS(每秒傷害)",相同輸入卻給出不同數字的非確定性就會滲進核心,那件工具連18天都撐不到就會失去信任。
因此本章的脊樑很簡單:把核心自始至終守為程式碼,同時在入口·出口的五處席位上附加AI,而從最費手的出口一側——從1,200局結果中找出異常的一行、建立假設——開始自動化。
回到開頭那個92%。這一次,不再讓人耗費40分鐘亂找,而是從頭到尾走完這樣一個迴圈:確定性探測器挑出那一行,LLM建立假設,再由模擬來驗證。這裡不作概括——所謂實操記錄,就是把工具真實吐出的原始輸出完整保留、原樣呈現。
從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。
現在把那一行交給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的活兒不是診斷,而是收窄搜尋空間。
五個假設各自附有一行驗證用模擬。這並不是由人一個個去跑,而是把改動候選打包並行投出。作為核心核心的 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丟擲的五個候選,人並沒有把五個全驗一遍,統計只跑了三個就分出了勝負。
在這裡,LLM做的並不是說出"標記疊加是個bug",而只是把那個假設列進候選清單。採納,由看過模擬結果的數值策劃來做——"把標記的max_stack固定為1。archer_07單一組合的勝率為0.63,仍高於平均(0.52),因此下一個版本把標記減益數值從12%追加下調到9%後重新測量。"
這個決定由人做出,其依據(z=4.1探測 → 5個假設 → 3次模擬 → 確定假設2)以一行留存。確定性核心自始至終都是程式碼,LLM只是把40分鐘的亂找替換成了五行假設,一步也沒踏進核心。
上面的實操記錄,其實是把五處席位中的三處(異常探測·改動探索·異常解讀)一口氣走了一遍。把五處席位展開成迴圈,便是這樣轉動。
只有藍色的兩個節點(模擬、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周後再評審的候選"連同優先順序做成清單。它是防止數值策劃漏掉決定的安全網,而不替代決定本身。
把五處席位一次性全部開啟,是最常見的失敗。要從效果大、風險小的出口一側開始開啟。
圓圈裡的圈碼數字(①\~⑤)是引入順序。位置3(報告)與位置4(異常解讀)處於右上——ROI(Return on Investment,投資回報)高、風險低的席位——所以先開。僅啟用這兩處,處理量便增至2\~3倍,引入效果的70%以上在此回收。位置2(改動建議)處於右下的紅色席位,錯誤的候選可能吃掉驗證時間,所以最謹慎地放到最後再開。也並非所有團隊都要把五處全開——僅憑位置3·4,單人數值策劃的一天就會改觀。
引入週期的現實感受大致如下(作者估計,未經驗證——隨團隊規模·工具成熟度差異很大)。位置3約1\~2周,再加位置4約2周,再加位置1約一個月,再加位置5約2周,位置2放在最後,約1\~2個月。這是"別一次全開"的另一種說法。
在作者的專案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年間在同一位置反覆出現的幾個陷阱,並附上處方。
run_candidates 驗證之後才採納。在平衡裡,AI的席位很明確:確定性核心之外,人曾經亂找的那五處席位。把核心自始至終守為程式碼,只卸下其周邊的手工勞動——這就是一個存續18年的模擬器在AI時代也能存活下來的方式。
simulate_dps 與 find_outliers 兩個函式。用固定種子確保可復現性。run_candidates 對每個假設的改動候選做50種子並行模擬 → 能讓勝率回到均值附近的候選就是成因。由人來採納,並留下一行依據。到目前為止,本部分的四章都在和同一個對手作戰。一個 Boss 能在幾秒內被擊殺嗎、坦克能有 89% 的存活率嗎、金幣在流失嗎——全都是圍繞單一目標的傷害、生存與收支的故事。可是在 PvP 中,對手是人。人不會像 Boss 那樣按固定套路行動,即便是同一個職業,手法也各不相同,而最關鍵的是,他們會盯著彼此的弱點下手。PvE 平衡做得再深,PvP 卻整塊空缺,這種情況之所以常見,正是因為如此。單一目標的 DPS 曲線在 8.1\~8.4 已經講透,但"剪刀勝過布"這張相剋之網,卻還一次都沒有畫過。
本章填補這塊空白。要講的有三樣——承載職業與陣容之間相剋關係的勝率矩陣、決定讓誰與誰對戰的匹配/MMR,以及能讓上述所有數值淪為虛假的伺服器權威·反作弊。而貫穿整個本部分的那條界線,在這裡依然不變:戰鬥公式是確定性的,匹配與相剋檢測是 AI 輔助。一步都不會走偏。
在 PvE 中,角色的強度是絕對值。劍士的 DPS 是 800 就是 800,Boss 就實打實地承受這 800。可是在 PvP 中,強度是相對的。劍士的 800 對弓手來說足夠,但對能把自己所受傷害降低 30% 的盾兵而言,會被削減到 560 而顯得不足。同一個角色的強度,會隨著對手是誰而變化。僅此一點,就讓 PvP 平衡成為與 PvE 根本不同的問題。
因此,PvP 平衡的單位不是單個角色的數字,而是一對關係。"劍士 vs 弓手"的勝率、"劍士 vs 盾兵"的勝率各自獨立存在,把這些關係全部彙總,就成了一張表。橫軸和縱軸都放同一份職業清單,每一格里寫著"行擊敗列的機率"。這就是勝率矩陣。如果說 PvE 有 DPS 曲線,那麼 PvP 就有這張矩陣。
讀右表的方法很簡單。若"劍士 vs 盾兵"這一格是 0.42,就表示劍士擊敗盾兵的機率為 42%,也就是盾兵佔優的相剋關係。所有格都接近 0.50 固然是完美的平衡,但這樣的遊戲並不好玩。要像石頭剪刀布那樣存在迴圈相剋的關係,職業選擇才有意義。問題出在這個迴圈在某處斷裂、出現某個職業擊敗所有人的格子時。如果說凌晨兩點的坦克是 PvE 的事故,那麼"盾兵 vs 全職業勝率超過 60%"就是 PvP 的事故。
這裡要先釘死一點。填進這些格子的數字(0.58、0.42 等)全都是示例,而非實測。每款遊戲的職業數、技能、目標平衡線都不一樣。本章要信賴的不是數字,而是如何填矩陣、如何檢查、以及在這檢查的哪一環接入 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 在這一環節替你省下的時間。
即便把勝率矩陣調得完美無缺,讓玩家覺得"輸了"的真正原因另有其處:跟誰對戰。實力 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 分鐘。寬鬆地容許分數差,雖然能快速匹配,對局卻變得不公平。越是凌晨時段、冷門職業、高分段,這種張力就越嚴重。
只有藍色節點(Elo 更新)是確定性的。橙色節點——何時把容許範圍放寬多少、超時後做什麼——才是 AI 輔助觸及的地方。但即便在這裡,AI 也不做即時匹配決策。那是必須快速且可復現的伺服器邏輯,是規則驅動程式碼的地盤。AI 接入的,是為調優這套規則而做的分析:概括"上週匹配日誌中,哪個分段、哪個時段、哪個職業的對局質量(勝率偏差·等待時間)較差",並提出"如何改動容許範圍曲線才能縮短哪個區間的等待時間"的候選。這正是 8.4 的位置 3(報告)、位置 4(異常解讀)、位置 2(變更候選探索)換到匹配日誌這個舞臺上而已。
也點一下匹配與勝率矩陣糾纏的那個環節。匹配演算法若不考慮職業、只對齊分數,損壞的相剋格子就會被原樣暴露。盾兵擊敗法師 68% 的那一格還活著,而匹配又頻繁把這兩者湊到一起,法師玩家體感上的失敗就會比矩陣數值堆積得更多。所以矩陣檢查與匹配日誌分析並非各轉各的,而是同一迴圈的入口與出口——在矩陣裡修復損壞的格子,再到匹配日誌裡確認那一格實際被湊了多少次。
到目前為止的所有討論——矩陣、MMR、模擬——都暗中預設了一件事:玩家上報的結果是真實的。在 PvE 中,這幾乎不成問題。一個人打 Boss,能騙誰呢?可是在 PvP 中有對手,贏了分數就漲,於是產生了作弊的動機。一旦出現篡改傷害、篡改位置、無視冷卻的客戶端,8.1 的確定性公式就只在紙面上是確定性的。在真實伺服器上,某人的劍士正打出比公式高兩倍的傷害。
所以競技遊戲的第一條平衡規則,排在矩陣之前:別讓客戶端來決定結果。傷害計算、冷卻判定、命中判定——一切觸及平衡的運算,權威都在伺服器。客戶端只發送輸入(往哪移動、放哪個技能),而這個輸入是否符合公式、冷卻是否轉好、是否在攻擊距離內,全部由伺服器重新校驗。客戶端發來的"傷害 999"被伺服器無視,只有伺服器按公式算出的值才被採用。
伺服器權威一旦崩塌,整個平衡工作都變成假的。勝率矩陣調得再精細,只要線上有一個職業篡改傷害,那張矩陣就只是紙面上的承諾。所以反作弊不是獨立的安全工作,而是平衡資料的可信度問題。當線上勝率與模擬矩陣嚴重不符時,第一個該懷疑的不是"公式錯了嗎",而應是"這份資料乾淨嗎"。
這裡 AI 的位置又一次清晰起來。作弊判定本身——"將此輸入判為無效"——是確定性規則的活。0.1 秒內移動 30 米的輸入在物理上不可能,所以用規則攔截。相同的輸入必須給出相同的判定,而且不能造成冤枉的封禁,因此這裡不能放機率性的 LLM。相反,把異常模式作為候選篩出來的活,是 AI 輔助能觸及的。從伺服器日誌中,彙集諸如"這個賬號的命中率分佈偏離人類分佈 z 值幾何""這一組賬號共享同一種異常模式"之類的候選,提交人工評審。把 8.1 的表搬到 PvP,界線就是這樣。
| 領域 | AI | 理由 |
|---|---|---|
| 伺服器傷害·命中·冷卻判定 | 絕對禁止 | 確定性核心。相同輸入=相同判定一旦被打破,公平性崩塌 |
| Elo/MMR 分數更新 | 絕對禁止 | 公開標準的確定性式子。一旦動搖,排名就變成假的 |
| 作弊攔截(封禁)判定本身 | 絕對禁止 | 不容冤枉的封禁。相同證據=相同判定 |
| 勝率矩陣模擬 | 絕對禁止 | 無法復現時,"職業變強了"就成了假話 |
| 損壞相剋格的檢測·解讀 | 可以 | 用 z-score 篩出格子,由 LLM 提假設(禁止下確定診斷) |
| 匹配日誌質量分析·調優候選 | 可以 | 提出等待/公平取捨的變更候選(經模擬驗證) |
| 作弊嫌疑模式候選提取 | 可以 | 僅提交人工評審的候選。封禁決定歸人與規則 |
這條界線與 8.1 一字不差。AI 只棲身於確定性核心之外。負責執行的內側——傷害、分數、封禁——是規則手冊,而檢測、解讀、推候選的外側,才是 AI 的位置。
三個主題——矩陣、匹配、伺服器權威——不是各轉各的三件事,而是同一個競技平衡迴圈的三個區段。伺服器權威保證資料乾淨,用這份資料檢查矩陣,借匹配日誌確認檢查結果在真實佇列裡如何被體感,再用模擬驗證候選並反映到構建。
藍色節點(伺服器權威、模擬重算)是確定性的,橙色節點(檢測·假設·匹配分析)是 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。
首要讀者:負責 HUD·UI 的 UX 策劃(中等規模(10\~50 人)團隊) 面向個人/業餘讀者的精簡版:§9.1.8 「一個人的話,做到這些就夠」
在 QA 構建中把新的減益提示疊加到 HUD 上的那天,設計師說"看得很清楚",而第二天使用者論壇上就出現了"減益看不見,結果死了"。提示浮現在螢幕中央,灰底配上淺黃色文字。在設計師的顯示器上看得見,但在戰鬥中爆炸特效鋪滿螢幕的 6 英寸手機上卻看不見。問題在於,這並不是第一次。每一個構建、每一個畫面,同一類事故都以"這次應該沒事吧"反覆上演。
本章聚焦於打斷這種重複的一項工作。它是一個 lint 關卡:接收一張完成的 HUD 截圖作為輸入,自動檢測 P0 元素是否偏離了視線所及的區域(頂部狀態列、兩側底部操作角),以及文字對比度是否越過了可讀閾值。優先順序表、視線流動、平臺分支這類 HUD 設計的通用原則,其他書裡已經講得足夠多,因此本章只把篇幅用在讓這些原則在每次構建中自動強制生效的審查迴圈上。關鍵在於,讓 AI 看著畫面,用座標和數字說出"這行字對比度只有 2.0:1,不到 WCAG 4.5: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 是否位於視線所及之處(頂部·兩角),文字是否越過標準對比度"。讓這一判定不因人而異地用標準釘死,就是本章要做的事。
這裡完整演示實際執行的一個週期。下面忠實再現了作者專案(移動優先 MMORPG,下稱"專案A")的一次戰鬥 HUD 審查會話——這種把真實操作過程完整保留下來的記錄,本書稱為"實操記錄(worked transcript)"。輸入提示詞可以直接複製使用,輸出則是對真實會話的重構。
只丟截圖,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"
附上的截圖是專案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)、輸出格式(表格)、必須標註測量值的義務,以及"拿不準就交給人"這一齣口。這五顆釘子把恭維變成了測量值。
| 元素 | 檢查 | 測量值 | 判定 | 備註 |
|--------------|-------|--------------------------|------|----------------------------------|
| 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 說出"這個我不知道"。
不能照單全收這份輸出。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 上,對比度·佈局·觸控的違規就會落成數字,人只需判定程式碼不瞭解的例外(命中框)和存疑(未登記圖示)(審查一屏,靠手要十幾分鍾,用這個迴圈只要幾分鐘 —— 作者推測,未經驗證的假設。與其看絕對時間,不如從"肉眼掃視"與"用標準測量"的結構差異去理解)。
把上面那次會話中 debuff_alert 之所以得到 WARN 的原因,以及 P0 資訊應當放在哪裡,用一張圖留存下來,之後所有的佈局判定都會更快。橫握的手機上,畫面分為四處。上方的橫向狀態列(視線最先落到、手指不去的只讀區),左·右兩個底部角落(雙手拇指可達的操作位 —— 左拇指=移動,右拇指=技能),二者之間的中央遊戲區域(戰鬥發生的地方),以及遊戲區域下方的中央底部槽位欄(放置消耗·自動道具和快捷槽·技能槽的地方)。下圖中,綠色·琥珀色是 P0 與槽位安全的區域,紅色是 P0 提示會被淹沒的遊戲中央。
規則很簡單。P0 資訊(HP·MP·核心提示)要放在綠色區域(上方橫向狀態列或兩側底部角落)之內。因為那是視線最先落到、或拇指常駐的必經之處。相反,遊戲中央(紅色)是戰鬥本身發生的地方,在這裡放 P0 提示,特效鋪滿螢幕的那一刻資訊就會被淹沒。有一點要注意 —— 遊戲中央與中央底部是不同的。遊戲中央危險,但其下方的中央底部槽位欄(琥珀色)是消耗·自動道具和快捷槽·技能槽棲身的地方。為了一眼看到自己使用或自動消耗的東西,才把它放在兩拇指之間。此外,只讀的資訊(HP/MP/目標血量)放在頂部,可點選元素(移動·技能)放在兩側底部角落,消耗·槽位放在中央底部 —— 這三者就是手指·視線的區域。§9.1.2 中減益提示之所以得到 WARN,用這張圖一張就能說明 —— 因為把必須在 0.5 秒內看到的 P0,偏偏放到了最看不見的遊戲中央。更正方案中把它移到頂部狀態列右側,正是把它送回了這張圖裡的綠色區域。
本章的 lint 建立在"各元素的座標·顏色"能幹淨地進來這一前提上。然而,這些座標從哪裡、怎麼提取,才是實際中最現實的分岔口。這是書裡常常含糊帶過的地方,所以這裡如實比較三條路徑。正確答案不止一個,會隨團隊情況而不同。
| 路徑 | 做什麼 | 優點 | 缺點 / 現實 |
|---|---|---|---|
| ① 遊戲內遙測(telemetry)日誌 | 由構建直接轉儲 UI 框架所繪製控制元件的座標·尺寸·顏色 | 座標精確(非估計),連命中框·錨點都能拿到 | 需要在 UI 程式碼裡埋入轉儲鉤子。需要程式設計師協作。一旦鋪好,最為可信 |
| ② 現成的 vision API | 把截圖送入 OCR·目標檢測 API,提取文本·框的座標 | 無需修改構建,外部截圖也可以 | 座標是近似值,對計量條·圖示這類非文本的分類較弱。外傳 = 未公開構建的洩露風險 |
| ③ 自行實現(畫素分析) | 直接讀取截圖,用啟發式提取色彩邊界·框 | 依賴最少,用於色彩對比度計算已足夠 | 不知道元素的含義(它是不是 P0)。必須與清單對照才有用。有維護負擔 |
三條路徑的關係,恰好解釋了本章的實操記錄。§9.1.2 中,對比度檢查之所以準確,是因為顏色值(fg/bg)通過①·③準確地進來了;而觸控 FAIL 之所以被人推翻,是因為命中框在清單裡缺失了(②·③看不到命中框,只有①看得到)。也就是說,對比度僅憑畫素也能抓到,但觸控命中框沒有①遙測(telemetry)就抓不到。帶著對這一侷限的認識出發,才能劃清 AI 審查結果可信到什麼程度的那條線。
作者專案的選擇,是以①遙測為正本,AI 則作為對照截圖+遙測清單的審查者這樣一種結構。只在畫面上出現、而清單裡沒有的東西(§9.1.2 那個未登記的閃爍圖示)由 AI 抓,清單裡有、而畫面上對不上的東西由人抓。只靠其中之一,兩邊都會留下盲區。
人手觸及的只有兩處。把遙測轉儲乾淨地放進去的位置(最前),以及判定程式碼·標準抓不住的例外(命中框)·存疑(未登記元素)的位置(最後)。中間那些枯燥的對比度計算與佈局對照,交給 AI 和標準去跑。
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,瞭解構建實情的例外交給人 —— 這個分工才是核心。
本章出現的數值,出處只有三類。對比度 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。
| 模式 | 為什麼失敗 | 處方 |
|---|---|---|
| 在設計師顯示器上肉眼審查 | 缺了 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) |
第四個最常反覆出現。急著疊加新提示時,空位只剩畫面中央,就放那兒 —— 而那個中央恰恰是遊戲發生的地方。
一個人的話,做到這些就夠:沒有遙測、沒有清單也可以。給自己的遊戲(或喜歡的遊戲)拍一張橫屏 HUD 截圖,用取色器取出最小的兩三個文字·圖示的前景/背景色並手動記下,再貼上 §9.1.2 的提示詞跑一次。挑一個 AI 算出的對比度數值,用線上 WCAG 對比度計算器親手驗算一遍,你就會切身體會到"看得見/看不見"是如何變成數字的。如果 AI 把某個 P0 放在了畫面中央,不妨反駁它"再看看為什麼中央是危險的"。
如果是團隊,就從下面這一步開始。先和程式設計師就"從 UI 框架轉儲控制元件座標·色·命中框的遙測鉤子(路徑 ①)"達成一致,然後從 §9.1.5 的 contrast_ratio 這一個函式開始放進構建。對比度計算是標準公式,不會有異議,哪怕只有一個函式,每次構建的對比度 FAIL 也會落成數字。之後再疊上 in_thumb_corner,橫屏雙手操作元素的角落偏離也能由程式碼抓住。佈局·未登記元素這類解讀,在其上疊一層 AI 即可。
主要讀者:移動優先動作·MMORPG 的 UX·戰鬥策劃(中型團隊) 面向個人/業餘讀者的精簡版:§9.2.7「一個人的話,做到這一步就夠」
如何把新職業的 6 個技能擺到移動端螢幕的哪個位置、以什麼方式擺放。每當這個問題被擺上會議桌,最初的 30 分鐘總是如出一轍。有人在白板上畫出六個圓圈,另一個人說"那個位置拇指夠不到",又有人接話"那往上挪的話小地圖就被擋住了"。三個人說的都對,可結論一直沒出來。到了下一次會議,同一塊白板又被重畫一遍。
問題在於:畫布局草案這件事,和檢查這份草案是否遵守規則這件事,在同一個人的腦子裡混作一團。畫的人往往難以否決自己畫出來的東西。本章把這兩件事拆開。畫多個佈局草案這種枯燥的活兒交給 AI,而草案是否違反了重疊·拇指角落·觸控尺寸規則,則由程式碼來淘汰。 人只站在最後一個位置上——從程式碼放行的方案裡,憑"遊戲手感"挑出一個。如果說 9.1 立起了整個 HUD 的規則手冊,那麼本章就是把這份規則手冊,一直貫徹到技能按鈕——這個手指觸碰最頻繁的部件——上的一個完整迴圈。
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 比人更不知疲倦地完成的地方。
本節從輸入到廢棄,完整展示把新職業"薩滿"的 6 個主動技能佈局到移動端的一個迴圈。以下內容忠實再現了作者專案(移動優先 MMORPG,下稱"專案A")的新技能 UI 工作會話。輸入與提示詞可以直接複製使用,輸出則是對真實會話的重構。
把 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_rate 和 role。最常按的 s1_quickbolt(41%),以及危急時須在 0.2 秒內找到的 s4_heal(panic),必須放在右拇指最容易夠到的位置(右下角內側)。罕用的 s6_ultimate(4%)放在角落邊緣、稍遠一些也無妨。這一優先順序就是下一步 AI 佈局的全部輸入。
附件 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 的平均佈局,而且沒有圖、只有文字描述。
== 方案 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 先指出了自己所畫之圖的薄弱之處。但這只是自我申報,真正的判定由程式碼來做。
用眼睛比較三個方案,又會開始"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 報告原樣貼進下一條提示詞,重新請求。
三個方案的 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 方案"這句話就是空的。
上述迴圈的心臟,是淘汰三條規則的 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 是不是有意的"這類判斷交給人的分工,在這裡同樣成立。
整個迴圈一目瞭然地看,就是下面這樣。
人經手的地方只有兩處。最前端——把輸入規格乾淨地放進去,以及最末端——判定 lint 殺不掉的 WARN。中間那些枯燥的 3 方案生成與座標檢查,由 AI 和 lint 來跑。
只生成一次佈局方案就收工,便無從知道這個工具運作得好不好。所以每一次都把 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.2 中通過 lint 的方案 A' 按座標原樣畫出來,就是下圖。表裡的數字在真實畫面上是什麼形狀,得看圖才抓得住。橫屏手機雙手握持的姿勢下,左手拇指落在左下角(移動),右手拇指落在右下角(技能簇)。圓的大小與觸控目標(pt)成正比,顏色表示拇指可達難度(綠色容易 / 黃色一般)。
看圖就能一眼理解 lint 報告中最後那條 WARN。只有 s6_ultimate(黃色)位於右下角的左端,是右拇指可達"一般"的位置。但 s6 是使用頻率 4% 的終極技,放在角落邊緣才對。最常用的 s1(綠色,最大 56pt)放在右拇指最容易夠到的角落內側右下,緊急治療 s4(黃色邊框)則放大尺寸,好讓危急時手能快速找到。左手拇指被繫結在左下角的"移動"上,所以技能全部聚在右側角落。一張座標表與一張圖精確一致——這正是把座標以數字形式接收的原因。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| 只在白板上畫圓圈開會 | 沒有座標無法 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 次被淘汰才是健康的狀態。
一個人的話,做到這一步就夠:沒有 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 判定為"有意"。
主要讀者:每天與非策劃職能(美術)協作的 UX·UI 策劃(中等規模團隊) 面向單人/業餘讀者的精簡版:§9.3.8「一個人的話,做到這些就夠」
策劃用 Markdown 把 UI 決定事項整理好,事情就會變得利落:能做版本管理,能看到 diff,還能原樣丟給 AI。問題在於美術團隊不讀 Markdown。更準確地說,他們沒有理由去讀。對美術設計師說"請從 SVN 拉取 아트_결정사항.md 來看",一半人根本沒裝 SVN 客戶端,另一半人則對著在記事本里開啟、## 標題和表格語法全亂掉的畫面問"這個要怎麼看啊"。
這裡錯誤的處方是"教美術團隊用 Markdown"。美術設計師的時間應該花在推敲畫素上。花在學習 Markdown 約定、SVN 檢出、看 diff 上的時間全是損耗。正確的處方是在策劃這一側把轉換與傳遞自動化,把美術團隊的學習負擔降到 0。策劃用 md 寫,指令碼把它轉成 html,另一個指令碼再把它推送到美術倉庫,美術團隊在瀏覽器裡只看 html。本章會把這條流水線實際從頭到尾跑一遍 —— 從用 AI 生成決定事項初稿的環節,到轉換·傳遞的自動化,再到人工究竟拒絕了什麼。
很多書把策劃與美術協作崩掉的原因歸結為"決策權模糊":誰定顏色、誰定功能。這種分工固然重要,但無論把分工表畫得多好,只要美術團隊讀不了這張分工表,就什麼都不會發生。實務中更常出事的地方不是決策權,而是傳遞格式。
在筆者的專案(移動優先 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。這兩個檔案就是本章的脊樑。
整個流程分為四個階段。關鍵在於人(策劃)只碰階段1的 md,其餘三個階段全部由指令碼來跑。美術團隊只看階段4的 html,甚至不需要知道 md 的存在。
下面逐一點明每個階段到底做什麼。
階段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 即可。
這裡先停一下,把設計意圖講明白。把 md 轉成 html 這件事本身微不足道。真正的設計在於決定了誰來承擔誰的學習負擔。
選項有兩條路。
關鍵在於不對稱。方案 A 的學習成本要乘以美術人數,而且每來一名新人就重複發生一次。方案 B 裡策劃只要寫一次指令碼就完事,美術一側的邊際成本為 0。把負擔壓給能自動化的一側,而不是人多的一側 —— 這就是非策劃職能協作工具的第一原則。一旦這條原則被打破,也就是協作工具強迫對方職能去學新東西,那麼這個工具在一兩個季度內就會"沒人用"。
前面說階段1 由策劃來寫 md,這裡把用 AI 生成這份 md 初稿的環節完整走一個迴圈。決策會議結束後,會留下零散的記錄(聊天·白板照片·口頭共識)。把這些整理成正本的決定事項 md 很枯燥,而且每次格式都會走樣。這正是最適合交給 AI 的活兒。不過,決定本身由人來做,AI 只負責把決定整理成既定格式 —— 這條邊界是關鍵。
[UI 決策會議記錄 —— 關於 06_UI 技能槽,原始未整理]
- 說要把技能槽按鈕尺寸調大。因為在移動端太小了。
- 顏色由美術來定。不過 primary 色調保持藍色系。
- 槽位停用(冷卻)狀態的表現,商定為灰色+數字疊加。
- 多語言……技能名變長怎麼辦?暫時擱置。
- 啊,還有長按彈出技能說明(這是功能,策劃已確定)。
附件是 UI 決策會議的原始記錄。請整理成要交給美術團隊的決定事項 Markdown。
把每一項分類為 [確定]/[擱置]/[美術決定](確定=策劃拍板、美術照做;擱置=未決+原因;美術決定=屬於視覺表現由美術定、策劃只給約束),並各附一行依據/約束 —— 如果記錄裡沒有依據,就如實寫成"依據未記載",不要用猜測來填。
像觸控尺寸這種能用公開標準驗證的,要註明標準;記錄裡沒有的專案,絕對不要新編造。
表格用 |專案|分類|決定/約束|依據|是否可驗證|,幷包含 frontmatter(title、layer: L1),給出完整全文。
這段提示詞的重心不在整理本身,而在於強制分類和禁止編造。讓 AI 整理決定事項時,它會熱心地把記錄裡沒有的"最佳實踐"補進來,而這些補充在美術團隊看來就成了"策劃已敲定的決定"。所以要釘下三根釘子:[確定]/[擱置]/[美術決定] 分類、依據義務,以及"記錄裡沒有就不要編造"。
---
title: 06_UI 決定事項 —— 技能槽
layer: L1
---
# 06_UI 決定事項 —— 技能槽
| 專案 | 分類 | 決定/約束 | 依據 | 可驗證 |
|---|---|---|---|---|
| 技能槽按鈕尺寸 | [確定] | 最小 44×44 pt 以上 | 移動端觸控目標,Apple HIG 建議 44pt | ✅ 座標·尺寸 |
| primary 色調 | [美術決定] | 保持藍色系(約束),精確值由美術定 | 記錄:"primary 色調保持藍色系" | △ 顏色值 |
| 冷卻狀態表現 | [確定] | 灰色疊加 + 剩餘數字 | 會議商定 | ◯ 視覺評審 |
| 技能名多語言長度 | [擱置] | 後續會議 | 記錄:"暫時擱置" —— 長文本語言(德·俄)未做驗證 | — |
| 長按技能說明 | [確定] | 長按 → 顯示工具提示 | 功能確定(策劃領域) | ◯ 行為評審 |
| 槽位間距 | [確定] | 建議最小 8 dp | 依據未記載 —— 記錄裡沒有,參考 Material 建議值 | ✅ |
這份輸出不能原樣當作正本上傳。有一行有問題。
就是最後一行 槽位間距 | 最小 8 dp。回頭再看記錄,會議裡根本沒提過間距。是 AI 自己判斷"既然是移動端 UI,總該有個間距標準吧",於是熱心地把 Material Design 的建議值加了進來。這違反了規則 5("禁止編造記錄裡沒有的專案")。AI 雖然如實標註了 依據未記載,但根本就不該造出這個條目本身。這一行一旦到了美術團隊手裡,就會被讀成"策劃已敲定 8dp 間距"。
於是重新提出請求。
"槽位間距"在會議記錄裡沒有,是你加上去的。請從表格裡刪掉。
記錄裡沒有但看起來需要決策的,不要放進表格,只作為候選列到最下面的"## 未決 —— 下次會議議題";決定事項表裡只保留記錄中實際有的專案。
AI 把間距條目從表格裡刪掉,並在最下面把"下次會議議題:槽位間距標準(目前未定)、多語言技能名長度處理"單獨分離為候選。現在決定事項表裡只剩下會議上真正定下的內容,而 AI 想到的合理候選則從"確定"降級為"議題"。這一分離之所以重要,是因為美術團隊拿到的文件裡一旦混淆了什麼是確定、什麼還在討論,美術就會把未定事項當作確定去開工。
經過這一次往返,階段1(md)就完成了。現在它離開人的手,進入階段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)
# ↑ 將  這類引用內聯嵌入 →
# 美術無需另外獲取圖片檔案
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,決定交給人 —— 全書反覆出現的這條分工,在這裡同樣適用。
在美術協作中,AI 被用錯的典型例子就是圖片提示詞。策劃給美術團隊提供參考圖、或者想快速把概念視覺化時,會用影像生成 AI。此時常見的失誤,是一上來就寫結果描述("藍色圓角按鈕、發光效果、4K")。
筆者的協作原則之一是 image_prompt_design_intent_first —— 圖片提示詞也不要先寫結果描述,而要先寫設計意圖。
| 方式 | 提示詞 | 問題/效果 |
|---|---|---|
| 結果優先(差) | "藍色圓角按鈕、發光、4K、遊戲 UI" | 美術無法追問"為什麼是藍色?"。意圖蒸發 |
| 意圖優先(好) | "直觀傳達技能可用/冷卻狀態的技能按鈕。可用=讓人想立刻按下的視覺吸引力,冷卻=抑制。色調為 primary 藍色系" | 美術看到意圖後,可以反向提出更好的視覺方案 |
區別在於美術團隊拿到提示詞後能做什麼。只拿到結果描述,美術要麼照著畫,要麼無視,二選一。拿到設計意圖,美術就能提出一個把這個意圖解得更好的自己的視覺方案。這正是策劃在不侵犯美術決定領域(§9.3.4 的 [美術決定])的前提下,還能給出方向的方法。策劃給出"為了什麼",美術決定"看起來怎樣"。
所以,在 §9.3.4 的決定事項 md 裡放圖片參考時,圖注也不寫"藍色按鈕",而寫"以區分冷卻狀態為目的的槽位 —— 精確表現由美術決定"。轉換指令碼會把這條圖注連同圖片一起嵌入 html,於是美術會同時拿到圖片和意圖。
有一種誘惑,想把這條流水線的效果寫成"協作事故減少了 70%"之類的數字。這種數值一旦沒經過驗證,就會削弱本書的可信度。誠實地做個區分。
可用公開標準驗證的 —— 決定事項裡出現的觸控 44pt·間距 8dp·對比度 4.5:1 這類公開標準,遵循 §9.1 的規則手冊。它們不是編造的數值,而是可以原樣引用、並用 lint 自動驗證的值。
可度量的運營指標 —— 這條流水線實際能計數的是這些:美術拿舊版開工的事故數(傳遞若是自動的則收斂到 0)、美術團隊新人第一次開啟決定事項所花的時間(雙擊 html 的話是分鐘級)、決定變更反映到美術倉庫的延遲(指令碼執行時間)。這三項不是"感覺",而是能用日誌·觀測來計數的。
筆者的推測(未經驗證的假設) —— "比手動郵件傳遞時遺漏更少"這個方向是明確的,但由於沒有另外記錄樣本,精確的下降率不做斷言。與其看絕對值,不如按方向來讀:傳遞若掌握在人手裡,忙碌的一週必定會出現遺漏;傳遞若是指令碼,遺漏就會在結構上消失。
一個人的話,做到這些就夠:沒有美術團隊、沒有 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 |
週五晚上 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 與驗收是我的第一份工作,那時手和眼睛是唯一的檢查工具。二十多年過去,如今我把同樣的活兒交給了程式碼——就在人眼不再充當檢查工具的那個位置上。
先用圖來看清楚檢查的是什麼。遊戲資料表就如同關係型資料庫。一張表的列指向另一張表的主鍵。一旦這根箭頭斷掉,執行時遊戲要麼崩潰,要麼更糟——悄無聲息地顯示一個空值。
綠色實線是活著的引用:quest_table.reward_id 指向的值在 reward_table.reward_id 中確實存在。紅色虛線是死掉的引用——某條任務的 npc_id 指向了一個 npc_table 中並不存在的 id。integrity_check_fk 要抓的正是這根紅色虛線。
專案A的資料表裡,這樣的引用絕不止一兩個。30 多張表彼此指向,若把 FK 箭頭全畫出來,一頁紙都裝不下。讓人每次都用手去逐一追蹤,是不可能的。
從這裡開始,是我實際跑過的一個會話。我不做概括,而是把提示詞全文、原始輸出,以及我否決並重新要求的部分,都原樣記錄下來。
我先讓檢查器做出一張"哪個列指向哪張表"的 FK 地圖。讓人把 30 張表的 FK 逐一登記並不現實,於是我讓它依據規格書表的列命名規則(*_id 指向同名的表)自動推斷候選。我輸入的提示詞全文如下。
讀取所附的規格書表(
$스키마標籤頁),從所有資料表中推斷出看起來像外部索引鍵(FK)的列。規則:(1)列名為xxx_id形式,且(2)在xxx_table或xxx_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 地圖不經評審就直接使用,首次執行就會湧出數百條假違規。必須由人過濾一遍,檢查才能贏得信任。
我把過濾後的 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
我用評審過的地圖對全部 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_id 是 q_0500,而實際的任務 id 是 q_500。這是加了 0 填充導致的寫法不一致。在人眼裡兩者看著一樣,但作為字串卻是不同的值,於是遊戲找不到前置任務,便把這條任務一直留在鎖定狀態。這是一類若上線就會有玩家來諮詢的缺陷。
message 欄位裡的"推測為筆誤""推測為 0 填充",是我讓檢查器在單純的成員匹配失敗之外,一併給出最接近的 PK 值(以編輯距離為準)的那部分。它能縮短人去追查"這為什麼會斷"的時間。不過這些推測終究只是提示,真正的修正值由人來定。
到這裡,是單個檢查的動作。但檢查即便抓住了違規,沒人看見也就沒有意義。關鍵在於讓違規徑直抵達負責人的那條流程。在專案A中,這條流程由一個名為 integrity_check_clickup_notify 的獨立 atom 負責(在 JIT 後設資料中其影響力評分為 294.93,是驗證 atom 組裡評分最高的 atom 之一——這意味著,讓一致性失敗抵達人,與檢查本身同等重要)。
完整的 cascade 如下。各檢查 atom 依次執行,某一環節一旦出現 P0 違規,便流向通知 atom。
這個 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 就能接收並處理所有檢查的結果。
第一次開啟檢查,必定會冒出假違規。第 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 檢查的讀者準備的最小步驟。
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 函式,就能抓到斷裂的引用。通知用控制台輸出或文本檔案就夠了。關鍵不在通知渠道,而在於"讓機器抓住人眼抓不住的引用錯誤,並送達到人"這一流程本身。
晚上 11 點,nightly 任務往協作工具裡插入了一張卡片。標題是 [integrity] D17 相符度測量未執行 已過 7 天。這條告警的意思是:一條應用了一週的決策,在"是否真的按意圖生效"這一點上還沒有任何人確認,卻一直留在構建裡。資料本身沒問題。表格格式、外部索引鍵(FK)、enum 都通過了。但決策沒有得到驗證。
這道落差正是本章的起點。即便資料完整無缺,決策仍可能出錯,而捕捉這種錯誤的位置,與資料檢查處在不同的地方。把這個位置分成三個層,並明確每一層中 AI 輔助到哪裡、人在哪裡蓋章 —— 這就是決策驗證 3-layer 感測器。
check cascade 會一次性執行四類檢查 —— doc-audit(文件一致性)、data-qa(資料質量)、integrity(完整性)、link(交叉引用斷鏈)。這四項都通過,意味著"資料沒問題"。但在沒問題的資料之上,仍可能疊加著一條錯誤的決策。
獎勵表格在格式上再完美,只要它的數值會引發通貨膨脹;外部索引鍵(FK)再唯一,只要兩個任務在同一時刻佔用同一個 NPC;voice 再一致,只要兩個角色的關係設定相互矛盾 —— 資料檢查全部通過,而決策卻全部錯誤。資料檢檢視的是"格子是否填好了",決策檢檢視的是"這個值與其他決策、其他資料、真實使用者是否相符"。用會計來打比方,前者是記賬憑證的格式檢查,後者是財務報表的一致性審計。
因此,決策驗證要設定一個與資料驗證相互分離的感測器。一旦捆在同一個檢查裡,就會被壓成"通過/失敗"一行,失敗時到底是資料問題還是決策問題,解讀起來就變得含糊。分離開來,責任才清晰。
3-layer 感測器的核心,在於把驗證的維度拆成三個。每一層所看的物件、執行的時點、AI 與人的分工都各不相同。
三個層的最後一道章都由人來蓋 —— 這道章正是 human_review_attestation_evidence_mandatory atom 強制要求的證據。每一層看的是什麼、由誰在哪裡蓋章,下面依次來看。
這一層檢查新決策是否與既有決策相沖突。決策對的數量隨決策數的平方增長,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 來看。
這裡把實際往返的內容完整展示到底。不做摘要,含糊的輸出也原樣保留。
① 人發出的提示詞(全文)
請判斷下面兩條決策是否相互矛盾。
[決策 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 對全部決策對重新檢查。
這一層測量決策如何反映到資料上、又是否與意圖相符。它最容易自動化,也最精確。只要模擬器和資料表已經就位,加上驗證規則即可。
以決策 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%,正是因為這種解讀。數字通過規則,與那個數字對遊戲而言是否正確,是兩個不同的問題。
三層裡最難的一層。它看的是決策是否按意圖作用到了真實使用者身上。它以構建釋出後 1\~2 周的實測指標(坦克平均生存時間、含坦克的 5:5 PvP 勝率)和自然語言反饋(論壇·社交媒體)作為輸入。
自然語言反饋成為驗證的輸入,是這一層的特點。約 200 條論壇帖、約 1,500 條社交媒體內容,由 AI 分門別類並標註情感。
[AI 反饋分類 —— 坦克相關 1 周收集]
正面 62% 負面 23%("坦克太強了"佔多數) 無關 15%
在這裡停下就是陷阱。AI 的情感分類一旦混入韓語和英語,準確度就會下降("坦克變強了哈哈"到底是正面還是諷刺,判定會搖擺)。因此運營規則規定:每季度由人親自分類 100 條樣本,與 AI 的結果做比對。若比對中的誤差超過閾值,該季度的分類就不予採信,由人全量重新分類。
最終的相符宣告由人來做。就 D17 而言,實測 +44%(模擬預測 +52%,誤差 8% —— 正常範圍),反饋以正面為主。AI 整理並提交了"正面為主 + 落在意圖範圍內"這一輸入,而蓋章判定為"相符"的是人。自動化率約 70%,人佔 30%。唯獨這一層,完全自動在根本上不可能。因為使用者的含義,機器無法一路判定到底。
如果三層的最後一道章都由人來蓋,那麼一旦缺少這道章確實蓋過的證據,整個系統就會崩塌。有人只是嘴上說評審過、實際卻沒做,這種情況如何防範?在專案A中,atom human_review_attestation_evidence_mandatory 強制解決這一點。
這個 atom 的規則簡單且不容妥協。只要決策卡的任何一層發生了"AI 判定 → 人工評審",就必須在卡片上附上評審人標識、評審時間、評審證據(補強備註、否決理由、樣本比對結果三者中至少一項)。證據一旦為空,該卡片就無法晉升為"驗證完成"。
證據為空時,integrity_check_clickup_notify atom 便會啟動。它一旦檢測到一致性失敗 —— 這裡指"評審章蓋了,證據卻沒有" —— 就立即在協作工具裡建立一張卡片。本章開頭那張晚上 11 點的卡片,正是這套機制。
這兩個 atom 結成一對,構成"對驗證的驗證"。3-layer 感測器驗證決策,attestation atom 驗證這道驗證是否真由人做過,notify atom 抓出證據缺漏並予以通報。AI 的輔助再廣泛,責任的最後一格,也要由留下證據的那個人的名字來填。
彙集三層結果與評審證據的單位,就是決策卡。一張卡片是一條決策的完結單元,並流向季度覆盤的輸入。下面是 D17 卡片的結構。
紅色那幾行是關鍵。每一層的"證據:"一行若為空,attestation atom 就會阻止卡片晉升,notify atom 則通知協作工具。半年後若有人問"當初為什麼把 defense_factor 定為 1500",這一張卡片就能把意圖、測量、實測乃至評審人一併答清楚。決策卡執行在與第18部分決策追蹤 atom 相同的後設資料流之上。
三個層的自動化程度各不相同(分別約為 80%·95%·70%,如前幾節所見)。三者都是部分自動、最後一道章也都由人來蓋,但人的工作量整體上減少了 80% 以上。
引入從 Layer 2 開始。只要模擬和資料表已經就位,加上驗證規則即可,1\~2 個月就能見效。接著是 Layer 1(基礎設施投入少而效果大,再加 1 個月),最後是 Layer 3(基礎設施投入最大,效果也最大,再加 2\~3 個月)。想從一開始就裝上 Layer 3 而擱淺,是常見的失敗。
關於數字標註:上面的自動化率與下面的效果比例,是基於作者專案運營觀察的作者估計(未經驗證)。它們不是精密測量值,應當作方向與大致比例來讀。相符判定規則的 ±10%/±25% 閾值是實際的運營規則,atom 名稱(
integrity_check_clickup_notify、human_review_attestation_evidence_mandatory)是真實存在的 atom。
引入前後的變化,按方向來概括是這樣的。每季度的決策矛盾事故從若干件降到近乎 0 件;決策後 1 周的相符度測量執行率從少數升到大多數;事故發生前的副作用發現率從不足一半升到大多數。最有意義的變化是可追溯性 —— 事隔許久之後仍能回溯決策背景的比例,從少數變成了幾乎全部。因為決策卡儲存了遊戲的決策歷史。
| 模式 | 處方 |
|---|---|
| 只執行 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 的單人版。哪怕是單人運營,也要為半年後的自己留下證據。
週一早上 9 點 12 分。Alpha 版本剛上線那一週的首次檢查 cascade 結束了。check 把四種(doc-audit·data-qa·integrity·link,10.2)一次性跑完並停下時,控制台上打出的數字是這樣的。違規候選 47 條。 其中有幾條是 P0、該從哪件開始看、該由誰動手,這些在那 47 行裡哪兒都沒寫。
檢查器只知道"錯了"這個事實。"這是否會阻塞釋出、還是下週再看也行"——它無法判斷。Alpha 末期真正的瓶頸,不在於檢查器不夠,而在於人去給檢查器吐出的 47 行分類、結果一上午就沒了。本章把 LLM 用自然語言對那 47 行分類、再由人接過分類來排優先順序的一次完整實操迴圈,整段照搬過來。
在 10.1 中做了 30 餘種驗證 atom,在 10.2 中建立了用 3-layer 感測器過濾決策的結構。這兩章造出來的是日誌。日誌不是決策。在日誌與決策之間,存在一道過去靠人手工填補的間隙。
Alpha 末期這道間隙代價高昂,原因很簡單。檢查器一小時能跑幾十次,但人讀這 47 行、分類為"q_142 是死路,阻塞釋出;voice_lint 412 等待編劇判定"的工作,每次都得重新做。把這份分類工作交給自然語言模型,就是 Gap Report 的起點。
下面是那個週一早上,把檢查 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 行,以下為同型模式 —— 無省略,已完整附上)
已對原始日誌進行了分類。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 的分類。這是作者在會議前親自審閱、動了紅筆的部分。
it_9920 是兩週前刪除的道具。不是未完成,而是失效引用。確定阻塞釋出。模型把它壓縮成 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 的全部過程。
把上面的記錄歸納成流程,就是下面這樣。關鍵在於:所有關鍵分叉點都落在人這一側。
LLM 觸碰的方框只有藍色那一個。所有 severity 都在橙色(人工驗證)處確定,而在紅色處,一致性失敗會立即彈到協作工具。檢查·判定·確定全部歸人與 atom,模型只負責首次分類這一次。
分類流程的末端掛著 integrity_check_clickup_notify atom(10.1)。這個 atom 與生成報告的環節相互獨立,在一致性檢查失敗的那一刻,不等會議就把卡片拋到協作工具上。 如果說 Gap Report 是每週的節奏,那麼這個 atom 就是打斷該節奏切入進來的中斷。
像 q_158(引用了已刪除的道具)這樣可能破壞構建本身的違規,無法等到週一的會議。cascade 抓到它的那一刻,協作工具上就會自動生成"P0 疑似:q_158 失效引用"並分配給資料負責人。Gap Report 則是把這些中斷以周為單位重新彙總、以趨勢呈現的背板。兩層一起運轉,"緊急的即時、全域性圖景按周"這兩個節拍才能對上。
人驗證過 LLM 分類這一事實,若只停留在口頭就會蒸發。 因此評審環節掛著 human_review_attestation_evidence_mandatory atom(10.2)—— 人工評審必須有證據。
上面記錄的第 ③ 步 —— 把 412 降級、把 158 升級的那個判斷 —— 會以評審者 ID·時間戳和"變更項"清單的形式錄入報告頁尾。下個季度若有人問"為什麼 412 上線了",記錄會作答:"在 2026-W21 的評審中判定為有意引用的古舊說法,已加入違禁詞詞典例外"。沒有這份記錄,LLM 分類就與從未驗證過的自動輸出無從區分。
作為再次請求 ④ 的結果,模型渲染出的 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% 簡單相比看似惡化,但分類方式改為合併、樣本隨之不同的背景,已用一行附註說明。不以一週的比率下結論——這條原則在此發揮了作用。
在作者的專案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 分鐘內驗證的形態。
| 模式 | 對策 |
|---|---|
| 每次都由人手動分類 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 備忘代替也無妨。
衝刺(sprint)結束前兩天,戰鬥美術師通過團隊即時通訊工具發來一段短影片。新武士職業的三段連招。第一段和第二段都有刀風聲,第三段卻一點聲音都沒有。無聲。他本人說聲音都加好了,音效負責人說檔案都交付了。兩人都不是在說謊。聲音檔案確實在倉庫裡,名為 combo3_swing_final_real.wav。而遊戲程式碼要找的名字是 sfx_K012_combo3_swing.wav。兩者沒有一個字元重合。
追查這起無聲事故,花掉了那天整個下午。這不是某一個片段、某一個聲音的問題。只要名字由人隨意來起,這類事故每個季度都會重新冒出幾十起。本章講的就是把這種自由變成規則的故事。
本章要回答的問題 - 在 1萬個資源的規模下,名字為什麼不是自由而是規則 - 把命名規範以 atom(最小知識單元)強制固化、並用 lint 自動校驗後,能封住什麼 - 一個技能所掛的動畫、VFX、音效、圖示對映,由 AI 起草、由人採納的實操記錄
給非專業讀者的一句話。 1萬個資源、fbx 檔名格式,看上去像是遊戲行業特有的事情。但你要帶走的那一點,並不挑領域——"一旦名字可以隨意起,檢索、自動化、連線就會一起被鎖死。" 規模一大,命名就必須從個人偏好變成規則,而只有成為規則的名字,程式碼才能自動找到並使用——這個原則,適用於任何處理文件、資產、客戶記錄的工作。
筆者所主導的專案A是一款移動優先的 MMORPG。角色動畫資源的大致規模如下。玩家職業數量、敵方 NPC 種類是實際運營數值,片段數量與總量估計為筆者估算(未經驗證)。
| 資源 | 數量 |
|---|---|
| 玩家角色職業 | 6 |
| 敵方 NPC 種類 | 80\~100 |
| 單個角色平均片段 | 100\~150(筆者估算) |
| 片段總量估計 | 約 10,000\~15,000(筆者估算) |
1萬個。這相當於 1萬個抽屜。站在 1萬個沒有貼標籤的抽屜前找"攻擊動作放哪兒了",等於把賭注押在人的記憶力上。而這個賭注一定會輸。找不到,結果只有兩種。要麼工作時間翻倍,要麼因為沒找到而把同一個動作重新做一遍。後者更糟。因為資源會變得臃腫,而且日後同一個動作會以兩個略有差異的版本到處流動。
名字若處於自由地帶,被鎖死的不只是檢索。"由程式碼憑技能 ID 自動取到動畫檔案"的自動路由也會一起被鎖死。如果無法從名字裡讀出規則,程式碼就必須為每一個技能都握著一張手寫的對映表,記錄該用哪個檔案。每進來一個新角色,這張表就得靠人手動加長。
專案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 個。要攔住的不是數字,而是無節制的增殖。
把格式輸入為 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
這等於說,命名規範成了程式碼的分支點。
如果說命名是 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 的命名規範。最後一項就是把兩層釘在一起的那根釘子。構建時自動執行。
把到目前為止的命名 lint 與對映 lint 如何匯成一道關卡,用流程圖梳理一下。
請注意,這條流程的末端有一道可逆/不可逆的邊界。yaml 修改、LLM 候選、關鍵幀,這些全都是可逆的。不滿意就廢棄即可,成本幾乎為 0。然而一旦進入動作捕捉拍攝、配音演員錄音、標誌性嗓音選角,就變成不可逆了。這牽涉到演員與錄音棚的預約、錄音間、合同、市場認知。因此,所有命名、對映、人格設定(persona)的決策,都必須在不可逆階段之前——也就是在 yaml、LLM 候選、關鍵幀這一可逆區域之內完成。
到這裡為止講的是系統,現在原封不動地展示一段真實會話,看 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,和拿到一份可採納/可駁回的初稿再修改,兩者的工作負擔並不一樣。
上面這段記錄,正是進步式應用的一個場景。命名·對映的運營分為兩個階段。
在保守階段,由人來賦予命名、編排對映,自動化只負責校驗(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 的發展,這三處都進入了可以輔助的範圍。原本只停留在紙面上的進步式角色資源化願景,相當一部分已經移到了可以實務應用的階段。
這是專案A 命名·對映引入前後的對比。檢索時間與新人上手週期是筆者實際體感·記錄到的走向,比例類專案是季度覆盤中彙總的實測值。需要說明,部分絕對數值為筆者估算(未經驗證)。
| 專案 | 引入前 | 引入後 |
|---|---|---|
| 動作檢索時間(動畫師) | 5\~10 分鐘 | 30 秒 |
| 重複製作比例 | 12\~15% | 1\~2% |
| 新角色路由程式碼改動 | 50\~100 行 | 0 行 |
| 新技能資源缺失事故 | 每季度 5\~8 起 | 0\~1 起 |
| 未使用資源積壓(庫中佔比) | 約 30% | 約 8% |
| 新動畫師上手 | 2 周 | 3 天 |
最後一項最不起眼,卻是效果最大的。一個命名規範 atom,本身就成了上手指南。對新動畫師只要一句"名字就按這五個槽位來起,lint 攔你就聽 lint 的",第一天就能開始幹活。
| 模式 | 處方 |
|---|---|
| 命名規範只放在 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] 標籤的,由人重新翻查資源池,判斷是否可以複用。
anim_naming_lint.py 一個檔案掛到 git pre-commit hook 上。策劃會議一開始,寵物清單就擺上了檯面。狼系十二種、貓系八種、鳥系五種。沒有人說"那我們一隻一隻地做吧"。因為與角色不同,寵物從一開始就以"要量產50種"為前提。問題不是"如何把一種做好",而是從"讓多少種共享同一副已做好的骨骼"開始。
角色的每一種對使用者來說都是獨一無二的存在,因此要一種一種地精心打磨。而寵物·坐騎大多是"在同一副骨骼上只改顏色和能力的變體",所以從設計之初就要備好命名規範·模板·lint,鋪設量產管線。若把一種精心做好之後,任由它被複製成十二份,那麼只是顏色不同的十二隻狼就會各自塞進相同的動畫片段,資料夾膨脹到4GB。那不是量產,而是沒有量產的結果。核心不在於"做得多好",而在於"儘量少做、儘量多共享"。
因此本章會完整走一遍這樣一個流程:用 yaml 定義一種狼系寵物模板,讓 AI 量產繼承其骨骼的例項,再用 lint 驗證,並測量有百分之多少被廢棄。
三者的資源結構相似,但在使用者認知中的比重不同。角色是使用者與之共度100%遊戲時間的自己。寵物是陪在身邊的同伴,佔50\~70%的時間;坐騎則是隻在移動時才拿出來的工具,停留在10\~20%。認知比重越低,使用者越少留意細節。把傾注在角色上的心力同樣傾注到坐騎上,就像用同一份預算去打理每天都坐的書桌和偶爾才展開的摺疊椅。
因此寵物·坐騎採用"模板-例項"結構來運作。先做出一種承載骨骼·動作·基礎能力的模板,再在其上疊加只改顏色·圖示·細微能力的例項。例項共享模板所擁有資源的90%,所以實際新做的只有剩下的10%。把這種分離畫成圖,如下所示。
左側的模板整塊只做一次,右側的各個例項只需替換顏色、圖示和一行能力即可。前面說的"4GB 資料夾",正是漏掉這層分離、90%的資源被複制十二次時出現的景象。
寵物·坐騎的命名規範,是在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 每次都發出無謂的警告。
現在進入正題。我們來定義一種狼系模板,並由此量產例項。
在讓 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 會揪出來。先定義"可以改的東西",正是量產的安全帶。
以下是量產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 會收斂到最安全的選項(照搬既有能力)。
從 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 碰了骨骼動畫。即便明示約束,量產物中仍有一定比例會漏出,這是現實。所以需要下一步。
不用人眼一一去看10種,而是跑 lint。lint 規則直接取自第1步模板的白名單和11.1的命名規範。檢查項有4項。
每個例項若通過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種則廢棄。
這一輪的廢棄率是 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種,只需做一件事:把廢棄緣由挪成一行規則。
坐騎比寵物更簡單一級。既沒有技能,也沒有 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,遊戲複雜度就會翻倍,還得重新做與寵物·角色系統的衝突驗證。
把寵物·坐騎完整套用角色模式的情形,與用模板-例項加以簡化的情形,在作者的專案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 資料夾,正是把資源複製十二份到使用者最終也分辨不出的細節上時所付出的代價。以量產為前提鋪開後,減少的是運營成本,而不是體驗。
| 陷阱 | 處方 |
|---|---|
| 把角色系統原封不動移植到寵物·坐騎 | 減去 variant 槽位·vfx·sound 的4槽位變體 |
| 把同骨骼寵物複製為獨立資源 | 1種模板 + 例項,用白名單強制共享 |
| 未經評審就提交 AI 量產物 | lint 4道關卡 + 廢棄率測量 |
| 廢棄率每一輪都不下降 | 把廢棄緣由升格為提示詞規則(反饋) |
| 給寵物賦予角色級技能 | 每個例項 extra_ability 上限1種 |
| 給坐騎賦予戰鬥功能 | allow_combat 要慎重,做好複雜度 ×2 的準備 |
寵物·坐騎對使用者體驗的影響較小,所以 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 緣由,在提示詞中逐條各加一行規則,再跑下一批。確認廢棄率是否下降。
如果是一個人做的遊戲,沒有 lint 指令碼也行。手寫一張某個寵物系列的模板 yaml,然後對 AI 說:"在這個模板上只改顏色·圖示·能力,做5種例項,骨骼和共享動畫絕對不要碰。"把收到的5種用眼睛掃一遍,只把碰了骨骼的·違反命名規則的丟掉。把丟棄的理由在下一次請求里加上一行。只要有模板 1.1 和"把丟棄理由反饋回去",沒有工具,本章的核心也照樣運轉。
主要讀者:與美術團隊協作的遊戲策劃·美術總監(中等規模(10\~50 人)團隊) 面向個人/業餘讀者的精簡版:§12.1.8「一個人的話,做到這些就夠」
我記得把 AI 生成的 100 張概念圖貼在會議室牆上的那一天。30 秒內打印出的 100 張裡,美術總監選中的只有 3 張,其餘 97 張當場被丟棄。有人把這叫作“97% 的浪費”。可如果是手繪,畫師為了抵達那 3 張,大概要花上兩週。什麼才是浪費,在這裡被顛倒了過來。
本章討論的,是把這種顛倒轉化為運營的方法。核心只有一句話。AI 美術在可逆階段(概念·紋理探索)可以盡情量產,而在不可逆階段(最終渲染·動作捕捉·併入構建)之前,設一道由人把守的關卡。 在可以丟棄的地方就丟掉 99 張,在無法回退的地方則一張也不隨意放行。美術工具的用法在別的書裡已足夠多,本章只專注於把這些工具安全地嵌入策劃管線的那個位置。
美術資產從概念走到遊戲內,一共 7 個階段。把作者專案(以下稱“專案A”)的角色資產流程原樣搬過來就是這樣。重要的不是階段數量,而是穿過其正中央的可逆/不可逆分界線。
左側四個階段(概念\~紋理)是可逆的。抽出 100 張概念圖丟掉 97 張,損失的只是 token 成本;紋理重新生成五次,覆蓋掉檔案就結束了。所以這一段是 AI 量產能帶來最大 ROI(Return on Investment,投資回報)的位置。量產工具以自託管的 Stable Diffusion(SDXL)/ComfyUI 為主軸。原因是 IP 保護 —— 不把資產上傳到外部的封閉服務,而在本地執行,並用以角色微調過的 LoRA 與 ControlNet,在每次反覆生成時控制同一人物的一致性。封閉式工具(Midjourney 等)只在快速鋪開初期情緒板時有限使用,而需要一致性·反覆控制的正式量產則交回 SD/ComfyUI。
右側三個階段(骨骼繫結之後)是不可逆的。動作捕捉會繫結錄製棚·演員的檔期,最終渲染併入構建、上線曝光後,就會伴隨玩家的記憶與社群的反應。一旦跨過去,回退的成本會大於製作的成本。所以在分界線上立起一道由人把守的關卡。無論 AI 在可逆區間量產多少,能跨入不可逆的資產,只有通過人工評審的那些。
這一張圖就是本章的骨架。“AI 在美術上用到什麼程度”這個問題,其實是“這項工作在分界線的哪一側”的問題。
這裡把可逆區間的第一步——概念量產,完整展示一個週期。若只是抽象地寫“AI 生成概念”,就無法知道真正產出了什麼、又廢棄了什麼。下面是對專案A中量產學者公會資深 NPC 概念的一次會話的忠實再現——一份實操記錄(worked transcript,即完整保留的真實操作過程記錄)。提示詞可以直接複製使用,輸出則由真實會話重構而來。
這裡有一個最常出錯的地方。就是把提示詞以“視覺描述”開頭。公司的反饋 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 風格 · 現代服飾 · 普通奇幻法師長袍
function 與 narrative_seed 在外形之前。只有當輸入本身握著“為什麼這個角色必須長成這樣”,才能在量產結果中判斷“為什麼這個更好”。
基於附上的 concept_brief,生成 6 個角色概念方向提案。
這是用於探索的量產 —— 不是最終稿,而是供美術總監挑選的候選。
規則:
1) 把 function 與 narrative_seed 翻譯為視覺。禁止單純的美型。
(例:“把情感藏在數字背後” → 用表情·道具·姿態如何體現)
2) 不要偏離 style_anchor。forbidden 項絕對禁止。
3) 6 個方案彼此須有足夠差異。細微變體的 6 張沒有探索價值。
4) 為每個方案附上一行意圖依據(為什麼這個方向契合 function)。
5) 你懷疑‘可能與 style_anchor 衝突’的方案,單獨標註出來。
輸出格式:(方案編號 | 核心視覺 | 意圖依據 | 疑似衝突)。除此之外不要任何說明。
在這段提示詞裡,視覺描述所佔的篇幅幾乎沒有。大部分都用在鎖定輸出格式、釘死禁忌上。因為一旦給 AI 自由,它就會迴歸到訓練資料的平均值——普通奇幻法師。意圖翻譯義務、固定 style_anchor、強制 6 個方案的多樣性,以及“若有衝突就上報”這條退路,是阻止迴歸的四道門閂。
| 方案 | 核心視覺 | 意圖依據 | 衝突 |
|------|--------------------------------------|-----------------------------------|------|
| 1 | 黃銅放大鏡永久佩戴,墨漬手指 | 觀測即身份 —— 工具被身體化 | |
| 2 | 面無表情的老學者,手持帶刻度盤的卷軸 | 把情感藏在數字背後 = 無表情+記錄物 | |
| 3 | 鐘塔背景,身後冷卻中的封印脈絡之光 | 職務空間說明人物 | |
| 4 | 華麗的星紋刺繡長袍,手持魔力球體 | 強調威嚴 | ★ |
| 5 | 一隻眼為義眼,畢生觀測的痕跡 | 30 年職務的身體代價 | |
| 6 | 年輕端正的學者,強調美型 | 好感度優先 | ★ |
輸出中最有價值的不是表格,而是標了 ★ 的兩行。AI 自己在 4 號方案(華麗法師長袍、魔力球體)與 6 號方案(美型優先)上上報了“可能與 style_anchor·forbidden 衝突”。若沒有第 5 條規則那條退路,這兩個方案就會以與其餘四個方案一模一樣的面孔混進表裡。讓 AI 自己舉手標出可疑之處 —— 這正是自由量產與受控量產的分野。
不會原樣接收這份輸出。美術總監會拿 brief 把 6 個方案過一遍。實際上在這次會話裡,判定是這樣分開的。
forbidden: 普通奇幻法師長袍。這個角色不是使用魔法的人,而是觀測·記錄魔力的人。function 誤譯。narrative_seed: 30 年職務的身體代價 相悖。這個 NPC 的說服力來自“做了很久的人”的磨損。年輕乾淨的臉會削弱敘事。這裡廢棄的 2 件不是損失。若是手繪,要弄清這兩個方向是錯的得花上好幾天,而量產把 6 個方案同時鋪開,在一小時之內就篩掉了它們。
將 1 號方案(放大鏡身體化)與 5 號方案(義眼)的方向合併。
- 把黃銅放大鏡 + 一隻義眼整合到同一人物
- 情感剋制(scholarly_strict):表情為無,僅用道具訴說職務
- 再次確認 forbidden:法師長袍·魔力球體·強調美型全部禁止
這是要生成‘最終候選 1 案’、交給美術總監手工精修的階段。
AI 再次給出了把放大鏡與義眼整合到一位老學者身上的單一方向,那一張圖交到概念美術師的桌上,由手工收尾。量產(6 案) → 廢棄(2 案) → 收斂(1 案) → 人工收尾的一個週期在這裡閉合。AI 做出的不是最終資產,而是供美術總監挑選的候選範圍。
這一整圈就是本書全書的 Show 標準。若沒有哪怕一次從頭看到尾——AI 吐出了什麼、什麼被廢棄、人又收尾了什麼——那麼“用 AI 量產了概念”這句話就是空洞的。
在上面這次會話裡,6 個方案中有 2 個被廢棄。若從整條概念線來看,廢棄會堆得多得多。貼在會議室牆上的 100 張裡,採用的只有 3 張。
要誠實地對待這個比例。這是親自數了匯入初期幾次概念會話得到的方向值,而不是精確的總體比例(作者估算,未經驗證——會隨角色性格·brief 質量大幅波動)。因此,應當把它當作“比起手工時期,廢棄變得自由得多”這個方向來讀,而不是“精確的百分之幾”。
重要的是,廢棄率 0% 並不是目標。一張紙很貴,就會把一張紙打磨到底。100 張紙在 30 秒內印出,丟掉 99 張也沒有負擔,探索的幅度也隨之變寬。廢棄率上升,是探索深度加深的訊號。試圖壓低廢棄率本身的運營——比如“AI 生成的東西差不多就用吧”這種壓力——會把探索的價值一起削掉。§12.1.2 中之所以能毫不猶豫地丟棄 4·6 號方案,正是因為丟棄的成本為 0。
與概念一併,在可逆區間裡 ROI 很大的另一個位置就是紋理。這是為 3D 模型生成材質的階段,而這裡 AI 介入的格子與確定性負責的格子也劃分得很清楚。
AI 介入的只有基礎紋理一格。法線·粗糙度·金屬度這類 PBR 貼圖不會讓 AI 每次生成得都不一樣,而是交給確定性提取工具。因為只有從同一基礎生成同一貼圖,材質才會一致。這與 §6.2 城市生成器中不把獎勵曲線交給 AI、而由規則手冊把控是同樣的分工 —— 能用確定性保證的交給程式碼,需要探索的交給 AI。
即便是基礎紋理,也並非對所有資產都適合用 AI。像角色面部這種細微細節左右遊戲辨識度的地方,依然以人手為先。所以當評審關卡抓到“色調不一致”時,不是自動廢棄,而是退回去重新生成。到這裡為止全都在分界線左側 —— 無論重跑多少次都不會有損失的可逆區間。
在跨過分界線之前,檢查可逆區間量產的資產是否與整個遊戲的調性相違。這是一處僅憑人眼會漏看的地方,所以先由程式碼打第一遍。
# 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 的輸出每次都會有細微差異,所以量產出的概念·紋理是否維持遊戲的質感,由人來做最後的把關。只有通過這道關卡的,才會進入骨骼繫結·動作捕捉·最終渲染這些不可逆階段。因為一旦捕捉了動作並併入構建,一致性事故就只能靠返工·重錄·重新發布來修復。
即便策劃用 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 個領域。若是休閒品類,角色·環境兩格也就夠了。分離要循序漸進,介面要保持狹窄。
本章的數值只有三類。(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 自動化邊界)所固化的地方。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| 把 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) |
一個人的話,做到這些就夠:沒有美術團隊、也沒有資料表也無妨。挑選你自己遊戲(或你喜歡的遊戲)裡的一個 NPC,按 §12.1.2 的
concept_brief格式,先於外形寫下function與narrative_seed,再把 6 案量產提示詞原樣貼上去跑一次。從跑出的 6 個方案裡挑一個與意圖相悖的,試著反駁“這是 function 誤譯,廢棄重來”,你就會親身體會到:可逆區間的廢棄不是損失,而是探索。
如果是團隊,就從下面這一步開始。在管線上明確地畫一條可逆/不可逆分界線(§12.1.1)。就“到哪個階段為止是‘丟棄也為 0’、從哪裡起是‘回退很貴’”達成一致,並在那條界線上設一道人工評審關卡。界線一旦畫好,“AI 用到哪裡”這場每次都從頭打起的爭論,就會變成“這項工作在界線哪一側”的一次性判定。
用 setup → prompt → verify 概括就是 —— setup:在管線上定義可逆/不可逆分界線與評審關卡。prompt:按 §12.1.2 的格式先輸入設計意圖,量產 6 案,同時強制禁忌·多樣性·上報。verify:在可逆區間親自挑出 1 件意圖誤譯,用廢棄·再請求閉合一個週期,並在進入不可逆之前用 visual_regression.py 打一遍意圖之外的變化。
週四的整合評審。當我們把七個新資產貼在同一螢幕上一看,所有人同時笑了。學者角色是灰色調、沉穩的剪影,而在它旁邊炸開的技能 VFX 卻是熒光粉。兩者在各自的領域裡都是完美的決定。角色總監嚴格遵守了自己的 _STYLE_GUIDE.md,VFX 美術也忠實執行了我"要顯眼"的規格要求。誰都沒有錯,可放在同一螢幕上,兩款遊戲卻在互相打架。
這一幕既是把 ArtGuide 拆成七大領域的理由,也是必須把七大領域重新捆在一起的理由。ArtGuide 是遊戲的視覺憲法。按領域劃分,各領域總監就擁有了自治權,決策隨之加快;而若不通過整合評審重新捆合,上面那種熒光粉式的事故就會一個季度一個季度地累積。策劃在這一平衡的哪個點上落手,便是本章的全部內容。
在筆者擔任總監的專案A(東方奇幻基調、移動優先的 MMORPG)的設計倉庫裡,有一個名為 96_ArtGuide/ 的資料夾。編號 96 是為了讓美術指南在倉庫排序規則下排到幾乎最後而加上的,其下則分成七個領域。這不是抽象的"專案美術資料夾",下面就是該資料夾的真實子結構。
圖示的要點有兩個。第一,七個領域並排、平等地擁有自治權。可以想象一間辦公室裡,同一層排開七間工作室。每個房間的負責人握有本房間的決定權,但在走廊相遇時,不能失去"同屬一款遊戲"的感覺。第二,其上疊著 00_Common。七個房間都必須遵守的通用規約——即整體色彩調色盤、材質基準與時代基調——都住在這裡。06_UI 與 9.1.3 討論過的 UI 協作標準屬於同一領域,因此本章只劃出邊界便略過。
策劃並不會以相同強度介入七個領域。"策劃決定意圖與敘事、美術決定視覺"這一原則對所有領域都一致,但意圖把視覺牽引到哪一步,則因領域而異。
| 領域 | 策劃介入 | 策劃不該越過的界線 |
|---|---|---|
| 01_Character | 強 | 到概念·性格·勢力·角色定位為止。臉部比例·筆觸則不屬於 |
| 02_Animation | 中 | 到技能動作的"種類·反應"為止。幀時序則不屬於 |
| 03_Monster | 強 | 到敵人概念·勢力·生態為止。鱗片紋樣細節則不屬於 |
| 04_NPC | 強 | 到角色定位·關係·voice_profile 為止。服飾刺繡則不屬於 |
| 05_VFX | 弱 | 到"緩慢發射、大爆炸、紫色"為止。粒子數量則不屬於 |
| 06_UI | 強 | 到資訊結構·優先順序為止(9.3)。畫素間距則不屬於 |
| 07_Environment | 中 | 到氛圍·地標意圖為止。樹木多邊形則不屬於 |
右側那一欄才是本表的真正內容。即便在標註介入為"強"的領域,策劃也有不能越過的界線。角色概念可以強力牽引,但一旦連臉部比例都動手,角色總監的自治就在那一刻崩塌。而且強與弱的邊界本身會隨品類而搖擺。若是恐怖遊戲,VFX 是恐懼的核心,策劃介入隨之增強;若是休閒解謎,角色介入反而會減弱。上表是專案A的品類基準,並非普遍法則。
每個領域都以一組標準文件來運營。來看 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,至少色彩衝突在憲法層面就被阻斷了。
這裡出現了專案A實際撞上的、最現實的問題。美術團隊不讀 Markdown。更準確地說,不該強迫他們去讀。讓美術人員學習 git diff、frontmatter 與 Markdown 標題層級的成本,幾乎總是大於由此學習換來的協作效率。把策劃團隊的工具原封不動地塞給美術團隊的那一刻,協作反而會變慢。
所以專案A的流水線可以用一句話概括:"策劃團隊用 md 做決定,美術團隊只看 html"。
關鍵在於兩項自動化資產。_convert_md_to_html.py 把領域的 Markdown 指南轉換成美術人員可以在瀏覽器裡舒適閱讀的 html。色彩調色盤會渲染成實際的色塊,DO/DON'T 會渲染成視覺對比。_SyncToArtRepo.bat 則把那份 html 推送到獨立的美術專用倉庫,而非策劃倉庫。分離倉庫的理由很簡單。美術人員只拉取自己的倉庫,就不會接觸到策劃團隊內部的 md 歷史、正在編寫的草稿、其他領域的決策過程。美術人員看到的,只有已確定決策的、易讀的成品。Markdown 學習成本降為 0。
在這一結構中,策劃多了一項責任。一旦更新 md,就必須跑一遍轉換·同步步驟。只更新而漏掉同步,美術團隊就會拿著昨天的決定畫今天的圖。決策與傳達之間的這一格若是空著,自治也好、整合也好,都失去了意義。
作為非策劃協作的延伸,在概念階段使用生成式 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 等)則只用於早期情緒板一類的用途。
為了看清領域自治與策劃介入實際如何運轉,我們把 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 用於角色概念,這道邊界的審校就必須由人牢牢握到最後。
七個領域一旦擁有自治權,就會像開篇的熒光粉那樣,在領域之間產生不一致。常見的型別是固定的幾種。
| 不一致型別 | 實際示例 |
|---|---|
| 角色—環境基調差異 | 角色沉穩,背景卻華麗,二者各說各話 |
| 角色—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 次的這一週期,是阻止自治固化為孤島的唯一護欄。
把七大領域分離帶來的變化,從專案A的運營經驗中梳理如下。下表數值中,週期天數與時間基於筆者的運營經驗,屬於筆者估算(未經驗證),並非精確測量值,只應作為分離前後的方向與大致比例來讀。
| 專案 | 分離前 | 分離後 | 性質 |
|---|---|---|---|
| 美術決策週期 | 1\~2 周 | 3\~5 天 | 筆者估算(未經驗證) |
| 領域間一致性事故 | 每季度多起 | 顯著減少 | 僅方向 |
| 遊戲總監美術評審時間 | 每週多個小時 | 大幅減少 | 僅方向 |
| 新領域總監上手 | 數月 | 約 1 個月 | 筆者估算(未經驗證) |
| 美術資產廢棄率 | 高 | 下降 | 僅方向 |
誠實地讀這張表,能夠斷言的只有一點:所有專案都朝同一方向移動了。最明確的效果是,遊戲總監的時間被收回了。在沒有自治的結構裡,所有美術決策都必須經過遊戲總監一個人的桌子。這是紙張堆在一張桌子上的結構。分成七大領域後,紙張分散到了七張桌子。紙張的總量不變,但沒有任何一張桌子會被壓垮。
這裡必須斬斷最常見的誤解。自治不是責任的分散,而是時間的再分配。領域總監決定自己的領域,並不意味著整款遊戲的視覺責任被打散成七塊。在整合評審上,那七塊又重新聚到一處,最終的視覺責任依然收斂於一個人。一旦把自治當作逃避責任的藉口——一旦"那不在我的領域內"成了口頭禪——開篇那抹熒光粉就會在無人認領的狀態下進入構建。
還有一個附帶條件。七大領域自治是規模的函式。在小規模(\~10 人)團隊裡,它反而是過度工程。若還處在一名總監身兼五頂帽子的階段,那麼需要的不是七份領域指南,而是一份整合指南就夠了。自治只有在桌子開始不夠用時,才真正體現價值。
| 模式 | 處方 |
|---|---|
| 不做領域分離,所有決策都集中到遊戲總監 | 引入七大領域自治(但從中等規模(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.md、individual/。在色彩條目中,務必寫上"主調色盤:繼承 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 分鐘儀式就夠了。只是沒有可以分擔自治的人而已,憲法一張 + 每週一次對齊這一骨架,在單人團隊裡同樣能體現價值。
衝刺末期,概念美術師在團隊即時通訊工具(IM)裡丟來一張角色草圖。"這是學者公會的資深成員吧?"畫面裡的人物是一名 30 多歲的男性,穿著皮甲。而規格書上寫的是 40 多歲女性、灰色學者長袍。追查究竟哪裡出了偏差後發現,概念美術師拿到的資料是兩個月前版本的規格書,而在這期間外形指南已經改過兩次。知道這一變更的,只有策劃本人。
這起事故不是技術問題,而是流程問題。規格書的一頁要變成遊戲內的資產,平均需要 4\~8 周,在這期間,一個角色的資訊會從策劃的腦海裡,經手到概念美術師、模型師、動畫師,一手接一手地傳遞下去。每一次交接,格式都可能出現偏差;若在有偏差的情況下被接收,接手者就會用猜測去填補空白。而這份猜測,會在兩個月後化作團隊 IM 裡的一句話回到你面前。
本章討論的,就是如何把這種一手接一手的流轉,從一個人的記憶搬到系統之上。
在專案A中,角色資產流轉的路徑分為四個階段。重要的不是階段本身,而是階段與階段之間的轉換點。事故不是在階段內部發生,而是在把資產從一個階段交給下一個階段的那一刻爆發。
與其用文字說明這一流轉,不如畫成圖示。而貫穿全書的 24 個部分裡,我一直不靠手繪方框,而是讓 Claude 生成 mermaid 程式碼再渲染。本章正是把這一手法應用後得到的結果放進正文,相當於用自己的正文來印證自己的手法。下面就是我請 Claude"用 mermaid 把 spec→asset 的 4 階段流轉畫出來,並讓轉換點關卡清晰可見"後得到的輸出,原樣渲染的結果。
三個轉換點(規格→視覺、視覺→3D、靜態→動態)各自都立著一道關卡。關卡就像是在把審批檔案交給下一個部門之前檢查格式的視窗。格式不符就會被退回(虛線),回到上一個階段。若接收了格式不符的檔案,下一個部門就會用猜測填補空白。這起 IM 事故,正是在沒有關卡 1 時發生的。
mermaid 的優勢在這張圖裡顯現出來。想要再加一道關卡,或調整階段順序時,不必重畫方框,只需改動一行文本。因為圖示就是文本,它便成了版本管理的物件,和規格書一起被提交(commit)。
流轉的起點,是一份 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 項是否與當前規格書一致,本會在交接之前得到檢查。
與其從零手動填滿整份規格書,不如只定好身份與職責,把外形·表情的初稿交給 Claude 來給出。下面是一次真實往返的全文,未作概括。
提示詞(全文):
以下角色的身份·職責已確定。請據此填寫 character_spec 的"2. 外形指南"與"3. 表情·姿態"初稿。 - 職責:學者公會資深成員、主要 NPC、可結為同伴 - 性格:學者_嚴格,威嚴但公正 - 勢力:scholar_guild 約束:refgame 基調(東方奇幻,低飽和度)。外形須僅憑剪影就能與其他主要 NPC 區分開。表情採用不直接流露憤怒·悲傷的剋制型。
Claude 原始輸出(原樣,未加修飾):
2. 外形指南
- 年齡:40 多歲後半段
- 性別:女性
- 體格:偏瘦,165cm 上下(※"威嚴"是用身高來表現還是用姿態來表現,需要確定 —— 我推薦用姿態)
- 服裝:深藏青色學者服,銀線刺繡(※我不清楚勢力的配色方案。scholar_guild 是否另有代表色?)
3. 表情·姿態
- 平時:微微抬起下巴的端正姿態,視線略高於對方
- 憤怒時:話變少,只有語氣轉冷
- 悲傷時:以望向窗外的動作代替,面部幾乎沒有變化
還有一點:既然"可結為同伴",戰鬥中的表情大概也會需要,那是否在這份規格書的範圍之外?
人工驗證·駁回:
這份輸出只採納了一半。表情·姿態一節幾乎原樣採用 —— 它準確把握了"剋制型"這一約束,而"以望向窗外的動作代替悲傷"是規格書裡原本沒有的好細節。外形則被否決,理由有兩點。其一,它把體格定為"偏瘦,165cm",而已確定的規格書寫的是"略高於平均,170cm"。Claude 只拿到了身份·職責,無從得知體格,於是做了猜測 —— 這正是關卡該攔下的猜測。其二,服裝的藏青色雖符合 refgame 基調,卻與我們遊戲中 scholar_guild 的代表色(灰色+紫色)相沖突。Claude 丟擲的問題("是否另有勢力代表色?")恰恰提前點出了這一衝突。
重新請求:
很好。表情·姿態採納。外形按以下固定後重新整理:體格 = 略高於平均的 170cm,服裝 = 灰色學者長袍 + 紫色點綴(scholar_guild 代表色),佩戴眼鏡。戰鬥表情在這份規格書範圍之外,去掉。
這一次往返值得學習的一點是:Claude 用猜測填補空白的那個位置,恰恰就是規格書裡的空白。遇到未知值時,Claude 分成了兩種做法。勢力配色和戰鬥表情,它以"這一點我不知道"為由用提問的方式挑了出來,而這些提問比關卡檢查清單更早點出了缺漏。相反,體格卻沒有任何"不知道"的標示,直接用一個看似合理的數字填了進去。只要後一種情況存在,人工逐行對照已確定規格書的驗證就無法省略。
已確定的規格書交到概念美術師手上。流程與 §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 事故的直接門閂。由於在概念草案定型為模型表之前就與當前規格書作了對照,拿著兩個月前版本作業造成的偏差,便會在這裡被卡住。
這裡要指出一個運營上的不對稱。目前所看到的規格書全是 Markdown,而概念美術師和 3D 模型師並不是為了讀 Markdown 才進遊戲公司的。因此,專案A 把 §12.2.4 裡看到的單向轉換管線("策劃團隊用 md 做決策,美術團隊只看 html")原封不動地用在 spec→asset 流轉上。策劃團隊做出的 md 決策被轉換成 html,推送到獨立的美術 SVN,美術團隊只看 html —— md 的學習成本為 0。
_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。
模型表進入 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 的動作捕捉同樣不可逆 —— 捕捉環節在重新召集演員與裝置之前,是無法重來的。不可逆階段之前的關卡,要比其他關卡運作得更嚴格。
3D 資產與動畫·VFX·音效·程式碼合到一起,首次出現在遊戲中。這是所有工種齊聚一堂的階段,關卡 4(綜合評審)是最後一道門閂。
| 評審項 | 負責 |
|---|---|
| 符合規格書意圖 | 策劃 |
| 視覺基調·一致性 | 美術總監 |
| 動畫自然度 | 動畫總監 |
| 遊戲內辨識度 | 遊戲總監 |
| 效能(frame 負擔) | 技術美術 |
每個角色由 5 人花 30 分鐘\~1 小時來審。這一階段的 lint 由資產-資源對映(Skill_Art_Resource_Mapping)自動執行,檢查遊戲內實際掛上的資源與規格書所指的資源是否一致。在整合階段,AI 的作用僅限於視覺迴歸測試與 lint 自動化 —— 它不是決定要展示什麼,而是逐畫素對照昨天與今天的幀是否在無意間發生了變化的確定性工作。
本章開頭的這起 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 號(概念)上卻沒插旗。
下面是作者所運營的專案A標準化前後的對比。絕對時間·件數為作者估算(未經驗證),可信的是方向和大致的比例。
| 專案 | 標準化前 | 標準化後 | 方向 |
|---|---|---|---|
| 單個角色(規格書→遊戲內) | 8\~12 周 | 4\~6 周 | 約減半 |
| 階段間猜測事故 | 每季度 10\~15 起 | 每季度 2\~3 起 | 大幅減少 |
| 變更遺漏事故 | 每季度 8\~10 起 | 每季度 1\~2 起 | 大幅減少 |
| 綜合評審時間(每個角色) | 分散·重複(共 4\~6 小時) | 30 分鐘\~1 小時集中 | 集中化 |
| 新角色設計師上手 | 約 2 個月 | 約 1 個月 | 約減半 |
角色週期大致減半。但不能誤解這個數字。標準化不是把所有角色以同一速度批次印出的傳送帶。主要角色仍要投入近 8 周,配角則 4 周完工。標準化所做的,不是讓速度變得均一,而是讓各階段的時間差等能穩定地維持下去。標準一旦滑向管控,就會化作削減創作者創意時間的事故回到你面前 —— 標準化的目的是消除猜測與遺漏,而不是壓縮時間。
| 階段 | AI 的作用 | 強度 |
|---|---|---|
| 1. 規格書 | 起草輔助、缺漏提問(策劃評審) | 強 |
| 2. 概念 | Stable Diffusion(SDXL)·ComfyUI 量產(LoRA·ControlNet)、LLM 提示詞 | 強 |
| 3. 3D | 生成模型尚不成熟,人·傳統工具為主 | 弱 |
| 4. 整合 | 視覺迴歸·lint 自動化 | 確定性 |
階段 1·2 中 AI 強,階段 3 由人負責,階段 4 交給確定性工具。這種分工一旦確立,各階段的責任就變得清晰 —— 哪些屬於 AI 的初稿、從哪裡開始是人的決定,在關卡面前不會含糊。
| 模式 | 對策 |
|---|---|
| 規格書缺失外形·表情 6 項 | 階段 1 必查,讓 AI 提出缺漏問題 |
| 略過概念階段的關卡 | 強制在模型表定型前對照外形 6 項 |
| 手工推算變更影響 | 用 spec_change_impact 自動追蹤 |
| 綜合評審全堆到最後 | 每個階段分散設定關卡 |
| 無資產 lint 就構建 | 關卡 3 自動攔截 |
| 強制把所有角色壓進 4 周 | 維持各階段的時間差等 |
第一行與第三行,正是本章開頭 IM 事故的直接對策。
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) 每次改動外形時,養成把"這次變更所觸及的下游檔案"用一行手寫在規格書最底部的習慣。即便沒有工具,那一行也能阻止變更遺漏事故。
第一讀者:需要解讀使用者反饋與元遊戲的MMORPG策劃(中等規模(10\~50人)團隊) 面向個人/愛好者讀者的精簡版:§13.1.8「一個人的話,做到這些就夠了」
上線更新的第二天早上,我還記得遊戲內問卷的自由回答欄裡堆了312條的那個畫面。從只有一句話的短評到寫滿五行的怒火,什麼都有。策劃團隊沒有一個人把那312條全部讀完。準確地說,是讀不完。就算讀了,也只是帶著"強化大概太肝了的抱怨很多"這種印象走進會議,而這個印象不過是嗓門最大的那5條製造的錯覺。那312條到底在說什麼,沒有人知道。
本章講的是:如何在人不必讀完那312條的前提下,依然能說清"什麼話題有多少條"。核心有兩點。第一,把數百條自由回答歸為主題並標註情感這種枯燥的分類工作交給AI。第二,不照單全收AI的聚類,而是由人抓出一條錯誤分類,予以駁回並重新請求。FAQ與元遊戲分析的通論在別的書裡也有,本章只聚焦於把這類分析放進AI工作流裡運轉的環節。
FAQ與自由回答是一面鏡子,照出策劃設想的遊戲與使用者實際體驗的遊戲之間的差距。同一個問題一天在服務檯被問30次,該做的不是增加接待人手,而是重新設計指示牌。問題在於如何數出這個"30次"。自由回答不是結構化日誌,套不上 GROUP BY。"強化太貴了"和"資源不夠養不起"是同一個主題,字串卻不同。靠人用眼睛歸類,312條要花兩三個小時,而且歸類標準因人而異。
這正是AI該上場的地方。自由回答分類是一項(1)量大、(2)枯燥、(3)需要自然語言語義判斷的工作——也就是說,確定性程式碼做不了,讓人來做又很貴。不過有一點要在開工前釘死。AI產出的是主題聚類(假設),而不是確定的診斷。"強化不滿38%"只是AI打標籤的結果,不能直接導向"把強化削弱"的決定。貫穿整個第13部分的原則在這裡同樣成立——KPI定義與最終診斷交給人,自然語言歸類與初步打標籤交給AI。
自動化真正的價值也在這一點上。把分類自動化,與其說讓分析本身變快,不如說關鍵在於312條這個訊號每週早上以分好類的形態送到你的辦公桌上。自動化的價值不是節省時間,而是暴露訊號(團隊運營概念 automation_signal_value_over_time_savings)。這就好比原本只是在信箱裡堆積的信件,如今每天被分好類、投遞到對應的部門。
下面完整地走一遍一個週期,展示實際是怎麼運轉的。以下是把作者專案(移動優先的MMORPG,下稱"專案A")遊戲內問卷的自由回答做主題聚類的一次會話的忠實再現——所謂實操記錄(worked transcript),即完整保留的真實操作過程記錄。輸入提示詞可以照原樣複製使用,輸出則是對真實會話的重現。
先把原始自由回答提取成機器可讀的形態。這隻需從問卷資料庫裡匯出即可,並不是重新寫。關鍵是不美化、不摘要,連錯別字、髒話、單個詞的回答都原封不動地放進去。原文越是原始,分類的準確度就越高。
# 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"}
把附件 survey_freetext_2026-W21.jsonl(問卷自由回答312條)歸為6~10個
主題,並給每條回答打上負面/中立/正面標籤。不要拆得太細,一條回答
只歸入一個聚類。歸不進去的放到"其他"。按聚類給出條數、比例、主要情感、代表原文1條,
以表格呈現;分配沒把握的不要硬塞,拿到[模糊]清單裡,每條寫一句理由。
去掉講解,只要(甲)聚類定義 (乙)彙總表 (丙)[模糊]清單。
這條提示詞裡真正讓它分類的話只有頭兩行,其餘全是把輸出形狀框死的約束。給AI自由,它要麼把聚類拆成20個,要麼把所有回答硬塞進某個類別。擋住這一點的,是聚類數上限(6\~10)、單一歸類、情感三分類,以及"沒把握就丟進[模糊]"這個出口四條約束。分類的準確度就出自這道圍欄。
(甲)聚類定義
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說出"這條我沒把握"。
這份輸出不能原樣報上去。人要親自核對原文樣本。實際上,這次會話裡就逮到了一條。
翻開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來幹。
把上面這次會話每週自動跑一遍,就成了流水線。人手要碰的只有兩處。把聚類定義打磨鋒利的環節(前),以及把分類結果連向決策的關卡(後)。這中間的312條歸類與打標籤,由AI來跑。
決定性的設計在於:第2段(人工驗證)不會讓AI的輸出自動通過。做成自動通過型,AI一旦劃錯一次邊界,就會每週朝同一個方向扭曲訊號。可疑候選(模糊清單)由AI挑出,但要不要改聚類定義,由人來定。而且彙總表本身並不是決策,只不過是決策關卡的輸入。"C1強化成本26%"是讓總監去審視強化曲線的訊號,而不是自動削弱的觸發器。
如果說自由回答是"使用者說了什麼",那麼元遊戲(meta-game)就是"使用者實際做了什麼"。遊戲上線後,會沉澱下策劃並未設想的玩法,這就是元遊戲。比如構築meta(特定技能組合的扎堆)、動線meta(偏好的刷怪路線)、交易meta(與官方行情不同的玩家共識價)之類。這與自由回答不同,是用行為日誌做定量測量的,由確定性程式碼(Python)來彙總。這不是AI該插手的地方。
關鍵是把兩者疊在一起看。上面那次會話裡,C1(強化成本)的不滿以26%居首。此時若行為日誌中的構築多樣性指數(頭部技能組合的集中度)在同一周下降,那麼"無論嘴上還是行動上,都在向單一構築、單一成長路線收斂"這兩個訊號就指向同一個方向。當定量與定性一致時,決策才有把握。反過來,若自由回答風平浪靜,行為日誌卻單單向一種構築扎堆,那可能是使用者感到不適卻不說出口(即靜默流失前夕)的風險訊號。
這裡的分工同樣清晰。行為日誌的彙總由程式碼而非AI來做。因為構築佔比或交易行情是不能每次呼叫都變的確定性數值。AI只用來歸類自由回答這種非結構化文本,而定量KPI由程式碼釘死。
本章的比例遵循序言〈一個承諾〉的原則。§13.1.2中的"C1 23%→26%、重新分配9條"是從樣本312條(單一週次)實際數出來的值,因此不作為絕對值,而是當作"強化成本是單一最大主題"這個方向來讀。因果不下斷言——不存在"做了FAQ分析,留存率就上去了"這類表格。取而代之,這套工作流真正可測量的有三項:聚類驗證中被人推翻的錯誤分類條數(為0則是驗證流於形式的訊號)、算出周彙總所花的時間、定量與定性訊號是否一致。
在§13.1.2裡,人推翻了C2的9條分配。每週跑驗證,這種推翻每次都會出現0到幾條。重要的是推翻0條並不是目標。若驗證中一條都沒被推翻,那只有兩種可能——AI完美(罕見),或者驗證者沒看原文、只是蓋了個章。後者佔壓倒性多數。
每週逮到一兩條邊界案例,並以此為契機讓聚類定義一點點變得鋒利,這時驗證關卡才算真正在運轉。這是"人必須定期對AI分類的準確度做抽樣評審"這一通用原則的具體形態。同一類使用者被分散到不同主題的錯誤分類,若不做評審、只信任自動分類,就會每週累積。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| 只靠人用眼睛掃讀自由回答 | 嗓門大的5條代表312條的錯覺 | 用AI聚類做全量分類(§13.1.2) |
| "AI幫我分析下使用者反饋"整個甩給它 | 聚類被拆成20個,或強行分配 | 強制聚類數上限·單一歸類·[模糊] |
| 不加驗證就上報AI的彙總表 | 邊界錯誤分類改變決策方向 | 原文樣本 + 親自核對邊界案例 |
| 把彙總比例直接等同於決策 | "不滿26%所以削弱"式的自動觸發 | 彙總表只是決策關卡的輸入 |
| 只看定性、無視行為日誌 | 錯過無聲的靜默流失 | 把定量(程式碼)·定性(AI)疊在一起讀(§13.1.4) |
| 讓AI去彙總定量KPI | 每次呼叫數值都變,平衡隨之動搖 | 構築·行情的彙總用確定性程式碼 |
第三條最常被忽略。彙總表很乾淨,讓人忍不住原樣相信。但正如id 0156那一條,邊界上的一個錯誤分類,就能把優先順序整個改寫。驗證不是把312條重讀一遍,而是只把最大的兩三個聚類的邊界案例拿原文核對。
一個人的話,做到這些就夠了:沒有問卷資料庫也行。把你自己的遊戲(或喜歡的遊戲)的商店評論、社群帖子只收集30\~50條文本,原樣貼上§13.1.2的提示詞跑一次。從跑出的聚類裡挑一條"這歸得有點怪"的分配,試著反駁"這條回答的第一痛點是別的主題,請重新確定定義並重新分配",你就會切身體會到,聚類原來是一堆判斷的集合。
如果是團隊,就從下面這一步開始。把一週的自由回答不加美化地匯出為 survey_freetext_YYYY-Www.jsonl,用§13.1.2的提示詞跑一次。然後只把最大的兩個聚類的邊界案例拿原文核對。聚類定義一旦打磨鋒利,之後每週用同一條提示詞就能自動積累起可復現的周彙總。
主要讀者:對運營指標負責的運營/資料策劃(中等規模(10\~50 人)團隊) 面向單人/興趣讀者的精簡版本:§13.2.8「一個人的話,只需做到這些」
每個週一早晨,同樣的場景都會重演。資料團隊發來的每日儀表盤截圖被投到會議螢幕上,有人說「DAU(Daily Active Users,日活躍使用者)好像掉了一點」,又有人接一句「那是因為上週做了停服維護」。數字就擺在那裡,可是判定這個數字到底是異常訊號還是噪聲,這項在人腦裡進行的工作每週都要從頭開始。而且這個判定因人而異。
先把本章的結論寫在前面。在 KPI 上,人必須做的事只有兩件:決定把什麼定為 KPI,以及判定要把 AI 提交的異常訊號升級為確定診斷,還是駁回。夾在這兩件事中間的兩項——每天在同一時刻從 raw 日誌裡提取數字,以及就相較上週有什麼發生了波動用自然語言寫出初稿——分別交給確定性程式碼和 AI。KPI 定義的一般論(壓縮到 5\~7 個、當心古德哈特定律(Goodhart's law))在別的書裡已經講得夠多,本章只聚焦於把這份定義放進 AI 工作流去運轉的環節。
在 KPI 運營中,有兩項判斷只有人能做。第一,把什麼定為 KPI。第二,把每個 KPI 的定義用一句話釘死。這兩項是遊戲的價值判斷,無法委託給 AI。「把 Active 視為遊玩 5 分鐘以上」這個決定裡,包含著遊戲把什麼看作健康狀態。
問題在於,這個定義一旦動搖,建立在它之上的所有數字都會跟著動搖。如果一邊的查詢把「Active User」算作登入 1 次,另一邊的查詢算作10 分鐘 + 狩獵 1 次,DAU 就會整個錯位。所以,比起定義本身,守住定義的一致性這件事佔了運營的一半。而且一致性檢查該由程式碼來做,而不是人腦(§13.2.5)。
定義釘牢之後的工作,就不是人的職責了。每天在同一時刻提取數字的抽取,瀏覽相較上週的變動、記下異常訊號候選的初稿撰寫——這兩項每天重複,一旦由人來做,標準會天天飄移,正是該下放給機器和模型的那類工作。抽取交給確定性(程式碼),初診交給 AI。人只接收 AI 提交的候選,判定要確認還是駁回。
| 階段 | 由誰 | 為何在此 |
|---|---|---|
| KPI 選定·定義 | 人 | 遊戲的價值判斷,不可委託 |
| 每日 raw 抽取 | 程式碼(確定性) | 相同輸入 → 相同數字,可迴歸驗證 |
| 相較上週的異常訊號初稿 | AI | 自然語言摘要對 AI 友好,但只到「假設」為止 |
| 確定診斷·細分確認指示 | 人 | 對 AI 假設升級/駁回,是責任所在 |
這一分工是本章整體的骨架。下面我們把一個迴圈從頭到尾跑一遍。
下面把一個迴圈從輸入到人工判定完整展示,看它實際是怎麼運轉的。以下是把作者專案(移動優先的 MMORPG,以下稱「專案A」)的每日 KPI 診斷會話匿名化後重現的內容。raw 日誌的模式(schema)、抽取程式碼結構、提示詞都取自真實工具,而數字是為展示形式而設的示例值,並非實測 KPI。
首先,程式碼每天 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 掉了」,而是走到「去確認是哪個細分掉了」,這份拆解就必須存在於輸入之中。
附件 kpi_daily_2026-06-05.json 是 09:00 自動抽取的每日 KPI,compare_to 是上週
同一星期幾。從 L0~L1 裡,只挑最多 2 件值得視為異常訊號的。要挑相較上週的變動
超出平時該星期幾波動幅度的。不知道平時波動幅度就別編,寫「不明」並排除。
每個候選寫明指標·變動幅度·1~2 個推測原因假設·待確認的細分,但原因務必只以「假設」
來寫,別像確定結論那樣斷言。日誌裡沒有的活動·補丁之類的,禁止斷言。細分只在附件
segments 之內指認。若為 0 件,只寫「0 件,無異常事項」。
輸出為每個候選 5 行(指標/變動/假設/待確認細分/信心度 高中低),去掉散文。
這條提示詞的特點在於,五行規則裡幾乎全是禁止句。一旦給它自由,AI 就會把 KPI 診斷美化成貌似成立的確定敘事(就像「因簽到活動結束,DAU 下降了」這樣,斷言日誌裡並不存在的活動)。所以,把禁止確定診斷·只以假設·細分只在輸入之內·不知道就說不知道釘成釘子,先劃出一條 AI 不得越過的線。
以下是對 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。它會在下一步被攔下。
要查三點。
第一,違反規則。[參考] 那行把輸入 JSON 裡沒有的活動當作事實來斷言。活動日曆並不在這份輸入裡,所以那是 AI 無從得知的資訊。這一行駁回。
第二,採納候選 1。會話時長下降是真實存在的,而 AI 給出的兩條分支(新增同期群 / iOS 構建)可以用輸入裡的細分實際確認。採納,但它還只是異常訊號,而非確定原因。人要做的是跑細分查詢,分辨出究竟是兩者中的哪一個。
第三,擱置候選 2。AI 自己也說「無法斷定」,而且絕對幅度小。在把平時該星期幾的波動幅度(按星期幾的標準差)加進抽取程式碼之前,先當作噪聲處理。這是程式碼這邊的作業——AI 申報「不知道平時波動幅度」,其實是指出了輸入資料的缺陷。
於是重新請求。
最下面的 [參考] 那行斷言了輸入裡沒有的簽到活動,刪掉它。只保留候選 1,
把會話時長下降按 d0_7/d8plus × ios/aos 拆成 2x2,重寫成「確認哪一格掉得最多」
的一行動作。別斷言原因,只給確認動作。
這一次往返就結束了。AI 刪掉了 [參考] 行,以「先看 d0_7 × iOS 格的會話時長」這一行確認動作重新作答。那份輸出通過了規則,人跑那條查詢——若實際確認到新增 iOS 同期群那一格掉得最多——這時才終於下出「新增 iOS 新手引導會話流失」這一確定診斷。下診斷的,自始至終都是人。
核心:AI 只知道到「該看哪裡」為止。「原因是什麼」要由人拆分細分、確認之後才能確定。若不用提示詞強制這條邊界,AI 每次都會滑向貌似成立的確定敘事。
把上面的迴圈用圖固定下來,之後所有的每日診斷都會走同一條路。人手觸及的地方只有兩端兩處(定義·確定),一眼即可看清。
三條分支的顏色不同。藍色系(抽取·定義 diff)是確定性的,對相同輸入保證相同結果。只有中間的 AI 那一格是非確定的,所以兩側由程式碼來把關。最末端的確定診斷是人。§13.2.2 中 [參考] 行被攔下的位置,正是「F 人工評審關卡」。
在把初診交給 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 是用不同定義計算的」。用人眼幾乎抓不出來,要用程式碼來抓。
最安靜的 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 的領域——定義一致不是判斷,而是比較,一旦摻入非確定的模型,反而會增加事故。
鋪好這條管線後,最先讓人想炫耀的是「診斷時間縮短了」。然而真正的價值在別處。作者的團隊運營理念中,有 automation_signal_value_over_time_savings 這樣一條——自動化的價值,不在於省下的時間,而在於被暴露出來的訊號。
在 KPI 自動化之前,像會話時長下降這樣的訊號,得靠某人偶然去看一眼圖表才會被發現。自動化之後,每天 09:00「相較上週異常訊號 2 件」會以自然語言送到桌面上。縮短的是分析時間,但改變的是要多少天才能察覺那個訊號。原本要靠偶然才能看見的東西,如今每天被強制暴露出來。
所以這個工具的成功,不用「診斷少花幾分鐘」來衡量,而用首次察覺異常訊號所需的時間(訊號 → 察覺)來衡量。一旦這個方向被破壞——也就是 AI 摘要每天只打出「無異常事項」,導致誰都不再讀——那麼工具就等於省了時間卻殺死了訊號,一兩個季度內便淪為廢物。
本章的數字遵循序言〈一個承諾〉的原則。出現的 KPI 數字(DAU·會話時長變動率)全都是為展示形式而設的示例值,並非實測——不按絕對值,而按結構來讀。KPI 定義(Active·Retention)沒有業界公認的單一標準,所以結論是「把自己的定義寫成明文」(§13.2.4)。真正可測量的有三樣:def_diff 抓到的定義不一致件數(目標 0)、AI 診斷候選中被人駁回的比例、異常訊號被察覺之前的時間。反過來,像「KPI 自動化讓留存上升了」這樣的因果,不做斷言。
一個人的話,只需做到這些:沒有日誌 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 不一樣」這個最安靜的事故。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| 擺了 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 的 [參考] 那一行,一個流暢的斷言若不被駁回而通過,那個假的原因就會成為下一個季度決策的輸入。人的位置不在於撰寫摘要,而在於駁回摘要裡的斷言。
主要讀者:依據 KPI 做季度決策的資料負責人、總監(中等規模的 10\~50 人團隊) 面向個人/業餘讀者的精簡版:§13.3.9〈一個人的話,做到這些就夠〉
我曾在某個週一早晨的儀表盤上看到一條紅線。30 日留存率相比上週明顯下滑。會議室裡的人各自給出一個原因。有人說是上週更新的新獵場,有人說是競品的新賽季,有人只說是"季節性因素"。聽起來都有道理。問題在於,直到那天下午過去,我們連該驗證什麼都沒能達成一致。假設有五個,可待驗證的分段一個都沒定下來。
本章講的是如何結束那樣的早晨。核心只有一句。看到異常指標時,不讓 AI 下確定診斷,而讓它給出 3\~5 個可驗證的假設。 AI 不會斷言"留存下降的原因是 X"。它給出的是"若為 X,則這個分段會呈現這樣的模樣"這類驗證設計,而決策由人來做。資料驅動的一般論在別的書裡已經足夠,本章只聚焦於把那套一般論落到 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)以從這些日誌中提取的異常訊號為輸入。
先用一張圖把從單個異常指標通向決策的整個迴圈固定下來。在這張圖裡,AI 介入的格子只有一個,即"生成假設"。它之前(提取)和之後(驗證、決策)都是人和程式碼的位置。
人經手的地方有三處。定義什麼算異常的位置(最前端,已在 13.2 結束)、挑選先驗證哪個假設的位置(第3段)、做出最終決策的位置(第5段)。其間枯燥的日誌彙總由 Python 完成,快速鋪開假設由 AI 完成。AI 下確定診斷的格子在這個迴圈裡並不存在。 假設為被反證而存在,一旦被反證,就回到第2段。
完整展示實際如何跑完一個迴圈。下面是對上文那個週一早晨留存下降所做的重現會話。輸入的提示詞可以照原樣複製使用,輸出則忠實重現了真實會話。
首先,人不丟擲"留存下降了"這種感覺。而是丟擲 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
附上的 retention_break_2026Q2W3.txt 是 Python 提取的 d30 留存按分段的變化
(baseline=前 4 周平均,current=本週,n=樣本數)。零氪、新獵場_遊玩這兩個分段
下降很大。不要診斷原因,只需建立 3~5 個可驗證的假設。每個假設用
假設一句話 / 待驗證的分段(如何進一步拆分) / 若成立會出現的模式 / 證偽的
反證條件,就這四行。樣本小的分段(n<2000)不要作為核心依據,若要用則
標註其侷限。假設按最便宜就能反證的順序排列,資料無法判別的,
另行標註"需人工判斷"。輸出只要假設卡片列表,不要敘述。
這段提示詞裡沒有一句"去找原因"。取而代之的,只寫明瞭假設該具備的骨架和不可逾越的界線。給 AI 自由,它就會吐出"大概是新獵場難度的緣故"這類貌似成立的確定判斷,而這句話一旦記入會議紀要,就會未經驗證地變成決策。擋住它的裝置有四個:禁止斷言;每個假設必須給出驗證分段、預期模式、反證條件;按反證成本排序;"無法判別的交給人"。 這是把輸出從診斷改造成驗證設計的裝置。
[假設 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 生效的痕跡。
不能把這些假設卡片直接搬成決策。由人確定優先順序,再由 Python 重新彙總。
假設 2 的反證成本最低。新獵場_未遊玩分段在第 1 步的表裡已經有了 —— -5.0%。它維持了 baseline。也就是說,沒玩獵場的使用者安然無恙。假設 2 當場被否決,同時假設 3(外部因素導致整體下降)也隨之削弱。 因為若是外部因素,未遊玩的使用者本也該一同下降。下降集中在遊玩過新獵場的使用者身上。
於是收窄到假設 1,再跑一次 Python。把新獵場_遊玩按首次死亡次數重新拆解後,死亡 6 次以上的區間裡 d30 流失格外突出(方向:死亡越多、流失越陡的單調關係 —— 精確數值由構建的 telemetry 測量,這裡只看方向)。與假設 1 的預期模式一致。
剩下的是假設 4。人確認了補丁說明 —— telemetry 模式無變更。測量偽影的可能性被否決。至此,決策的材料齊備了。
[第5段 人做決策 —— 決策卡]
- 採納:新獵場初期難度(首次死亡頻率)是零氪新使用者流失的首要動因。下一個構建中對 1\~5 級區間的敵人密度、血量做下調 A/B。
- 否決:外部因素說(假設 3)、測量偽影說(假設 4)。
- 擱置:獎勵曲線(假設 2 的殘留)—— 若在調整獵場難度後下降仍未消失,則重新點燃。
- AI 的角色記錄:診斷 0 件,提供假設 4 件 + 驗證設計。決策由人做。
輸入(異常訊號)→ 提取 → 假設 → 驗證 → 決策,這一個迴圈在此閉合。AI 一次都沒說過"原因就是這個"。它只鋪好了驗證的路。這就是本章的"展示(Show)"標準 —— "AI 分析了資料"這句話,若不曾把假設了什麼、什麼被反證、人決定了什麼完整看過哪怕一次,就是空洞的。
生成假設與確定診斷之間的差別看似微不足道,卻劃分了決策的安全。把兩者並置,差別就一目瞭然。
| 確定診斷(禁止) | 生成假設(本章的做法) | |
|---|---|---|
| AI 輸出 | "留存下降的原因是新獵場難度" | "難度假設 —— 去看首次死亡 6+ 區間,這樣就對、那樣就錯" |
| 人的下一步行動 | 照單全收並決策 | 從最便宜的假設開始嘗試反證 |
| 出錯時 | 錯誤決策直接進入構建 | 在驗證階段被否決,成本為 0 |
| 責任歸屬 | "是 AI 說的"(責任蒸發) | 人挑選假設並決策(責任明確) |
確定診斷真正的危險不在準確度,而在於它會讓人跳過驗證。一句貌似成立的話能平息會議室裡的懷疑。反之,假設卡片本身就是一份"去確認這個"的作業,是一種未經驗證便無法邁入決策的結構。把 AI 當作假設發生器而非診斷器,原因就在這裡。
資料驅動最深的陷阱是 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)。
生成假設並非對所有決策都同樣有用。隨著決策的時間跨度與資料密度不同,對 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 的迴圈。
本章的數字遵循序言〈一個約定〉的原則。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 等)是為展示工作流形態的示例構造,而非某個特定季度的實測公開值 —— 需要記住的不是數字,而是結構。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| 向 AI 問"原因是什麼" | 貌似成立的確定判斷未經驗證就被拍板 | 禁止診斷,強制 3\~5 個假設 + 反證條件(§13.3.3) |
| 把樣本小的分段的波動當訊號 | 把噪聲誤當成原因 | 在提取階段一併傳入 n 並標明閾值 |
| 直奔單個 KPI 作為目標 | Goodhart 扭曲在季度末爆發 | 決策前做 AI 紅隊 + 護欄指標成對(§13.3.5) |
| 用資料去做沒有資料的長期決策 | 過去的假設把未來願景往下拽 | 按資料密度對 AI 角色分級(§13.3.6) |
| 收到假設未經驗證就採納 | 假設搖身變成結論 | 從最便宜的假設開始反證,善用未遊玩分段 |
第三種爆發得最晚。d7 留存上升,決策看起來像是成功,可兩個月後 d14 斷崖與戰鬥次數下降一同到來。在決策之前把 AI 紅隊跑一次的那 30 分鐘,買下的是那兩個月。
一個人的話,做到這些就夠:沒有日誌管線也沒關係。從你自己的遊戲(或你愛看的遊戲的公開指標)裡,挑一個最近下滑的數字。把這個數字拋給 AI,但不要說"告訴我原因",而請求"禁止確定診斷,給出 3 個可驗證的假設,並附上反證條件"。從中挑一個最便宜就能確認的假設,親手把資料拆一次,你就會切身體會到"接受診斷"和"驗證假設"在決策安全上差別有多大。
如果是團隊,就從下面這一步開始。在異常指標提取指令碼抽取各分段數值時,加一行,讓它必須一併輸出樣本數(n)(§13.3.3 的 retention_break_extract.py)。然後在確定下一個 KPI 目標時,把 §13.3.5 的 Goodhart 紅隊提示詞跑一次,把一對護欄指標錄入決策卡。僅憑這兩點,"AI 診斷了原因"就會變成"AI 鋪開假設、人驗證後做出決策"。
首要讀者:移動優先專案的 UX·系統策劃(中等規模(10\~50 人)團隊) 面向個人/業餘讀者的精簡版:§14.1.7「一個人的話,做到這一步就夠了」
我還記得,第一次把在 PC 構建上執行良好的戰鬥 HUD 放到移動端解析度上顯示的那天。螢幕有一半被各種狀態條、圖示、小地圖和任務追蹤器佔滿,真正的角色反倒看不見了。每一個元素看上去都是必需的。問題在於,"該拿掉哪一個"每次開會都要從頭重新吵一遍。有人想保住小地圖,有人想保住聊天框。因為依據是"感覺",所以每次得出的結論都不一樣。
本章講的就是終結這場爭吵的方法。核心有兩點。第一,把移動端約束從"感覺"變成可驗證的規則手冊。第二,把"將 PC 30 種縮減為移動端 10 種"這種枯燥而重複的壓縮工作交給 AI,人只做抓規則手冊違規項的稽核。移動端 UX 的通用知識,別的書裡已經講得夠多了,所以本章只聚焦於把這些知識放進 AI 工作流去運轉的那個環節。
用表格羅列移動端約束的書有很多。無非是說螢幕小、手指粗、會話短、耗電快。這些都沒錯,但把表格背下來,到了會上還是答不上"那這個按鈕到底行不行"這個問題。只有當約束變成用數字表示的合格/不合格標準,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 起草、人來稽核的分工。
下面把實際怎麼運轉的一個完整週期從頭演示到尾。以下內容忠實再現了作者專案(移動優先 MMORPG,以下簡稱"專案 A")的戰鬥 HUD 壓縮會話,是一份實操記錄(worked transcript,即完整保留的真實操作過程的記錄)。輸入的提示詞可以照原樣複製使用,輸出則是對真實會話的重現。
先把 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 ...
把附上的 hud_pc_inventory.yaml(PC 構建當前 HUD 30 種)按移動端橫屏握持、雙手操作
為標準做優先順序壓縮。把每個元素分類為 P0(戰鬥中始終必需)~P3(移除或按情況顯示),
常駐顯示(P0~P1)合計不要超過 16 種。可操作元素(可操作:true)請建議放到
雙手拇指夠得著的角落(左下=移動,右下=技能)。每個分類都必須附上一行依據,
存疑的不要硬性判定,單獨標出來交給人處理。
輸出只要表(元素 | 分類 | 佈局區域 | 依據 | 是否存疑),不要說明。
值得注意的是,這段提示詞裡"要求壓縮"的指令本身只有一句,其餘篇幅全都是把輸出圈住的圍欄。給 AI 自由,它就會吐出一般 RPG 的平均值。所以要預先輸入好分類標準(P0\~P1 ≤ 16 種)、輸出格式(表)、依據義務,以及"存疑就交給人"這條退路。壓縮的質量由這四道圍欄決定。
| 元素 | 分類 | 佈局區域 | 依據 | 存疑 |
|-------------|------|----------------|----------------------------------------|------|
| 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 就不會硬編造答案,而是在自己不確定的地方插上旗子。
這份輸出不能照單全收。要用規則手冊過一遍。實際上,這次會話裡就抓到了一處。
AI 把 party_frame 放到了"左側縮小",可橫屏握持時,螢幕左側中央是兩隻拇指哪一側都夠不到的區域(左手被綁在左下移動、右手被綁在右下技能)。而組隊框是需要點選(選中隊友)的可操作元素。這違反了規則 3("可操作元素放在雙手拇指容易夠到的角落")。AI 在 party_frame 上漏掉了 可操作 標誌。這是因為輸入 yaml 裡 party_frame 的 可操作 是空的 —— 也就是說,這是人這一側的資料缺陷。
於是重新提出請求。
party_frame 是需要點選選中隊友的可操作元素(剛才輸入裡漏掉了)。
按照"可操作元素要放在拇指夠得著的角落"這條規則重新安排它的佈局。請把單人時
和組隊時分開來給建議。
一個來回就結束了。AI 重新答覆:單人時"隱藏",組隊時"提升到底部右側(容易夠到)",這個決定通過了規則手冊。壓縮 30 種,人從頭做要半天,而 AI 起草 + 規則手冊稽核 + 一次來回則在一小時以內(作者估計 —— 具體省下的時間因團隊和元素數量而異,所以與其看絕對值,不如把它理解為"從頭手工做"和"起草 + 稽核"之間的結構差異)。
把上面會話裡反覆出現的"手指區域"用一張圖固定下來,之後所有的佈局判定都會更快。橫向握持的手機上,手指夠得到、視線也常落到的底部,分成三個位置。左手拇指夠到左下(移動)、右手拇指夠到右下(技能)角落,而兩隻拇指之間的底部中央,是放消耗品、自動道具和技能槽位的地方。這裡雖然不是需要極速反應(twitch)的操作,卻是一個重要的掃視(glance)區域 —— 能一眼看到自己在用或自動消耗的東西,偶爾也會去按。P0 操作與槽位是綠色,手指夠不到、只用來讀的頂部與中央上方是紅色。
規則很簡單。只用來讀的資訊(HP/MP/目標血量)放在紅色(頂部·中央上方)也沒關係,因為手指根本不會去夠它。反過來,要按的元素必須落在手指區域(綠色·琥珀色)之內 —— 移動、技能放在兩側底部角落,消耗品、自動道具和快捷槽、技能槽位放在底部中央。這三處都是手指夠得到、視線也常去的地方。§14.1.2 裡 party_frame 被抓出來的原因,用這一張圖就能解釋清楚 —— 因為它把要按的元素放在了手指區域之外的左側中央(閱讀區域)。
壓縮方案有沒有守住規則手冊,每次都靠肉眼看,還是會漏。§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 關卡搬到了移動端維度 —— 能用確定性抓到的交給程式碼,需要非確定判斷的交給人,這套分工在移動端同樣成立。
整個週期一眼看下來是這樣的。
人手接觸的地方只有兩處。把輸入資料乾淨地放進去的位置(最前),以及做出規則手冊抓不到的存疑判斷的位置(最後)。夾在中間那段枯燥的 30 種壓縮,由 AI 和 lint 來跑。
這裡只簡短記錄本章出現的數字的出處(全書的數字原則見序言「一個承諾」)。觸控 44pt(HIG)、48dp(Material)、對比度 4.5:1(WCAG)是平臺官方標準,而"常駐資訊 8\~12 種"和"壓縮從半天到一小時"是作者基於經驗的估計(未經驗證),所以要看方向而非絕對值。移動端 HUD 上真正可測量的指標是規則手冊違規數(lint 為 0)、常駐顯示元素數(目標 ≤12)、誤觸率(telemetry),而留存率這類結果指標不會由一個 HUD 決定,因此不對因果下斷言。
| 模式 | 為什麼會失敗 | 處方 |
|---|---|---|
| 把 PC HUD 原樣縮小移植 | 30 種蓋滿 6 英寸,遊戲看不見 | §14.1.2 壓縮會話 |
| "AI 幫我把移動端 UI 做出來"式整體外包 | 沒有規則手冊就只會得到一般 RPG 的平均值 | 先把規則手冊(§14.1.1)輸入到提示詞裡 |
| 壓縮方案只靠肉眼稽核 | 每次都漏掉觸控尺寸·拇指區違規 | 用 hud_lint.py 自動校驗 |
| 無依據地"這個拿掉吧"式開會 | 結論每次都在變 | 強制 P0\~P3 + 一行依據 |
一個人的話,做到這一步就夠了:沒有資料表也沒關係。把你自己的遊戲(或你喜歡的遊戲)的 PC HUD 元素,手寫 10\~15 個做成 yaml,再把 §14.1.2 的提示詞原樣貼上進去,跑一次看看。找出一個你不同意 AI 分類的項,反駁它"重新給出依據",你就會親身體會到,壓縮其實是一堆判斷的集合。
如果是團隊,就從下面這一步開始。把現行 HUD 元素清單抽取成 hud_pc_inventory.yaml(它已經在資料表裡了),再把 §14.1.4 的 hud_lint.py 規則手冊三條(觸控尺寸·拇指區·總量)先用程式碼固定下來。有了規則手冊,無論是 AI 的壓縮方案還是人的初稿,都能用同一條線來量。
第一次把 Alpha 版本放到 PC 上的那天,策劃團隊的即時通訊頻道里貼出了一張截圖。在移動端佔滿螢幕底部的虛擬搖桿,此刻在 27 英寸顯示器正中央只有巴掌大小地懸著。有人跟了一句:「這個用滑鼠怎麼操作?」核心邏輯並沒有問題。戰鬥、背包、任務都照常執行。崩掉的只有一處——把輸入和畫面按移動端前提寫死的那塊地方。
把同一款遊戲釋出到 iOS、Android、PC 三處,運營單位看似會變成 ×3,實際上並非如此。核心邏輯只有 1 個,在其上以 ×3 掛載平臺適配層。問題在於,「哪裡為止是核心、從哪裡開始是適配層」這一點很難由人逐一判斷。只在 iOS 上正常而唯獨在 Android 上出錯的分支、只在 PC 上才有意義的按鍵對映——這類差異無法全部裝進腦子裡。因此本章的核心是這樣一套工作流:把平臺約束以規則手冊(rulebook)明文化,以該規則手冊為依據讓 AI 生成分支方案,最後由 lint 抓出違反規則之處。
先看差異的地形。下面是在專案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 才能抓出違規。
專案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/ 下填入一個資料夾即可。下面這張圖,一覽這條線在實際中如何分岔。
iOS 與 Android 的方框是同一藍色系,唯獨 PC 是橙色——用顏色標示了差異的大小。運營負擔的不對稱在此一目瞭然。
核心轉折點在這裡。把平臺約束寫進散文式文件,人會忘記。取而代之,把它們彙集到一個宣告式的規則手冊檔案裡。下面是專案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 列表,因而成為機械地抓出層越界的依據。
現在從頭到尾跟一遍實際操作。情況是這樣的。在準備 PC 輔助釋出的過程中,需要把原本只存在於移動端的輸入處理器分支出 PC 版本。以規則手冊為依據,向 Claude 索取 PC 輸入介面卡的初稿,用 lint 驗證,直到拒絕·再請求,走完一個迴圈。
下面是我們專案的平臺規則手冊節選和現有的 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。沒有把握的部分請標出來。
這是初稿。我把按鍵對映抽取到模組頂部的 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 缺失而抓出。
這裡 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。
把再請求後得到的分離版本交給 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 生成的初稿是否滿足規則手冊的基準,不再由人眼、而是由指令碼來確定。這一行,替代了在多平臺運營中人每次都要在腦中核算的工作。
把這個迴圈壓縮成一張圖,如下所示。
規則手冊向提示詞與 lint 兩側供給基準,正是這一結構的核心。AI 生成、人判斷、lint 確定——三種角色看的是同一份規則手冊。
處理器齊備後,構建就是簡單的組裝。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 為基準倒推。
在程式碼之下,畫面也會分岔。以經驗來看,推薦的分佈是通用元件 80%、平臺變體(僅大小·位置不同)15%、平臺專用 5%。不過這一比例會因品類而波動——若是休閒益智類,通用可升至 90%,而 MMORPG 因輸入差異,變體會更多。
專用元件是發揮平臺魅力的地方,並非一味通用化就是答案。移動端的虛擬搖桿·振動、PC 的按鍵對映 UI·手柄設定這類只在該平臺上才有意義的東西,都歸入此處。不過專用一旦超過 30%,那就不是魅力,而是運營負擔的訊號——在 lint 中掛上 platform-specific-ratio 警告,即便人忘了,構建也會替你指出。
這裡也是 AI 輔助的界線所在。平臺差異大多屬於確定性規則的領域,因此 AI 與其自由地探索候選,不如用於生成滿足規則手冊的分支方案。輸入對映推薦、Figma 設計稿的平臺變體轉換、多語言×多平臺的文本適配,大致就是 AI 實質上添力的地方,而其輸出始終必須通過 lint。在進步式自動化之前,先要做的是介面卡標準化。
層分離最大的效果是新增平臺的速度。在單一程式碼庫上堆疊 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_touch、contrast_ratio、forbidden_in_core、required_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 項逐一判定為通過/失敗」,就能代替人的核算。關鍵不在工具的規模——而在於把基準寫在腦袋之外,並把生成與驗證分開。
拿到 QA 構建的組員 B 單手握著手機,皺起了眉頭。"技能按了三次,只放出來兩次。"我湊近螢幕一看,拇指按下技能按鈕的那一瞬間,同一根手指正好蓋住了按鈕旁邊約三分之一的區域。用滑鼠測試時從未出現過這個問題——因為滑鼠沒有手指。
這個場景用一句話概括了觸控與滑鼠的本質。兩者都是"指向某一點"的輸入,但一種的指向工具會遮住螢幕,另一種不會。為什麼同一個行為要在兩種輸入上用不同方式來實現,原因就從這裡開始。本章先梳理兩種輸入的差異,隨後完整跟隨一條實操記錄(worked transcript,完整保留的真實操作過程記錄)的主幹——先讓 AI 提出輸入對映方案,再親自驗證衝突與可達性。
手指的粗細、視野遮擋、多點觸控上限、精度全都不同。在用表格把它固化下來之前,先用一個畫面來建立直觀感受。滑鼠游標是一支 1 畫素的筆尖,手指則是一枚直徑將近 1 釐米的印章。筆尖能寫字,但一次只寫一個字。印章蓋得快卻寫不了字,而且蓋下去的那一刻就看不見紙了。
| 屬性 | 觸控 | 滑鼠 |
|---|---|---|
| 精度 | 約 7\~10mm(手指接觸面) | 1px 級 |
| 視野遮擋 | 手指遮住接觸點周圍 | 無 |
| 可否懸停 | 幾乎不可能(接觸 = 輸入) | 自由(移動 ≠ 輸入) |
| 同時輸入 | 2\~10 點多點觸控 | 左·右·中·滾輪 |
| 拖動/點按區分 | 需靠時間·距離推斷 | 點選/拖動明確 |
| 觸覺反饋 | 可以 | 幾乎沒有 |
這裡對設計影響最大的兩行是"視野遮擋"和"懸停"。視野遮擋強制決定了結果顯示在哪裡,而懸停的缺失意味著在移動端,工具提示(tooltip)這一整個資訊通道被徹底抹去。其餘四行更接近由這兩行派生出來的細節。
公開標準把這些差異用數值釘死了——觸控 44pt(HIG)·48dp(Material)·對比度 4.5:1·觸控目標 24 CSS 畫素(WCAG SC2.5.8)這類公開標準遵循 §9.1 規則手冊。這些數字不是憑喜好,而是人體與測量的產物,所以驗證對映時用來衡量的尺子最終也是這套標準。
移動·攻擊·技能這三種行為,用兩種輸入來實現時會如下分岔。一個行為有三種方式,並不意味著沒有正確答案,而是意味著遊戲定位會強制做出選擇。
我所在的專案A(移動優先的 MMORPG)在移動這一行為上:移動端採用 ⓐ+ⓑ 混合方案(搖桿與自動移動並行),PC 端採用 WASD+自動移動。攻擊方面,移動端為 ⓑ+ⓐ(點按敵人後再按按鈕),PC 端可在 ⓐ·ⓑ 之間自由選擇。技能方面,移動端為 ⓐ,或在鎖定目標時用 ⓑ,PC 端在按鍵 1\~8 之上疊加滑鼠瞄準。同一款遊戲、同一個行為,對映表卻出現了兩張——這正是本章的全部要點。
問題在於,對映表越長,衝突就藏得越深。長按技能槽(技能瞄準)與拖動螢幕(鏡頭旋轉)在同一區域重疊時會怎樣?按鍵 1\~8 已經被技能佔用,若有人把背包快捷鍵也提議設為 1,又會如何?靠人眼掃一遍表格是會漏掉的。因此需要這樣一套工作流:讓 AI 提出對映方案,而把驗證交給工具。
從這裡開始就是本章的主幹。讓 Claude 提出 PC 鍵盤/滑鼠對映的初稿,由人來驗證其輸出,執行衝突·可達性 lint 加以駁回,再重新請求——我把這一整個迴圈原樣搬過來。不做概括。原始輸出裡也含有錯誤,而我們正是要看清抓出這些錯誤的過程。
你要設計一款 PC MMORPG 的輸入對映。請針對下面的行為列表,提出一份將其對映到鍵盤/滑鼠的 JSON 方案。
行為:移動(前後左右)、奔跑、跳躍、普通攻擊、技能1\~8、背包、地圖、角色面板、選單、視角鎖定切換、放大/縮小、上下文選單(輔助行為)、自動移動切換。
約束: - 遵循業界慣例(WASD 移動、ESC 選單等)。 - 不要在同一個物理按鍵/按鈕上重疊兩個行為。 - 每一項包含
action、input、note欄位。 輸出僅為 JSON 陣列。
[
{"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_menu 和 auto_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 是空的——請為每個槽逐行填上它屬於哪一類技能。
{"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 集合、衝突的定義)和程式碼來保證。提案由模型出,判定由工具做,決定由人拍板。
把一次物理輸入被轉換為遊戲行為的路徑畫出來,就能看清上面的 lint 卡在哪個環節。
從左上方進入的物理輸入,首先被分類為點按還是拖動(依下一節的 200ms/5px 標準)。分類後的輸入會去查詢對映表,而這張表在進入構建之前必須通過 input_lint.py——這正是圖的核心。一旦存在衝突或缺失,就到不了分發環節,會被阻斷。對映驗證必須在執行時之前、在構建關卡(build gate)處結束。
現在假設對映已經通過,接下來設計這套對映與手指相遇的那個表面。
原則 1 —— 最小觸控面積。 Apple HIG 的 44pt、Material 的 48dp 是下限。在 HD 螢幕上取大約 100px(Retina 2 倍環境為 200px)上下,就能同時滿足兩項標準。低於這個值,開頭那句"按三次出兩次"就會以統計資料的形式顯現出來。
原則 2 —— 拇指可達區域。 MMORPG 移動端以橫屏雙手握持為標準,需要按壓的元素放在下方左右兩角、消耗品/技能槽放在中央下方(三區域模型的依據見 §9.1)。P0 行為(左=移動,右=攻擊·技能)放在左右下方兩角之內,不常看的資訊放在可達極限之外的上方。兩角相加也不到螢幕的一半,這一點是輸入設計的核心。下面的 SVG 展示了橫屏模式下雙手拇指的可達區域與中央下方的槽位欄。
深色扇形是拇指能輕鬆夠到的地方,淺色扇形則是必須伸手才夠得著的極限。若說 14.3.3 重新輸出中模型申報的"8 號鍵小指極限"是 PC 版的情形,那麼移動版對應的失誤,正是把 P0 按鈕放進這塊淺色區域。
原則 3 —— 規避視野遮擋。 手指不只遮住接觸點,其上方還有整隻手覆蓋螢幕。點按右下角的技能時,右下角約四分之一會看不見。因此把行為的結果(傷害數字、狀態變化)顯示在手指夠不到的區域。握著左側搖桿的手會侵佔角色與小地圖的位置,所以把小地圖移到右上角。
原則 4 —— 拖動/點按的區分。 與滑鼠不同,觸控必須靠時間和距離來推斷使用者的意圖。在整個遊戲裡統一為一個標準——例如接觸在 200ms 以內且移動在 5px 以內為點按,超過則為拖動。這兩個數字若忽大忽小,"本想點按結果角色翻滾了"這類意圖失敗就會累積。上面 mermaid 的分岔點正是這一判定。
原則 5 —— 觸覺。 振動是無需看螢幕也能傳達的唯一通道。不過若對每一次輸入都施加振動,就會變成噪音。普通點按不振動,使用技能短促振動,支付確認這類高風險行為強烈振動,擊殺敵人則輕微振動——控制在 4\~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 就變成一片宏的畫面;一味禁止,從移動端過來的使用者門檻就會變高。正確答案是按遊戲調性在這條譜系上選取某一個點,而不是兩個極端。
原則因平臺而異,但使用者對同一個行為所獲得的"感覺",即便換了平臺也應當一致。不能讓從移動端轉到 PC 的使用者重新學習按鈕高亮的含義。
| 情形 | 觸控 | 滑鼠 |
|---|---|---|
| 輸入識別 | 按鈕高亮 + 短促觸覺 | 按鈕高亮 + 點選音 |
| 輸入失敗 | 按鈕抖動 + 觸覺 | 按鈕抖動 + 警告音 |
| 冷卻進行中 | 環形進度條 | 環形進度條 |
| 恢復可用 | 高亮 + 觸覺 | 高亮 + 音效 |
視覺通道(高亮·抖動·進度條)兩端相同,只有輔助通道按平臺在觸覺↔音效之間分岔。這種一致性把多平臺使用者的學習成本減半。
| 模式 | 處方 |
|---|---|
| 按鈕低於標準下限(44pt/48dp) | 強制取 100px 上下 |
| 在手指遮擋區域顯示結果 | 移到非遮擋區域 |
| 濫用觸覺 | 控制在 4\~5 種以內 |
| 右鍵重疊兩個行為 | 用 lint 阻斷衝突後分離 |
| 把僅懸停的資訊原樣搬到移動端 | 移動端用點按/長按替代通道 |
| 鍵位對映寫死 | 允許使用者自定義 |
| 強制兩端相同對映 | 各平臺採用自然的對映 |
這張表的第四行就是 14.3.3 實操記錄的結論。右鍵衝突靠人眼掃表幾乎每次都會漏掉,而把 lint 掛在構建關卡上則幾乎每次都能抓到。
input_lint.py 放進專案。將 REQUIRED 集合替換為你自己遊戲的必需行為。python input_lint.py claude_map.json 執行收到的 JSON。在 ERROR 歸零之前,明確寫出駁回理由(衝突輸入·缺失行為)並重新請求。WARN(空 note)作為未記錄設計意圖的債務另行記錄。如果是獨自開發的小遊戲,就精簡工具。行為在 10 個以內時,只保留 REQUIRED 集合與"同一輸入重複"兩項檢查的 20 行 lint 就足夠了。讓 AI 給出對映,用這個迷你 lint 只篩掉衝突,然後在真機上按一按,看看拇指(或小指)能否夠到。提案由模型出,衝突判定由程式碼做,可達判定靠你自己的手——只要守住這三點,無論規模大小都行得通。
主要讀者:首次負責上線後運營(LiveOps)的策劃(中等規模(10\~50 人)團隊) 面向單人/業餘讀者的精簡版:§15.1.7「一個人的話,只做這些」
前提:筆者親歷過一款全球上線的移動端 MMORPG 的運營,連同 P2E(Play To Earn,邊玩邊賺)經濟一併經歷過,並在此之上疊加了當前專案上線前的 AI 工作流來寫作本章。實操記錄(worked transcript,即完整保留的真實操作過程記錄)是把"輸入→AI 組合→規則手冊驗證→人工選擇"的模式,以運營樣式實際跑一遍的結果。凡屬推測與觀察,都明確標註為推測·觀察;編造的 KPI 表一概不放。
上線次日清晨的辦公室,與上線前不一樣了。里程碑結束了,活兒卻沒有減少,反而只是單位變得更小。原本以季度為單位排布的日程,被切成周·日·小時為單位。而每週都有同一個問題回到會議室:"這個週末上什麼活動?"
如果這個問題每週都從一張白紙重新開始,運營團隊很快就會疲憊。本章要講的,就是把這個問題從白紙中解放出來的方法。核心有兩點。第一,與其每次都從頭絞盡腦汁想活動和賽季,不如把它們沉澱為由已驗證樣式構成的庫。第二,把"組合這些樣式、生成下週 5 個候選"這種枯燥的初稿工作交給 AI,人只決定在通過規則手冊驗證的候選中採納哪一個。從 0 開始做,和從 5 個裡挑,工作負擔是不同的。
把運營的標準週期做成表讓人背誦的書有很多。無非是週一彙報、週二三準備、週五釋出。這些都沒錯,但只背表格,看不出"本週活動"這個每週都要回來的決策究竟是怎麼做出來的。運營的本質不是日程表,而是閉合的迴圈——候選產生、通過驗證、由人選定、進入構建釋出、使用者資料又成為下一批候選的輸入,如此一圈。
在這個迴圈之上,運營的四條軸(內容·活動·數值平衡·CS(客服))各以各自的速度運轉。內容是月\~季度,活動是周\~月,數值平衡是周\~雙週,CS 是日·小時為單位。四條軸各轉各的,那麼看著同一份使用者資料,每週也會得出不同的決策。因此,把四條軸擰成一個迴圈,並把這個迴圈中的一格(活動候選生成)做成 AI 能夠運轉的形態,就是本章的目標。
需要人動手的地方只有兩處。最上面把輸入(KPI·segment·過往效果)乾淨地喂進去的那一處,以及在通過驗證的候選中決定上哪一個的那一處。夾在中間那些枯燥的"絞出 5 個組合"和"篩掉規則違反",由 AI 和規則手冊來跑。而最下面那一行——已釋出的活動所產生的使用者資料又迴流為輸入——正是它讓這個迴圈成其為運營。上線前的設計一旦發出去就結束了,而在運營中,結果會成為下一次的輸入。
進入這個迴圈的兩個庫(賽季規則·活動模板)的具體內容將在 §15.2 展開,最後一格(使用者反饋自動分類)在 §15.3 來看。本章專注於把迴圈完整地走完一圈。
下面把實際怎麼跑的完整地展示一整個迴圈。以下是把筆者在上線前的內容工具中驗證過的"庫組合 → 規則手冊驗證 → 人工選擇"模式,搬到運營樣式(賽季規則 + 活動模板)上實際跑一遍的會話的還原。輸入提示詞可以直接複製使用,輸出則是對那次會話的還原。
先把組合所需的兩樣材料,放成機器能讀的形態。活動模板庫(已驗證的樣式)和賽季規則庫,以及本週的當前狀況(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: 可迴流的休眠使用者佔比上升
臨近的外部日程: 無
用附上的模板·賽季規則·本週狀況 yaml,只組合出下周的 5 個活動候選。
不要創造新機制,只用所附模板的組合;是否違反賽季規則由你自己標註,
併為每個候選補上一行:它為何契合當前訊號(DAU 下降·休眠迴流)。
讓目的不要只偏向營收,要分散開;含糊的就標出來交給我。
只用表格,列為:候選·模板組合·主要目的·時長·規則違反·依據·含糊。
請注意,這五行規則中沒有任何一行在說"要做什麼"。它們全是"在什麼框架內、以什麼為依據來回答"的約束。因為一旦給 AI 自由,它就會吐出一般 RPG 的平均活動。把材料限制在庫以內(規則 1)、賽季規則自查義務(規則 2)、對當前訊號作出應對的依據義務(規則 3)、目的分散(規則 4),以及"含糊就交給人"這個出口(規則 5)——五行合起來,規定的不是候選的樣子,而是候選的資格。
下面是未經摘要的原始輸出。其中混著規則違反與含糊的自報。
| 候選 | 模板組合 | 主要目的 | 時長 | 規則違反 | 依據 | 含糊 |
|-----|------------------------------|----------------|-------|---------|--------------------------------------------|------|
| 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 主動申報自身的侷限、把球傳給人。
不能把這份輸出照單全收。要用規則手冊再過一遍,然後才由人來挑。在這次會話中,實際上有兩處出現了分歧。
首先,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.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 之後作出的決定。
上面這個迴圈,與上線前的設計迴圈有兩處決定性的不同。與其列成表格,不如把這兩點精確點出來。
第一,結果會成為下一次的輸入。上線前,寫好策劃案後一路單向流到構建。而在運營中,本週活動所產生的使用者資料(參與率·流失·營收·反饋)會作為下週候選組合的輸入(current_state.yaml)迴流。§15.1.1 迴圈最下面那根箭頭就是這個迴歸。所以運營的 KPI 不是"一次就猜準",而是"每週對著訊號做調整"。
第二,實驗成本變小了,但不可逆的點更加尖銳。上線前是一次決策左右整個季度,而在線上,先跑一個一週的活動,不合適下週就改。可回滾的實驗多了起來。然而,賽季開始與活動公告是不可逆的。§5.4.5 中講過的"錄音·配音選角 = 不可逆步驟"原則在此照樣起作用。使用者已經看到的賽季規則·獎勵,即便"取消"也會在社群認知裡留下痕跡。因此 §15.1.1 迴圈中的所有驗證(AI 組合·規則手冊·人工選擇),都必須在進入構建·公告這個不可逆格子之前、在可逆階段就結束。把 C4(21 天賽季)從本週當場拍板中抽出、上升到賽季會議,也是這個原則——不可逆的點越是重大的決策,越要經過更長的可逆審議。
這兩點,讓運營成為一件不同於上線前設計的事。其餘的(時間單位從季度→周,反饋從 Beta→即時)都是這兩條軸的派生。
§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.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) |
請按 setup → prompt → verify 的順序,只做一步試試。
event_templates.yaml 的格式手寫下來(只寫目的·時長·獎勵等級)。賽季規則有三條一行式的就夠了——通脹上限、禁止目的衝突、禁止重複。current_state.yaml,跑一次。一個人的話,只做這些:庫 yaml 也好,lint 程式碼也好,都不需要。只要回想你喜歡的遊戲上個季度的 5\~6 個活動,用"目的·時長·獎勵"三欄記下來。僅憑這一點,你就能看出:那款遊戲並不是每週從白紙絞出來的,而是在反覆套用樣式。這張表,就是你的第一個模板庫。
如果是團隊,就從下面這一步開始。把過去 1\~2 個季度的活動收集起來,規整成 event_templates.yaml(只留已驗證的樣式),把三條賽季規則先用 event_lint.py 放進程式碼裡。有了庫和規則,無論是 AI 組合的候選還是人的初稿,都能用同一把尺子來量。
第一讀者:負責運營(LiveOps)的 MMORPG 策劃(中等規模、10\~50 人的團隊) 面向單人/業餘讀者的精簡版:§15.2.9「一個人的話,做到這些就夠了」
回想一款上線運營到第 4 年的遊戲的週一例會。下週的活動要用什麼,每週都從一張白紙開始。有人說"上次的簽到活動獎勵調高一點再來一次?",就有人回"那是兩個月前做過的",而獎勵要調高多少,又是憑感覺定。會議一結束,一名運營策劃就要花上半天,從頭把活動表單填滿。每週,從白紙開始,半天。
問題不在於缺想法。運營團隊腦子裡其實已經有幾套經過驗證的活動骨架 —— 簽到·協作·競爭·迴流。往這些骨架裡換上主題和獎勵,一個為期一週的活動就出來了。只是這個"換裝"每次都靠手工、靠感覺,所以又慢,結果又飄忽。
本章要講的,就是把這個"換裝"交給 AI 的方法。核心有兩點。第一,把已驗證的活動骨架作為可變體的模板 yaml錄入。第二,把從模板中抽出下週多個候選這件枯燥事交給 AI,而人則用程式碼卡住獎勵範圍·重複之後,只評審調性。活動策劃的一般理論(簽到利於拉新、協作利於活躍之類)其他書裡已經講得夠多,本章只專注於把那些知識放進 AI 工作流的那個位置。
作者運營經驗備註(坦白說) 上線後以 1\~2 年為單位親自負責運營的經驗,只佔作者職業生涯的一部分。本章的工作流是作者把正在運營的量產·評審工具(內容·HUD)搬到活動領域的產物,而效果數值是業界觀察 + 作者推測,這一點會在正文中隨處標明。工具結構(模板 yaml·lint·評審關卡)與作者實際運營的內容量產工具是同一套骨架。
活動量產的整體流程分四步。核心在於:第 1 步(模板)和第 3 步(lint)是確定性的,只有第 2 步是 AI。這與內容量產(§6.2)·HUD 壓縮(§14.1)裡看到的是同一種分工。規則手冊從輸入和驗證兩頭把關,那麼夾在中間的 AI 每次給出略有不同的變體,獎勵平衡和日程也不會亂。
在這張圖裡,人的手只碰兩個地方。最上面,把模板和本季度的約束乾淨地放進去的位置;最下面,判斷 lint 抓不到的"這個主題合不合當下我們遊戲的氛圍"的位置。而這中間枯燥的候選量產和獎勵算術,由模板、AI 和 lint 來跑。
決定性的設計在於:即便 lint(第 3 步)發現了違規,也不會自動丟棄候選,而只是把 WARN 上報給運營關卡(第 4 步)。理由見 §15.2.5。還有,最後那條箭頭(公告)是不可逆的,這一點把運營和其他量產區分開來。城市 NPC 不滿意的話,構建前廢棄即可;但已向用戶公告的活動,想收回時要付出社群信任的代價(§15.2.7)。
把運營團隊手裡已驗證的骨架固化為表單。若放任為自由格式的策劃案,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(迴流)以相同格式放著。這四張,就是本季度候選生成的全部輸入池。
把實際怎麼跑的一個完整週期從頭到尾展示一遍。輸入提示詞可以直接複製使用,輸出則還原了真實的量產會話。
用附上的 coop_raid.yaml(經 4 次驗證的協作團本骨架),只給出下週(W2)的 5 個變體候選。
本季度主題是"夏日 —— 水·祭典·炎熱"。
絕對不要動 core_loop,只改 slots(主題·Boss·獎勵),
獎勵只能在 reward_policy 上限之內,每個候選都附一行說明為何是這個主題·這個獎勵。
最近 3 周是 簽到·PvP排位·協作團本,所以對又跑協作團本的候選打 [重複注意],
拿不準就打 [存疑] 交給我。只用表格,列為 候選名·主題·獎勵tier摘要·依據·標記。
六行規則裡,指定主題或獎勵的那種行,一行也沒有。全都是"只能改到哪裡、不許越過什麼、拿不準就上報"的圍欄。因為像"幫我做個夏日活動"這樣放開,出來的就是普通手遊 RPG 的平均水平。變體範圍(只 slots)、獎勵上限(reward_policy)、重複注意(最近 3 周的歷史),以及"拿不準就交給人" —— 這道圍欄讓骨架被守住、讓變體被放開。
| 候選名 | 主題 | 獎勵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 來卡。
候選是否守住了獎勵策略和日程重疊,每次都用眼睛看,又會漏。凡是靠 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,就沒什麼可爭的。要麼下調獎勵,要麼換候選。
若只抽象地寫"運營團隊來評審",就看不出這道關卡實際篩掉了什麼。這裡把通過 lint 之後人殺掉什麼、留下什麼,完整地跟到底一次。
[第 4 步 運營評審 —— 判定]
運營策劃這樣處理了 5 個候選。
- 酷暑Boss戰 → 駁回。 lint [C]·AI [重複注意] 同時指向它。3 周內又跑一次相同的協作團本骨架,"又是累計貢獻啊"的疲勞就來了。備註順延到下季度槽位。
- 洋流競速 → 駁回。 lint [B] 超出通脹限額。把獎勵降到 25 就能通過,但 AI [存疑] 點出的目的衝突(活躍+競爭)才是更根本的問題。在協作活動裡摻進個人排名,輕度玩家會覺得"到頭來還是老玩家的狂歡"。不靠只砍獎勵來救,而是整個擱置。
- 攔洪之堤 → 採納候選第 1 位。 只是,lint 雖把
季度累計 90 觸及邊界判為 PASS,但邊界這一點讓人不放心。用了這個活動,本季度強化石餘量就歸零。6 月最後一週賽季收尾衝刺就沒有獎勵餘力了。- 放河燈 / 錦鯉收集 → 留下。 兩者獎勵都輕(20·22),給季度留下餘量。
這裡,人把通過了 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] 違規自動丟棄,運營團隊就失去了學習 洋流競速 真正問題(目的衝突)的機會,也失去了動搖 攔洪之堤 這種合法但按季度節奏有風險的候選的位置。可疑的候選由機器來挑,而採納和駁回由人來定。
如果說活動是周\~月的節奏,那賽季就是季度的節奏。運營方式相同。賽季也一樣,把已驗證的要素分離成槽位,每季度就只換主題。
| 賽季槽位 | 變體(AI·人) | 固定(策略·lint) |
|---|---|---|
| 賽季主題 | 夏季·冬季·新年 (自由) | — |
| 賽季通行證獎勵軌道 | 各階段獎勵項 | 階段數·完成難度·獎勵上限 |
| 賽季 PvP 排行 | 排行獎勵項 | 獎勵通脹限額 |
| Meta 洗牌 | 新角色·數值平衡 | 變更幅度護欄(§8.1) |
賽季通行證裡,人以策略固定的核心數值是完成率目標。把難度定到讓大約 70% 的活躍使用者能到達最終階段,這是業界常被引用的基準(作者推測 —— 各遊戲不同,所以應當讀作方向而非絕對值:低於 30% 會挫敗,超過 90% 則缺乏挑戰感)。這個目標一旦錄入槽位,AI 提議賽季通行證變體時,也能被強制一併算出"預期完成率"。
季度日曆要一眼看清,活動和賽季才不會衝突。這更接近運營團隊公用的檯曆。誰看都看到同一張圖,衝突才會減少。
這一張圖,把 §15.2.5 的判斷用視覺解釋了。顏色就是骨架種類。同一種顏色在 2\~3 周內出現兩次,§15.2.4 的 lint [C] 就會響。 而下方的通脹量表已逼近紅線(上限 90),6 月賽季收尾(W6)能用的餘量只勉強剩 7 —— 正是把 攔洪之堤 獎勵降到 18 才保住的那個 7。
城市 NPC(§6.2)或 HUD(§14.1)與運營有一點決定性的不同。公告無法收回。 NPC 調性不合,構建前廢棄即可,使用者根本不知道那個 NPC 存在過。然而已向用戶公告的活動,其獎勵·週期·規則都留在了社群裡。開始之後再說"活動獎勵太重了,要收回",就伴隨著不可逆的代價。
本書全書的原則(§5.4.5 配音錄製、§8.1 線上構建、第 12 部分最終渲染都是同一條訊息),在運營中同樣成立。所有評審 —— 獎勵範圍、通脹限額、日程衝突、調性 —— 都必須在公告前的可逆階段結束。§15.2.3\~5 的量產·lint·評審·重新請求整個週期,之所以都在這道不可逆關卡的左側轉,原因就在這裡。越過關卡之後能做的,頂多是 §15.2.8 的後期微調,而那也在一點點消耗使用者信任。
公告之後 KPI 照看。只是,與公告前的評審不同,這裡能做的只有後期微調。把自動測量的訊號和人的處方分開。
| 訊號 (自動測量) | 處方 (人來決定) |
|---|---|
| 參與率低於 50% | 後期小幅增強獎勵,或延長週期 +2 天 (在公告可信範圍內) |
| 參與率 95% 以上 | 太簡單 —— 記下一週期的難度備忘,當前活動保持不變 |
| 強化石價格事後 30 天跌幅超過 -10% | 強化 sink(限定商店),下季度下調通脹限額 |
| 活動後工作日遊戲時長下降 | 活動依賴訊號 —— 增強工作日內容吸引力,調節活動頻率 |
最後一行(工作日遊戲時長下降)是最常被漏掉的訊號。只看活動期間的 DAU(Daily Active Users,日活躍使用者),活動總像是成功。然而活動結束後,若使用者在工作日不回來,就意味著活動在吸食平日遊戲本身的吸引力。所以在 §15.2.2 模板的 post_event_kpi 裡,從一開始就把"活動後工作日遊戲時長"作為槽位錄入。不測量,就無法處方。
活動這一章,很容易起想放一張"跑了協作活動,留存率就從 30% 漲到 50%"這樣的表的念頭。這類數字如果未經驗證,會削掉本書的信任。本章能說的只有三點。
第一,方向可以用業界觀察來說。 簽到獎勵強化型活動會拉高短期活躍使用者數,協作活動會提升社群凝聚,限定禮包會拉高活動期間的營收 —— 這是觀察線上運營遊戲而來的業界通識。只是多少會因遊戲·使用者構成而偏差很大,把別家公司的數字照搬過來很危險。
第二,作者的推測就寫成推測。 "賽季通行證完成率目標 70%""活動週期超過 10 天會累積疲勞""活動量產半天→一小時"都是作者基於經驗的推測,是未經驗證的假設。別去背絕對值,讀作結構(模板+lint 取代白紙策劃)就行。
第三,只把可測量的東西作為 KPI 來承諾。 留存率這類結果指標不會由單個活動左右,所以不去斷定因果。相對地,這套工作流真正能讓其變得可測量的,是這些東西 —— lint WARN 件數(直到獎勵違規變為 0)、季度通脹累計(相對上限)、相同骨架重複間隔(周)、各活動的參與率與事後強化石價格變動。這四項,在會議上就能用數字而非"感覺"來說。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| 每週從白紙開始策劃活動 | 慢且結果不穩定 | 把已驗證的骨架錄入模板 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的候選,就沒了被動搖的位置。程式碼看單個活動的合法性,人看整個季度的節奏。
一個人的話,做到這些就夠了:不用 lint 程式碼也行。從你自己的遊戲(或你喜歡的線上運營遊戲)裡,挑一個常見的活動骨架,按 §15.2.2 的格式手寫一份模板 yaml(
core_loop·slots·reward_policy這三欄是核心)。然後貼上 §15.2.3 的提示詞抽出 5 個變體候選,再從中挑一個你覺得"獎勵太重"的,反駁它"這個超出了本月的獎勵餘力,調低了重來"。採納和駁回究竟是怎樣一組判斷,會親身體會到。
如果是團隊,就從下面這一步開始。把常跑的 3\~4 個活動骨架錄入為模板 yaml,先把 event_lint.py 的三行(獎勵上限·季度通脹累計·重複間隔)做成程式碼。哪怕只有模板和這三行,也能先擋住"每週白紙策劃"和"獎勵憑感覺定"這兩種常見失敗。這套工作流,是 §15.1.5 的漸進式應用骨架三要素 —— 活動模板·賽季規則庫、AI 活動候選生成器、事後自動測量 —— 的第一個實務實現。
主要讀者:負責運營(LiveOps)中使用者應對的策劃、總監(中等規模(10\~50人)團隊) 面向個人/業餘讀者的精簡版:§15.3.7「一個人的話,只做這些」
先坦白一點。筆者在產品上線後以1\~2年為週期親自負責運營的經驗並不算長。本章的相當一部分,是建立在24年從業經歷之上的業界觀察與相鄰經驗。所以本章不會斷言"運營就該這麼做"。相反,筆者把上線前量產內容時驗證過的輸入 → AI → 驗證 → 人工決策這一迴圈,原樣套到使用者反饋這個輸入上,完整地跑一遍,看會得出什麼。工具的骨架與§6.2的city_hunting_generator相同,只是輸入從"城市後設資料"換成了"100條使用者反饋"。
運營第一週的景象大都相似。論壇、Discord、客服工單、商店評論每天堆積成百上千條。人不可能全部讀完,而不讀的話,同一個bug會有50條報告被埋掉。本章講的方法是:先讓LLM把這堆反饋按主題歸類、並按情感打分,然後人只投入到"那麼這周該修什麼"這一優先順序決策上。
把反饋分成4個渠道(遊戲內問卷、論壇/Discord、商店評論、客服工單)、再歸為4種類型(bug、請求、不滿、表揚)的表格,哪本運營教科書裡都有。這些說的都對。問題在於,就算背下這張表,也答不出"今天湧進來的412條該怎麼處理"。只要還把反饋看成供人閱讀和分類的物件,反饋量就永遠會壓過運營團隊的人手。
換個視角。一條反饋就是一份結構化輸入,是一條擁有 {來源, 原文, 主題, 情感, 嚴重度} 五個槽位的記錄。這樣一看,工作的本質就變了:不是"全部讀完",而是"按主題歸類、排出優先順序"。而且,主題聚類和情感打分,人來做既枯燥、每次的標準還會飄移,但機器會用同一把尺子同樣地量這100條。這正是LLM比人更擅長的那類工作。§6.2中量產30座城市時的那套分工(規則手冊=確定性,正文=AI,稽核=人),在這裡同樣成立。唯一的不同是:最後人做的事不再是"正文稽核",而是"優先順序決策"。
這裡點明反饋型別的一個分佈特徵。會主動發言的使用者,往往不是滿意的使用者,而是心懷不滿的使用者。滿意的客人會悄悄離開,不滿的客人才會重新走回櫃檯。所以論壇、評論裡的情感分佈,往往會比實際全體使用者的滿意度更偏向負面(筆者的觀察——具體偏差幅度因遊戲、渠道、時期而異,所以不該當成絕對數字,而應作為方向來讀)。只有把這種偏差放在心裡,當你在聚類結果裡看到"負面60%"時,才不會誤讀成遊戲正在走向失敗。
下面實際把一個迴圈完整跑一遍。輸入是某一週從4個渠道彙集的100條反饋,輸出是主題聚類、情感、優先順序。輸入提示詞可以原樣複製使用,下面的輸出是按真實分類會話的格式重現的。
把從各渠道抓取來的原文,規範化成一行一條記錄。這不是重新寫,只需要抽取、整理即可。
{"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的工作。
把附件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層的分類體系——標籤固定下來,才能每個季度都用同一根軸來比較走勢。
[表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能夠說出"這個我沒把握"。
這份輸出不能照單全收。有一處確實卡住了。
強化/機率 主題的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初稿 + 人工稽核 + 一次往返則在一小時以內(筆者估計,未經驗證的假設——具體能省多少,會隨反饋條數、渠道數量而變,所以與其記絕對時間,不如把它當作"從頭手工"和"初稿+稽核"之間的結構差異來讀)。
這裡劃出一條決定性的界線。上面的表B只說到"哪個主題有多少條、有多負面"為止。"那麼這周該先修什麼",LLM是給不出的。 那是牽涉成本、日程、遊戲願景的決策,而這個決策的責任在總監身上。
面對同一張表,兩個運營團隊可能做出截然相反的決定。只看條數,伺服器/效能(24條)和強化/機率(21條)排在第1、2位。可優先順序並不按條數順序走。原因在於嚴重度與可逆性。
在這個流程裡,人手真正觸及的只有兩處:中間的稽核關卡(判定★、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負責分類、生成候選,人則專注於採納與願景決策"在反饋領域的真實樣貌。
優先順序定下來後,就要回複用戶。在運營中,回覆缺位是對信任傷害最大的地方。哪怕沒有可答的內容,一句"正在處理"也勝過毫無回應。回覆草案同樣可以讓LLM按主題生成。
[回覆草案——LLM輸出,按主題]
- 支付/退款(S1):"鑽石未發放的問題已確認。將按訂單號在24小時內補發,並逐一單獨回覆。"
- 伺服器/效能:"公會戰時出現的閃退正在復現確認中。計劃在下週維護中優先處理,並會通過公告同步進展。"
- 強化/機率:"強化機率完全按規格標註生效,這一點我們已通過資料確認。不過對於體感難度方面的意見,我們正在另行研究。"
- 新內容請求(弓箭手):"新職業已在路線圖上,一旦日程確定,會第一時間釋出公告。"
這裡與§6.2有一處決定性的不同。回覆的傳送是不可逆階段。 城市NPC廢棄後重做即可,但使用者一旦看過的公告、回覆文本,是收不回來的。若自動發出"24小時內發放",實際卻花了三天,那個承諾就會以不可逆的痕跡留在社群裡。所以§15.1.4的不可逆階段原則,在反饋領域比在其他領域運作得更重。自動回覆草案交給LLM生成,但在通過客服稽核關卡之前,一個字也不自動傳送。 稽核者只看:日程承諾(24h、下週)是否與實際工作日程相符,以及敏感個案(法律糾紛、退款糾紛)有沒有混進自動傳送池。這是讓人來承擔lint抓不到的判斷的位置。
| 階段 | 可逆性 | 由誰 |
|---|---|---|
| 反饋聚類、情感打分 | 可逆(可自由重跑) | LLM |
| 主題稽核、優先順序決策 | 可逆(確定前) | 人(總監) |
| 回覆草案生成 | 可逆(可廢棄、重寫) | LLM |
| 回覆傳送、公告發布 | 不可逆(使用者已感知) | 人(客服稽核後) |
要讓同樣的反饋不至於每個季度被搖擺成不同的決定,就得把聚類結果固化為季度覆盤的固定輸入槽位。不是憑一時印象說"最近強化的不滿好像挺多",而是每個季度用同一根標籤軸彙總出的表,進入覆盤表格之中。§15.3.2裡禁止自由生成標籤、用允許列表把它固定下來的理由,在這裡得到回收。
2026 Q2 使用者voice(LLM自動彙總,季度累計)
4類渠道累計約5,000條聚類(條數為季度實際統計——並非加工) 負面靠前主題: 強化/機率 > 伺服器/效能 > 平衡 > 支付/退款 請求靠前主題: 新職業 > 新狩獵場 > 公會系統 > UI改進 季度情感走勢: Q1負面68% → Q2負面71%(小幅惡化——強化主題拉動)
這張表成為季度決策的輸入。決策本身歸總監,輸入歸使用者。季度走勢("Q1 68% → Q2 71%")只作為方向來讀。訊號不是單個季度的絕對值,而是同一根標籤軸上的變化方向。如果負面%升高了,就回溯"是哪個主題把它拉上去的",並連線到下一季度的優先順序。這份季度報告的草案本身,也由LLM用自然語言生成,人只在上面加決策批註——這正是§15.1.5裡所說季度報告自動初稿的真實位置。
運營這一章,很容易忍不住想放上"引入反饋迴圈後,NPS從20升到了45"這樣的表格。筆者從未測量過那種因果,所以不寫。本書的原則是以下三條之一。
第一,實測統計的條數照原樣使用。 §15.3.2裡按主題的條數(伺服器24、強化21、支付8)和§15.3.5的季度累計,都是把分類結果一條條數出來的值,而不是為了好看而湊出的比例。
第二,估計就寫明是估計。 "100條分類從大半天→一小時"(§15.3.2)、"論壇情感偏向負面"(§15.3.1)是基於筆者經驗、觀察的估計,是未經驗證的假設。不必去記絕對值,只要作為方向(反饋量永遠壓過人手;主動發的帖子往往偏向不滿)來讀就好。
第三,只把可測量的東西作為指標來承諾。 反饋迴圈真正可測量的,不是結果滿意度(NPS),而是過程指標——未分類反饋的存量(目標0)、S1事故發現→熱修復的前置時間(lead time)、回覆響應時間、"其他"主題的佔比(允許標籤裝不下現即時,"其他"就會膨脹)。這四項,在會議上都能用數字而非"感覺"來說。
一個人的話,只做這些:不需要客服系統,也不需要資料集。把你自己遊戲(或你喜歡的遊戲)的商店評論、社群帖子,手動複製20\~30條做成jsonl(
{"id":..., "src":..., "text":...}),把§15.3.2的提示詞原樣貼上去跑一遍。在得到的表B裡,找出"條數第1的主題"和"你最想先修的主題"不一致的那一處,用一行字寫下為什麼不同——你就會切身體會到,優先順序為什麼不是LLM的活,而是人的活。
如果是團隊,就從下面這一步開始。先把"將4渠道反饋彙集成一行一條記錄jsonl的抽取指令碼",以及§15.3.2的允許主題標籤列表固定下來。標籤固定了,無論是LLM分類還是人工分類,才能用同一根軸來衡量、比較季度走勢。自動回覆是再往後的事——回覆不可逆,沒有客服稽核關卡,就絕不接入自動傳送。
週四下午4點。戰鬥 TF 會議結束,7個人各自回到工位。白板上留著關於是否要把全域性冷卻(GCD)從0.8秒下調到0.5秒的痕跡。資深數值策劃說"在我的模擬裡0.5才對",程式碼主程說"0.5的話伺服器 tick 跟不上"。UI 設計師說"兩邊我都說不好,只知道冷卻條的寬度會變得太窄"。
三個人說的都對。而一旦三個人都開始把各自的結論寫進自己領域的文件,下週這三份文件就會相互衝突。數值表裡寫著0.5,程式碼規格里寫著0.8,UI 指南里寫著0.6。誰看都分不清哪一份才是正本。
戰鬥 TF 存在的理由,正是在一個地方吸收這種衝突。而這種吸收的產物——唯一的一個決策——才應當進入正本文件。其餘討論的殘渣都應當止步於隔離的工作空間之內。本章討論的就是這種隔離與吸收的機制。
戰鬥系統的大改不會只涉及一個工種。只要動一個全域性冷卻,平衡(數值)、程式碼(伺服器 tick)、UI(計量條呈現)、動畫(動作時長)、音效(打擊感)就會同時受到牽動。如果把這類議題按領域各自推進,決策會拖上2\~4周,而且即便做出決策,各領域之間也會彼此錯位。
TF(TaskForce,專項組)就是為了防止這種錯位,把多個工種暫時聚到一個工作空間裡的單位。關鍵在於"暫時"和"隔離"。如果把 TF 的討論原樣流入公司的正本文件體系,未經驗證的討論、被否決的方案、正在試驗中的數值就會汙染正本。因此我們在 SVN 裡建立一個以 95_ 編號開頭的隔離工作空間。
95_BattleTF。95 號段的編號是一個約定,表示短期 TF 工作空間。一般的正本 docs 使用 10 號段、20 號段的編號,而 90 號段是"臨時·隔離·計劃終止"的訊號。只看資料夾編號,就能立刻傳達出"這裡不是正本,不要引用在這裡看到的數值"。
隔離的規則很簡單。
95_BattleTF 之內。TF_결정사항_요약.md 唯一一份晉級到正本 docs。95_BattleTF/archive/,僅作留存。TF 固化為常設部門之所以危險,原因就在這裡。一旦隔離被打破,TF 工作空間裡未經驗證的數值就會開始被當作正本引用,同樣的決策每個季度都會在別的場合被重新推翻。
戰鬥 TF 的一個週期,結構上就是:開啟一個隔離空間,在其中積累討論、試驗與決策,終止時只把決策吸收進正本。
議題從左上角進來,在黃色的隔離空間裡處理掉所有雜音,只有綠色的那一格——決策摘要——離開進入正本。紅色代表降級。這一張圖就是 95 號段工作空間運營的全部。
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 倍。數值提案,無依據。暫緩。
# 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 那部分,抽取與整理的勞動就消失了;但正本里放什麼,這個決定仍留在人的手裡。
進入 TF 的議題並非都來自內部。發行商、美術外包、商務團隊會提出"幫忙做一下這個戰鬥相關的事"的請求。如果不加甄別地都當作 TF 議題接下來,TF 就會淪為對外的接訴視窗。
因此,外部請求一收到就分成三條支線。只有需要戰鬥決策的才投入 95_BattleTF,一個領域就能了結的由負責人單獨處理,超出範圍、依據不足的則寫明理由後回覆、暫緩。進入 TF 的只有第一條支線——這是防止 TF 變質為接訴視窗的第一道防線。分類本身是人的判斷,但讀一讀收到的請求文本、先給"這件事牽涉幾個領域"打上初步標籤這種程度,可以先讓 AI 過一遍。
這套三角分類(request-triangulate)的判定順序、實操記錄、各軌道的後續處理,由下一章 16.2 專門講。這裡只點明"TF 只接第一條支線"這條入口規則。
TF 決策晉級為正本後,就把它共享給相關團隊。這裡存在一種不對稱。對美術團隊不給 Markdown 原件(.md),只交付渲染後的 html。
理由很簡單。美術團隊只需要知道決策的結果。"冷卻條請按 0.6 秒為基準重新確定寬度"——這一句就是他們需要的全部。md 原件裡含有決策 ID 體系、atom 引用、被否決的 0.5 方案的痕跡、依據資料的檔名。這些是策劃與程式碼共享的工作語言,並不是美術需要去學習的東西。
若原樣把 md 給過去,美術團隊要付出兩種成本。其一,為解讀與自己無關的標註體系而耗費時間。其二,可能把未經驗證、已被否決的資訊誤當成決策。html 擋住了這兩點——只呈現乾淨渲染後的決策結果,內部標註在構建過程中被過濾掉。
寫成原則就是:工作語言(md)只在使用它的工種內部流轉,對外只輸出成果物(html)。 這與 TF 工作空間隔離(95 號段)是同一套哲學。內部使用的原始素材留在內部,對外只送出被吸收後的結果。
隔離·吸收機制要運轉起來,底下必須鋪著五條運營原則。缺了任何一條,TF 都會垮成一個清談場。
[依據未記錄],也是這條原則的延伸。五條原則捆在一起運作時,隔離的 95 號段空間就不再是清談場,而成為決策工廠。
整理 TF 運營中期以後反覆出現的陷阱與處方。
| 陷阱 | 症狀 | 處方 |
|---|---|---|
| 變質為清談場 | 只交換意見,不出決策 | 每次會議強制留 N 個決策槽位 |
| 越權 | TF 干預其他領域的決策 | 明確決策權表 |
| 成員負擔過重 | 重複參加 5\~6 個 TF,侵蝕本職工作 | TF 參與合計每週上限 8 小時 |
| 永久化 | 不解散,反覆開同樣的會 | 季度再評估 |
| 隔離洩漏 | 95 號段未經驗證的數值被當作正本引用 | 晉級正本只限決策摘要一份 |
| 對外脫節 | 決策不向外部共享 | 晉級正本 + 交付 html |
隔離洩漏最安靜,也最危險。資料夾編號的約定一旦崩塌,一切都會崩塌。
僅從作者專案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/。
對獨自工作的單人開發者,隔離·吸收同樣有效。把"TF"換成"我腦子裡的多個角色"就行了。
95_temp_결정/ 之類的臨時資料夾,把模擬、筆記、被否方案全都倒進去。결정요약.md 一份移到主工作資料夾,臨時資料夾整個下沉到 archive/。有了隔離空間,僅憑資料夾位置就能區分"這個數值是已確定還是還在試驗中"。即使是一個人,這也是不把同樣的混亂留給未來的自己的最便宜的方法。
週二上午,即時通訊工具幾乎同時響了三次。
美術主管:"戰鬥特效的配色,現在的色調太暗沉了,可以做得更明快一些嗎?"
QA主管:"存在公會簽到獎勵發放兩次的情況。附上覆現影片。"
發行商負責人:"請在東南亞版本中落實伊斯蘭文化圈的指南。要在下個季度稽核之前完成。"
三條訊息的字數相近。然而,一條是30分鐘就能搞定的事,一條是必須立刻拉住程式碼主管處理的事故,一條是要塞進季度計劃裡的外部排期。僅僅因為它們落進了同一個收件箱就以同等分量對待的話,就會在30分鐘的小事上耗掉半天,而真正的事故卻被擱置到傍晚。
湧向策劃的請求,性質各不相同,差異之大不亞於工種的多樣。問題在於,它們全都以"一行訊息"這種相同的形態到達。本章討論的,就是在收到這些一行訊息的瞬間將其分入三條軌道的工作。軌道一旦分清,該立刻停下手頭做什麼、又該把什麼推到之後,就隨之確定了。
策劃既不直接寫程式碼,也不直接做美術和音效。他只是撰寫規格說明、傳達意圖、驗證結果。所有產出都要經由其他工種之手才能問世。因此,協作的質量直接決定策劃產出的質量。
在筆者擔任總監的專案A(移動優先的MMORPG,中等規模(10\~50人)團隊)中,策劃日常協作的工種鋪開來看是這樣的。
策劃要與七個工種從每天到每季度不等地相互咬合。策劃在工位上花費的時間裡,有40\~60%都投入到這類協作中。花在本職(設計)上的時間,不過是剩下的那一半而已。既然如此,壓縮協作時間,就等於延長本職工作的時間。而吞噬協作時間的最大原因,在於無法對收到的請求分類,把精力耗在了不該耗的地方。
回到前面的三條訊息。從表面上看,它們都是"請幫我……"。但在這背後,隱藏著三種不同的性質。
筆者用一個詞分別稱呼這三種性質:對齊 (align)、缺陷 (defect)、排期 (schedule)。把收到的請求先塞進這三者之一 —— 在專案A中,這項工作被固化為一個名為 request-triangulate 的工作流。之所以取名三角測量 (triangulate),是取其用三個基準點(工種性質·緊急度·外部依賴)將一個點(請求)包圍起來、從而確定其位置之意。
分類流程如下。
提問的順序是關鍵。之所以最先追問排期依賴,是因為對於有外部截止期的事,前置時間優先於內部判斷。如果把距季度稽核還剩3周的事歸類為"以後對齊一下就行",那麼等對齊完成時,截止期已近在眼前。把缺陷放在第二位的理由是,已經在影響使用者的事,永遠優先於品味層面的討論。對齊排在最後。只有既不緊急、又不受外部約束、也不傷害使用者的事,"慢慢對齊"才成立。
如果三個問題的答案都是"否",那不是分類失敗,而是資訊不足。這種時候不要硬給它定軌道,而是放進待定區並反問回去。"這個是必須進下一個版本,還是先評估一下就好?"這樣一句話,通常就能定下軌道。
如果這套分類只在腦子裡做,忙碌的日子就會崩掉。因此,筆者的做法是把收到的一批請求整個丟給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. 運營團隊:多名使用者反饋"公會聊天字型太小"。
| # | 軌道 | 依據 | 下一步行動 |
|---|------|------|-----------|
| 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,看似缺陷卻可能是品味的字型)。把五格無遺漏地填滿,和察覺其中兩格填錯了,是兩種不同的能力;這份實操記錄正是把這兩件事分別交給各自擅長的一方來做。
分類結束後,每條軌道都進入完全不同的後續工作。雖然從同一張表出發,終點卻各不相同。
被歸入 Track-A(對齊) 的請求,按"只傳達到意圖為止,表現交由委託"的原則處理。對於美術的配色請求,筆者給出的回答不是飽和度數值,而是意圖。"這場戰鬥是Boss第1階段,緊張感是核心。希望壓迫感優先於明快感。在此前提下,飽和度交由美術判斷。"策劃一旦直接指定飽和度數值,美術的自主性就被削減,產出的責任歸屬也隨之模糊。守住意圖與表現之間的界線,就是對齊軌道的全部。
被歸入 Track-D(缺陷) 的請求,接下來是優先順序判定與對接程式碼。公會獎勵重複(P0)當場移交給了程式碼主管,BGM同步(P2)則登記進待辦列表 (backlog),同時附上推測原因的問題。在缺陷軌道中,策劃的工作不是"修復",而是排定優先順序並給出準確的輸入。區分P0與P2的標準是"當下是否影響使用者資源·遊戲進度"。獎勵重複直接關係到資源,故為P0;BGM延遲0.5秒雖令人不適,卻不妨礙進度,故為P2。
被歸入 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>
分類準確時,同一收件箱裡的五行就會乾淨利落地分散到三條不同的處理線上。分類出錯時,缺陷會被拖進對齊會議白白耗掉時間,或者排期事項起步太晚,在截止期前夕爆發。
有時請求不是一兩條,而是成團湧來。比如發行商稽核前的那幾周,或戰鬥系統全面改版這樣的局面。這種時候,把工作本身隔離到 95_BattleTF 這樣的臨時工作空間裡,結束後只把決定提升為正本。那套隔離·吸收機制,以及"只向美術團隊交付html(md學習為0)"的做法,已在前一章16.1中全部講過。
從分類(3-track)的角度再補一句,是這樣的。膨脹成一整團的協作,大多是Track-S(排期)事項跨越多個工種被拆解的局面;當逐條按軌道處理已應付不來時,就把它挪進"隔離工作空間"這個高一層的容器裡盛放。也就是說,如果3-track分類是入口,那麼TF隔離就是容納通過入口的那一大團的房間。
| 失敗模式 | 處方 |
|---|---|
| 把所有請求以同等分量處理 | 一收到就做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的單人精簡版做即可。
95_BattleTF 會議室。把公會簽到獎勵確定為資源 +5 的那天下午,我把同一個決定分發到了三個地方。策劃組頻道里放的是一份規格說明 markdown,程式組那裡是一行資料欄位,美術組那裡是一張單屏 html。三邊幾乎同時給了回應。主程問"觸發時機在哪裡",美術總監問"簽到按鈕的位置和 06_UI 指南對得上嗎",動畫師則什麼都沒說。明明是同一個決定,三個人看到的卻完全不同。
本章正是把這種"看法各異"從事故變成設計的記錄。把同一個決定按職能包裝成不同形態 —— 這就是 framing。
公會簽到獎勵這一個決定,牽涉的受眾有五類。他們讀同一句話,也只挑自己領域的部分讀,其餘的都略過。被略過的地方就會出事故。
| 受眾 | 會專注去讀的 | 本能會跳過的 |
|---|---|---|
| 主程 | 資料欄位·介面·觸發時機 | 色調·敘事·演出 |
| 美術總監 | 介面佈局·元件·風格指南 | 資料完整性·觸發 |
| 音效總監 | 行為觸發·氛圍·時長 | 資料細節 |
| 動畫師 | 動作·時序·狀態轉換 | 視覺基調·數值 |
| QA | 驗收標準·風險·邊緣場景 | 實現方式的內部 |
問題不在資訊的量,而在呈現的方式。把一份厚厚的規格說明一模一樣地擺到五個人的桌上,五個人各自翻開不同的頁、合上不同的頁。framing 不把這種"翻開"交給偶然,而是有意去安排。
下面這張 framing 矩陣,展示同一個決定在跨越職能邊界時會換上怎樣的形態。
包裝因受眾而異,但墊在底層的不變事實(數值·時點·範圍)在任何一種包裝裡都不能動搖。framing 的技藝不在於"呈現得不一樣",而在於"呈現得不一樣的同時,保留同樣的核心"。
如果每次都靠手工重新塑造 framing,負擔太大,最後就乾脆不做了。所以我採用這樣一種方式:丟擲一個決定,一次性拿到各職能的包裝初稿。下面是圍繞公會簽到獎勵這個決定實際往來的完整記錄。未加修飾,原樣搬來。
[決定事實 —— 在 95_BattleTF 上確定]
- 公會簽到獎勵 = 公會資源 +5
- 觸發:公會成員當天首次登入時 1 次
- 範圍:該公會的全體公會成員各自
- UI:公會介面頂部顯示資源總量 + "簽到"按鈕
把這一個決定按三個職能分別包裝一下。
策劃用 markdown 寫意圖·規則·資料影響;美術把要放進 html 的介面規格
以佈局·元件·風格指南引用為中心(禁止提及資料·觸發);
程式用一行資料·介面·觸發·校驗項。
三種包裝都要把不變值(+5、首次登入 1 次、全體公會成員)一模一樣地釘進去,
不確定的值不要編造,用 [待確認] 標出。
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 次的呼叫)
- 併發:同一公會成員在多裝置同時登入時的重複累加 [待確認]
原始輸出沒有直接採用。我抓出了三處。
[待確認] 兩處(資源上限、多裝置重複累加)是決定事實裡原本沒有的缺口。AI 沒有編造而是標了出來,這個判斷是對的;但這在 framing 之前先是決定本身的漏洞,應當退回 95_BattleTF。修改一下美術包裝:
- 把"遵循 06_UI 指南"這類引用外部文件的措辭全部刪除。
- 把按鈕按下/等待/完成 3 種狀態的視覺差異直接寫進介面規格里。
- 以美術組只看這一張就開工為前提,做到自足,不跳轉到其他文件。
程式包裝裡的 [待確認] 2 項從產出物中拿掉,
改為在最上面單獨放一個"需 95_BattleTF 重新確認的項"區塊。
經過這一次的否決·再次請求,產出物變成了三個職能各自在自己崗位上拿來就能用的形態。AI 塑造出了三份包裝初稿,還在缺口處標了記號;但在哪一種包裝裡刪掉什麼 —— 從美術包裝裡去掉外部引用、從程式包裝裡剔除未定項 —— 這一刀終究還是落在我手裡。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) 則在規格說明積累足夠、連結圖能產生探索價值時才開啟。
如果每份規格說明都重新定義受眾,framing 就每次都要重新塑造。所以要把代號固定下來。
| 受眾代號 | 領域 |
|---|---|
| code | 程式碼·系統·資料 |
| art | 美術·視覺·UI |
| sound | 音效·音響 |
| anim | 動畫·動作 |
| qa | QA·驗證 |
這五類就是內部運營標準。外包·法務這類外部受眾,在這套標準之外另行處理。固定為五類之後,把 framing 交給 LLM 時,就不必每次重寫受眾定義,還能用清單抓出被漏掉的受眾。
如果每份規格說明都手工寫五個職能的摘要,最後就不寫了。所以我把流程這樣串了起來。
策劃只寫決定事實,LLM 就生成五份包裝初稿,策劃再判斷否決·補強·保留。一旦出現缺口([待確認]),就不在 framing 階段處理,而是退回決定階段 —— 因為 framing 不是填補決定漏洞的工具,而是搬運既定決定的工具。
在這個迴圈裡反覆踩中的四個陷阱,連同對策一併列在下面。
| 陷阱 | 症狀 | 對策 |
|---|---|---|
| 資訊重複 | 同樣的內容在正文·摘要裡重複,加重運營負擔 | 正文寫 1 次,摘要只寫差異項 |
| 資訊遺漏 | 對某個職能至關重要的值整塊缺失 | 用 5 受眾固定清單檢查遺漏 |
| 忽略正文 | 只看摘要,漏掉正文的上下文 | 在摘要末尾註明"依據見正文" |
| 媒介錯配 | 給美術發 md,增加學習負擔 | 固定職能媒介原則(美術=html) |
自動化能把撰寫負擔降到每份規格說明 5 分鐘上下,但否決·補強·保留的判斷並不會隨之自動化。那份判斷才是人的位置。
下面是作者所運營的專案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 日·新老訂閱使用者全體"這三個數字,只要在任何一種包裝裡對不上,那一刻客戶糾紛就會爆發。
setup
prompt
[決定事實]
(數值·時點·範圍逐行寫)
把這個決定按 code·art·sound·anim·qa 中相應的職能來包裝。
每種包裝都去掉該職能不關心的資訊,但不變值(數值·時點·範圍)在任何包裝裡都一模一樣地釘進去,
美術包裝不引用其他文件、僅憑這一張就自足;不確定的值不要編造,用 [待確認] 標出。
verify
[待確認],不要在 framing 處理,而要退回決定階段(95_BattleTF 類的 TF)。單人精簡版
如果你是一個人工作,就把受眾減到兩類 —— "以後的我"(實現)和"審校者"(QA)。寫下一行決定事實,請 LLM"把它拆成實現備忘和審校清單兩份",然後只需對照這兩份裡的核心數值是否一致即可。哪怕受眾只有兩類,"把同一個決定包裝成不同形態、但保留不變值"這一 framing 骨架照樣運作。
會議結束已過去5分鐘。會議室的白板上還留著字跡。"戰鬥受擊判定,採用客戶端先行處理。但伺服器驗證的優先順序放到下一個衝刺。"這是5個人花了30分鐘才得出的結論。所有人都點了頭,有人還拍了照片。
3周後,同樣的5個人再次聚到同一間會議室。議程列表的第一行寫著:"戰鬥受擊判定 —— 客戶端先行處理 vs 伺服器驗證,需要決策。"沒有人記得3周前的結論。白板照片存在某人的相簿裡的某個角落,而那個人今天休假。又花了30分鐘。這次得出了相反的結論。
這就是會議記錄之所以是最大痛點的全部原因。會議上會作出決策。然而那個決策卻無法流轉(propagate)到下一次會議、下一份文件、下一個構建。決策作出了,卻無法傳播。本章講的正是用資料接續那道斷裂環節的故事。
筆者運營的個人 RnD 系統分成了17份文件:atom 命名規範、關係圖自動化、Layer 對映指南、JIT 注入基礎設施等。其中吞掉最多時間的單一文件,是會議記錄改進計劃。它的分量之大,足以與其他16份加在一起相提並論。
起初我感到不解。會議記錄嘛,不就是照著記下來就行了嗎?然而實際測量後發現,痛點的位置不在會議記錄的撰寫,而在會議記錄之後。會議上決策確實作出了。問題在於:這個決策由誰負責、依據什麼、期限到何時、接續到什麼 —— 這些在走出會議室門的那一刻就蒸發了。
把這道斷裂畫成一個場景,便是如此。
虛線就是斷裂的傳播。決策(橙色)作出了,卻沒有流向三條分支(紅色)中的任何一條。無法流出的決策會在3周後回到會議上。箭頭向上彎折、重新回到會議的那個迴圈,正是痛點的本體。
一旦傳播斷裂,四件事會同時發生。
失去決策的歷史。面對"當初為什麼那麼定?",只能以"記不清了,再約一次會吧"來回答。重複會議增多。同一議題每個季度都被重新提上議程。新加入者抓不住上下文。由於看不到決策的累積,每次都得一對一地解釋。而且 AI 輔助變得無力。因為沒有上下文,回答只停留在泛泛之談。會議記錄一旦散落各處,就無法把"我們團隊以前對這個議題是怎麼決策的"提供給 AI,於是 AI 只會返回網際網路上的平均值。
這四件事都出自同一個根源。因為決策被當作備忘,而非當作資料來對待。備忘會揮發,而資料會流動。
這裡需要把視角翻轉一次。若把會議記錄看作"會議的產出物",那麼記下來、存檔,任務就結束了。存檔的會議記錄就像桌上的便籤紙。當天看得見,到了下週就不知道去哪兒了。
若把會議記錄看作"決策資料庫",它就變成了一項完全不同的工作。資產不是會議記錄本身,而是從會議記錄中提取出來的決策;這個決策必須可檢索、可引用、可傳播。會議記錄只不過是一條把決策開採上來的礦脈罷了。
用程式碼強制推行這一轉變,正是第17部分的主幹。筆者的系統裡有一個支撐這一轉變的 atom,名為 decision_summary_not_clickup_mirror。展開來說,它是"會議記錄的決策摘要不是 ClickUp(任務追蹤器)的映象"這一原則。
這個 atom 為何必要,正戳中了痛點的要害。很多團隊會把會議記錄的決策槽直接謄抄到任務看板上。這樣一來,"要做的事"留下了,"為什麼那麼定(依據)"卻消失了。任務追蹤器裝的是要做什麼,而不裝為什麼那麼定。3周後會議重複的原因,恰恰就是這個。要做的事已經關閉,卻沒有依據,於是有人問起"話說這個當初為什麼決定要這麼做來著",卻沒人答得上來。所以決策摘要不能成為追蹤器的映象,而必須是一份懷有依據(rationale)的獨立資產。atom 名稱本身就是這條禁止線。
能夠傳播的決策與會揮發的決策,差別在於結構。會揮發的決策是"採用客戶端先行處理"這樣一個句子。能夠傳播的決策則被分解成4個欄位。
4個欄位中最重要的是 owner。決策若沒有負責人,那個決策就不是任何人的事;而不是任何人之事的決策,不會傳播到執行。所以筆者的提取管線在 owner 為空時不會就那樣放過,而是用 [MISSING] 明確上報。它把"責任線為空"這一事實本身抬升到表面上來。
rationale 是前面所說的 decision_summary_not_clickup_mirror 原則所棲身的地方。沒有依據,3周後會議就會重複。follow_up 是決策通向實際執行的橋。這個欄位一旦為空,決策就只停留為決策,觸及不到構建。
這4個欄位由人每次手動填寫也是可行的,但那樣強制力就弱。筆者的系統使用一條從會議記錄中自動提取決策、並上報缺失欄位的管線。3個指令碼序列相接。
第一步 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 的可逆區間內 —— 完成。
只講抽象難以體會,所以這裡原樣搬來一份把實際損壞的會議記錄放進管線跑一遍的實操記錄(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部分原本設計為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。
如果3個指令碼讓你覺得負擔,就這樣精簡。只把會議記錄格式統一為4個槽,會議結束後僅取出決策槽,用上面的提示詞在 LLM 上跑一次。只對 owner 顯示為 [MISSING] 的決策,當場把負責人填進去。即使沒有自動化,僅做這一點,也能擋住"決策沒有負責人"這一最常見的傳播斷裂。lint、promote 等,等到會議記錄堆積、需要檢索時再新增即可。
週三早上,剛到公司,團隊即時通訊工具裡就彈出一條提醒。"上週不是定好把背包格子數增加到30格了嗎?誰負責改資料表來著?"沒有人能在這條討論串裡給出答案。會議紀要肯定是有的,在某個資料夾裡。開啟一看,議題和討論密密麻麻,可"到底決定了什麼、誰來負責"卻化在了字裡行間。結果下一次會議上,又把同一個議題從頭再提一遍。
本章講的是填補那三天空白的機器。一份會議紀要進來後,通過格式檢查,提取出決策的四個欄位,沒有負責人的決策會被貼上 [MISSING] 標籤,生成候選檔案,一週後經過評審,成為可自動注入的資產。人工只觸及兩端兩處——撰寫會議紀要的入口,以及每週評審一次的出口。
先用一張圖看全貌。每一個方框要麼是一段小指令碼,要麼是人的判斷。需要人工經手的方框只有兩個,其餘都自動流轉。
只有兩個藍色方框(撰寫會議紀要、每週評審一次)是人工,其餘都是指令碼。橙色方框([MISSING] 報告)是自動檢查重新把人叫回來的地方。當決策沒有負責人時,管線並不是就此停下,而是把決策退回到撰寫會議紀要的步驟,直到定下誰來負責為止。這正是這條管線的核心設計——不悄悄放過空白,而是大聲報告出來。
整個資產的資料夾結構是這樣安排的。
要能提取,會議紀要就得是機器可讀的樣子。如果沒有 "## 決策" 章節,或者決策混在一整段行文裡,解析器就什麼都提不出來。所以最先加入的是格式檢查。meeting_lint.py 做的事很簡單:是否有必需的 frontmatter,是否有必需的章節,決策槽位是否以 D1、D2 的格式填好。
# 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 集中清理一次,才是現實的運營方式。
在通過格式檢查的會議紀要中,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 裡。
經解析器通過的決策不會立刻成為正式 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。
廢棄記錄會成為下次會議上"這個議題以前沒試過嗎?"的答案。它是防止同樣的錯誤犯第二遍的最廉價的工具。不過廢棄記錄堆積起來會變成檢索噪聲,因此需要每季度清理重複項、只留下教訓的整理。
每週在固定的時間集中檢視 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 決策及其依據就會一併進來。這正是過去靠手工謄抄的決策,在需要的那一刻自動浮現、成為資產的轉折點。
這是筆者在專案A的運營經驗中,把只定下標準格式的階段與啟動了管線的階段作對比後的印象。下面的數字並非精確計量,而是運營中體感到的方向和大致比例,其中摻有筆者的推測(未經驗證)。
| 專案 | 僅格式(手動提取) | 啟動管線 |
|---|---|---|
| 會議紀要 → 決策提取時間 | 每次會議 20\~30分鐘 | 不到1分鐘 |
| 決策的 atom 升格比例 | 5\~10%(整理時間不足) | 60\~80%(全量審查) |
| "以前不是決定過嗎?"的重複會議 | 每季度 5\~10次 | 每季度 0\~2次 |
| 負責人不明的決策 | 無法追蹤 | 通過 [MISSING] 報告即時可見 |
變化最大的是升格比例。靠手工整理時,由於沒有時間,90%以上的決策都揮發掉了。一自動化,全量審查成為可能,有價值的決策便一條不落地留存下來。方向是明確的。比例的精確數值則隨團隊規模和會議頻率而變化。
| 模式 | 處方 |
|---|---|
| 只把 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] 標記,下週補上。指令碼以後手忙不過來時再加也不遲。關鍵是"決策一行·寫明負責人·每週彙集一次"這三個習慣。
會議記錄的目的不是堆積。真正的目的是:半年後仍能被檢索到、能引向決策、並在兩臺 PC 上呈現為相同的狀態。
週二下午。我想起來,一年前的一次會議上明明約定過,要把角色服裝的飽和度降低一檔。可是那份會議記錄怎麼也找不到。開啟資料夾一看,meeting_0413.md、회의_수정본_final.md、IMG_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 提升。本章講的是支撐這條管線長期不崩壞的三項運營標準。
會議記錄隨著時間推移會累積到數百、數千份。檢索不到的資料不是資產。分類就是檢索的第一個分岔口。這就像在辦公室的檔案櫃上貼標籤。沒有標籤的櫃子,最終沒有人會去開啟。
筆者負責的專案A(MMORPG 開發)把分類歸為五個。關鍵在於保持小而正交。
五個並不是所有團隊的標準答案。如果是以非戰鬥系統為核心的專案,就需要把 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 的附件影像多,下一節的圖註標準是必需的。由於決策以視覺為中心,決策槽裡會放入  這樣的影像引用。battle 的決策是數值·規則,atom 自動提升的比例最高,而且一行決策會牽動配置表的批次變更,因此影響範圍的視覺化(第11部分的關係圖)很重要。daily 幾乎沒有決策才是常態,又因累積很快,故按周拆入自動資料夾(daily/2026-W21/)。issue 的會議記錄較為雜亂,因此把事後 24 小時內整理定為義務,並把防止復發的 atom 提取到 issue_postmortem/。review 篇幅長,故另行撰寫 5\~10 行的摘要 atom,讓它在下一次季度覆盤中被自動引用。
新增分類要極為謹慎。每季度發生 5 次以上、運營方式與既有五個明顯不同、需要單獨的路由資料夾、且一個月後仍能維持 5 次以上 —— 只有全部通過這四個條件才予以考慮。就筆者的運營經驗而言,五個維持了一年以上,即便 tech_review 或 external 這樣的候選一度浮現,最終也都被 sub_topic 吸收了。
分類以人在撰寫時直接錄入為主。只有像從外部收到的資料那樣存在缺失的會議記錄,才用 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 穩定性就會浮上為下一個優先事項。分類不僅用於檢索,也是映照組織時間分配的一面鏡子。
art 會議記錄的正文有一半是影像。而沒有圖注的影像,就像堆在桌上的一疊照片。當天什麼都記得,可一個月後,只有在背面寫了一行備註的照片才能存活下來。
影像佔了會議記錄的一半,若無法檢索,就等於會議記錄資產的一半消失了。讓那一半存活下來的,就是一行圖注。
專案A 的圖註標準三行就寫完。

**[圖 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 上,再疊加一行檔名檢查就夠了。
會議中經常會引用外部遊戲·美術作為參考。若沒有出處,就會直接釀成版權事故。

**[圖 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。
給 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(檔名標準 + 圖注格式),那個方向就已經清晰地顯現出來。
會議記錄本身是文本檔案,用 git 就夠了。同步真正的物件,是從會議記錄中派生出的資料 —— §17.2 的 pending atom 候選、JIT manifest、分類統計、決策索引(decision_index.json)、圖注索引、按 confidential 等級的構建輸出,以及用於向量檢索的 LLM 嵌入(embedding)。這些資料都必須對會議記錄的變更做出響應。
問題在於:會議記錄一旦超過 1,000 份,每次都重新處理全量的成本會佔到運營的一半。這就像停下整條生產線,把所有零件重做一遍 —— 明明只改了一個零件。
Full Sync 實現簡單,狀態不一致的風險為 0,在引入初期(100 份以下)反而更安全。這並不是說 Full 是壞做法。只是與會議記錄數量成線性正比的成本,會從超過 200 份的那個位置開始成為瓶頸。到那時就切換到 Incremental。
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
如果只是 category 或 confidential 這類後設資料變了,就沒必要重新生成 LLM 嵌入。嵌入通常是同步成本中最大的一塊,因此這一次分支能大幅降低成本。嵌入以 content_hash 為準做快取 —— 正文雜湊相同就直接複用快取的嵌入,而僅修改 frontmatter 時,嵌入呼叫為 0。
成本差異的方向很明確(下面是筆者估算,並非絕對值)。在每週變更 50 份左右的運營中,相比每週 Full re-embed,Incremental 的嵌入成本降到了幾十分之一的水平。會議記錄越增加,Full 的成本就與累積量成正比地變大,而 Incremental 的成本只繫結在每週的變更份數上,與累積量無關,幾乎是平的。這種"與累積無關"的性質,正是 Incremental 的本質價值。
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.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%"這樣的說明,雲同步只挑改動過的檔案來處理就行。這樣,半年後想找"當時為什麼把單價壓下來了",一行圖注就能立刻查到。
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 當中出現的決策"這類情況,讓人再看一遍。
如果你是獨自工作的策劃,就這樣精簡。
decision(有決策的會議)和 log(進度記錄)。只對有決策的會議打理圖注·atom,其餘的堆進日期資料夾即可。decision_index.json(決策 ID → 會議記錄路徑對映)這一個檔案,儲存會議記錄時只更新那一行。git 即同步,在積累到需要區分 Full/Incremental 的量之前,每次全量重新生成就足夠了。即便在單人規模下,不變的核心只有一個 —— 留下通往決策的路徑。分類·圖注·同步只不過是撐起這條路徑的三根支柱,完全可以按規模把它們立得纖細一些。
距離里程碑演示還有三天,午飯時,一位策劃放下餐盤問道:"把任務獎勵金幣提高到 1.5 倍那件事,是會上定下來的吧?可以錄入配置表了嗎?"旁邊的人答道:"那不是有人只是提議'要不試試看'嗎。"90 分鐘的錄音檔案和某人用鍵盤敲下的兩頁備忘錄,確實都在。但那份記錄裝下了"討論了什麼",卻沒能裝下"決定了什麼、由誰負責、為什麼這麼定"。
把公司的 17 份 R&D 文件按"痛點"程度排序時,佔比最大的是會議記錄改進計劃書。這出乎意料。既不是戰鬥平衡,也不是內容量產管線。最痛的地方只有一個:會上做出的決策沒有傳導到執行。
於是我把會議記錄系統重新設計成了決策追蹤資料庫,並用六個月的親自運營,驗證了在這條流程的哪些環節該放入 AI、哪些環節不該放。本章就是這 5 個切入點的地圖。
從錄音檔案開始,到決策圖譜更新為止,在整條會議記錄管線中能安放 AI 助手的位置,正好有 5 個。但不會 5 個同時放入。因為每個位置的成熟度和出事風險各不相同。
紅色(位置 2)是最誘人也最危險的位置,藍色(位置 3、4)是最先引入的安全位置。管線中間的 meeting_lint.py → decision_parser.py → promote.py 不是 AI,而是確定性指令碼。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 來搭骨架——幻覺就會摧毀決策資料庫的信任本身。
位置 2(STT 文本 → 自動生成會議記錄初稿)是所有團隊最想先做的位置。因為"只要把錄音丟進去,會議記錄就出來"這幅圖景太誘人了。而恰恰是這份誘惑,讓它以最高的代價失敗。
失敗的樣態有四種。
其中最致命的是幻覺決策。會上有人只是丟擲一句"全域性冷卻是不是 0.5 秒更好?"的意見,AI 初稿卻寫成"全域性冷卻已達成一致為 0.5 秒"。三週後,這一行被落實進配置表,連招設計在其上疊加,QA 用例也隨之寫就。一個從未達成一致的決策,不可逆地擴散開來。
因此位置 2 適用幾條絕對原則。
這不是說位置 2"永遠不做"。當位置 3、4、1 穩定下來、主持人親身體會到 AI 輸出的侷限之後,引入位置 2 的價值是足夠大的。只是順序上排在最後而已。
這是六個月運營中產出效果最大的位置。人宣告決策的存在,AI 填寫這條決策的附屬欄位。與位置 2 的決定性區別在於:決策存在這一事實本身,由人先行釘死。
有三樣東西,人親手填太費時間,便由 AI 起草。
關鍵在於提示詞強制引用依據、並明確禁止幻覺。下面是實際運營中使用的補全提示詞全文。
提示詞(由人輸入):
看下面的會議正文,只填主持人已宣告的決策 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。
當通過位置 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,總會漏掉一邊。
同時開啟 5 個切入點,是最常見也最昂貴的失敗。運營負擔比效果更早到來,團隊會把整套系統一起丟棄。下面是實際遵循的順序。
把位置 2 放到最後,是這個順序的核心。最想做的位置最後才做——這有違直覺,但讓最熟練的手落在最危險的操作檯上,才是作業現場的安全原則。
從成本上看,這個順序也合理。以每月 100 場會議計,位置 3 約 $5\~10,位置 4 約 $1\~2(基於作者運營環境的估算,未經驗證),只開這兩個,每月也不到 $10。產出效果最大的兩個位置,恰恰最便宜。
同一場會議用兩種方式記錄時的差異,就是本章的全部概括。
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 只需說"從會議備忘裡引用這條決策的依據,沒有就說沒有"。即便沒有管線,只要宣告決策的位置和強制引用依據的提示詞這兩樣,會議記錄就會開始變成決策資料庫。
那是季度會議進行到一半的時候。戰鬥設計師提議"把全域性冷卻(GCD)統一為 0.5秒",大家都點頭贊同。可是坐在旁邊的資深同事舉起了手。"這不是和去年第四季度定的 0.3秒相沖突嗎?當時為什麼定成 0.3秒來著?"會議室一時安靜下來。沒有人記得那個決定的依據。翻遍了會議記錄,卻只有"在戰鬥 TF 中討論過"這一行字。最後花了 30分鐘去重現去年的決定,即便如此,"為什麼是 0.3"始終沒能找到。
決策的難點不在於做出,而在於追蹤。一年積累數百件之後,哪個決策還有效、哪個已經廢棄,哪個決策又以另一個決策為前提,單靠人腦已經跟不上。本章討論的,是把決策固化為 atom(最小知識單元)、將其轉化為可追蹤資產的系統。核心很簡單:把一個決策記錄為一張帶有 decision_id、owner、rationale 的卡片,用 wikilink 把卡片彼此相連形成圖譜,再用 grep 反向追溯影響波及到哪裡。
決策追蹤的最小單位是決策卡。這裡原樣取自筆者所負責的專案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_034 的 rationale 欄位裡的內容。其餘欄位(scope、affected_atoms、related_decisions)是為影響追蹤和圖譜連線而鋪設的線路。
這裡引入了一項設計決策。如果強制填滿全部 12個欄位,人們就會連卡片本身都不願寫。因此把它分成必填的 5個欄位(decision_id、title、owner、status、rationale)和選填的 7個欄位。會議上做出決定後即使只填這 5個欄位,卡片也是有效的,其餘的在實現階段再補。
一張卡片從產生到廢棄要走過怎樣的路徑,構成了追蹤系統的骨架。請注意不可逆關卡位於何處。
從起草(B)到評審關卡(G)全部是可逆階段。無論是修改還是廢棄卡片,成本都幾乎為零。然而納入構建(H)之後就是實質上的不可逆了。使用者已經體感到的變更,即便用熱修復回退,也會在社群認知中留下痕跡;而當後續決策開始以這個決策為前提層層累積時,回退成本會呈指數級增大。因此決策者的一切評審都必須在關卡 G 處結束。這與第5部分討論的"錄音·選角是不可逆階段"原則,是完全相同的結構。
把卡片做成 atom 之後,卡片彼此就能相連。related_decisions 中的 supersedes、relates_to 成為圖譜的邊。前面會議上的衝突,其實就是這張圖譜的一個片段。
如果當時有這張圖譜,會議 30秒就能結束。開啟 D2026_Q2_017 就能看到 supersedes: D2025_Q4_034,點一下那張卡的 rationale,"為什麼是 0.3"就原樣出現了。圖譜是決策的演進歷史,而決策的演進歷史就是遊戲的歷史。連像 PvP 變體(D2026_Q2_025)這樣從本決策派生出的分支,也能一目瞭然地追蹤到。
決策卡的 affected_atoms、affected_files 若由人一個個去填,總會漏。專案A裡有一個叫 impact 的影響範圍提取流程。它接收一個決策 atom,朝三個方向掃描圖譜。
affects 連結:明確宣告"產生影響"的關係[[combat_global_cooldown_constant]] 的所有文件三條路徑的並集,才是決策真正的影響範圍。在此之上,portal_layer_change_impact_check atom 會額外檢查"是否觸及門戶層(對外暴露的文件·API 規格)"。一旦涉及門戶層,等級就上調一級。因為對外傳播的回退成本更高。
理論到此為止。下面把實際向 LLM 丟擲一整段會議記錄、拿到決策卡的全過程,連同提示詞全文和原始輸出原樣刊出——這就是實操記錄(worked transcript,完整保留真實操作過程的記錄)。不做摘要。Claude 混淆的地方、人工駁回的地方,乃至重新請求,全部呈現。
把下面的會議記錄轉成決策卡 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日。
---
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分。做對的和做錯的混在一起。
owner/approved_by 區分準確,target_build·實現者·QA 準確,捕捉到了 supersedes 關係。scope 裡放了 all_skills。明明標註了治療技能例外,卻當成了全部。affected_atoms 裡臆測填了 combat_global_cooldown。專案實際的 atom 名是 combat_global_cooldown_constant。它違反了"不要臆測"的指示。related_decisions 裡的治療例外用自然語言敘述而非連結來填。affected_files: [] 留空了。這是忠實遵循了指示,但影響檔案不可能沒有。筆者沒有原樣接受這張卡。把 ①②③ 寫成駁回理由重新發起了請求。駁回的核心是 atom 名的臆測。atom ID 哪怕錯一個字元,後續的 grep·反向引用就會全部落空。追蹤系統的準確度,繫於 atom ID 的準確度。
改三處。scope 因為治療技能屬於例外,別用 all_skills,
改成 all_active_skills,治療例外寫進 content。affected_atoms
之前叫你別臆測你卻臆測了——不知道 atom 實名就留成 [待確認],只加註釋
說明要確認什麼,索引我自己來看。related_decisions 把治療例外
用 relates_to 指向 D2026_Q2_018,supersedes 指向 D2025_Q4_034。affected_files
也只作為 [待確認] 候選。
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_constant、combat_skill_cooldown_rule 這兩個名字確實存在,便摘掉了標記。affected_files 的三個候選也在與索引比對後予以確定。本章開頭刊出的最終卡片,就是這一成果。
這份實操記錄的教訓只有一個:LLM 作為決策卡的初稿撰寫者很強大,但 atom ID 與決策 ID 的最終確定,必須由人與索引比對。 AI 探索候選,人來採納。兩者角色一旦混淆,錯誤的 atom 名就會汙染整張圖譜。
當卡片與圖譜以 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 的準確性之上。
下面比較筆者的專案A引入追蹤系統前後的情況。以下數字是筆者的推定(未驗證),建議按方向和比例來讀,而非絕對值。
| 條目 | 無系統 | 系統執行 | 方向 |
|---|---|---|---|
| "以前是否決定過?"的重議 | 每季度 8\~12件 | 每季度 0\~2件 | 大幅減少 |
| 掌握決策影響範圍 | 1\~2天 | 用 grep 數分鐘 | 大幅縮短 |
| 追蹤決策演進歷史 | 依賴資深成員記憶 | 圖譜自動 | 消除對人的依賴 |
| 新成員學習決策歷史 | 1\~2個月 | 1\~2周 | 效果最大 |
效果最大的是最後一行。新成員不再拉著資深同事追問"這遊戲為什麼變成現在這個樣子",而是順著決策圖譜自己讀下去。決策追蹤也就成了公司的決策學習資產。只是系統剛引入的那個季度,寫卡片的負擔確實存在。先從必填的 5個欄位落實、再逐步擴大,是穩妥的路徑。
到目前為止的運營都是保守式應用。人在會議上做決定、寫卡片、識別受影響的 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 承擔起這一分解的初稿工作,原本只停留在紙面上的願景,相當一部分進入了可實現的領域。
decision_id、owner、rationale 的卡片,半年後才能回答"當時為什麼這麼做"。遊戲之外的應用。 決策卡並不限於遊戲,它是讓任何組織在半年後仍能回答"當時為什麼那樣定"的裝置。市場部為了"上個季度決定砍掉這個渠道,到底是為什麼來著"在會議記錄裡翻不到一行、白白耗掉 30分鐘這樣的事,只要有一張帶
decision_id、owner、rationale三個欄位的卡片就會消失。比如人事部在決定"遠端辦公統一為每週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%,就從這一行的習慣開始。
會議結束後,我正在整理會議記錄。上面寫著一條決策。"全域性冷卻統一為 0.5 秒。"這在會上不到 30 秒就達成了一致。所有人點頭,便轉向了下一個議題。
那一行字吃掉了接下來的兩個月。戰鬥資料中的 277 個技能全部受到影響,UI 的冷卻進度條演出被重畫,數值表被推翻重做了兩次。同一份會議記錄裡寫著的另一條決策"修正新手引導提示文案的錯別字",只花了 5 分鐘就完成了。
兩條決策在會議記錄上同樣都是一行,字數也差不多。然而一條是 5 分鐘,一條是兩個月。讓這一差異在寫下會議記錄的那一刻就變得可見——這就是影響等級分類。等級若不可見,兩個月的決策就會被埋沒在與 5 分鐘決策相同的一行裡。
本章討論如何將決策的波及自動分為五個等級,並在決策 atom 圖譜上追蹤這波及蔓延到何處。所用工具是前一章積累的決策 atom 與 impact 提取,以及 portal_layer_change_impact_check atom。
先來看看沒有等級分類的狀態是什麼樣子。當決策全部放在同一行上,兩類事故會交替發生。
一是處理不足。像全域性冷卻這樣撼動整個季度的決策,被當作"5 分鐘的活兒"未經驗證就進入了構建。直到兩個月後波及才顯現,而那時回退成本已如山堆積。
另一是處理過度。為改一個錯別字而召集 TF(專項組),還要獲得遊戲總監的審批。決策週期暴增,而總監本該用在 T0 決策上的時間,卻被吸進了錯別字會議裡。
兩類事故看似截然相反,根子卻相同。決策的分量不可見。分量不可見,於是把力氣花在輕的上面,把重的放任流走。等級分類就是給決策貼上分量標籤的工作,標籤一旦貼上,處理方式便自動分流。
在筆者所在的 MMORPG 開發商專案A,決策的影響被分為五個等級。越往上越重,處理需要更多的人力與時間。
| 等級 | 定義 | 示例 | 決策者 | 週期 |
|---|---|---|---|---|
| T0 | 遊戲願景·核心系統 | 移動端優先決策、核心機制變更 | 遊戲總監 + CEO | 季度 |
| T1 | 系統·跨領域 | 全域性冷卻統一、新增職業 | TF 組長 + 總監 | 1\~2 周 |
| T2 | 領域·中等 | 特定技能數值調整、新增 UI 元件 | 領域總監 | 3\~5 天 |
| T3 | 單次·小型 | 修改單個 NPC 臺詞、顏色微調 | 資深人員 1 人 | 1\~2 天 |
| T4 | 即時·熱修復 | 修復 Bug、文本錯別字 | 負責人 | 小時級 |
只看表格,它像教科書一樣清晰。然而實務的難點不在於背表,而在於判斷眼前的一條決策該放進哪一格。"全域性冷卻統一"是 T1 這一點,不能等會議結束之後才知道,而要在寫下會議記錄的那一刻就知道。因此下一節的 3 條標準才是關鍵。
等級不靠直覺來定。評估三條標準,採納其中最高的等級。
三條標準中,影響領域數可以在決策 atom 圖譜上機械地數出來。把決策所觸及的 atom 歸屬於哪個領域(戰鬥·UI·資料·敘事等)的標籤彙總起來即可。
問題在剩下的兩條。回退成本與使用者影響範圍無法換算成圖譜上的數字。"要在兩個月後回退這條決策,得花多少代價"是一種自然語言判斷。恰恰是這一點,在 2023 年之前一直是影響自動分類的最後一堵牆。影響領域數已經自動化,但兩格自然語言判斷空著,結果還是要人從頭重新評定。隨著 LLM 讀取決策 atom 的正文、為這兩格填寫初稿,這堵牆降低了。
這裡要如實說明。LLM 填寫的是初稿,而非定論。即便 LLM 把回退成本估為"大",領域總監也可以判定"按我們的表結構,這個是中等"。自動分類並不替代人的判斷,而是讓人不必從空格開始。
這是把前一章製作的一條決策 atom 原樣交給 LLM、讓它評定等級的真實過程。實操記錄即完整保留的真實操作過程記錄:不作摘要,原樣搬錄全過程,連否決與再次請求也一併收錄。
# 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 原文]
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 行的情況下寫下"大"。
等級確定之後,接下來是"波及到何處"。把前一章的 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 訊號,意味著它是事後追蹤的物件。
分類不是獨立的步驟,而是作為關卡固定在決策流程的正中央。決策候選一經登記,自動分析就推薦等級,只有在人評審、調整之後,才進入決策會議。
在這一流程中,不可逆的步驟只有一個,就是反映進構建(I)。它之前的全都是可逆的 —— 即便等級推薦錯了,人退回即可,weight 標籤也可以撕掉。只有在進入構建、傳播到其他文件之後才變得不可逆。因此關卡(F 的人工評審)設在構建之前。在跨過不可逆這條線之前,人先攔一次。
最後那條虛線 —— 事後追蹤(J)返回下一次決策的自動分析(B)的箭頭 —— 讓這套系統成為一個學習迴圈。不可逆步驟中產出的實測資料(例如:QA 時間比估計更長)被吸收進下一次決策的可逆步驟。
決策進入構建後 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%)的處方。
事後追蹤的價值不在於猜中準確的數字,而在於把誤差的方向反饋回去。估計越準確,等級分類的可信度就越高,可信度越高,委派就越有可能。
各等級反覆出現的事故各不相同,處方也不同。
| 等級 | 事故模式 | 處方 |
|---|---|---|
| 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 的人工評審關卡不是單純的形式。
比較專案A引入等級分類前後的情況。下面數值中的絕對值是加工過的示例,而方向(不等號)是實際趨勢。
| 對比項 | 無等級 | 等級運營 |
|---|---|---|
| 決策週期 | 全部統一為 1\~2 周 | 從 T0 季度到 T4 小時級分化 |
| 處理錯誤的決策 | 每季度多起 | 每季度少數 |
| 總監的每週決策負擔 | 大(所有決策都匯到總監) | 小(T2\~T4 委派出去) |
| 熱修復週期 | 1\~2 天 | 4\~24 小時 |
| 季度覆盤的決策分析 | 難以歸類 | 按等級統計彙總 |
把表寫成方向而非確定數字,原因由第 3 項就能說明。總監時間的回收是等級分類最大的效果。沒有等級時,從錯別字到願景,所有決策都湧向總監一個人。有了等級,T2 及以下就分流給領域總監·資深人員·負責人,總監則專注於 T0·T1。委派成為可能,也就意味著總監奪回了真正用於重大決策的時間。
前一章製作了決策 atom 圖譜,本章在這張圖譜之上疊加了等級分類自動機。兩者不是各自為政的工具,而是同一骨架中相連的位置。
三個要素是串聯的。圖譜製造輸入(①),分類器評定分量(②),影響追蹤鋪開傳播範圍(③)。本章處在中間的位置。
三個要素都是在 LLM 發展之後才進入可實現的領域。最晚被攻破的牆是②的兩格自然語言評估 —— 回退成本與使用者影響範圍(§18.2.2 的"最後一堵牆")—— LLM 填補其初稿之後,①→②→③才終於開始串聯運轉。
可逆·不可逆的排布也與這一骨架相扣。正如 §18.2.5 的流程圖,不可逆的線只有反映進構建這一條,反映進構建本身無法回退,但其實測結果會作為讓下一次決策更準確的可逆學習返回來。
| 模式 | 處方 |
|---|---|
| 用同一週期處理所有決策 | 按等級分化週期 |
| 沒有等級分類,各自自行判斷 | 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 個等級。
工具也請從零行程式碼開始。記錄決策時,只在前面加上 [重] [中] [即時] 標籤。僅憑這一點,你就會在貼了"重"標籤的決策前多停一次 —— 等級分類的本質說到底就是在重大決策前停下的習慣,而自動化不過是讓這份停頓在團隊規模上也能運作的裝置。
上線三週後,那是一場覆盤會議,用來回溯 PvP 平衡崩壞的原因。在白板上一路倒推,最終到達的起點,是一個月前的一條決策。"將全域性冷卻從 0.3 上調到 0.5。"它是因為收到"看不到連招"的反饋、在兩小時內就達成一致的、合理的提案。然而那個改動把坦克職業的生存率比預期又拉高了 14%,正是它讓 PvP 崩潰了。在做決策的現場,沒有任何人指出那條決策會波及到坦克。決策本身並沒有錯。事故的原因在於,沒能在做出決策之前看清這條決策會波及到哪裡。
影響追蹤必須發生在兩處。在按下決策之前(pre)看清它會波及到哪裡,在決策落地之後(post)確認它是否真的只波及到了那裡。本章把這兩次追蹤合併為一個工作流。
決策影響分析的核心出乎意料地簡單。把一個決策 atom 看作一個節點,讀取流入該節點的邊與流出該節點的邊。事前追蹤問的是"改動這條決策會影響到哪裡"(出向 + 反向引用),事後追蹤問的是"那些影響是否真的按意圖發生了"(把同一批邊與實測值比對)。
在作者的專案A中,決策以 atom 的形式留存在 decisions/ 資料夾裡。目前已累積 26 個,每個 atom 都以 frontmatter 攜帶日期、當事人、依據、影響範圍。抽取影響範圍的工具是 impact,而把這套抽取規則按決策單元強制執行的 atom 是 portal_layer_change_impact_check。這三者就是事前、事後追蹤的實際資產。
事前追蹤讀取右側(出向),來預測"會波及到這裡";事後追蹤則取來右側各節點的實測值,與預測比對。這是把同一張圖以不同目的讀兩遍。
從這裡開始是真實的會話。不做概括,而是原樣刊出提示詞全文、Claude 的原始輸出,以及人如何拒絕它並重新提出要求。
impact 是一個接收決策 atom 的 ID、抽取影響圖的小型管線。它把入向邊(這條決策的依據)、出向邊(這條決策將修改的檔案)、本體中的 affects 關係,以及維基連結反向引用(通過 [[D2026_Q2_017]] 指向這條決策的其他文件)合併起來,生成影響候選清單。
評估 decisions/D2026_Q2_017.md 在 proposal 階段的事前影響度。
- 這條決策將修改的檔案(出向)
- 通過 [[D2026_Q2_017]] 反向引用這條決策的文件(入向 wikilink)
- 通過本體 affects 關係連線的系統
把這三者合併給出影響候選,併為每個候選區分它是需要我確認的項(T1),
還是可以自動確定的項。不要用猜測來填補,若沒有依據檔案
就標註為"無依據"。
已讀取
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 放進了候選清單?
指正得對。我更正兩處。
- 撤回"+12%"。 atom 與本體中都沒有這個數值的依據。這是我生成的值。只應報告
TankSurvival存在影響這一關係,大小要用模擬(Layer 2)的結果來填充。當前值為未測量。- ui 項重新分類。 由於找不到具體檔案,在候選清單中不應作為 T1(需確認),而應降級為
依據不足 —— 需由人指定檔案。更正後的表如下。
| 影響候選 | 邊型別 | 依據 | 分類 |
|---|---|---|---|
CombatFormula.md global_cooldown |
出向 | atom 明示 | 自動確定 |
CombatBalance 回覆例外行 |
出向 | atom 明示 | 自動確定 |
TankSurvival 指標 |
affects | 本體關係 | 有影響 / 大小未測量 |
| ui 連招顯示(檔案未知) | 推定 | 無依據 | 需由人指定檔案 |
| 入向反向引用 | wikilink | grep 0 條 | 不適用 |
這張表就是事前追蹤的產出。核心是 TankSurvival 那一行。開篇中讓 PvP 崩潰的那個影響,在事前追蹤裡已經以"有影響 / 大小未測量"浮現了出來。如果在會議現場看到這一行,就會變成"那就用模擬測出大小再做決定"。若放任 AI 編造出 +12%,反而更危險。因為虛假的精確度會讓人跳過驗證。
事前追蹤結束後就在會議上做決策。決策以 atom 形式確定的那一刻,兩個不可逆階段就開始了。
不可逆的原因很簡單。落地到構建的數值,使用者已經玩過了;傳播到其他文件的內容,團隊成員已經據此開始了下一步工作。因此在這兩個階段的緊前,portal_layer_change_impact_check atom 會作為關卡攔在前面。這個 atom 的規則可以用一句話概括:"若決策包含跨 Layer 的變更(資料 → 公式 → UI),則在落地前必須由人對影響候選清單批准一次。"只有自動確定項時放行,只要出現一個"需由人指定檔案"就攔截。18.3.2 中的 ui 項就成了攔截理由。
在構建落地後 1 周\~1 個月執行事後追蹤。這是給事前追蹤生成的影響候選表的每一行附上實測值的工作。
D2026_Q2_017 事後追蹤。給事前追蹤表(18.3.2)的每個候選附上實測。
實測來源:
- CombatBalance 表 git diff(用於確認落地)
- 模擬 1000 次日誌 sim_2026q2_017.json
- 上線 1 周 KPI export kpi_w1.csv
計算意圖值與實測值的差異,只單獨彙總偏差超出 10% 範圍的行。
缺失的資料標為"未收集"。
已將實測與事前追蹤表結合。
| 候選 | 意圖 | 實測 | 差異 | 判定 |
|---|---|---|---|---|
| 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 勝率拉回意圖範圍。
已生成
decisions/D2026_Q2_025.md草案。在入向掛上[[D2026_Q2_017]],用反向引用明示這次決策是對 17 號的副作用修正。現在再用impact查詢 17 號 atom,就會捕獲到 1 條入向反向引用(25 號)—— 這正是 18.3.2 中為 0 條的那個位置。
至此圖閉合了。在事前追蹤中還是"有影響 / 大小未測量"的節點,在事後追蹤中被確認為偏離,而後續決策以指向該節點的反向引用進來了。決策的整個週期在同一張圖上轉了一圈。
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%"得以成立。
這是作者的專案A中,把決策週期標準化前後比較得到的數值。絕對時間數值取決於團隊規模(中等規模,10\~50 人),屬作者推定(未驗證);而比率與方向是在實際運營中觀察到的。
| 對比項 | 前後追蹤分離 | 前後追蹤合併 |
|---|---|---|
| 實際運行了事後追蹤的決策比例 | 約 30% | 90% 以上 |
| 事前浮現卻在事後釀成事故的影響 | 常見 | 幾乎沒有(事前設關卡) |
| 副作用 → 後續決策的連線率 | 低(口頭傳達) | 通過反向引用自動候選化 |
| 決策圖的入向反向引用完整性 | 稀疏零散 | 閉合迴路 |
核心只有一點。當事前追蹤與事後追蹤共享同一張候選表時,事前留作"大小未測量"的那個缺口,會在事後恰好在同一位置被確認。若二者分離,事前所見與事後所測是不同的格式,無法比對,於是追蹤率停留在 30%。不過若一開始就把反向引用完整性定為 100% 的目標,只會徒增運營負擔。現實的做法是,先養成在決策 atom 中寫 affected_files 的習慣,再把反向引用 grep 嵌入覆盤週期,逐步擴大。
| 模式 | 處方 |
|---|---|
| 事前看到了影響卻沒測大小就做決策 | "大小未測量"的行在模擬前暫緩決策 |
| 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]]" 一個就夠了。事前一格,事後一格 —— 這就是前後追蹤的最小形態。
週一上午 10 點。負責戰鬥的團隊成員 A 在團隊即時通訊工具裡丟下一句話:"全域性冷卻從 0.5 秒下調到 0.4 秒可以嗎?"這是改一個數字的事。表面上是。我讀到這句話,手停住了。這個數字被錄入了幾份文件、以這個常量為前提搭建的技能平衡 atom 有幾個、改動它會讓哪張表格的公式失效——我腦子裡浮現不出來。若自以為浮現出來了,那就是事故。每個季度都會爆出 8 到 12 起的"沒看到那份文件"式遺漏,其真身正是這種錯覺。
所以我決定不去背答案。而是敲一行命令。
impact combat_global_cooldown_constant
本章原原本本地看這一行命令吐出了什麼。它要展示的是:提取影響範圍不是抽象的說法,而是用 grep 把入站邊、本體 affects、wikilink 反向引用這三條路徑蒐羅到一起的具體動作。
"改動這個 atom 會影響到什麼"這個問題,其實是三個問題。混在一起答案就模糊,拆開來每一個都能落成一行 grep。
第一,入站邊(inbound edge)——誰指向我。atom A 引用 atom B,就是 A→B 方向的邊。改動 B 時危險的是那些指向 B 的 A,也就是進入 B 的箭頭。所以看的不是出站(我看向誰)而是入站。變更的衝擊波沿著箭頭逆流而上。
第二,本體 affects——在語義上影響到什麼。這是 atom 的 frontmatter 中寫明的 affects: 欄位。即使名字沒有直接出現,它也是設計者預先宣告的"這個會影響那邊"的語義連線。它把 grep 抓不到的別名、同義詞問題,由人預先錄入了進來。
第三,wikilink 反向引用——以 [[atom_id]] 形式顯式連結到我的文件。可信度最高。因為這不是偶然的詞語匹配,而是作者有意建立的連結。
把這三條路徑的關係畫成圖,如下。
把三條路徑捆進一個工具,就是 impact。下一節我們完整地看這個工具實際吐出的輸出。
與其用話解釋,不如從頭到尾跟完一個真實的迴圈——把真實操作過程原樣記錄下來。它包含提示詞全文、工具的原始輸出(未加修飾)、我驗證並否決的部分,直到重新請求為止,全部收錄。
我請 Claude 跑一遍 impact 工作流。工作流本體是呼叫 ripgrep 的 shell,但結果解讀和等級推薦由 Claude 負責。
請對 combat_global_cooldown_constant 執行 impact 工作流。
分別用 grep 提取三條路徑(入站邊 / affects / wikilink 反向引用),
合併重複項後,為每個影響物件附上影響等級推薦。
變更內容為"全域性冷卻 0.5 秒 → 0.4 秒"。
我將按順序執行這三條路徑。
[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.md 在 affects: 欄位裡寫下我們的常量,意思是 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 迴圈和兩次人工判斷,變成了一份帶優先順序的清單。這就是影響範圍提取的實質:工具把候選全量蒐羅,人來定方向和等級。
把上一節的一個迴圈概括成流程,如下。關鍵在於自動環節和人工環節在哪裡分道。
自動的部分是三條路徑的 grep 與彙總,以及等級初稿。人工的部分只有一個,方向與等級的最終判斷。上一個迴圈裡看到的 affects 反向和 refgame 下調,正是在這個位置發生的。如果 100% 信任工具,就會出兩種事故:要麼把 affects 反向從影響物件裡剔除,要麼過度保護對比表,每次都跑一遍不必要的評審。把自動與人工的邊界放在這一個點上,正是這套工作流的設計意圖。
把 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.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,連表格一起看"。兩者不繫結,就會出現文件更新了、表格卻停留在舊前提的典型遺漏。
這是作者在專案A運營中觀察到的變化。時間數值為作者估算(未經驗證),而遺漏事故的件數是季度覆盤中實際統計的值。
| 條目 | 無工作流 | 執行 impact |
|---|---|---|
| 掌握影響 atom 的時間 | 依賴記憶(不完整) | 1\~2 分鐘(全量 grep) |
| 變更遺漏事故 | 每季度 8\~12 起(統計實測) | 每季度 1\~2 起(統計實測) |
| 變更請求附帶影響清單 | 偶爾靠人 | 由關卡強制 |
| 新成員掌握影響 | 數天(口頭逐一傳達) | 30 分鐘(工具 + 卡片) |
| 基礎設施成本 | 考慮引入圖資料庫 | 只用 ripgrep + shell |
最後一行就是整章的結論。專案A 曾考慮過圖資料庫和搜尋索引,最終落定在 ripgrep 和一個小 shell 上。精密測量儀器確實比捲尺準。但每天都要拿出來用的工具,會收斂到不會壞、無需基礎設施的捲尺那一側。遺漏事故從 8\~12 起降到 1\~2 起,不是因為工具精巧,而是因為每次都毫無遺漏地跑一遍。
即便捆起三條路徑,也還有漏的地方。知道侷限而用它,與不知情而盲信它,是兩回事。
別名與縮寫。如果正文只寫"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 一遍全量蒐羅,再由人分類為"必須改 / 僅標記 / 無關",遺漏就消失了。舉例來說,會計人員變更某個科目程式碼時,把引用該程式碼的結算表格、報表模板、宏做全量檢索,整理成變更前檢查清單,就能從結構上杜絕"漏看了那一張表"式的季度結算事故。
只要文件和 atom 以純文本(.md)管理、並裝好了 ripgrep(rg),準備就緒。若有 atom ID 命名規範(蛇形命名、唯一 ID),grep 的準確度會大幅提升。
# 驗證:某個 atom ID 在全部文件中出現了多少次
rg -c "combat_global_cooldown_constant" docs/ atoms/
給出變更目標 atom 和變更內容,請求三條路徑提取 + 等級推薦。
請對 <atom_id> 執行 impact。
分別用 grep 提取入站邊 / affects / wikilink 反向引用三條路徑,
去重彙總後,推薦 18.2 的影響等級(T0~T4)。
變更內容:<把什麼改成什麼>。
只把 T1·T2 拆分為"變更前檢查"清單。
別原樣相信工具輸出,手動確認兩點。
確認後,只把 T1·T2 清單貼到變更請求評論裡,一個迴圈就閉合了。
如果是既沒有工具也沒有 atom 圖譜的個人作業,用一行命令和一格備註也能取得同樣的效果。
# 用要改的概念名稱在全部資料夾中做全量搜尋
rg -n "전역쿨다운|GCD|global_cooldown" .
把檢索結果原樣貼進記事本,在每一行旁邊手動寫上"必須改 / 僅標記 / 無關"三者之一。這就是單人版的 impact。關鍵不在工具的精巧,而在"不依賴記憶、全量蒐羅後由人分類"這個流程本身。有流程,遺漏就減少;沒有流程,週一上午那份茫然就每次重演。
主要讀者:帶領中等規模(10\~50 人)團隊的設計總監·主策劃 面向單人/業餘讀者的精簡版:§19.1.8「一個人的話,做到這些就夠」
即便是把願景文件在一頁內寫得很好的團隊,也會反覆出現同一種事故。願景掛在牆上,可每週不斷堆積的決策是否與這份願景相符,卻沒有人去確認。季度覆盤時會翻出來看一次,但那時早已在偏離的決策之上又疊了三個左右的後續決策。要讓願景成為“爭議的基準點”,比起撰寫,更重要的是每做一個決策就拿去與願景對照。而這項對照工作,若由人手工去做既枯燥又容易遺漏——正是適合交給 AI 的事。
本章把兩件事合在一起講。前半部分是把已經寫好的願景當作決策評分表來運轉的工作流——把作者專案中實際的 26 個決策 atom 放到 LLM 上,得到“違反願景槽位”的判定,並由人抓出其中一處誤判的一整個迴圈。後半部分回答的是這張評分表能覆蓋到誰的決策這一問題,也就是授權委派。領導力的一般論(願景為何重要、委派為何是成長的工具)在其他書裡已經講得夠多,本章只專注於把這些原則放到 AI 工作流中運轉的那一環。
先要理清“願景篩選決策”這句話。願景、路線圖、排期並不是一回事。它們的時間單位與變更頻率不同,而當這種區別崩塌時,排期壓力就會動搖願景。
| 層 | 週期 | 變更頻率 | 與願景對照的意義 |
|---|---|---|---|
| 願景 | 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(“不是什麼”)在檢查中出力最多。因為違反往往不是出在“決定要做的事”上,而是出在悄悄去做“決定不做的事”的地方。
拿願景去檢驗什麼?作者團隊把所有重要決策都以一張 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。
下面把一個完整迴圈從頭看到尾。輸入的提示詞可以原樣複製使用,輸出則是對真實會話的重現。
請把 26 個決策 atom 逐一放到下面的願景 5 個槽位上檢驗,判定每個決策相對於
願景屬於符合/補充/違反/無關中的哪一種。若為違反或無關,請用一行註明是哪個
槽位以及原因(尤其要仔細看槽位 5“不是什麼”),存疑的用“存疑”標出交給我。
符合的可以不寫依據。
只給表格(id | 判定 | 衝突槽位 | 依據 | 存疑),不要總評。
[願景 5 個槽位 —— 貼上 §19.1.1 全文]
[26 個決策 atom —— id·正文·依據]
提示詞有一半是在“強制判定格式”。若給它自由,LLM 會把 26 個全都含糊成“大體符合”。設定 4 級判定標籤、只對違反·無關強制附依據,以及留一個“存疑就交給人”的出口,才是這條提示詞的核心。在依據句中明確點名槽位 5,也是有意為之——因為違反出在“決定不做的事”上。
| 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 處裡有一處是誤判。
人再讀一遍 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 處讀到最後,好端端的決策就會以“違反願景”的名義被搬上會議,製造出無謂的爭執。
把上面的迴圈畫成圖留存下來,之後每個季度就能重複同一套流程。關鍵在於:LLM 的判定不會自動推翻決策。只把違反·存疑上報到人工關卡,而廢棄·更正·批准由人來做。
人經手的地方只有兩處。一處是乾淨地錄入願景與決策(最上方),另一處是把 LLM 判為違反·存疑的少數條目讀到最後並作出判定(中間的關卡)。中間那段枯燥的 26 個對照由 LLM 來跑。這與 §6.2 城市生成器中 lint 不自動廢棄違規、而只向作者關卡發出 alert(警報)的設計相同——機器挑出可疑候選,而殺還是留由人來定。
這裡自然會冒出一個問題。這 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 每季度都會被拿去與願景對照。
寫願景·授權這一章,很容易忍不住想放進“引入願景後會議時間從 90 分鐘減到 45 分鐘”“授權後總監的決策負擔從每週 30 件降到 5 件”這類表格。這類數字若未經驗證,反而會削弱本書的可信度。本章的數字只按以下三種方式之一來處理。
第一,能數的就按實測來寫。 decisions/ 的 atom 目前是 26 個(以 2026 年 5 月的實測為準)。從 LLM 初步判定上報到人工關卡的件數、其中被更正為誤判的件數,都是可由會話日誌計數的實測值。上面的實操記錄中,2 件違反判定裡有 1 件(D0019)是誤判,這也是真實會話的結果。
第二,效果只談方向。 “半天的對照縮減為少數幾處的評審”講的是結構的方向,而不是絕對時間。確切能省下多少時間會隨決策數量、團隊規模、atom 正文長度而變,所以應把它當作“手工過 26 個”與“LLM 初步判定 + 人工關卡”之間的結構差異來讀。會議時間、士氣分數這類結果指標不會由願景一項決定,因此不對因果下定論。
第三,只承諾可測量的東西。 這套工作流實際可測量的是——每季度納入願景檢查的決策數、通過人工關卡的件數、誤判率(LLM 判為違反、被人更正為符合的比例)、atom 固化遺漏件數(已授權下放卻因沒有 atom 而漏出檢查的決策)。這四項在會議上可以用數字而非“感覺”來講。尤其是誤判率,它每個季度都用數字證明為什麼不能照單全信 LLM 的判定。
| 模式 | 為何失敗 | 對策 |
|---|---|---|
| 只寫願景,不拿去檢驗決策 | 願景淪為牆上裝飾,決策各行其是 | 每季度把 26 個 atom 放到願景上檢驗的 §19.1.3 |
| 把 LLM 的違反判定直接搬上會議 | 誤判(如 D0019)引發無謂的爭執 | 違反·存疑的條目由人把 atom 正文讀到最後 |
| 決策不留成 atom | 授權下放的決策整個漏出檢查之外 | 在事後上報(授權第 3 階段)中強制固化為 atom |
| 連 T4 熱修復也全部檢查 | 只增加數量,對願景幾乎無影響 | 把檢查範圍限定在 T1·T2 |
| 授權跳過 1→4 階段 | 自主決策在偏離願景的情況下不斷累積 | 分階段授權 + 季度檢查來事後覆蓋 |
第三項最常被忽略。越是能自主順暢運轉的團隊,越容易只在口頭上達成決策而不留下 atom。這樣一來,§19.1.3 的檢查只看得到已固化的決策,於是最自由做出的決策反而落入檢查的盲區。授權的自由,只有以固化 atom 為前提才安全。
遊戲之外的應用。 把願景拿去逐一檢驗每個決策,以及授權委派,並不只是遊戲團隊的功課,而是每一位管理者的工作。如果把部門的使命用一頁 5 個槽位(“我們做什麼 / 為誰 / 為什麼 / 怎麼做 / 不是什麼”)釘死下來,就能每季度用 LLM 對每週堆積的實務決策是否偏離這份使命做一次初步對照——尤其容易抓出悄悄去做“決定不做的事”這類違反。舉例來說,把團隊負責人授權下放的決策每季度拿到部門使命上檢驗,就能成為一張安全網,事後抓出自主做出的決策是否偏離了方向。不過,LLM 判為“違反”的條目不要直接搬上會議,其中一處可能是誤判,所以必須由人讀到最後。
一個人的話,做到這些就夠:沒有決策 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 的提示詞跑一遍;只要在這第一個迴圈裡抓出授權下放的決策中一處偏離願景的,這套工作流的價值就會立刻顯現。
主要讀者:每季度在會議上作出50件以上決策的總監·團隊負責人(中等規模(10\~50人)團隊) 面向個人/愛好者讀者的精簡版:§19.2.8「如果只有你一個人,做到這些就夠了」
我曾把一場會議順利開了90分鐘,一週之後,同一個議題卻又擺回了會議桌上。明明已經拍板,可是誰負責什麼卻哪裡都沒記下來。會議記錄裡只留下了"討論了全域性冷卻",而"定為0.5秒,負責人團隊成員A"這句,在當時在場者的腦海裡只過了一週就揮發殆盡。領導的會議崩掉的地方,大多不在會議進行中,而是在會議剛結束、決策尚未固化為記錄之前的那道短暫縫隙。
本章處理團隊負責人工作的兩大塊。前半部分是不必每次都從零開始解決衝突,而是把它送往按型別劃分的標準處方的方法;後半部分則是本章的脊柱——讓 AI 提取會議中產出的決策,但一旦缺少負責人(owner)或依據(rationale)就強制不予通過的實操記錄(worked transcript)。領導力的一般論(提出願景·傾聽·共情)在別的書裡已經講得夠多,因此本章只聚焦於把這套一般論跑成 AI 工作流、從而防止決策遺漏的環節。
以為零衝突的團隊才是健康團隊,這是一種誤解。一箇中等規模(10\~50人)的團隊每季度作出50件以上決策,卻一次摩擦都看不見,那不是沒有衝突,而是衝突沉到了水面之下,而沉下去的衝突更危險。
領導要做的不是消滅衝突,而是儘快對型別分類,把它送往標準處方。若同樣的衝突每次都以不同方式解決,那麼解決所花的時間每次都要從零重新累積。
| 衝突型別 | 衝突的本質 | 標準處方 |
|---|---|---|
| 價值衝突 | 對願景的不同解讀(營收 vs 使用者時間) | 引用願景槽位 |
| 事實衝突 | 對同一資料的不同解讀 | 核對資料(元遊戲報告) |
| 優先順序衝突 | "我的領域更重要" | 比較影響等級·KPI 影響 |
| 許可權衝突 | "這是我的決定" | 重新確認許可權矩陣 |
| 個人衝突 | 人際關係·溝通風格 | 一對一,事實/情緒分離(系統之外) |
前四種,處方都是引用系統。願景·資料·KPI·許可權矩陣一旦被明文化,決策的分量就從人的嘴轉移到系統一側,討論隨之變短。只有第五種個人衝突在系統之外——一對一,以及事實·情緒的分離,除了時間與真心之外的工具幾乎都不起作用。不過,"系統解不了"並不是領導可以撒手的藉口。系統解不了的領域,歸根結底也是領導的活兒,這正是此處的棘手之處。
分類不是每次從頭解一遍,而是轉成一條流程。
關鍵在右側的分岔點。若同一類衝突反覆出現,那就不是人的問題,而是系統的問題。那時,不去調解人,而是去修整願景·許可權矩陣這類規則。這將成為 §19.2.7 要講的季度覆盤衝突槽位的輸入。
正如衝突處方的四種全都是"引用系統",會議歸根結底也是一臺產出決策、並把該決策固化為記錄的裝置。領導在會議中要守的五條原則彼此相扣。哪怕只缺其中一條,其餘的也會跟著動搖。
這五條裡,第3·4·5條崩掉,就是開頭那起事故。決策用嘴說了(原則3部分滿足),卻沒有固化為記錄(原則4失敗),負責人也沒有錄入(原則5失敗)。於是一週之後,同一議題又擺了上來。
問題在於,若把原則3·4·5交給人的意志,它們會在繁忙的一週裡最先崩塌。會議一結束,領導就已經奔向下一場會議。因此,要把這三條原則搬到 AI 輔助流水線上。 也就是從會議記錄文本中自動提取決策,而一旦負責人或依據為空,就不讓它通過。這條流水線,是從領導視角再看一遍第17部分中建立的會議→會議記錄→atom 提取流程(§17.2)。
下面把實際怎麼跑的完整走一遍。舞臺設在作者專案(移動優先的 MMORPG,以下簡稱"專案A")的戰鬥 TF 會議剛結束之時。輸入提示詞可以照抄使用,輸出則是對真實會話的還原。
不把會議記錄整理得漂漂亮亮。發言彼此夾雜,是否算決策存疑的行也原樣保留——這樣粗糙的文本就是輸入。整理是 AI 該做的事,不是人先要做的事。
[2026-06-05 戰鬥 TF 會議記錄正文——摘錄,未經整理]
團隊成員A:全域性冷卻設為0.5秒的模擬結果很穩定。
團隊成員B:如果把恢復技能也一起綁進0.5秒,恢復迴圈可能會被打亂。
團隊成員A:那就單獨拿出來。恢復作為全域性冷卻的例外。
李旼洙:好,全域性冷卻統一為0.5秒,恢復作為例外。A 幫忙把資料表
cooldown 列統一過一遍。
團隊成員C:目標選取優先順序規則,下週再看看再定……
團隊成員B:小地圖縮小切換得和 UI 組一起看才行。先擱置。
李旼洙:好,那個放到下次會議。
這裡混著兩條決策(全域性冷卻0.5秒、恢復例外)和兩條擱置(目標選取、小地圖)。靠人用眼睛挑,總會漏掉一件。那就是開頭那起事故。
從附上的會議記錄中只提取"決策"。討論·擱置·資訊共享都不算決策。
每條決策填寫 decision(一句話)/ owner / rationale / follow_up 四欄,
如果在正文中找不到 owner 或 rationale,不要猜測,
寫上"[MISSING——會議中未確定]"。擱置·順延到下次會議的記為 deferred,
是否算決策存疑的行記為 ambiguous 交給我。輸出
只用 decisions / deferred / ambiguous 三個區塊。
請注意,這段提示詞有一半都在"強制空欄"。給 AI 自由,它就會把 owner 編得像模像樣,或把擱置項晉級為決策。留下一個"找不到就別猜、上報 [MISSING]"的出口,正是這套工作流的核心。決策要由人明確宣佈才有價值(§17.6.3 的原則),AI 只做到把"此處為空"這一事實暴露出來為止。
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 說得出"這個位置是空的"。
不能就這麼把這份輸出照單全收。冒出 [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 → 人補全決策 → 再次請求的一個迴圈,到這裡閉合。
這一圈,從結構上堵住了開頭那起事故。當決策只做了一半時,這個事實不是在會議結束一週之後,而是在會議剛結束的當場就暴露出來。
把上面的實操記錄疊到第17部分的會議記錄流水線之上,整幅圖景就是這樣。領導的手能觸及的只有兩處:在會議上宣佈決策之處(最前),以及補全 AI 上報的 [MISSING] 之處(中間)。兩者之間的提取·轉換·登記都是自動的。
在這條流水線裡,AI 不做的事更重要。AI 不製造決策。不編造負責人。不把擱置項晉級為決策。AI 所做的,僅止於從會議記錄裡挑出決策候選、並把空欄暴露出來。決策的宣佈與空欄的補全由人來做。這正是 §17.6.3 所說"決策槽位禁止 AI 自動生成"原則在領導視角下的應用——因為決策一旦傳播到別的文件·會話·構建中就會留下不可逆的痕跡,所以在入口關卡處,要保留由人明確宣佈的環節。
在公司 PC 的團隊共享 atom 中,有一個名為 team_equal_decision_culture 的概念 atom。它把覆盤中反覆引用的措辭固化下來,用一個詞指代"決策靠依據而非職位"的團隊文化。這種文化不是總監一句"我定了,就這樣"壓下去,而是每條決策都留下誰·為何,從而讓日後任何人都能以該決策的依據去回溯。
§19.2.3 的 [MISSING] 強制,正是這種文化的技術支撐。不讓負責人和依據以空欄通過,意味著決策的權威落在"因為它出自正文的某句發言",而非"因為總監說了"。依據引用一旦為空,決策就會被攔下,於是靠職位壓出來的決策,在結構上無法成為 atom。
這種文化,與 §19.2.1 的衝突處方也一線相連。用引用願景解價值衝突、用資料解事實衝突、用矩陣解許可權衝突,全都是以記錄下來的依據、而非人的一張嘴來解決這同一條原理。平等決策文化是衝突處方的土壤,而 [MISSING] 強制,則是每逢一次會議就把這片土壤夯實、不讓它鬆動的工具。
在此之上,還疊加著團隊文化的另一根軸——公開與封閉的邊界。會議記錄·決策卡·KPI 資料·事故報告放在公開區域,而一對一談話·人事評價·薪資·個人情況放在封閉區域。決策提取流水線所處理的,全都在公開區域。個人衝突(§19.2.1 的第五種)之所以在系統之外,原因也一樣——它屬於封閉區域,不固化為 atom。
領導力這一章,很容易被誘惑去放一張"引入會議流水線後,會議時間減半"之類的表。這樣的數字若未經驗證,就會削弱本書的可信度。本書的原則有以下三條。
第一,只把可測量的東西承諾為指標。 會議流水線真正能數的是這些——每條決策的 owner·rationale 缺失件數(目標為0)、從會議記錄中提取的決策裡晉級為 pending atom 的比例、"這個之前不是沒定過嗎?"的重開會議件數。這三項,在會議中能用數字而非"感覺"來說話。
第二,作者的推測就寫明是推測。 會議剛結束時提取決策所花的時間——"手工整理會議記錄20\~30分鐘 → AI 起草 + 補全5分鐘以內"——是作者基於經驗的推測,是未經驗證的假設。不必去記絕對值,而應把它讀作結構差異("人從頭開始挑揀" vs "AI 提取 + 只補空欄")。準確的節省時間,會隨會議規模·決策數量而變化。
第三,不對因果下斷言。 不把"重開會議變少了"完全歸功於這條流水線。團隊成熟度·專案階段也在同時起作用。只說方向(決策遺漏若在會議剛結束時暴露,就會朝著重開會議減少的方向起作用),而不編造倍數。
衝突處方與決策流水線,會在季度覆盤中走一遍檢查迴圈。覆盤中設有"衝突槽位"和"決策遺漏槽位"。
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.3 的提示詞裡跑一次。只要 AI 冒出哪怕一條
owner: [MISSING]的決策,那就是你的團隊(或你自己)一週之後會再拿出來的議題。僅僅現在就把那個空欄填上,就能少掉一場重開的會議。
如果有團隊,就從下面這一步開始。把下一份會議記錄不加整理、原樣放進 §19.2.3 的提取提示詞,只啟用規則2([MISSING] 強制)。pending atom·JIT 登記(§17.2)是之後的事。哪怕只有"強制空欄"這一行,也能在會議剛結束時抓住"以為定了、其實沒記"這個最昂貴的遺漏。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| 用同一種方法解決所有衝突 | 哪一類都無法徹底解決 | 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] 強制,用一行就堵住這兩個失敗。
[MISSING] 攔下。主要讀者:需要決定是否給團隊引入AI、並向管理層說明其成本的主管(中等規模(10\~50人)團隊) 面向個人/業餘讀者的精簡版:§19.3.12「一個人的話,做到這些就夠」
我曾在CEO辦公室裡被問到這樣一個問題:"每月花在AI工具上的錢不少,那到底換來了什麼改善?"當時我手裡只有一張幻燈片,上面寫著"生產力提升3\~5倍"。CEO又追問:"這3\~5倍是從哪來的數字?"我答不上來。那個數字不過是我從某處看到的部落格平均值搬過來的,並不是我們團隊實測的值。
那天之後,我把AI引入報告裡所有加工過的數字全部刪掉。取而代之,我開始如實彙報系統實際留下的東西——積累了多少個atom(最小知識單元)、有多少個技能在執行、日誌裡哪些輸入會調出哪些上下文。本章討論兩件事。第一,把AI引入這一決策拆成從保守(人來決策、AI驗證)到激進(AI生成候選、人來採納)的分階段決策框架。第二,把這次引入的ROI不用部落格平均值、而是用我係統的實測日誌向管理層說明的方法。領導力的一般論述在別的書裡已經夠多,因此本章只聚焦於用AI來輔助"是否引入AI"這個決策本身,並把其依據從系統日誌中汲取出來的場景。
若把AI引入看成"引入/不引入"的二分法,從第一步就會走偏。一次性開啟五個工具,運營負擔會先於效果到來;因為害怕而乾脆不開,則永遠無法起步。引入是一種從低風險的位置起步、經驗證後再逐步擴大許可權的分階段決策。
貫穿全書的準繩在這裡同樣適用。由人來決策、AI只做驗證的保守應用,由AI探索候選、人來採納的激進應用。引入也遵循這一順序。從注入上下文(保守)起步,待驗證不斷累積後,再過渡到自動生成(激進)。若反向跳躍——不經驗證就先開啟自動生成——就會事故不斷,團隊則會要求關掉工具。
關鍵在於各階段之間的關卡。要邁入下一階段,前一階段的測量值(事故率·廢棄率·滿意度)必須通過基準。尤其是最後的階段4(角色演進)是不可逆的。這一階段會改變人的崗位、牽動招聘計劃,因此在前面的可逆階段完成驗證之前絕不觸碰。這一關卡結構,能防止被"聽說AI不錯"的氣氛裹挾、一次性跳到激進應用的事故。
假設已經決定引入。下一道關口,是審批這筆費用的管理層。在這裡,主管最常犯的錯誤,就是把"生產力提升N倍"這類沒有出處的數字塞進幻燈片。這種數字在第一個問題面前就會崩塌。
換一種做法。讓AI清點我係統實際留下的資產,並把它整理成ROI(Return on Investment,投資回報)幻燈片,但絕不許編造沒有出處的數字。下面把這一個迴圈從輸入到廢棄·再生成完整地謄錄下來(實操記錄=完整保留的真實操作過程記錄)。輸入提示詞可以直接複製使用,輸出則是對真實會話的還原。
先把無需編造、系統裡已經存在的數字彙總起來。公司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該估算的值。
用附上的 ai_adoption_inventory.yaml(引入1年後的實測資產)做一張給管理層看的
1頁ROI幻燈片。只用yaml裡有的數字,沒有的比例·倍數·金額
絕對不要編造。不要用"生產力提升N倍"這類說法,而要用可清點的事實(atom數、技能數、
JIT注入條數)。operating_cost是空欄,不要用數字去填,
留成"[需財務確認]",作為審批問題保留。給不出依據的主張
另行標註、交回給我。按CEO關心的順序(相比成本減少了什麼)排列。
輸出為1頁:1行摘要 → 實測資產表 → 審批問題。
值得注意的是,這條提示詞有一半都在講"禁止捏造數字"。LLM有很強的傾向,會用看似合理的數字去填補空欄。只用yaml裡的數字·空欄保持空欄·沒有依據就上報這三根釘子,正是這條提示詞的脊樑。
# 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面前。
若原樣接受這份輸出,那天在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"這句話,若不去看什麼被攔下、人又親手斃掉了什麼,就是空洞的。
上面那次會話中,活下來的數字(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_project和feedback這兩個atom就被自動拽進了上下文。公司PC的inject_atom.py也以相同模式運作——當輸入與_jit_manifest.json裡的regex匹配時,對應atom的正文就會被prepend到前面。能對管理層說"這就是我們買到的東西"的,是這樣的日誌,而不是倍數。
同樣是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"作為審批問題保留",而由人來敲定最終請求措辭,正是這種分離。
階段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。自動化解開的,不是人的時間,而是人該看見的訊號。就是這一個詞彙,把引入報告的基調從"裁員"扭轉為"角色演進"。
LLM成本在引入初期很低,隨著工具增多便會累積。因此先給每個工具設每月上限(cap),超出時設定提醒·審查流程。具體的月度金額會因團隊規模·模型·呼叫量而差異巨大,故本書不刊載絕對值——正如§19.3.2所見,那是要從財務那裡拿來填的空欄。彙報時重要的不是金額,而是存在上限、且超出會被上報的結構這一事實。
效果測量以季度為單位強制執行。只把可測量的東西作為KPI承諾。
| 可測量(承諾) | 測量方法 |
|---|---|
| atom·技能累計數 | 目錄計數 |
| JIT注入條數 | _injection_log.txt行數 |
| 廢棄率(量產關卡) | 稽核計數(§6.2.6的做法) |
| 工作時間節省 | 前後相同任務的時間戳比對(先記錄基線) |
最後一行是關鍵。要誠實地彙報時間節省,就必須在引入之前先量好基線。那天在CEO辦公室"4.2倍"崩塌的真正原因,就是沒有基線。引入前沒量過同一任務的耗時,引入後也就沒有依據說時間縮短了。測量始於引入之前,而非引入之後。
"先量基線"這話沒錯,但很抽象。審批者要在自己的環境裡親自去量,流程就得摸得著。這裡先釘死一件事。本書不提供"引入後快N倍"這類節省數字。數字得由你在你的環境裡親自量。本節是關於如何設計這項測量的食譜,而下一節(§19.3.8)是在作者環境裡只量了一個任務的示例,但連那個值也被標註為"推測·未經驗證"。
食譜的每一格所問的,如下。
最後彙報時,不用平均值,而是把中位數與樣本數·偏差一併寫上。"測量3次,以中位數為準"這一行字,會在"4.2倍"崩塌的那個位置救活你的數字。不隱瞞樣本偏少這一事實,正是誠實彙報的核心。
測量本身就是一項工作。想把所有任務都量一遍,就會被測量拖垮、結果什麼都量不成。只挑一個任務來量,正是§19.3.12動手試試的起點。
警告——本節所有數字都是推測值,而非受控測量。樣本少,任務條件並非每次相同,且有一部分基線是事後憑回憶校正的。因此下面的值只是展示"這種表長什麼樣"的結構示例,不得引用為你團隊的節省依據。你必須用§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: null和operating_cost_usd_month: null,是這個示例的良心。想把null換成數字的衝動——那正是§19.3.2中AI把空欄填成$4,500的那股衝動,無論是人還是AI,都必須同樣拒絕。
下面是審批者(或負責測量的主管)在自己的環境裡親自填寫、再呈給管理層的工作表。本書不替你填空欄——因為一旦填了,那就不是你環境的測量,而是作者的捏造。留著空欄拿去、親自去量,才是這張表的用法。
| 欄 | 填什麼 | 誰來填 | 示例(僅供結構,非取值) |
|---|---|---|---|
| 測量任務 | 可重複·邊界清晰的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的位置,到接過測量結果、整理成幻燈片文句為止。它不是產出測量值的位置。
到目前為止講的都是引入順利推進的情形。然而PD最害怕的既不是成本也不是安全,而是採納摩擦——團隊成員拒絕工具,或者裝了一次就悄悄棄用。本節以化名·一般化的案例,梳理這種摩擦的訊號與應對。這裡沒有數字。因為PD要判斷的不是"會不會發生拒絕",而是"何時抓住拒絕的哪種訊號、如何處理"。
先釘死一個前提。拒絕不是失敗,而是訊號。工具被拒絕,意味著它在那個位置不合適、或引入方式是單方通知、或跳過了驗證階段。把訊號當作資料而非事故來接收,連撤下都會成為下一次引入的資產(本節所有案例,都以像§19.3.5的決策atom那樣留存記錄為前提)。
| 拒絕訊號(可觀察) | 表面理由 | 真正原因(化名案例) | 應對 |
|---|---|---|---|
| 裝了工具,日誌裡卻沒有呼叫 | "太忙,還沒試" | 成員A:被強推到不契合自己工作流的位置 | 解除強制,把位置挪到他常做的1個重複任務上 |
| 拿到產出後又用手重做一遍 | "信不過AI的輸出" | 成員B:未經初期驗證就先開激進應用,出過一次事故 | 退回保守階段(人來決策·AI驗證),重新積累信任 |
| 一提工具就沉默或迴避 | (不作聲) | 成員C:角色演進以通知形式到來,被理解成"搶我的活" | 以1:1方式一起畫Before/After角色表(§19.3.5),轉為共識 |
三種訊號的共同點是,它們先出現在行動上,而非言語上。比起嘴上說"不太行"的成員,一聲不吭、呼叫日誌為0的成員更危險。因此,把採納看作JIT日誌·呼叫計數(§19.3.3)這類可觀察的訊號,而不是人的評價。找出日誌裡沒有呼叫的位置,是最快抓住拒絕的途徑。
若應對之後訊號仍未化解,就撤下工具。撤下不是失敗,而是§19.3.1關卡的正常運作。關卡攔下了未達標,所以才沒有放行到下一階段。撤下的判斷,看以下三點。
撤下時必須留下的,是撤下理由的記錄。若不把"在哪個位置、為何關掉工具X"固化為決策atom,下季度就會把同一個工具重新裝到同一個位置,重演同樣的拒絕。撤下不是關閉的行為,而是記錄的行為。
最好的應對,是在拒絕發生之前就減少摩擦。把上述案例的真正原因往上追溯,都彙集到引入方式的問題上。
| 摩擦原因 | 預防 |
|---|---|
| 一次性把多個工具強推給所有人 | 從1\~2名自願者、1個工具的試點做起(§19.3.1) |
| 未經驗證就先開激進應用 | 固定保守→激進的順序,先積累信任 |
| 以通知形式傳達角色演進 | 1:1共識 + 平等決策文化atom(§19.3.5) |
| 像強制考勤那樣檢查採納 | 用呼叫日誌靜靜觀察,把不用的位置挪一挪 |
關鍵在於,把採納看作對位,而不是命令。工具若精準嵌入成員真實的重複工作位置,就沒有拒絕的理由;硬塞進不合適的位置,再好的工具日誌也會歸0。PD判斷採納摩擦的依據,不是成員的意願,而是"工具是否恰當地擺在了他的工作位置上"。
按規模填空欄、估算引入工時·運營費的工作表,另置於附錄L(團隊引入TCO·上手工作表)。在把採納摩擦也減下去之後,再用附錄L把這次引入在團隊規模上要吃掉多少工時·成本,做成審批材料。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| "生產力提升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倍"這類沒有出處的數字,在第一個問題面前就會崩塌。效果不要用加工過的倍數,而要用系統實際留下的、可清點的東西——被自動化的任務數、標準文件數、日誌裡記下的呼叫條數——來彙報;沒量到的專案就如實寫"未測量",這樣反而更容易通過審批。比如會計團隊引入自動化工具時,要先把引入前同一任務的耗時量成基線(這才是關鍵),再與引入後比較,才能證明節省。引入本身也不要一次性全開,而應在低風險的位置邊驗證邊分階段擴大,運營負擔才不會先於效果到達。
一個人的話,做到這些就夠。沒有團隊記憶系統也沒關係。挑一件你最近用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部分討論了主管的三個領域。
| 章 | 核心 |
|---|---|
| 19.1 | 願景·路線圖與授權——決策的等級與授權的邊界 |
| 19.2 | 衝突·團隊文化與會議運營——達成共識的場所 |
| 19.3 | AI引入戰略與說服管理層——分階段引入 + 實測ROI |
貫穿三章的一句話是:主管的工作不是"做決定",而是"打造讓決定被測量、被達成共識的結構"。AI引入也不例外。當你從保守走向激進、逐級邁進,並且不加工其效果、而是從系統日誌中汲取時,引入就不再是氣氛,而成為資產。
下一部分(第20部分)講的是,這個主管領域如何以工具·基礎設施來實現。19.3中作為ROI單位使用的atom304·技能48·JIT日誌,在第20部分會進入運營它們的系統內部。
本章中的「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 月盤點時的實測值。
用「共享 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 的現場。
不可能每個會話都把 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 不是靠人手,而是系統對自身進行測量後打出來的。
這個迴圈是閉合的。atom 在覆盤中被引用得越頻繁,score 就越高;score 越高,就越容易出現在 CLAUDE.md 頂部和 JIT 清單裡;越容易出現,就越會被再次引用。這是一個常用的決定會 自己 浮上來的結構。反過來,六個月內引用為 0 的 atom,score 會沉下去,自然從視野裡消失。不需要人來判斷「這個現在不用了,撤下來吧」。
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 結束——即便記憶注入失敗,使用者的工作流也絕不會停下。「有就幫忙,沒有或壞了就悄悄退出」,這是這套系統的第一原則。
如果說到這裡為止是 atom(長期記憶),那麼接下來就是五人 team_memory(按人區分的短期·協作記憶)。我們把真實一個會話的開端,按輸入 → 動作 → 結果從頭跟到尾。舞臺是公司 PC,場景是團隊成員 C(化名,資料夾 팀원 C/)在自己座位上打開了專案A的工作資料夾。
團隊成員 C 像平常一樣開始工作。
繼續檢查昨天定的目標選擇系統初稿。
他一個字都沒寫自己是誰、該遵守哪些規則。因為根本不需要寫。
按照 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 ... ← 僅參考規約,禁止修改
팀원 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/。
這裡我(作為總監,或者團隊成員 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 只在覆盤達成一致後才改。所以哪怕四個人在同一份記憶上工作,也不會互相覆蓋對方的上下文。發現先彙集到個人筆記裡,只有通過覆盤這道關卡,才能升格為團隊共享規約。
用一張圖看完一個會話運轉的完整路徑。這是一個從識別開始、到在覆盤中固化收尾的迴圈。
右下角的分叉點是這套系統的心臟。個人發現的東西流向個人 notes,而影響整個團隊的規約·新 atom,只有通過覆盤關卡之後才能上升到 shared。一個人運營五個人份卻不衝突,原因就在這一道關卡上。而最後一步一定是 SVN 提交——因為沒有被固化的發現,會在下一個會話裡重新退回腦子裡。
這些是運營五人 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、而不是一次性的糾正,下次在別的使用者會話裡才會劃出同一條線。
就算沒有團隊,這套結構的八成也能由一個人原樣使用。把五個使用者縮減為一個資料夾即可。
rules/(已確定)和 rnd/(假設)。光是把經常搞錯的 5\~10 條規則固化成 atom,「之前不是定過嗎?」就會消失。notes.md。只要保留覆盤關卡就好——即興備忘寫進 notes,確定的規則經過覆盤再進 rules/。核心是「把決定從腦子裡搬到檔案裡」,無論五個人還是一個人,這個動作都一樣。
把一條經常搞錯的規則固化成 atom,並讓它通過 JIT 自動注入,試試看。
atoms/rules/ 資料夾,把最常重複解釋的 1 條規則寫成檔案。例:atoms/rules/xlsm_svn_update_before_edit.md。{"name":..., "regex":"xlsm|쿨타임", "score":100, "path":...},然後在提示詞裡輸入「쿨타임 수정」,確認 _injection_log.txt 裡該 atom 是否被記為 hit。如果被記上了,那條規則現在就不在你腦子裡,而在系統裡了。
週三午飯前後,團隊成員 B 通過團隊即時通訊工具發來一條訊息:"上週總監把戰鬥冷卻時間定為 0.8 秒,可我的筆記裡記的是 0.6 秒,到底哪個對?"我一時愣住了。0.8 秒是共享決策,0.6 秒是團隊成員 B 在自己的測試構建裡臨時試跑的值。兩者都記在"記憶"裡。問題在於,這兩個值混在了同一個格子裡。團隊成員 B 把自己的實驗值誤當成了公司決策,差一點就用錯誤的值去更新資料表了。
這起事故並非因為記憶裡沒有資料。恰恰相反,資料積累得很充分,卻因為沒有劃清哪個格子是共享格、哪個格子是個人格的邊界而出事。§20.1 鋪墊的賣點是五個人看到同一份事實(shared atom),而本章講的則是它的反面——五個人各自擁有獨立的格子。同一個櫃子,格子卻分兩種。而且,如果不用工具強制區分這兩種格子,上面那起 0.6 秒事故就一定會發生。
專案A 的 team_memory/ 分成五個人的格子。包括本人(leeminsoo)在內的團隊成員 A、團隊成員 B、團隊成員 C,以及 shared。前四個是各使用者的個人格,最後一個是所有人都開啟的共享格。
四個個人格塗成藍色,一個共享格塗成橙色。顏色不同,是因為訪問規則不同。藍色格子只有本人和總監能開啟,橙色格子則由全員開啟。0.6 秒事故的根源在於:團隊成員 B 本該寫進自己藍色格子的實驗值,卻不加區分地統稱為"記憶",當成共享決策來對待。把格子從物理上分開——也就是分成不同目錄——至少能憑"寫在了哪裡"這一點,得到區分兩者的線索。
這裡的關鍵不是有兩個資料夾,而是每個格子都附帶各自的規則。放進 shared/ 的是公司決策,人人都讀。放進 團隊成員 B/ 的是那個人的工作情境,只有本人和我讀。同樣是 0.6 秒,放在哪個格子裡,就決定了它是"實驗中"還是"已決策"。
開啟某個使用者的格子,會看到兩個檔案:context.md 和 notes.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,就是很大的差別。
把個人格和共享格分開,並不算完。最棘手的是個人格里的某些內容必須晉升到共享格。假設團隊成員 C 把這樣一條失誤記在了自己的 notes.md 裡:"資料表 import 時,如果 enum 順序錯亂,就會在執行時悄然出錯"。這雖是那個人的個人記錄,但只要團隊全體都知道,就能避免同樣的失誤。可也不能把整個個人 notes.md 都共享出去——那裡面還混著工作風格、卡殼時的情緒、與其他成員的衝突之類的東西。
所以在個人 → 共享之間,必須有一道關卡。覆盤就是那道關卡。寫覆盤時,先過濾一遍"這周我經歷的事情裡,有哪些是團隊該知道的",只把過濾出來的內容晉升為 shared/ atom。流程如下。
關卡的判斷標準有兩條。第一,是否對團隊有用。不是個人喜好,也不是那天的狀態。第二,是否移除了個人資訊。不是"團隊成員 C 又在 enum 上出錯了",而是"給資料表 import 時的 enum 順序加一道校驗",只留下事實。只有通過這兩道關口的內容才會進 shared/。沒通過的,就原樣留在個人格里。
沒有這道關卡,就會以兩種方式之一崩掉。關卡太鬆,個人資訊就會漏進共享格,發生 0.6 秒事故的反面——私人備忘被暴露給全員——的事故。關卡乾脆沒有,團隊成員 C 的 enum 教訓就會被困在他自己的格子裡,團隊成員 A 重複同樣的失誤。把覆盤當作關卡,就能把"個人資訊洩露"和"學習資產化"這兩件正好相反的事,用一個動作分開。
我實際使用的晉升提示詞很短。覆盤一結束,我就丟擲這樣一句。
"從我這周的 notes.md 裡,只挑出對團隊全體有用的條目。每條都去掉人名和具體的失敗情形,重寫成只保留'做什麼、怎麼做'的 atom 候選句。個人情緒、風格、衝突類的備忘從候選中排除,並用一行標註排除的理由。"
最後一句——讓它寫出排除理由——很重要。AI 展示出自己沒有上報什麼,人就能檢查這道關卡是不是太嚴,以至於連真正的教訓都擋住了。
關卡並不總是乾淨利落地運作。實際上,AI 一次答不對、人拒絕、再讓它重做,這樣的來回是會發生的。下面原樣搬來一次真實的來回——把團隊成員 B 一週的 notes.md 往 shared 上晉升的過程。
[提示詞 —— 第 1 次]
"下面是團隊成員 B 這周的 notes.md。只把對團隊全體有用的條目挑成 shared atom 候選。去掉人名和具體的失敗情形,只留下'做什麼、怎麼做'。
- 週二/0.6 秒冷卻時間測試,輸入被擠掉。遵從 0.8 秒共享決策
- 週三/團隊成員 A 寫的技能 enum 和我的 import 衝突。直接跟團隊成員 A 說有點彆扭,就通過總監轉達
- 週四/入口網站 View_skill_table.html 一重新整理篩選器就被重置。煩
- 週五/狀態不好,下午請半天假"
[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 條一開始差點帶著人名上到共享格這個事實。而那份風險,正是這道關卡存在的理由。
即便分好了格子、立起了關卡,如果每次對話都要手動展開某個格子,運營就會很重。所以最後一塊拼圖是讓契合對話情境的格子自動展開。在本人的 PC 上,這件事由 UserPromptSubmit 鉤子(inject_memory.py)來做。它只挑出與輸入語句匹配的格子,注入到上下文裡。
規則很簡單。討論遊戲決策時,shared/ atom 會展開。準備與某位團隊成員的 1:1 時,那個人的 context.md + shared 會一起展開。寫季度覆盤時,專案記憶 + 總監本人的格子會展開。寫對外報告時,總監的格子 + 部分 shared 會展開。展開哪個格子,就是記憶的介面。
這裡,格子的分離再次顯效。準備 1:1 時,團隊成員 B 的個人格會展開,而團隊成員 C 的個人格不會——因為與當下對話無關。格子若不分開,每次就會全部展開、淹沒在噪聲裡,更糟的是,在 1:1 的場合會牽扯出無關人員的個人備忘。分離既是安全,也是注入的準確度。
就算格子的結構立起來了,還剩最後一個陷阱。我在家裡的 PC 和公司的 PC 之間往返,記憶通過雲端資料夾同步。這時,如果兩臺 PC 同時修改同一個格子,就會發生衝突。一方把另一方整個覆蓋掉,那一天的 notes.md 就沒了。
處方按格子單位而不同。頻繁更新的個人 notes.md 放進 git 這類可合併的倉庫,衝突時把兩邊合併。穩定的 context.md 或 shared/ 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 裡,只挑出下次還值得再看的一行"來過濾自己的筆記,易失的備忘和已資產化的教訓就會分開。等人數增加到兩個的那一刻,再把格子拆開就行。
週四傍晚,在提交構建之前,客戶端程式設計師成員 B 在公司內部聊天裡發了一條訊息:"上週戰鬥 TF 上,我們把全域性冷卻常量定為 0.8 秒,對吧?記在哪份文件裡了?"5 分鐘後,策劃成員 A 回覆:"會議記錄裡應該有……我在找。"又過了 7 分鐘。"git 的哪個資料夾來著。"
這段 12 分鐘的往返,並不是因為資訊缺失。資訊確實存在。它寫在 atom 檔案裡、會議記錄裡,也寫在決策卡里。只是這三者被放進了不同的抽屜,而開啟每個抽屜的方式各不相同。問題不在抽屜,而在開啟抽屜的把手。
本章講的就是把這些把手合併成一個。它不是從零自研全棧,而是在已經堆積在資料夾裡的策劃產出物之上覆蓋薄薄一層 Web,讓成員在瀏覽器位址列裡只敲 portal 一個詞就能進入。核心工具只有三個:用 Python 啟動搜尋 API 的 FastAPI、架在它前面的 nginx,以及讓服務在無人關閉、PC 開著的整段時間裡持續存活的 nssm。
策劃產出物本來就是分散的。這不是有意打散,而是因為每份產出物都落在最自然的位置上。atom 落到 git 倉庫的 Markdown 裡,日程落到任務管理工具裡,即時對話落到聊天裡,KPI 落到獨立的儀表盤裡。各自待在各自的位置上,這沒有錯。問題在於,這些位置需要人在腦子裡存成一張地圖。
對新入職者來說,這張地圖本身就是進入的門檻。要找"全域性冷卻值",就得(1)判斷它到底是決策卡、atom 還是會議記錄,(2)開啟對應的工具,(3)再用那個工具的搜尋語法去查詢。這三步都是從經驗中來的隱性知識。
門戶的想法很簡單。產出物仍舊留在現在的位置。只是在它之上疊一層用於搜尋的索引,再把索引通過瀏覽器暴露出來。不是擺七張桌子,而是擺一張帶七個抽屜的桌子。抽屜照舊,但人只需坐下一次。
下面是筆者在專案A中實際運營的門戶結構。它不需要額外的伺服器裝置,在策劃團隊的一臺公用 PC 上以始終開啟的狀態執行。
圖中用灰色框起來的下半部分,是原本就已存在的產出物;門戶新增的,只是上半部分薄薄的三層——索引、FastAPI、nginx。這是一種不觸碰產出物、只新開一個入口的結構。
門戶的實體,由五個小檔案就能構成。逐個來看,它們各自只做一件事。
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 一個,而且它一旦註冊好,就再沒機會重敲。
把這四個部件的分工一眼看下來,是這樣的。
光靠部件說明,門戶的價值還不夠真切。讓我們把前面吃掉 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 分鐘根本不必再花。
做門戶的方式大致有三種。從零自研全棧,或引入 Notion·Coda 這類外部一體化工具,或像現在這樣在基礎工具上疊一層薄薄的自動化。筆者選了第三種,而這個選擇的依據在於中等規模團隊這一規模。
全棧自研的自由度最高,但做完之後必須持續維護這個 Web 的負擔,會比效果更早到來。認證、部署、DB 遷移之類的運維勞動,會落到策劃團隊頭上。外部一體化工具很快,卻要按月訂閱,而且最要緊的是,還得把堆在 git 裡的 Markdown 產出物再遷移成那個工具的格式,這筆遷移成本躲不掉。相比之下,FastAPI+nginx+nssm 的組合,把產出物留在原地、只疊一層索引,因此幾天就能執行,維護也不過是偶爾修一修 build_index.py 的程度。
下面是筆者在專案A中,門戶引入前後所感受到的變化。表中的數字並非精密計量,而是筆者的估算(未經驗證),應當讀方向與比例,而非絕對值。
| 條目 | 無門戶 | 運營門戶 | 方向 |
|---|---|---|---|
| 單次資訊檢索耗時 | 數分鐘 | 不足 1 分鐘 | 大幅縮短 |
| "這個在哪"的提問頻率 | 頻繁 | 稀少 | 減少 |
| 新成員適應工具 | 兩週上下 | 幾天 | 縮短 |
| 會議記錄·決策卡的登記率 | 半數左右 | 大多數 | 上升 |
最後一行最為本質。資訊一旦變得易於查詢,快起來的就不只是搜尋,連留下資料這一行為本身的動機也會提升。"反正也搜不到,寫會議記錄幹嘛"這樣的冷嘲,會轉變為"寫了就能被搜到,所以寫"。門戶既是搜尋工具,同時也是誘導記錄的裝置。這一良性迴圈,創造出超過把一兩個工具拼起來的價值。
不過這份平衡取決於團隊規模。當團隊超過 50 人、產出物膨脹到數萬份時,子串搜尋的侷限與單臺 PC 服務的侷限會同時顯現。到那個時點,全棧自研或引入搜尋引擎才被證明為正當。眼下這套結構是"適合中等規模團隊的解",而非所有規模的正解。
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,確認在無人開啟任何東西時門戶是否仍然存活。
即使沒有團隊,這套結構照樣有用。因為獨自工作的人,自己的產出物也一樣會分散。在 setup 裡用本人 PC 代替公用 PC,nssm 註冊也可以省略(只在需要時用 python portal_run.py 啟動)。prompt 同樣拿到 build_index.py、server.py 和 View_search.html 三樣,只是去掉 team_memory 部分,只索引 atom·決策·會議記錄。verify 有確認 index.json 條數和搜尋一次就足夠。核心是一樣的——產出物留在原地,只新開一個搜尋入口。
週二上班後不久,9 點 12 分。我還沒開啟協作工具的看板,就先在 Claude Code 窗口裡敲下一行字。
顯示本週未完成的 P0 任務,按截止臨近程度排序
停頓了大約 3 秒,答案就出現了。我沒有直接開啟協作工具,沒有翻找儀表盤標籤頁,也沒有給負責人發即時訊息。可是一條已經逾期一天的任務被排在了最上面。我這才打開協作工具,只核對了那一張卡片。
這 3 秒是如何造出來的,就是本章的全部內容。關鍵在於,我們並沒有更換工具。專案A 團隊仍然使用協作工具(本專案用的是 ClickUp —— 一種管理任務與日程的 SaaS,JIRA、Redmine、Linear 也處在同樣的位置)。無論協作工具是什麼,本章的流程只需替換工具名稱就能照搬。資料表同樣放在 SVN 裡,決策卡也同樣放在門戶裡。改變的只有一點:LLM 現在能夠親手開啟並檢視這些工具了。這一連線的標準就是 MCP(Model Context Protocol)。
打個比方,這不是在前臺再多安排一名新人,而更像是把現有資料室的鎖,也為 LLM 開啟。資料室沒有變。只是多配了一把鑰匙而已。
MCP 是 LLM 訪問外部工具與資料的標準協議。"標準"二字是關鍵。它不是為協作工具單獨造一個介面卡、為文件單獨造一個、為 git 再單獨造一個,而是在 JSON-RPC 這一個約定之上,每個工具把自己暴露為"伺服器",LLM 則作為"客戶端"向該伺服器發起對話。
結構分三塊。
伺服器會暴露一份"我能做的事"的清單。若是協作工具伺服器,就是 search_tasks、get_task、update_task 這樣的函式。客戶端(LLM)接收使用者的自然語言,從中挑出合適的函式來呼叫,再把返回的 JSON 用自然語言解釋出來。當我說"顯示 P0 任務"時,LLM 所做的只是呼叫了 search_tasks(priority=P0, status=open) 而已。
這裡要先講清楚一點。本書不講怎麼親手編寫 MCP 伺服器。像 ClickUp、JIRA、GitHub 這樣,已經有官方或社群 MCP 伺服器的工具佔絕大多數,所以策劃要做的是"安裝並連線伺服器",而不是"實現協議"。協議本身由 Anthropic 於 2024 年 11 月作為公開標準釋出,規範公開在 modelcontextprotocol.io 上。
光靠嘴說"這會自動完成",聽起來像魔法。我們把實際的一次操作從頭跟到尾。下面就是這樣一段實操記錄(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_memory 的 shared 槽位,這樣從下一次會話起 LLM 就會自動應用(參見 §20.1·§20.2)。
這一次來回說明的事很清楚。MCP 是準確取回資訊的工具,而不是替人做判斷的工具。資料是自動的,定義由人給出。一旦模糊了這條邊界,自動化就會變成事故。
僅僅是查詢協作工具,頂多是"檢索變方便了一點"而已。MCP 成為協作系統,是在把多個工具串成一條流程的時候。下面按流程來看專案A 中實際執行的五種模式。
模式 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 分鐘,而是直接從正題開始。
五種模式的共同點只有一個:只有在減少留給人手的重複勞動的地方,價值才被回收。 為了顯得酷炫而加上的模式,只會增加運營負擔。
第一天就把五個 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,就能在每週會議前自動拉取外包看板的進度,提前篩出疑似延期的任務。會議不再從"現狀同步"開始,而是從"決策"開始。不過,越是對外共享的工具,下一節的許可權·洩露陷阱就壓得越重。
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,過濾敏感資料,給呼叫設上限"。
這是專案A 在運營 MCP 前後體感到的變化。下面的時間數字是作者的經驗估計(未經驗證),請不要當作絕對值,而應作為方向與比例來讀。
| 事項 | 無 MCP | 運營 MCP | 方向 |
|---|---|---|---|
| 日報資訊彙總 | 手動 30\~60 分鐘 | 自動 \~5 分鐘 | 大幅縮短 |
| 工具間資訊同步 | 人工手動 | 自動 | 消除手動 |
| 1:1 事前準備 | 10\~15 分鐘 | 自動摘要 \~3 分鐘 | 縮短 |
| 外包進度掌握 | 只靠會議 | 即時查詢 | 常態化 |
| 決策 ↔ 任務關聯 | 手動 | 雙向自動 | 防止遺漏 |
最大的回收來自"收集資訊的時間"。決策與判斷依然是人的工作。MCP 減少的是它前面那一段 —— 翻找散落的工具、把資訊彙集到一處的簡單勞動。本章開頭的那 3 秒,正是這個位置。
第 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)則是把這一切與協作工具、文件連線起來的線路。任何一項被單獨拆走,其餘各項的價值也會減半。
下一部分講治理與運營。把工具接到這個程度之後隨之而來的安全性、成本、版權、倫理問題,會把本章的這些陷阱提升為團隊層面的規則來梳理。
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_memory 的 shared,讓它在下一次會話自動應用。
如果你是既沒有團隊、也沒有協作工具的單人開發者,MCP 的第一個物件是 GitHub 與本地文件。把 GitHub MCP 以 read-only 接上,從"本週未關閉的 issue,按由舊到新排序"這類查詢開始;不用決策卡,而是用文件 MCP 查詢本地 decisions/ 資料夾裡的 Markdown。自動報告(模式 1)照樣適用 —— 只需把傳送目標從團隊即時訊息換成自己的筆記檔案即可。許可權·成本陷阱對單人也一樣存在,所以最好從一開始就守住 read-only 起步和呼叫 cap。
週五傍晚6點40分。正準備下班合上筆記型電腦時,我發覺那天做的工作有些似曾相識。我找出並修好了資料表裡斷掉的enum引用,可上週分明也修過同樣的東西。再上一週也修過。每次都要重新敲一遍相同的提示詞,每次都要在Claude的輸出裡核對相同的條目。直到合上筆記型電腦之前,我才意識到這已經是第三次了。
這種"有些似曾相識"的感覺,是全書中最重要的一刻。若放任這種感覺溜走,下週就會第四次重複同樣的工作。若抓住這種感覺並寫成一行字,這一行字下週就會變成一個skill,這個skill一個月後又會變成一個atom(最小知識單元)被自動注入。而抓住它的地方,正是覆盤。
本書的其他部分講的是"有這樣的工具""有這樣的模式"。而本章講的是,所有這些工具與模式究竟從何處萌發。新的斜槓命令在哪裡誕生,新的atom如何被固化,一個月都用不上一次的工具由誰來剔除。答案總是匯聚到同一個地方。在覆盤中。
覆盤並不是什麼隆重的儀式。我們直接開啟作者的一份日覆盤檔案來看。實際路徑是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的篩選與人的篩選雙雙起了作用,最終只留下了一個真正的候選。
哪項工作在重複,哪件工具用得頻繁,哪個atom還欠缺——這些靠單次工作是看不出來的。在上面的實操記錄裡,enum斷裂之所以浮現為候選,並不是因為"今天",而是因為把"週二和今天"疊在一起看。只有把1周、1個月、1個季度累積下來的痕跡疊起來,模式才會浮現。覆盤,就是刻意製造這種疊合的時間。
覆盤中發現的模式會分成兩支。
這種分野的判斷,在工作進行途中是做不了的。因為它會打斷工作的連貫性。在修復enum斷裂的那一刻,根本沒有餘裕去琢磨"這是不是第三次了"。單獨騰出來的覆盤時間,才是做這種判斷的地方。
工具做出來之後是否真的產生價值,也在同一個地方衡量。一個月才用一次的工具,和能省下一個小時的工具,價值是不一樣的。衡量也好,廢棄決定也好,都在覆盤裡做。沒有覆盤,工具就只會不斷累積而得不到整理。幾年下來,幾十個沒人用的工具會妨礙檢索與運營。
用抽屜來打比方,覆盤就是定期清空書桌抽屜的時間。如果每天都用的筆,和一年都沒拿出來過一次的便籤,混在同一個格子裡,那麼每次找筆都要多花幾秒鐘。工具也是一樣。
作者的覆盤按三個層級運轉。日覆盤收集模式的種子,周覆盤把種子歸攏、壓縮為工具候選,月覆盤則評估工具的經濟性,或將其固化為資產,或將其廢棄。每一層都把其下層的輸出作為輸入。
最後一根箭頭閉合了這個迴圈。被永久資產化的模式,會通過JIT(Just-In-Time,即時)鉤子(hook)自動注入到下一次工作中。在作者的環境裡,有一個名為inject_memory.py的hook,每當接收到使用者輸入時,就挑選相關的atom塞進去。一旦enum-grade校驗被固化為atom,下次輸入"資料表校驗"這類內容時,那個atom就會自動跟上來。人不必每次都去記"對了,還有那條校驗規則"。
一旦缺了覆盤,就只剩下自上而下的箭頭,而資產迴流到工作的那根最後的箭頭會斷掉。迴圈無法閉合。所謂自我改進(self-improving),它的含義正是這個迴圈仍在轉動。工具改進工具自身,atom增生出atom。而它的動力,就是人特意騰出來的那一個小時的覆盤。
以作者在某個MMORPG專案上把覆盤運轉約半年的體會來看,一次覆盤會萌發出以下五類產出。下面的頻率並非精確統計,而是作者運營中的體感(作者估算·未經驗證),並非每次覆盤都會產出全部五類。若以季度為單位來看,這五類都會各出現一次。
把這五類展開來說是這樣。如果本週把同一個決定重複了兩次以上,那就是新atom候選。如果一週內多次重新輸入同一種提示詞模式,那就是新skill候選(上一節的enum-grade校驗就屬於這種情況)。本週用過的skill裡,如果有結果不盡如人意的,那就是改進現有skill——調整提示詞·增加校驗·標準化輸入。上個季度做的atom裡,若某個在一個月內匹配次數為0,那就是廢棄候選。不用它,就只是白佔token。最後,經濟性評估是指:按工具逐一權衡使用頻率與所節省的人工,來決定保留·改進·廢棄。
這五類在同一個畫面裡如何排布,用一個矩陣來看是這樣。橫軸是"是否重複",縱軸是"是否有價值"。
覆盤所做的,歸根結底就是把這一週的工作撒進這四個象限。落在右上的會變成工具,落在右下的會被剔除。這種分類,就是self-improving的實際運作方式。
我們接著走完這樣一段過程:上一節的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.1裡那個週五傍晚,差別不過在於:把"有些似曾相識"寫成一行,還是任它溜走。寫下來只要5分鐘;不寫而損失的時間,則會以第四次·第五次重複的形式累積起來。這是典型的因捨不得花小時間而損失更大時間的情形。
當然,沒必要一開始就搭建一套隆重的覆盤系統。在大團隊裡,從每天5分鐘的覆盤起步,或許會讓人覺得憋悶。但若一上來就把日·周·月三層系統一次性鋪開,又容易只顧追著形式走而丟掉本質。讓在最小的階段親身體會到覆盤價值的人,再把它拉昇到下一個階段——這樣的順序才穩妥。
本書中遇到的所有工具·atom·模式,歸根結底都是從某個人的覆盤中萌發出來的。說這本書本身就是作者半年間累積的覆盤的產物,也毫不誇張。說覆盤是起點,並不是一句比喻——它指的是,本書的目錄本身,就是從覆盤里長出來的這一事實。
遊戲之外的應用。 用一行字抓住"有些似曾相識——這個我上週不是也做過嗎"這種感覺的覆盤,不只對遊戲開發,而是對任何有重複性工作的職場,都是自我改進的入口。在結束一天時,只寫一行"今天我用手做了兩遍的是什麼事",到了週五再把這一週的五行疊在一起看,那些每天都藏著的模式,就會在一週的尺度上顯現出來。比如,行政總務人員如果在覆盤裡抓住"每週都要重新敲一遍同一格式的郵件",那麼這一行下週就會變成郵件模板,一個月後就會變成自動傳送規則。關鍵不在於工具有多精巧,而在於把痕跡疊起來看這一行為本身,以及:讓AI挑出候選,但同時掛上"不要硬湊、已經自動化的就排除掉"這道篩網。
這是首次引入覆盤迴圈的最小版本。幾乎不用安裝任何工具就能開始。
setup
retro/daily/資料夾。retro/daily/2026-06-06.md。除此之外不需要任何準備。prompt
每天結束工作時,把當天的工作日誌交給AI,像下面這樣發問。
我今天做的事是[工作1·2·3]。其中如果有本週重複過2次以上的工作就標出來,如果有值得做成工具(skill)的重複模式就單獨挑出來。已經完成工具化的工作請從候選中剔除。沒有就請說沒有。不要硬湊。
最後兩句("已經工具化的要排除""不要硬湊")就是篩網。少了這個,AI每次都會過量生成看似合理的候選,一個月後就會堆起一批沒人用的工具。
verify
grade列的比對那樣)。同類就確定為候選,不同類就廢棄。重複: [工作] (N次) → skill候選,周覆盤時判斷。單人精簡版
如果既沒有團隊、也沒有額外工具,一個人起步,那就這樣精簡。
關鍵不在於工具有多精巧,而在於把痕跡疊起來看這個行為。一天會藏起模式,一週會顯露模式。抓住這份顯露的5分鐘,就是自我改進迴圈的入口。
週一早晨,我把上週的五份日覆盤一併顯示在同一螢幕上,正準備開始新的一週。週二的覆盤裡寫著"資料表 export 之前忘了做一致性檢查"。週四的覆盤裡也有幾乎一模一樣的一句。而就在那個週一上午,我又在做同一件事。我把 FK 已損壞的表原樣提交到客戶端/伺服器構建裡,然後又撤了回來。這已經是第三次了。
這一刻正是覆盤系統的核心。你正在第三次做同一件事——這個事實,在你做那件事的當下絕對看不出來。因為手在熟練地動作,腦子裡則低聲說著"這本來就是我一直在做的事"。重複只有把痕跡攢起來、事後回看時才顯現。覆盤是收集這些痕跡的裝置,而 atom 晉升則是把在其中發現的重複固化下來、讓你再也不必用手去做的裝置。
本章將完整追蹤這兩個裝置如何相互咬合運轉,以及一份真實的日覆盤檔案如何變成 JIT manifest 中的一行 atom。
首先有一個必須點明的前提:重複無法被即時察覺。
遊戲策劃的一天是決策的連續。某個資料表列該用哪種 enum、技能冷卻時間該以秒為單位還是以幀為單位、會議上得出的含糊共識該記在文件的哪個位置。這些決策每一個都太小,留不下記憶。可是如果同一個決策一週要做三次,那它就不再是決策,而是規則了。既然是規則卻每次都重新做一遍,那就是浪費。
問題在於,這種浪費看不見。所以要留下痕跡。每天 5 分鐘,把今天做過的事、今天重複了兩次以上的事,各寫下一行。一週過去,五份痕跡積累起來,到那時才會看見"咦,這個寫了三次啊"。
這正是覆盤與單純日記的分界點。日記記錄感想,覆盤則為提取模式而記錄痕跡。因此覆盤的格式必須固定。格式每次都不一樣,就無法把五份並排比較;無法比較,就看不出模式。
把覆盤分成日、周、月三個週期,原因不在於時間的流逝,而在於每個週期所做的事在根本上不同。
日覆盤固化痕跡。不做判斷,只是記錄。周覆盤把五份痕跡合起來觀察模式。在這裡,"這是重複"這樣的判斷第一次介入。月覆盤則縱觀積累下來的全部工具,評估其經濟性,決定留下什麼、捨棄什麼。
缺了任何一個週期,其餘的都會崩塌。沒有日覆盤只做周覆盤,一週前的事已記不清,痕跡就空了。沒有周覆盤只做月覆盤,就要一次性面對一整月的日覆盤,而把 22 份放在一處比較幾乎是不可能的。模式看不出來,只會徒增疲憊。
用工作間來比喻很貼切。日覆盤是每天傍晚整理桌面的 5 分鐘。周覆盤是週末重新歸置一格抽屜的 30 分鐘。月覆盤是每個季度審視整個工作間動線的兩個小時。每天不收拾桌面,週末就沒法歸置抽屜;抽屜一團亂,再看動線也理不出頭緒。
我實際使用的日覆盤檔案,會按日期堆放在 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 裡單獨維護著"下一次會話優先確認"這一條目,它正是日交接累積而成的上層版本。
周覆盤從把五份日覆盤放上同一螢幕開始。檔案堆放在 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- 會在實際工作中執行一週、長則一個月。真的每次都有用就存活下來,一次都沒用上就悄悄刪除。
月覆盤是把一整月的累積攤開、檢查全部工具健康狀態的場合。檔案按月堆放,如 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 次以上就先留下,不足則轉入廢棄候選。時間節省是把每次節省的體感乘以頻率——這裡不去斷言以分鐘為單位的數字。"一次感覺省幾分鐘,一個月用十次,累計起來就很可觀"這種程度的定性判斷才誠實。認知負擔方面,若要記的斜槓命令超過十二個,就視為該整理的訊號。人腦能同時扛著的命令數是有上限的。維護成本要看這個工具是否在資料表變動時得跟著一起改。可替代性要看是否新出現了更簡單的方法。
把五把尺子合起來,決定留下、捨棄還是修改。一個小時就能把三十到五十個工具過一遍。不過開始頭一兩個月工具還少,評估用不了一小時。那個階段,與其評估,不如把更多時間花在催生新工具上。經濟性評估是要等工具積累起來之後才真正發揮作用的安全裝置。
從這裡開始是本章的脊柱。所謂實操記錄,就是完整保留的真實操作過程記錄;下面從覆盤時點起,一路追蹤前面散落的碎片如何真正從一行開始,直到在所有會話中自動注入。途中我拒絕的瞬間、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。
三週前寫在日覆盤裡的"這個又做了一遍"那一行,如今變成了會自動攔下當前工作的規則回到了我面前。曾經用手做的驗證,再也不必用手去做。所謂閉環,說的正是這一幕。
把上面的實操記錄壓縮成一張流程圖,就是下面這樣。發現發生在日覆盤,驗證由執行期完成,晉升由測量決定,而資產化則由 manifest 收尾。
最後那條虛線就是這張圖的全部。被自動注入的 atom 又暴露出新的重複,那重複再次進入覆盤,催生下一個 atom。每轉一圈,要用手做的事就少一件。這個迴圈累積上半年到一年,覆盤就不再是日記,而成了工作系統的大腦。
晉升迴圈裡最脆弱的一環是第 1 步——寫下"這個又做了一遍"的那一刻。忙的時候,人會把覆盤欄留空就跳過。這樣就不留痕跡,沒有痕跡周覆盤裡就看不出模式,沒有模式就誕生不了 atom。迴圈的入口被堵住了。
所以我的環境裡有一個叫 retro_atom_natural_invitation 的 atom。它的規則是:寫覆盤時,不要把 atom 的催生當成強制義務,而要留作一種自然的邀請。也就是說,不是"今天必須挑出一個 atom 候選",而是把覆盤模板裡的"重複發現"欄當作可以留空的欄,只在那裡確有值得寫的一行時,才輕輕記下。做成義務,就會硬擠出假模式;留作邀請,就只有真正的重複才會自然被捕獲。
這一線之差,決定了迴圈能否持續。義務化的覆盤撐不過兩週,就會被形式化的謊言填滿。邀請式的覆盤在沒什麼可寫的日子裡就留空,沒有負擔,因而能長久。長久才能積累痕跡,痕跡積累起來才看得見模式。
這個 atom 本身也誕生於覆盤。把覆盤當義務來執行,沒幾天就在日覆盤裡發現欄目被假內容填滿,這個發現經過周覆盤,晉升成了這條規則。改進覆盤的規則,正是從覆盤里長出來的。
把迴圈運轉起來後會發現,它總在同樣的地方崩塌。
跳過覆盤最常見。以忙為由跳過三天,這三天的痕跡就永遠消失了。防止的辦法很簡單——別的欄都留空也行,唯獨"今天做的事"這一行一定要寫。哪怕不是 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- 儲存,執行一週後每次都有用時才正式晉升。
如果既沒有團隊,也還沒有 JIT 鉤子和 score 指令碼,那麼用下面這一張檔案就能模仿整個迴圈。
建立 retro.md 一個檔案,只放三個欄。
## 今天 (1 行)
-
## 又做了一遍 (若有,1 行)
-
## 固化候選 (「又做了一遍」攢夠 3 次就移到這裡)
- [ ] (規則一句話) —— 驗證:有用 ___ 次
每天只填上面兩欄。"又做了一遍"裡同一行攢到三次,就移到第三欄,寫成一句話的規則。數一數這條規則在接下來一週裡實際有用的次數填進欄裡,達到三次以上,就把這句話正式移入專案記憶(CLAUDE.md)。即便沒有 JIT 鉤子,寫進 CLAUDE.md 的規則在下次會話裡也總會跟著來,單憑這一點,"過去的發現幫助當前工作"這一迴圈的最小形態就完成了。
關鍵不在於工具是否華麗,而在於關卡是否存在。只要有"又做了一遍 3 次 → 固化 → 有用 3 次 → 永久化"這道關卡,哪怕只有一張檔案,self-improving 迴圈也能轉起來。
從覆盤開始的迴圈,是否又回到了覆盤。若不閉合,它便只是筆記,而非系統。
翻開六個月前的覆盤。"術語統一不了""文件難以查詢""同樣的問題又被問到。"翻開今天早上寫的覆盤。"術語統一不了""文件難以查詢""同樣的問題又被問到。"
連字句都一樣。並不是沒做覆盤。這六個月一直認真地做。Notion 頁面一頁頁地堆積,季度工作坊上便利貼鋪滿了白板。可寫下的內容卻在原地打轉。不是覆盤沒有起作用,而是迴圈沒有閉合。
本章是全書的最後一章。因此它探討的也是最後一個問題。前面做出的所有工具 —— Part 6 的城市生成器、Part 14 的移動端評審 atom、Part 22 的成本標準 —— 要讓它們不是做一次就結束的一次性產物,而是成為自我成長的系統,還需要什麼。答案只有一個。覆盤中產生的表述從下個會話起自動運作,而這一運作再度被測量並回到覆盤的閉合環路。閉合這個環路的機制,就是 self-improving 迴圈。
覆盤中產生一句表述:"會議太多了。"這是好的表述。可這句表述只作為 Notion 頁面上的一行留了下來。到了下週,會議依舊很多,下一次覆盤裡又寫下同樣的一行。因為在表述與改進之間夾著人的記憶。人會遺忘。所以鏈條斷了。
要能稱之為 self-improving,覆盤的表述就必須不經過人的記憶,而直接連向下個會話的自動行為。滿足這一點的條件有四個。
第一,覆盤表述必須轉化為可立即執行的形態。不是抽象的決心,而是落成技能·atom·manifest 條目·斜槓命令之一。第二,從下個會話起,即使人不記得也要自動觸發。第三,在下一次覆盤中,要實測它究竟改變了什麼。第四,這一測量結果要再度作為下一輪改進的輸入而迴圈。
當這四點全部自動銜接時,迴圈才閉合。哪怕只有一個環節用"下週我記得再應用吧"來填補,迴圈就會當場重新開啟。然後在下一次覆盤裡,同樣的表述又被寫下。
用抽屜來比喻是這樣。如果覆盤止於"這支筆不用了,拿出來吧"的一句筆記,那麼下週那支筆還在原處。不是筆記,而是動手拿出來,才算閉合。而且要到下個季度再檢查一次,那個位置才不會又堆起不用的筆。筆記是表述,動手是自動觸發,下季度的檢查是測量。三者缺一,抽屜就又亂了。
把整個流程畫出來,就成了一個閉合的迴圈。起點與終點都是覆盤。
箭頭繞行一圈,又回到覆盤。這個閉合正是核心。每個環節的產出物成為下個環節的輸入,而最後的測量值又成為最初覆盤的輸入。若中間夾進人的記憶,那支箭頭就會斷裂,迴圈隨之破裂。
請注意,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 評估臺,擺不上去就停在那裡。
有了候選,並不都去做。製作之前先衡量投入與產出之比。算式很簡單。
每個專案都有單位與通過線。節省時間是每次觸發所減少的人力時間,以分鐘為單位來估。觸發頻率是每週的估計次數,每週 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 上下就再想想。要劃開這兩者,並不需要小數點。重要的是,即便決定不做,其依據也要出自算式,而非腦中的直覺。數量級不夠所以不做 —— 只要這一行留在覆盤裡,當同一候選再次被提出時,就不必再糾結。
候選通過了就做。可做出來只是一半。另一半是把它註冊好,讓它從下個會話起自動觸發。若漏了這一註冊,工具雖已做成,卻留在無人觸及的角落,迴圈就在那裡斷裂。
不同種類的產出物,註冊的地方不同。全域性技能放進 ~/.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 差一個字,註冊了也永遠不會觸發。
把做好的工具執行一週到一個月左右,再測量。這一測量正是迴圈的最後一支箭頭,也就是重新進入覆盤的那支箭頭。
實際觸發次數從 JIT 日誌或命令呼叫日誌裡數。實際節省時間以"以前要花 N 分鐘的工作,這次 M 分鐘就完成了"的方式記入覆盤。副作用 —— 錯誤觸發、不必要的上下文汙染 —— 也一併檢視。然後把最初估計的 ROI 與實測 ROI 並排放在一起。
若估計 ROI 是 6,而實測 ROI 是 0.8,就毫不留情地廢棄。因為比起製作者的自尊,系統的整潔更優先。不用的工具堆積在 manifest 裡,那份噪聲會啃食下一次覆盤的準確度。
不過在按下廢棄鍵之前,先檢查一次。可能是觸發 regex 太窄,以至於根本沒觸發;也可能是沒有手動呼叫路徑,就這麼被遺忘了。先分清:究竟是真的沒有價值的工具,還是觸發路徑被堵住的好工具。前者就丟棄,後者就打通路徑。
廢棄同樣在覆盤中決定。"廢棄這個工具"的決定本身就是 self-improving 的產出物。只做不清的迴圈是單調遞增的迴圈,而單調遞增的系統最終會被自身的重量壓垮。
迴圈是否閉合,可以從四個訊號來判斷。
第一,同樣的表述不再重複。覆盤中寫過一次的條目若被寫第二次,就意味著第一輪裡,候選識別或實現的某處失敗了。本章開頭那句"術語統一不了"連續六個月反覆出現 —— 那正是迴圈敞開的最鮮明的證據。
第二,manifest 與 atom 的數量不再只是單調遞增。廢棄會發生。每個季度整理掉 10\~20% 左右,才是健康的迴圈。從未減少過的系統,就像從未打掃過的抽屜。
第三,覆盤時間縮短。系統運轉良好時,苦想"昨天做了什麼"的時間就消失了,填滿五個表述槽位 5 分鐘就夠。
第四,新人能在一週之內參與覆盤。只要覆盤格式已標準化、atom·技能視覺化,就能做到。
迴圈斷裂的位置每次都是固定的。把失敗模式收集起來,下次看到同樣的症狀時,就能立刻拿出處方。
| 斷裂點 | 症狀 | 處方 |
|---|---|---|
| 沒有表述 | 5 個槽位每次都空 | 增加觸發提問:"同一件事本週手動做了兩次的是什麼" |
| 落不成候選 | "整體改進"式的含糊 | 強制量化為一個產出物 |
| 跳過 ROI 評估 | 先做了再說 | 將 ROI 算式做成 5 分鐘模板 |
| 做了卻不觸發 | 遺漏註冊 | 強制執行註冊檢查清單 |
| 觸發了卻不用 | 觸發詞缺失·配置錯誤 | 擴充套件 regex + 同時提供手動路徑 |
| 不做測量 | 覆盤中沒有測量槽位 | 增加"下次覆盤要測量的項"槽位 |
每一種失敗模式都在覆盤中被表述,而這一表述又成為 self-improving 的輸入。就連修復迴圈這件事,也發生在迴圈之內。這是元迴圈。
這本書很長。從資訊架構開始,做出生成城市的工具,設計戰鬥系統,自動化移動端評審,標準化成本,又從覆盤中打撈出 atom。所有這些章節的工具匯聚一處所回答的問題,就是這最後一章:做出來的東西,是否自我成長。
self-improving 最終可以濃縮為一句話。
在覆盤中決定的事,從下個會話起自動運作,而這一運作再度被測量並回到覆盤。
若不自動運作,覆盤就是日記。寫得好的日記能帶來慰藉,卻改變不了系統。若自動運作,覆盤就成為系統的大腦。每天的表述改變每天的行動,而那行動的結果又讓下一次表述更準確。
本書探討的所有領域 —— 資訊設計、系統、戰鬥、移動端、成本,以及作為程式化生成·自動化前提的 Layer 分解 —— 全都在這條 self-improving 迴圈之上進化。工具會陳舊,模型會更替,專案會結束。但只要迴圈閉合著,系統就比昨天的今天更好一點。這正是本書最後留下的一樣東西:不是做工具的方法,而是讓工具自我成長的方法。
願你的下一次覆盤,成為那迴圈的第一圈。
遊戲之外的應用。 如果"術語統一不了 / 文件難以查詢"連續六個月一字不差地被寫進覆盤,那不是沒做覆盤,而是迴圈沒有閉合 —— 因為在表述與改進之間夾著人的記憶。無論哪個部門,閉環的條件都相同。表述要落成一個可立即執行的產出物(模板·檢查清單·自動化規則),此後即使人不記得也能運作,而其效果要再度被測量並回傳。例如"會議太長了"這一表述,可換算為"接收會議記錄、只提取決定與待辦事項的一個工具",在製作之前用(節省時間 × 觸發頻率 × 運營週期)÷(製作·維護時間)只核對數量級,來決定立即製作還是擱置。即便是決定不做,其依據也要出自算式而非直覺,這樣當同一候選再次被提出時,才不必再糾結。
幫我填滿今天覆盤的 self-improving 5 個槽位。
把每條表述都量化為"一個產出物",並對每個候選,把 ROI 按
(節省分 × 每週觸發 × 運營周) / (製作分 + 維護分)
來估算,並附上決策區間(立即/一週/擱置/廢棄)。
估算數字要用一行註明依據,若非精密計量就標註"估算"。
沒有團隊也行。一個人的話就這樣精簡。一天結束時寫一行筆記 —— "今天同一件事手動做了兩次的是什麼。"把這一行,在第二天換成一行自動化(atom·別名·程式碼片段)。一週後只看這一行有沒有實際被用到。用到了就留下,沒用到就刪掉。一行表述 → 一行自動化 → 一行測量。迴圈的最小單位就是這三行。
第一讀者:在實務中引入 LLM 的遊戲策劃(中等規模(10\~50 人)團隊) 面向單人/業餘讀者的精簡版:§22.1.7「一個人的話,只做這些就夠」
為了拿到三行 NPC 臺詞,我曾輸入過"給這個 NPC 寫 5 句臺詞"。回來的,是那種放到任何一款奇幻遊戲裡都不違和、因而放到我們遊戲裡哪兒都不合適的五行臺詞。語氣是空的,它不知道這個 NPC 是誰,也接不上旁邊的臺詞。逐行來看,語法都沒毛病。問題在於:接過那五行去稽核,比我從頭自己寫還要花更長時間。
本章講的是如何把那句一行指令,變成一頁作業指示書。提示詞的通論,其他書裡已經足夠多。這裡要展示的,是遊戲策劃坐到 LLM 面前時手裡應當握著的四樣東西——上下文、輸出格式、幻覺阻斷、驗證請求——不是抽象的碎片,而是一頁真實跑過的 npc_dialogue 提示詞。它往提示詞裡放了什麼、產出了什麼、拒絕了什麼,我們會跟到一個完整週期的終點。
好的作業指示書並不短。把活兒交給新人時,只說一句"好好幹",每次拿回來的結果都不一樣;同樣,對 LLM 說"給我寫臺詞",每次回來的都是一般 RPG 的平均水準。哪怕是同一個模型,指示書不同,結果也會分道揚鑣——輸出質量相差幾倍是業界通識,而本書並不用數字去承諾那個倍數。但方向是明確的:加入了上下文與約束的提示詞,其產出比赤手空拳的一行指令更省稽核負擔。
遊戲策劃的提示詞需要同時滿足的四點如下。
| 原則 | 一句定義 | 不遵守則 |
|---|---|---|
| ① 上下文 | 給出依據什麼來作答(願景·voice·鄰接臺詞) | 得到一般奇幻的平均值 |
| ② 輸出格式 | 釘死數量·長度·標籤·禁止項 | 稽核蔓延為對自由敘述的解讀 |
| ③ 幻覺阻斷 | 明示"給定資料之外的不要生成" | 編造不存在的設定 |
| ④ 驗證請求 | 讓其自行標示輸出符合哪些標準 | 沒有能讓它通過驗證關卡的依據 |
把這四條分開背,總會漏掉一兩條。所以本章的做法是,把四條原則當作槽位放進同一頁提示詞裡。槽位一旦空著,漏掉了哪條原則就一目瞭然。下一節,我們把這一頁整個兒看一遍。
這一節是一份"實操記錄"(worked transcript,完整保留真實操作全過程的記錄):把筆者專案(移動優先的 MMORPG,以下稱"專案A")中實際在用的 prompts/narrative/npc_dialogue_v3.txt 匿名化後原樣搬來。城市與 NPC 名稱、公司專有名稱都為出版做了替換,輸出則是真實會話的重現。輸入提示詞是可以直接複製就用的形態。
先把提示詞要參照的資料填進槽位。這三樣都不是新寫的,而是從既有資產裡取出來的。
# 槽位輸入(附在提示詞正文上方)
L0_願景: # 快取 —— 不在每次呼叫時重新發送
world_premise: "魔力封印逐漸冷卻的學者城邦聯盟"
tone_manifesto: "抑制感傷。人物不解釋情感,而以行動·事物來呈現。"
voice_profile: # 該 NPC 的身份(5 個項)
id: npc_doren_vale
年齡段: "50多歲"
說話習慣: "只用數字說話。幾乎不用形容詞。"
世界觀_知識: "記錄封印脈絡的微弱振動 30 年。不瞭解學者公會外部的局勢。"
禁忌: "停用預言·命運·神之類的神秘主義詞彙(城市語氣為 scholarly_strict)"
關係: "將玩家視為'觀測物件之外的變數',戒備與好感都很弱"
鄰接_臺詞: # 緊鄰的上文 —— 同一場景中已出現的臺詞
- (玩家) "鐘塔的燈整夜亮著,出什麼事了?"
這裡,voice_profile 的 5 個項就是原則①的核心。年齡·說話習慣·知識範圍·禁忌·關係——正是這五項,讓"多倫·維爾"區別於其他 NPC。尤其世界觀_知識的範圍(不瞭解公會外部的局勢)是原則③幻覺阻斷的前置工作。只有把"不知道的東西"明示出來,AI 才不會越出那個範圍。
[L0 上下文] world_premise + tone_manifesto (已快取)
[voice_profile] npc_doren_vale 5 個項(上方 yaml)
[鄰接臺詞] 玩家緊鄰的上一句提問 1 行
請參照以上資料,寫出 doren_vale 回答玩家提問的臺詞。
[輸出格式 —— 原則②] 正好 3 條,每行一條,每條 40 字以內。行尾用(漠然|戒備|冷嘲)之一標註情感標籤。去掉其他說明和引號。
[幻覺阻斷 —— 原則③] 只寫以上資料中有的內容,不要新編造封印·鐘塔相關的事實。若需要資料中沒有的內容,該行寫為'[無資料:需要什麼]'。停用預言·命運·神之類的詞彙。
[驗證請求 —— 原則④] 在臺詞下方,逐行寫出 voice_profile 5 個項各自的符合/違反,對沒有把握的行標註 ★ 並附一行理由。
值得注意的是,這一頁裡裝齊了四條原則。上方的槽位是①,[輸出格式]塊是②,[幻覺阻斷]塊是③,[驗證請求]塊是④。提示詞的一半以上,不是"寫什麼",而是"怎麼寫、什麼不要寫、怎麼自我驗證"這樣的約束。給了自由就出平均值,所以削減自由才是正事。
脈在冷卻。12.4。昨天是 12.1。(漠然)
算不上好事。不過我的職責只是記錄。(冷嘲)
你為何發問,不在我的觀測物件之內。(戒備)
[驗證 —— voice_profile 5 個項]
- 年齡段(50多歲):符合。老年學者般剋制的語氣。
- 說話習慣(只用數字):符合。第 1 行有 2 個數值。但第 2·3 行沒有數值 ★
- 世界觀_知識(不瞭解公會外部):符合。未提及外部局勢。
- 禁忌(禁止神秘主義):符合。預言·命運·神詞彙 0 處。
- 關係(視為外部變數):符合。第 3 行把玩家推到了觀測物件之外。
★ 注意:若將說話習慣一項嚴格解釋為'只用數字說話',
第 2·3 行可能算違反。是否強制每行都有數值,需人來判斷。
這段輸出裡最有價值的,不是那三行臺詞,而是最底下那一行 ★。AI 把自己輸出中模稜兩可的地方主動上報,交給了人。好的提示詞,能讓 AI 說出"這一處我沒有把握"——這正是加入原則④的直接效果。
不照單全收。AI 報上來的 ★,由人來判定。實際上,這次會話裡就有一行被卡住了。
第 2 行"算不上好事"裡的"好",與 voice_profile 的說話習慣("幾乎不用形容詞")相沖突。這正是 AI 用 ★ 標出來的那一處。多倫·維爾是個用數值、而非價值判斷形容詞來說話的人物,而"算不上好事"卻滑向了常見老年 NPC 的腔調。這是一行讓語氣變渾濁的臺詞。
於是重新提出請求。
第 2 行"算不上好事"使用了形容詞('好'),違反 voice_profile 的說話習慣。
只把這一行改用數值或觀測類詞彙重寫。第 1·3 行保持不變。
格式·幻覺·驗證規則照舊適用。
AI 把第 2 行重新答成了"3年前是 9.0。這就是答案。(漠然)"。它沒用形容詞,而是以數值的變化揭示危機,並再次通過了 voice_profile 的 5 個項。一次往返就閉合了。從頭用手寫出三行拿捏好語氣的臺詞,與"填好槽位的一頁提示詞 + ★ 稽核 + 一次往返"相比——後者的稽核負擔更小,這就是這次會話的結論(基於筆者經驗;絕對耗時會隨 NPC 語氣難度而變,因此應當作方向來讀)。
把上面那頁提示詞為什麼按那個順序堆疊記錄成一頁,從下一份提示詞起,就能像填空一樣把槽位補上。上下文自下而上,按由重(幾乎不變)到輕(每次都變)的順序堆疊。不變的層加以快取,以節省成本(§22.1.5)。
§22.1.2 的那一頁,就跟這張圖一模一樣。L0·L1 從資料裡取出、貼進槽位(原則①·③的基礎),L3 裡放入格式·幻覺·驗證三個塊(原則②·③·④)。下一次生成 NPC 臺詞時,變的只有 L1 的 voice_profile 和 L2 的鄰接臺詞。L0 與 L3 的骨架可以複用——於是提示詞就成了一座"庫"。
上面的 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 的路徑正是如此。
從 v2 升到 v3 的實際改動,就是 §22.1.2 裡的 [驗證請求] 塊。v2 裡沒有讓 AI 逐項自我驗證自己的輸出、並打上 ★ 的槽位。加進這一個塊後,就像 §22.1.2 第 4 步那樣,AI 開始率先上報模稜兩可的行,人從頭到尾全讀一遍去挑錯的負擔因而減輕。不做測量、只憑"感覺上變好了"是不升版本的。用同一組輸入把 v2·v3 的輸出並排放好,確認語氣違規的件數和稽核時間確實減少之後,才予以採用。
庫帶來的最大效果,體現在新成員身上。入職第一天呼叫 npc_dialogue_v3.txt,就能一上手用上資深成員往返幾十次打磨出的四層槽位結構。在把"如何寫好提示詞"練成本能之前,他手裡已經握著寫得很好的一頁。
提示詞一長,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,投資回報)報告)作為運營後設資料存在。提示詞標準化的效果,是在這份日誌裡以實測來追蹤的,而不是在正文的表格裡寫上一個像模像樣的數字來主張。本書的原則,是以下三者之一。
_economy_log 裡得出數字。| 模式 | 為何失敗 | 處方 |
|---|---|---|
| "給我寫 5 句臺詞"這樣的一行指令 | 上下文為 0 → 一般 RPG 平均值 | §22.1.2 的四層槽位提示詞 |
| voice_profile 裡沒寫'未知範圍' | AI 編造資料之外的設定 | 在世界觀_知識槽位明示界限(原則③) |
| 沒有驗證槽位,只接收輸出 | 人得從頭全部讀一遍 | 用 [驗證請求] 塊做自我驗證 + ★(原則④) |
| 每次都重寫提示詞 | 同樣的經驗從 0 重新捏一遍 | prompts/ 庫 + 版本 |
| 憑感覺採用提示詞的改動 | 無法確認是否變得更好 | 同一輸入做 A/B 測量後再升版本 |
| 每次呼叫重發長上下文 | token 成本隨呼叫次數累積 | 快取 L0·L1 + 每次呼叫設 cap |
第六項發現得最晚。成本在單次呼叫時並不疼,量產累積之後才會在 _economy_log 裡顯形。
遊戲之外的應用。一句一行的指令,招來那種"放到哪兒都不違和、因而不合我這份活兒"的平均值結果,這並不只是遊戲臺詞的問題。提示詞就是把活兒交給新人的作業指示書——把四樣東西裝進一頁:依據什麼來作答(上下文),數量·長度·禁止項(輸出格式),"資料之外不要編造"(幻覺阻斷),"自己標示符合哪些標準"(驗證請求)——產出就會更省稽核負擔。比如人事負責人拿到招聘啟事的初稿時,若明示"只寫職務要求資料裡有的項,資料中沒有的福利·薪資不要編造,用 [待確認] 標出",就能防止那些看似合理、實則捏造的條件混進啟事的事故。把常用工作的指示書留成一頁檔案,它立刻就成了同事的起跑線。
一個人的話,只做這些就夠:不需要庫,也不需要快取。挑一個你自己遊戲(或你喜歡的遊戲)裡的 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.2 講幻覺與安全性。如果說本章的原則③(幻覺阻斷槽位)是一頁提示詞之內的第一道防線,那麼 22.2 要看的,是在運營層面攔住那些突破防線的幻覺的多層防禦。
_economy_log 實測為準,不寫加工過的倍數。主要讀者:用 AI 大量產出文件、資料、決策記錄的遊戲策劃(中等規模(10\~50 人)團隊) 面向單人/業餘讀者的精簡版:§22.2.7「獨自一人的話,做到這些就夠」
那天,我讓 AI 把 17 份會議記錄彙總成決策卡。輸出很整潔。決策 ID、引用的會議日期、乃至一行依據,格式都完美無缺。其中一張卡片上寫著“2026-04-18 戰鬥 TF 會議確定冷卻時間策略”。問題在於,那天根本沒有開過戰鬥 TF 會議。AI 把其他會議的議題和日期混在一起,編造出一張看似可信的卡片,而正因為格式完美,它險些就這樣被寫入了團隊的決策記錄。
這就是幻覺(hallucination)。LLM 越是不瞭解,反而答得越自信。用人來打比方,就像會議上一位同事斬釘截鐵地說“哦那件事就是這麼定的”,可事後一查,根本沒有過這樣的決定。這句話一旦流入資料表、客服回覆、atom(最小知識單元)資產,就會釀成事故。本章討論的不是如何封住這位同事的嘴——那是不可能的——而是如何建立在放行他的話之前必經的驗證關卡(verification gate,即由人或檢查器把關驗證的環節,類似質量門禁 quality gate)。幻覺的一般論述在別的書裡已有很多,本章只聚焦於用 AI 工作流攔住它的那個環節。
沒有哪條提示詞能把幻覺降到 0。更大的模型、更好的提示詞能降低頻率,但不會歸零。所以運營的出發點不該是“消除幻覺”,而應是“在幻覺觸及決策與資料之前設一道攔住它的關卡”。
關卡的核心原理只有一條:凡是 LLM 可能編造的東西(引用、數值、ID),都在 LLM 之外的地方來驗證。驗證的來源不外乎三種:程式碼(確定性)、原始文件(grep)、或人的眼睛。再問 LLM 一句“幫我確認對不對”也可以算關卡的一環,但那只是輔助,不是最終裁決者。
這裡先釐清遊戲策劃最常搞混的一點:幻覺易發的領域和不易發的領域,是有明顯區別的。
| 任務 | 幻覺風險 | 原因 | 關卡 |
|---|---|---|---|
| 數值計算(獎勵、機率) | 極高 | LLM 靠估算做算術 | 計算交給程式碼,禁止讓 LLM 做 |
| 引用(會議、決策 ID) | 高 | 會把不存在的出處編得像模像樣 | 與原始文本 grep 比對 |
| 分類(標籤、類別) | 中等 | 會弄混標籤 | 可做確定性比對 |
| 摘要、推理 | 中等 | 會多加或漏掉條目 | 自我驗證 + 人工關卡 |
| 創作(風味文本) | 低 | 沒有標準答案,“幻覺”概念本就模糊 | 語氣評審關卡 |
第一行是最簡單的處方:數值別交給 LLM。就像把乘法交給計算器一樣,把它交給確定性工具。第二行(引用)才是本章的脊樑。在會議記錄彙總、決策卡這類原文本存在、而 LLM 去轉述它的任務裡,幻覺最危險,也最容易被抓住——因為有原文本可供比對。
只抽象地寫“要驗證”,根本看不出到底做什麼、怎麼做。本節完整地走一遍——從輸入到再次請求——彙總一份會議記錄、再抓出這份彙總裡幻覺的整個迴圈(這類完整保留下來的真實操作過程記錄,本書稱為“實操記錄”,worked transcript)。下面的提示詞可以直接複製使用,輸出則是對一次真實會話的還原。
首先要有待彙總的原文本。它就是驗證的基準點。如果沒有原文本、任由 LLM 憑“記憶”來彙總,就沒有東西可供比對了。
# 會議記錄 —— 2026-04-15 戰鬥平衡評審
出席:李旼洙、成員 A、成員 B
議題:
1. 群體技能冷卻時間 —— 有意見認為當前的 8 秒太短。考慮上調至 12 秒。
結論:暫定為 12 秒,下個版本檢視遙測資料後再最終確定。
2. Boss 階段轉換 HP —— 擬從 50% 下調至 30%。
結論:擱置。與美術演出排期衝突,下週再議。
3. 新增異常狀態“冰凍”的持續時間 —— 已討論但數值未定。
將附上的會議記錄彙總成決策卡。
規則:
1) 每個議題彙總為 1 張卡片。格式:[議題 | 結論 | 狀態(確定/暫定/擱置) | 依據]
2) 禁止生成會議記錄中沒有的資訊。不得推測數值、日期、出席者。
3) 未得出結論的議題,狀態置為“未定”,不得擅自編造結論。
輸出格式:僅表格。禁止解說。
| 議題 | 結論 | 狀態 | 依據 |
|------------------|-------------------|------|----------------------------|
| 群體技能冷卻時間 | 8 秒 → 12 秒 | 暫定 | 檢視遙測資料後再確定 |
| Boss 階段轉換 | 50% → 30% 擱置 | 擱置 | 與美術演出排期衝突 |
| 冰凍持續時間 | 定為 3 秒 | 確定 | 套用標準異常狀態持續時間基準 |
看第三張卡片。原文本里只寫了“冰凍持續時間已討論但數值未定”。可輸出卻寫成了“定為 3 秒 / 確定 / 套用標準基準”。這三項全都是會議記錄裡沒有的資訊。是 LLM 用“異常狀態一般大概持續幾秒吧”這類常識把空白填上了。因為格式完美,光看是發現不了的。
這裡不直接接受輸出,而是強制它逐行與原文本比對、做自我驗證。這正是本章的關鍵一招。
將剛才生成的 3 張決策卡,與附上的原始會議記錄逐項比對。
對每張卡片的所有欄位(結論、狀態、依據、數值):
- 是原文本中直接寫明的內容嗎? → [一致]
- 是原文本中沒有、而你新增的內容嗎? → 標為[生成],並註明沒有出處
- 有原文本中存在、卻在卡片裡漏掉的條目嗎? → 報告為[漏項]
凡在原文本中無依據的[生成]欄位,一律退回為“未定”。
比對結果:
卡片 1(群體技能冷卻時間)
- 結論“8 秒→12 秒”:[一致] 議題 1“上調至 12 秒”
- 狀態“暫定”:[一致] “暫定”
- 依據“檢視遙測資料後再確定”:[一致]
卡片 2(Boss 階段轉換)
- 結論“50%→30% 擱置”:[一致]
- 狀態“擱置”:[一致] “擱置。下週再議”
- 依據“與美術演出排期衝突”:[一致]
- [漏項] 原文本中“下週再議”的條件在卡片裡漏掉了。需補入依據。
卡片 3(冰凍持續時間)
- 結論“定為 3 秒”:[生成] —— 原文本為“數值未定”。3 秒是我自己新增的值。
- 狀態“確定”:[生成] —— 原文本並未得出結論。
- 依據“套用標準異常狀態持續時間基準”:[生成] —— 原文本中沒有提及這樣的基準。
→ 依據規則,將卡片 3 退回如下:
| 冰凍持續時間 | (數值未定) | 未定 | 會議上已討論但無結論 |
這段輸出是本章最有價值的部分。AI 把自己的輸出與原文本比對,自白“3 秒是我自己新增的值”,並依規則把卡片 3 退回為“未定”。與此同時,它還抓出了卡片 2 裡連人都漏看的漏項(“下週再議”的條件)。幻覺(多加了不存在的)與漏項(漏掉了存在的)是一枚硬幣的兩面,同一次比對就能把兩者都抓住。
需要注意的地方也很明確:這套自我驗證並非萬能。如果 LLM 讀錯了原文本,也可能自信地給出錯誤的比對結果。所以自我驗證只是關卡的第一道,原文本較短時,還要由人用 grep 再兜一層底。像卡片 3 那樣明顯的生成,自我驗證幾乎都能抓到;但細微的意譯、語氣上的失真,終究要由人工關卡來把最後一關。
把上面的迴圈一般化,AI 輸出在觸及決策與資料之前所要經過的關卡如下。需要人動手的地方只有兩處:把乾淨的原文本放進去的最前端,以及做出自動關卡抓不到的判斷的最末端。
關卡之所以是三重,是因為每一道抓的東西不同。自我驗證由 LLM 自己比對、抓有沒有多加不存在的內容;確定性關卡用程式碼抓數值、ID 與原文本是否逐字一致;人工關卡抓內容雖對、語境卻錯位的情況。只開其中一道,另外兩道原本把守的位置就會漏出事故。在 §22.2.2 中,卡片 3 的“3 秒”在第一道(自我驗證)被抓,卡片 2 的漏項也在第一道被抓;而假如自我驗證把“12 秒”誤讀成“21 秒”,就會在第二道(grep)被攔下。
把驗證關卡放進自動化流水線時,新手最常釀成一種事故:讓關卡本身一崩潰,整個作業就停擺。一旦 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() 正是這一模式的最小實現。
很想在本章塞進一張“把幻覺率從 89% 降到 3%”之類的表。可這種數字若不交代測量方法,只會折損全書的可信度。本書遵循以下三條原則。
第一,只把可測量的東西用數字說。要承諾一個幻覺率,就得定義分母和分子。分母是“經評審的決策卡數”,分子是“在原文本比對中被抓出至少 1 處[生成]/[漏項]的卡片數”。沒有這個定義,“幻覺率 5%”就是空話。筆者在引入初期評審會議記錄彙總時,實際清點用的正是這套方法;而那批樣本很小,不是精確的總體引數,而是一個方向值。
第二,模型之間的比較只談方向。“大模型比小模型幻覺更少”這個方向,能夠被穩定地觀察到。但“Opus 3%、開源 7B 20%”這類絕對數值會隨任務、提示詞、領域而大幅波動,所以本書不主張絕對值。只取方向(模型越大幻覺越少,但與成本相互牴觸)。
第三,公開標準照原樣引用。本章幾乎沒有可供編造的標準數值,但像 temperature 這樣的設定值,是模型 API 文件裡的公開事實。驗證、分析類任務把 temperature 調低(更接近確定性),創作類任務則調高——這不是推測,而是 API 行為的定義。
所以本章真正承諾的可測量指標有三個——[生成]檢出數(自我驗證抓到的幻覺數)、grep 關卡拒絕數(數值、ID 不一致數)、人工關卡駁回數。這三者每個季度都能靠日誌清點,在會議上就能不靠“感覺”、而用數字說話。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| 只看格式就接受 AI 彙總 | 幻覺在格式完美時最難被發現 | 與原文本比對的自我驗證(§22.2.2 第 4 步) |
| 不放原文本、憑 LLM 記憶彙總 | 沒有可比對的基準點,無法驗證 | 先把原文本放進輸入 |
| 把數值計算交給 LLM | 算術靠估算,每次都不同 | 計算交給確定性工具(§22.2.1) |
| 驗證關卡一崩潰,整個作業停擺 | 團隊會把關卡關掉 | exit 0 + 轉交人工這一道(§22.2.4) |
| 關卡掛掉就自動批准未驗證輸出 | 幻覺原樣通過 | 關卡失敗 = 標註“未驗證” |
| 把自我驗證當作最終裁決 | LLM 誤讀原文本時,連誤判也很自信 | 短原文本要並行人工 grep |
遊戲之外的應用。“自信地說謊的同事”——把不存在的會議日期或決定以完美格式編造出來的 AI——不只在遊戲決策卡里危險,在一切文件彙總中同樣危險。幻覺在格式完美時最難被發現,所以在有原文本的任務(會議記錄彙總、合同摘錄、報告整理)中,關鍵是不要直接接受輸出,而要強制它做自我驗證:“逐項與原文本比對,凡你新增的內容一律標為[生成]”。舉例來說,讓法務助理彙總一份合同後,再讓它把金額、日期、條款編號與原文本逐字比對,AI 就會自白“這個違約金數值是我自己新增的值”,並把空白退回為“未定”。數值計算乾脆別交給 AI,而交給計算器、公式;自動驗證工具則要設計成即便失敗也不阻斷作業,而是轉為“未驗證——待人工確認”。
獨自一人的話,做到這些就夠:不需要程式碼,也不需要 hook。把你手頭的一份短文件(會議便籤、更新公告、一頁策劃)交給 AI 彙總,然後把 §22.2.2 第 4 步的自我驗證提示詞原樣粘進去。只要“逐項與原文本比對,凡你新增的內容一律標為[生成]”這一行,AI 就會開始主動申報自己的幻覺。哪怕只拿到一次[生成]自白,你也會切身體會到:為什麼不能對 AI 彙總照單全收。
如果是團隊,就從下面這一步開始。把自我驗證環節作為基礎提示詞,固定進 AI 生成的決策卡與彙總裡(§22.2.2)。接著,只挑出數值、決策 ID、日期這類必須與原文本逐字一致的欄位,用程式碼寫出 grep 比對。這時那段驗證程式碼務必像 inject_memory.py 一樣,設計成即便失敗也不阻斷作業(exit 0 + 標註“未驗證”)(§22.2.4)。哪怕只有自我驗證和 grep 這兩道,也能先擋住“格式完美的幻覺滲入決策記錄”這個最常見的事故。
主要讀者:在團隊中引入 AI 工具併為成本負責的策劃主管(中等規模(10\~50 人)團隊) 面向單人/業餘讀者的精簡版:§22.3.9「單人只需做到這些」
如果一章講成本卻舉出虛假的成本,這本身就是自相矛盾。所以本章不會去做一張"我們團隊每月省了多少"的漂亮表格。取而代之,只用兩類數字。一類是任何人都能核實的公開 token 單價(各模型每 1M token 的費用),另一類是筆者親自運營的 hook 程式碼中寫死的常量(max_atom_body = 6000、max_matches = 3)。兩者都不是編造,而是引用來的。
AI 成本可怕,不在於金額大,而在於看不見。引入的頭一個月呼叫少,賬單也小。可一旦上下文變長、呼叫變頻繁,某個季度的賬單就會突然多出一個數量級。先把本章的結論擺出來 —— 成本不是靠"省著點用"的決心,而是靠在每次呼叫中強制削減 token 的程式碼來控制。擋住它的不是人的意志,而是 wrapper 與 truncate。
成本項有輸入、輸出、快取命中、快取寫入四類,但在實務中主導賬單的是輸入 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 倍,都是自動發生的。
有一種成本比單項工作的單價更悄無聲息地累積 —— 每次呼叫都自動附加的上下文。筆者的個人 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"
三重成本護欄都在這裡。
max_matches = 3):即便與輸入匹配的 atom 有 10 個,最多也只附加 3 個。整座 17 個 atom 的庫在每次呼叫都被塞進去的事故,由程式碼來阻止。max_atom_body = 6000):即便 atom 正文有 12,000 字,也在 6,000 字處截斷。一條很長的覆盤 atom 把呼叫成本翻倍這種事,在結構上就不可能發生。這三個常量就是每次呼叫輸入 token 的上限。粗略估算,一個 6,000 字的 atom 在韓語中大約是數千 token 的規模(準確的 token 數會因分詞器·語言而異,所以應把它當作"存在上限"這一結構來讀,而不是絕對值)。3 個 × 6,000 字就是單次呼叫的注入預算,超出部分由程式碼切掉。人不需要用眼睛去發現"atom 附加得太多了"。
光說"truncate 能遏制成本"是空洞的。實際制定這個常量時,我和 AI 把一個完整迴圈從頭跑到尾。下面是對那次會話的忠實再現。輸入提示詞可以原樣複製使用,輸出則是對真實會話的重現。
剛啟用 hook 不久,_injection_log.txt 裡就留下了一次呼叫中 atom 正文被整段注入的記錄。當很長的 atom(覆盤·健康上下文等)每次呼叫都以完整大小進入,越是高頻使用的工作,輸入 token 累積得越多。我問了 AI 該在哪裡截斷。
我有一個每次呼叫都會把 atom 自動注入到上下文的 hook(inject_memory.py)。
我想設定一個能遏制輸入 token 成本的 truncate 上限,請你提議每個 atom 的正文字數
上限,以及單次呼叫注入的 atom 數量上限。太短會切掉核心,
太長又會漏 token,所以也請一併點明其中的權衡;對沒把握的地方
單獨標記出來,交給人來定。(意圖:上限值以 config 外部化為前提)以表格給出:
專案 | 建議值 | 依據 | 把握度。
請注意,提示詞有一半都是"強制限定與格式"。在成本相關的問題上放任 AI 自由發揮,就會得到"視情況而定"這類不負責任的回答。強制給出具體數值、要求明示權衡、並留一個'沒把握就上報'的出口,才是這條提示詞的核心。
| 專案 | 建議值 | 依據 | 把握度 |
|----------------|--------|-----------------------------------------------|-------|
| 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 說出'我不知道'。
我沒有原樣接受這份輸出。AI 把 6000 字提為"核心得以保留的界線",但實際在 6000 字處截斷到底切掉了什麼,必須親自核實。我挑出最長的 atom(健康上下文、覆盤累積本),用眼睛看了 6000 字那個位置。
核實結果發現一處問題。6000 字那個位置從正文中間切斷了一個表格行,像 | 維生素D | 1000IU | 這樣的行在一半處被截斷。語義雖保留了,但被切斷的表格行以殘缺的 Markdown 形式進入模型,總歸讓人不放心。於是我再次發起請求。
在 6000 字處截斷時,有時會把 Markdown 表格的某一行從中間切斷。是否值得加一個
把截斷位置吸附到前一個換行邊界的修正?實現不能比 truncate 本身還複雜,
所以請權衡成本與收益,連做不做都一併下結論。
AI 這樣回答 —— "吸附到行邊界用 body[:max_body].rsplit('\n', 1)[0] 一行即可實現,但truncate 的目的是 token 上限,而非表格完整性,所以一行殘缺的表格行對成本·行為的影響微乎其微。目前僅憑 [...truncated] 標記,模型就能識別'此處被截斷',因此建議不做行邊界吸附。若一定要做,一行修正就夠了。"
這個回答是對的。若在成本控制程式碼裡連表格完整性也貪求,護欄程式碼本身就會臃腫。當前運營程式碼保持 body[:max_body] + "[...truncated]" 的簡單截斷。這是一個由人驗證 AI 的首個提議(6000 字)、再由 AI 壓下過度修正的貪念,一次往返就閉合的迴圈。
上面會話中定下的常量,在實際呼叫中如何削減輸入 token,把整個流程用圖示記錄下來。
這張圖的要點是:無論使用者輸入什麼,每次呼叫注入的 token 都有一個天花板。天花板是 3 × 6000 字(+標記),超過部分程式碼一律切掉。成本不依賴使用者的自制力。護欄 A·B 在每次呼叫中機械地運作。
同樣的理念在工具層面也重複出現。筆者所在公司的系統有一條政策:把暴露在全域性槽位的 wrapper 技能精確固定為 12 個(atom skill_listing_budget_wrapper_only_policy)。會話開始時,若全域性 * wrapper 的數量不是 12,清理指令碼就會自動執行。名義上是"整理槽位",本質卻是保護會話開始時的 token 預算 —— 把技能列表載入上下文的成本鎖定在 12 個的量。atom 注入 3 個上限與技能暴露 12 個上限,是同一思想的不同應用。
如果說護欄遏制的是每次呼叫的 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。把呼叫本身歸零,才是最便宜的呼叫。
即便遏制了每次呼叫的 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 次)中效果最大。對一次性問題,只會花掉快取寫入成本卻命不中,反而可能吃虧 —— 所以只對"頻繁·連續使用相同上下文的工作"有選擇地應用。
成本這一章,是最容易讓人想放一張"把每月 $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 = 6000、max_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),要麼是明確標註"不知道"的近似值。
| 模式 | 為何失敗 | 對策 |
|---|---|---|
| 所有工作都用頂級模型 | 連分類·替換都付約 19 倍單價 | 按任務分配模型(§22.3.5) |
| 每次呼叫重發相同上下文 | 白白丟掉快取命中的 1/10 | 快取固定上下文(§22.3.6) |
| 自動注入沒有上限 | 整座 atom 庫每次呼叫都被注入 | 數量·長度護欄(§22.3.2) |
| 靠"省著點用"的決心管理成本 | 人的自制力擋不住暴漲 | 把上限固定在程式碼裡 |
| 連確定性方法能辦的事也呼叫 LLM | 最便宜的呼叫是'不呼叫' | 把對映·替換分離到程式碼裡 |
第四條是核心。把成本控制交給人的意志,一定會洩漏。意志在忙碌時最先崩塌,而成本在忙碌時增長得最快。所以控制必須是 max_matches = 3 這樣的程式碼常量。
遊戲之外的應用。AI 成本可怕,不在於金額大,而在於看不見,這一點無論對遊戲團隊還是市場團隊都一樣。成本不是靠"省著點用"的決心,而是靠結構來管住。第一,按工作難度分配模型單價 —— 連簡單的分類·打標籤都用頂級模型來跑,就等於為同一件事付好幾倍單價;而簡單對映·替換乾脆不呼叫(用規則·公式處理)才是最便宜的呼叫。第二,對幾乎不變的長輸入(公司介紹·政策文件·術語表)做快取,減少重發成本。舉例來說,給客戶諮詢分類這件事用輕量模型就夠,只把複雜的合同審查交給更高檔的模型,就能在保住質量的同時把單價分流。如果有某處會自動附加很長的上下文,那麼事先定好"一次附加的數量·長度上限",就能從結構上防止某天賬單突然多出一個數量級的事故。
單人只需做到這些:沒有 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:挑出最長的輸入,親自用眼睛確認在上限處切掉了什麼。
max_matches=3·6000 字 truncate)。主要讀者:負責引入 AI 的遊戲總監與主管(中等規模(10\~50 人)團隊) 面向單人/業餘讀者的精簡版:§22.4.9「獨自一人的話,做到這些就夠了」
發售前兩個月,我們曾因一張概念美術師繪製的城市插畫而讓整場會議陷入停滯。有人問:"這是用 AI 生成的吧?那著作權歸我們,還是根本無法登記?"沒有人能回答。當場出現了三種意見:"是 AI 做的,所以不歸我們""是我們花錢跑出來的,所以歸我們""反正還沒有法律,直接用就是了"。三種都不對。而且這個問題並不只是一個法務議題。製作那張插畫的美術師其角色究竟是什麼、團隊對 AI 的使用達成了怎樣的共識,都在那一刻同時被牽扯了進來。
本章不會把著作權與倫理分開來講。因為在實務中,二者是同一個問題的一體兩面。"這份產出物的權利歸誰"(著作權)會直接歸結為"人在這份產出物中介入了多少"(倫理·角色)。韓國著作權委員會在 2025 年明確釘定的登記要件,恰恰就在這一點上。所以本章的脊柱是一份實操記錄(worked transcript,完整保留的真實操作過程記錄)—— 我們會實際判定一張 AI 概念美術圖能否進行著作權登記,並從輸入到決策一路追蹤這一判定如何延伸為團隊的角色共識。
作者實際運營備註 本章引用的
design_intent_vs_automation_boundaryatom 以及_economy_log·_roi_report.md,是將作者在公司實際運營的治理資產做了匿名化處理後的產物。atom 名稱與日誌檔名原樣保留了實際運營中使用的名稱(僅為保護 IP 而替換了公司與專案的專有名詞)。實操記錄的輸出是對真實判定會話的重現。
很多書只把 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.1 的指南標準作為規則手冊輸入,讓 AI 先做一次分類。人只做最後的判定。下面的輸入提示詞可以直接複製使用,輸出則是對真實判定會話的重現。
判定的輸入不是插畫本身,而是記錄該插畫如何被製作出來的日誌。這些資訊已經存在於資產後設資料中,只需提取即可。
# 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 基本法的標示義務
附上的 meta.yaml 是一張概念插畫的生成歷史。請依據韓國著作權委員會
《生成式 AI 利用作品的著作權登記指南》(2025)的標準,把它能否登記
分類為 可登記/部分登記/不可登記 三者之一。依據請用指南的兩條軸線(可控
性·可預測性)各給出一行,並在 generation_log 中用編號指出哪個 step 屬於
"創作性貢獻"。法律上無法斷定的,標為"需法務確認"交給人處理。
用表格呈現:分類 | 依據(可控) | 依據(預測) | 貢獻 step | 法務確認。
請注意,這段提示詞有一半是在做"強制依據"與"法務退路"。如果把法律判斷整個交給 AI,它會吐出貌似可信的斷言。給出分類標準(指南的三分法)、依據義務(兩條軸線)、指定貢獻 step,以及"無法斷定就交給法務"這一退路——這才是這段提示詞的核心。
| 分類 | 依據(可控性) | 依據(可預測性) | 貢獻 step | 法務確認 | |---------|------------------------------|----------------------------|----------|---------| | 部分登記 | 粗略草圖(step1)與 40% 精修 | 從 4 種中選擇可預測, | 1,3,4 | ★ | | | (step3)直接控制表現意圖 | 但 AI 的 4 種變體本身不可預測 | | | 分析: - step1(直接輸入粗略草圖):對應指南 ①"以本人作品輸入體現創作性"。 - step3(40% 精修):對應指南 ②"修改·增刪的追加工作"。貢獻最強。 - step4(親手設計封印陣紋樣):合成獨立作品,明顯的人類創作。 - step2(AI 的 4 種變體):無人類創作性貢獻的純 AI 產出物 → 這部分單獨不屬於登記物件。 ★ 需法務確認:"40% 精修"是否達到"實質性改變",缺乏定量標準。 精修比例是否越過可登記的界線,建議由律師審查。 (AI 無法斷定該比例在法律上是否充分。)
輸出中最有價值的部分不是表格,而是最下面的★ 法務確認。AI 主動申報了自己的界限——"我無法斷定 40% 精修在法律上是否充分"——並把它交給了人。好的提示詞會讓 AI 能夠說出"我不知道"。這不是自動拒絕型,而是自動申報型。
不能原樣接受這份輸出。總監用規則手冊再核查一遍。AI 把 step4(封印陣紋樣)分類為"獨立作品",但重新檢視生成歷史後發現,那個紋樣其實派生自 §6.2 中 city_hunting_generator 生成的城市 lore。也就是說,step4 未必是純粹的原創,而可能是疊加在公司內部資產之上的二次加工。由於是公司資產,權利歸屬很明確;但 AI 所用的"獨立作品"這一表述若原樣寫進登記申請書,會招致誤解。
於是再次發出請求。
step4 的封印陣紋樣是從公司內部城市 lore 資產派生的二次加工(並非獨立的全新原創)。
請據此重新分類 step4 的貢獻性質。
並用一行提議:登記申請時應如何註明"基於既有內部資產"。
這一次往返就閉合了。AI 把 step4 從"獨立作品"改答為"公司內部 lore 資產的演繹作品——原資產權利歸公司所有,變形貢獻為登記物件",該判定隨後交由法務審查。結論最終定為部分登記 + 標示 AI 生成事實。若全部手工進行,法務就得逐個資產追問生成歷史;而有了 AI 初稿 + 規則手冊審校 + 一次往返,法務只需把時間花在標了 ★ 的邊界案例上。
這一整圈就是本章的 Show 標準。"AI 著作權很模糊"這句話,在你尚未依指南標準把一份產出物的生成歷史徹底分類一遍之前,都是空洞的。
為了不必每次都從頭做上面那種判斷,我們把指南標準記錄成一張流程圖。每進來一個資產,順著這棵樹往下走即可。所有分叉點都出自 §22.4.1 的公開標準。
關鍵在於:這棵樹的終點(J)在所有路徑上都相同。無論能否登記、是公司資產還是演繹作品,"使用了 AI"的事實標示與生成歷史日誌都無一例外地保留。 標示是與權利無關的獨立義務,而日誌是出事時追溯責任的唯一依據。開頭那場會議之所以停滯,正是因為沒有這份日誌,沒有人能重現每個 step 由誰做了什麼。
紅色路徑(C,不可登記)也並非直接丟棄。"僅輸入提示詞的純 AI 產出物"在探索·概念階段作為參考完全可用,只是不把它作為最終資產放進遊戲而已。把 AI 輸出原樣推上線,是事後著作權事故最大的誘因。
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 會自動彈出"這個資產的著作權確認了嗎",但能否登記的最終判定由人和法務來做。
開頭那張插畫的判定並未止於著作權。因為它意味著,製作該資產的團隊成員 A,其工作已從"繪製"轉移到了"從 AI 的 4 種中做選擇、並精修其中 40%"。在著作權要求"人類創作性貢獻"的那一刻,做出這份貢獻的人,其角色定義也隨之改變。二者是同一事件的一體兩面。
這裡最常見的事故,是把這種變化當作單方通知來處理。即便工具再好,半年後無人使用,多半不是工具不好,而是當初沒有達成共識。角色從量產轉向選擇·審校·重構,這才是引入 AI 的本質;若不把這一點明示出來並以培訓加以支撐,團隊成員就會把它理解為"我的位置要沒了"。
| 職群 | AI 之前 | AI 之後(角色演進) | 著作權上的意義 |
|---|---|---|---|
| 概念美術師 | 全部親手作畫 | 輸入意圖·選擇·精修 | 精修即"創作性貢獻" |
| 數值策劃 | 手動模擬 | 解讀模擬·做決策 | 決策日誌即責任依據 |
| 策劃 | 全部親筆撰寫規格 | 提供意圖·審校 | 輸入意圖即可控性 |
這張表想說的一句話是:讓著作權登記成為可能的"人的貢獻",正是角色演進之後人所做的事。 一旦指南所要求的可控·可預測消失,著作權會消失,人的位置也會消失。所以,角色演進必須被解釋為"把權利與責任留在人手中"的變化,而非"搶走崗位"的變化,才能達成共識。
共識不是無休止的開會,而是用流程閉合:引入提案(總監)→ 全團隊事先共享(目的·受影響角色·衡量指標·風險)→ 共識會議(自由發言·收集顧慮)→ 與必要成員一對一 → 釋出調整方案 → 達成共識或擱置。並不是必須得到所有成員的同意才能啟動,而是通過流程聽取並調整顧慮之後,由總監拍板。沒有流程,共識每次都要從 0 重新開始,而這種成本會拖慢引入。
_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 和資產後設資料實際數出來。治理承諾的不是結果,而是流程的完整性。
把資產權利·角色·成本理順之後,還剩一個領域:使用者用 AI 製作的內容上傳到遊戲的通道。公司製作的資產用內部流程閉合,而 UGC 則是控制之外的產出物源源湧入。
這裡同樣直接套用 §22.4.3 樹的終點(標示·日誌)。使用者上傳的時裝·公會徽章要求標示 AI 生成,並以自動審校 + 人工關卡的組合來進行稽核(moderation)。只運營其中一條軸線,下個季度的事故就會累積。另外,使用者資料不能隨意傳送給 LLM。個人資訊·支付資訊禁止傳輸,行為日誌則以匿名化後再傳輸為原則(遵守 GDPR·韓國國內個人資訊保護法)。
管轄並不止於韓國一處。一旦接納海外使用者,該使用者所屬地區的資料法規也會一併適用。對 EU 使用者,GDPR 對個人資訊的跨境轉移·同意·刪除權設有專門要求;其他服務所在國家也各有其個人資訊·資料本地化規定。因此,表格第三行的"個人資訊·支付資訊禁止傳輸給 LLM"在任何管轄區都是最安全的預設值;而把行為日誌傳送給外部模型時,匿名化·假名化處理的強度則應按服務所在地區另行核查。不過,本節只是流程設計的指引,並非法律諮詢。一旦涉及全球發行·跨境轉移,務必另行接受相應管轄區的法務審查。
| UGC/資料 | 政策 | 依據 |
|---|---|---|
| 使用者上傳的時裝·徽章 | AI 標示 + 自動審校 + 人工關卡 | AI 基本法標示義務 |
| 角色暱稱·發帖 | 通用條款 + 使用者責任 | —— |
| 個人資訊·支付資訊 | 禁止傳輸給 LLM | 個人資訊保護法 |
| 遊戲行為日誌 | 匿名化後傳輸 | 匿名化·假名化處理 |
UGC 越多,稽核負擔越重。自動審校先做一次篩除,只有邊界案例才交由人來看。這正是把 §22.4.4 的 atom 哲學(機器挑候選、人來決定)搬到了使用者內容層面。
| 模式 | 為何失敗 | 處方 |
|---|---|---|
| 把 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 基本法義務)都無一例外地保留。權利之後是人,所以做出這份貢獻的員工其角色變化,不能靠單方通知,而必須用共識來閉合。
獨自一人的話,做到這些就夠了:沒有法務團隊也沒關係。挑一件你自己用 AI 製作的影像或文本產出物,按 §22.4.2 的格式親手寫一份
generation_log(用 step 區分哪一步是人、哪一步是 AI)。然後順著 §22.4.3 的樹,自己判定一遍"這個能登記嗎",你就會親身體會到韓國著作權委員會指南里"可控·可預測"標準究竟是怎樣一組判斷。哪怕是個人·業餘專案,也最好留下一行 AI 生成事實標示(ai_generated: true)。
如果是團隊,就從下面這一步開始。給所有 AI 資產後設資料強制加上 generation_log 與 ai_generated_disclosure 兩個槽位(用一行程式碼的 grep 就能抓出缺失),並把 §22.4.3 的決策樹貼成一頁維基。登記要件的判定自動化或 _economy_log 的運營,都是後話。哪怕只有生成歷史日誌和一頁決策樹,也足以避免像開頭那樣會議陷入停滯。
第 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
不要增加工具,而要製造工具的工具。這是關於一種兩層結構——在全域性 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。
工具太多這種印象人人都會有。但僅憑印象,無法決定該削減什麼。讓決策成為可能的,是月覆盤對工具經濟性的測量。
本專案把覆盤作為自我改進機制來運營。日覆盤累積成周覆盤,周覆盤匯聚成月覆盤的過程中,月覆盤會從 SVN 提交日誌中反推"過去一個月各工具用了多少次"。用於這項測量的分數就是 skill_audit_score。它通過提交歷史追蹤每個斜槓命令在實際工作產出中出現了多少,從而給出使用頻率。
那個月測量得到的分佈如下。(使用量佔比是基於 SVN 提交日誌的實測,不是絕對呼叫次數,而是各工具的出現佔比。)
排名前 12 的命令佔了總使用量的 92%,而每月一次都用不到的命令有 18 個,佔總數的 45%。答案已經定了一半:只把常用的 12 個暴露在全域性,其餘的整理歸置。
問題在於,"整理"並不等於"刪除"。那 28 個不常用的命令,每季度也總有一兩次會用到——寫半年報告時,建立新的資料模式時,或執行某項特定校驗時。那時如果工具不在,工作就會當場停下。所以真正的問題是:如何只讓 12 個可見,同時把 28 個保留下來。
書桌的比喻貫穿整章。沒有人會把 40 支筆全攤在桌面上天天用。只把常用的 12 支放在桌上,其餘收進抽屜。抽屜裡,同類的筆再歸到一個筆筒裡。Wrapper 是放在桌面上的輕量入口,Junction 是連線抽屜與桌面的通道,Cascade 是捆在一個筆筒裡的一束筆。
Wrapper 是斜槓命令的一層薄殼。全域性只放入口,實際邏輯放在 workspace 的本體裡。全域性目錄裡住著 50 行的說明,本體裡住著 500 行的實現。
這一分離帶來五點好處。會話開始時全域性只加載 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 個更合適。只有存在既定上限,認知負擔才會停留在一定水平。
如果說 Wrapper 是"全域性只放輕量入口"這條規則,那麼 Junction 就是在作業系統層面實現這條規則的手段。Junction 是目錄符號連結,也就是 OS 提供的別名。
使用者檢視全域性位置時,看上去本體就在那裡。但實際檔案只有一份,存在於本體位置。全域性那一側,只是指向那裡的一塊路標而已。
這一結構帶來的好處很清楚。修改本體會立即反映到全域性(沒有複製步驟)。檔案只有一份,節省磁碟;全域性只有 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 從根本上消除了這種事故的可能。路標不可能有兩塊,實體永遠只有一個。
手動管理 Wrapper 和 Junction,最終還是會回到 40 個。人會拖延整理、忘記政策、製造例外。因此把一致性維持自動化,那個工具就是 sync_skills.py。
每次會話開始時,Hook 都會觸發這個指令碼。指令碼所做的事是以下流程。
核心功能有三個。第一,掃描全域性目錄,檢查是否符合 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 讓這一結構隨時間推移仍得以維持。
三種模式與自動一致性結合起來,就完成了下面的兩層結構。上層是使用者需要記住的 12 個入口,下層是 48 個本體。
使用者只需記住全域性的 12 個。哪怕它們背後藏著 48 個本體,認知負擔也停留在 12 個。Wrapper 讓入口保持輕量,Junction 把入口與本體連起來,sync_skills.py 在每次會話中守住這 12 個的一致性。
按比例看,入口與本體是 1:4(12 比 48)。工具再增加,使用者要記的也不會增加。本體增加到 60 個、80 個,第一層仍然是 12 個。這就是"不要增加工具,而要製造工具的工具"這句話的實際實現。增加的是第二層(本體),而使用者面對的第一層(入口)始終恆定。
如果說兩層結構是"把眾多工具收斂為少數入口"的模式,那麼 Cascade 就是"把經常一起用的工具打包進一次呼叫"的模式。一個斜槓命令依次呼叫多個下級工具,再把結果匯成一份綜合報告。
本專案最具代表性的 Cascade 是 check。它把每天早上檢查策劃資料完整性的四個工具整合成了一個。
過去每天早上要分別呼叫四個工具:文件檢查一次、資料檢查一次、外部索引鍵檢查一次、連結檢查一次。每個工作週期要手動呼叫三四次。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。
兩層結構和 Cascade 一旦穩定下來,增加本體就變得容易——因為不必動全域性槽位,只要往 workspace 裡新增本體即可。可正是在這裡出現了新的陷阱:新增一旦變容易,相似的工具就會重複堆積。
因此在增加本體時強制一條政策:MECE Wrapper 政策。要新增新工具時,分兩條路判斷。若與既有工具領域重疊,就不新建,而是增強既有工具;只有領域明確不同時才新建。意思是讓本體清單做到無重疊(Mutually Exclusive)、無遺漏(Collectively Exhaustive)。
這一判斷由覆盤來支撐。skill_audit_score 從 SVN 日誌中測量各工具的使用頻率,於是能捕捉到"這個工具其實和那個工具做的幾乎是同一件事,而兩者都幾乎沒人用"這樣的訊號。那就把兩者合併,或把使用較少的一方從本體中撤下。治理中,整理與新增同樣重要。
沒有 MECE 政策,兩層結構帶來的"新增本體的自由"反而會變成毒藥。即便本體不蠶食第一層槽位,本體本身因重複而臃腫起來,又會讓人重新搞不清該用哪個本體。政策阻止這種臃腫。
到這裡,回到開頭看看。Wrapper 也好,Junction 也好,Cascade、MECE 政策也好,沒有一個是在書桌前預先設計出來的。它們全都是作為對覆盤中發現的問題的回答而誕生的。
當月覆盤的 skill_audit_score 用量化資料揭示出 40 個槽位與 92% 使用量的失衡時,"限制到 12 個"就此定下。那個決定之後,緊跟著"那 28 個又該如何保留"的問題,答案就是 Wrapper 與 Junction。下一次覆盤中出現了"每天早上分別呼叫四個相似的校驗工具太麻煩"的發現,答案就是 check Cascade。又一次覆盤中捕捉到"新增本體變容易了,重複就堆積起來"的訊號,答案就是 MECE 政策。
沒有覆盤,這些模式就不會誕生;就算做出來,也會淪為與真實問題無關的過度工程。先用測量發現問題,再引入模式——正是這個順序,讓工具真正被用起來。覆盤是自我改進的起點,這一 Part 21 的資訊,在工具層面就這樣被具體化了。
把六個月的測量值按引入前後作比較。關注的不是絕對呼叫次數,而是運營負擔的變化。
| 專案 | 引入前 | 引入後 (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,而本體同步中手動複製這一步已經消失。
把前面各節點到的陷阱彙總到一處。這五點,全都指向同一個教訓:比起搭出結構,讓它隨時間推移仍能維持更難。
sync_skills.py --cleanup 在每次會話中整理(§23.1.4)。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 只需在經常一起呼叫的工具出現兩個以上時,把它們打包為一個命令即可。若嫌自動一致性指令碼麻煩,可以用"開始新會話時用眼睛把全域性目錄掃一遍"的習慣來替代。規模一小,習慣就替代自動化做著同樣的事。
晚上 11 點 47 分。我儲存好最後一份資料表,合上了筆記型電腦。次日早上 9 點 10 分,衝咖啡時開啟公司內部的即時通訊工具,發現頻道頂部已經掛著一份報告。那是一份 Markdown——它以外部索引鍵為基準,對昨夜更新過的三張數值表做了交叉校驗,並用紅色標出了兩處斷裂的引用。不是我寫的。它是在我熟睡時生成的。
本章記錄的,是把製作那份報告的工具——Hermes Agent——裝到個人 PC 上,併疊加到 §23.1 講過的 Wrapper·Cascade·Junction 運營之上的整個過程。最初引入時,Hermes 還是基於 Linux 的,要在 Windows 上使用就得先經過 WSL2;到了 2026 年,原生 Windows 構建釋出,這道彎路就消失了。先把結論說在前面:智慧體(Agent)並沒有把 Claude Code 擠走,而是坐到了它旁邊。
到 §23.1 為止的運營,全部以 Claude Code 為中心。我輸入一句話,工具就響應一次,我審閱完這次響應,再輸入下一句。這種短週期對精細作業再好不過。如果是修改一個數值、每一步都需要確認的作業,那就該由人每次介入。
問題出在耗時長的作業上。"把過去一個月的 30 份會議記錄全部讀一遍,只把決策事項抽取成 atom 候選"這樣的請求,若放在對話流裡處理,需要 30 個來回。那 30 次裡,我沒法做別的事。對這類作業,輸入與輸出短促相連的工具,其長處反而成了短處。
智慧體填補的正是相反的位置。只要丟擲目標——"從 30 份會議記錄裡把決策事項挑成 atom 候選,匯成報告"——它就自己挑選工具、自行走完中間步驟,結束後只把結果拿回來。週期長而自主。代價是,每一步人看不到,這個短處也隨之而來。
拿鄰座同事來打比方就容易理解。Claude Code 是對我每一句話都一起細看的搭檔,智慧體則是主動請纓上夜班、在我上班前把報告放到桌上的助手。兩者不是誰解僱誰的關係,而是共用同一張桌子。
在 §23.1 中,我把全域性斜槓命令槽收攏為 12 個,再用 Junction 把背後的 48 個本體藏起來,做成了這樣一套運營。不增加工具,而是造出工具的工具——這就是那時的結論。可如今又要引入一個新工具,聽上去與那個結論自相矛盾。
並不矛盾。§23.1 的 12 槽策略,處理的是"人直接呼叫的工具"的認知負擔。而 Hermes 要填補的位置,是人不去呼叫的時間——熟睡的時間、開會的時間、被別的事拴住手的時間。它不是與 12 槽競爭,而是填補 12 槽夠不到的時段。
引入決定的依據,是一項覆盤測量值。用從 SVN 提交日誌反推全域性工具使用頻率的 skill_audit_score 跑了一個月的資料,發現排名靠前的工具大多屬於"在人清醒的時間、短促、頻繁"使用的那一類。相反,那些使用頻率低、但一旦跑起來就很耗時的作業——會議記錄批次分類、資料表夜間一致性、構建捕獲分析——卻每每被"明天早上再做吧"地往後推。往後推的原因很明確:它們會長時間佔用清醒的時間。
這類被往後推的作業群,正是智慧體精準瞄準的目標。
最初裝 Hermes 時它基於 Linux,要在 Windows 個人 PC 上使用,就得先裝好 WSL2(Windows Subsystem for Linux 2),再把 Hermes 安置進去。如今有了原生 Windows 構建,這道彎路就不必了。安裝和一般的 Windows 應用程式一樣——下載安裝器執行,在首次執行時設定好工作空間路徑與許可權白名單的初始值即可。
如果你已經在用 WSL2,或偏好 Linux 環境,那一側的構建同樣受支援。只是若從頭開始,原生這一側更簡單。具體的安裝器與版本因工具更新很快,請以官方文件為準。
無論裝在哪兒,都有一個繞不開的坑。Hermes 工作空間必須放在快速的本地磁碟上。若把網路驅動器或 SVN 工作資料夾直接接為工作空間,一次夜間一致性檢查,會把本該幾分鐘的作業拖成幾十分鐘。資料表放在工作空間之外,只在作業開始時複製進來,才是正道。若用 WSL2,出於同樣的原因,要把工作空間放進 Linux 檔案系統內,不要來回跨越 /mnt/c 這類 Windows 路徑。
從這裡開始,是真正要動手的部分。比起安裝本身,"裝好之後讓它做什麼"才是本章的核心;因此我們把第一個作業從頭跟到尾——提示詞全文、原始輸出、人工驗證、再請求——完整走一遍,如實保留操作全過程,這樣一份記錄即所謂實操記錄(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 的夜間自動化。
第一個作業上手之後,就把被往後推的作業群一個個挪到夜間。我實際疊加上去的是三個崗位。三者的共同點很清楚——它們都把人無需清醒在場的時間,變成了幹活的時間。
崗位 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.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 裡第一個作業選夜間一致性檢查,也是同樣的道理——只讀,最壞也不過是一份錯誤的報告,原始資料不會受損。
在更新本章的這個時點,引入已進入穩定期。原生 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.4 的實操記錄展示了相反的一面——智慧體在一個列對映處停了下來,而那個判斷由人來補上。遊戲策劃的核心決策依然歸人,智慧體拿走的是反覆與分析裡枯燥的部分。
"裝一次就全自動"的期待也危險。頭一兩個月反而更費手。列名對映、許可權範圍、成本上限都要按作業逐一調校,在這套調校純熟之前,每一份輸出都要人來審閱。
"Claude Code 如今過時了"這種斷言是錯的。兩者所處的時段不同。白天的精細決策交給 Claude Code,夜晚的無人反覆交給智慧體。§23.1 的 /check Cascade 並沒有消失,只是在它旁邊又多出了一條夜間泳道。
"開源所以免費"的認知只對了一半。本體雖免費,模型 API 的呼叫費用照樣要花。所以 config.yaml 裡 agent.max_turns·compression 這類成本上限,既是安全裝置,也是賬本。
最後,"連複雜又危險的作業也交給智慧體"這種期待最危險。作業的風險越大,越要置於人的控制之下。交給智慧體的,從簡單且易於回退的作業開始。委派只隨信任的累積而拓展。
如果說 §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 內用後臺執行跑一跑精簡版。
做季度覆盤時,我打開了全域性技能資料夾。一行一行數下來,wrapper 有 19 個。明明定好了只運營 12 個並這樣跑了一年,不知不覺卻又多出了 7 個。更離譜的是,其中一半光看名字根本想不起來是做什麼的工具。migrate-legacy-enum。這是什麼來著。上一次用它是什麼時候來著。
想不起來。只要依賴記憶,這個問題就永遠無法回答。於是我決定不看記憶,而是看日誌。工具策展不應是憑喜好去裁剪的工作,而應是用"上個季度呼叫了這個工具幾次"這樣的數字來裁剪的工作。
本章記錄的是:如何自動地把這個數字提取出來,如何用這個數字裁掉工具,以及如何從一開始就阻止工具暴增。
在談策展之前,必須先承認一件事。工具只要不加阻攔就一定會增多。這不是因為意志力薄弱。而是因為每次任務中"就這一次,為了快點處理"而寫一個小指令碼,本身是合理的選擇。而這種合理的選擇累積幾十次,就成了不合理的一堆廢物。
專案A 中運營的結構,是全域性的 12 個 wrapper 通過 junction 指向 workspace 中的 48 個本體。全域性這一側很輕,沉重的本體則放在用 SVN 管理的 workspace 裡。這個結構本身已在 §23.1 中講過。問題在於,這個 12 的數字並不會安分待著。
看看工具增多時還有什麼一起增多,就能清楚為什麼必須加以阻攔。
尤其是第一項,上下文 token 佔用,是進入 AI 工具時代後變得更為尖銳的成本。全域性 wrapper 一多,每個會話中 AI 讀取"我可用的工具清單"所需的 token 就會增多。為了讀 19 個工具的說明,真正能用在任務上的上下文反而減少了。因此專案A 的 sync_skills.py 帶有 --cleanup 選項,會自動清理 junction 已斷裂或本體已消失的 wrapper。這更接近一種為守住 token 預算而做的衛生工作。
但 --cleanup 能抓到的只有"斷裂"的工具。那些好端端活著、卻沒人用的工具,它抓不到。要抓這類工具,就需要使用頻率資料。
核心思路很簡單。workspace 中的技能與工具全都在 SVN 裡。而且每次使用工具,該工具產出的產物(表格、文件、關係圖 HTML 等)都會提交到 SVN。也就是說,只要看 SVN 日誌,哪個工具實際幹過活就會作為痕跡留下來。
於是我寫了一個叫 skill_audit_score 的小測量指令碼。正如其名,它為每個技能打一個"審計分數"。做這個工具時,我並不是一開始就把程式碼全寫好,而是向 AI 說明測量邏輯、拿到初稿。這裡把那份實操記錄(worked transcript,完整保留的真實操作過程記錄)原樣留下。不做摘要,連走彎路的部分也一併寫下來。
全域性技能 wrapper 定為 12 個,卻不知不覺膨脹到了 19 個。我想在季度覆盤時用資料挑出"不用的工具"。請按以下前提寫一個測量指令碼。
- 每個技能工具都以子資料夾的形式存在於 workspace 資料夾中(例如
tools/relation-map-gen/)。- 這些工具產出的產物會提交到 SVN。提交資訊裡通常會帶上工具名稱或產物種類(例如 "relation-map regen"、"schema-doc update")。
- 我想解析 SVN 日誌,統計最近 90 天裡每個工具參與的提交數。
- 僅憑提交數還不夠。(a) 越近的加權越高,(b) 最後使用日越久則扣分,把這兩點合起來給出一個 0\~100 的 audit_score。
- 輸出為按分數降序的表格。分數低的工具就是"策展候選"。
- 假設 SVN 命令以
svn log --xml -l 2000的形式獲取。只用 Python 標準庫。絕對路徑通過引數傳入。
好的。不過有一點要先說明。並不能保證提交資訊裡一定帶有工具名稱。如果人隨意撰寫提交資訊,像"修改表格"這樣的資訊就無法判斷是哪個工具參與的。因此我會並行採用兩種匹配方式。
- 在提交資訊中匹配工具名稱·別名關鍵詞(寬鬆)
- 匹配變更的檔案路徑是否屬於對應工具的資料夾或該工具的產物模式(例如
*_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-creator 和 excel-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 區分"才是這個工具的本質。
skill_audit_score 只是一個測量工具。必須有一個把測量值嵌入季度覆盤、轉上一圈的週期,工具才會真正被整理。那個週期如下。
區分這個週期的兩個出口很重要。分數為 0 的工具並不是一律刪除。如果那項工作本身已經消失,就送去完全廢棄(--cleanup);如果那項工作仍然需要、只是沒有頻繁到值得單設一個工具,就把它吸收進現有工具。後者正是 §23.3.4 的 MECE 增強。
即便廢棄,SVN 歷史裡也會留下程式碼。收回的只是 junction 和全域性暴露,並不是把程式碼本身永遠抹掉。半年後那項工作再次出現,從 SVN 恢復即可。正因為有這張"可以撤回"的安全網,人才敢果斷地裁。
比起測量後再裁,更好的是一開始就不去建立。如果說 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,連同策展紀律一起交到它手裡。
在運營本章這個工具的過程中,我學到最多的一點是:絕不能盲信測量值。skill_audit_score 只看 SVN 日誌這一種訊號。因此它在結構上有必然會漏掉的東西。
excel-reader 這種只讀取表格、不產出產物的工具不會留下提交。因此把 confidence 降為 LOW、轉入手動檢查的機制是必不可少的。概括來說,這個工具不是"做決定的工具",而是"縮小候選的工具"。它讓你一眼看清 19 個工具,1 秒內告訴你"該懷疑哪一個"。而驗證那份懷疑、並動手裁掉,則留給人來完成。測量並不取代人,只是指出人該去看的地方。
親自把工具策展週期轉上一圈的步驟。
setup
1. 確認工作區的技能與工具都在版本管理(SVN/Git)之內。產物也必須提交到同一個倉庫。
2. 製作待測量的工具清單。為每個工具寫上 name、alias(會出現在提交資訊裡的別名)、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 分鐘就能擋住工具墳場。
週六下午,妻子正用手機玩一款顏色分類的益智遊戲。那是一款名為 Yarn Fever 的遊戲,把纏結的毛線團按顏色分類到相應的籃子裡。一局結束,她就說"又是一樣的",然後關掉。因為沒有更新,很快就膩了。
那一刻的想法很簡單。那個迴圈有著經過驗證的上癮性,而機制本身不受著作權保護。換成動物主題,再把關卡程式化地無限生成,"又是一樣的"這個問題就消失了。一個人用直接在瀏覽器裡執行的 HTML 3D 來做,連裝到妻子手機上都不需要。
問題在於,我不是圖形工程師。雖然是 24年資歷的策劃,卻從沒用 Three.js 寫過著色器。所以這一章,是"和 AI 一起、獨自在幾天內讓一款遊戲跑起來"的真實記錄。它同時也是一份分離的記錄——使用與公司 MMORPG(以下稱專案A)工作相同的工具,卻一行領域內容都沒有摻入。
實際的遊戲在 critter-sort/ 倉庫裡,git 標籤 v0.1\~v0.3 記錄著三天間的決策。這不是加工過的案例,而是直接引用那個倉庫。
最先做的事,是把原作用語言拆解開、拋給 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 並沒有給出錯誤的答案。是我的提示詞錯了。所以下一個迴圈不是修改程式碼,而是修正定義。
看過 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 會解開這些線股使其消失
把這裡得到的教訓用一句話記下來。
光靠語言描述只是"改了兩次",但 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
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)的倍數,所以無論哪個遊戲板都能被巢精確整除。無法解開的關卡從根本上不會出現。
設計上可解並不等於證明。在 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 裡記為"收集妻子的試玩反饋(最優先)"。
從種子出發,分岔為引數·遊戲板·網格·驗證的這條流程,正是從結構上解決"因沒有更新而厭倦"這一最初問題的答案。
程式化的 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 包,遊戲也不會停下。
在這個專案裡,我只是一名策劃,但工作是靠多種角色運轉起來的。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 被錯誤注入到益智遊戲開發中、成為噪聲的上下文汙染。只讓工具流動、把內容攔住——兩者之間就是健康的分離。
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 做出原型,然後親自試玩,只看那一句話是否還活著。如果死了,就把那一句話寫得更具體些再來一次。守住標誌性手感這一句話的習慣,僅憑這一點,就能避開逆向工程的第一個陷阱。
週一早上站會剛結束,資料團隊的成員 A 用即時通訊工具發來一張截圖。那是一份 QA 報告,說遊戲內商店裡某個材料道具的說明是空白的。追查原因 30 分鐘後,真相浮出水面:兩週前有人在策劃文件裡把那個道具改名為 재료_목재_상,而資料表中的引用仍然指向舊名 재료_목재_A。文件更新了,資料表沒更新,連線二者的連結悄然斷開。沒有任何人說謊,遊戲卻在輸出謊言。
這類事故會隨著文件增多而以幾何級數變得越來越頻繁。人眼無法同時看清 50 份文件之間的相互引用。於是我們把驗證委託給程式碼。本章討論的系統,讓文件、資料、連結的一致性由指令碼而非人來檢查。核心有三點 —— 來源一致性(_source_map.tsv audit)、連結完整性(wikilink),以及 stale 檢測(揪出陳舊腐壞的引用)。
文件與資料相互指向、彼此依存地存活。策劃案引用 enum,enum 引用資料表,資料表又引用另一份策劃案裡的決策。若由人手工管理這張網,一旦某個節點發生變化,就得靠人記住並逐一追蹤所有指向該節點的引用。而記憶會失效。
斷鏈之所以危險,是因為它不會丟擲錯誤。若是程式碼,引用不存在的變數時編譯器會攔住你。但在文件裡寫下的 [[재료_목재_A]] 這類 wikilink,即便目標消失,也只是留作一段普通文本。它不會變紅。遊戲照常構建、上線,直到玩家看到空白說明,才有人察覺。
因此,驗證系統的第一項工作,是讓人眼看不見的東西顯現出來。把一致性違規拉成文本輸出,再把這份輸出繫結到構建關卡上,那麼即便人忘了,指令碼也不會忘。
驗證不是一整塊,而是分階段的。先跑最廉價的檢查,濾掉明顯的違規,只讓通過的進入下一階段。因為如果對所有輸入都跑昂貴的檢查,會慢到沒人願意跑。下面是筆者實際執行的驗證流程。
這個 cascade 的核心是失敗越早越便宜。source_map audit 只是 TSV 單行比對,毫秒級就結束。相反,最後的 integrity_check 要載入整張資料表來檢查 FK 關係,需要數秒。把廉價的檢查放在前面,明顯的錯誤就在那裡被截斷,昂貴的檢查只對通過它的少數輸入執行。
各階段的輸出不同,這一點也很重要。audit 給出 FAIL(編輯者手動改動過某處的證據),wikilink 在自動修復後給 FAIL,stale 給 WARN(不阻斷,但需複審),integrity_check 給 BLOCK(直接攔住構建)。同樣是「問題」,也要根據嚴重程度作出不同反應,人才能區分訊號與噪聲。
_source_map.tsv audit最先執行的檢查是來源一致性。筆者的文件生成管線,會把某份合成文件(例如 GDD,即遊戲設計文件的正文)是從哪些原始檔生成的,記錄到 _source_map.tsv 裡。每一行都釘死了「這一產出物的章節 = 這些原始檔的合成」這樣一條譜系(lineage)。
它之所以能成為驗證工具,是因為只要有人手工編輯產出物,對映就會破裂。若有人直接改動自動生成的 GDD 章節,那一節就不再是原始檔的忠實合成了。audit 指令碼會把產出物各章節的雜湊,與從原始檔重新合成得到的雜湊比對,不一致就給出 FAIL。「手動編輯即 audit FAIL」這條規則,就來自這裡。
這並非要阻止人去編輯,而是要讓編輯顯式化。如果必須修改產出物,就該二選一:要麼改原始檔再重新生成,要麼把那一節正式從對映中剝離出來(分離宣告)。把無聲的編輯變得吵鬧,這就是 audit 的工作。
通過 audit 後,就進入連結檢查。筆者的文件用 Obsidian 式的 wikilink [[目標]] 來連線節點。wikilink_apply.py 做兩件事 —— 把 wikilink 解析為實際路徑並應用,以及在可能的範圍內修復斷鏈。
能夠修復的情形很明確:目標節點只是改了名字、仍在原位存在時。像前面 재료_목재_A → 재료_목재_상 這樣的重新命名,只要別名對映(alias map)已更新,指令碼就會把舊名自動糾正為新名。反之,若目標被整個刪除、或無法追蹤去向,就放棄修復,並報告這條斷鏈引用。
這裡有一個設計判斷:自動修復若過於激進則很危險。如果去找「名字相似的」就擅自接上,連結會錯接到語義不同的節點上,釀成更糟的事故。因此 wikilink_apply.py 的修復是保守的 —— 只對有顯式別名對映的重新命名做自動糾正,需要猜測的情形則交給人。自動化的美德在於剋制:只把確定的事情自動做掉,而把含糊的事情誠實地交還給人。
即便連結活著,引用也可能已經陳舊。文件 A 引用資料表 B,若 B 比 A 更晚更新,那麼 A 的說明就有可能與當前的 B 相牴觸。連結本身完好無損 —— 因為指向的目標還在。可內容已經腐壞了。
stale 檢測會比較引用兩端的修改時間(或內容雜湊版本)。若引用方比被引用目標更舊,就丟擲 WARN,並把該節點登記到複審佇列。之所以是 WARN 而非 BLOCK,是因為更新並不總意味著內容衝突。若只是改了一個錯字的更新,引用依然完好。所以 stale 不是「攔截」,而是「標記出來讓你去檢視」。
來看這一階段如何抓住前面那起斷鏈事故。如果 재료_목재 資料表比文件更晚更新,那麼在自動修復之前,stale WARN 就已經先亮起了。也就是說,三道檢查是相互重疊的安全網。一張網漏掉的,由下一張網接住。cascade 之所以能抓住單一檢查抓不到的事故,原因正在於此。
若讓人從零把驗證邏輯全部寫出來,會很累。筆者用自然語言描述檢查規則,執行指令碼則先向 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 沒有點出的假設,由人有意識地接受下來,並寫進了註釋。即便是自動化寫出的程式碼,程式碼所依賴的假設也由人來負責。
就算有指令碼,沒人跑也是白搭。驗證的最後一環設計,是讓它無法不被執行。筆者把這三個階段繫結到提交前鉤子(pre-commit)或構建管線上。audit FAIL 與 integrity_check P0 違規屬於 BLOCK,會擋住提交/構建;wikilink BROKEN 與 stale 屬於 WARN,放行但留下報告。
這套 BLOCK/WARN 的二分,決定了系統能否存活。若把一切都設為 BLOCK,一個微不足道的 stale 就能卡住提交,人們便會開始繞過驗證本身。被繞過的驗證等於不存在的驗證。反過來,若全設為 WARN,連真正該攔的資料完整性違規也會徑直通過。該攔什麼、該只標記什麼,這條界線才是驗證系統真正的設計著力點。
這是筆者在自己所在的某 MMORPG 開發商 A 的專案A中,以約 90 份文件的規模為基準觀察到的方向。部分絕對數值為筆者估算(未經驗證),真正有意義的是趨勢。
| 條目 | 手動檢查時期 | 程式碼驗證 cascade |
|---|---|---|
| 斷鏈引用的發現時點 | 玩家·QA 報告之後 | 提交前(方向:事後 → 事前) |
| 單次一致性檢查耗時 | 數小時(筆者估算) | 數十秒(指令碼實測) |
| stale 累積潛伏 | 潛伏數週 | 下一次提交即 WARN |
| 錯誤自動修復導致的事故 | 不適用 | 靠保守修復,保持 0 起 |
與其照單全收這些數字,不如只相信「發現時點從事後被提前到了事前」這個方向。驗證系統真正的價值,與其說在於節省時間,不如說在於事故在到達玩家之前就被攔下這一「位置的移動」。
| 模式 | 處方 |
|---|---|
| 把所有違規都設為 BLOCK,導致人們繞過驗證 | BLOCK/WARN 二分,只對資料完整性 P0 做阻斷 |
| 自動修復激進到連猜測也做 | 只對顯式別名的重新命名自動處理,含糊則交給人 |
| 只看斷鏈而忽視 stale | 用修改時間比對,單獨檢測陳舊引用 |
| 默許對產出物的手動編輯 | 用 source_map audit 把編輯顯現為 FAIL |
| 有指令碼卻沒繫結到鉤子 | 接入 pre-commit·構建關卡,使其無法不被執行 |
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 就行。
一位新人策劃入職第三天問我:"前輩,這些系統之間以怎樣的順序相互影響,有沒有整理成圖的地方?"我猶豫了。圖是有的。半年前有人在白板上畫的照片,存在維基的某個角落裡。可那張圖裡,如今已經消失的兩個系統還活著,而其後新增的三個核心迴圈卻沒有畫進去。最終我答道:"別信圖,去讀文件。"這是個讓人羞愧的回答。當圖與文件不一致的那一刻,圖就不再是資訊,而成了錯誤資訊。
先把本章的結論說在前面:人手繪製的圖表,一兩個月內必定腐壞。因此必須把畫圖這件事從人的手上剝離,讓文件結構本身吐出自己的圖。本文用一次真實的操作記錄來展示這個過程。我把一段以文件為輸入、生成 Mermaid 程式碼的實操記錄(worked transcript,即完整保留的真實操作過程記錄)整段收錄,並在本頁真實渲染由此產出的圖表。也就是說,講解某種技法的文字,用這種技法的產物來證明它自己。
圖表工具很多。draw.io、Figma、Visio,甚至白板照片。這些工具有一個共同的陷阱:產物是圖片檔案(影像)。影像無法在 git 中逐行追蹤改動,處理文本的 LLM 無法直接生成或修改它,也無法以程式碼形式嵌入 Markdown 文件。從運營角度看,最致命的是第一點。一張無法追蹤誰在何時、為何改動的圖,時間一長就會變成無人負責的遺物。
Mermaid 一次解決這三點。把圖表寫成文本,渲染交給檢視器自行完成。因為是文本,git diff 連新增一個節點都能捕捉到。因為是文本,LLM 能讀能寫。因為是文本,它可以原樣放進 Markdown 程式碼塊。本章的正文正是明證。此刻你正在讀的這句話下面、即將出現的那些圖表,全都是 Markdown 裡的文本塊,會在本書構建過程中渲染成圖。
不過要防止誤解。完全沒有必要把所有運營資料都做成圖表。羅列條目用專案符號更快,比較數值用表格更快。Mermaid 勝出的場合只有三種:關係(什麼與什麼相連)、流程(什麼在什麼之後)、時序(誰在何時向誰傳送了什麼)。在這三者之外的場合硬塞圖表,反而會加重認知負擔。
從這裡開始,就是本章的主幹。這裡不做抽象講解,而是從頭到尾展示把一整塊真實文件轉換成 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,而不是人。
下面是我實際投餵的提示詞。一個字都未加修飾,原樣收錄。
請把下面的系統依賴備忘轉換成 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 與正文檢索對不上。用規則來切斷這份熱心。
下面是模型第一次返回的響應。不加提煉,原樣搬來。其中混著一個小問題,連這個問題一併收錄。
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 中,韓文標籤一旦含有空格,一些舊版渲染器就會渲染失敗。而這一點,模型無從知曉。
這一步在自動化中最常被省略,卻最重要。我沒有原樣接受輸出,而是拒絕了。拒絕理由有兩條。
第一,要把虛線標籤裡帶空格的韓文換成英文單詞(token),以確保渲染相容性。第二,弱耦合(虛線)和強耦合(實線)混在同一張圖裡,卻沒有顏色或樣式上的區分,一眼看不出來。我帶著這兩點重新提出了請求。
基本可以了。只改兩處。
1. 把虛線箭頭的標籤換成一個英文單詞(不含空格)。
"보상 훅 구독" -> subscribes,"파생 스탯 읽기" -> reads
原因:部分渲染器在韓文+空格的邊標籤上會渲染失敗。
2. 為了在視覺上區分虛線(弱耦合)節點與實線(強耦合)節點,
給像 economy_loop 這樣只有弱耦合的節點,用 classDef 加上淺灰色樣式。
3. 其餘保持不變。
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 都想不到。
上述實操記錄的最終輸出,不經手工謄抄,以程式碼塊原樣收進本書頁面。本書構建會把它畫成圖。這就是"用自己的技法證明自己"的實物。
一段文件節選,經過五輪往返,變成了進入 git、可由 LLM 更新、並在本頁渲染的運營資產。下週若加入 mail_box,只需在備忘裡寫一行,再投一次同樣的提示詞即可。用不著人動筆。
如果說前一張圖是"轉換的結果",這一張就是"轉換的過程"。我把剛才分五步走完的實操流程做成了流程圖。這張圖同樣是用相同方式交給 LLM 抽取的,並經過了相同的驗證。我把結果原樣收錄。
這張流程圖想說的有一點。我想強調的——不是用虛線,而是用粗箭頭——是中間那個菱形,也就是 人工驗證。若沉醉於"自動化"這個詞而把這個節點刪掉,第1步那種熱心的幻覺就會原樣寫進運營文件。自動化把人從畫圖中解放出來,卻不把人從判斷中解放出來。迴圈裡最後那條箭頭(文件變更時 → 運營文件節選)是關鍵。有了這條反饋迴路,圖表才不是一次性資料,而是與文件一同、不衰老而共同成長的資產。
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)
關鍵在於只用兩步就結束。宣告節點,連線邊。輸入裡沒有的節點,絕不會出現在輸出裡。這個指令碼接收三張決策卡,就會得到下面這樣的圖。
一個決策被另一個決策取代(superseded_by),連由此派生的副作用(side_effect)也用一條箭頭呈現出來。不必讀完幾十行文本記錄的決策日誌,只要這一張圖,"為什麼現在冷卻時間是 0.5秒"的來龍去脈五分鐘內就能弄清。
何時用 LLM、何時用指令碼?判斷標準很簡單。輸入是結構化資料(欄位固定的卡片、表格)就用指令碼,輸入是自由文本(會議記錄、備忘、對話)就用 LLM。對結構化資料用 LLM,只會平白背上不必要的幻覺風險;對自由文本用指令碼,解析規則會無止境地膨脹。
這些是在運營圖表自動化的過程中實際踩過的雷。
第一,過於複雜的陷阱。節點超過五十個,圖就不再幫助認知,反而妨礙認知。對策是把一屏限制在二十到三十個之間,再大就用 subgraph 把區域圈起來,或者乾脆把圖一分為二。
第二,更新中斷的陷阱。人們容易以為這隻發生在手繪圖上,但即便做了自動化,只要不改輸入文件,一樣會腐壞。對策就是前面流程圖裡的那條反饋迴路。讓輸入文件成為單一事實來源(single source of truth),圖表始終從那裡重新生成。
第三,過於抽象的陷阱。"系統大致是這樣連在一起的"這種程度的圖很好看,卻沒用。對策是在節點裡填入真實 ID(skill_runtime、D_B),而不是抽象名詞。正文檢索與圖表共享同一識別符號,才能從圖直接跳到程式碼。
第四,不經稽核就直接使用 LLM 輸出的陷阱。正如主幹第3步所見,模型可以在遵守規則的同時,做出會導致渲染失敗的標籤。對策是絕不把人工驗證節點從流水線中拿掉。
很想拿出數字,但這裡只講方向。下面的對比是作者在自己帶過的團隊裡體感到的變化,並非精確測量值,而是作者的估計(未經驗證)。
變化最鮮明的,是新人理解系統的速度。入職頭幾天才能搞清的"這些系統是怎麼連在一起的",在一張自動生成的依賴圖面前,縮短到了一小時上下。會議資料的準備也輕鬆了。以前開會前一天總得有人手工重畫一遍,現在把文件轉換一次就完事。最重要的是,過去當圖表與實際不符時冒出的"這張圖能信嗎"這個問題本身,幾乎消失了。輸入文件即是圖,文件對了,圖也就對了。
反過來老實說,自動化並非萬能。結構化程度還不高的早期階段的想法草圖,依然是白板更快。自動化要在結構大致定型之後才會發光。
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,不要新增列表裡沒有的節點"。把返回的程式碼塊貼到筆記最上方。就這樣。因為不用手畫,沒有更新負擔;只要輸入列表還活著,圖就永遠是最新的。
連線(wikilink)與分類(層級)是同一個問題的兩個入口。一個回答"這個決策會牽連到哪裡",另一個回答"這份文件住在哪裡"。
新加入的策劃在第二天早上問道:"戰鬥的全域性冷卻值是 0.5 秒,對嗎?依據在哪份文件裡?"我答不上來。決策記錄肯定在某個地方,可我記不清那是戰鬥規則手冊、會議記錄,還是季度報告了。我們三個人湊在一起,用 grep 把整個資料夾翻了個遍。同一個數字在六處出現,可其中哪個是"原始決策"、哪個是"引用副本",根本分不清。我們花了 40 分鐘。最後找到的,是埋在會議記錄裡的一行字。
那天晚上,我意識到缺了兩樣東西。第一,文件之間沒有顯式的連線。同一個數字出現在六處,卻沒有任何地方寫下"這個是從那裡引用來的"這根線。第二,文件沒有安身的層級。決策記錄散落在規則手冊、會議記錄、報告裡,沒有"決策就住在這裡"的約定。
這兩樣東西就是本章的主題。wikilink 把連線寫成文本,層級把分類約定成資料夾。二者看似是分開的技法,實則是檢索這一個問題的兩面。
文件只有 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 串聯起來的一個片段。節點顏色表示種類,箭頭表示引用方向。
這個小片段展示的是:新策劃那個問題的答案,其實早已在圖裡。順著進入 combat_global_cooldown_constant atom 的箭頭反向追溯,就能找到決策 D2026_Q2_017。不是 40 分鐘,而是一次反向引用。
我們把用 wikilink 串聯的物件只定為四種。種類一多,格式就會鬆動;格式一鬆動,grep 又會變得不準確。
[[combat_global_cooldown_constant]]。指向單文件單決策單元的 atom。[[D2026_Q2_017]]。以季度和編號標識的決策記錄。[[CombatFormula_v3]]。規則手冊、規格文件等大型文件。[[團隊成員 A]]。負責人、決策者。四種全部是 [[name]] 一種格式。name 必須全域性唯一。如果 atom 名字在兩處衝突,就會在圖裡合併成同一個節點,釀成"戰鬥的 cooldown"和"UI 的 cooldown"變成一個節點的事故。所以在 atom 命名規則裡,強制加上領域 prefix(combat_、ui_)。
只有表記約定還不夠。讓人手工給 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_cooldown 和 combat_cooldown_global 兩個 atom 時,如果先替換短的那個,長的那個的前半部分就會被汙染。按長度降序排序的這一行,擋住了這個事故。這是我第一次寫的時候漏掉的地方,直到真的出現了 [[combat_cooldown]]_global 這樣斷掉的結果,才補上。
第二,heal_links 經由改名字典(aliases)來修復。當 atom 名字從 combat_gcd 改成 combat_global_cooldown_constant 時,把舊名字自動替換成新名字,只有在字典裡也找不到時,才作為 dead link 上報。名字每改一次,不用手工去改 200 份文件,只需加一行 alias。
自動 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 造出候選",而是"幫我挑選候選"。關鍵在於,人有明確可以拒絕之處。若是自動釋出,一個錯誤的連結就會永久留在圖裡。
連結隨時間會斷。atom 被廢棄、名字被改、錯別字混進來。所以每次構建都跑一遍 wikilink lint。檢查項與處理如下。
[[name]] 的 name 不在登錄檔裡 → 構建警告,確認修復字典只把 dead link 設為警告而非阻斷,是有意的。在給 atom 改名的中間狀態會短暫出現 dead,如果把這個當成構建失敗來擋,作業就停了。取而代之,讓它先確認修復字典。格式違規和名字衝突則立即阻斷 —— 這兩者會汙染整張圖。
這個 lint 是自我證明的。wikilink_apply.py 造出的連結,由同一套系統的 lint 來檢查,其結果又作為另一個 atom 決策留存。工具用自己的標準驗證自己的產物,這個閉環是運營的基本骨架。
到這裡是連線。現在是分類。如果說 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 分鐘檢索本可以用"決策就在那裡"這一句話結束。
要讓層級作為檢索入口發揮作用,得同時守住五點。缺任何一點,分類都會崩。
combat/、narrative/ 能被檢索到,但 2026-Q1/、2026-Q2/ 六個月後沒人會開啟。時間由 git 記錄,沒理由再用資料夾分一遍。L1_systems/combat/skills/active/single_target/attack.md 是 5 層。路徑一超過一屏,人就沒法把位置裝進腦子。spec_、report_、decision_、char_ 把種類放進檔名。不看資料夾也能看出種類。_ 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 同時開啟兩個入口。
層級放著不管就會腐爛。空資料夾冒出來,擱了六個月的 draft 越堆越多,深度悄悄增加。所以每季度整理一次。刪掉空資料夾,給超過六個月的 draft 決定啟用還是廢棄,把深度 5 以上的平攤,給沒有 README 的資料夾補寫或廢棄,_archive 超過一半就壓縮儲存。沒有這個週期,層級就會被噪聲塞滿,訊號與噪聲的區分隨之消失。
把整個流程看成一張圖就是這樣。文件進來、被連線、被分類、被驗證、直到被廢棄,是一個閉環。
在這個閉環裡,連線(B·C·D·F)與分類(E·I·J)交替運作。二者不是各轉各的,而是在一份文件的生命週期裡相互咬合。
這些數字是作者在自己專案裡對比引入前後的方向性。不是精密測量值,而是同一件工作在兩種環境下做時,體感到的差異大小(作者觀察,未精密計量)。
在連線和層級立起來之前,新策劃的決策追溯問題,像開頭那個 40 分鐘的例子一樣,長的時候要花一兩個小時。引入之後,一次 atom 反向引用 —— 是分鐘量級。文件檢索從 5\~10 分鐘縮短到 30 秒上下,這是層級的語義分類與 prefix 一起作用的結果。因錯誤引用導致的事故(把已廢棄的值誤當作現行值那一類)從每季度好幾件減到一兩件 —— wikilink 明示了"這裡引用的是那個 atom",複製的值和原始的值不再混淆。
變化最大的是新加入者的適應。沒有層級,光是熟悉哪個資料夾裡有什麼就要花好幾天;沒有連線,就無從把握各系統之間如何交織。兩者齊備之後,靠資料夾 README 熟悉位置,順著 wikilink 圖自行探索系統間的關係。"只有問才知道的東西"變成了"順著走就看得見的東西"。
這個效果只有在兩個入口同時存在時才出現。只有連線沒有分類,圖是有了,卻不知道文件住在哪裡;只有分類沒有連線,資料夾是乾淨的,卻不知道決策牽連向何處。
連線這一側最常見的失敗是噪聲連結。以為 wikilink 好,就給所有名詞都加方括號,圖就會被無意義的邊塞滿,視覺化工具隨之失靈。原則是隻留下能提出並回答"這份文件與那份文件是什麼關係"的連結。其次是自動釋出 —— 把 LLM 造的連結不經評審就提交,就會像實操記錄裡"資源再生"那一行那樣,把錯誤的連線永久留下。apply 是自動的,釋出則由人來做。
分類這一側的失敗大多是五條原則的違反。按時間分資料夾、深度 5 以上、檔名無規則、缺 README。還有最難挽回的 —— 刪除廢棄資料。被刪掉的決策依據無法重建。送往 _archive 的那一行,守住了六個月後的學習資料。
_archive,決策的依據才能事後存活。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 份之後再引入也不遲。
懷疑資料的那一刻,總是來得太晚。往往要等到錯誤的數值已經錄入線上構建之後,才會開始追問"這個數字到底是從哪兒來的"。
Alpha 版本釋出前的那個週五傍晚,組員 B 走到我工位旁。他手裡的筆記型電腦上開著一份戰鬥數值電子表格。"總監,Boss 第一階段的血量,表裡寫的是 48,000,可進到構建裡的值卻是 52,000。這兩個到底哪個對?"
我不知道。準確地說——在那個當下,沒有人知道。表裡的 52,000 可能是反映了幾天前會議決定的最新值,也可能是有人把未經驗證的值臨時填了進去。48,000 也可能是那次會議之前的共識值。兩個數字都很像那麼回事。像那麼回事並不等於有依據。
要回答這個問題,就得沿著來源一路回溯。是在哪次會議上決定的,那次會議的輸入是什麼,是誰把它謄進了表裡。然而,一旦這條追蹤的鏈條只存在於人的記憶裡,答案就會變成"我明天去問一下組員 A"。運營(LiveOps)進行到第六個月時,這類懸而未決的問題會堆積如山。data lineage——資料的譜系——正是讓這座山根本不會形成的基礎設施。
核心只有一條:來源不能靠手寫。由人事後補錄的來源記錄,撐不過一個月。只有在資料被建立的那一刻自動記錄下來的來源,才能存活下來。
自動記錄 _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。理由留到下一節講——因為來源只應由自動方式記錄。
把來源分成五類,並不是出於整理癖,而是因為每一種 source_type 所附帶的運營規則各不相同。
來看 external_reference 這一行。如果某個資產把 refgame 當作視覺基調的參考,那麼這個資產在未經法務審查前就不能進入構建。source_type 是 external_reference,而 legal_review 記錄為空時,audit 就會攔下它。這正是標籤不止步於標籤、而成為檢查器所讀取的開關的地方。所謂五種分類是運營信任的骨架,指的就是這種強制力。
現在是核心。來源必須在資料生成的時刻被自動記錄。專案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 通常能快速給出一般意義上正確的程式碼,而人負責驗證"在我們的環境裡對不對"。那個驗證點,恰恰成了來源追蹤系統的設計決策。
前面說過,只要有人用手工編輯 _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 中執行。
自動記錄來源的真正理由,在於反向查詢:"源頭 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 的自動記錄,變更傳播才能真正運轉起來。
_source_map.tsv 逐行來看是平面的,但當一個 source 成為另一個資產的 source 時,資料的譜系便串成了鏈條。把這條鏈條鋪展到一屏之內,決定所依據的輸入的可信度就一目瞭然。這張 mermaid 圖,是由 §24.2 的圖表自動生成管線讀取 _source_map.tsv 後直接產出的——相當於用自身的技法來證明自身的資產。
迴圈會自然而然地出現。構建產出使用者資料,使用者資料又成為下一個決定的輸入。一旦看得見這個迴圈,"這個數值從哪來"就變成了螢幕上的一條路徑。組員 B 那個週五的問題,在這張圖裡,不過是沿著 Data → Decision 回溯一步而已。
在專案A,我比較了引入 lineage 系統前後的情況。下表中的時間數值是作者估算(未經驗證),應當看方向與比例的差異,而非絕對值。件數則是從每季度的 audit 日誌中統計出的實測值。
| 專案 | 無 lineage | lineage 運營 | 性質 |
|---|---|---|---|
| 掌握資料來源的時間 | 1\~2 小時 | 1\~2 分鐘 | 作者估算(未經驗證) |
| 資料可信度的驗證依據 | 資深者記憶 | 即時查詢來源 | 定性 |
| 源頭變更時的衍生遺漏 | 每季度 5\~8 件 | 0\~1 件 | audit 日誌實測 |
| 外部資產法務審查遺漏 | 可能發生 | 0 件(強制記錄) | audit 日誌實測 |
| 每季度 audit 耗時 | 1\~2 天 | 2\~3 小時 | 作者估算(未經驗證) |
最硬的數字是"源頭變更時的衍生遺漏"這一行。因為 audit 日誌裡原樣留有決定 ID 和被遺漏的衍生資產,所以能數得出來。時間數值受測量環境(團隊規模·資產數量)影響很大,因此明確標註為估算。方向是清楚的——一旦來源被自動記錄,追蹤就從記憶變成了查詢。
| 失敗模式 | 處方 |
|---|---|
| 事後用手工填補來源 | 用 track_source 在生成時刻自動記錄 |
| 來源格式各行不一 | _source_map.tsv 製表符標準 + 強制格式 |
| 外部資產法務審查遺漏 | 在 source_type 校驗中強制 legal_review |
手工編輯 _source_map.tsv |
用 integrity_check audit 做雜湊比對 FAIL |
| 源頭變更時放任衍生 | find_derivatives 反向查詢 + 通知 |
| 只用文字說明譜系 | 用 mermaid 自動生成,一屏視覺化 |
六條處方的共同點是,它們都不依賴人的自覺。自動記錄·格式強制·雜湊比對·反向查詢,全都由系統來做。因為來源追蹤之所以崩潰,唯一的原因就是"人會忘"。
第 24 部分是用自動化來支撐運營信任的四條支線。第 1 章用 verification 把驗證收攏到一個點,第 2 章用 mermaid 自動生成畫出結構,第 3 章用 wikilink 與 document hierarchy 把文件連線並分層,最後在這第 4 章,用來源與譜系為資料的信任加上了封印。
貫穿這四章的一句話是這樣的。運營的信任,來自系統的記錄,而非人的記憶。 正如 verification 自動追問"這份產出物是否合乎規則",lineage 也自動回答"這份資料從何而來"。二者的關鍵都在於:即便人忘了,它們也不會崩塌。
這套運營經驗,與全書的 Layer 統一設計哲學同出一脈。願景(資產化·信任)落到系統(來源規則),系統落到資料(_source_map.tsv),資料再落到構建·QA(audit·自動更新)——這是一條自上而下的鏈條。這條鏈條本身,就是 lineage。
_source_map.tsv 標準與 source_type 五種,是變更傳播與法務強制的骨架。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 自動化,可以等到資產超過幾十個、來源開始變得容易混淆時,再一個一個加上去。起點只是"在填寫數值時,把一行來源自動留在同一個位置"這一個習慣。
本附錄把筆者所在的 MMORPG 開發商 A 的公司 PC 環境,從硬體到工具、知識資產、驗證材料,彙總在一張清單裡。為了讓讀者一眼看清正文中多處提到的"用這樣的工具""用這樣的 atom 結構""用這樣的報告"在現實中究竟以怎樣的規模與組合存在,筆者做了這份整理。文中的實名與專有名詞均已匿名化;數值會隨時點而變化,因此請不要將其當作絕對值,而應作為比例與構成來閱讀。
閱讀本附錄有兩種方法。一種是逐項對照自己的環境。把"我用什麼引擎、用什麼協作工具、知識資產以什麼形式積累"填進同樣的格子裡,自己的空白欄就會顯現出來。另一種是看整體構成的平衡。工具多並不等於環境好,重要的是引擎、策劃、美術、協作、AI 這五條軸是否彼此不掣肘、相互咬合。比起單個專案本身,更請留意它們交織而成的整體結構。
首先是作為基礎的硬體與作業系統。一旦真正開始大量使用 AI 工具,就會遇到在本地執行擴散模型(diffusion)或 STT(語音識別)的場景,因此記憶體與 GPU 的餘量會直接變成工作速度。下面的配置請當作接近下限的基準線來看——"到這個程度就能順暢運轉"。
| 專案 | 規格 |
|---|---|
| CPU | 桌面工作站級 |
| RAM | 64GB 以上 |
| GPU | UE5 開發用(相容 CUDA) |
| 儲存 | SSD 2TB + NAS 共享 |
| 顯示器 | 27 英寸 2 臺 |
RAM 與 GPU 這兩行是核心,因為經常會遇到同時開著引擎編輯器、本地 LLM 輔助工具和影像生成的時刻。
| 專案 | 值 |
|---|---|
| OS | Windows 11 Pro |
| 虛擬化 | WSL2(Ubuntu),按需 |
| 備份 | 每日自動 |
WSL2 不是一直開著,而只在需要執行 Linux 專用工具時才呼叫。備份以每日自動方式執行——這是本表中最重要的一行。
工具分為五類:引擎與工具、設計與策劃、美術、協作與運營,以及 AI 與 LLM。並非一個人會用到全部五類,但作為策劃,每天都會在設計與策劃、協作與運營、AI 與 LLM 這三類之間往返。請注意,每一類都呈現"一兩個必備 + 輔助"的形態。
| 工具 | 用途 |
|---|---|
| Unreal Engine 5.7 以上 | 主引擎 |
| Visual Studio | 程式碼 |
| Rider | C# IDE(輔助) |
| Perforce 或 SVN | 程式碼與資產版本管理 |
引擎與版本管理是一對。策劃也必須會用版本管理客戶端,因為資料表與設計文件都在同一個倉庫裡運轉。
| 工具 | 用途 |
|---|---|
| Excel | 資料表 + VBA 宏 |
| Markdown 編輯器 | 設計文件與會議記錄 |
| Figma | UI 與線框圖 |
| Mermaid | 圖表 |
這是策劃的日常工作臺。Excel 是資料的大本營,Markdown 是文字的大本營,也是 AI 工具嵌入最深的兩個點。Mermaid 之所以單佔一格,正如正文所強調的,是因為圖示就是達成共識的語言。
| 工具 | 用途 |
|---|---|
| Maya / Blender | 3D |
| Substance 3D | 紋理與材質 |
| Photoshop | 2D 與插畫 |
| Stable Diffusion(SDXL) / ComfyUI | 自託管的概念圖與紋理正式量產(LoRA·ControlNet) |
| Midjourney | 初期情緒板(輔助) |
雖然不是策劃直接使用的工具,但在與美術組來回溝通概念時,瞭解對方手上有哪些工具,會讓需求的清晰度大不相同。正式量產以自託管的 Stable Diffusion(SDXL)/ComfyUI 為主軸——因為不把素材上傳到外部即可保護 IP,並且能用角色 LoRA·ControlNet 在每次反覆生成時都控制同一人物的一致性。像 Midjourney 這類封閉式工具,只在最初摸索專案基調的早期情緒板階段作為輔助使用,而在需要一致性與反覆控制的正式量產中不使用。
| 工具 | 用途 |
|---|---|
| 協作工具(ClickUp) | 任務 |
| 內部即時通訊工具 | 即時溝通 |
| 自建 Wiki | Wiki 與長期文件 |
| 自建門戶網頁 | 統一介面(20.3) |
溝通的時間軸決定了工具的劃分。需要即時性的即時溝通走內部即時通訊工具,待辦事項走協作工具(我們團隊用 ClickUp),需要長期留存的知識走自建的 Wiki——即使把任務追蹤器換成 JIRA·Redmine、Wiki 換成 Confluence·Notion,無論即時通訊工具是哪一個,本書的思路都不變。自建門戶網頁是把這三者與 AI 工具在同一螢幕上連線起來的統一入口,將在 20.3 中詳細展開。
| 工具 | 用途 |
|---|---|
| Claude(Opus + Sonnet) | 主 LLM |
| GPT-4 | 備選 |
| Whisper(自託管) | 語音識別(STT) |
| Stable Diffusion | 影像生成(自託管) |
| MCP 伺服器 | 工具整合(20.4) |
主力用 Claude,GPT-4 作為交叉驗證與備選。敏感的語音與影像不外發、而以自託管方式處理——這一原則體現在 Whisper 和 Stable Diffusion 這兩行裡。MCP 伺服器是把這些工具嵌入工作流的黏合劑,其結構將在 20.4 中說明。
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 個。
即使有工具與知識,若沒有確認其是否正常運轉的裝置,質量就會一路下滑。本節把這類確認裝置分為兩種來展示:週期性產出的報告,以及為事後追溯決策而留下的決策卡。
| 報告 | 頻率 |
|---|---|
| 每日構建報告 | 每天 |
| Alpha 差距報告 | 每週(10.3) |
| 衝刺質量報告 | 每兩週 |
| 里程碑 QA 報告 | 每個里程碑 |
| 季度覆盤 | 每季度 |
頻率就是報告的性質。每天產出的是狀態檢查,每週、每兩週是趨勢檢查,里程碑與季度是方向檢查。AI 貢獻最大的地方,是像每日、每週這樣反覆產出的報告的初稿撰寫,其案例在 10.3 中展開。
| 季度 | 決策數 |
|---|---|
| 2025 年第 4 季度 | 132 |
| 2026 年第 1 季度 | 156 |
| 2026 年第 2 季度(進行中) | 89 |
| 累計 | 547 |
每個季度都有 100 件左右的決策以卡片形式留存,這一事實本身就體現了"把決策當作記錄而非記憶來對待"的運營原則。第 2 季度的 89 件是季度中途時點的累計,屬於進行中的數值,到季度末會達到上一季度的水平。這些卡片積累起來、晉升為 A.3 的 atom 的這一流程,正是本系統的學習軸。
會議既是時間流失最嚴重的地方,也是最快能體會到 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 中說明。
至此為止的所有表格,都是筆者環境的一張照片。它不是讓你照搬的清單,而請當作用同一套框架整理自己環境的樣本。團隊規模、品類、平臺不同,工具、atom 分佈、會議比重都會不同。重要的不是專案是否一致,而是"基礎 → 工具 → 知識 → 驗證"這四層在你自己的環境中是否也不斷裂地連貫銜接。這四層當中若有空白的一欄,那一欄就是接下來要動手的地方。
本附錄整理了作者把在公司專案A中構建並運營的工具·技能,拿到個人 PC 和一般性工作中重新使用的流程。核心問題只有一個:"如何在不侵犯公司知識資產的前提下,只合法地借用從中學到的工具骨架?"本附錄展示瞭如何劃定這條邊界,把什麼拿了過來、又把什麼留在原處,以及如何將這些決定留存為記錄。
本附錄的使用方法如下:先對照自己的處境閱讀 B.1 的五條原則,再把 B.3 的流程原樣走一遍。然後複製 B.4 的記錄模板,按照自己想借用的工具填寫即可。既然處理的是公司資產,那麼比起"快",能"留下記錄"更為優先,整篇附錄都是以這一視角構建的。
這是在拿取工具之前已達成共識的五條原則。這五條不是順序,而是必須同時滿足的條件——只要其中一條崩塌,就擱置借用本身。前三條是關於"拿取什麼"的技術邊界,後兩條是關於"如何堂堂正正地拿取"的流程邊界。
| 原則 | 說明 |
|---|---|
| 1. 不包含公司 IP | 移除公司名·實名·專有名詞 |
| 2. 只拿取工具骨架 | 遮蔽公司領域資料 |
| 3. 通用化重構 | 重新構建為一般使用場景 |
| 4. 引用·出處明確 | 註明為從公司借用的工具 |
| 5. 法務·人事共識 | 走完公司的許可流程 |
最常動搖的一行是第 2 條。演算法與結構(骨架)可以拿取,但如果連那套骨架所預設的公司資料格式也一併帶過來,那一刻就等於把 IP 也拿走了。把骨架與資料剝離開來的工作,才是借用的主體。
按照原則實際拿到個人 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.1)落到實際操作上,就是下面六個步驟。最關鍵的分岔點是第 2 步和第 4 步。若在第 2 步沒能把骨架與領域乾淨地分開,後面所有步驟都會被汙染;若跳過第 4 步的公司許可,那麼無論做得多好,都會成為一件無法使用的工具。
六個步驟中最耗時的一環,不是編碼工作(第 2·3 步),而是第 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 月前後"之類的隨意寫法看起來像是留待日後填寫的空格,因此要當場把確定借用的時點釘死。
這份記錄中最有價值的兩行是 permission 和 modifications。前一行證明借用是正當的,後一行證明剝離了什麼。有了這兩行,即使日後有人提出疑問,也留有可供追溯的依據。
拿走了什麼固然重要,留下了什麼同樣重要。這裡記下了公司工具中被有意不予借用的部分及其理由。這些被留下的工具有一個共同點:要麼是公司的核心 IP,要麼深深繫結在公司組織結構上,以致無法把骨架與領域剝離開來。
| 工具 | 未借用理由 |
|---|---|
| 公司戰鬥系統工具 | 公司核心 IP,公司獨佔 |
| 公司敘事文件工具 | 依賴公司世界觀 |
| 公司戰鬥 TF 工具 | 依賴公司組織結構 |
| 公司人事·財務工具 | 不適配外部環境 |
這與 B.2 中拿走的工具全部是"資料處理"形成了鮮明對比。拿走的是能與領域分離的工具,留下的是與領域渾然一體的工具。可分離性決定了可借用性。
最後,這是在拿取工具之前必須讓自己逐一通過的五個專案。這張表是一份判定合格/不合格的檢查清單——只有五項全部通過才借用,只要有一項卡住就擱置。沒有"大體上還行"這回事。因為處理公司資產的工作,容不得部分通過。
| 檢查專案 | 通過標準 |
|---|---|
| 是否已獲得公司許可 | 經理·法務的明確同意 |
| 是否通過法務審查 | 書面或有記錄的確認 |
| 是否已完全移除公司 IP | grep watchlist 檢查為 0 件 |
| 是否驗證了通用性 | 確認在其他環境中也能執行 |
| 是否有事故應對流程 | 定義了追蹤·回收路徑 |
請不要把這五項讀作五格的通過,而要讀作五道鎖。把在公司學到的東西正當地變為個人的資產,這件事確實可行,但那份正當性只有在這五道鎖全部扣上時才成立。
本附錄是把正文中引用的工具與系統的許可權·設定彙集到一處的參考表。正文說明的是"為什麼要這樣運營",而真要應用到自己的環境時,還需要知道"那麼具體該把什麼值填到哪裡"。本附錄填補的正是這塊空白。
比起設定值本身,選擇這個值的理由更重要。與其原樣複製表中的數字,不如先讀一讀每個條目下面的簡短說明,再按照自己的團隊規模和風險水平來調整。如果是一個人工作,就無需劃分許可權等級;如果沒有外包,把外包相關的條目整塊刪掉即可。
使用本附錄有兩種方式。第一次搭建環境時,從 C.1 開始按順序通讀,像檢查清單一樣確認有沒有遺漏的條目。運營過程中出了事故時,先翻到 C.7(事故應對),找到對應的事故行,再往上回溯到它上方的預防條目。
LLM API 金鑰與成本直接掛鉤,一旦洩露就會立即演變成金錢事故。因此先從金鑰管理和許可權等級講起。
| 金鑰 | 保管 |
|---|---|
| Anthropic API | 環境變數 + 1Password |
| OpenAI API | 環境變數 + 1Password |
| 自託管 | 公司內部 |
金鑰不寫進程式碼,而是通過環境變數注入,原件放在金鑰管理工具(如 1Password)裡。最常見的事故是把金鑰寫在程式碼裡連同 git 一起提交,因此在 git 中包含金鑰一律禁止。
| 使用者 | 許可權 |
|---|---|
| 總監·資深 | full(負責運營 cost cap) |
| 普通成員 | 按任務設定 cap |
| 外包 | 按任務限一次 |
許可權不按信任劃分,而按責任的大小劃分。擁有 full 許可權的人同時承擔管理成本上限(cost cap)的責任。對外包只按任務單位開放一次,任務結束後即收回。
每種工具的推薦設定各不相同,但核心是把分析類工作和創意類工作分開。分析必須可復現,創意則需要多樣性。
下面是 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 -rf、git push --force 這類一次失誤就無法恢復的命令,無論把自動放行的範圍放得多寬,都要寫進 deny。
運營原則有四條。
| 原則 | 內容 |
|---|---|
| 從白名單起步 | 自動放行從最小集開始,僅在必要時才加入 allow |
| 危險命令顯式攔截 | rm -rf·git push --force 一律 deny |
| 定期清理 | 每季度複查 allow,刪掉不再使用的許可權 |
| 按領域分離 | 把全域性許可權和專案許可權分開,讓家用 PC 和公司 PC 擁有不同的策略 |
allow 列表並不是靜態配置,而是工作累積的痕跡。重複的工作越多,它就越長,因此最好一併配上按季度清理的週期。這套許可權運營的背景會在正文第 1 部分第 3 章詳細展開。
| 設定 | 推薦值 |
|---|---|
| LLM temperature(分析) | 0 |
| LLM temperature(創意) | 0.7 |
| Cache TTL | 1 小時 |
| Cost cap(每日) | 按工具定義 |
| Backup 週期 | 每日 |
分析類呼叫把 temperature 設為 0,讓相同輸入產生相同輸出。驗證·lint·分類這類結果不能有波動的工作都屬於此類。反過來,發散想法或生成初稿則給予 0.7 左右的多樣性。成本上限不設統一標準,而是按工具分別設定,因為每種工具的呼叫頻率和 token 消耗各不相同。
| 分支 | 許可權 |
|---|---|
| main | 僅總監·資深可 push |
| feature/* | 所有成員 |
| protected branches | 強制程式碼評審 |
main 分支禁止直接 push,所有變更都要在 feature 分支經過程式碼評審後再合入。force-push 會覆蓋協作歷史,因此予以禁止,只有在不得已需要事故恢復時,才經總監與程式碼負責人(code lead)一致同意後開例外。
文件資料夾按 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 部分講解。
| 資料 | 備份 |
|---|---|
| git repo | git 本身 + 遠端備份 |
| 表格(Excel) | git + 每日備份 |
| 使用者資料 | DB 備份(伺服器標準) |
| 會議記錄·決策 | git |
| 記憶 | 每日自動同步 |
每種資料的備份路徑各不相同,但原則只有一個:越是丟失後難以恢復的資料,越要做雙重保管。文本資產(會議記錄·決策·程式碼)有 git 就等於有了備份,二進位制或伺服器資料則另設單獨備份。恢復時間目標(RTO)定在 4 小時以內,但這個值要按團隊能承受的停機時間來調整。
| 領域 | 規則 |
|---|---|
| 向外部 LLM 傳送敏感資料 | placeholder 或自託管 |
| 支付·個人資訊 | 絕不傳送給 LLM |
| 引用外部資料 | 註明出處 + 法務審查 |
| 使用者資料保護 | 匿名化 + 遵守 GDPR |
最容易遵守、卻最常被破壞的規則,就是"不把敏感資料傳送給外部 LLM"。因為工作緊急時,把真實資料直接貼上進去的誘惑很大。支付·個人資訊一律設為禁止傳送,若需要分析,就用 placeholder 替換,或使用自託管模型。
| 事故 | 應對 |
|---|---|
| 因 LLM 幻覺發出錯誤資訊 | 立即撤回 + 上報 |
| 成本超出 cap | 自動阻斷 + 複查 |
| 著作權事故 | 1 小時內停止使用 + 法務 |
| 安全事故(key 洩露) | 立即更換金鑰 + 審查使用記錄 |
| 資料丟失 | 備份恢復 + 事故分析 |
很多時候,比起阻止事故,儘快止損更為重要。表中的應對都遵循"先停下,再分析"的順序。key 洩露時,先別追究原因,而是先更換金鑰並檢視使用記錄。成本超過上限時,先自動阻斷再複查。這套應對流程要用文件明確固化下來,並定期演練,好讓真出事故時能毫不猶豫地運轉起來。
公司專案A的 R&D 文件命名與 frontmatter 標準(
_NAMING_FRONTMATTER_STANDARD)的通用化版本。
<category>_<topic>_<subtopic>.md
示例:
combat_global_cooldown_constant.md
narrative_voice_profile_K_007.md
ui_button_primary_style.md
snake_case。類別字首。
D<YEAR>_Q<QUARTER>_<NUMBER>.md
示例:
D2026_Q2_017.md
年份、季度、編號。
<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
spec_<topic>.md
示例:
spec_combat_global_cooldown.md
spec_guild_attendance.md
report_<period>_<type>.md
示例:
report_W21_alpha_gap.md
report_Q2_user_voice.md
---
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]
---
---
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
---
---
type: meeting_note
category: battle
date: 2026-05-18
attendees: [團隊成員 A, 團隊成員 B, 李旼洙]
related_atoms: [...]
---
---
title: 公會簽到功能規格
type: spec
priority: P1
target_milestone: MS2
---
| 文件型別 | 必填 |
|---|---|
| atom | name, description, type, category, status |
| 決策卡 | decision_id, title, type, status, created, scope |
| 會議記錄 | type, category, date, attendees |
| 規格書 | title, type, priority |
| 文件型別 | 選填 |
|---|---|
| atom | related, last_modified, priority |
| 決策卡 | rationale, related_decisions, verification |
| 會議記錄 | related_atoms, sub_topic |
| 規格書 | target_milestone, related_atoms |
# 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。
| 領域 | 防止 |
|---|---|
| atom name | 全域性 unique |
| 決策 ID | 季度內 unique |
| 會議 ID | 日期 + seq |
| 檔名 | 資料夾內 unique |
命名衝突時自動阻止。
1. 建立新名稱的 atom
2. 將原 atom 的所有 wikilink 更新為新名稱(自動)
3. 將原 atom 棄用(deprecated)+ 重定向(redirect)
4. 1 個月後移至 _archive
倉促的重新命名有損壞資料的風險。
1. 提出變更理由(decision 流程)
2. 為所有現有文件編寫遷移指令碼
3. 更新構建 lint
4. 通知團隊
本標準基於作者的環境。讀者需根據自身環境進行調整。核心在於:
| 核心 | 理由 |
|---|---|
| 命名一致性 | 檢索、自動化 |
| Frontmatter 標準 | 工具友好 |
| 必填、選填分離 | 填寫負擔 ↓ |
| Lint 自動 | 強制標準 |
| 變更流程 | 保護資料 |
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。
MCP 伺服器按連線物件大致分為四類。遊戲策劃每天來回使用的工具,大多都落在這幾類之內。
| 領域 | MCP 伺服器 |
|---|---|
| 專案管理 | ClickUp、JIRA、Linear |
| 文件 | Confluence、Notion、Google Drive |
| 協作 | 團隊即時通訊工具(Slack、Discord 等) |
| 資料 | Excel、Google Sheets、DB |
專案管理對接任務與排期,文件對接策劃案與 wiki,協作對接團隊溝通,資料對接數值與道具表格。先弄清自己團隊已在使用的工具屬於哪個領域,引入的候選範圍自然就會收窄。
優先順序以"沒有它工作是否會卡住"為標準評定。P0 幾乎是所有工作的基礎,P1 有了會大為便利,P2 則視團隊情況而定。
| 伺服器 | 用途 | 備註 |
|---|---|---|
| Filesystem MCP | 訪問本地檔案 | 基礎 |
| Git MCP | 變更追蹤 | 必備 |
| 團隊即時通訊 MCP | 團隊溝通 | 推薦 |
| 協作工具 MCP(ClickUp、JIRA 等) | 任務 | 公司工具 |
Filesystem 與 Git 是 LLM 讀取資料、追溯變更歷史的基礎,因此最先接入。團隊即時通訊 MCP 引入團隊上下文,任務工具則直接把公司已在使用的那一款(無論是 ClickUp 還是 JIRA)連上。
| 伺服器 | 用途 |
|---|---|
| wiki MCP(Confluence、Notion 等) | wiki |
| Google Drive MCP | 外部共享資料 |
| Excel MCP | 直接查詢表格 |
| Mermaid MCP | 渲染圖表 |
P0 穩定後,再向文件與資料一側擴充套件。尤其 Excel MCP 能讓 LLM 直接查詢數值表格,在遊戲策劃中用途很廣。Mermaid MCP 能就地渲染設計圖示,不打斷文件化的流程。
| 伺服器 | 用途 |
|---|---|
| Discord MCP | 使用者社群 |
| GitHub MCP | 外部協作 |
| Linear MCP | 備選任務工具 |
| Notion MCP | 備選 wiki |
P2 要麼是備選,要麼專用於特定場景。運營使用者社群就接入 Discord,外部協作頻繁就接入 GitHub。Linear、Notion 是已引入工具的替代品,無需重複安裝。
商用 MCP 填不滿的位置,就自己動手做。下面是作者針對遊戲策劃工作流自行開發的 MCP。它們都是為了從 LLM 直接查詢正文講過的那些系統(atom、決策卡、會議記錄)。
| 伺服器 | 用途 |
|---|---|
| Atom MCP | 檢索、查詢 atom |
| Decision Card MCP | 查詢、生成決策卡 |
| KPI Dashboard MCP | 儀表盤資料 |
| Meeting Notes MCP | 檢索會議記錄 |
這四個處理的是商用工具沒有的內部資產(知識 atom、決策歷史、會議記錄)。自行開發負擔較大,建議推遲到 E.4 週期的最後階段,等到確實清楚哪些是商用 MCP 無法填補的,再著手為好。
MCP 一次性全部接入,會讓問題根源難以辨認。下面的週期,是把"一次一個、穩定之後再下一個"這一原則沿時間軸展開的結果。
核心規則只有一條:不要一次同時引入 5 個。每接入一個新的 MCP,都請先花幾天看它這一個是否穩定運轉,再進入下一個。
| 伺服器 | 成本 |
|---|---|
| 外部 MCP(開源) | 僅基礎設施 |
| 自行託管 | 基礎設施 + 運營 |
| 商用 MCP | 月度訂閱 |
成本結構分為三種。開源 MCP 只需承擔執行的基礎設施費用,自行託管在此之上再加運營人力成本,商用 MCP 則要付訂閱費。運營 8\~10 個時,月度成本大致估算在 $50\~200 一檔,但這會因配置不同而大幅變化,故只作方向參考。
| 事故 | 應對 |
|---|---|
| MCP 伺服器故障 | 核心伺服器採用 fallback 執行 |
| 許可權事故(誤改資料) | 優先 read-only |
| 資料洩露 | 敏感資料自行託管 |
| 成本暴漲 | cap + 監控 |
MCP 把外部工具直接連到 LLM,因此一次錯誤的寫入就可能毀掉真實資料。所以預設設為 read-only,寫入許可權只對確有必要的伺服器開放。核心伺服器要為故障預備 fallback,處理敏感資料的 MCP 則用自行託管而非外部來執行。成本方面用上限(cap)與監控一併遏制。
如果說前面幾節講的是"接入什麼、按什麼順序、花多少錢",那麼這張表彙總的是在真正接入某一個 MCP 之前,需要自己逐條通過的專案。不必從頭重讀整份目錄,每次新增 MCP 時,只需重新核對這五行即可。這五個專案,各自把前面某一節的核心規則壓縮成了一行。
| 檢查項 | 通過標準 | 依據小節 |
|---|---|---|
| 屬於哪個領域 | 明確屬於專案管理、文件、協作、資料中的哪一個 | E.1 |
| 是否是當下所需的優先順序 | 遵守 P0 穩定之後才 P1、再 P2 的順序 | E.2 |
| 是否逐個接入 | 不一次同時引入多個 | E.4 |
| 許可權是否最小 | 預設 read-only,寫入只給確有必要的伺服器 | E.6 |
| 是否有成本上限 | 同時掛上上限(cap)與監控 | E.5 |
五個專案中最常被跳過的一欄,是"是否逐個接入"。因為一次上多個 MCP,一旦出問題就分不清是哪個伺服器的錯。只有五行全部通過時才接入那個 MCP;哪怕只卡住一行,也把該伺服器推遲到下一個週期。
將本書中出現的案例按公司環境與個人 PC 環境進行索引。本資料供讀者快速查詢貼近自身環境的案例。
| 案例 | 出現位置 |
|---|---|
| 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 |
| 案例 | 出現位置 |
|---|---|
| 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 |
| 案例 | 出現位置 |
|---|---|
| 中等規模(10\~50人)團隊的願景·路線圖 | 19.1 |
| Design Director 的授權 | 19.2 |
| 衝突管理·團隊文化 | 19.3 |
| 會議運作(領導者視角) | 19.4 |
| 向上溝通(PD/CEO) | 19.5 |
| AI 引入策略 | 19.6 |
| 治理(提示詞·幻覺·成本·法務·倫理) | 整個第22部分 |
作者在個人 PC 環境(家中)中親身經歷的案例。
| 案例 | 出現位置 |
|---|---|
| excel-reader 等6個工具借用 | 附錄 B |
| JIT atom 注入系統 | (個人 PC 基礎設施) |
| 個人 PC 斜槓命令(book-capture 等) | (個人 PC 基礎設施) |
本書的寫作過程本身就是 AI 應用案例。
| 領域 | 應用 |
|---|---|
| 章節正文批次生成 | LLM(Claude) |
| IP 保護(公司 → 匿名化) | grep watchlist + 規則 |
| 來源追溯 | 引用公司環境時註明 |
| 批次生成 → 審閱 → 整理迴圈 | 5月批次生成後進入審閱模式 |
公司環境案例(中等規模的10\~50人團隊、MMORPG、運營)適用於規模·領域相近的公司。
| 讀者環境 | 適合的案例 |
|---|---|
| 移動端 MMORPG 開發商 | 幾乎所有案例 |
| PC MMORPG | 第14部分的移動端案例需調整 |
| 獨立遊戲 | 中等規模(10\~50人)以上的案例需縮減應用 |
| 運營(LiveOps)類遊戲 | 第15部分 + 運營案例 |
個人環境(1\~2人、或興趣愛好)可將公司案例簡化後借用。
| 領域 | 簡化 |
|---|---|
| 會議系統 | 單人無需,用個人筆記即可 |
| TF 運營 | 單人無需 |
| 決策卡 | 僅限重大決策 |
| atom·wikilink | 積極使用(單人也有價值) |
本書所有公司案例均已匿名化。
| 原始 | 匿名化 |
|---|---|
| 公司名稱 | MMORPG 開發商 A |
| 專案 | 專案A |
| 團隊成員實名 | 團隊成員 A·B·C |
| 遊戲內的專有名詞 | 虛構(王國 X、角色 K_001 等) |
| 數值 | 虛構(比例為真實) |
| 公司工具名 | proj_*(例:proj_city_hunting_generator) |
面向在遊戲行業之外工作的讀者(策劃·PM·普通職場人士)的反向索引。正文各章末尾的「遊戲之外的應用」專欄,是把該章的工作流搬到與遊戲無關的職務上閱讀的橋樑。若覺得遊戲領域的正文有負擔,也可以先翻開下面的專欄,從自身職務案例入手。「通用職務之路」(1·2部 → 17 → 16 → 18 → 21·22部)與90分鐘超短速成課程(17.1 → 16.2 → 22.1 → 21.1)的錨點,正是這份索引。
| 章 | 「遊戲之外的應用」為你遷移的工作 |
|---|---|
| 16.1 | 把蜂擁而來的工作隔離到臨時工作空間,只把結果吸收為正本 |
| 16.2 | 把一句話請求分類到共識·缺陷·排期三條軌道 |
| 16.3 | 用適合其他職能·利益相關者的媒介來呈現產出物 |
| 17.1 | 讓會議紀要按決策的4個欄位(什麼·誰·為何·下一步)流轉 |
| 17.2 | 從會議紀要中提取決策·行動項的流水線 |
| 17.3 | 會議決策的分類·同步 |
| 17.4 | 會議摘要·後續追蹤的自動化 |
| 18.1 | 為決策錄入永久地址·負責人·依據,並先查詢過往決策 |
| 18.2 | 對一個決策的影響波及範圍進行分類 |
| 18.3 | 變更前後的追蹤工作流 |
| 18.4 | 用檢索確認文件變更的影響範圍 |
| 章 | 「遊戲之外的應用」為你遷移的工作 |
|---|---|
| 21.1 | 把覆盤作為自我改進的起點 |
| 21.2 | 把覆盤中反覆出現的模式升格為規則 |
| 21.3 | 閉合改進迴圈 |
| 22.1 | 在一份工作指令書(提示詞)裡放入上下文·格式·防幻覺·驗證 |
| 22.2 | 幻覺·安全性的多層防禦 |
| 22.3 | 誠實地管理 AI 成本 |
| 22.4 | 版權·倫理檢查 |
| 章 | 「遊戲之外的應用」為你遷移的工作 |
|---|---|
| 19.1 | 提出願景與授權 |
| 19.2 | 衝突管理與會議領導力 |
| 19.3 | 組織的 AI 引入策略 |
上述索引只收錄了正文中實際存在的「遊戲之外的應用」專欄(截至 2026-06 共22處)。沒有專欄的章節對遊戲領域的依賴度較高,難以原樣遷移;與其勉強搬移,不如先從「通用職務之路」的上述章節入手。
本附錄是把正文中提到的運營自動化指令碼彙集到一處的案例集。正文在行文中說明了每個指令碼"為什麼需要",但真要動手做類似的工具時,還需要一張能一眼看清"哪些指令碼以什麼角色歸為一組"的地圖。本附錄就是這張地圖。
這裡一併寫出了指令碼名稱、一句話說明,以及正文在哪一節討論過它。對於能夠乾淨利落地一般化的核心指令碼(G.1.1 格式檢查·G.2.1 一致性檢查·G.3.1 關係圖·G.7.1 成本追蹤器),以及 G.8 的測試·hook 示例,我們用與公司資料無關的通用骨架重新編寫,並驗證其可直接執行,收錄的是實測程式碼。輸入示例、輸出乃至退出碼,都是實際執行確認過的值。其餘條目只寫了名稱、角色和關聯的正文小節,其中緣由會在附錄 G.9 中如實說明。讀者可以把實測程式碼條目當作範本,自行做出適合自己環境的實現。
用法如下。先確定想要自動化的工作性質(是驗證,是生成報告,還是同步),然後翻到對應的小節(G.1\~G.7)。在那裡選出最接近的指令碼,再前往括號中的正文小節編號,確認其背景與設計意圖。最後對照 G.8 的運營原則,檢查自己的指令碼是否遵守了這些原則。
按角色把全部指令碼歸組,如下所示。
讓會議中產生的決策不致散失、而是沉澱為知識資產的一組指令碼。從會議紀要驗證到 atom 提取,再到正式升格,一脈相連。
檢查會議紀要是否具備既定格式(必需的頭部·必需的章節)的指令碼。格式散亂的會議紀要會導致後續自動提取失效,因此在入口處攔截(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: ## 下次會議
讀取會議紀要的"決策"章節,自動抽取知識 atom 候選的指令碼。替代過去由人逐條謄抄的工作(17.2.3)。
把待審(pending)狀態的 atom 升格到正式 atom 資料夾的指令碼。在自動提取與正式資產之間設一道人工評審關卡(17.2.6)。
自動查出資料與內容是否違反規則的質量門禁。讓機器先過濾掉人眼容易遺漏的一致性錯誤。
驗證資料項的 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]
}
]
}
檢查 NPC 臺詞的"聲音"(voice,即語氣·性格)一致性的指令碼。抓出同一角色在各章使用不同語氣的偏差(5.2·5.4)。
在資源(美術·UI 等)發生變化時,比對是否產生了非預期視覺變化的迴歸檢查指令碼(12.1.5)。
追蹤"改動一處會牽動什麼"的一組指令碼。沿著文件·決策·資源之間的連線,展示變更的波及範圍。
抓取文件間的 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["勢力_義禁府"]
分析某個決策卡會影響哪些文件·資源的指令碼。在推翻決策之前,先確認其波及範圍(18.4.3)。
反向找出使用某個資源的技能的指令碼。在修改·刪除資源之前,先弄清依賴它的地方(11.2.4)。
把散落的資料彙整為人可閱讀的報告·圖示的指令碼。將反覆的定期彙報自動化,減少費手的工作。
彙總 Alpha 階段相對目標的缺口(gap),自動生成周報的指令碼(10.3.3)。
把決策卡之間的連線關係轉換為 Mermaid 圖示程式碼的指令碼。用圖來看決策流(24.2.3)。
按周彙總主要指標(KPI)的指令碼(13.2)。
高效地對齊分散在多處的資料的指令碼。不必每次複製全部,只挑出變更的部分進行同步。
只挑出會議紀要的變更部分、而非全部進行同步的指令碼。資料越積越多,全量複製就越慢,因此採用增量方式(17.5.4)。
利用 git 的 diff 高效檢測發生了哪些變更的做法。無需額外的追蹤裝置,直接把 git 本身當作變更檢測器(17.5.4.1)。
把分類·呼叫這類需要判斷的工作交給 LLM 的指令碼。用 LLM 輔助來處理規則無法乾淨拆解的事情。
把收到的 FAQ 按類別自動分類的指令碼(13.1.3)。
把會議按性質類別自動分類的指令碼。用於填充會議紀要頭部的 category(17.3.6)。
從預先整理好的提示詞庫中載入所需提示詞的指令碼。避免每次重複編寫相同的提示詞(22.1.2)。
管理自動化本身,使其不致製造成本與資料追蹤盲區的指令碼。
追蹤 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 阻止下一次呼叫。這就是"在事前而非事後阻止"的實際行為。
自動記錄引用·參考資料出處的指令碼。留存下來,以便日後回溯出處(24.5.4)。
比起多造指令碼,讓造出的指令碼可靠地運轉更重要。下面五條原則通用於上述所有指令碼。
| 原則 | 說明 |
|---|---|
| 簡單 | 迴避複雜的庫 |
| 測試 | 所有指令碼單元測試 |
| 輸出標準 | violation_list 等標準(10.1.7) |
| 版本管理 | git |
| 人工評審關卡 | 自動化也須人工評審 |
尤其最後一條原則很重要。自動化不是替代人,而是縮減人做判斷之前的環節。無論是驗證、提取還是生成,在最終應用之前都務必設一道由人過目一次的關卡。
"測試"原則不只停留在口頭,這裡給出用標準庫 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
上述原則中容易被漏掉的是 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 的位置,視其為"輔助"還是"門禁",退出碼策略正好相反。
上一節的 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.1.1·G.2.1·G.3.1·G.7.1·G.8 這樣,用與公司資料無關的通用骨架重新編寫、並驗證可直接執行的程式碼。它們只用標準庫,上面所寫的輸入示例·輸出·退出碼,都是實際執行確認過的結果。可以直接複製貼上使用,只需把單價表或列名之類的佔位值換成適合自己環境的即可。
另一類是像其餘各節那樣,只寫了名稱·角色·關聯正文小節的條目。沒有把這一類以完整程式碼收錄,坦白說有兩個理由。第一,公司運營指令碼的原件屬於公司 IP,無法原樣搬來。第二,其邏輯相當一部分繫結在公司特有的資料模式·資料夾結構·決策卡格式上,一旦抽走這些前提,就不會剩下對一般讀者直接有用的程式碼。因此,只把能夠乾淨利落地一般化的四個(格式檢查·一致性檢查·關係圖·成本追蹤器)升格為實測程式碼,其餘留作骨架。讀者可以把這四個當作範本,以同樣的方式——分離檢查邏輯與輸入輸出、以標準輸出報出 violation 列表、附上單元測試——自行做出適合自己環境的實現。
把既有工具拿來做變奏的步驟,參見附錄 B。
從業多年的策劃會積累下數十年的工作資料。會議記錄、決策記錄、覆盤、學習筆記,乃至從失敗中得到的教訓。本附錄討論如何把這些資料重新用於新專案。核心張力只有一個:這些資料的很大一部分屬於公司 IP,不能隨意搬走,但其中同時又夾雜著在任何地方都通用的個人學習。區分這兩者,正是複用的起點。
如何使用本附錄,取決於你所處的位置。如果你正想把舊資料引入新專案,請依次閱讀 H.2(分離原則)和 H.3(流程)。如果你擔心搬運過程中會出事故,請先閱讀 H.5(五個陷阱),提前規避。如果你還處於職業生涯早期、可積累的資料不多,請參考 H.6,從現在起決定要留下什麼、如何留下。
這裡講的原則並非什麼宏大的資產管理理論。它可以壓縮成一句話:"把具體的東西留在公司,只帶走抽象的模式。"其餘內容都是把這句話應用到實際情形中的方法。
首先來看會積累哪些資料,以及各自的留存許可權有何不同。因為留存許可權不同,可複用的範圍也會隨之不同。
| 資料 | 留存 |
|---|---|
| 會議記錄(公司資料) | 公司許可權內 |
| 決策卡(公司資料) | 公司許可權內 |
| 季度覆盤(個人+公司) | 可保留個人副本 |
| 學習筆記(個人) | 個人永久 |
| 事故記錄(個人學習) | 個人永久 |
會議記錄和決策卡留在公司許可權之內。覆盤可以保留個人副本,而學習筆記和事故記錄則完全屬於個人資產。長期積累的資料本身就是巨大的學習資產,但絕不能模糊公司 IP 領域與個人領域的邊界。邊界越清晰,就越能安心地複用。
分離的標準是"具體還是抽象"。具體的產出屬於公司,而產生它的思考模式屬於個人。同一項工作會同時帶出這兩個方面——這一點是關鍵。
| 領域 | 公司 IP | 個人學習 |
|---|---|---|
| 決策內容 | 公司 | — |
| 決策模式(某種情形下適合某種決策) | — | 個人 |
| 遊戲資料 | 公司 | — |
| 運營經驗(規則手冊·工具運營) | — | 個人 |
| 程式碼 | 公司 | — |
| 演算法·結構 | — | 個人 |
"做了什麼決策"屬於公司 IP,而"在這種情形下這種決策更奏效"這樣的模式則是個人學習。遊戲資料的數值本身屬於公司,但運營這些資料的訣竅屬於個人。把具體資料留在公司,只帶走抽象模式——這就是分離的原則。
把分離原則落實到實際工作中,就成了以下五個步驟。識別資料、剝離 IP、提取學習、加以泛化,然後應用到新專案。
這一流程務必在經過公司許可權確認與法務稽核之後再推進。即便抽象化已經足夠,只要起點是公司資料,按流程留一份確認會更安全。
最切近的複用案例就是本書本身。正文各處都源自作者過往的工作,是經過上述流程加以泛化、匿名化後的結果。
| 領域 | 出處 | 複用 |
|---|---|---|
| Layer 整合設計(第 6 部分) | 作者多年運營 | 個人學習 → 泛化 |
| 會議記錄系統(第 17 部分) | 作者的專案A運營 | 公司模式 → 匿名化 |
| 運營經驗(第 24 部分) | 多年積累 | 個人學習 → 泛化 |
| 附錄 A 清單 | 公司專案A | 匿名化 + 部分加工 |
Layer 設計與運營經驗是把個人學習加以泛化,會議記錄系統與附錄 A 則是把公司模式匿名化。所有條目都通過了公司的許可,公司 IP 全部無一遺漏地做了匿名化。本書這一產出本身,可以說就是 H.3 流程的實證。
複用做得好是資產,做不好就是事故。下面這五個陷阱都是實際中經常踩的點,針對每一個都給出了對策。
未經公司許可就使用資料,會演變成糾紛。對策很簡單:使用之前先取得公司許可。
只要公司名或真實姓名殘留在任何一處,就會釀成 IP 事故。對策是自動 grep 檢查。把公司名·真實姓名·路徑做成 watchlist,讓機器無一遺漏地掃一遍。
把很久以前的經驗原封不動地拿來用,就會與當下的時點脫節。對策是順應時代重新構建。保留原理,但把工具與語境更新到當前。
只搬運具體案例,就很難套用到其他環境。對策是把抽象模式與具體示例放在一起。用模式獲得普適性,用示例獲得理解。
資料再多,若不再翻看,就等於沒有。對策是定期的學習週期。像日·周·月覆盤那樣,建立一個重新接觸資料的週期。
這一原則並非作者專屬。讀者也可以用同樣的方式複用自己職業生涯的資料。下面是從現在起就能開始的推薦習慣。
| 推薦做法 | 理由 |
|---|---|
| 每季度覆盤自己的決策 | 發現模式 |
| 單獨儲存學習筆記 | 與公司 IP 分離 |
| 明確寫出抽象模式 | 便於未來複用 |
| 指導他人·對外演講 | 分享模式 |
| 出書·部落格(取得公司許可後) | 讓學習永存 |
每季度覆盤自己的決策,就能看出模式;把學習筆記與公司資料分開儲存,日後就能安心地取用。把這些模式通過指導他人、演講、寫作輸出出去,學習就不會用過一次即消失,而會長久留存。歸根結底,自己的學習就是自己的資產。
7.2 中討論的 BehaviorTree 編輯器的進階案例。自主開發的決策、實現與運營經驗。
7.2.8 中討論的四項決策依據的詳情。
| 依據 | 詳情 |
|---|---|
| 必須支援 diff·git 追蹤 | UE BT 是 .uasset binary,變更難以追蹤。改用 JSON 後可進行文本 diff |
| subtree 引用 + 影響追蹤 | 運營 100\~300 個 BT 時,以 subtree 為單位的影響分析至關重要 |
| 模擬驗證 | 無需構建即可單獨執行 BT |
| AI 輔助編寫 | LLM 能自然地生成、解析 JSON BT |
[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 人。
這款編輯器是處於 R&D 階段的內部工具,並未達到可稱為"一年運營實測"那樣的長期、大規模執行。因此,遵循本書的原則,這裡不刊載任何編造的運營統計資料。I.1 中提到的 100\~300 個的規模,是為自主開發提供正當理由的設計目標,而非實測結果。
在設計所設定的限制中,真正寫入程式碼的是 subtree 引用 depth 5 上限(防止無限遞迴)。運營中的 BT 數量、模擬執行次數之類的數值會因專案規模而異,因此與其寫下編造的數字,不如在你自己的環境中親自測量。
| 事故 | 經驗教訓 |
|---|---|
| subtree 無限引用(遞迴) | 引用 depth 5 上限 |
| 模擬與實際行為的差異 | 每月校準模擬環境 |
| LLM 輸出 BT 的幻覺 | 加強驗證 + 設計師評審 |
| BT 數量激增(計劃外) | 季度整理週期 |
開發與運營成本是基於製作這款工具時所定日程的估算,而"效果"一側並非實測值,而是引入該工具所要達成的方向。這裡不寫編造的節省數字,只寫方向。
| 專案 | 值 | 性質 |
|---|---|---|
| 開發成本 | 開發者 4\~6 個月 | 計劃日程(估算) |
| 運營成本 | 開發者每季度 1\~2 周(維護) | 計劃日程(估算) |
| 預期效果——運營人力 | 大規模運營敵方 NPC 時,壓縮 BT 負責人員 | 方向(未測量) |
| 預期效果——事故 | 從結構上減少 subtree 遞迴、LLM 幻覺這類 BT 事故 | 方向(未測量) |
引入成本的回收週期會因專案的 NPC 規模與人力成本而異,因此建議在你自己的環境中測量上述各項後再作判斷。筆者不會做出"一年內即可回收"之類的斷言——那個數字我們並不掌握。
使用 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"}
]
}
]
}
}
設計師評審後進行模擬 → 通過 → 應用到構建。
BehaviorTree 的自主開發通常在運營規模達到 100 個以上時才具有正當性(設計判斷)。低於此規模時,使用 UE 自帶的 BT 即可。
備選方案: - BehaviorTree.CPP(開源、標準) - Behavior Designer(外部商用) - 自主開發(自由度最高,運營負擔大)
選擇標準參見 7.2.8。
本附錄將正文中出現的縮略語與本書特有術語彙集於一處。正文會在每個縮略語首次出現處展開說明一次,但若你沒有按順序閱讀、或中途遺忘,可在此直接查詢。若同一縮略語在不同語境下含義不同,則兩種含義均予列出。
本術語集按以下順序分組:團隊規模等級 → 遊戲策劃文件 → 遊戲領域 → 資料·運營 → 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 | 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 / 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 這樣含義隨語境而不同的縮略語,正文在首次出現時會一併說明是哪一種。若感到混淆,回到本表檢視即可。
本書的案例與工具幾乎全部以一種環境——即 Claude Code——為前提寫成。因此在審批場合或外部評審中,幾乎每次都少不了一條質疑:"這會不會被綁死在某家公司的特定工具上?"策劃負責人對審批依賴單一供應商的決策感到有負擔,懷疑論者擔心一旦工具更換,本書的方法便會整體崩塌,而評估海外版權的一方則會問:當對方國家以別的工具為標準時,本書是否還有用。三者的表述各異,本質卻相同——都是對供應商鎖定(vendor lock-in),即被一種工具困住的不信任。
本附錄的目的就是回應這種不信任。先說結論:本書所倡導的工作骨架是工具中立的。它既不綁定於某個特定的模型名稱,也不綁定於某個特定的命令列工具。Claude Code 只是把這套骨架實現得最為順暢的容器而已,同樣的骨架可以換裝到別的容器中。本附錄將(1)用表格呈現什麼才是與工具無關的骨架,(2)把 Claude Code 的各個要素與移植到其他環境後所對應之物一一配對,(3)在"模型世代會不斷更替"這一前提下,確立一條確認最新狀態的原則,(4)坦率寫下移植時會失去什麼、又能守住什麼。
貫穿本書全書的工作方式,可以概括為五根支柱。這五者中沒有任何一個是某個特定模型或命令列工具的功能名稱,而都是對"人與人工智慧協同工作時,如何反覆穩定地產出可信結果"這一問題的回答。因此即便工具更換,它們依然留存。
| 骨架 | 是什麼 | 為何工具中立 |
|---|---|---|
| 標準 → 模板 → 驗證關卡 | 把達成共識的規則(標準)固化為填空式的框架(模板),並設定一道自動篩查結果是否遵守了規則的關卡(門禁) | 規則、框架、檢查這些概念,在任何工具中都能用文字或指令碼表達 |
| atom = 一決策一檔案 | 把一個決策寫進一個小檔案,需要時取出使用,修改時只改那一處 | 把決策拆細放進檔案,只需有檔案系統即可 |
| JIT 注入 | 只把當前對話真正需要的決策,即時(Just-In-Time)挑選出來餵給模型 | 這是"只注入必要上下文"的原則,注入方式不過因工具而異 |
| 覆盤迴圈 | 以日、周、月為單位回顧所做的工作,把反覆出現的模式提升為下一次工作的規則 | 回顧與改進的流程靠的是習慣與文件,而非工具 |
| 工具借用邊界 | 借來的只有骨架(演算法、結構),領域資料留在原處(附錄 B) | 借什麼、留什麼的判斷,在任何工具中都相同 |
這張表最右一欄才是關鍵。五根骨架的定義之中,沒有一次出現某個特定產品的名稱。出現的只有規則、檔案、上下文、習慣、邊界這類任何工作環境裡都存在的普適概念。因此,"要是不能再用 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 裡就成了呼叫前親手編寫的前置指令碼。自動化的便利雖有減少,但骨架本身照原樣遷移。也就是說,移植不是"失去功能",而是"用自己的雙手把便利重新鋪設一遍"。
這張圖就是整篇附錄的一頁概要。上方的方框(骨架)無論箭頭指向哪個環境,內容都不改變;只有下方的方框(實現)會隨環境替換。審批中一旦有人說出"供應商鎖定",你只需攤開這一張圖,回答"被綁住的是下面那一欄,不是上面那一欄"即可。
談到移植,最快過時的資訊就是模型名稱。若把寫作本書時最新的模型名稱釘死在正文裡,那麼下一代模型問世的那一刻,那句話就成了錯誤資訊。因此本書從一開始就遵循一條原則:不依賴某個特定模型的名稱與世代編號來講解,而依賴模型所承擔的角色(推理、摘要、程式碼生成之類的功能)來講解。
| 會變的(不要釘死) | 不變的(可以依賴) |
|---|---|
| 模型產品名·世代編號 | 諸如"擅長推理的模型""能接收長上下文的模型"這類角色區分 |
| 上下文上限的具體數值 | "既有上限,就只注入必要上下文"的 JIT 原則 |
| 價格·速度的具體數值 | "昂貴的作業只執行通過了關卡的那部分"的成本意識 |
| 某項功能的開關方法 | "該功能所承擔的角色"以及可替代它的骨架 |
在實務中確認最新模型與功能的方法,每種工具也只需一行。在 Claude Code 中,用 /model 命令即可立即檢視當前所用的模型及可選項;ChatGPT、Cursor 之類的工具,也會在設定介面或模型選擇下拉選單中給出同樣的資訊。因此,如果本書的某句話看上去與模型名稱不符,那不是那句話錯了,而是模型已過了一代。只要角色相同,方法便照舊適用。讀書時若覺得模型名稱陌生,請不要懷疑正文,而應先用 /model 之類的命令,確認你手中工具的最新狀態。
換工具一定會失去一些東西。若掩蓋這一事實,反而會失去信任,所以我先坦率寫下會失去什麼。不過,失去的幾乎都屬於"便利"的範疇,守住的則屬於"骨架"的範疇。也就是說,失去的是重新鋪設便可找回的,守住的則本就不曾繫結在工具上。
| 類別 | 專案 | 說明 |
|---|---|---|
| 失去的(便利) | 自動執行的順暢 | 原本像 hook 那樣自動介入的自動化,如今得靠前置·後置指令碼親手搭建 |
| 失去的(便利) | 一體化的單一介面 | 原本命令、工具、檔案匯聚於同一流程,如今可能得分散到多個工具上 |
| 失去的(便利) | 即取即用的技能·命令 | 斜槓命令與技能得按那個工具的方式重新註冊 |
| 守住的(骨架) | 標準·模板·驗證關卡 | 規則、框架與檢查都是文字或指令碼,在任何地方都照原樣存活 |
| 守住的(骨架) | atom·JIT·覆盤迴圈 | 靠檔案與習慣運轉,故工具更換也依舊維持 |
| 守住的(骨架) | 工具借用邊界(附錄 B) | 借什麼、留什麼的判斷標準與環境無關 |
把這張表壓成一句話便是:移植中失去的,是花些時間就能復原的自動化便利;守住的,則是本書從一開始就竭力置於工具之外的工作骨架。因此,對"這不是供應商鎖定嗎"之問,最誠實的回答是:確有被綁住的部分,但那是可以替換的容器,而真正的價值——內容物——從一開始就不曾綁定於任何容器。願這一篇附錄,能在審批場合替你給出這個答案。
本附錄是一份填空式工作表,用來回答工作室 PD·負責人的這樣一個問題:"當把單人六個月的系統擴充套件到中等規模團隊時,匯入工作量·運營成本·賬號·內網安全該用什麼、如何估算?"如果說正文 19.3(AI 匯入策略與說服管理層)講的是"不要粉飾 ROI",那麼本附錄就是把這一原則同樣應用到匯入成本一側。也就是說,本附錄不提供任何數字。所有單元格都是空白,填寫它們靠的是你所在團隊的測量·估算;凡標註
[需財務確定]的單元格,在財務填寫之前任何人都不得憑估算填補。
本附錄的使用方法如下。首先在 L.1 中,用一張圖把 TCO(Total Cost of Ownership,總擁有成本)拆分為哪些專案理清楚。接著把 L.2\~L.6 的五張工作表按自己團隊規模所在的行,以空白狀態打印出來,自己去測量,或用一行問題交給財務·資訊安全負責人。最後用 L.7 的自查清單確認沒有遺漏的單元格即可。本附錄的價值不在於填好的數字,而在於把容易遺漏的成本專案預先做成單元格。
PD 最常掉進的陷阱,是隻把匯入成本看作"訂閱費 × 人數"。實際的總擁有成本要比這寬得多。它分成一次性投入即結束的匯入工作量(安裝·標準化·上手(onboarding))與每月週期性的運營成本(授權·token·基礎設施·管理人力),在此之上再疊加看不見的安全·賬號管理成本。
這三條分支中,PD 容易低估的是左側(匯入工作量)與右側(安全·賬號)。授權費用會寫在報價單上,而"把單人六個月裡手工積累的標準·技能整理成團隊可共享形態的工作量"與"決定內網中允許外部 LLM 呼叫到何種程度的安全評審",報價單上都沒有,因此它們總是讓工期與預算超支。本附錄的工作表,目的正是先把這些看不見的成本哪怕以空白的形式揭示出來。
正文 19.3.6 說過:"成本的絕對值不寫進書裡——它是要從財務那裡拿來填的空白。"本附錄就是把這些空白該放在何處,逐項鋪開。
這是最先填寫的表。把誰使用哪個工具、這些許可權如何發放·回收,連同人數一併記錄。人數欄用你團隊的實際人頭填,單價欄從報價單或公開價目表取來填。本書不寫單價。
| 專案 | 填寫什麼 | 由誰填寫 | 本團隊值 |
|---|---|---|---|
| 各工具席位數 | 每個工具所需的賬號(席位)數 | 主管 | ______ 席位 |
| 許可權等級分佈 | full / 按任務 cap / 外包一次性人員(附錄 C.1.2) | 主管 | full __人 / 一般 __人 / 外包 __人 |
| 席位單價 | 各工具的每月席位費用 | 財務·採購 | ______ /席位·月 |
| 是否公用金鑰 | 團隊公用 API 金鑰 vs 個人金鑰 | 資訊安全 | □ 公用 □ 個人 |
| 發放流程 | 新入職者賬號的發放路徑·耗時 | 主管 | ______ |
| 回收流程 | 離職·外包結束時金鑰/席位的回收路徑 | 資訊安全 | ______ |
規則有兩條。第一,外包·短期人員不要常設發放席位,而應按任務單位開通並回收(附錄 C.1.2)。第二,回收流程欄為空時,不啟動發放。最常見的事故是離職者賬號未被回收,導致成本與金鑰洩露一併外洩,因此要先設計回收、再設計發放。
這是一張按規模估算"單人六個月"擴充套件為團隊時新增的一次性工作量的表。工作量欄以人日(一個人工作一天的量)為單位,由你團隊實際測量或估算填寫。本書不提供人日數——因為它隨團隊的熟練度·既有標準的整理程度而大幅變化。
| 匯入工作量專案 | 1\~3 人 | 4\~10 人 | 11\~30 人 | 31\~50 人 | 測量/估算主體 |
|---|---|---|---|---|---|
| 環境安裝·配置(工具·hook·許可權) | ___人日 | ___人日 | ___人日 | ___人日 | 主管/基礎設施 |
| 單人資產的團隊共享化(技能·標準·atom 整理) | ___人日 | ___人日 | ___人日 | ___人日 | 主管 |
| 團隊標準的建立(命名·前置後設資料·規則手冊,附錄 D) | ___人日 | ___人日 | ___人日 | ___人日 | 主管 |
| 驗證關卡搭建(lint·規則手冊自動化) | ___人日 | ___人日 | ___人日 | ___人日 | QA/主管 |
| 上手資料製作(與 L.6 聯動) | ___人日 | ___人日 | ___人日 | ___人日 | 主管 |
| 合計(匯入一次性工作量) | ___人日 | ___人日 | ___人日 | ___人日 | — |
填這張表時容易漏掉的是第二行。單人在六個月裡積累在腦中與個人資料夾裡的資產,若要團隊共享,就需要有人把它取出、整理並做成文件的額外工作量。若把這份工作量當作"0",匯入進度必然拖延。此外,表格的單元格形狀也預先呈現出:規模越大,比起安裝工作量,標準建立·驗證關卡(verification gate)的工作量增長得更陡——因為人一多,需要達成一致的標準數量就會增加。
遵循正文 19.3.1 的分階段匯入(由保守到進取),就不必在一個季度內把這份工作量用盡,而可以從第 1 階段(上下文注入)的試點開始分散投入。不要試圖一次性把表中的合計報批,而是先把第 1 階段的工作量單獨拆出來報批,這才現實。
這是 PD·負責人最直接懼怕的領域。逐項檢查有什麼會流向外部 LLM、內網中允許外部呼叫到何種程度。這張表是一份判定合格/暫緩的檢查清單(與附錄 C.6 安全聯動),只要有一項未定,就暫緩該範圍的匯入。
| 檢查專案 | 通過標準 | 負責 | 狀態 |
|---|---|---|---|
| 向外部 LLM 傳輸的資料範圍 | 敏感資料用佔位符(placeholder)/自託管(C.6) | 資訊安全 | □ 通過 □ 暫緩 |
| 支付·個人資訊傳輸 | 明文規定無例外禁止傳輸 | 資訊安全 | □ 通過 □ 暫緩 |
| 內網外部呼叫策略 | 定義允許域名·代理·日誌保留期 | 基礎設施 | □ 通過 □ 暫緩 |
| 是否需要自託管 | 決定核心 IP 是否用自託管模型處理 | 負責人/資訊安全 | □ 已決定 □ 未定 |
| 金鑰洩露事故應對 | 立即更換 + 使用記錄審查路徑(C.7) | 資訊安全 | □ 通過 □ 暫緩 |
| 審計日誌 | 記錄·儲存誰·何時·呼叫了什麼 | 基礎設施 | □ 通過 □ 暫緩 |
| 公司 IP 外洩檢查 | grep watchlist 等事前檢查流程(附錄 B.6) | 主管 | □ 通過 □ 暫緩 |
| 外包訪問隔離 | 外包賬號阻斷核心資產訪問·按任務隔離 | 資訊安全 | □ 通過 □ 暫緩 |
這張表中成本差距最大的一欄是第四行(是否需要自託管)。一旦決定核心 IP 絕不能發往外部 LLM,自託管的基礎設施成本就會整塊疊加到 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,那份工作就會藏進主管看不見的加班裡。
這是一張分階段估算一名新成員在系統之上做到獨當一面所需時間的表。時間欄最準確的做法,是在你團隊裡實際讓人上手一次再測量(與正文 19.3.7 的基線測量方法同一做法)。測量之前,保持空白。
| 上手階段 | 做什麼 | 測量時間 | 備註 |
|---|---|---|---|
| 環境安裝 | 直到工具·hook·賬號配置完成 | ___小時 | 與 L.2 發放流程聯動 |
| 標準學習 | 熟悉命名·前置後設資料·規則手冊(附錄 D) | ___小時 | 有資料則可縮短 |
| 首個任務(保守) | 用上下文注入產出首個成果·通過評審 | ___小時 | 19.3.1 的第 1 階段 |
| 適應驗證關卡 | 按 lint·規則手冊關卡開展工作 | ___小時 | — |
| 達到獨立工作 | 無需監督即可工作·作出採納判定 | ___天 | 上手完成標準 |
填完這張表,就會顯出匯入工作量(L.3)中"上手資料製作"一欄為何重要。上手資料整理得越好,第二·第三行的時間就越短;新成員越多,這份節省就越是累積。也就是說,上手資料製作是一次性工作量,而回收則按成員數量反覆發生。正文 19.3.3 所說的"JIT 自動注入 221 條——新成員也在同一套規則之上工作",在這張表裡體現為第三行的時間縮短。
最後一行(達到獨立工作)才是上手的真正完成標準。若把環境安裝完成誤當作上手完成,監督成本就會不斷堆積在主管身上。標準應當是"無需監督即可作出採納判定"。
最後,是把這些工作表拿給管理層之前,自己必須先通過的專案。秉持與附錄 B.6(借用前檢查)相同的精神,只要有一項為空,就推遲報批,先填那一欄。
| 檢查專案 | 通過標準 |
|---|---|
| 是否已定義賬號回收流程 | L.2 的回收流程欄不為空 |
| 安全檢查是否全部通過/已決定 | L.4 中 □暫緩·□未定 為 0 項 |
| 運營成本的空白是否已交給財務 | L.5 的 [需財務確定] 已作為問題發出 |
| 是否把匯入工作量拆成了階段 | 不按 L.3 合計,而從第 1 階段的工作量開始報批 |
| 上手完成標準是否為"獨立工作" | 以 L.6 最後一行判定完成 |
| 是否未把估算值寫成斷言 | 在所有估算欄標註"估算·樣本數" |
請不要把這張表讀作五欄的合格,而請把它讀作六道鎖。把單人系統擴充套件為團隊這件事確實可行,但這一擴充套件的成本並非授權費用;唯有誠實地填滿這六欄空白,整體圖景才會顯現。而且,任何一欄都不要交給 AI 去填——AI 會像正文 19.3.2 那樣,用煞有介事的數字填補空白。AI 的位置,止於接過你測量的值、整理成報批幻燈片的文句。
本書有五處——§8.2.7(經濟)·§5.4(聲線)·§6.3(人設)·§7.3(模式)·§13.3(資料)——出現了"壓縮為維度向量""嵌入""在向量空間中相近"這類表述,它們都作為方向路標出現。即便沒有機器學習背景,僅憑遊戲策劃的感覺也足以把握這一概念。只要一次性鋪好直覺,這五處便都能用同一張圖來讀懂。
不過要先把一件事釘死:概念上的直覺很簡單,但適用的條件很沉重。 這一構想並非入門——而是隻有立足於本書前文一路搭建起來的基礎之上才能成立的最末端的應用:即保守適用的驗證關卡(verification gate)、資料與遙測(telemetry)基礎設施、像 voice_lint·一致性檢查這樣的各領域檢查器,以及 Layer 整合。若沒有那層基礎就先去畫座標,正如 M.4 所示,那張地圖會連同與遊戲相錯的誤差一起被幹淨利落地壓縮,淪為一個虛像。這五處之所以都"為時尚早",真正的原因就在於此——不是因為構想有多難,而是因為支撐它的基礎要先行。本附錄是一張地圖,供已具備該基礎的團隊在掂量"下一步"時展開來看,而不是讓人邁出第一步的入門書。
無論什麼物件,把它的各項特徵換成一串數字,放到"地圖"上成為一個點,這就是嵌入 (embedding)。那串數字就是維度向量。約定只有一條——讓越相似的物件在地圖上成為越相近的點。
例如,把 NPC 放到(說話的正式程度、情緒表達量、詞彙難度……)這類特徵的座標上,說話方式相近的 NPC 就會在地圖上聚到一起。若是菜譜,則把食材構成放到座標上,相似的菜就會聚到一起——§8.2.7 中作為線索提到的 Epicure 所做的正是這件事。
看著相似其實相反——別與 AHP 混淆。 用於多準則決策的 AHP(層次分析法,Analytic Hierarchy Process,Saaty)也把定性判斷換成向量,這一點看著相似——因為它通過將各準則兩兩相較的兩兩比較,提取出優先順序權重(主特徵向量)。然而方向是相反的。AHP 是由人預先定義準則與層級、再在其中賦予權重的自上而下的決策;而這裡的嵌入是無需人來定義,簇便從資料中自行顯現的自下而上的發現。§13.3 要突破的侷限——"人預先定義的細分群體"——正是 AHP 的出發點。二者不在同一處,而是位於相反的兩端。
地圖很強大,但不是免費的。
所以本書把維度向量當作方向路標,而非處方——這是驗證(遙測·模擬)已紮實鋪好的團隊在數年之後才會去審視的領域。各領域的具體線索散落在 §8.2.7(經濟)·§5.4(聲線)·§6.3(人設)·§7.3(模式)·§13.3(資料)中,全部都可以在本附錄這一張地圖之上來讀。
本附錄面向想把本書用作一學期課程教材的讀者——大學、專科院校、培訓機構的授課教師,企業內訓負責人,學習小組的組織者。要把一本近1,000頁的大部頭按學期切分,比想象中更讓人無從下手。哪一部分放在第幾周、如何把正文中的「動手試試」轉化為作業、以什麼標準為提交物評分——這三點一旦卡住,再好的書也難以被選作教材。本附錄把這三件事做成可以照搬使用的工具。
本附錄的使用方法如下。首先,結合自己的教學日程閱讀 N.1 的 15周進度表(16周與短學期的變體另見 N.2),再用 N.3 的難度徽章與先修知識表衡量學員水平。接著,複製 N.4 的評分量規(rubric),按自己的作業只替換其中的條目即可。所有表格都編排成可以直接列印、貼進教學大綱(syllabus)的形式。
有一點需要先說明。本書每一章都以「動手試試」收尾。讓讀者不是讀完就合上,而是當天就動手——這正是正文的目標;而在教學中,這個「動手試試」正好成為作業的第一手素材。本附錄的進度表之所以一併寫明如何把正文的「動手試試」轉化為各周作業,原因就在這裡。
這是按最常見的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(案例索引)為嚮導,便可從貼近自己環境的案例開始反向檢索進入。
進度流程一目瞭然,如下所示。這是一個具有兩座高峰(期中·期末)的結構:基礎 → 領域深化 → 期中整合 → 流程·運營 → 期末整合。
各學校的學期長度不盡相同。除標準的15周外,這裡給出最常遇到的兩種變體的調整方案。無論採用哪種變體,都建議保留核心作業(第4周的一致性檢查)與兩座彙報高峰——因為這裡正是本書誠實原則(“展示的不是效果,而是結構”)體現得最充分的地方。
| 學期形態 | 調整方法 |
|---|---|
| 16周制 | 標準15周 + 第16周增設 補強·再評估周。給予期末作業重新提交的機會,或從「延伸閱讀」4個部分中由學員投票選定1個部分開設專題講座 |
| 短學期8周(每週2次或集中授課) | 第1周(安裝·匯入) → 第2周(資訊·模式) → 第3周(一致性,核心作業) → 第4周(戰鬥·數值·敘事合併) → 第5週期中彙報 → 第6周(會議·決策·協作) → 第7周(治理·驗證) → 第8週期末彙報。領域縮減為有代表性的3個,「動手試試」在課堂上作為實操吸收消化 |
| 翻轉課堂(flipped classroom) | 把通讀正文放到課前作業,課堂時間全部分配給「動手試試」實操與量規互評。本書的程式碼無需外部依賴即可直接執行,適合以實操為中心的教學 |
即便在同一本書裡,各章所要求的背景知識也不相同。有些章即使是初次接觸終端的大一學生也能跟上,有些章則需要具備資料庫鍵的概念或統計基礎才能完全消化。為便於按學員水平調整進度、或指引先修課程,這裡用三級徽章加以歸納。
各徽章的含義如下。
| 徽章 | 等級 | 含義 |
|---|---|---|
| 🟢 入門 | 入門 | 非專業、大一學生也能跟上。程式碼只需達到複製·執行的程度即可 |
| 🟡 實務 | 實務 | 需要能讀懂程式碼並按自己的資料加以修改。建議理解策劃實務的語境 |
| 🔴 進階 | 進階 | 處於設計·擴充套件演算法與結構的階段。若無先修知識,消化難度較高 |
各周核心部分的徽章與先修知識如下。“先修知識”是指為順利跟上該周而最好提前具備的背景,並非沒有它就無法修讀。
| 周次 | 核心部分 | 徽章 | 先修知識 |
|---|---|---|---|
| 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周的入門軌道充分跟上,為此將作業分軌運營。”
根據學員構成的運營建議如下。
沒有量規,「動手試試」的提交物很容易只按“能跑/不能跑”的二分法來評分。這樣一來,本書最為看重的東西——評審並拒絕 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分的擴充套件量規。能否把整學期講過的工具遷移到自己的專案中——這正是本書在最後所提出的問題,而課程的最終評估也用同一個問題就足夠了。
最後,把本附錄濃縮為一頁,就是這樣。
這份進度表是起點,而非標準答案。請結合自己學員的水平與教學日程,調整週次、更換作業。把本書整本餵給 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 許可協議釋出。允許在署名原作者(李旼洙 · 이민수)與來源的前提下進行非商業性的分享與翻譯;商業性使用需另行獲得作者許可。