這個 Bug 前幾天測試時就露出了馬腳,但一直被我「假裝沒看到」拖延至今,今天決定一次把它解決!
串接 LLM API 時遇到 503 Service Unavailable 或網路突然「鏈結崩壞」完全是日常。以往系統是「算完好感度 ⇒ 直接更新 Firestore ⇒ 發送訊息」,只要 API 在半路拋 Exception,Firestore 裡的好感度就被偷加偷扣,對話卻沒發出去,導致嚴重的狀態錯亂。
這裡借用 Atomicity 的概念:把「產生回應」與「好感度落地」視為同一個應用層操作,只有 API 成功後才 Commit。確定拿到回應才寫入 Firestore,並用 preview_affection 的預覽值即時調製 Prompt,讓莉莉 Agent 被誇獎的當下就能傲嬌軟化,不再出現「下一輪才反應過來」的呆滯感!
system_instruction,完全不污染對話庫。app.py(交易防禦與 RAG 動態隔離)核心改動程式碼:app.py
「Preview 預覽 ⇒ 送 API ⇒ Success ⇒ Commit 落地」
try:
# 先發動意圖雷達判斷 (CHAT 或 TASK)
intent = router.classify_intent(client, user_input)
if intent == "CHAT":
# 背景靜悄悄地感知情緒,換算成好感度增減
delta = evaluator.evaluate_emotion(client, user_input)
# 只在 CHAT 路線評分:TASK 是叫她算數學、寫程式,
# 那種話沒有情緒溫度可言,拿去評分只是白花一次 API
#
# 這裡只 preview 不落地:好感度要等 send_message 成功之後才真的寫入。
# 若先寫入而 API 掛掉(例如 503 過載),就會出現
# 「角色沒有回應、好感度卻加了」的狀況,而且收不回來
preview_affection = st.session_state.fsm.preview_affection(delta)
# 用假設值取這一輪的 System Instruction,
# 所以誇她的那句話當下就會讓她軟化,不會慢一拍
base_instruction = st.session_state.fsm.get_system_instruction(preview_affection)
# --- RAG 記憶檢索與 System Instruction 疊加 ---
rag_context = rag_builder.build_rag_context(user_input)
final_instruction = base_instruction + ("\n\n" + rag_context if rag_context else "")
response = st.session_state.chat.send_message(
user_input,
config=types.GenerateContentConfig(system_instruction=final_instruction)
)
ai_reply = response.text
# 確定拿到回應了,好感度才真的落地寫進 Firestore
st.session_state.fsm.update_affection(delta)
fsm.py(純函數預覽與動態人設生成)import db
class FSM:
def __init__(self, character_id: str = "lily", default_affection: int = db.DEFAULT_AFFECTION):
self.character_id = character_id
# 自動向 Firebase 的 FSM 集合獲取記憶
self.affection = db.get_fsm_affection(self.character_id, default_val=default_affection)
def preview_affection(self, delta: int) -> int:
"""算出加上 delta 之後的好感度,但「不」改動自身、也不寫入資料庫"""
return min(max(0, self.affection + delta), 100)
def update_affection(self, delta: int) -> int:
"""確定 API 回覆成功後,才真正寫入記憶體與 Firestore 雲端"""
self.affection = self.preview_affection(delta)
db.save_fsm_affection(self.character_id, self.affection)
return self.affection
def get_state(self, affection: int | None = None) -> str:
"""根據好感度回傳當前狀態標籤(支援傳入 preview 預覽值)"""
value = self.affection if affection is None else affection
if value <= 15:
return "HOSTILE" # 防備警戒
elif value <= 75:
return "TSUNDERE" # 傲嬌嘴硬
else:
return "IN_LOVE" # 暴走列車
前幾天一直在串基礎建設、搞 RAG 檢索,整體感真的乾到不行,完全沒啥打程式碼的激情。
直到今天把這套 FSM 狀態防禦給補起來。之前測試時最惱人的就是遇到 API 噴 503:好感度已經偷偷加進 Firestore 了,畫面卻因為 API 爛掉而沒印出訊息,搞得資料庫裡的她好像默默接受了我的讚美,但畫面上卻毫無反應,直接演變成幽靈狀態。
另一個很明顯的問題是反應遲鈍。我剛誇完她,舊邏輯讓她這一輪還在跟你在那邊「哼!笨蛋!」,要等到下一輪對話她才突然像神經傳導過來一樣變溫柔,超怪。
今天搞了這個 Preview + 兩階段 Commit 機制:
鐵人賽正式過半,雖然前幾天真的乾,但看著 Agent 漸漸成形,有爽喔!!