บทนำ: ทำไม NPC ต้องการ LLM
ในยุคที่เกม indie กำลังเติบโตอย่างก้าวกระโดด ผมใ以ฐานะนักพัฒนาอิสระที่ทำเกม RPG 2D ตัวหนึ่ง พบว่าปัญหาใหญ่ที่สุดคือการทำให้ NPC ดูมีชีวิตชีวา ระบบ dialog tree แบบเดิมใช้ไม่ได้อีกต่อไปเมื่อผู้เล่นต้องการประสบการณ์ที่สมจริง
บทความนี้จะสอนวิธีสร้างระบบสนทนา AI สำหรับ NPC โดยใช้
สมัครที่นี่ เพื่อเข้าถึง LLM API ราคาประหยัด — อัตรา ¥1=$1 ซึ่งช่วยลดต้นทุนได้ถึง 85%+ เมื่อเทียบกับผู้ให้บริการอื่น
สถาปัตยกรรมระบบ NPC ที่ขับเคลื่อนด้วย LLM
┌─────────────────────────────────────────────────────────┐
│ Game Client │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Player │───▶│ Dialog │───▶│ LLM │ │
│ │ Input │ │ Manager │ │ Engine │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Context │ │ Caching │ │
│ │ Memory │ │ Layer │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
ระบบประกอบด้วย 4 ส่วนหลัก:
- Dialog Manager: จัดการสถานะและ flow ของบทสนทนา
- LLM Engine: เชื่อมต่อกับ API เพื่อสร้างคำตอบ
- Context Memory: เก็บประวัติการสนทนาต่อ NPC แต่ละตัว
- Caching Layer: ลดจำนวน API call และประหยัดค่าใช้จ่าย
การตั้งค่า HolySheep API สำหรับ Game Development
สำหรับโปรเจกต์ indie ที่มีงบประมาณจำกัด การเลือกใช้ HolySheep AI เป็นทางเลือกที่ชาญฉลาด เพราะราคาของ DeepSeek V3.2 อยู่ที่ $0.42/MTok เท่านั้น ต่ำกว่า GPT-4.1 ที่ $8 ถึง 19 เท่า และยังรองรับ WeChat/Alipay สำหรับผู้ใช้ในประเทศจีน
import anthropic
import json
from typing import List, Dict, Optional
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class NPCMemory:
"""โครงสร้างข้อมูลสำหรับจดจำบทสนทนาของ NPC"""
npc_id: str
npc_name: str
npc_personality: str
npc_background: str
conversation_history: List[Dict] = field(default_factory=list)
player_relationship: float = 0.5 # ค่า 0.0 - 1.0
class GameLLMClient:
"""
คลาสหลักสำหรับเชื่อมต่อกับ HolySheep API
ใช้สำหรับเกม NPC ทุกประเภท
"""
def __init__(self, api_key: str):
# ⚠️ สำคัญ: base_url ต้องเป็น https://api.holysheep.ai/v1 เท่านั้น
self.client = anthropic.Anthropic(
base_url="https://api.holysheep.ai/v1",
api_key=api_key
)
self.model = "claude-sonnet-4.5" # หรือเลือก deepseek-v3-2 สำหรับประหยัด
def build_system_prompt(
self,
memory: NPCMemory,
game_context: Dict
) -> str:
"""สร้าง system prompt ที่มีข้อมูล NPC และบริบทเกม"""
relationship_status = self._get_relationship_status(
memory.player_relationship
)
system_prompt = f"""คุณคือ {memory.npc_name} ตัวละครในเกม RPG
บุคลิกภาพ: {memory.npc_personality}
ประวัติ: {memory.npc_background}
ความสัมพันธ์กับผู้เล่น: {relationship_status}
กฎการสนทนา:
1. ตอบสนองตามบุคลิกภาพและความสัมพันธ์ที่กำหนด
2. กล่าวถึงเหตุการณ์ในเกมที่เกี่ยวข้องได้
3. ไม่เปิดเผยข้อมูลที่ NPC ไม่ควรรู้
4. ใช้ภาษาที่เป็นธรรมชาติ เหมาะกับตัวละคร
ข้อมูลเกมปัจจุบัน:
- เวลาในเกม: {game_context.get('game_time', 'ไม่ระบุ')}
- สถานที่ปัจจุบัน: {game_context.get('location', 'ไม่ระบุ')}
- เหตุการณ์ล่าสุด: {game_context.get('recent_event', 'ปกติ')}
"""
return system_prompt
def _get_relationship_status(self, value: float) -> str:
if value >= 0.8:
return "เป็นมิตรมาก - รู้สึกผูกพันและไว้วางใจ"
elif value >= 0.6:
return "เป็นมิตร - ยินดีช่วยเหลือ"
elif value >= 0.4:
return "เป็นกลาง - พูดคุยได้ปกติ"
elif value >= 0.2:
return "ไม่ค่อยไว้วางใจ - ระมัดระวัง"
else:
return "เป็นปฏิปักษ์ - ต่อต้าน"
def generate_response(
self,
memory: NPCMemory,
player_message: str,
game_context: Dict,
max_tokens: int = 256
) -> tuple[str, Dict]:
"""
สร้างคำตอบจาก NPC
Returns:
(response_text, metadata)
"""
system_prompt = self.build_system_prompt(memory, game_context)
# รวมประวัติการสนทนา 5 รอบล่าสุด
recent_history = memory.conversation_history[-10:] if memory.conversation_history else []
messages = []
for msg in recent_history:
messages.append({
"role": msg["role"],
"content": msg["content"]
})
messages.append({"role": "user", "content": player_message})
response = self.client.messages.create(
model=self.model,
max_tokens=max_tokens,
system=system_prompt,
messages=messages
)
npc_response = response.content[0].text
# บันทึกประวัติการสนทนา
memory.conversation_history.append({
"role": "user",
"content": player_message,
"timestamp": datetime.now().isoformat()
})
memory.conversation_history.append({
"role": "assistant",
"content": npc_response,
"timestamp": datetime.now().isoformat()
})
metadata = {
"usage": response.usage,
"model": response.model,
"stop_reason": response.stop_reason
}
return npc_response, metadata
def update_relationship(
self,
memory: NPCMemory,
delta: float,
action: str
):
"""ปรับค่าความสัมพันธ์ตามการกระทำของผู้เล่น"""
old_relationship = memory.player_relationship
# ปรับค่าตามประเภทการกระทำ
if action == "help":
delta = abs(delta) * 1.2 # ช่วยเหลือมีผลมากกว่า
elif action == "attack":
delta = -0.2 # โจมตีลดความสัมพันธ์แน่นอน
elif action == "gift":
delta = abs(delta) * 1.5 # ของขวัญมีผลดีมาก
memory.player_relationship = max(0.0, min(1.0,
memory.player_relationship + delta
))
return {
"before": old_relationship,
"after": memory.player_relationship,
"change": delta
}
ระบบ Dialog Manager สำหรับเกม
ต่อไปคือระบบจัดการบทสนทนาที่รองรับหลาย NPC พร้อมกัน มีระบบ emotion detection และ response filtering
from enum import Enum
from typing import Dict, Optional, Callable
import threading
class DialogState(Enum):
IDLE = "idle"
TALKING = "talking"
WAITING = "waiting"
ENDED = "ended"
class DialogManager:
"""
จัดการบทสนทนาของ NPC ทั้งหมดในเกม
รองรับ multi-threaded access
"""
def __init__(self, llm_client: GameLLMClient):
self.llm_client = llm_client
self.npc_memories: Dict[str, NPCMemory] = {}
self.active_dialogs: Dict[str, DialogState] = {}
self.emotion_keywords = {
"happy": ["ดีใจ", "ยินดี", "มีความสุข", "เพลิดเพลิน"],
"sad": ["เศร้า", "เสียใจ", "ผิดหวัง", "ร้องไห้"],
"angry": ["โกรธ", "เกลียด", "รำคาญ", "หัวร้อน"],
"fear": ["กลัว", "หวาดกลัว", "วิตกกังวล"]
}
self._lock = threading.Lock()
def register_npc(self, npc: NPCMemory):
"""ลงทะเบียน NPC ใหม่เข้าสู่ระบบ"""
with self._lock:
self.npc_memories[npc.npc_id] = npc
self.active_dialogs[npc.npc_id] = DialogState.IDLE
def start_dialog(self, npc_id: str, game_context: Dict) -> str:
"""เริ่มบทสนทนากับ NPC"""
if npc_id not in self.npc_memories:
raise ValueError(f"NPC {npc_id} ไม่พบในระบบ")
memory = self.npc_memories[npc_id]
self.active_dialogs[npc_id] = DialogState.TALKING
# สร้าง greeting อัตโนมัติ
greeting_prompt = f"ทักทายผู้เล่นในฐานะ {memory.npc_name} ตามบุคลิกที่กำหนด"
greeting, _ = self.llm_client.generate_response(
memory=memory,
player_message=greeting_prompt,
game_context=game_context
)
return greeting
def send_message(
self,
npc_id: str,
player_message: str,
game_context: Dict,
emotion_filter: bool = True
) -> tuple[str, Optional[str]]:
"""
ส่งข้อความไปยัง NPC และรับคำตอบ
Returns:
(response, detected_emotion)
"""
if npc_id not in self.npc_memories:
raise ValueError(f"NPC {npc_id} ไม่พบในระบบ")
memory = self.npc_memories[npc_id]
self.active_dialogs[npc_id] = DialogState.WAITING
response, metadata = self.llm_client.generate_response(
memory=memory,
player_message=player_message,
game_context=game_context
)
# ตรวจจับอารมณ์จากคำตอบ
detected_emotion = None
if emotion_filter:
detected_emotion = self._detect_emotion(response)
# ปรับความสัมพันธ์ตามอารมณ์ที่ตรวจพบ
emotion_delta = {
"happy": 0.05,
"sad": -0.03,
"angry": -0.08,
"fear": -0.05
}
if detected_emotion in emotion_delta:
self.llm_client.update_relationship(
memory,
emotion_delta[detected_emotion],
f"emotion_{detected_emotion}"
)
self.active_dialogs[npc_id] = DialogState.TALKING
return response, detected_emotion
def _detect_emotion(self, text: str) -> Optional[str]:
"""ตรวจจับอารมณ์จากข้อความ"""
text_lower = text.lower()
for emotion, keywords in self.emotion_keywords.items():
for keyword in keywords:
if keyword in text_lower:
return emotion
return None
def end_dialog(self, npc_id: str):
"""จบบทสนทนากับ NPC"""
if npc_id in self.active_dialogs:
self.active_dialogs[npc_id] = DialogState.ENDED
def get_relationship(self, npc_id: str) -> float:
"""ดึงค่าความสัมพันธ์ของ NPC"""
if npc_id in self.npc_memories:
return self.npc_memories[npc_id].player_relationship
return 0.0
def save_game(self) -> Dict:
"""บันทึกข้อมูลบทสนทนาทั้งหมด (สำหรับ save game)"""
with self._lock:
return {
"npc_memories": [
{
"npc_id": mem.npc_id,
"npc_name": mem.npc_name,
"conversation_history": mem.conversation_history,
"player_relationship": mem.player_relationship
}
for mem in self.npc_memories.values()
]
}
def load_game(self, save_data: Dict):
"""โหลดข้อมูลบทสนทนา (สำหรับ load game)"""
with self._lock:
for data in save_data["npc_memories"]:
npc_id = data["npc_id"]
if npc_id in self.npc_memories:
self.npc_memories[npc_id].conversation_history = data["conversation_history"]
self.npc_memories[npc_id].player_relationship = data["player_relationship"]
ตัวอย่างการใช้งาน
def main():
# เริ่มต้น LLM Client
client = GameLLMClient(api_key="YOUR_HOLYSHEEP_API_KEY")
# สร้าง Dialog Manager
dialog_manager = DialogManager(client)
# ลงทะเบียน NPC
village_elder = NPCMemory(
npc_id="elder_001",
npc_name="ปราชญ์หมู่บ้าน",
npc_personality="ใจเย็น ฉลาด เป็นห่วงชาวบ้าน ชอบเล่าเรื่องเก่า",
npc_background="อดีตนักเวทย์ที่เกษียณตัวมาอยู่หมู่บ้าน 50 ปี",
player_relationship=0.6
)
dialog_manager.register_npc(village_elder)
# ข้อมูลบริบทเกม
game_context = {
"game_time": "พระอาทิตย์ตกดิน",
"location": "ลานกลางหมู่บ้าน",
"recent_event": "มีคนแปลกหน้าเข้ามาในหมู่บ้าน"
}
# เริ่มบทสนทนา
greeting = dialog_manager.start_dialog("elder_001", game_context)
print(f"ปราชญ์หมู่บ้าน: {greeting}")
# ผู้เล่นถามเรื่องต่างๆ
response1, emotion1 = dialog_manager.send_message(
"elder_001",
"ท่านปราชญ์ครับ มีเรื่องอยากถามเกี่ยวกับปราสาทเก่าแก่ทางเหนือ",
game_context
)
print(f"ปราชญ์หมู่บ้าน: {response1}")
print(f"อารมณ์: {emotion1}")
# ตรวจสอบความสัมพันธ์
relationship = dialog_manager.get_relationship("elder_001")
print(f"ความสัมพันธ์: {relationship:.2f}")
# จบบทสนทนา
dialog_manager.end_dialog("elder_001")
if __name__ == "__main__":
main()
ระบบ RAG สำหรับ World Knowledge
สำหรับเกมที่มีโลกกว้างขวาง NPC จำเป็นต้องมีความรู้เกี่ยวกับสถานที่ ตัวละคร และประวัติศาสตร์ ระบบ RAG (Retrieval-Augmented Generation) จะช่วยให้ NPC ตอบคำถามได้ถูกต้อง
from typing import List, Dict, Tuple
import hashlib
class WorldKnowledgeRAG:
"""
ระบบ RAG สำหรับเกม - ดึงข้อมูลจากฐานความรู้เกม
ใช้งานร่วมกับ LLM เพื่อให้คำตอบที่ถูกต้อง
"""
def __init__(self):
# ฐานข้อมูลความรู้แบบง่าย (ในโปรเจกต์จริงใช้ vector DB)
self.knowledge_base: Dict[str, List[str]] = {
"locations": [],
"characters": [],
"history": [],
"lore": []
}
self.entity_cache: Dict[str, Dict] = {}
def add_knowledge(
self,
category: str,
content: str,
entity_name: Optional[str] = None
):
"""เพิ่มความรู้ใหม่เข้าฐานข้อมูล"""
if category not in self.knowledge_base:
self.knowledge_base[category] = []
self.knowledge_base[category].append(content)
if entity_name:
entity_id = self._generate_entity_id(entity_name)
if entity_id not in self.entity_cache:
self.entity_cache[entity_id] = {
"name": entity_name,
"facts": []
}
self.entity_cache[entity_id]["facts"].append(content)
def retrieve(
self,
query: str,
category: Optional[str] = None,
top_k: int = 3
) -> List[str]:
"""
ค้นหาความรู้ที่เกี่ยวข้องกับ query
ในโปรเจกต์จริงควรใช้ embedding model และ vector similarity
"""
relevant_docs = []
categories_to_search = (
[category] if category else self.knowledge_base.keys()
)
for cat in categories_to_search:
if cat not in self.knowledge_base:
continue
for doc in self.knowledge_base[cat]:
# คำนวณ relevance score แบบง่าย
score = self._calculate_relevance(query, doc)
if score > 0.3: # threshold
relevant_docs.append((score, doc))
# เรียงตามคะแนนและดึง top_k
relevant_docs.sort(key=lambda x: x[0], reverse=True)
return [doc for _, doc in relevant_docs[:top_k]]
def _calculate_relevance(self, query: str, document: str) -> float:
"""คำนวณคะแนนความเกี่ยวข้อง (แบบง่าย)"""
query_words = set(query.lower().split())
doc_words = set(document.lower().split())
intersection = query_words & doc_words
if not intersection:
return 0.0
# Jaccard similarity
return len(intersection) / len(query_words | doc_words)
def _generate_entity_id(self, name: str) -> str:
"""สร้าง ID สำหรับ entity"""
return hashlib.md5(name.encode()).hexdigest()[:8]
def get_entity_info(self, entity_name: str) -> Optional[Dict]:
"""ดึงข้อมูล entity โดยตรง"""
entity_id = self._generate_entity_id(entity_name)
return self.entity_cache.get(entity_id)
def build_context_for_npc(
self,
npc_knowledge: List[str],
relevant_knowledge: List[str],
recent_events: List[str]
) -> str:
"""สร้าง context ที่ส่งให้ LLM พร้อมข้อมูลจาก RAG"""
context_parts = []
if npc_knowledge:
context_parts.append("ข้อมูลที่ NPC รู้:")
for fact in npc_knowledge:
context_parts.append(f"- {fact}")
if relevant_knowledge:
context_parts.append("\nข้อมูลที่เกี่ยวข้อง:")
for fact in relevant_knowledge:
context_parts.append(f"- {fact}")
if recent_events:
context_parts.append("\nเหตุการณ์ล่าสุด:")
for event in recent_events:
context_parts.append(f"- {event}")
return "\n".join(context_parts)
class EnhancedGameLLMClient(GameLLMClient):
"""เวอร์ชันที่รวม RAG เข้ามาด้วย"""
def __init__(
self,
api_key: str,
rag_system: Optional[WorldKnowledgeRAG] = None
):
super().__init__(api_key)
self.rag = rag_system or WorldKnowledgeRAG()
def generate_response_with_context(
self,
memory: NPCMemory,
player_message: str,
game_context: Dict
) -> Tuple[str, Dict]:
"""
สร้างคำตอบพร้อม context จาก RAG
"""
# ดึงความรู้ที่เกี่ยวข้อง
relevant_knowledge = self.rag.retrieve(player_message)
# ดึงข้อมูล entity ที่กล่าวถึง
entities = self._extract_entities(player_message)
entity_info = []
for entity in entities:
info = self.rag.get_entity_info(entity)
if info:
entity_info.extend(info["facts"])
# รวม context
rag_context = self.rag.build_context_for_npc(
npc_knowledge=[memory.npc_background],
relevant_knowledge=relevant_knowledge + entity_info,
recent_events=[game_context.get("recent_event", "")]
)
# ดัดแปลง system prompt
base_system = self.build_system_prompt(memory, game_context)
enhanced_system = f"{base_system}\n\nข้อมูลเพิ่มเติม:\n{rag_context}"
# เรียก API
recent_history = memory.conversation_history[-10:] if memory.conversation_history else []
messages = [
{"role": msg["role"], "content": msg["content"]}
for msg in recent_history
]
messages.append({"role": "user", "content": player_message})
response = self.client.messages.create(
model=self.model,
max_tokens=256,
system=enhanced_system,
messages=messages
)
npc_response = response.content[0].text
# บันทึกประวัติ
memory.conversation_history.append({
"role": "user",
"content": player_message
})
memory.conversation_history.append({
"role": "assistant",
"content": npc_response
})
return npc_response, {"usage": response.usage}
def _extract_entities(self, text: str) -> List[str]:
"""แยกชื่อ entity จากข้อความ (แบบง่าย)"""
# ควรใช้ NER model ในโปรเจกต์จริง
words = text.split()
# คืนค่าคำที่ขึ้นต้นด้วยตัวพิมพ์ใหญ่ (ชื่อเฉพาะ)
return [w for w in words if w and w[0].isupper()]
ตัวอย่างการใช้งาน RAG
def setup_rag_for_game():
"""ตั้งค่าฐานความรู้เกม"""
rag = WorldKnowledgeRAG()
# เพิ่มข้อมูลสถานที่
rag.add_knowledge(
"locations",
"ปราสาทแห่งหมอกเทพ - ตั้งอยู่บนยอดเขาสูงทางเหนือ สร้างเมื่อ 500 ปีก่อน โด่งดังเรื่องสมบัติลึกลับ"
)
rag.add_knowledge(
"locations",
"หมู่บ้านโบราณวังน้ำ - หมู่บ้านเล็กๆ ที่
แหล่งข้อมูลที่เกี่ยวข้อง
บทความที่เกี่ยวข้อง