iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
AI Engineering

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

Day 12|把 prompt 從腳本裡搬出來,改角色設定不用碰程式

  • 分享至 

  • xImage
  •  

模組三|AI 生圖與生片產線(Day 10–19)

昨天講風格與角色解耦:畫風走圖片、角色走文字。今天講那些「角色文字」實際放在哪裡。

這篇是純工程,沒有 AI 的部分。但它決定了整條產線好不好改。

問題:prompt 寫在腳本裡,改一個角色要動程式

第一版的生圖腳本長這樣(真的是這樣):

CHARACTERS = [
    ("char-01",
     "front-line assault unit, crimson short hair, heavy combat exosuit with exposed "
     "chip-blade forearm, confident aggressive pose holding a glowing energy cleaver, "
     "red circuitry glow, battle-worn"),
    ("char-02",
     "long-range sniper unit, silver long hair in low ponytail, sleek prosthetic arm, "
     "cold calm expression, kneeling with an oversized rail sniper rifle, cyan scope glow"),
    ...
]

一個 Python list,20 個 tuple,每個裝一大段英文描述。

這在只有三個角色的時候完全沒問題。到 20 個角色之後,它有三個很實際的毛病。

毛病一:改設定變成改程式。

我想調整某個角色的服裝描述(一個純內容的決定),但我要打開 .py、找到那個 tuple、在一堆引號和逗號之間編輯多行字串。改錯一個引號整支腳本壞掉。

內容和程式碼綁在一起,就等於每次改內容都要承擔改壞程式的風險。

毛病二:那段描述沒有地方放脈絡。

角色的設定不只有 prompt。還有 canon 摘要、為什麼這樣設計、哪些地方不能改、參考了哪幾章。

這些東西在 Python list 裡沒有位置。寫成註解會很難看,而且註解跟字串是分開的,改一邊很容易忘記另一邊。

毛病三:一個角色一個 prompt 不夠。

我後來需要針對不同模型寫不同版本的 prompt。同一個角色,送給不同引擎的寫法不一樣。

在 list 結構裡這代表 tuple 要變成 3-tuple、4-tuple……每加一種模型就要改所有 20 筆的結構。

怎麼做:一角一檔,腳本去讀

現在的結構:

docs/character-prompts/
  01-焰刃.md   02-霜瞳.md   03-綠語.md   ...  13-季讀.md
  主律.md  斷香者.md  焙九.md  秤兒.md  縷生.md  調律者.md  靛煙.md
  index.md

20 個角色檔 + 一個索引。

每個檔案的結構固定:

# 秤兒

## Canon 摘要
(這個角色的設定重點、不能改的地方)

## 三視圖 Prompt
```plaintext
(風格段 — 已棄用,見下)
(角色描述 — 腳本讀這裡)

Negative Prompt

(不要出現什麼)

備註

Flux 專用 Prompt

text ...

Midjourney 專用 Prompt

text ...


腳本這樣抽:

```bash
# 取第一個 text 區塊的第 2 段起(角色描述,跳過舊版風格開頭段)
desc=$(awk '/^```text$/{n++; inblk=1; next} /^```$/{inblk=0} inblk && n==1' "$f" \
  | awk -v RS='' 'NR>=2{print; print ""}')

# 第二個 text 區塊 = negative prompt
neg=$(awk '/^```text$/{n++; inblk=1; next} /^```$/{inblk=0} inblk && n==2' "$f")

第一個 awk 抓「第 n 個 code fence 之間的內容」,第二個 awk 用 RS=''(空行分段)取第二段以後。

約定就兩條:第 1 個 text 圍欄是三視圖 prompt,第 2 個是 negative。

為什麼用 Markdown 而不是 JSON

這是這篇我最想講的決定。

JSON 或 YAML 更適合機器讀,這沒有爭議。但這些檔案的主要讀者是我:我改設定、我查 canon、我比對兩個角色的差異,都是用眼睛在讀。

Markdown 讓同一個檔案同時滿足兩邊:

Markdown + 圍欄 JSON
人讀 有標題、有分節、可以放說明段落 一堆跳脫字元的長字串
機器讀 需要一個 awk 約定 直接 parse
放脈絡 隨便放,## 備註 想寫多少寫多少 要另外設欄位,而且不能換行
改一段長英文 就是一段文字 要處理引號跟 \n

代價是機器讀的那端要寫五行 awk。 我認為這筆交易很划算:那五行 awk 寫一次就不用再碰,而我讀這些檔案是每週的事。

判準是:這份資料的主要讀者是人還是機器? 主要讀者是人的,選人類友善的格式,然後補一個小 parser。

效果:改設定真的不用碰腳本

現在的流程是:打開 秤兒.md,改 ## 三視圖 Prompt 裡的文字,存檔,重跑腳本。

gen_turnarounds.sh 從頭到尾不知道有幾個角色、叫什麼名字。它只做一件事:

for f in docs/character-prompts/*.md; do

新增一個角色 = 新增一個檔案。 腳本零改動。

一個小細節:單角失敗不中斷

第七個倒了,我把它記下來,然後讓後面十三個繼續走

OUT="$out" python3 - <<'PY' || echo "FAILED $name (continuing)"

某個角色生成失敗(API 錯誤、內容審核擋下、逾時),印一行 FAILED 然後繼續跑下一個。

這在批次任務裡很重要。20 個角色跑到第 7 個掛掉,如果整支腳本結束,前面 6 個的成果還在但你要手動判斷從哪裡接。讓它跑完,最後看有幾行 FAILED,重跑那幾個就好。

(不過這裡有個陷阱:重跑的前提是 skip 邏輯正確。我的 skip 邏輯是壞的,Day 16 會講。)

代價:我在同一個目錄裡犯了 Day 9 的錯

我用手指數到第二個罐子就拿走了,中間被插進來的那個我沒發現

講完好的部分,講一個我今天才發現的問題。

那個目錄裡除了 20 個 .md,還有兩個檔案:

prompts.json
prompts.yaml

打開來看,它們裝的是同一批內容:

欄位: ['id', 'file', 'name', 'code', 'role', 'sources',
       'canon_summary', 'prompt_base', 'negative_prompt',
       'prompt_flux', 'prompt_midjourney']

prompt_basenegative_promptprompt_fluxprompt_midjourney.md 裡那四個圍欄的內容,在 JSON 裡全部又有一份。YAML 裡再一份。

我實測了同步狀況:

.md 角色檔: 20   prompts.json 筆數: 20
md 有但 json 沒有: 無
json 有但 md 沒有: 無
canon_summary 與 md 不一致: 0/20
prompts.yaml '- id:' 筆數: 20

目前完全同步。 但這不是好消息,這是還沒出事而已。

因為:

  • 腳本只讀 .md
  • 沒有任何機制會在 .md 改動時更新 json / yaml
  • 也沒有任何檢查會告訴我它們不同步了

這跟 Day 9 那張落後 180 回的狀態表,是一模一樣的結構。 一份沒人讀、沒人維護、看起來很正常的衍生資料,安靜地等著過期。

差別只在於狀態表已經爛了,這兩個檔案還沒。

正確的做法應該是:json / yaml 由 .md 生成,不是手寫維護。 寫一支 20 行的腳本,跑一次產出,需要時重跑。

我還沒做。而且照 Day 9 的結論,我大概不會做,因為它不在我的工作路徑上。

代價二:四個圍欄只用了兩個。

每個檔案有 Flux 和 Midjourney 專用 prompt,但現行產線一個都沒用到。它們是之前試別的模型時留下的。

沒有壞處,但它們也在等著過期:設定改了,那兩個圍欄不會有人記得改。

代價三:awk 的約定是隱性契約,而它已經被違反了。

「第 1 個 text 圍欄是三視圖 prompt、第 2 個是 negative」這件事,只寫在腳本裡。

我原本要在這裡寫「如果哪天有人多加一個圍欄就會抽錯」當成假設的風險。然後我去查了一下,發現它已經發生了。

其中一個角色檔中間多了一節:

## 三視圖 Prompt
```text ... ```          ← 圍欄 1

## 三視圖 Prompt v2(新風格 · 風格參考工作流)
```text ... ```          ← 圍欄 2  ← 腳本把這個當成 negative prompt

## Negative Prompt
```text ... ```          ← 圍欄 3  ← 真正的 negative,從來沒被送出去過

而圍欄 2 的內容是一段完整的正面 prompt:風格指示、版面規格、配色、角色描述、渲染要求全都在裡面。

所以這個角色每次生成,送出去的指令實際上是:

請避開:白色亮面裝甲、深紅短鮑伯頭、緋紅晶刃、高細節畫風⋯⋯

它在叫模型避開這個角色自己的設計。

20 個檔案裡只有這一個中招(其餘 19 個都是 4 個圍欄)。而它沒有報錯、沒有異常、輸出看起來完全正常——跟 Day 16 要講的那個 bug 是同一種形狀。

修法是不要用序號定位。 改成用標題:

fence_under() {  # $1=檔案  $2=完整的 H2 標題行
  awk -v want="$2" '
    /^## /      { insec = ($0 == want); next }
    !insec      { next }
    /^```text$/ { inblk=1; next }
    /^```$/     { if (inblk) exit }
    inblk       { print }
  ' "$1"
}

desc=$(fence_under "$f" '## 三視圖 Prompt' | awk -v RS='' 'NR>=2{print; print ""}')
neg=$(fence_under "$f" '## Negative Prompt')

再加兩行空值防呆:抽不到內容就 skip 並印出原因,不要送空字串出去。

序號是位置,標題是語意。 位置會因為任何插入而失效,語意不會。

帶走什麼

一、內容和程式碼要分開,判準是「改這個東西的人在想什麼」。

如果修改時你在做的是內容決定(這個角色該是什麼樣子、這段文案怎麼寫、這個閾值該多少),那它就該在資料檔裡,不該在程式裡。

在程式裡的代價不是不能改,是每次改都要繞過程式的語法,而且改壞的風險跟改內容的風險混在一起。

二、資料格式看主要讀者是誰。

  • 主要給機器讀 → JSON / YAML
  • 主要給人讀、機器偶爾讀 → Markdown + 一個小 parser

寫 parser 的成本是一次性的,讀不舒服的格式的成本是每次的。

三、用語意定位,不要用序號定位。

「第 2 個圍欄」「第 3 欄」「倒數第二個元素」,這類基於位置的約定,會被任何一次插入無聲地破壞。

而且破壞的方式最惡劣:它不會報錯,它會拿到另一個合法但錯誤的東西。

改用有語意的錨點(標題、鍵名、標記),插入就不再是威脅。

我這次的教訓是:我在寫文章時把它當成「未來可能的風險」,結果一查發現它已經在 repo 裡活了一段時間。你認為「將來可能會發生」的隱性契約問題,值得現在就去 grep 一次。

四、任何「同一份內容存在兩個地方」的結構,都要問一句:誰負責同步?

答案只有三種:

  1. 生成:一邊是源頭,另一邊由腳本產出(可靠)
  2. 檢查:兩邊都手寫,但有 CI 或測試比對(可接受)
  3. 紀律:靠人記得(會爛,只是還沒爛)

我的 .md / json / yaml 是第 3 種。它現在 0/20 不一致,看起來很健康——但健康的是今天的狀態,不是這個結構。

判斷一個重複是不是問題,不要看它現在同不同步,要看它不同步的時候誰會知道。沒有人會知道的,就是遲早的事。


明天 Day 13,講一個完全不同類型的問題:同一個 prompt 連續 4 次被判 NSFW,改寫措辭完全無效。我會講排查的順序、最後怎麼繞過去,以及一個結論——有時候換模型比改 prompt 快得多。


上一篇
Day 11|附一張參考圖,然後叫模型忽略這張圖裡的角色
系列文
《矽墟》:我把一部科幻小說當成軟體專案來管12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言