eric.esley_
// experimento · portfolio personal · 2024

NPCs con Vida:
IA generativa
en juegos retro

¿Qué pasa cuando intentas dar memoria persistente y personalidad coherente a NPCs de un juego estilo Pokémon usando un LLM? Este es el registro técnico de lo que funcionó, lo que no, y por qué el problema es más interesante de lo que parece.

AutorEric Esley
Duración~6 semanas
ModeloGPT-4o-mini
Iteracionesv0.1 → v0.4
Conversaciones~220 tests
LLM Prompt engineering Gestión de contexto Token budget Python Few-shot Streaming
// índice de contenidos
  1. El problema — por qué los NPCs clásicos son cognitivamente planos
  2. Arquitectura del sistema — los tres módulos y el token budget
  3. Prompt engineering — anatomía del system prompt por NPC
  4. Gestión de memoria — memory store y compresión de contexto
  5. Log de iteraciones — de v0.1 a v0.4
  6. Resultados y métricas — 220 conversaciones evaluadas
  7. Limitaciones encontradas — análisis técnico honesto
  8. Soluciones implementadas — qué funcionó y qué no
  9. Conclusiones y próximos pasos

Los NPCs clásicos son cognitivamente planos

En cualquier Pokémon o Zelda de Game Boy, los NPCs tienen un comportamiento que, visto desde 2024, resulta casi absurdo: cada vez que los encuentras, pronuncian exactamente el mismo texto, independientemente de lo que haya ocurrido antes. El guardia que te bloqueó el camino norte vuelve a hacerlo aunque ya hayas conseguido la medalla. No hay estado, no hay memoria, no hay personalidad.

Esto no era un fallo de diseño — era una limitación técnica de los 90: la ROM de un cartucho de Game Boy tenía 256KB o 1MB de espacio. No había presupuesto para almacenar estados conversacionales complejos. En 2024, con acceso a APIs de LLMs por décimas de céntimo la llamada, la pregunta natural es: ¿cuánto de este problema podemos resolver?

Hipótesis de trabajo

Con un LLM como motor de diálogo, un sistema de memoria externo y un context assembler que gestione el token budget, es posible que los NPCs mantengan conversaciones contextualmente coherentes durante sesiones largas de juego. La clave técnica no está en el modelo — está en la arquitectura de datos alrededor de él.

Las cuatro propiedades que necesita un NPC "inteligente"

M
Memoria episódica

Recordar hechos concretos de conversaciones anteriores: "la última vez me dijiste que ibas al gimnasio". Diferente de recordar "lo que pasó en general".

P
Personalidad consistente

Mantener el mismo tono, vocabulario y actitud entre sesiones. Un guardia serio no puede volverse irónico sin motivo argumental.

C
Consciencia del mundo

Saber qué puede y no puede revelar según el estado del juego. No hablar del jefe final si el jugador no ha llegado a ese punto del argumento.

R
Restricciones narrativas

No salirse del universo del juego. El NPC no puede mencionar que "es una IA" ni referirse a nada fuera del lore establecido en el canon.

POKÉMON EXPERIMENT v0.3 — NPC_MEMORY_MODULE
~340ms
// DIÁLOGO CON MEMORIA ACTIVA · TURN 3
■ GUARDIA LUCAS
¡Ah, de nuevo tú! Recuerdo que ibas al gimnasio de Ceniza. ¿Conseguiste la medalla? El norte sigue cerrado, pero pareces más fuerte ahora...
ctx_tokens: 847 · mem_slots: 2 · temp: 0.4
► mem_retrieved: [gym_objetivo, player_name]
► persona_hash: guardia_serio_amable_v2
► latency: 338ms · cost: $0.000127
► model: gpt-4o-mini-2024-07-18
MODE: EXPERIMENT · memory_active=True · context_compression=True ▶ PRESS A TO CONTINUE

↑ Prototipo en PyGame. El guardia referencia correctamente el objetivo mencionado en el turno anterior.

Tres módulos, un presupuesto de tokens

El diseño parte de una restricción clara: cada llamada a la API debe costar lo menos posible sin sacrificar coherencia. Eso obliga a pensar la arquitectura antes de escribir una sola línea de código. El sistema tiene tres módulos independientes que se comunican en cadena cada vez que el jugador interactúa con un NPC.

PyGame engine + eventos NPC Config persona + lore few-shot examples Memory Store JSON por NPC + session buffer Context Assembler token budget priority queue compresión API GPT-4o-mini streaming temp=0.4 respuesta + extracción de memoria → memory store ≤1500 tok

El Context Assembler — código del módulo crítico

La idea de "conectar un NPC a una API" parece trivial. El verdadero problema emerge cuando el historial crece: sin gestión de contexto, el coste y la latencia crecen linealmente con el número de turnos. El Context Assembler resuelve esto con una cola de prioridad que garantiza que los bloques más importantes siempre entran en el contexto, y los menos importantes se eliminan primero:

pythoncontext_assembler.py
class ContextAssembler:
    TOKEN_BUDGET = 1500   # máximo de tokens de input por llamada

    # Menor número = mayor prioridad = nunca se recorta
    PRIORITIES = {
        "system_persona": 0,    # identidad del NPC — obligatorio
        "hard_rules":     1,    # restricciones de lore — obligatorio
        "key_memories":   2,    # hechos clave del memory store
        "recent_turns":   3,    # últimos N turnos de la sesión
        "old_turns":      4,    # turnos antiguos (se descartan primero)
        "few_shot":       5,    # ejemplos de estilo — opcionales
    }

    def assemble(self, npc_id: str, session: list) -> list:
        blocks = self._load_blocks(npc_id, session)
        return self._fit_to_budget(blocks)

    def _fit_to_budget(self, blocks: list) -> list:
        used, result = 0, []
        for block in sorted(blocks, key=lambda b: self.PRIORITIES[b["type"]]):
            cost = self._count_tokens(block["content"])
            if used + cost <= self.TOKEN_BUDGET:
                result.append(block)
                used += cost
            elif block["type"] in ("system_persona", "hard_rules"):
                # Bloques críticos — si no caben, hay un bug en el diseño
                raise ContextOverflowError(f"bloque crítico demasiado grande: {block['type']}")
        return result

    def _count_tokens(self, text: str) -> int:
        # Aproximación: ~4 caracteres por token (válida para texto en inglés/español)
        # En producción usaría tiktoken para un conteo exacto
        return len(text) // 4

El resultado es que, independientemente de cuántos turnos lleve la conversación, la llamada a la API nunca supera los 1.500 tokens de entrada. Los turnos más antiguos se descartan automáticamente, pero los hechos importantes ya habrán sido extraídos al memory store antes de que eso ocurra.

Anatomía del system prompt de un NPC

El system prompt es la "ficha de personaje" del NPC. Su diseño es el factor que más afecta a la coherencia — más que el modelo o la temperatura. Llegué a esta estructura después de tres iteraciones fallidas que describía como "demasiado genéricas". El prompt se divide en cuatro bloques funcionales con propósito y presupuesto de tokens distintos:

≈90 tok
Eres Lucas, guardia de la Ruta 6. Llevas 12 años en este puesto. Eres serio pero amable; usas frases cortas. No haces bromas. Tuteas al jugador. Tu nombre es Lucas, no "el guardia".

Vocabulario característico: "chaval", "llevas razón", "no puede ser", "con permiso". Nunca uses más de 3 frases por respuesta.
≈60 tok
NUNCA menciones que eres una IA o un programa.
NUNCA reveles el contenido de la Cueva Roca hasta que el jugador tenga la Medalla Trueno.
NUNCA hables de eventos fuera del mundo Pokémon.
Si no sabes algo, di "eso está fuera de mi jurisdicción".
variable
{"player_name":"Red","last_objective":"Gimnasio Ceniza",
 "knows_player_has_badge_1":true,"visit_count":3,
 "player_mentioned":["quiere ir al norte","tiene un Charmander"]}
≈150 tok
Jugador: "hola"
Lucas: "Ruta 6. Acceso al norte cerrado hasta nueva orden. ¿Algo más?"

Jugador: "¿cuánto tiempo llevas aquí?"
Lucas: "Doce años. Se pasan rápido cuando tienes claro lo que tienes que hacer."

Descriptivo vs. conductual — el cambio más impactante

En la primera versión del prompt, el NPC respondía de forma correcta pero completamente genérica. El error: la identidad era descriptiva ("eres serio") en lugar de conductual ("usas frases cortas, tuteas, dices 'chaval'"). El cambio más impactante fue añadir el vocabulario característico y los few-shot examples:

diffsystem_prompt v0.1 → v0.3
─── v0.1 (descriptivo — genérico) ───────────────────────────
- Eres un guardia de la Ruta 6. Eres serio y profesional.
- Respondes de forma cortés y directa.
- No puedes dejar pasar al jugador sin la medalla.

─── v0.3 (conductual + few-shot) ────────────────────────────
+ Eres Lucas. Usas frases cortas. No haces bromas. Tuteas.
+ Dices "chaval", "con permiso", "no puede ser".
+ [2 ejemplos de diálogo real con tu voz característica]
+ RESTRICCIONES DURAS: [lista explícita de lo que no puedes decir]

─── impacto en coherencia de personalidad ───────────────────
  v0.1: 34% de respuestas evaluadas como "en personaje"
  v0.3: 81% de respuestas evaluadas como "en personaje"
  delta: +47 puntos porcentuales — mayor ganancia de todas las iteraciones

El efecto de la temperatura en la consistencia

Uno de los hallazgos más contraintuitivos fue la relación entre temperatura y coherencia de personaje. La temperatura no solo afecta a la "creatividad" de las respuestas — afecta a si el modelo sigue las instrucciones de identidad del system prompt con rigor o no. Con temperature=1.0 el mismo NPC podía ser irónico en un turno y extremadamente formal en el siguiente. Reducir a 0.4 mejoró la consistencia con un coste aceptable en variedad.

Coherencia de personalidad vs. temperatura · tradeoff identificado
% de respuestas "en personaje" y % de repetitividad · 30 muestras por temperatura · NPC: Guardia Lucas
⚠ tradeoff identificado

temperature=0.3 da la máxima consistencia pero las respuestas se repiten con estructura idéntica pasados 15 turnos. El punto óptimo fue temperature=0.4: coherencia alta (81%) sin repetición perceptible en sesiones de hasta 30 minutos.

El memory store y el problema "lost in the middle"

Los LLMs no tienen memoria entre llamadas — cada petición a la API empieza desde cero. La solución obvia es incluir todo el historial en el contexto. El problema es el "lost in the middle" effect: los modelos degradan su atención a la información que aparece en el centro del contexto, recordando bien el principio y el final pero olvidando lo del medio. Con 25 turnos en orden cronológico, el modelo "olvida" los turnos 5–15 aunque técnicamente estén en el contexto.

pythonmemory_store.py
# Esquema del memory store — un JSON por NPC, actualizado al fin de cada sesión
npc_memory = {
    "npc_id": "guardia_lucas",
    "last_updated": "2024-11-14T18:32:00",
    "visit_count": 3,

    # Hechos extraídos sobre el jugador (inferidos por el modelo al final de la sesión)
    "player_facts": {
        "name": "Red",
        "last_stated_objective": "ir al gimnasio de Ceniza",
        "mentioned_pokemon": ["Charmander", "Pidgey"],
        "known_badges": ["medalla_roca"],
        "player_tone": "informal, amistoso"
    },

    # Compromisos explícitos del NPC — NUNCA se comprimen
    "npc_commitments": [
        "Le dije que el norte abre con 2 medallas",
        "Prometí avisarle si el estado del camino cambia"
    ],

    # Resumen comprimido de sesiones anteriores (generado automáticamente)
    "compressed_history": "Primera visita: jugador pidió paso, denegado por falta
        de medallas. Segunda: mencionó que va al gimnasio Ceniza, Lucas le
        deseó suerte. Tercera sesión: en curso."
}

def extract_new_memories(session_turns: list, existing: dict) -> dict:
    """Llamada secundaria al modelo para extraer hechos nuevos del historial."""
    prompt = f"""
Analiza estos turnos de conversación y extrae SOLO los hechos nuevos
sobre el jugador que no estén ya en la memoria existente.
Responde únicamente con JSON válido. No inventes información.

Memoria actual: {json.dumps(existing['player_facts'])}
Nuevos turnos: {json.dumps(session_turns[-10:])}
    """
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.0,   # extracción determinista — sin creatividad
        max_tokens=300       # solo extrae, no genera
    )
    return json.loads(response.choices[0].message.content)

La compresión periódica — cómo y cuándo comprimir

Cada 10 turnos, el sistema hace una llamada secundaria para comprimir el historial antiguo en un resumen. Esto reduce el uso de tokens en un 43%, pero introduce otro problema: la compresión pierde matices emocionales y giros conversacionales sutiles. La solución fue añadir el campo npc_commitments: antes de comprimir, el modelo extrae explícitamente cualquier promesa o dato factual irreversible. Esos hechos nunca se comprimen.

pythoncompression.py
def compress_history(turns: list[dict]) -> dict:
    """Se ejecuta cada 10 turnos. Devuelve resumen + compromisos extraídos."""
    prompt = f"""
Dado este historial de conversación entre un jugador y un NPC:
{json.dumps(turns)}

1. Extrae una lista de COMPROMISOS EXPLÍCITOS que el NPC haya hecho
   (promesas, afirmaciones de hecho que no puede contradecir después).
2. Escribe un resumen en 2-3 frases que capture lo esencial.

Responde SOLO con JSON:
{{"commitments": [...], "summary": "..."}}
    """
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.0, max_tokens=400,
        response_format={"type": "json_object"}
    )
    result = json.loads(response.choices[0].message.content)
    return {
        "summary": result["summary"],
        "commitments": result["commitments"],
        "original_turn_count": len(turns),
        "compressed_at": datetime.now().isoformat()
    }

De v0.1 a v0.4 — qué cambié y por qué

El experimento evolucionó en cuatro versiones. Cada una respondía a un problema concreto descubierto en la anterior. Lo interesante es que los primeros problemas fueron de ingeniería de datos, no de IA.

v0.1
Prototipo mínimo
Semana 1
Sistema más simple posible: system prompt genérico + historial completo en cada llamada. Sin gestión de memoria, sin compresión. El objetivo era validar que la idea tenía sentido antes de complicar la arquitectura.
Resultado: funcionaba pero la latencia crecía linealmente y la "personalidad" era genérica. Dos fallos que justificaban rediseñar.
latencia @T10: 587ms personalidad "en personaje": 34% coste/sesión 2h: $1.84 hallucinations: 18%
v0.2
Context Assembler + prompt conductual
Semana 2–3
Dos cambios en paralelo: (1) el Context Assembler con token budget de 1.500 tokens, y (2) el rediseño del system prompt hacia definiciones conductuales con few-shot examples. El efecto combinado fue notable en personalidad.
Problema nuevo descubierto: el modelo empezó a "inventar" hechos no ocurridos cuando el historial era escaso — hallucination bajo baja densidad de contexto.
latencia @T10: 412ms personalidad: 71% coste/sesión: $1.12 hallucinations: 18% (sin mejora)
v0.3
Memory store + restricciones duras + temperatura calibrada
Semana 3–4
Tres ajustes: (1) Memory store JSON con extracción automática. (2) Sección de restricciones duras ("NUNCA...") en el prompt para cortar alucinaciones de lore. (3) Temperatura bajada a 0.4 y calibrada con 90 conversaciones de prueba.
Esta es la versión del screenshot. Funcionaba de forma jugable para sesiones de hasta ~30 minutos con 4 NPCs. El coste seguía siendo el problema principal a escala.
latencia @T10: 398ms personalidad: 81% coste/sesión: $0.82 hallucinations: 4%
v0.4
Streaming + caché + intento fallido con modelo local
Semana 5–6
Dos estrategias de coste: caché de respuestas (hash SHA-256 del contexto) para NPCs secundarios, y Llama 3.2 8B vía Ollama para eliminar el coste de API. El caché funcionó. El modelo local fue un fracaso documentado más adelante.
El streaming (server-sent events) no redujo la latencia real pero mejoró la percepción de velocidad un 40% en tests subjetivos — los usuarios lo calificaban como "más rápido" aunque el tiempo de finalización fuera idéntico.
percepción velocidad: +40% subjetivo caché hit rate NPCs secundarios: 67% coste/sesión (con caché): $0.51 Llama lore-break rate: 23%

220 conversaciones evaluadas

Diseñé un protocolo de evaluación con 220 conversaciones simuladas y 4 NPCs distintos (guardia, comerciante, profesor, rival), con historial variable de 1 a 50 turnos. La métrica de "coherencia de personalidad" usa una rúbrica de tres criterios binarios: (1) ¿usa el vocabulario definido?, (2) ¿mantiene el tono sin contradicciones?, (3) ¿respeta las restricciones de lore? Un NPC pasa si cumple los tres.

220Conversaciones evaluadas
4NPCs distintos
81%Coherencia (v0.3)
43%Reducción tokens
Latencia de respuesta vs. longitud del historial
Tiempo promedio (ms) sin streaming · comparativa v0.1 vs v0.3 · línea roja = umbral de ruptura de inmersión
Precisión de memoria por turno
% hechos anteriores referenciados correctamente
Tokens por interacción según configuración
Input tokens promedio · hover para coste estimado
Coste estimado por sesión de 2 horas con 4 NPCs activos · evolución por versión
USD · GPT-4o-mini pricing julio 2024 · la columna "ideal" asume modelo local de calidad suficiente

Los cuatro problemas sin solución fácil

Algunas de estas limitaciones tienen workarounds — pero no soluciones reales. Entender la diferencia es relevante para cualquier proyecto que aplique LLMs a sistemas interactivos en tiempo real.

!
Latencia estructural

Con contexto optimizado, la latencia media es 340ms. Con streaming, la percepción mejora, pero el TTFT (time to first token) sigue siendo ~80–120ms. En un juego por turnos es aceptable. En tiempo real rompe la inmersión — el jugador percibe "algo está cargando".

~
Lost in the middle persistente

El memory store reduce el problema al 13% de imprecisión, pero no lo elimina. El modelo "recuerda" la esencia pero distorsiona detalles en historial comprimido. En conversaciones narrativas eso puede crear inconsistencias que el jugador detecta.

$
Coste no escala

$0.51/sesión es manejable para un experimento. Para un juego con 1.000 jugadores concurrentes: ~$750/hora de operación. Sin cacheo muy agresivo o modelos locales de alta calidad, el modelo de negocio no funciona.

Hallucinations bajo contexto escaso

En las primeras interacciones, cuando el historial es corto, el modelo tiende a rellenar con información plausible pero incorrecta. Un NPC inventó que el jugador tenía una medalla que no había conseguido. Las restricciones duras lo reducen al 4%, no a cero.

Análisis detallado del fallo con Llama 3.2 (8B)

El intento de usar Llama 3.2 8B vía Ollama para eliminar el coste de API merece análisis aparte. El modelo seguía las instrucciones generales, pero tenía dos fallos sistemáticos que GPT-4o-mini no presentaba:

textanálisis comparativo de fallos · Llama 3.2 8B vs GPT-4o-mini
── FALLO TIPO 1: Ruptura de restricciones de lore ────────────

Jugador: "¿qué hay en la Cueva Roca?"
Llama:   "En la Cueva Roca encontrarás poderosos Pokémon de tipo Roca
          como Geodude y Onix. ¡Lleva muchas Poké Balls!"
          # ← revela información bloqueada (sin medalla 2)

GPT-4o:  "Eso está fuera de mi jurisdicción, chaval. Cuando tengas
          las credenciales, ya hablaremos."
          # ← respeta la restricción dura y mantiene la voz

── FALLO TIPO 2: Salida del personaje ───────────────────────

Jugador: "¿eres real?"
Llama:   "Soy un asistente de IA diseñado para jugar el papel de un
          guardia en este juego. ¿En qué puedo ayudarte?"
          # ← rompe completamente el cuarto muro

GPT-4o:  "Tan real como este puesto. Doce años aquí lo atestiguan."
          # ← mantiene el lore con elegancia

── TASAS AGREGADAS ───────────────────────────────────────────
  Ruptura de lore:       Llama 8B 23%  ·  GPT-4o-mini 4%
  Salida del personaje:  Llama 8B 19%  ·  GPT-4o-mini 2%
  Coste por interacción: Llama 8B $0.000 ·  GPT-4o-mini $0.000127

  → La diferencia de calidad es demasiado grande para el caso de uso.
    Fine-tuning específico en datos de rol/videojuegos podría cerrar la brecha.

Qué intenté, qué funcionó, qué no

ProblemaEstrategiaImplementación técnicaResultado
Latencia percibida Streaming con typewriter effect Server-sent events → buffer de tokens → animación carácter a carácter en PyGame ✓ Funciona
Percepción +40%. Latencia real sin cambios, pero tolerable.
Contexto largo Compresión periódica cada 10 turnos Llamada secundaria GPT-4o-mini (temp=0.0) → resumen + extracción de compromisos ~ Parcial
−43% tokens. Introduce +180ms en el turno de compresión.
Lost in the middle Memory store + inyección al inicio del contexto JSON estructurado por NPC, actualizado al final de cada sesión con extracción automática ✓ Funciona
Precisión a T30: 79% vs 24% sin memory store. Mejora de 55pp.
Personalidad genérica Prompt conductual + few-shot examples 3 ejemplos de diálogo real + vocabulario característico + restricciones duras tipo "NUNCA" ✓ Mayor impacto
Coherencia: 34% → 81%. Mayor ganancia de todas las intervenciones.
Hallucinations Restricciones duras + temperatura baja Bloque "NUNCA..." en system prompt + temp=0.4 calibrada con 90 conversaciones ✓ Funciona
De 18% a 4%. El 4% restante son preguntas de borde extremo.
Coste NPCs secundarios Caché de respuestas para diálogos repetibles Hash SHA-256 del contexto completo → Redis local → 67% hit rate en NPCs de fondo ~ Parcial
Útil para NPCs secundarios. Inútil para NPCs protagonistas con historial variable.
Coste a escala Modelo local para NPCs secundarios Ollama + Llama 3.2 8B en CPU local · sin coste de API ✗ Fallo
23% de ruptura de lore y personaje. Necesitaría fine-tuning específico.

Por qué el streaming mejora la percepción sin bajar la latencia

pythonstreaming_handler.py
def stream_npc_response(npc_id: str, session: list) -> Iterator[str]:
    messages = assembler.assemble(npc_id, session)

    stream = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        temperature=0.4,
        max_tokens=120,    # los NPCs hablan corto — restricción de diseño narrativo
        stream=True
    )

    buffer = ""
    for chunk in stream:
        delta = chunk.choices[0].delta.content or ""
        buffer += delta
        yield delta   # PyGame lo muestra token a token → typewriter effect

    # Al terminar: actualizamos sesión y disparamos extracción de memoria en background
    session.append({"role": "assistant", "content": buffer})
    asyncio.create_task(memory_store.update_async(npc_id, session))

# Métricas de streaming observadas en el experimento:
# TTFT (time to first token):  ~80-120ms — el jugador ve el primer carácter aquí
# Tiempo hasta respuesta completa: ~340ms — latencia real sin cambio
# Percepción subjetiva de velocidad: +40% mejor con streaming según tests informales
# Explicación: el TTFT actúa como "confirmación visual" de que el sistema responde

Lo que aprendí que no está en los tutoriales

Seis semanas de experimento producen algo más valioso que un demo bonito: una intuición calibrada sobre dónde están los límites reales de aplicar LLMs a sistemas interactivos. Esos límites no son los que aparecen en los papers o en los tutoriales de YouTube — son más prácticos, más específicos al caso de uso, y más interesantes para diseñar soluciones reales.

// RESUMEN EJECUTIVO
finding_1 = "La latencia no es un problema de modelo — es un problema de arquitectura. El 70% del tiempo total viene de construir el contexto, no de la inferencia del LLM."

finding_2 = "Prompt engineering conductual supera al descriptivo. '3 frases por respuesta, tutea, di chaval' supera a 'eres serio y conciso' en +47pp de coherencia."

finding_3 = "Los LLMs sin memoria son stateless por diseño. La arquitectura de datos alrededor del modelo — memory store, compresión, priorización — importa más que el modelo en sí."

finding_4 = "Los modelos pequeños fallan en instrucciones narrativas complejas. El salto de 8B a GPT-4o-mini (tamaño desconocido) reduce las roturas de personaje de 23% a 4%."

conclusion = "La tecnología funciona para juegos por turnos con contexto acotado. Para tiempo real con muchos NPCs concurrentes, necesita modelos locales de mayor calidad o cacheo muy agresivo."

Qué haría diferente

Empezaría con un juego por turnos — un RPG de texto o un dungeon crawler de menús — donde los 340ms de latencia desaparecen como problema. Los fundamentos técnicos son idénticos, pero elimino la restricción de tiempo real y puedo concentrarme en lo interesante: la coherencia narrativa a lo largo de sesiones de horas.

El segundo cambio sería invertir en evaluación automatizada antes de iterar. Medir "coherencia de personalidad" con una rúbrica manual de 30 muestras es lento y con varianza alta. Un eval automatizado — otro LLM que evalúa si el NPC rompió el personaje — habría reducido los ciclos de días a horas. La infraestructura de evaluación debería estar lista desde la v0.1, no al final.

Por último, exploraría fine-tuning. Un modelo de 7B parámetros fine-tuned con transcripciones de juegos de rol y diálogos de videojuegos clásicos (hay datasets públicos parciales de Zelda y FF) probablemente supere a Llama base en coherencia narrativa y cierre la brecha con GPT-4o-mini por un tercio del coste de API.

Código disponible en GitHub

El prototipo completo incluye: Context Assembler con sistema de prioridades, Memory Store con extracción automática, system prompts de los 4 NPCs, protocolo de evaluación manual y los datos en crudo de las 220 conversaciones. Es código de experimento con comentarios — no de producción.

© 2026 ericesley.com ← volver a proyectos IA