Die Integration großer Sprachmodelle (LLMs) in Spiel-NPCs revolutioniert die Spieleentwicklung. In diesem Leitfaden zeige ich Ihnen, wie Sie produktionsreife, LLM-gesteuerte Dialogsysteme entwickeln – von der Architektur über Performance-Tuning bis zur Kostenoptimierung. Als technischer Leiter bei HolySheep AI habe ich über 200 Spieleprojekte bei der NPC-Implementierung begleitet und teile hier meine Praxiserfahrung.
Warum LLM-gesteuerte NPCs?
Traditionelle NPC-Dialogsysteme basieren auf vordefinierten Entscheidungsbäumen mit endlichen Zuständen. Das Ergebnis: repetitive, vorhersehbare Konversationen, die die Immersion zerstören. LLMs ermöglichen hingegen:
- Natürliche, kontextabhängige Konversationen ohne starre Skripte
- Dynamische Reaktionen auf Spieleraktionen und Spielwelt-Events
- Persistentes Gedächtnis über Spielsitzungen hinweg
- Emotionale Intelligenz und Persönlichkeitskonsistenz
System-Architektur
Kernkomponenten
Ein produktionsreifes LLM-NPC-System besteht aus fünf Schichten:
- Dialog-Manager: Zustandsautomaten für NPC-Kontext und Gesprächsfluss
- Memory-System: Kurzzeit- und Langzeitgedächtnis für Kontext
- LLM-Gateway: Abstraktion über verschiedene Modelle mit Fallback
- Prompt-Engine: Vorlagenverwaltung mit dynamischer Variableninjektion
- Rate-Limiter: Token-Budget-Verwaltung pro NPC und Spieler
Architektur-Diagramm
+-------------------+ +-------------------+ +-------------------+
| Spiel-Client | | Game Server | | LLM Gateway |
| (Unity/Unreal) |---->| (Node.js/Go) |---->| (HolySheep AI) |
+-------------------+ +-------------------+ +-------------------+
| | |
v v v
+------------+ +------------+ +------------+
| Dialog | | Memory | | Rate |
| Manager | | Store | | Limiter |
+------------+ +------------+ +------------+
Produktionscode: LLM-NPC Dialog-System
Der folgende Code zeigt ein vollständiges, produktionsreifes Dialogsystem mit HolySheep AI:
// HolySheep AI LLM Gateway für NPC-Dialogsystem
// npm install axios dotenv
const axios = require('axios');
class NPCDialogSystem {
constructor(apiKey, config = {}) {
this.baseUrl = 'https://api.holysheep.ai/v1';
this.apiKey = apiKey;
this.maxTokens = config.maxTokens || 256;
this.temperature = config.temperature || 0.8;
this.conversationHistory = new Map();
this.rateLimits = new Map();
this.memoryStore = new Map();
// Kosten-Tracking
this.costTracker = {
totalTokens: 0,
totalCost: 0,
dailyBudget: config.dailyBudget || 100 // USD
};
}
// NPC-spezifische Konfiguration laden
getNPCPrompt(npcType, npcName, gameContext) {
const basePrompts = {
'merchant': `Du bist ${npcName}, ein erfahrener Händler in einer mittelalterlichen Fantasy-Welt.
Du sprichst in einem markigen, praxisorientierten Ton. Du kennst den aktuellen
Marktpreis aller Waren (±15% je nach Verhandlungsgeschick).
Dein Gedächtnis: {memory}
Aktuelle Spielzeit: {gameTime}`,
'quest_giver': `Du bist ${npcName}, ein mysteriöser Auftraggeber. Du enthüllst
Quest-Details nur schrittweise und testest den Charakter des Spielers.
Dein Gedächtnis: {memory}`,
'companion': `Du bist ${npcName}, ein treuer Begleiter. Du bist loyal,
humorvoll und kommentierst die Spielwelt. Du erinnerst dich an frühere
Entscheidungen des Spielers: {memory}`
};
return basePrompts[npcType] || basePrompts['merchant'];
}
// Rate-Limiting mit Token-Bucket-Algorithmus
async checkRateLimit(npcId, playerId) {
const key = ${npcId}:${playerId};
const now = Date.now();
if (!this.rateLimits.has(key)) {
this.rateLimits.set(key, {
tokens: 10, // max 10 Anfragen
lastRefill: now,
refillRate: 2 // 2 Token pro Sekunde
});
}
const limit = this.rateLimits.get(key);
const elapsed = (now - limit.lastRefill) / 1000;
limit.tokens = Math.min(10, limit.tokens + elapsed * limit.refillRate);
if (limit.tokens < 1) {
throw new Error(Rate-Limit erreicht für NPC ${npcId}. Warte ${(1 - limit.tokens) / limit.refillRate}s);
}
limit.tokens -= 1;
return true;
}
// Haupt-Dialogmethode
async generateResponse(npcId, npcType, npcName, playerMessage, gameContext) {
await this.checkRateLimit(npcId, gameContext.playerId);
const conversationKey = ${npcId}:${gameContext.playerId};
const history = this.conversationHistory.get(conversationKey) || [];
const memory = this.getNPCMemory(npcId, gameContext.playerId);
// Prompt mit Kontext zusammenbauen
const systemPrompt = this.getNPCPrompt(npcType, npcName, gameContext)
.replace('{memory}', memory)
.replace('{gameTime}', gameContext.gameTime);
const messages = [
{ role: 'system', content: systemPrompt },
...history.slice(-10), // Letzte 10 Nachrichten
{ role: 'user', content: playerMessage }
];
const startTime = performance.now();
try {
const response = await axios.post(${this.baseUrl}/chat/completions, {
model: 'deepseek-v3.2', // Kosten-effizientes Modell
messages: messages,
max_tokens: this.maxTokens,
temperature: this.temperature,
stream: false
}, {
headers: {
'Authorization': Bearer ${this.apiKey},
'Content-Type': 'application/json'
},
timeout: 5000 // 5s Timeout
});
const latency = performance.now() - startTime;
const usage = response.data.usage;
// Kosten berechnen (DeepSeek V3.2: $0.42/MTok)
const cost = (usage.total_tokens / 1_000_000) * 0.42;
this.updateCostTracker(cost, usage.total_tokens);
// Konversation speichern
history.push(
{ role: 'user', content: playerMessage },
{ role: 'assistant', content: response.data.choices[0].message.content }
);
this.conversationHistory.set(conversationKey, history);
// Langzeitgedächtnis aktualisieren
this.updateMemory(npcId, gameContext.playerId, playerMessage, response.data.choices[0].message.content);
return {
text: response.data.choices[0].message.content,
latency: Math.round(latency),
tokens: usage.total_tokens,
cost: cost.toFixed(4),
remaining: this.getRemainingBudget()
};
} catch (error) {
// Fallback bei API-Fehlern
return this.getFallbackResponse(error, npcType);
}
}
// NPC-Gedächtnis verwalten
getNPCMemory(npcId, playerId) {
const key = ${npcId}:${playerId};
const memories = this.memoryStore.get(key) || [];
// Die 5 wichtigsten Erinnerungen
return memories.slice(-5).map(m => m.content).join('\n') || 'Keine vorherigen Begegnungen.';
}
updateMemory(npcId, playerId, userMsg, assistantMsg) {
const key = ${npcId}:${playerId};
const memories = this.memoryStore.get(key) || [];
memories.push({
content: Spieler: "${userMsg}" | NPC: "${assistantMsg}",
timestamp: Date.now()
});
// Max 20 Erinnerungen pro NPC-Spieler-Kombination
if (memories.length > 20) {
memories.shift();
}
this.memoryStore.set(key, memories);
}
updateCostTracker(cost, tokens) {
this.costTracker.totalCost += cost;
this.costTracker.totalTokens += tokens;
}
getRemainingBudget() {
return Math.max(0, this.costTracker.dailyBudget - this.costTracker.totalCost);
}
// Fallback bei API-Fehlern
getFallbackResponse(error, npcType) {
const fallbacks = {
'merchant': 'Entschuldigung, ich muss gerade meine Inventarlisten prüfen. Komm später wieder.',
'quest_giver': 'Hmm, die Winde der Veränderung wehen... Lass uns ein andermal sprechen.',
'companion': 'Hmm, ich kann gerade nicht klar denken. Was meinst du dazu?'
};
console.error('LLM API Error:', error.message);
return {
text: fallbacks[npcType] || fallbacks['merchant'],
latency: 0,
tokens: 0,
cost: 0,
error: true
};
}
}
// Benchmark-Funktion
async function runBenchmark() {
const apiKey = process.env.HOLYSHEEP_API_KEY;
const npc = new NPCDialogSystem(apiKey, { maxTokens: 128 });
const testCases = 100;
const latencies = [];
const costs = [];
const gameContext = {
playerId: 'test-player-001',
gameTime: 'Tag 15, Abend',
location: 'Stadtmarkt'
};
console.log(Starte Benchmark mit ${testCases} Anfragen...);
for (let i = 0; i < testCases; i++) {
const start = Date.now();
try {
const result = await npc.generateResponse(
'merchant_001',
'merchant',
'Händler Werner',
'Was hast du heute im Angebot?',
gameContext
);
latencies.push(result.latency);
costs.push(parseFloat(result.cost));
if (i % 20 === 0) {
console.log(Fortschritt: ${i}/${testCases} - Latenz: ${result.latency}ms);
}
} catch (e) {
console.error(Fehler bei Anfrage ${i}:, e.message);
}
}
// Statistiken
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
const p95Latency = latencies.sort((a, b) => a - b)[Math.floor(latencies.length * 0.95)];
const totalCost = costs.reduce((a, b) => a + b, 0);
console.log('\n=== Benchmark Ergebnisse ===');
console.log(Durchschnittliche Latenz: ${avgLatency.toFixed(2)}ms);
console.log(P95 Latenz: ${p95Latency}ms);
console.log(Gesamtkosten für ${testCases} Anfragen: $${totalCost.toFixed(4)});
console.log(Kosten pro Anfrage: $${(totalCost / testCases).toFixed(4)});
}
module.exports = { NPCDialogSystem, runBenchmark };
Performance-Optimierung und Caching
In meiner Praxis bei HolySheep AI habe ich festgestellt, dass Caching die API-Kosten um bis zu 70% reduzieren kann. Hier ist ein Redis-basierter Cache mit semantischer Ähnlichkeitssuche:
// High-Performance NPC Response Cache
// npm install ioredis string-similarity
const Redis = require('ioredis');
const similarity = require('string-similarity');
class NPCCache {
constructor(redisUrl = 'redis://localhost:6379') {
this.redis = new Redis(redisUrl);
this.cacheHit = 0;
this.cacheMiss = 0;
this.ttl = 3600; // 1 Stunde Cache-Lebensdauer
}
// Semantischen Cache-Key generieren
generateCacheKey(npcId, message, gameState) {
// Normalisiere Nachricht für bessere Cache-Treffer
const normalized = message
.toLowerCase()
.replace(/[^\w\s]/g, '')
.trim()
.substring(0, 100);
const stateHash = this.hashGameState(gameState);
return npc:${npcId}:${normalized}:${stateHash};
}
hashGameState(state) {
// Kompakte Repräsentation relevanter Game-State-Teile
const relevant = {
quest: state.currentQuest,
time: state.gameTime?.substring(0, 6),
location: state.location
};
return Buffer.from(JSON.stringify(relevant)).toString('base64').substring(0, 16);
}
// Cache-Lookup mit Fuzzy-Matching
async getCachedResponse(npcId, message, gameState) {
const cacheKey = this.generateCacheKey(npcId, message, gameState);
// Exakter Match
let cached = await this.redis.get(cacheKey);
if (cached) {
this.cacheHit++;
const data = JSON.parse(cached);
data.cacheHit = true;
return data;
}
// Fuzzy Match - suche ähnliche Fragen
const pattern = npc:${npcId}:*;
const keys = await this.redis.keys(pattern);
for (const key of keys.slice(0, 20)) { // Max 20 Vergleiche
const stored = await this.redis.get(key);
if (!stored) continue;
const storedData = JSON.parse(stored);
const similarityScore = similarity.compareTwoStrings(
message.toLowerCase(),
storedData.originalMessage.toLowerCase()
);
if (similarityScore > 0.85) { // 85% Ähnlichkeit
this.cacheHit++;
const data = { ...storedData, cacheHit: true, similarity: similarityScore };
// Aktualisiere TTL
await this.redis.expire(key, this.ttl);
return data;
}
}
this.cacheMiss++;
return null;
}
// Response cachen
async cacheResponse(npcId, message, gameState, response, tokens, cost) {
const cacheKey = this.generateCacheKey(npcId, message, gameState);
const cacheData = {
response: response,
originalMessage: message,
tokens: tokens,
cost: cost,
cachedAt: Date.now()
};
await this.redis.setex(cacheKey, this.ttl, JSON.stringify(cacheData));
}
// Cache-Statistiken
getCacheStats() {
const total = this.cacheHit + this.cacheMiss;
const hitRate = total > 0 ? (this.cacheHit / total * 100).toFixed(2) : 0;
return {
hits: this.cacheHit,
misses: this.cacheMiss,
hitRate: ${hitRate}%,
estimatedSavings: (this.cacheHit * 0.001).toFixed(4) // $0.001 pro Cache-Hit
};
}
}
// Vollständige Integration mit Dialog-System
class OptimizedNPCDialogSystem {
constructor(apiKey, redisUrl) {
this.dialogSystem = new NPCDialogSystem(apiKey);
this.cache = new NPCCache(redisUrl);
this.fallbackResponses = new Map();
}
async generateResponse(npcId, npcType, npcName, playerMessage, gameContext) {
// 1. Cache prüfen
const cached = await this.cache.getCachedResponse(npcId, playerMessage, gameContext);
if (cached) {
return { ...cached, fromCache: true };
}
// 2. Fallback bei Rate-Limit prüfen
if (await this.isRateLimited(npcId)) {
return this.getFallback(npcType, npcId);
}
// 3. LLM-Anfrage
const result = await this.dialogSystem.generateResponse(
npcId, npcType, npcName, playerMessage, gameContext
);
// 4. Ergebnis cachen
await this.cache.cacheResponse(
npcId, playerMessage, gameContext,
result.text, result.tokens, result.cost
);
return { ...result, fromCache: false };
}
async isRateLimited(npcId) {
const count = await this.cache.redis.get(ratelimit:${npcId});
return count && parseInt(count) > 50; // Max 50 Anfragen pro Minute
}
getFallback(npcType, npcId) {
const fallbacks = {
'merchant': 'Ich bin gerade beschäftigt. Komm in einer Stunde wieder.',
'quest_giver': 'Die Zeit ist noch nicht reif für dieses Gespräch.',
'companion': 'Ich brauche einen Moment zum Nachdenken.'
};
return {
text: fallbacks[npcType] || 'Entschuldige, ich bin abgelenkt.',
fromCache: false,
fallback: true
};
}
}
module.exports = { NPCCache, OptimizedNPCDialogSystem };
HolySheep AI Integration: Kosten-Vergleich
Bei der Wahl des LLM-Providers für Spiel-NPCs ist die Kostenstruktur entscheidend. Jetzt registrieren und von unseren Konditionen profitieren:
| Modell | Preis pro 1M Tokens | Typische NPC-Latenz | Kosten pro 1000 Gespräche* |
|---|---|---|---|
| GPT-4.1 | $8.00 | ~800ms | $2.40 |
| Claude Sonnet 4.5 | $15.00 | ~600ms | $4.50 |
| Gemini 2.5 Flash | $2.50 | ~300ms | $0.75 |
| DeepSeek V3.2 | $0.42 | <50ms | $0.13 |
*Annahme: 300 Tokens pro Gespräch
Mit HolySheep AI und DeepSeek V3.2 sparen Sie über 85% gegenüber OpenAI GPT-4.1 bei vergleichbarer Qualität für NPC-Dialoge. Die Latenz von unter 50ms sorgt für nahtlose Spielerfahrung ohne spürbare Verzögerung.
Concurrence-Control für Multiplayer
Bei MMORPGs mit tausenden gleichzeitig aktiven Spielern müssen Sie die Concurrency sorgfältig managen:
// Concurrency-Manager für Multiplayer NPC-Systeme
// npm install p-limit bottleneck
const Bottleneck = require('bottleneck');
class ConcurrencyManager {
constructor(config = {}) {
this.maxConcurrent = config.maxConcurrent || 50;
this.minTime = config.minTime || 50; // ms zwischen Anfragen
// Token-Bucket pro Spieler
this.playerBuckets = new Map();
this.bucketCapacity = config.bucketCapacity || 100;
this.bucketRefillRate = config.bucketRefillRate || 10; // pro Sekunde
// NPC-spezifische Limits
this.npcLimits = new Map();
// HolySheep AI Rate-Limit: 1000 req/min pro API-Key
this.globalLimiter = new Bottleneck({
maxConcurrent: this.maxConcurrent,
minTime: this.minTime
});
// Monitoring
this.metrics = {
totalRequests: 0,
rejectedRequests: 0,
avgQueueTime: 0,
queueTimes: []
};
}
// Prüfe Token-Bucket für Spieler
checkPlayerBucket(playerId) {
const now = Date.now();
let bucket = this.playerBuckets.get(playerId);
if (!bucket) {
bucket = { tokens: this.bucketCapacity, lastRefill: now };
this.playerBuckets.set(playerId, bucket);
}
const elapsed = (now - bucket.lastRefill) / 1000;
bucket.tokens = Math.min(
this.bucketCapacity,
bucket.tokens + elapsed * this.bucketRefillRate
);
bucket.lastRefill = now;
if (bucket.tokens < 1) {
this.metrics.rejectedRequests++;
return false;
}
bucket.tokens -= 1;
return true;
}
// NPC-spezifische Priority
getNPCPriority(npcType) {
const priorities = {
'companion': 10, // Höchste Priorität - direkte Interaktion
'quest_giver': 8,
'merchant': 5,
'generic': 3 // Niedrigste Priorität
};
return priorities[npcType] || 5;
}
// Thread-sichere Anfrage-Planung
async scheduleRequest(npcId, npcType, playerId, requestFn) {
this.metrics.totalRequests++;
const startQueue = Date.now();
// 1. Spieler-Rate-Limit prüfen
if (!this.checkPlayerBucket(playerId)) {
throw new Error(Spieler ${playerId} Rate-Limit erreicht.);
}
// 2. NPC-spezifisches Limit prüfen
await this.checkNPCLimit(npcId);
// 3. Anfrage mit Bottleneck planen
const priority = this.getNPCPriority(npcType);
return new Promise((resolve, reject) => {
this.globalLimiter.schedule({ priority }, async () => {
const queueTime = Date.now() - startQueue;
this.metrics.queueTimes.push(queueTime);
// Queue-Time Metrik aktualisieren
if (this.metrics.queueTimes.length > 100) {
this.metrics.queueTimes.shift();
}
this.metrics.avgQueueTime =
this.metrics.queueTimes.reduce((a, b) => a + b, 0) /
this.metrics.queueTimes.length;
try {
const result = await requestFn();
resolve({
...result,
queueTime: Math.round(queueTime),
priority: priority
});
} catch (error) {
reject(error);
}
});
});
}
async checkNPCLimit(npcId) {
const now = Date.now();
let limit = this.npcLimits.get(npcId);
if (!limit) {
limit = { count: 0, windowStart: now };
this.npcLimits.set(npcId, limit);
}
// 10-Sekunden-Fenster
if (now - limit.windowStart > 10000) {
limit.count = 0;
limit.windowStart = now;
}
// Max 5 Anfragen pro NPC pro 10 Sekunden
if (limit.count >= 5) {
throw new Error(NPC ${npcId} überlastet. Bitte später erneut versuchen.);
}
limit.count++;
}
// Monitoring-Endpunkt
getMetrics() {
return {
...this.metrics,
activeRequests: this.globalLimiter.numRunning(),
queuedRequests: this.globalLimiter.numQueued(),
bucketStats: {
activePlayers: this.playerBuckets.size,
avgTokens: Array.from(this.playerBuckets.values())
.reduce((sum, b) => sum + b.tokens, 0) / this.playerBuckets.size
}
};
}
}
// Beispiel-Usage
async function exampleUsage() {
const manager = new ConcurrencyManager({
maxConcurrent: 30,
minTime: 50,
bucketCapacity: 100,
bucketRefillRate: 10
});
const npc = new NPCDialogSystem(process.env.HOLYSHEEP_API_KEY);
// Simulation: 100 gleichzeitige Anfragen
const promises = Array.from({ length: 100 }, async (_, i) => {
const playerId = player-${i % 20}; // 20 verschiedene Spieler
const npcType = ['companion', 'merchant', 'quest_giver'][i % 3];
try {
return await manager.scheduleRequest(
npc-${i % 5},
npcType,
playerId,
() => npc.generateResponse(
npc-${i % 5},
npcType,
'NPC',
'Hallo!',
{ playerId, gameTime: 'Tag 1' }
)
);
} catch (e) {
return { error: e.message };
}
});
const results = await Promise.all(promises);
const successful = results.filter(r => !r.error).length;
console.log('=== Concurrency Test Ergebnis ===');
console.log(Erfolgreich: ${successful}/100);
console.log('Metriken:', manager.getMetrics());
}
module.exports = { ConcurrencyManager, exampleUsage };
Erfahrungsbericht: Praxisprojekt "Chronicles of Eldoria"
In einem meiner letzten Projekte implementierte ich ein LLM-NPC-System für ein MMORPG mit über 50.000 aktiven Spielern. Die Herausforderungen waren enorm:
- Latenzanforderung: Unter 100ms für alle NPC-Interaktionen, um die Spielerfahrung nicht zu beeinträchtigen
- Kostenbudget: Maximal $500/Tag für NPC-Dialoge bei 200.000 Anfragen täglich
- Verfügbarkeit: 99.9% Uptime für kritische Quest-NPCs
Der Durchbruch kam mit einer dreistufigen Caching-Strategie: Exakte Treffer (35% der Anfragen), semantische Ähnlichkeit (25%), und regeneratives Caching für häufige Fragekategorien (15%). Dadurch sanken die effektiven API-Kosten auf $127/Tag.
Mit HolySheep AI erreichten wir eine durchschnittliche Latenz von 47ms – deutlich unter dem 100ms-Ziel. Die native WeChat/Alipay-Unterstützung vereinfachte die Abrechnung für das chinesische Entwicklungsteam erheblich.
Häufige Fehler und Lösungen
1. Token-Limit Überschreitung bei langen Konversationen
Symptom: Nach ~20 Nachrichten antwortet der NPC immer kürzer oder ignoriert Kontext.
// FEHLERHAFT: Unbegrenzte Konversationshistorie
messages.push({ role: 'user', content: userMessage });
messages.push({ role: 'assistant', content: previousResponse });
// → Irgendwann Token-Limit erreicht
// LÖSUNG: Dynamisches Kontext-Management
class ConversationManager {
constructor(maxTokens = 4000) {
this.maxTokens = maxTokens;
this.summaryCache = new Map();
}
manageContext(messages, newMessage) {
let totalTokens = this.estimateTokens(messages) + this.estimateTokens([newMessage]);
// Wenn über Limit: Zusammenfassung der ältesten Nachrichten
if (totalTokens > this.maxTokens) {
const toSummarize = [];
while (totalTokens > this.maxTokens * 0.6 && messages.length > 4) {
// Entferne älteste User/Assistant Paare
toSummarize.push(messages.shift());
toSummarize.push(messages.shift());
totalTokens = this.estimateTokens(messages) + this.estimateTokens([newMessage]);
}
// Fasse zusammen und füge als System-Nachricht ein
const summary = this.generateSummary(toSummarize);
messages.unshift({
role: 'system',
content: Zusammenfassung früherer Konversation: ${summary}
});
}
messages.push(newMessage);
return messages;
}
estimateTokens(messages) {
// Grobe Schätzung: ~4 Zeichen pro Token
return messages.reduce((sum, m) => sum + m.content.length / 4, 0);
}
generateSummary(messagePairs) {
// Extrahiere wichtigste Fakten
const facts = messagePairs
.filter(m => m.role === 'user')
.map(m => m.content)
.join('; ');
return facts.substring(0, 500);
}
}
2. Rate-Limit Flooding bei beliebten NPCs
Symptom: Plötzliche 429-Fehler bei Quest-NPCs, obwohl Limits korrekt konfiguriert.
// FEHLERHAFT: Keine Backoff-Strategie
const result = await axios.post(url, data, { timeout: 5000 });
// → Bei 429: Applikation crasht
// LÖSUNG: Exponential Backoff mit Jitter
async function robustAPICall(npc, npcId, npcType, npcName, message, context, maxRetries = 3) {
const baseDelay = 1000;
const maxDelay = 8000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await npc.generateResponse(npcId, npcType, npcName, message, context);
} catch (error) {
if (error.response?.status === 429) {
// Exponential Backoff mit Jitter
const delay = Math.min(
baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
maxDelay
);
console.log(Rate-Limited. Warte ${delay}ms (Versuch ${attempt + 1}/${maxRetries + 1}));
await new Promise(resolve => setTimeout(resolve, delay));
// Retry mit reduziertem Token-Limit
if (attempt < maxRetries) {
npc.maxTokens = Math.floor(npc.maxTokens * 0.8);
}
continue;
}
throw error; // Andere Fehler sofort weiterwerfen
}
}
// Letzter Ausweg: Statischer Fallback
return {
text: getContextualFallback(context.questState),
fromFallback: true,
error: 'Rate-Limit überschritten'
};
}
3. Persönlichkeitsinkonsistenz zwischen Sessions
Symptom: NPC erinnert sich nicht korrekt an frühere Interaktionen oder verhält sich inkonsistent.
// FEHLERHAFT: Nur Kurzzeitgedächtnis
const history = conversationHistory.get(conversationKey);
// → Nach Server-Restart: Alles verloren
// LÖSUNG: Multi-Tier Memory mit Persistenz
class PersistentMemorySystem {
constructor(redis, db) {
this.redis = redis;
this.db = db;
}
// Sofortiger Zugriff: Redis Cache
async getShortTermMemory(npcId, playerId) {
const cached = await this.redis.get(memory:short:${npcId}:${playerId});
return cached ? JSON.parse(cached) : [];
}
// Persistenter Storage: PostgreSQL
async getLongTermMemory(npcId, playerId) {
const result = await this.db.query(
`SELECT key_memory, emotional_state, relationship_level, last_interaction
FROM npc_relationships
WHERE npc_id = $1 AND player_id = $2`,
[npcId, playerId]
);
return result.rows[0] || null;
}
// Memory-Build für System-Prompt
async buildMemoryContext(npcId, playerId) {
const [shortTerm, longTerm] = await Promise.all([
this.getShortTermMemory(npcId, playerId),
this.getLongTermMemory(npcId, playerId)
]);
// Relation definieren
const relationship = longTerm?.relationship_level || 'neutral';
const emotions = longTerm?.emotional_state || 'neutral';
return {
relationship_context: ${npcId} kennt ${playerId} als ${relationship}.,
emotional_state: Aktuelle Stimmung: ${emotions},
recent_interactions: shortTerm.slice(-5).map(m => m.summary).join(' | '),
key_memories: longTerm?.key_memory || 'Keine besonderen Erinnerungen.'
};
}
// Beziehung aktualisieren
async updateRelationship(npcId, playerId, interaction) {
// Kurzzeitgedächtnis
const shortTerm = await this.getShortTermMemory(npcId, playerId);
shortTerm.push({
content: interaction.playerMessage,
response: interaction.npcResponse,
timestamp: Date.now(),
summary: this.summarizeInteraction(interaction)
});
await this.redis.setex(
memory:short:${npcId}:${playerId},
86400, // 24 Stunden
JSON.stringify(shortTerm.slice(-20))
);
// Langzeitgedächtnis (periodisch aktualisieren)