Bối Cảnh Dự Án
Chào mọi người, tôi là một tech lead với 8 năm kinh nghiệm phát triển game tại studio indie của Việt Nam. Hôm nay tôi muốn chia sẻ hành trình chúng tôi triển khai hệ thống NPC thông minh trong game RPG sử dụng LLM Agent, từ quyết định chọn HolySheep AI cho đến quá trình implementation thực tế.
Dự án bắt đầu khi chúng tôi nhận ra rằng các NPC truyền thống với script cứng không còn đáp ứng được kỳ vọng của người chơi. Một câu chuyện game RPG hấp dẫn đòi hỏi NPC có khả năng giao tiếp tự nhiên, phản ứng theo ngữ cảnh, và tạo ra trải nghiệm độc đáo cho từng người chơi.
Vì Sao Chúng Tôi Cần LLM Agent Cho Game NPC
Trước khi đi vào chi tiết kỹ thuật, tôi muốn giải thích tại sao giải pháp LLM Agent lại quan trọng cho game NPC hiện đại:
- Hội thoại tự nhiên: Người chơi có thể giao tiếp với NPC bằng ngôn ngữ tự nhiên thay vì chọn từ menu cố định
- Phản ứng theo ngữ cảnh: NPC nhớ được lịch sử tương tác và phản ứng phù hợp với hành động của người chơi
- Quest dynamic: Nhiệm vụ được tạo ra dựa trên tình huống thực tế trong game
- Tiết kiệm công sức viết script: Thay vì viết hàng nghìn dòng dialogue, chúng tôi chỉ cần prompt tốt
Chọn HolySheep AI — Quyết Định Quan Trọng
Ban đầu, đội ngũ của tôi thử nghiệm với API chính thức của OpenAI. Kết quả rất ấn tượng về mặt chất lượng, nhưng chi phí vận hành là ác mộng. Với 50.000 người chơi active hàng ngày, mỗi người tương tác với NPC khoảng 20 lần/session, chi phí API đã vượt xa ngân sách của một studio indie.
Sau khi thử nghiệm nhiều giải pháp relay API, chúng tôi tìm thấy HolySheep AI — một nền tảng API relay với:
- Tỷ giá ¥1 = $1: Tiết kiệm 85%+ so với API chính thức
- Độ trễ trung bình dưới 50ms: Đáp ứng yêu cầu real-time của game
- Hỗ trợ WeChat/Alipay: Thanh toán dễ dàng cho thị trường châu Á
- Tín dụng miễn phí khi đăng ký: Không rủi ro khi thử nghiệm
- Bảng giá 2026 rõ ràng: GPT-4.1 $8/MTok, Claude Sonnet 4.5 $15/MTok, Gemini 2.5 Flash $2.50/MTok, DeepSeek V3.2 chỉ $0.42/MTok
Kiến Trúc Hệ Thống NPC Thông Minh
Chúng tôi xây dựng hệ thống gồm 4 module chính:
- Conversation Manager: Quản lý lịch sử hội thoại và context
- LLM Agent Core: Kết nối với HolySheep API
- NPC Behavior Controller: Điều khiển hành vi NPC dựa trên response
- Memory System: Lưu trữ và truy xuất ký ức NPC
Code Implementation — Bước 1: Cài Đặt Base Agent
Đầu tiên, chúng tôi tạo một Unity MonoBehaviour để wrap LLM Agent. Điều quan trọng: luôn sử dụng https://api.holysheep.ai/v1 làm base_url.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using System.Text;
using System.Json;
namespace GameLLM
{
[Serializable]
public class ChatMessage
{
public string role; // "system", "user", "assistant"
public string content;
}
[Serializable]
public class ChatRequest
{
public string model;
public List messages;
public float temperature = 0.7f;
public int max_tokens = 500;
}
[Serializable]
public class ChatResponse
{
public List choices;
}
[Serializable]
public class Choice
{
public Message message;
}
[Serializable]
public class Message
{
public string content;
}
public class LLMNpcAgent : MonoBehaviour
{
// ⚠️ QUAN TRỌNG: Không bao giờ dùng api.openai.com
private const string BASE_URL = "https://api.holysheep.ai/v1";
private const string API_KEY = "YOUR_HOLYSHEEP_API_KEY";
[Header("NPC Configuration")]
[SerializeField] private string npcName = "VillageElder";
[SerializeField] private string npcSystemPrompt = "Bạn là một cụ già thông thái trong làng. Bạn biết nhiều về lịch sử và có thể đưa ra lời khuyên cho người chơi.";
private List conversationHistory = new List();
private readonly int maxHistoryLength = 20;
void Start()
{
// Khởi tạo system prompt
conversationHistory.Add(new ChatMessage
{
role = "system",
content = npcSystemPrompt
});
}
public async Task SendMessageAsync(string playerMessage)
{
// Thêm tin nhắn của người chơi vào lịch sử
conversationHistory.Add(new ChatMessage
{
role = "user",
content = playerMessage
});
// Trim history nếu quá dài
TrimConversationHistory();
// Gọi API
string response = await CallHolySheepAPIAsync();
// Thêm response vào lịch sử
conversationHistory.Add(new ChatMessage
{
role = "assistant",
content = response
});
return response;
}
private async Task CallHolySheepAPIAsync()
{
// Sử dụng DeepSeek V3.2 để tiết kiệm chi phí - chỉ $0.42/MTok
var request = new ChatRequest
{
model = "deepseek-chat",
messages = conversationHistory,
temperature = 0.7f,
max_tokens = 300
};
string jsonBody = JsonUtility.ToJson(request);
byte[] bodyBytes = Encoding.UTF8.GetBytes(jsonBody);
using (UnityWebRequest request = new UnityWebRequest(BASE_URL + "/chat/completions", "POST"))
{
request.uploadHandler = new UploadHandlerRaw(bodyBytes);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Authorization", $"Bearer {API_KEY}");
request.timeout = 10; // Timeout 10 giây cho game
await request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"API Error: {request.error}");
return "Xin lỗi, có lỗi xảy ra. Vui lòng thử lại sau.";
}
ChatResponse response = JsonUtility.FromJson(request.downloadHandler.text);
return response.choices[0].message.content;
}
}
private void TrimConversationHistory()
{
// Giữ lại system prompt + maxHistoryLength messages gần nhất
while (conversationHistory.Count > maxHistoryLength + 1)
{
conversationHistory.RemoveAt(1); // Luôn giữ system prompt ở index 0
}
}
public void ResetConversation()
{
string systemPrompt = conversationHistory[0].content;
conversationHistory.Clear();
conversationHistory.Add(new ChatMessage
{
role = "system",
content = systemPrompt
});
}
}
}
Code Implementation — Bước 2: NPC Behavior Controller
Module này xử lý response từ LLM và chuyển đổi thành action cho NPC. Chúng tôi sử dụng JSON parsing để extract commands từ response.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace GameLLM
{
public enum NPCActionType
{
Dialogue,
GiveItem,
StartQuest,
CompleteQuest,
Emotion,
Teleport,
Combat
}
[Serializable]
public class NPCTask
{
public NPCActionType actionType;
public string target;
public string value;
public Dictionary parameters;
}
public class NpcBehaviorController : MonoBehaviour
{
[Header("References")]
[SerializeField] private LLMNpcAgent llmAgent;
[SerializeField] private Animator npcAnimator;
[SerializeField] private DialogueUI dialogueUI;
[Header("Settings")]
[SerializeField] private float typingSpeed = 0.05f;
[SerializeField] private bool enableDebug = true;
private Queue taskQueue = new Queue();
private bool isProcessingTasks = false;
public async Task<string> ProcessPlayerInput(string input)
{
if (enableDebug)
Debug.Log($"[NPC] Nhận input: {input}");
// Gửi đến LLM và nhận response
string response = await llmAgent.SendMessageAsync(input);
if (enableDebug)
Debug.Log($"[NPC] Response: {response}");
// Parse response để tìm tasks
List<NPCTask> tasks = ParseResponseForTasks(response);
foreach (var task in tasks)
{
taskQueue.Enqueue(task);
}
// Xử lý tasks
if (!isProcessingTasks)
{
ProcessTaskQueue();
}
// Trả về text dialogue cho UI
return ExtractDialogueText(response);
}
private List<NPCTask> ParseResponseForTasks(string response)
{
List<NPCTask> tasks = new List<NPCTask>();
// Format: [ACTION:type|target|value]...[/ACTION]
// Ví dụ: [ACTION:GiveItem|player|HealthPotion][/ACTION]
int startIndex = 0;
while ((startIndex = response.IndexOf("[ACTION:", startIndex)) != -1)
{
int endIndex = response.IndexOf("[/ACTION]", startIndex);
if (endIndex == -1) break;
string actionString = response.Substring(startIndex + 8, endIndex - startIndex - 8);
string[] parts = actionString.Split('|');
if (parts.Length >= 1)
{
NPCTask task = new NPCTask();
if (Enum.TryParse<NPCTask>(parts[0], out NPCTaskType type))
{
task.actionType = type;
}
if (parts.Length > 1) task.target = parts[1];
if (parts.Length > 2) task.value = parts[2];
tasks.Add(task);
}
startIndex = endIndex + 9;
}
return tasks;
}
private string ExtractDialogueText(string response)
{
// Loại bỏ các action tags để lấy text thuần
string text = System.Text.RegularExpressions.Regex.Replace(
response,
@"\[ACTION:.*?\[/ACTION\]",
""
);
return text.Trim();
}
private async void ProcessTaskQueue()
{
isProcessingTasks = true;
while (taskQueue.Count > 0)
{
NPCTask task = taskQueue.Dequeue();
await ExecuteTask(task);
}
isProcessingTasks = false;
}
private async Task ExecuteTask(NPCTask task)
{
switch (task.actionType)
{
case NPCTaskType.GiveItem:
await ExecuteGiveItem(task);
break;
case NPCTaskType.StartQuest:
await ExecuteStartQuest(task);
break;
case NPCTaskType.Emotion:
ExecuteEmotion(task);
break;
case NPCTaskType.CompleteQuest:
ExecuteCompleteQuest(task);
break;
}
// Delay giữa các tasks
await Task.Delay(500);
}
private async Task ExecuteGiveItem(NPCTask task)
{
if (enableDebug)
Debug.Log($"[NPC] Tặng item: {task.target} cho {task.value}");
// Gọi GameManager để xử lý
// InventoryManager.Instance.AddItem(task.target, 1);
await Task.CompletedTask;
}
private async Task ExecuteStartQuest(NPCTask task)
{
if (enableDebug)
Debug.Log($"[NPC] Bắt đầu quest: {task.target}");
// QuestManager.Instance.StartQuest(task.target);
await Task.CompletedTask;
}
private void ExecuteEmotion(NPCTask task)
{
if (npcAnimator != null && !string.IsNullOrEmpty(task.target))
{
npcAnimator.SetTrigger(task.target); // "Happy", "Sad", "Angry", etc.
}
}
private void ExecuteCompleteQuest(NPCTask task)
{
if (enableDebug)
Debug.Log($"[NPC] Hoàn thành quest: {task.target}");
// QuestManager.Instance.CompleteQuest(task.target);
}
}
}
Code Implementation — Bước 3: Memory System
Để NPC có "ký ức" dài hạn, chúng tôi xây dựng hệ thống memory đơn giản nhưng hiệu quả. Dữ liệu được lưu local và đưa vào context khi cần.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace GameLLM
{
[Serializable]
public class NpcMemoryEntry
{
public string key;
public string value;
public long timestamp;
public int importance; // 1-10
public NpcMemoryEntry(string key, string value, int importance = 5)
{
this.key = key;
this.value = value;
this.timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
this.importance = importance;
}
}
public class NpcMemorySystem : MonoBehaviour
{
[Header("Settings")]
[SerializeField] private string npcId;
[SerializeField] private int maxMemoryEntries = 100;
[SerializeField] private int importanceThreshold = 3;
private List<NpcMemoryEntry> memories = new List<NpcMemoryEntry>();
private string savePath;
void Awake()
{
savePath = Application.persistentDataPath + $"/npc_memory_{npcId}.json";
LoadMemories();
}
public void AddMemory(string key, string value, int importance = 5)
{
// Kiểm tra trùng lặp
var existing = memories.Find(m => m.key == key);
if (existing != null)
{
existing.value = value;
existing.timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
existing.importance = Math.Max(existing.importance, importance);
}
else
{
memories.Add(new NpcMemoryEntry(key, value, importance));
}
// Trim nếu quá nhiều
TrimMemories();
SaveMemories();
}
public string GetContextualMemory()
{
// Lấy memories quan trọng gần đây
var relevantMemories = memories
.FindAll(m => m.importance >= importanceThreshold)
.ConvertAll(m => $"[{m.key}]: {m.value}");
if (relevantMemories.Count == 0)
return "Không có ký ức đặc biệt.";
return string.Join("\n", relevantMemories);
}
public string GetRelationshipStatus(string playerId)
{
var relation = memories.Find(m => m.key == $"relation_{playerId}");
return relation?.value ?? "Người lạ";
}
public void UpdateRelationship(string playerId, string change)
{
AddMemory($"relation_{playerId}", change, importance: 8);
}
public void AddQuestMemory(string playerId, string questId, string status)
{
AddMemory($"quest_{questId}_for_{playerId}", status, importance: 7);
}
private void TrimMemories()
{
if (memories.Count > maxMemoryEntries)
{
// Sắp xếp theo importance và timestamp
memories.Sort((a, b) =>
{
int cmp = b.importance.CompareTo(a.importance);
if (cmp != 0) return cmp;
return b.timestamp.CompareTo(a.timestamp);
});
memories.RemoveRange(maxMemoryEntries, memories.Count - maxMemoryEntries);
}
}
public void SaveMemories()
{
string json = JsonUtility.ToJson(memories);
System.IO.File.WriteAllText(savePath, json);
}
private void LoadMemories()
{
if (System.IO.File.Exists(savePath))
{
string json = System.IO.File.ReadAllText(savePath);
memories = new List<NpcMemoryEntry>(
JsonUtility.FromJson<List<NpcMemoryEntry>>(json) ?? new List<NpcMemoryEntry>()
);
}
}
}
}
Code Implementation — Bước 4: Optimized NPC với Streaming
Để cải thiện trải nghiệm người chơi, chúng tôi implement streaming response. NPC sẽ "nói" từng từ thay vì đợi toàn bộ response. Điều này giảm perceived latency từ 2-3s xuống còn dưới 500ms.
using System;