Como Criar um LLM do Zero: Aprendizados e Passo a Passo

Ashley Innocent

Ashley Innocent

7 abril 2026

Como Criar um LLM do Zero: Aprendizados e Passo a Passo

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.
botão

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. Projetos como GuppyLM (8.7M), nanoGPT de Karpathy (124M) e MicroLM (1-2M) se enquadram nesta categoria.

LLMs minúsculos podem: - Treinar em um laptop ou Google Colab - Caber inteiramente na memória da CPU - Ser inspecionados, modificados e depurados no nível do peso

Eles não podem: - Lidar com raciocínio complexo - Gerar texto coerente de formato longo de forma confiável - Corresponder à profundidade factual dos modelos de produção

O valor não está na saída. Está na compreensão que você obtém ao construir um.

Componentes centrais: como um LLM realmente funciona

Antes de escrever qualquer código, você precisa saber o que as quatro partes principais fazem.

Tokenizador

O tokenizador converte texto bruto em IDs inteiras. "Olá, mundo!" se torna algo como [15496, 11, 995, 0]. Cada inteiro mapeia para uma unidade de subpalavra de um vocabulário fixo.

Por que isso importa para o trabalho com APIs: as contagens de tokens afetam diretamente a latência e o custo. Entender como os tokenizadores dividem o texto ajuda você a escrever prompts que se encaixam nas janelas de contexto e evitam truncamentos inesperados.

GuppyLM usa um tokenizador simples em nível de caractere. Modelos de produção como o GPT-4 usam BPE (codificação de par de bytes) com vocabulários de 50K-100K tokens.

Camada de Embedding

A camada de embedding converte IDs de token em vetores densos. Cada token recebe um vetor aprendido (por exemplo, 384 dimensões no GuppyLM). Esses vetores carregam significado semântico: tokens semelhantes acabam próximos no espaço vetorial.

Embeddings de posição são adicionados por cima, para que o modelo conheça a ordem dos tokens.

Blocos Transformer

Esta é a computação central. Cada bloco tem duas partes:

Autoatenção: permite que cada token observe todos os outros tokens na sequência e decida quais importam para prever o próximo token. O GuppyLM usa 6 cabeças de atenção em 6 camadas.

Rede neural feed-forward: um MLP de duas camadas aplicado à representação de cada token após a atenção. O GuppyLM usa ativação ReLU, que é mais simples do que o SwiGLU usado em arquiteturas mais recentes.

Camada de Saída

Após o bloco transformer final, uma camada linear projeta a representação de cada token para um vetor de tamanho igual ao vocabulário. Aplica-se softmax para obter probabilidades, escolhe-se o próximo token mais provável (ou amostra-se), e repete-se.

Construindo um LLM mínimo em Python

Aqui está um LLM mínimo funcional baseado na abordagem do GuppyLM. Isso roda em PyTorch padrão.

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

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 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}")

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:]  # 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()

O que isso ensina sobre o comportamento da API de IA

Construir isso revela várias coisas que o tornam um consumidor de API melhor.

Temperatura e amostragem são mecânicas, não mágicas

A temperatura divide os logits antes do softmax. Temperatura mais alta = distribuição mais plana = saída mais aleatória. Temperatura mais baixa = distribuição mais nítida = saída mais determinística. Quando sua API de produção retorna resultados inconsistentes com temperature=0.0, não é um bug. A temperatura zero verdadeira é um argmax ganancioso, e muitas APIs a "arredondam" ligeiramente para evitar saídas degeneradas.

Janelas de contexto são limites rígidos, não sugestões leves

A linha idx_cond = ids[:, -SEQ_LEN:] no loop de inferência mostra exatamente o que acontece no limite de contexto. O modelo descarta silenciosamente os tokens mais antigos. Se a sua integração de API assume que o modelo se lembra de todo o histórico da conversa, isso não acontece após um certo ponto. Veja [internal: how-ai-agent-memory-works] para saber como os agentes lidam com esse problema.

Tokens em streaming são apenas passos de inferência tornados visíveis

APIs de streaming não fazem nada arquitetonicamente diferente. Elas executam o loop de inferência e enviam cada token para o fluxo de resposta à medida que é gerado. Compreender isso ajuda ao escrever a lógica de nova tentativa: um fluxo interrompido no meio da geração não pode ser retomado, precisa recomeçar.

Logits explicam por que a saída estruturada é difícil

O modelo atribui probabilidade a cada token no vocabulário a cada passo. Gerar JSON válido exige que o token correto vença em cada posição. Bibliotecas como Outlines e Guidance restringem a distribuição de logits para impor a gramática no momento da inferência. Quando você vê APIs de IA oferecendo modos de "saída estruturada", é isso que elas estão fazendo internamente.

Como testar integrações de API de IA com o Apidog

Uma vez que você entende como a inferência de LLM funciona, você pode escrever testes de API muito melhores. Os Cenários de Teste do Apidog permitem encadear chamadas de API e fazer asserções sobre a estrutura das respostas de IA.

Por exemplo, ao testar uma API de chat em streaming:

  1. Crie um Cenário de Teste no Apidog com seu endpoint /v1/chat/completions
  2. Defina asserções para verificar a estrutura da resposta: response.choices[0].finish_reason == "stop", response.usage.total_tokens < 4096
  3. Adicione uma etapa de acompanhamento que envia a resposta como contexto para a próxima rodada, simulando uma conversa de várias rodadas
  4. Use o Smart Mock do Apidog para simular o endpoint de IA e testar o tratamento de erros do seu aplicativo: simule finish_reason: "length" (saída truncada), finish_reason: "content_filter" e tempo limite de rede no meio do streaming

É assim que você testa integrações de IA sem gastar créditos de API em cada execução de CI. Consulte [internal: api-testing-tutorial] para uma visão mais ampla das abordagens de teste de API.

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 vários 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 que cheguem à produção.

Avançado: quantização e otimização de inferência

Uma vez que você tem um LLM minúsculo funcionando, duas técnicas valem a pena entender porque se aplicam diretamente a como os modelos de produção são servidos.

Quantização

Os pesos em nosso modelo são floats de 32 bits por padrão. A quantização os reduz para inteiros de 8 bits (INT8) ou mesmo 4 bits (INT4). Isso reduz o uso de memória em 4 a 8 vezes com uma perda modesta 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 de produção executam modelos quantizados. Quando você vê diferentes qualidades de saída em diferentes "versões" do mesmo modelo, a quantização está frequentemente envolvida.

Cache KV

Em nosso loop de inferência, recalculamos a atenção em toda a sequência a cada passo. Sistemas de produção armazenam em cache os pares chave-valor de tokens anteriores (o cache KV) para que cada novo token precise de apenas uma nova computação de atenção. É por isso que o primeiro token em uma resposta em streaming leva mais tempo do que os subsequentes.

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 resposta real para a maioria dos desenvolvedores: use a API de produção para seu aplicativo, mas execute um modelo minúsculo para entender o que está acontecendo "sob o capô". Os dois não estão competindo. O artigo [internal: open-source-coding-assistants-2026] aborda ferramentas que misturam essa linha com configurações de "traga seu próprio modelo".

Conclusão

Construir um LLM minúsculo do zero leva um fim de semana. O que você obtém não é um sistema de produção; é um modelo mental funcional de como todo modelo de linguagem, do GuppyLM ao GPT-4o, realmente funciona. Essa compreensão compensa toda vez que você depura uma integração de streaming, ajusta parâmetros de amostragem ou projeta asserções para seus testes de API de IA.

O projeto GuppyLM é um bom ponto de partida. Clone-o, treine-o em qualquer conjunto de dados de texto e passe uma tarde lendo o loop de inferência. Então, volte para suas integrações de API de produção e você as verá de forma diferente.

Experimente os Cenários de Teste do Apidog para trazer o mesmo rigor aos seus testes de API de IA que você aplicaria a qualquer outro sistema de backend.

botão

FAQ

Quantos parâmetros um LLM "minúsculo" precisa para gerar texto coerente?Cerca de 10M-50M de parâmetros com um conjunto de dados de treinamento decente podem produzir frases localmente coerentes. Abaixo de 1M, você obtém "gibberish" na maioria das tarefas. O GuppyLM, com 8.7M, funciona para conversas curtas em seu domínio de treinamento (60 tópicos).

Posso executar um LLM minúsculo sem uma GPU?Sim. Modelos com menos de 100M de parâmetros funcionam bem na CPU, embora a inferência seja mais lenta. O modelo acima (1.2M de parâmetros) gera tokens em milissegundos em uma CPU de laptop.

Em qual conjunto de dados devo treinar?Modelos em nível de caractere funcionam bem com textos do Projeto Gutenberg, subconjuntos da Wikipedia ou qualquer corpus de texto simples. O GuppyLM usa um conjunto de dados de conversas de 60K entradas no HuggingFace (arman-bd/guppylm-60k-generic). Para geração de código, use The Stack ou CodeParrot.

Qual a diferença entre temperatura e amostragem top-k?A temperatura dimensiona a distribuição de logits (controla a aleatoriedade geral). O top-k restringe o pool de amostragem aos k tokens mais prováveis antes de aplicar a temperatura. Eles são aplicados juntos: primeiro o top-k filtra os candidatos, depois a temperatura molda as probabilidades dentro desse conjunto.

Por que meu LLM às vezes se repete?A repetição é um modo de falha onde o modelo atribui alta probabilidade a tokens que acabou de gerar porque apareceram no contexto. APIs de produção usam penalidades de repetição (um ajuste de logits que desconta tokens gerados recentemente). Adicione repetition_penalty=1.1 em sua chamada de API para reduzir isso.

Quanto tempo leva para treinar um LLM minúsculo?O modelo acima treina para uma saída coerente em menos de 2 horas em uma única GPU (RTX 3060 ou equivalente). O GuppyLM treina no Colab aproximadamente no mesmo tempo. Modelos maiores (100M+) precisam de configurações multi-GPU e dias de treinamento.

Qual é a maneira mais rápida de transformar um LLM minúsculo em um endpoint de API real?Exporte para o formato GGUF usando o script de conversão do llama.cpp e, em seguida, sirva com o llama-server. Isso lhe dá um endpoint de API compatível com OpenAI rodando localmente. Você pode então apontar o Apidog para ele para testes, veja [internal: rest-api-best-practices].

Como os LLMs de produção lidam com contextos maiores do que sua janela de treinamento?Técnicas como RoPE (Rotary Position Embedding) com escalonamento estendido, atenção com janela deslizante e geração aumentada por recuperação (RAG) estendem o contexto efetivo. A arquitetura central do transformer não muda; são modificações na forma como as informações de posição são codificadas e como a janela de atenção é aplicada.

Pratique o design de API no Apidog

Descubra uma forma mais fácil de construir e usar APIs