Modelos de Código
O que são “modelos de código”?
Modelos de código (code models) são modelos de linguagem de grande porte (LLMs) adaptados para tarefas de programação: completar código, gerar funções a partir de docstrings, refatorar, escrever testes, corrigir bugs e navegar em repositórios com múltiplos arquivos. A maioria dos modelos de código modernos ainda se baseia na Arquitetura Transformer, mas eles diferem de modelos de chat de uso geral de três formas práticas:
- Eles são treinados e ajustados (tuned) com muito código-fonte e texto de desenvolvedores (issues, docs, Q&A, mensagens de commit).
- Eles oferecem suporte a padrões de geração centrados em código, especialmente edição (editing) e preenchimento de lacunas (infilling) (não apenas “continuar o próximo token”).
- Eles são avaliados com medições sensíveis a código, frequentemente envolvendo compilação e testes unitários em vez de pura similaridade textual.
Este artigo foca em três técnicas amplamente usadas que tornam os LLMs de código úteis em fluxos reais de desenvolvimento:
- Preenchimento no meio (fill-in-the-middle, FIM) para preenchimento de lacunas e edições
- Contexto em nível de repositório (repository-level context) para entendimento e mudanças em múltiplos arquivos
- Avaliação específica para código (code-specific evaluation) para medir correção funcional e utilidade para desenvolvedores
Por que código é diferente de linguagem natural (e por que isso importa)
Código compartilha semelhanças superficiais com texto (são sequências de tokens), mas tem propriedades que empurram modelos e ferramentas em direções diferentes:
- Restrições fortes de sintaxe e semântica: um único colchete ausente pode quebrar a compilação; um tipo errado pode quebrar o comportamento em tempo de execução.
- Dependências de longo alcance: uma chamada de função pode depender de um tipo definido milhares de linhas acima (ou em outro arquivo).
- Nomeação precisa e uso correto de APIs: a correção frequentemente depende de identificadores exatos e convenções de bibliotecas.
- Executabilidade: código pode ser executado, permitindo avaliação automatizada (testes, compilação, análise estática).
- Alto valor do contexto local: o código imediatamente ao redor (imports, membros de classe, nomes de variáveis) determina fortemente o que deveria vir a seguir.
Essas propriedades motivam objetivos de treinamento especializados (como FIM), construção de contexto (recuperação em nível de repositório) e métodos de avaliação (métricas baseadas em testes).
Fundamentos: objetivos de treinamento e tokenização para código
A maioria dos modelos de código começa a partir de um objetivo de modelagem de linguagem causal (causal language modeling) (“prever o próximo token”), mas código se beneficia de objetivos adicionais e pré-processamento.
Considerações de tokenização
A tokenização (tokenization) (veja Tokenização) afeta o quão eficientemente um modelo consegue representar identificadores, linguagens sensíveis a whitespace e padrões comuns de sintaxe.
Realidades comuns em código:
- Identificadores divididos em subtokens (
get_user_id→get,_,user,_,id), o que pode ajudar a generalizar entre estilos de nomeação, mas pode prejudicar a fidelidade de correspondência exata. - Espaços em branco e indentação importam para linguagens como Python.
- Tokens não ingleses aparecem (identificadores Unicode, comentários em muitos idiomas).
Alguns ecossistemas de modelos de código usam vocabulários especializados ou tokens adicionais para coisas como indentação e marcadores de preenchimento de lacunas (discutidos abaixo).
Além de “próximo token”: por que objetivos importam
Se você treinar apenas “prever o próximo token”, você obtém um autocompletar (autocomplete) forte, mas edição mais fraca. Fluxos reais de trabalho de desenvolvedores frequentemente se parecem com:
- “Insira código aqui, mantendo o que está abaixo intacto”
- “Substitua este bloco por uma versão mais segura”
- “Corrija este teste falhando sem reescrever o arquivo”
É aí que o preenchimento no meio se torna uma técnica central.
Preenchimento no meio (FIM)
O que é FIM
Preenchimento no meio treina (e aplica via prompt) um modelo para gerar um trecho ausente entre um prefixo e um sufixo, em vez de apenas continuar a partir de um prefixo.
Conceitualmente, em vez de modelar:
- Dado
prefix, gerarcontinuation
FIM modela:
- Dado
prefixesuffix, gerarmiddlede modo queprefix + middle + suffixseja coerente e correto.
Isso se alinha de perto com tarefas de edição: o desenvolvedor já tem código acima e abaixo do cursor e quer que o modelo “preencha o espaço”.
Como FIM é implementado (treinamento e prompting)
Uma receita comum de treinamento para FIM pega um arquivo de código, seleciona um trecho contíguo como middle e reorganiza a sequência com tokens especiais:
<fim_prefix>prefix<fim_suffix>suffix<fim_middle>middle (target)
Os tokens exatos variam por família de modelos, mas a ideia é consistente: o modelo aprende a condicionar em ambos os lados de um “buraco”.
Na prática, ferramentas de IDE também aplicam FIM em tempo de inferência ao empacotar o que está acima do cursor como prefixo e o que está abaixo como sufixo.
Exemplo prático: preenchendo o corpo de uma função
Suponha que você tenha um arquivo Python com uma assinatura de função e um esqueleto de return:
def normalize_email(email: str) -> str:
# TODO: normalize
return email
Um prompt no estilo FIM conceitualmente fornece ambos os lados:
- Prefixo: até
# TODO: normalize - Sufixo:
return emaile tudo depois
O modelo gera o meio:
def normalize_email(email: str) -> str:
email = email.strip()
local, _, domain = email.partition("@")
return f"{local.lower()}@{domain.lower()}"
Por que isso é melhor do que completar puramente da esquerda para a direita:
- O modelo consegue ver que deve terminar em um corpo de função válido que flui para o sufixo.
- Ele pode evitar gerar um
returnextra se o sufixo já tiver um (ou decidir modificá-lo, dependendo da interface de edição).
FIM para edições de código e patches
FIM também é útil para edição do tipo “substitua este bloco”. Muitas ferramentas representam uma edição como:
- Prefixo: código antes da região
- Sufixo: código depois da região
- Meio: a substituição
Isso se parece com aplicar um patch sem forçar o modelo a regenerar o arquivo inteiro (o que reduz deriva e mudanças alucinadas).
Benefícios e trade-offs
Benefícios
- Melhor completamento baseado no cursor e edições
- Integração mais estável com o código existente (menos reescrita)
- Funciona bem com modificações “cirúrgicas”
Trade-offs
- Exige formatação cuidadosa de prompts e tokens especiais (dependente do modelo)
- Sufixos grandes consomem a janela de contexto (context window) (veja Janelas de Contexto)
- Lacunas ambíguas podem levar a preenchimentos plausíveis porém errados, a menos que restrições adicionais (tipos/testes) sejam fornecidas
Contexto em nível de repositório
Por que contexto de arquivo único não é suficiente
Muitas tarefas reais exigem entender e modificar código entre arquivos:
- Adicionar uma funcionalidade que toca definições de API, implementação e testes
- Corrigir um bug causado por uma incompatibilidade sutil de contrato entre módulos
- Atualizar pontos de chamada após refatorar a assinatura de uma função
Um modelo de código limitado a um arquivo frequentemente “alucina” definições ausentes ou usa a superfície de API errada — uma instância mais ampla de Alucinações, mas com consequências especialmente severas em código.
O que significa “contexto em nível de repositório”
Contexto em nível de repositório é o conjunto de sinais e artefatos de uma base de código que ajuda o modelo a gerar mudanças corretas:
- Arquivos-fonte relacionados (definições, interfaces, utilitários)
- Configuração de build (dependências, layout de módulos)
- Testes e fixtures
- README / docs / comentários
- Símbolos e referências (onde funções/tipos são definidos e usados)
- Commits recentes ou descrições de issues (em alguns fluxos)
Como repositórios podem ser muito maiores do que a janela de contexto de um LLM, normalmente você precisa de seleção, não apenas “incluir tudo”.
Abordagens comuns para contexto de repositório
1) Recuperação sobre chunks de código (estilo RAG para código)
Um padrão prático:
- Indexe o repositório dividindo arquivos em chunks (por exemplo, 200–400 linhas ou chunks baseados em AST).
- Calcule incorporações (embeddings) para cada chunk.
- No momento da consulta, gere a incorporação da solicitação do usuário e recupere os
kchunks mais relevantes. - Forneça-os ao modelo como contexto.
Isso frequentemente é combinado com heurísticas sensíveis a símbolos (abaixo). Embora “recuperação” (retrieval) seja um tema mais amplo, ela é especialmente importante para código porque definições exatas importam (assinaturas de função, tipos, invariantes).
2) Indexação de símbolos (definições e referências)
Em vez de (ou além de) incorporações, construa um índice de:
- Definições de funções/classes/tipos
- Onde cada símbolo é referenciado
- Grafos de importação e relações entre módulos
Isso pode ser tão simples quanto uma abordagem baseada em grep ou tão sofisticado quanto um índice de servidor de linguagem (Language Server Protocol, LSP).
Isso ajuda a responder perguntas como:
- “Onde
UserRepository.save()está definido?” - “Quais são os pontos de chamada de
parse_config()?”
3) Contexto estrutural: AST e grafos de chamadas
Código é naturalmente estruturado. Ferramentas podem extrair:
- Árvores sintáticas abstratas (abstract syntax trees, ASTs)
- Grafos de chamadas (call graphs)
- Grafos de dependência (dependency graphs)
Então selecionar contexto com base em relações estruturais em vez de pura similaridade textual. Por exemplo, ao modificar foo(), incluir:
- Sua definição
- Callees e callers diretos
- Tipos e constantes relevantes
Isso frequentemente melhora a precisão em comparação com recuperação baseada apenas em incorporações.
4) Sumarização e contexto hierárquico
Quando arquivos são grandes demais, você pode:
- Incluir os trechos mais relevantes literalmente
- Fornecer sumários de módulos menos críticos (gerados ou pré-computados)
- Usar uma abordagem “map-reduce” (map-reduce): sumarizar submódulos e, depois, sumarizar os sumários
Isso é especialmente útil para modelos de contexto longo, mas ainda limitado por Janelas de Contexto.
Exemplo prático: fazendo uma mudança ciente do repositório
Tarefa: “Adicione um parâmetro timeout ao cliente HTTP e garanta que todos os pontos de chamada o repassem.”
Um fluxo ciente do repositório poderia:
- Recuperar a definição da classe
HttpClient. - Recuperar o parsing de configuração (
Config, variáveis de ambiente). - Recuperar usos de
HttpClient.get(...)em todos os serviços. - Recuperar testes que cobrem timeouts.
Então o modelo pode gerar um diff coerente multi-arquivo: atualizar a assinatura, propagar o parâmetro e atualizar testes.
Sem contexto de repositório, o modelo pode:
- Inventar uma configuração de
timeoutque não existe - Deixar de atualizar pontos de chamada
- Usar convenções da biblioteca HTTP errada
Integração com ferramentas: o modelo não é o sistema inteiro
O contexto em nível de repositório normalmente é implementado por um sistema ao redor do modelo, não apenas pelo próprio modelo:
- Um plugin de IDE reúne buffers abertos e contexto do cursor
- Um serviço de backend indexa o repositório
- Uma camada de recuperação seleciona chunks relevantes
- O LLM recebe um prompt curado
- Um verificador executa formatadores/testes e retorna feedback
Esse padrão “LLM + ferramentas” se sobrepõe a LLMs com Uso de Ferramentas e é cada vez mais o padrão para assistentes de código sérios.
Riscos e boas práticas
- Envenenamento de contexto (context poisoning): trechos de código irrelevantes ou maliciosos podem direcionar as saídas. A seleção de contexto deve ser conservadora e consciente da proveniência (provenance).
- Vazamento (leakage) e privacidade: repositórios podem conter segredos. Garanta varredura de segredos e manuseio estrito de dados.
- Obsolescência (staleness): o contexto recuperado deve corresponder à árvore de trabalho atual (incluindo edições não commitadas).
- Excesso de contexto (overstuffing): adicionar contexto demais pode degradar a performance; a qualidade da seleção importa mais do que a quantidade.
Avaliação específica para código
Avaliar modelos de código é fundamentalmente diferente de avaliar a qualidade de chat.
Por que similaridade textual não é suficiente
Dois trechos de código podem ser textualmente diferentes, mas funcionalmente equivalentes. Por outro lado, um trecho pode parecer plausível, mas falhar na compilação ou nos testes.
Portanto, a avaliação de código frequentemente enfatiza:
- Corretude executável
- Sucesso de compilação
- Comportamento sob testes
- Corretude e integração de API
Métodos centrais de avaliação
1) Pass@k (correção funcional baseada em amostragem)
Uma métrica comum em benchmarks de programação:
- Gerar
ksoluções candidatas (via amostragem por temperatura/top-p (temperature/top-p sampling); veja Estratégias de Decodificação) - Executar testes unitários para cada candidata
pass@kestima a probabilidade de que ao menos uma daskamostras passe
Isso reflete o uso real: desenvolvedores frequentemente aceitam/iteram sobre múltiplas sugestões.
2) Execução de testes unitários e execução em sandbox
O sinal mais forte é: passa nos testes?
Requisitos práticos:
- Execução em sandbox (sandboxing) segura (execução de código não confiável)
- Limites de recursos (tempo/memória)
- Controles de determinismo quando possível
Ressalva: testes podem ser incompletos; passar em testes não é prova de correção.
3) Taxa de sucesso de compilação / verificação de tipos
Para linguagens compiladas ou tipadas (Java, Rust, TypeScript), métricas úteis incluem:
- Faz parsing com sucesso
- Compila com sucesso
- Passa na verificação de tipos (type-check) sem erros
Elas se correlacionam com utilidade para desenvolvedores mesmo quando a correção semântica completa é mais difícil de julgar.
4) Métricas de análise estática e lint
Execute ferramentas como verificadores de estilo (linters), formatadores (formatters) e analisadores estáticos (static analyzers):
- Conformidade de estilo (por exemplo, formatação)
- Padrões comuns de bugs (variáveis não usadas, código inalcançável)
- Checagens de segurança (problemas de taint (taint issues), APIs inseguras (unsafe APIs))
Isso é especialmente útil em cenários de “edição” nos quais você quer evitar regressões.
5) Correspondência exata e métricas de similaridade de código (limitadas, mas comuns)
Alguns conjuntos de dados ainda usam:
- Correspondência exata (exact match)
- Métricas do tipo BLEU (BLEU-like)
- CodeBLEU (misturando heurísticas de sintaxe e fluxo de dados)
Elas são fáceis de calcular, mas se correlacionam de forma imperfeita com a correção.
Benchmarks e famílias de tarefas (representativas)
Embora o panorama de benchmarks mude rapidamente, categorias comuns incluem:
- Síntese algorítmica de funções (por exemplo, tarefas no estilo HumanEval/MBPP): gerar uma função que passa em testes ocultos.
- Programação competitiva / síntese mais difícil (estilo APPS): soluções mais longas, mais casos de borda.
- Correção de bugs e geração de PRs (estilo SWE-bench): produzir patches que fazem os testes de projetos reais passarem.
- Raciocínio em nível de repositório (estilo RepoBench): tarefas que exigem contexto multi-arquivo.
- Completar código (code completion): medir acurácia de tokens ou distância de edição sob configurações realistas de cursor (frequentemente internas/baseadas em telemetria de IDE).
Evitando armadilhas de avaliação
- Contaminação de dados: benchmarks de código podem vazar para corpora de treinamento (duplicação de código aberto é onipresente). Avaliações robustas rastreiam proveniência e usam descontaminação.
- Overfitting aos testes: modelos (ou pipelines) podem “jogar” com testes fracos. Use testes ocultos diversos e casos adversariais.
- Configurações irreais: avaliar sem contexto de repositório pode subestimar ferramentas reais que usam recuperação; por outro lado, avaliar com contexto oráculo pode superestimar performance no mundo real.
Juntando tudo: aplicações típicas de modelos de código
Autocomplete na IDE e edições inline (forte em FIM)
- Completar baseado no cursor
- “Preencha este bloco” ou “implemente este stub”
- Pequenas refatorações (“extrair função”, “renomear símbolo” com suporte de ferramentas)
FIM brilha aqui porque o usuário já tem o código ao redor e quer mínima perturbação.
Mudanças multi-arquivo e trabalho de funcionalidades (contexto de repositório + ferramentas)
- Adicionar uma funcionalidade entre API, implementação e testes
- Migrar uma versão de biblioteca
- Implementar um novo endpoint e atualizar clientes
Contexto em nível de repositório e integração com ferramentas (busca, indexação, execução de testes) dominam.
PRs automatizados e correção de bugs (loops guiados por avaliação)
Um padrão comum é um loop iterativo:
- Propor um patch
- Rodar testes / verificação de tipos
- Alimentar as falhas de volta ao modelo
- Refinar o patch
Isso se conecta de perto com iteração em tempo de inferência (inference-time iteration) e Cômputo em Tempo de Teste: gastar mais computação (mais tentativas, checagens mais profundas) para melhorar a correção.
Dicas práticas para usar modelos de código de forma eficaz
- Prefira interfaces de preenchimento/edição ao modificar código existente. Elas reduzem reescritas acidentais.
- Forneça restrições concretas: assinaturas de função, comportamento esperado, exemplos e casos de borda.
- Inclua a saída de testes falhando ao depurar. Stack traces ancoram o modelo na realidade.
- Peça diffs mínimos: “Mude apenas o necessário; mantenha APIs públicas estáveis.”
- Use decodificação determinística (temperatura mais baixa) para refatorações e edições; use amostragem (temperatura mais alta, múltiplos candidatos) para síntese e depois valide com testes.
- Trate saídas como hipóteses: compile, verifique tipos e rode testes. Modelos de código são poderosos, mas não são autoridade.
Limitações e problemas em aberto
- Confiabilidade semântica: modelos ainda produzem lógica plausível porém incorreta, especialmente com requisitos subespecificados.
- Escala de repositório: mesmo com contextos longos, a maioria dos repositórios excede as janelas de contexto disponíveis; a qualidade da recuperação é crítica.
- Dependência de ferramentas: os melhores sistemas são “LLM + indexador + verificador”; o modelo sozinho raramente é suficiente para mudanças em nível de produção.
- Segurança e licenciamento: proveniência de dados de treinamento, tratamento de segredos e conformidade com políticas continuam sendo preocupações em andamento.
Resumo
Modelos de código são LLMs otimizados para fluxos de programação em que correção e integração com o código existente importam. Três técnicas são centrais:
- Preenchimento no meio (FIM) habilita forte preenchimento de lacunas e comportamento de edição ao condicionar tanto em prefixo quanto em sufixo.
- Contexto em nível de repositório torna modelos úteis além de tarefas de arquivo único via recuperação, indexação de símbolos e seleção estrutural.
- Avaliação específica para código mede o que desenvolvedores valorizam — compilação, testes e aplicabilidade de patches — em vez de apenas similaridade textual.
Juntas, essas técnicas transformam preditores genéricos de próximo token em sistemas práticos que conseguem autocompletar, refatorar e ajudar a manter projetos reais de software — especialmente quando combinadas com elaboração cuidadosa de prompts (prompting), uso de ferramentas (tool use) e validação executável (executable validation).