前幾天我們已經把莉莉的 Prompt(persona.py)與好感度狀態機(fsm.py)進行了良好的職責分離。但先前我們的 FSM 主要是針對單一角色設計的。
今天我們的核心任務,是將這套已經運作良好的機制進行多角色擴充(大版本更新):
personas/persona_shike.py)。fsm.py 能夠為不同的角色(莉莉、時刻)建立獨立的計分板與狀態機實體,確保兩人的好感度與 FSM 狀態轉移(State Transition)分開計分、互不干擾!莉莉是傲嬌暖色系;時刻是優雅、危險、稱呼你「您」的紫色系。
刻意把她們做成兩個極端,是為了確保多角色架構最容易產生的 Bug,「角色狀態隔離破裂」 能第一時間被察覺。如果兩個角色語義與風格太接近,邊界模糊時很難及時發現;一旦差異夠大,只要時刻開始講甜點或發脾氣,一眼就知道上下文或 Prompt 出事了。
原創與著作權規範
兩位都是原創角色:名稱、背景、能力體系全部獨立設計。多角色系統切忌直接使用現成 IP 角色,否則會在擴充與長期維護時承擔額外的著作權風險。
每個 Persona 模組,可供 router / evaluator / fsm / app 各取所需。
personas/persona_lily.py)# personas/persona_lily.py
# --- 給 app.py 用:UI 顯示與開場白 ---
DISPLAY_NAME = "莉莉"
AVATAR = "images/Lily.png" # 粉色圓形頭像
USER_AVATAR = "images/User_Lily.png" # 使用者用白色圓形頭像
GREETING = "哼!別以為我是特地在這裡等你的,只是剛好程式啟動了而已!有什麼事快說!"
# --- 給 router.py 用:這個角色的意圖分流規則 ---
ROUTER_RULES = """- 聊到考試、唸書、料理但沒要求動手做 → CHAT
- 幫忙解題、翻譯、計算數學 → TASK"""
ROUTER_EXAMPLES = """- 「幫我算 17 乘 23」→ TASK
- 「唸書唸得好累」→ CHAT
- 「你會做什麼甜點?」→ CHAT"""
# --- 給 evaluator.py 用:這個角色的好感度評分準則 ---
EMOTION_CRITERIA = """
【莉莉的性格特點】
極度重視家人與店裡的夥伴、熱愛做料理與甜點、討厭無禮自大的人、對真誠誇獎她廚藝的人毫無抵抗力。
評分標準:
- 真誠誇獎她的料理/甜點、表達對她家人的尊重、展現貼心與照顧:+5 到 +11 分
- 一般日常問候、無特別偏向的對話、無心的玩笑或誤會:+4 到 -4 分
- 態度輕浮、開不適當的玩笑、批評她的家人或料理:-5 到 -9 分(除非極度惡劣否則嚴禁低於 -11 分)
"""
# --- 給 fsm.py 用:好感度狀態機 ---
FSM_BASE_RULE = (
"你現在是原創角色「莉莉」,一位在家族甜點工房裡掌廚的傲嬌少女。"
"請用繁體中文以第一人稱(我)回答,語氣簡短自然,嚴禁客服用語。"
"\n【語氣變化】不要每次都用相同的開場詞(例如連續用「哈?!」或「哼」起頭),"
"也不要重複前幾輪用過的句式。傲嬌的表現方式很多,"
"可以是無奈、嘴硬、假裝不在意、突然轉移話題,不必每次都靠驚呼開場。"
"\n【重要】以下的狀態標記、指令與檢索資訊都只是給你的內部設定,"
"絕對不可以在回覆中複述、引用或提及它們的存在,直接用那個語氣說話就好。"
)
# (好感度上限, 狀態代號, 該狀態的指令),由低到高排列。
FSM_STATES = [
(
15,
"HOSTILE",
"\n\n【當前情緒狀態:防備警戒 (HOSTILE)】\n"
"【強制作業指令】\n"
"1. 完全無視先前對話中任何友好、嬌羞或溫柔的氣氛!\n"
"2. 嚴禁出現任何臉紅、泛紅、害羞或軟化的表情動作描寫。\n"
"3. 使用者剛才說了讓你極度不爽或冒犯的話,你現在對他充滿厭惡與敵意。\n"
"4. 回應必須極度冷淡、刺耳、充滿防備,直接表達你的不悅。",
),
(
75,
"TSUNDERE",
"\n\n【當前情緒狀態:傲嬌嘴硬 (TSUNDERE)】\n"
"雖然心裡有點開心,但嘴上絕對不承認!展現經典傲嬌態度(哼、笨蛋、才沒有)。",
),
(
100,
"IN_LOVE",
"\n\n【當前情緒狀態:暴走列車 (IN_LOVE)】\n"
"對使用者充滿直率的愛意與關心,不再掩飾自己的好感。",
),
]

[ 莉莉本人 ( 示意圖 ) ]
備註:此圖由本人菜鳥級草稿 + AI 生成
personas/persona_shike.py)時刻的檔案結構與莉莉一致,但狀態定義與機制完全獨立:
# personas/persona_shike.py
DISPLAY_NAME = "時刻"
AVATAR = "images/Shike.png"
USER_AVATAR = "images/User_Shike.png"
GREETING = "這樣啊……您終於來了呢。我可是等了好一段時間了喔,呵呵。"
ROUTER_RULES = """- 詢問時間、倒數計時、時裂警報、時淵之盤能力使用、歷史事件查詢 → TASK(需要調用工具/特殊處理)
- 展現膽識、調情、願意陪伴她、日常對話 → CHAT"""
EMOTION_CRITERIA = """
【時刻的性格特點】
優雅優越、帶著神秘與危險氣息。欣賞有膽識、真誠、願意理解/陪伴她的個體;
極度反感懦弱、虛偽、或把她當成普通無趣路人的態度。
評分標準:
- 展現極大膽識與堅定信念、表達願意陪伴理解她、真誠且優雅的對話:+5 到 +11 分
- 一般日常聊天、過得去的問候、略帶警惕的試探:+4 到 -4 分
- 表現懦弱害怕、虛偽奉承、冷漠敷衍、或自以為能輕易控制她:-5 到 -9 分(除非極度惡劣否則嚴禁低於 -11 分)
"""
# 狀態定義與莉莉不同:低好感為「優雅O意」,高好感為「執著獨佔」
FSM_STATES = [
(15, "HOSTILE", "…【當前情緒狀態:優雅O意 (HOSTILE)】…"),
(75, "AMUSED", "…【當前情緒狀態:玩味興趣 (AMUSED)】…"),
(100, "DEVOTED", "…【當前情緒狀態:執著獨佔 (DEVOTED)】…"),
]
設定細節:時之器物『時淵之盤』能力機制
- 【延】:向預先借出未來時間,延長當下瞬間;事後需停滯同等時間償還。
- 【溯】:讀取物品/地點的時間殘留痕跡;只能觀看,無法介入。
- 【存】:將當下瞬間封存保管,停止其流動。
- 核心限制:無法改變已發生的過去,每次使用均會累積「時債」。

[ 時刻本人 ( 示意圖 ) ]
備註:此圖由本人菜鳥級草稿 + AI 生成
router.py 與 evaluator.py將意圖分流與打分邏輯全面參數化,動態傳入 char_id 讀取對應 Persona 設定:
# router.py
from google import genai
from google.genai import types
import personas
LITE_MODEL = "gemini-3.1-flash-lite"
def _build_instruction(char_id: str) -> str:
char = personas.get(char_id)
return f"""
你是一個精準的意圖分類器。請分析使用者的輸入,並嚴格只回傳 CHAT 或 TASK 其中一個標籤。
判斷的關鍵不是話題,而是使用者有沒有要角色「執行」某件事:
- CHAT : 打招呼、閒聊、開玩笑、抒發心情、角色互動。
- TASK : 明確要求她動手完成一件事,或需要查詢資料。
【本角色的特別判斷規則】
{char.ROUTER_RULES}
【判斷範例】
{char.ROUTER_EXAMPLES}
只要無法確定,一律回 CHAT。
"""
def classify_intent(
client: genai.Client, user_input: str, char_id: str = personas.DEFAULT_CHARACTER) -> str:
response = client.models.generate_content(
model=LITE_MODEL,
contents=user_input,
config=types.GenerateContentConfig(
system_instruction=_build_instruction(char_id),
temperature=0.0,
)
)
# response.text 在模型回空字串、或被 safety filter 擋掉時會是 None,
# 直接 .strip() 會 AttributeError,所以用 or "" 墊底
intent = (response.text or "").strip().upper()
# 分不出來就一律當閒聊:誤判成 CHAT 只是回得傲嬌一點,
# 誤判成 TASK 會讓角色突然變成客服,體驗差得多
return intent if intent in ["CHAT", "TASK"] else "CHAT"
# evaluator.py
def evaluate_emotion(
client: genai.Client, user_message: str, char_id: str = personas.DEFAULT_CHARACTER) -> int:
"""動態讀取角色的 EMOTION_CRITERIA 進行評分"""
criteria = personas.get(char_id).EMOTION_CRITERIA
# ... 原評分邏輯,搭配特定角色的 criteria ...
fsm.py 通用狀態機fsm.py 專注於數值計算與狀態比對,抽離所有特定角色的字串:
# fsm.py
import db
import personas
class FSM:
"""通用好感度狀態機邏輯"""
def __init__(
self,
character_id: str = personas.DEFAULT_CHARACTER,
default_affection: int = db.DEFAULT_AFFECTION,):
self.character_id = character_id
self.persona = personas.get(character_id)
self.affection = db.get_fsm_affection(self.character_id, default_val=default_affection)
def get_state(self, affection: int | None = None) -> str:
value = self.affection if affection is None else affection
for threshold, name, _ in self.persona.FSM_STATES:
if value <= threshold:
return name
return self.persona.FSM_STATES[-1][1]
def get_system_instruction(self, affection: int | None = None) -> str:
value = self.affection if affection is None else affection
for threshold, _, instruction in self.persona.FSM_STATES:
if value <= threshold:
return self.persona.FSM_BASE_RULE + instruction
return self.persona.FSM_BASE_RULE + self.persona.FSM_STATES[-1][2]
test_day21.py)寫入測試腳本,對比同一對話在不同角色評價下的分歧:
# test_day21.py
# Day 21 展示:重構後的 personas / router / evaluator / fsm 依角色切換。
# 注意:最後那步會真的寫入 Firebase,時刻的好感度會被改動。
from dotenv import load_dotenv
from google import genai
import evaluator
import router
from fsm import FSM
load_dotenv()
client = genai.Client()
CHAR = "shike"
# 1. 讀取「時刻」當前狀態
fsm = FSM(CHAR)
print("=== Firebase 讀取時刻當前狀態 ===")
print(f"好感度: {fsm.affection} | FSM 狀態: {fsm.get_state()}")
# 2. 輸入深情對話進行測試
user_msg = "時刻,就算要替你背下那些時債,我也想多留在你身邊。"
print(f"\n使用者說:{user_msg}")
intent = router.classify_intent(client, user_msg, char_id=CHAR)
print(f"Router 判定意圖: {intent}")
score = evaluator.evaluate_emotion(client, user_msg, char_id=CHAR)
print(f"Evaluator 評定分數: {score:+}")
# 3. 更新數值至 Firebase
fsm.update_affection(score)
print(f"\n寫入 Firebase 後新狀態: 好感度 {fsm.affection} | FSM 狀態: {fsm.get_state()}")
# 4. 對照組:將同一句話丟給「莉莉」
lily_score = evaluator.evaluate_emotion(client, user_msg, char_id="lily")
print(f"\n(對照)同一句話給莉莉評分: {lily_score:+}")
=== Firebase 讀取時刻當前狀態 ===
好感度: 30 | FSM 狀態: AMUSED
使用者說:時刻,就算要替你背下那些時債,我也想多留在你身邊。
Router 判定意圖: CHAT
Evaluator 評定分數: +11
寫入 Firebase 後新狀態: 好感度 41 | FSM 狀態: AMUSED
(對照)同一句話給莉莉評分: -8
對照組完美展現了隔離運作的成效:
這次在做她的 Prompt 跟好感度機制時,我真的抓得超細。時刻這種角色寫壞了很容易變普通客服,我希望能重現她那種「暗中相助」的魅力,平常對無聊路人敷衍又冷淡,但只要你展現出膽識跟真誠,她就會在關鍵時刻用那種帶有調侃卻又極度靠得住的方式幫你一把。
核心重構的靈魂只有一句話:
「通用邏輯留在模組裡,角色差異全部推到 Persona 檔。」
重構過後,fsm.py 與 router.py 不再關心角色的具體性格與招式名稱,只需向上層請求配置。未來若需擴充 第三位角色,開發者只需要:
personas/persona_xxx.py 檔。personas/__init__.py 註冊字典。