iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
AI Engineering

《矽墟》:我把一部科幻小說當成軟體專案來管系列 第 18

Day 18|出圖提示詞裡,不准出現角色的名字

  • 分享至 

  • xImage
  •  

模組三|AI 生圖與生片產線

一篇 294 個字的短篇,四個角色,一行指令:

/novel-characters novel/短篇/04.01_沒有痛的殘象.md

十分鐘後拿到這些:

novel/短篇/04.01-角色卡/
├── 04.01_沒有痛的殘象-cast.json    28 KB
├── 04.01_沒有痛的殘象-cast.md      27 KB
├── report.html                     95 KB
└── images/*.png                    4 張三視圖

四個角色,每個一份:人物畫像、卡通形象提示詞(中英雙份)、音色提示詞(中英雙份)、三視圖提示詞、逐字引文。

而其中一條硬規則是:出圖提示詞裡絕對不准出現角色的名字。

這條規則第一次看到會覺得莫名其妙——描述一個角色,卻不准叫他的名字。今天這篇就從這條規則講起,因為它是 Day 11 那條「參考輸入會全盤繼承」的反面,而反面比正面更難想到。

問題:200 回,每一回都要重新回答同一組問題

Day 28 會講 20 章重構成 200 回這件事。這裡先講它造成的一個副作用。

一回短篇大約 300 字。每一回我都要回答同一組問題:

  • 這一回誰在場?
  • 這一回他們各自做了什麼、變成了什麼樣子?
  • 要配圖的話,圖裡的人該長什麼樣、穿什麼、拿什麼?
  • 要配音的話,這個人的聲音該是什麼質地?

前兩題是創作,後兩題是規格。而後兩題的答案,理論上 200 回都一樣——焰刃永遠是緋紅短鮑伯頭配白黑外骨甲,霜瞳永遠是銀色低馬尾配軌道狙擊槍。

問題是「理論上一樣」跟「實際上一樣」中間隔著 200 次手寫。

我第一版的做法是每回配圖時,在插圖 manifest 裡重新寫一段人物描述。寫到第三回就出現了 Day 11 講過的老毛病:同一個人,第一回寫「銀髮低馬尾」,第三回寫成「銀色長髮」,第五回乾脆只寫「狙擊手」——於是模型畫出一個泛用狙擊手。

每一次重寫,都是新增一個會漂的來源。

而我掃過那 20 份角色設定檔之後的結論是:三視圖那張 PNG 才是真正的規格,文字設定檔只是它的註解——因為三視圖每次生圖都會被讀,錯了下一張圖就會錯;而文字設定檔沒有任何流程會讀它,死掉沒有症狀。這篇要處理的是下一層問題:註解本身要怎麼大量生產而不互相矛盾。

這個 skill 實際做的事:兩趟掃描 + 一個校驗器

novel-characters 的管線長這樣:

原文 .md
  │
  ├─ chunk      切塊(單次上限 24 塊 / 約 33 萬字元,超了明確報 truncated)
  │
  ├─ 第一趟:抽角色   → roster-NN.json    {name, aliases, note, quotes}
  │
  ├─ merge      按名字+別名收斂,notes 累加、quotes 去重,按出現塊數降序
  │
  ├─ 第二趟:出角色卡 → card-<slug>.json  {persona, image, voice}
  │
  ├─ validate   ⛔ 不能跳
  │
  └─ render     → cast.md + report.html(自動嵌 images/<slug>-turnaround.png)

兩趟分開的理由,是第二趟看不到原文。

這聽起來像缺陷,其實是刻意的:第一趟的 note 欄位如果寫得不夠密,第二趟就編不出東西。所以 skill 的第一趟指示裡直接寫死「後一趟看不到原文,只看得到你寫的 note,所以細節必須落在 note 裡」——用結構逼出品質,而不是用「請寫詳細一點」這種話。

長篇時第一趟可以每塊一個子代理並發跑。我這次的短篇只有一塊,跳過並發,直接自己讀。

實際跑起來

$ node scripts/novel-characters.mjs chunk book.txt wd
{ "chunks": 1, "chars": 294, "truncated": false }

一塊,294 字元,沒截斷。chunks == 1 就跳過第一趟的並發,自己讀完寫成 roster-00.json

$ node scripts/novel-characters.mjs merge wd
[ { "name": "調律者", "aliases": ["我"], ... }, ... ]

merge 這步在單塊時看起來多餘,但它做的事在多塊時很重要:把「陸行遠 / 陸 / 姑娘」收斂成一個人。這次它處理的是「調律者 / 我」——第一人稱敘述者跟角色名的合併。

然後是第二趟出卡,最後:

$ node scripts/novel-characters.mjs validate 04.01_沒有痛的殘象-cast.json book.txt
✓ 4 个角色全部通过校验

校驗器擋掉的四類錯

這一步是整個 skill 最值得抄的部分。它檢查四件事,而這四件事的共同點是:模型真的會犯,而且犯了不會報錯。

一、引文必須逐字

我把抄下來的六行覆回原件上,才看見有幾行已經對不上

角色卡裡有個 persona.evidence 欄位,放的是原文引用。校驗器拿它逐字比對原始檔案,對不上就 exit 1。

這條規則在這次實際救了我一次,而且是我完全沒預期的方向。

我做完 04.01 的角色卡、validate 顯示「✓ 4 个角色全部通过校验」之後,繼續往下做別的事。中途我回頭再跑了一次同一個指令:

✗ 6 处违规:

  [調律者] 引文不是原文逐字片段:綠語不讓我直接接進調律壇。
  [調律者] 引文不是原文逐字片段:我點起訊號香,想只測方向。
  [綠語] 引文不是原文逐字片段:她說這段東西沒有痛過。
  [綠語] 引文不是原文逐字片段:她說沒有傷口的東西,先別急著縫。
  [霜瞳] 引文不是原文逐字片段:霜瞳帶回的晶片比綠語的還薄。
  [霜瞳] 引文不是原文逐字片段:霜瞳把它放到香案最外側。

角色卡沒動過。是原文被改了。

我在做角色卡的同時,正稿那邊在潤稿。原文從:

綠語不讓我直接接進調律壇。

改成:

我問綠語,能不能直接接進調律壇。

她搖頭,說沒有傷口的東西,先別急著縫。

意思相近,但敘事方向反了——原本是綠語主動阻止,改完變成調律者主動請示、被拒絕。這對角色卡是有影響的:arc 那一欄本來寫「第一次接受同伴的煞車」,改完之後應該是「第一次讓別人決定他能做到哪裡」。

其他五處也一樣:加了「邊緣沒有一點磨損」、「霜瞳自己把它放到最外側」、多出一句「綠語說可以只測方向」。都是潤稿層級的小改,沒有一處會讓角色卡看起來壞掉。

這就是逐字校驗的價值:它不檢查意思對不對(那檢查不了),它只檢查「這句話今天還在不在」。

而衍生物跟來源脫鉤,正是從這種「小到不值得同步」的改動開始的。

這條可以直接搬:任何複製了來源片段的衍生物——文件引程式碼、投影片引數據、測試名稱引需求敘述——都該存一份逐字比對,而且要能重跑。因為語意有沒有漂沒辦法自動檢查,但「這個字串還在不在」可以。

二、出圖提示詞不准出現人名

我以為只蓋了名字,章一抬起來,紙上印的是一整張圖

開頭那條規則。理由 skill 寫得很直接:

圖像模型對這些偏見極重,會畫成它記憶裡的角色而不是你的角色。描述這個人,不要叫他的名字。

Day 11 的結論是「參考輸入會全盤繼承,你必須明確關掉不要的維度」。當時講的是參考圖。

而角色名字是一個你沒意識到自己附上去的參考輸入

模型看到一個名字,會去找它訓練資料裡叫這個名字的角色,然後把那個角色的長相摻進來。你以為你只是在標示「這是誰」,實際上你附了一張看不見的參考圖。

而且這個污染沒有症狀:你不會收到警告,只會拿到一張「怪怪的,但說不上哪裡怪」的圖。

所以校驗器直接掃 image.prompt / image.promptZh / image.turnaround 三個欄位,出現任何角色名、別名就報錯。用機械檢查代替自律。

(這條規則後來害我炸掉一次生圖——因為我把它搬到了另一條規則剛好相反的管線上。明天 Day 19 會講。)

三、語言分工

角色卡是中英混合的:personavoice 的描述欄位強制中文(給人看、給我自己判斷),image.prompt / negativePrompt / tags / turnaround / voice.prompt 強制英文(給模型吃)。

skill 的文件裡特別點名一個最容易寫錯的地方:

voice.timbre / pitch / pace / accent / emotion / referenceHint 是中文,不是英文

因為它們就在 voice.prompt(英文)隔壁,寫著寫著就一路英文下去了。

這條規則的存在本身就是證據——它是被真實輸出打出來的,不是設計時想到的。

四、importance 只能是那四個值

protagonist / major / supporting / minor。enum 檢查,最無聊的一條,但模型真的會自己發明 leadsecondary


四類錯,共同形狀是:都不會爆炸,只會安靜地產出稍微錯一點的東西。

而稍微錯一點的東西,是最貴的那種錯——因為它會通過人工目視。

我改了 skill 預設的三件事

skill 是通用的,專案是具體的。這次有三個地方我沒照預設走。

一、畫風換成專案自己的 house style

skill 預設的 image.styleFlat vector cartoon with ink-wash colouring——扁平向量卡通。

而這個專案的既有視覺是另一回事:assets/turnarounds/mj-prompts/ 底下每個角色一份 prompt,開頭全部是同一串骨架:

official game character design sheet, premium industrial sci-fi anime style,
character turnaround presentation, full body front view side view back view,
strict orthographic, neutral standing A-pose, same character shown three times,
exact same outfit and proportions in all views, ...

如果我讓 skill 用預設畫風產一批角色卡,那份卡就會描述一個「扁平向量卡通版的焰刃」,跟 200 回插圖裡的那個人不是同一套視覺。

所以我把四張卡的 image 區塊全部改寫,對齊既有骨架,角色外觀細節逐項照抄 mj-prompts/01-焰刃.txt 那類檔案(緋紅短鮑伯、右前臂內建刀匣、白黑外骨甲配紅色重音線)。

skill 的預設是它自己的意見,不是你的規格。 產出跟既有資產不一致的時候,改的是產出。

二、三視圖不重生,直接接既有的

skill 的第 8 步是用 codex 內建的 $imagegen 幫主要角色生三視圖。

但這四個角色的官方三視圖早就存在:

assets/turnarounds/char-01-ef-turnaround.png   # 焰刃
assets/turnarounds/char-02-turnaround.png      # 霜瞳
assets/turnarounds/char-03-turnaround.png      # 綠語
assets/turnarounds/char-tuner-turnaround.png   # 調律者

重生一輪要花錢,而且畫風一定會飄(skill 自己的文件就寫了「同一批角色各自獨立出圖,畫風會漂」)。

所以我沒生圖,改成 symlink:

ln -sf ../../../../assets/turnarounds/char-01-ef-turnaround.png images/焰刃-turnaround.png

render 那步只認 images/<slug>-turnaround.png 這個路徑,不管它底下是實體檔還是連結。報表裡 12 個 images/ 引用全部指到既有的官方圖。

零成本、零漂移,而且是同一份檔案,不是複製品——三視圖哪天更新,角色卡報表跟著更新。

三、輸出放子資料夾

skill 預設把產出丟在原書同級目錄。這個專案的 novel/短篇/ 底下是 200 份稿源,多丟 3 個檔案 + 一個 images/ 進去會很難看。

改成 novel/短篇/04.01-角色卡/。一回一個資料夾。

兩回之間,我複製了什麼、重寫了什麼

做完 04.01 接著做 04.02(同樣四個角色)。這一步才是真正驗證這套流程的地方。

欄位 04.01 → 04.02 為什麼
image.*(全部) 原封不動 外觀不隨劇情變
voice.timbre/pitch/pace/accent 原封不動 音色不隨劇情變
voice.emotion 微調 「做決定時句尾壓實」是這一回才有的
persona.appearance 加一件道具 綠語這回多掛了共用熱水壺
persona.oneLiner 全部重寫 一句話抓這個人「在這一回」做了什麼
persona.arc / temperament / motivation 全部重寫 這一回的變化
persona.evidence 全部換 綁在這一回的原文

這張表就是 Day 11 那條「要固定的東西走不變的通道,要變的東西走可變的通道」的第二次應用——只是這次的「通道」不是 prompt 的參數位置,是 JSON 的欄位分組。

imagevoice 整塊複製,是因為它們在設計上就不該隨回數變。如果哪一回我發現自己在改 image.prompt,那代表兩件事之一:這一回角色真的換裝了(那要回頭更新三視圖),或者我手滑了(那是 bug)。

欄位分組本身就是一個約束機制:把「該變的」和「不該變的」放進不同的欄位,改動的位置就會告訴你出了什麼事。

這份角色卡不是聖經

這件事必須講清楚,否則前面那條「三視圖才是規格」就被自己推翻了。

novel-characters 產的是衍生物,不是來源。它的來源有兩個:

  • 外觀規格 → assets/turnarounds/*.png(三視圖 PNG)
  • 這一回的角色狀態 → 那一回的原文

角色卡是把這兩個東西合成的一次性快照。它會過期——第 50 回的時候,04.01-角色卡/ 裡的東西早就跟現況無關了。

但這不要緊,因為它從一開始就不是 canon,而是「04.01 這一回的角色狀態」。 它的有效期就是那一回。

真正危險的做法是反過來:拿角色卡當設定檔、下一回從角色卡複製、再下一回從再下一回複製。三回之後你就有了第三個會漂的來源,而且沒有人記得它是從哪裡長出來的。

所以我的規矩是:每一回都從原文 + 三視圖重新產一次,不從上一回的角色卡複製。

看起來比較笨,但笨的那條路沒有累積誤差。

代價

代價一:短篇太短,skill 的分塊機制完全用不到。

294 字元對一個設計來處理數十萬字的東西來說是浪費。真正該用它的是整卷或整部,短篇單回其實只用到「校驗器 + 輸出格式」這兩件事。

我還是用了,理由是輸出格式統一比效率重要——200 回的角色卡如果格式各異,那跟沒有一樣。

代價二:image / voice 整塊複製是我手動做的。

上面那張表講得像是有機制在保障,其實沒有。是我自己在寫 04.02 的時候,把 04.01 的 image 區塊複製過去。

真正該做的是把 imagevoice 抽成一份 per-character 的共用檔,每回的卡只 $ref 過去。現在沒有,所以「不該變的東西」實際上每回都被重新輸入一次——這正是我這篇在批評的事,只是我還沒修。

代價三:只有引文有防護,歸納摘要沒有。

上面那次原文改動,validate 抓出 6 處引文不符——但它抓不到 persona.arc 那句「第一次接受同伴的煞車」也一起過期了。那句不是引文,是歸納,校驗器不驗它。

我是看到那 6 條錯誤、回去讀新版原文,才順手發現 arc 也要改。如果那 6 句引文剛好都沒被潤到,我就不會回去讀原文,arc 就會一直錯下去。

也就是說:逐字引用有防護,歸納摘要沒有——而角色卡的大部分內容都是歸納摘要。引文校驗實際上是在當「來源變了」的煙霧偵測器,它的覆蓋率取決於引文引得夠不夠多。

代價四:這套流程沒有跨回一致性檢查。

04.01 和 04.02 的焰刃 image.prompt 是不是同一份,目前靠我自己看。沒有腳本會 diff 兩回之間該相同的欄位。

該補的東西很明顯(比對同名角色的 image 區塊、不同就報錯),但我還沒寫。這是「工作路徑旁邊的東西維護不動」的另一個例子——差別只在於這次我知道它會爛。

帶走什麼

一、用機械檢查代替自律。

那四類錯(引文不逐字、prompt 混入人名、語言分工錯、enum 亂填)都是「知道規則也還是會犯」的那種錯。寫在文件裡沒有用,寫成 exit 1 才有用。

判斷句:這條規則違反的時候,會有東西叫嗎?

  • 會 → 它是規格
  • 不會 → 它是建議,而建議會爛

二、逐字比對能做,語意比對不能做,所以把該逐字的部分逐字化。

引用、識別碼、路徑、版本號,這些東西都該存原文並機械比對,而且要能隨時重跑。衍生物不是產出的那一刻壞掉的,是來源後來被改了、而沒有人回頭看衍生物。

我這次就撞到了:正稿在潤,角色卡在另一個資料夾裡靜靜過期。校驗器不懂潤稿改了什麼,但它知道那六個字串已經不存在了。

三、衍生物要標明有效期,不要讓它變成第二來源。

角色卡是快照,快照會過期,這沒問題。有問題的是把快照當成來源、下一份從快照複製。

規矩很簡單:每次都從真正的來源重新生成,不從上一份衍生物複製。

貴一點,但不會累積誤差。而累積誤差是那種你發現的時候已經來不及回頭的東西。


明天 Day 19,把這份角色卡拿去做事:同一張插圖生了九次,兩個坑都是靜默失敗——一條規則被我搬離了它成立的前提,還有一張我從頭到尾沒打開過的立繪。最後救場的不是更長的 prompt。


上一篇
Day 17|我昨天答應給成本數字,今天給不出來
系列文
《矽墟》:我把一部科幻小說當成軟體專案來管18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言