บทนำ: ทำไม 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", "หมู่บ้านโบราณวังน้ำ - หมู่บ้านเล็กๆ ที่