TL;DR
Construir un modelo de lenguaje mínimo desde cero requiere menos de 300 líneas de Python. El proceso revela exactamente cómo funcionan la tokenización, la atención y la inferencia, lo que lo convierte en un consumidor de API mucho mejor cuando integra LLMs de producción en sus aplicaciones.
Introducción
La mayoría de los desarrolladores tratan los modelos de lenguaje como cajas negras. Envías texto, salen tokens y, en algún punto intermedio, ocurre la magia. Ese modelo mental funciona bien hasta que necesitas depurar una integración de API defectuosa, ajustar los parámetros de muestreo o averiguar por qué tu modelo sigue alucinando datos estructurados.
GuppyLM, un proyecto que recientemente llegó a la página principal de HackerNews con 842 puntos, hace que los elementos internos sean visibles. Es un transformador de 8.7M de parámetros escrito desde cero en Python. Se entrena en menos de una hora en una GPU de consumo. El código cabe en un solo archivo. El objetivo no es competir con GPT-4; es desmitificar lo que realmente hacen los LLM.
Este artículo explica cómo construir un pequeño LLM, qué hace cada componente y qué le enseña la comprensión de los elementos internos cuando trabaja profesionalmente con APIs de IA.
¿Qué hace que un modelo de lenguaje sea "pequeño"?
Un LLM de producción como GPT-4 tiene cientos de miles de millones de parámetros. Un LLM "pequeño" se encuentra en el rango de 1M a 25M de parámetros. Proyectos como GuppyLM (8.7M), nanoGPT de Karpathy (124M) y MicroLM (1-2M) entran en esta categoría.
Los LLM pequeños pueden: - Entrenarse en un portátil o Google Colab - Caber completamente en la memoria de la CPU - Ser inspeccionados, modificados y depurados a nivel de pesos
No pueden: - Manejar razonamientos complejos - Generar texto coherente de formato largo de manera fiable - Igualar la profundidad factual de los modelos de producción
El valor no es el resultado. Es la comprensión que se obtiene al construir uno.
Componentes principales: cómo funciona realmente un LLM
Antes de escribir cualquier código, necesita saber qué hacen las cuatro partes principales.
Tokenizador
El tokenizador convierte el texto sin procesar en ID de enteros. "Hello, world!" se convierte en algo como [15496, 11, 995, 0]. Cada entero se asigna a una unidad de subpalabra de un vocabulario fijo.
Por qué esto es importante para el trabajo con API: los recuentos de tokens afectan directamente la latencia y el costo. Comprender cómo los tokenizadores dividen el texto lo ayuda a escribir prompts que se ajusten a las ventanas de contexto y eviten truncamientos inesperados.
GuppyLM utiliza un tokenizador simple a nivel de caracteres. Los modelos de producción como GPT-4 utilizan BPE (codificación de pares de bytes) con vocabularios de 50K-100K tokens.
Capa de incrustación (Embedding layer)
La capa de incrustación convierte los ID de tokens en vectores densos. Cada token obtiene un vector aprendido (por ejemplo, 384 dimensiones en GuppyLM). Estos vectores llevan un significado semántico: los tokens similares terminan juntos en el espacio vectorial.
Las incrustaciones de posición se añaden encima, de modo que el modelo conoce el orden de los tokens.
Bloques de transformador
Este es el cálculo central. Cada bloque tiene dos partes:
Autoatención (Self-attention): permite que cada token mire todos los demás tokens en la secuencia y decida cuáles son importantes para predecir el siguiente token. GuppyLM utiliza 6 cabezas de atención en 6 capas.
Red de avance (Feed-forward network): una MLP de dos capas aplicada a la representación de cada token después de la atención. GuppyLM utiliza activación ReLU, que es más simple que la SwiGLU utilizada en arquitecturas más nuevas.
Cabezal de salida
Después del bloque final del transformador, una capa lineal proyecta la representación de cada token a un vector de tamaño igual al vocabulario. Aplica softmax para obtener probabilidades, elige el token siguiente más probable (o muestrea) y repite.
Construyendo un LLM mínimo en Python
Aquí hay un LLM mínimo funcional basado en el enfoque de GuppyLM. Esto se ejecuta en PyTorch estándar.
import torch
import torch.nn as nn
import torch.nn.functional as F
# Hyperparameters
VOCAB_SIZE = 256 # character-level: one slot per ASCII char
D_MODEL = 128 # embedding dimension
N_HEADS = 4 # attention heads
N_LAYERS = 3 # transformer blocks
SEQ_LEN = 64 # context window
DROPOUT = 0.1
class SelfAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
self.n_heads = n_heads
self.head_dim = d_model // n_heads
self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
self.proj = nn.Linear(d_model, d_model, bias=False)
self.dropout = nn.Dropout(DROPOUT)
def forward(self, x):
B, T, C = x.shape
qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim)
q, k, v = qkv.unbind(dim=2)
q = q.transpose(1, 2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
# Causal mask: each token can only attend to previous tokens
scale = self.head_dim ** -0.5
attn = (q @ k.transpose(-2, -1)) * scale
mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
attn = attn.masked_fill(mask, float('-inf'))
attn = F.softmax(attn, dim=-1)
attn = self.dropout(attn)
out = (attn @ v).transpose(1, 2).reshape(B, T, C)
return self.proj(out)
class TransformerBlock(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
self.attn = SelfAttention(d_model, n_heads)
self.ff = nn.Sequential(
nn.Linear(d_model, 4 * d_model),
nn.ReLU(),
nn.Linear(4 * d_model, d_model),
nn.Dropout(DROPOUT),
)
self.ln1 = nn.LayerNorm(d_model)
self.ln2 = nn.LayerNorm(d_model)
def forward(self, x):
x = x + self.attn(self.ln1(x))
x = x + self.ff(self.ln2(x))
return x
class TinyLLM(nn.Module):
def __init__(self):
super().__init__()
self.embed = nn.Embedding(VOCAB_SIZE, D_MODEL)
self.pos_embed = nn.Embedding(SEQ_LEN, D_MODEL)
self.blocks = nn.ModuleList([
TransformerBlock(D_MODEL, N_HEADS) for _ in range(N_LAYERS)
])
self.ln_f = nn.LayerNorm(D_MODEL)
self.head = nn.Linear(D_MODEL, VOCAB_SIZE, bias=False)
def forward(self, idx):
B, T = idx.shape
tok_emb = self.embed(idx)
pos = torch.arange(T, device=idx.device)
pos_emb = self.pos_embed(pos)
x = tok_emb + pos_emb
for block in self.blocks:
x = block(x)
x = self.ln_f(x)
logits = self.head(x)
return logits
# Initialize and count parameters
model = TinyLLM()
total_params = sum(p.numel() for p in model.parameters())
print(f"Model size: {total_params:,} parameters") # ~1.2M
Bucle de entrenamiento
import torch.optim as optim
def train(model, data, epochs=100, lr=3e-4):
optimizer = optim.AdamW(model.parameters(), lr=lr)
model.train()
for epoch in range(epochs):
# data: tensor of token IDs, shape [batch, seq_len+1]
x = data[:, :-1] # input: all tokens except last
y = data[:, 1:] # target: all tokens shifted by 1
logits = model(x)
loss = F.cross_entropy(logits.reshape(-1, VOCAB_SIZE), y.reshape(-1))
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}, loss: {loss.item():.4f}")
Inferencia (generación de texto)
@torch.no_grad()
def generate(model, prompt_ids, max_new_tokens=50, temperature=1.0, top_k=10):
model.eval()
ids = torch.tensor([prompt_ids])
for _ in range(max_new_tokens):
idx_cond = ids[:, -SEQ_LEN:] # crop to context window
logits = model(idx_cond)
logits = logits[:, -1, :] / temperature # last token only
# top-k sampling
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float('-inf')
probs = F.softmax(logits, dim=-1)
next_id = torch.multinomial(probs, num_samples=1)
ids = torch.cat([ids, next_id], dim=1)
return ids[0].tolist()
Lo que esto le enseña sobre el comportamiento de las APIs de IA
Construir esto revela varias cosas que lo convierten en un mejor consumidor de API.
La temperatura y el muestreo son mecánicos, no mágicos
La temperatura divide los logits antes del softmax. Una temperatura más alta = una distribución más plana = una salida más aleatoria. Una temperatura más baja = una distribución más nítida = una salida más determinista. Cuando su API de producción devuelve resultados inconsistentes con temperature=0.0, no es un error. La temperatura cero real es un argmax codicioso, y muchas API la redondean ligeramente para evitar salidas degeneradas.
Las ventanas de contexto son límites estrictos, no sugerencias flexibles
La línea idx_cond = ids[:, -SEQ_LEN:] en el bucle de inferencia muestra exactamente lo que sucede en el límite del contexto. El modelo elimina silenciosamente los tokens más antiguos. Si su integración de API asume que el modelo recuerda el historial completo de la conversación, no lo hace después de cierto punto. Consulte [interno: cómo-funciona-la-memoria-del-agente-de-IA] para saber cómo los agentes manejan este problema.
Los tokens de streaming son solo pasos de inferencia hechos visibles
Las API de streaming no hacen nada arquitectónicamente diferente. Ejecutan el bucle de inferencia y envían cada token al flujo de respuesta a medida que se genera. Comprender esto ayuda cuando se escribe la lógica de reintento: un flujo interrumpido a mitad de la generación no se puede reanudar, debe reiniciarse.
Los logits explican por qué la salida estructurada es difícil
El modelo asigna probabilidad a cada token en el vocabulario en cada paso. Generar JSON válido requiere que el token correcto gane en cada posición. Librerías como Outlines y Guidance restringen la distribución de logits para imponer la gramática en el momento de la inferencia. Cuando ve que las API de IA ofrecen modos de "salida estructurada", esto es lo que están haciendo internamente.
Cómo probar integraciones de API de IA con Apidog
Una vez que comprende cómo funciona la inferencia de LLM, puede escribir pruebas de API mucho mejores. Los Escenarios de prueba de Apidog le permiten encadenar llamadas a API y afirmar la estructura de las respuestas de IA.
Por ejemplo, al probar una API de chat en streaming:
- Cree un Escenario de prueba en Apidog con su endpoint
/v1/chat/completions - Establezca aserciones para verificar la estructura de la respuesta:
response.choices[0].finish_reason == "stop",response.usage.total_tokens < 4096 - Agregue un paso de seguimiento que envíe la respuesta como contexto al siguiente turno, simulando una conversación multi-turno
- Use Apidog's Smart Mock para simular el endpoint de IA y probar el manejo de errores de su aplicación: simule
finish_reason: "length"(salida truncada),finish_reason: "content_filter"y un tiempo de espera de red a mitad de la transmisión
Así es como se prueban las integraciones de IA sin quemar créditos de API en cada ejecución de CI. Consulte [interno: tutorial-de-pruebas-de-api] para una visión más amplia de los enfoques de pruebas de API.
Probando aserciones de recuento de tokens
{
"assertions": [
{
"field": "response.usage.completion_tokens",
"operator": "less_than",
"value": 512
},
{
"field": "response.choices[0].finish_reason",
"operator": "equals",
"value": "stop"
},
{
"field": "response.choices[0].message.content",
"operator": "not_empty"
}
]
}
Ejecute esto en varios modelos (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) en un solo escenario de prueba para detectar diferencias en el esquema de la API antes de que lleguen a producción.
Avanzado: cuantificación y optimización de inferencia
Una vez que tenga un pequeño LLM en funcionamiento, vale la pena comprender dos técnicas porque se aplican directamente a cómo se sirven los modelos de producción.
Cuantificación
Los pesos de nuestro modelo son floats de 32 bits por defecto. La cuantificación los reduce a enteros de 8 bits (INT8) o incluso de 4 bits (INT4). Esto reduce el uso de memoria en 4-8x con una pérdida de precisión modesta.
# Example: dynamic INT8 quantization in PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
Las API de producción ejecutan modelos cuantificados. Cuando ve una calidad de salida diferente en diferentes "versiones" del mismo modelo, la cuantificación suele estar involucrada.
Caché KV
En nuestro bucle de inferencia, recalculamos la atención en toda la secuencia en cada paso. Los sistemas de producción almacenan en caché los pares clave-valor de los tokens anteriores (la caché KV) para que cada nuevo token solo necesite un nuevo cálculo de atención. Por eso, el primer token en una respuesta de streaming tarda más que los siguientes.
LLM pequeño vs. API de producción: cuándo usar cada uno
| Caso de uso | LLM pequeño | API de producción |
|---|---|---|
| Aprendizaje de los internos del modelo | Lo mejor para | Excesivo |
| Prototipado de una nueva aplicación | Calidad insuficiente | Lo mejor para |
| Datos privados/sensibles | Buena opción | Depende del proveedor |
| Despliegue offline/en el borde | Viable | No posible |
| Sensible al costo, alto volumen | Posible con compensaciones | Caro a escala |
| Tareas que requieren mucho razonamiento | No viable | Requerido |
La respuesta real para la mayoría de los desarrolladores: use la API de producción para su aplicación, pero ejecute un modelo pequeño para comprender lo que sucede bajo el capó. Los dos no compiten. El artículo [interno: asistentes-de-codificación-de-código-abierto-2026] cubre herramientas que desdibujan esta línea con configuraciones de "traiga su propio modelo".
Conclusión
Construir un LLM pequeño desde cero lleva un fin de semana. Lo que se obtiene no es un sistema de producción; es un modelo mental funcional de cómo funciona realmente cada modelo de lenguaje, desde GuppyLM hasta GPT-4o. Esa comprensión da sus frutos cada vez que depura una integración de streaming, ajusta los parámetros de muestreo o diseña aserciones para sus pruebas de API de IA.
El proyecto GuppyLM es un buen punto de partida. Clónelo, entrénelo con cualquier conjunto de datos de texto y pase una tarde leyendo el bucle de inferencia. Luego, vuelva a sus integraciones de API de producción y las verá de manera diferente.
Pruebe los Escenarios de prueba de Apidog para aportar el mismo rigor a sus pruebas de API de IA que aplicaría a cualquier otro sistema de backend.
Preguntas frecuentes
¿Cuántos parámetros necesita un LLM "pequeño" para generar texto coherente?Alrededor de 10M-50M de parámetros con un conjunto de datos de entrenamiento decente pueden producir oraciones localmente coherentes. Por debajo de 1M, se obtiene galimatías en la mayoría de las tareas. GuppyLM, con 8.7M, funciona para conversaciones cortas en su dominio de entrenamiento (60 temas).
¿Puedo ejecutar un LLM pequeño sin una GPU?Sí. Los modelos con menos de 100M de parámetros funcionan bien en la CPU, aunque la inferencia es más lenta. El modelo anterior (1.2M de parámetros) genera tokens en milisegundos en la CPU de un portátil.
¿En qué conjunto de datos debo entrenar?Los modelos a nivel de caracteres funcionan bien con textos del Proyecto Gutenberg, subconjuntos de Wikipedia o cualquier corpus de texto plano. GuppyLM utiliza un conjunto de datos de conversación de 60K entradas en HuggingFace (arman-bd/guppylm-60k-generic). Para la generación de código, utilice The Stack o CodeParrot.
¿Cuál es la diferencia entre temperatura y muestreo top-k?La temperatura escala la distribución de logits (controla la aleatoriedad general). Top-k restringe el grupo de muestreo a los k tokens más probables antes de aplicar la temperatura. Se aplican juntos: primero top-k filtra los candidatos, luego la temperatura da forma a las probabilidades dentro de ese conjunto.
¿Por qué mi LLM a veces se repite?La repetición es un modo de fallo en el que el modelo asigna una alta probabilidad a los tokens que acaba de generar porque aparecieron en el contexto. Las API de producción utilizan penalizaciones por repetición (un ajuste de logits que descarta los tokens generados recientemente). Agregue repetition_penalty=1.1 en su llamada a la API para reducir esto.
¿Cuánto tiempo lleva entrenar un LLM pequeño?El modelo anterior entrena para obtener una salida coherente en menos de 2 horas en una sola GPU (RTX 3060 o equivalente). GuppyLM entrena en Colab en aproximadamente el mismo tiempo. Los modelos más grandes (100M+) necesitan configuraciones multi-GPU y días de entrenamiento.
¿Cuál es la forma más rápida de pasar de un LLM pequeño a un endpoint de API real?Exporte al formato GGUF utilizando el script de conversión de llama.cpp, luego sírvalo con llama-server. Esto le proporciona un endpoint de API compatible con OpenAI que se ejecuta localmente. Luego puede apuntar Apidog hacia él para realizar pruebas, consulte [interno: mejores-prácticas-de-rest-api].
¿Cómo manejan los LLM de producción el contexto más largo que su ventana de entrenamiento?Técnicas como RoPE (Rotary Position Embedding) con escalado extendido, atención de ventana deslizante y generación aumentada por recuperación extienden el contexto efectivo. La arquitectura central del transformador no cambia; estas son modificaciones a cómo se codifica la información de posición y cómo se aplica la ventana de atención.
