LLMs Principiante 13 min de lectura

NPCs con LLM: arquitectura básica

Cómo conectar un NPC de Unity a un LLM (OpenAI, Anthropic) y conseguir que responda al jugador. System prompt, contexto, costos por interacción y latencia explicados sin humo.

Publicado: · Por Juanjo "Banyo" López

0. Introducción

Un NPC con diálogo escrito a mano envejece en la primera partida. El jugador prueba la línea “interesante”, la oye, y al volver a hablar con el mismo NPC ya sabe qué va a salir. Los LLMs (Large Language Models) abren otra puerta: el NPC compone su respuesta en runtime a partir de un prompt y el contexto de la conversación.

No es magia. Es una llamada HTTP a un servicio externo (OpenAI, Anthropic, Google) o local (Ollama, llama.cpp) que devuelve texto. Lo que tú escribes es el prompt, lo que el modelo entiende como “quién es este personaje y qué sabe”. Lo que pagas (en API comercial) son los tokens que entran y los que salen.

Este tutorial es el punto de entrada. Aquí solo conectas Unity a una API y consigues que un NPC responda. Memoria persistente, function calling, RAG y LLMs locales viven en tutoriales aparte.

En este tutorial vas a ver:

  • Qué es un LLM y qué hace cuando lo llamas.
  • La arquitectura mínima de un NPC con LLM (cliente HTTP, system prompt, history).
  • Los costos reales por interacción y cómo no quemar tu wallet.

1. Demo

NPC con LLM: flujo completo Jugador escribe → cliente arma prompt + history → API → respuesta del NPC. Lo que ves en pantalla.

2. Concepto y arquitectura

Un LLM es un modelo entrenado para predecir el siguiente token (≈ una palabra o trozo de palabra) dado un texto previo. Cuando lo “llamas” no le mandas tu pregunta en limpio: le mandas un contexto formado por un system prompt (las reglas del personaje), el historial de la conversación y el mensaje actual del jugador. El modelo continúa la conversación generando la siguiente respuesta token por token.

2.1 ¿Qué pasa exactamente cuando llamas a un LLM?

El cliente arma un JSON con la lista de mensajes y la manda por HTTPS a la API. El servidor procesa el contexto, genera tokens uno a uno hasta llegar a un stop token o al max_tokens, y devuelve la respuesta. Cada llamada es independiente: el servidor no recuerda nada entre requests. Si quieres memoria, tú la mandas en cada llamada.

Cliente Unity ↔ API LLM
Cliente (Unity)messages: [ [ role: “system”, … ] [ role: “user”, … ]]API (OpenAI / Anthropic)1. Tokeniza contexto2. Predice siguiente token3. Repite hasta stop4. Devuelve respuestaHTTP POST · JSON bodyJSON · content + usage[ content: ”…”, usage: in 120, out 45 ]

El cliente arma el JSON con system + history + user y lo manda por HTTPS. La API tokeniza, predice y devuelve content + usage. Cada llamada es stateless.

2.2 ¿Qué es un agente NPC con LLM en términos de código?

Un objeto que, mínimo, tiene:

  • systemPrompt — quién es el personaje. Inmutable durante la sesión.
  • history — lista de mensajes (rol + contenido). Crece con cada turno.
  • client — el envío HTTP a la API.
  • Límites: maxHistoryMessages (cuántos turnos guardar), maxTokens (cuántos puede generar la respuesta), temperature (qué tan creativo es).

El bucle es simple: jugador escribe → tú agregas su mensaje al history → llamas a la API con system + history → recibes respuesta → la agregas al history → la muestras.

2.3 ¿Cómo se escribe un system prompt para un NPC?

El system prompt es el “alma” del personaje. Debe especificar:

  1. Identidad: nombre, rol, edad aproximada, origen.
  2. Tono: formal/informal, irónico, agresivo, melancólico.
  3. Conocimientos: qué sabe del mundo del juego, qué no sabe.
  4. Restricciones: qué nunca puede decir, qué nunca puede revelar.
  5. Formato: longitud preferida, si usa primera persona, si describe acciones entre asteriscos.

Ejemplo para un mercader en una taberna:

Eres Maerwyn, mercader nómada en una taberna del reino de Veldria.
Tienes 50 años, hablas con cansancio pero con humor seco.
Vendes pociones, mapas y rumores. Nunca rebajas precios sin un motivo en la conversación.
No sabes nada del mundo real ni de tecnología; si el jugador pregunta algo así, te confundes con gracia.
Respondes en 1-3 frases. Acciones físicas entre asteriscos (*se inclina*).

2.4 System prompt vs historial: ¿qué va dónde?

Va en system promptVa en historial
Identidad del personajeLo que dijo el jugador
Reglas inmutablesLo que dijo el NPC en turnos anteriores
Estado del mundo estático (lore)Eventos recientes (“el jugador acaba de matar al guardia”)
Formato de respuesta esperadoCambios de estado dinámicos

El system prompt se manda en cada llamada porque el modelo no lo recuerda. Sí, eso significa que pagas esos tokens cada turno. Hay una técnica para reducirlo (prompt caching) que verás en el tutorial de costos.

2.5 ¿Cómo se limita el contexto sin romper la coherencia?

El history crece sin freno. Si no lo recortas, en 20 turnos tu llamada manda 10 000 tokens y la latencia se dispara. Estrategias:

  • Sliding window: guardas solo los últimos N turnos (N entre 6 y 20 según juego).
  • Summary rolling: cuando llegas al límite, pides al LLM un resumen de los turnos antiguos y reemplazas esos turnos por el resumen.
  • Eventos pinned: marcas algunos turnos como “no descartar” (la primera vez que el jugador dio su nombre, una promesa importante).

Para un NPC tipo mercader o guardia, sliding window de 8-10 turnos basta. Para un compañero de aventura con memoria larga necesitas el tutorial de memoria persistente.

2.6 ¿Cuánto cuesta cada interacción en una API comercial?

Los precios se publican en USD por millón de tokens y cambian. Para fijar ideas con valores recientes (siempre verifica al implementar):

Modelo (ejemplo)Input USD/1M tokOutput USD/1M tok
Modelo económico (Haiku-tier, GPT mini)~0.25 – 1.00~1.00 – 4.00
Modelo medio (Sonnet-tier, GPT-4o)~3.00 – 5.00~10.00 – 20.00
Modelo top (Opus-tier, GPT-4 frontier)~15.00~75.00

Un turno típico de NPC en un juego: ~800 tokens de input (system + history) y ~80 tokens de output. Con modelo económico, un turno cuesta del orden de 0.0003 USD (3 centavos por 100 turnos). Con modelo top, de 0.018 USD por turno.

Multiplica por la cantidad de turnos que un jugador hace en una sesión, por jugadores activos, y verás por qué la elección del modelo importa. Detalles en el tutorial de costos.

3. Pseudocódigo

class NpcLlmAgent
    systemPrompt: String
    history: List<Message>
    maxHistoryMessages: Int = 10
    maxTokens: Int = 200
    temperature: Float = 0.8

function chat(agent: NpcLlmAgent, userMessage: String) -> String
    agent.history.append(Message("user", userMessage))
    trimHistory(agent)
    messages = [Message("system", agent.systemPrompt)] + agent.history
    response = callApi(messages, agent.maxTokens, agent.temperature)
    agent.history.append(Message("assistant", response.content))
    return response.content

function trimHistory(agent: NpcLlmAgent)
    while agent.history.length > agent.maxHistoryMessages
        agent.history.removeFirst()

function callApi(messages: List<Message>, maxTokens: Int, temp: Float) -> ApiResponse
    body = { model: "...", messages: messages, max_tokens: maxTokens, temperature: temp }
    return httpPost(API_URL, body, headers={Authorization: "Bearer " + API_KEY})

Tres ideas: el system prompt no se persiste en history (se inyecta cada llamada), el trim evita el crecimiento infinito, y la API es stateless desde el lado del servidor.

4. Implementación en Unity / C#

Cliente mínimo con HttpClient apuntando a la API de OpenAI. El mismo patrón sirve para Anthropic cambiando endpoint y formato del body.

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

public class NpcLlmAgent : MonoBehaviour {
    [TextArea(4, 10)] public string systemPrompt;
    public int maxHistoryMessages = 10;
    public int maxTokens = 200;
    [Range(0f, 1.5f)] public float temperature = 0.8f;

    readonly List<(string role, string content)> history = new();
    static readonly HttpClient http = new();

    public async Task<string> Chat(string userMessage) {
        history.Add(("user", userMessage));
        TrimHistory();

        var messages = new List<object> {
            new { role = "system", content = systemPrompt }
        };
        foreach (var (role, content) in history)
            messages.Add(new { role, content });

        var body = JsonUtility.ToJson(new {
            model = "gpt-4o-mini",
            messages = messages.ToArray(),
            max_tokens = maxTokens,
            temperature = temperature
        });

        var req = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions");
        req.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("OPENAI_API_KEY")}");
        req.Content = new StringContent(body, Encoding.UTF8, "application/json");

        var resp = await http.SendAsync(req);
        var json = await resp.Content.ReadAsStringAsync();
        var reply = ParseAssistantMessage(json); // helper que extrae choices[0].message.content
        history.Add(("assistant", reply));
        return reply;
    }

    void TrimHistory() {
        while (history.Count > maxHistoryMessages) history.RemoveAt(0);
    }

    string ParseAssistantMessage(string json) {
        // En producción usa Newtonsoft.Json o System.Text.Json. JsonUtility de Unity no maneja bien estructuras anidadas.
        return ""; // implementación omitida por brevedad
    }
}

5. En otros engines

  • Godot: HTTPRequest node + el mismo JSON. Si usas C# en Godot, el código es prácticamente idéntico al de arriba.
  • Unreal: FHttpModule + IHttpRequest. Más verboso pero misma idea. Plugins como OpenAI-Api-Unreal empaquetan el cliente.
  • JavaScript / Electron / web: fetch directo. Cuidado igual con la API key — usa proxy.

La parte que cambia entre engines es el cliente HTTP y la UI; el contenido del prompt y la arquitectura no cambian.

6. Quiz

Pon a prueba lo que entendiste

Responde una por una. La explicación aparece al elegir, correcta o no.

  1. Tu NPC olvida quién es cada vez que el jugador empieza una nueva línea de diálogo. ¿Qué te falta?

  2. Tras 30 turnos, las respuestas del NPC tardan 8 segundos y cuestan el triple. ¿Qué estás haciendo mal?

  3. ¿Por qué NO meter la API key dentro del build de Unity?

  4. ¿Qué va en el system prompt y qué va en el historial?