Em resumo
Construir um modelo de linguagem mínimo do zero leva menos de 300 linhas de Python. O processo revela exatamente como a tokenização, a atenção e a inferência funcionam, o que o torna um consumidor de API muito melhor ao integrar LLMs de produção em suas aplicações.
Introdução
A maioria dos desenvolvedores trata os modelos de linguagem como caixas-pretas. Você envia texto, tokens saem, e em algum lugar no meio, a mágica acontece. Esse modelo mental funciona bem até que você precise depurar uma integração de API quebrada, ajustar parâmetros de amostragem ou descobrir por que seu modelo continua alucinando dados estruturados.
GuppyLM, um projeto que recentemente atingiu a página principal do HackerNews com 842 pontos, torna os detalhes internos visíveis. É um transformer de 8.7M parâmetros escrito do zero em Python. Ele treina em menos de uma hora em uma GPU de consumidor. O código cabe em um único arquivo. O objetivo não é competir com o GPT-4; é desmistificar o que os LLMs realmente fazem.
Este artigo descreve como construir um LLM minúsculo, o que cada componente faz e o que a compreensão dos detalhes internos ensina você ao trabalhar profissionalmente com APIs de IA.
💡 Se você está testando integrações de API de IA, os Cenários de Teste do Apidog permitem verificar respostas em streaming, fazer asserções sobre a estrutura de tokens e simular conclusões de casos extremos sem gastar créditos de produção. Mais sobre isso depois.
O que torna um modelo de linguagem "minúsculo"?
Um LLM de produção como o GPT-4 tem centenas de bilhões de parâmetros. Um LLM "minúsculo" está na faixa de 1M a 25M de parâmetros. Exemplos: GuppyLM (8.7M), nanoGPT de Karpathy (124M), MicroLM (1-2M).
O que você pode fazer com LLMs minúsculos:
- Treinar em um laptop ou Google Colab
- Caber inteiramente na memória da CPU
- Inspecionar, modificar e depurar o modelo no nível do peso
Limitações:
- Não lida com raciocínio complexo
- Não gera texto coerente de formato longo de forma confiável
- Não possui a profundidade factual dos modelos de produção
O valor está no entendimento técnico, não na saída gerada.
Componentes centrais: como um LLM realmente funciona
Antes de implementar, entenda as quatro partes principais:
Tokenizador
O tokenizador converte texto em IDs inteiros. Por exemplo, "Olá, mundo!" pode virar [15496, 11, 995, 0]. Cada inteiro corresponde a uma subpalavra de um vocabulário fixo.
Por que importa para APIs: contagem de tokens afeta latência e custo. Saber como os tokenizadores dividem o texto ajuda a escrever prompts que cabem na janela de contexto e evitam truncamentos.
- GuppyLM: tokenização por caractere.
- GPT-4 (produção): tokenização BPE (byte-pair encoding) com vocabulários de 50K-100K.
Camada de Embedding
Converte IDs de token em vetores densos (por exemplo, 384 dimensões). Tokens semelhantes ficam próximos no espaço vetorial. Embeddings de posição são somados para informar a ordem à rede.
Blocos Transformer
Cada bloco contém:
-
Autoatenção: permite que cada token veja todos os outros da sequência para prever o próximo.
- GuppyLM: 6 cabeças de atenção em 6 camadas.
- Rede feed-forward: MLP de duas camadas, ativação ReLU (mais simples que SwiGLU dos modelos recentes).
Camada de Saída
Após o último bloco transformer, uma camada linear projeta os vetores para o tamanho do vocabulário. Aplica softmax para probabilidades, escolhe (ou amostra) o próximo token, repete.
Construindo um LLM mínimo em Python
Abaixo, um LLM funcional baseado no GuppyLM, usando PyTorch padrão:
import torch
import torch.nn as nn
import torch.nn.functional as F
# Hiperparâmetros
VOCAB_SIZE = 256 # um slot por caractere ASCII
D_MODEL = 128 # dimensão do embedding
N_HEADS = 4 # cabeças de atenção
N_LAYERS = 3 # blocos transformer
SEQ_LEN = 64 # janela de contexto
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)
# Máscara causal: cada token só "vê" anteriores
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
# Inicialização e contagem de parâmetros
model = TinyLLM()
total_params = sum(p.numel() for p in model.parameters())
print(f"Model size: {total_params:,} parameters") # ~1.2M
Loop de treinamento
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 de IDs de tokens, shape [batch, seq_len+1]
x = data[:, :-1] # entrada: todos exceto o último
y = data[:, 1:] # alvo: todos menos o primeiro
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}")
Inferência (geração 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:] # corta para a janela de contexto
logits = model(idx_cond)
logits = logits[:, -1, :] / temperature # só o último token
# 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()
O que isso ensina sobre o comportamento da API de IA
Ao implementar esse modelo, você entende na prática como consumir APIs de IA de forma mais eficiente.
Temperatura e amostragem são mecânicas, não mágicas
A temperatura atua nos logits antes do softmax. Temperatura alta = distribuição mais plana = mais aleatoriedade. Temperatura baixa = distribuição mais "dura" = mais determinismo. Se sua API retorna resultados inconsistentes com temperature=0.0, não é bug: o argmax puro pode ser ajustado pelas APIs para evitar saídas degeneradas.
Janelas de contexto são limites rígidos
A linha idx_cond = ids[:, -SEQ_LEN:] mostra que o modelo descarta tokens antigos. Não confie que o histórico da conversa é sempre lembrado pela API após a janela de contexto ser excedida. Veja [internal: how-ai-agent-memory-works] para estratégias.
Tokens em streaming são apenas passos de inferência expostos
APIs de streaming apenas expõem cada token gerado no loop de inferência. Se o streaming for interrompido, precisa reiniciar – não há como retomar do meio.
Logits explicam a dificuldade de saída estruturada
O modelo precisa acertar o token correto a cada passo para, por exemplo, gerar JSON válido. Ferramentas como Outlines e Guidance restringem a distribuição de logits para impor gramática. APIs de IA com "saída estruturada" fazem isso internamente.
Como testar integrações de API de IA com o Apidog
Com o entendimento do funcionamento do LLM, você pode criar testes de API melhores. Os Cenários de Teste do Apidog permitem orquestrar chamadas de API e fazer asserções sobre as respostas de IA.
Exemplo de teste para API de chat em streaming:
- Crie um Cenário de Teste no Apidog com seu endpoint
/v1/chat/completions - Defina asserções sobre a resposta:
response.choices[0].finish_reason == "stop"response.usage.total_tokens < 4096
- Adicione uma etapa que envia a resposta como contexto para simular conversas multi-turno
- Use o Smart Mock do Apidog para simular retornos da IA e testar erros:
- Simule
finish_reason: "length",finish_reason: "content_filter"e timeouts no meio do streaming
- Simule
Assim, você testa integrações de IA sem gastar créditos de API em cada execução de CI. Veja [internal: api-testing-tutorial] para outras estratégias.
Testando asserções de contagem 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"
}
]
}
Execute isso em múltiplos modelos (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) em um único Cenário de Teste para capturar diferenças de esquema de API antes de ir para produção.
Avançado: quantização e otimização de inferência
Com o LLM minúsculo funcional, entenda duas técnicas essenciais para produção:
Quantização
Pesos do modelo são floats de 32 bits por padrão. Quantização reduz para INT8 ou INT4, economizando memória (4-8x menos) com pouca perda de precisão.
# Exemplo: quantização dinâmica INT8 em PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
APIs normalmente rodam modelos quantizados. Diferenças de qualidade entre versões de modelos geralmente envolvem quantização.
Cache KV
No loop de inferência acima, recalculamos atenção para toda a sequência a cada token. Sistemas de produção armazenam em cache os pares chave-valor (KV) dos tokens anteriores. Assim, só o novo token precisa de computação extra. Isso explica por que o primeiro token do streaming é o mais lento.
LLM minúsculo vs. API de produção: quando usar cada um
| Caso de uso | LLM minúsculo | API de produção |
|---|---|---|
| Aprendizado dos detalhes internos do modelo | Melhor para | Exagerado |
| Criação de protótipos de um novo aplicativo | Qualidade insuficiente | Melhor para |
| Dados privados/sensíveis | Boa opção | Depende do provedor |
| Implantação offline/edge | Viável | Não possível |
| Sensível ao custo, alto volume | Possível com concessões | Caro em escala |
| Tarefas que exigem raciocínio | Não viável | Necessário |
A recomendação prática: use a API de produção para seu app, mas rode um modelo minúsculo para entender o que acontece "por baixo dos panos". Os dois são complementares. Veja o artigo [internal: open-source-coding-assistants-2026] para ferramentas que misturam abordagens.
Conclusão
Construir um LLM minúsculo do zero leva um fim de semana. O objetivo não é produção, mas sim entender como todo modelo de linguagem – do GuppyLM ao GPT-4o – realmente funciona. Esse conhecimento é útil ao depurar integrações de streaming, ajustar parâmetros de amostragem ou criar testes para APIs de IA.
O projeto GuppyLM é um ótimo ponto de partida. Clone, treine em um dataset de texto e analise o loop de inferência. Depois, volte para suas integrações de API e enxergue tudo com outros olhos.
Experimente os Cenários de Teste do Apidog para trazer rigor aos seus testes de API de IA, do mesmo jeito que você testa qualquer backend.
FAQ
Quantos parâmetros um LLM "minúsculo" precisa para gerar texto coerente?
Cerca de 10M-50M com um dataset decente produzem frases localmente coerentes. Abaixo de 1M, tende a gerar "gibberish". O GuppyLM (8.7M) funciona para conversas curtas em seu domínio de treinamento (60 tópicos).
Posso rodar um LLM minúsculo sem GPU?
Sim. Modelos com menos de 100M rodam bem na CPU, embora mais lentos. O modelo acima (1.2M) gera tokens em milissegundos em laptop.
Em qual dataset treinar?
Modelos de nível caractere: textos do Projeto Gutenberg, Wikipedia ou corpus simples. GuppyLM usa conversas (60K entradas) no HuggingFace (arman-bd/guppylm-60k-generic). Para código, use The Stack ou CodeParrot.
Diferença entre temperatura e top-k?
Temperatura ajusta a aleatoriedade geral. Top-k limita os candidatos aos k mais prováveis antes de aplicar a temperatura. Combine ambos: primeiro top-k filtra, depois temperatura define distribuição.
Por que meu LLM repete?
Repetição ocorre quando o modelo atribui alta probabilidade a tokens já gerados. APIs de produção usam penalidades de repetição (repetition_penalty=1.1) para mitigar.
Quanto tempo para treinar um LLM minúsculo?
O modelo acima chega a coerência em menos de 2h em uma GPU (RTX 3060). GuppyLM treina no Colab em tempo similar. Modelos maiores (100M+) exigem multi-GPU e dias de treino.
Como transformar um LLM minúsculo em endpoint de API real?
Exporte para GGUF com o script do llama.cpp e sirva via llama-server. Assim, você terá um endpoint compatível com OpenAI local. O Apidog pode testar direto nesse endpoint – veja [internal: rest-api-best-practices].
Como LLMs de produção lidam com contextos maiores?
Técnicas como RoPE (Rotary Position Embedding) com escalonamento, atenção com janela deslizante e RAG (retrieval augmented generation) estendem o contexto. O transformer central não muda; o truque está em como codificar posição e aplicar a janela de atenção.
Top comments (0)