用 LLM 打造 AI 地城主:多人 TRPG 遊戲引擎的架構設計
深入解析一套由大型語言模型驅動的多人桌遊系統架構,涵蓋 NPC 語意記憶(pgvector)、雙層行動驗證引擎、劇本自動生成 Pipeline、即時 WebSocket 多人互動,以及 LLM 並發控制策略。
專案動機
桌遊(TRPG)最大的門檻是「找到一個好的 DM(地城主)」。一個好的 DM 需要即興反應能力、對規則的深入理解、以及創造沉浸感的說故事技巧。
如果 LLM 能扮演 DM 的角色呢?
這個專案的目標是打造一套完整的 AI 驅動 TRPG 遊戲引擎。玩家透過類終端機的 Web 介面進行遊戲,AI 負責:NPC 對話、場景描述、行動判定、戰鬥旁白、甚至整個劇本世界的自動生成。
系統全貌
[Next.js Frontend] ←WebSocket→ [NestJS Backend]
│ │
│ ┌────────┼────────┐
│ │ │ │
│ ▼ ▼ ▼
│ [PostgreSQL] [Redis] [Ollama/LLM]
│ + pgvector
│
CLI 風格 UI
打字機效果
骰子動畫
技術棧
| 層級 | 技術 | |------|------| | Monorepo | pnpm workspaces + Turborepo | | Frontend | Next.js 14 (App Router) | | Backend | NestJS (TypeScript) | | Database | PostgreSQL + pgvector | | Cache/PubSub | Redis | | WebSocket | Socket.IO | | LLM | Ollama (qwen2.5:7b-instruct) | | ORM | Drizzle ORM | | Auth | JWT + Google OAuth |
LLM 整合:五個關鍵系統
LLM 不是一個簡單的「生成文字」功能,而是深入到遊戲的多個核心系統中。
1. NPC 對話與人格
每個 NPC 有結構化的人格定義:
interface NpcPersonality {
traits: string[]; // 性格特點
speaking_style: string; // 說話風格
background: string; // 背景故事
knowledge: string[]; // 知道的事情
restrictions: string[]; // 不會透露的祕密
}
對話時,系統將人格轉換成中文 system prompt,注入記憶和場景上下文:
function buildNpcConversationPrompt(npc: Npc, memories: Memory[], playerName: string) {
return `你是「${npc.name}」,${npc.personality.background}
性格特點:${npc.personality.traits.join('、')}
說話風格:${npc.personality.speaking_style}
你記得的事情:
${memories.map(m => `- ${m.content}`).join('\n')}
限制:你絕對不會主動提及以下資訊:
${npc.personality.restrictions.join('\n')}
現在「${playerName}」正在跟你說話。請以角色的身份回應。`;
}
2. NPC 語意記憶(pgvector)
NPC 不只是「每次對話都從零開始」。他們有持久記憶——而且是語意搜索的記憶。
當一段對話結束,系統會:
- 用 LLM 生成 768 維的 embedding vector
- 存入
npc_memories表(帶有importance權重 1-10)
下次對話時,用玩家的當前語句做語意搜索:
SELECT content, importance,
1 - (embedding <=> $queryVec::vector) AS similarity
FROM npc_memories
WHERE npc_id = $npcId
AND embedding_status = 'completed'
AND 1 - (embedding <=> $queryVec::vector) >= 0.7
ORDER BY similarity * (importance::float / 10.0) DESC
LIMIT 5
排序公式是 similarity × importance——確保重要記憶(如「玩家曾經救過我的命」)不會被瑣碎但語意相似的記憶蓋過。
為什麼用 pgvector 而不是 Pinecone/Qdrant?
- 不需要額外的服務(已經有 PostgreSQL)
- NPC 記憶的規模不大(幾千條而非幾百萬條)
- Drizzle ORM 用
customType就能支援
3. 雙層行動驗證引擎
玩家在遊戲中可以嘗試做任何事情。系統需要判斷「這個行動在這個世界中是否合理」。
第一層:規則引擎(快速、確定性)
function validateByRules(action: string, worldRules: WorldRules): ValidationResult {
// 關鍵字檢查:中世紀世界不能用手機
if (worldRules.era === 'medieval' && containsModernTech(action)) {
return { valid: false, reason: '這個世界沒有這種科技' };
}
// 物理規則:沒有飛行能力不能飛
if (isFlightAction(action) && !playerHasFlightAbility()) {
return { valid: false, reason: '你沒有飛行的能力' };
}
// 簡單移動:直接通過
if (isSimpleMovement(action)) {
return { valid: true, determined: true };
}
// 無法確定 → 交給 LLM
return { determined: false };
}
第二層:LLM 判定(複雜情境)
規則引擎無法判斷時(例如「我嘗試用巧言說服守衛放我進去」),送給 LLM 判斷:
const llmPrompt = `世界設定:${worldSetting}
物理規則:${physicsRules}
魔法系統:${magicSystem}
玩家狀態:位於${location},擁有${inventory}
玩家嘗試的行動:${action}
請判斷這個行動是否合理,回傳 JSON:
{ "valid": bool, "result": "行動的結果描述", "reason": "判斷理由", "side_effects": [] }`;
雙層設計的好處:
- 簡單情況不浪費 LLM 算力(規則引擎毫秒級回應)
- 複雜情況才動用 LLM(保持靈活性)
- LLM 失敗時 fallback 為「允許但警告」(遊戲不會卡住)
4. 劇本世界自動生成
最強大的功能:輸入一個劇本概念,LLM 自動生成整個可玩的世界。
生成是鏈式的——每一步都參考前面的結果:
Locations → NPCs → Items → Quests
│ │ │
│ ▼ │
│ (放在哪個地點)│
│ ▼
└─────────────(出現在哪裡)
async runGeneration(scenario: Scenario) {
// Step 1: 生成地點
const locations = await this.generateWithLlm(
buildLocationsPrompt(scenario)
);
// Step 2: 生成 NPC(知道有哪些地點)
const npcs = await this.generateWithLlm(
buildNpcsPrompt(scenario, { generatedLocations: locations })
);
// Step 3: 生成物品
const items = await this.generateWithLlm(
buildItemsPrompt(scenario, { generatedLocations: locations })
);
// Step 4: 生成任務(知道地點和 NPC)
const quests = await this.generateWithLlm(
buildQuestsPrompt(scenario, {
generatedLocations: locations,
generatedNpcs: npcs,
})
);
}
用 JSON Schema 約束 LLM 輸出格式 + Zod 後驗證,確保結果結構正確。失敗則以 temperature=0.3 重試一次。
5. LLM 並發控制
多人同時遊戲時,LLM 請求可能瞬間暴增。用 Semaphore + Priority Queue 控制:
class LlmService {
private semaphore = new Semaphore(MAX_CONCURRENCY); // 預設 3
private queue = new PriorityQueue();
async complete(prompt: string, priority: 'high' | 'low') {
if (priority === 'low' && this.queue.isFull()) {
return null; // 低優先級在佇列滿時直接丟棄
}
await this.semaphore.acquire(priority);
try {
return await this.callLlm(prompt);
} finally {
this.semaphore.release();
}
}
}
優先級設計:
- high:玩家即時操作(NPC 對話、行動判定)→ 一定要排到
- low:背景任務(embedding 計算、劇本生成)→ 佇列滿就丟棄
這確保了玩家體驗不會因為背景任務而卡頓。
遊戲系統支援
系統不只支援 D&D,而是一個通用的 TRPG 引擎:
- D&D 5e:六大屬性、技能檢定、HP/AC/先攻
- 克蘇魯的呼喚 7e:SAN 值、理智檢定、暫時/永久瘋狂
角色創建是一個 6 步驟的精靈介面:遊戲系統 → 基本資料 → 種族/職業 → 屬性點數 → 技能分配 → 確認。
前端:CLI 風格的沉浸體驗
整個遊戲介面模擬終端機風格:
- 打字機效果:AI 的回應逐字顯示,模擬 DM 說話
- 命令輸入框:玩家輸入行動(不是選擇題)
- 骰子動畫:技能檢定時顯示擲骰結果
- 經驗值進度條:角色成長視覺化
- ASCII Art 標題:增加復古感
多人即時互動
透過 Socket.IO 實現多人同世界互動:
- 玩家加入同一個 world instance
- 一個玩家的行動結果會廣播給其他玩家
- NPC 的回應所有人都看得到
- 戰鬥時按先攻順序輪流行動
學到的經驗
LLM 輸出的不確定性是最大挑戰
即使用 JSON Schema 約束,LLM 偶爾還是會輸出無法解析的結果。必須在每一層都做 fallback:
- 格式錯誤 → 重試一次(低 temperature)
- 重試失敗 → graceful degradation(預設行為)
- 永遠不讓遊戲卡在「LLM 失敗」上
7B 模型的能力邊界
Qwen 2.5:7b 能處理大多數簡單的 NPC 對話和行動判定,但在複雜的劇本生成和多步推理上偶爾會出問題。未來可能需要:
- 簡單任務用 7B
- 複雜任務(劇本生成)用更大的模型或雲端 API
pgvector 的實用性
對於「每個 NPC 幾百條記憶」這個規模,pgvector 完全夠用。不需要專門的向量資料庫。但 Drizzle ORM 不原生支援 vector 型別,需要用 customType 手動處理序列化。
結語
這個專案展示了 LLM 在遊戲領域的一個有趣應用:不是用 AI「取代」人類 DM,而是讓更多人能在沒有 DM 的情況下也能享受 TRPG 的樂趣。
技術上最有價值的模式是「雙層驗證」(規則引擎 + LLM fallback)和「語意記憶」(pgvector embedding + importance 加權)。這兩個模式在 TRPG 之外的場景也有廣泛的應用價值——任何需要「在規則框架內做創造性判斷」的系統都可以參考。
關於作者

GWP4 STUDIO 創辦人,超過 8 年軟體開發經驗,專精資料工程、全端開發與系統架構。 持續在部落格分享專案實作經驗與技術心得。