O Aider é um passo à frente e tanto na hora de integrar modelos de linguagem de grande porte (LLMs) no desenvolvimento de software.
Essa ferramenta de linha de comando vai além do que a gente tá acostumado a ver, trazendo um sistema bem inteligente que mistura análise estática de código, gerenciamento de contexto adaptativo e integração com ferramentas modernas como GitHub Actions e SonarQube.
Feito pra turbinar a produtividade dos devs, ele tem uma arquitetura que junta análise de código profunda, gerenciamento esperto de contexto e uma validação bem rigorosa das mudanças no código.
Tudo isso ajuda a ferramenta a detectar problemas, entender o contexto do código e prever o impacto das alterações.
Neste artigo, vou explorar como essa mágica acontece, mostrando a arquitetura interna, os algoritmos que ele usa e como ele se integra com outras ferramentas.
Ao longo dessa análise, vou mostrar como tudo funciona junto pra criar um sistema que dá um gás no desenvolvimento de software com ajuda da inteligência artificial.
Sumário
- 1. Introdução ao Aider
- 2. Análise de Código e Integração com LLMs
- 3. Gerenciamento de Janela de Contexto
- 4. Integração com CI/CD e Qualidade de Código
- 5. Processamento e Validação de Edições
- 6. Conclusão
1. Introdução ao Aider
O Aider é uma evolução na junção de inteligência artificial e engenharia de software, indo além das limitações das ferramentas tradicionais de ajuda na programação.
Feito como uma aplicação Python modular e expansível, ele traz vários subsistemas especializados que trabalham juntos pra oferecer uma experiência de desenvolvimento com suporte de IA bem integrada.
No coração da arquitetura tá a classe Coder
, que gerencia a interação entre os diferentes componentes do sistema.
Ela funciona como um mediador, cuidando da comunicação entre a interface de entrada/saída, o modelo de linguagem (LM), o sistema de análise de código e o gerenciador de repositório, simplificando a interação entre essas partes.
A implementação da classe Coder
mostra o quanto a arquitetura é bem pensada. A implementação da classe Coder
revela a sofisticação da arquitetura:
class Coder:
def __init__(self, io: InputOutput, model: Model, edit_format: str = "whole"):
self.io = io # Cuida da interação com o usuário e sistema de arquivos
self.model = model # Gerencia a comunicação com o LLM
self.edit_format = edit_format # Define o formato de edição (whole, diff, etc.)
self.repo_map = RepoMap(io=io, root=io.root) # Mantém um mapa do repositório
self.chunks = ChatChunks() # Controla a janela de contexto
self.linter = Linter(encoding="utf-8", root=io.root) # Valida as mudanças no código
O Aider se diferencia das ferramentas de análise estática tradicionais, como ESLint ou Pylint, por usar uma abordagem híbrida.
Enquanto essas ferramentas seguem regras fixas pra encontrar problemas, o Aider combina a análise estrutural do código com o Tree-Sitter e a compreensão semântica dos LLMs.
Isso permite não só identificar problemas, mas também entender o contexto do código e prever o impacto das mudanças.
O Tree-Sitter é essencial aqui, pois cria árvores sintáticas (CSTs e ASTs) pra várias linguagens de programação.
Essas estruturas capturam a gramática do código, permitindo uma análise e navegação precisa. Ele usa um algoritmo de parsing GLR, que é bem robusto e lida bem com erros, além de ser eficiente pra ambientes de desenvolvimento interativos.
Já o LiteLLM serve como uma camada que facilita a comunicação com diferentes provedores de LLMs, usando um padrão adapter que normaliza as interfaces.
Isso dá uma flexibilidade enorme, permitindo que o Aider se adapte a vários modelos sem precisar mudar muito o código. Além disso, o carregamento preguiçoso (lazy loading) do LiteLLM ajuda a melhorar o tempo de inicialização, o que é ótimo pra experiência do usuário.
O ChatChunks é outra inovação importante, cuidando da janela de contexto dos LLMs. Ele usa um algoritmo inteligente pra priorizar e comprimir informações, maximizando o uso do espaço limitado de tokens.
O contexto é organizado em categorias (sistema, exemplos, histórico, arquivos, etc.), o que ajuda a focar nas informações mais relevantes pra tarefa em questão.
Na imagem acima, dá pra ver a arquitetura com essas características.
Organização em Camadas: A arquitetura é dividida em camadas bem definidas, separando a interface do usuário, o núcleo da aplicação, a análise de código, a edição de código e as integrações externas. Isso facilita a manutenção e a escalabilidade.
-
Componentes Principais:
- Coder: O coração do sistema, que coordena todas as operações.
- ChatChunks: Cuida da janela de contexto, priorizando e comprimindo informações.
- RepoMap: Analisa e indexa o código, criando um mapa do repositório.
- Diferentes Coders: Implementações específicas pra cada formato de edição (whole, diff, etc.).
Fluxo de Dados: As setas mostram como as informações fluem entre os componentes. Um comando do usuário é processado, analisado e transformado em edições de código, com cada parte do sistema fazendo sua parte.
Integrações Externas: O sistema se conecta com LLMs através do LiteLLM e se integra com ferramentas de controle de versão e CI/CD, como GitHub Actions.
Codificação por Cores: Cada subsistema tem uma cor diferente, o que ajuda a visualizar e entender a arquitetura de forma mais clara.
2. Análise de Código e Integração com LLMs
A ferramenta consegue entender e modificar código de forma inteligente graças à sua infraestrutura avançada de análise sintática e semântica, combinada com modelos de linguagem poderosos. Vamos explorar como isso funciona.
2.1 Análise Sintática com Tree-Sitter
O Aider usa o Tree-Sitter, uma biblioteca de parsing que cria árvores sintáticas completas do código-fonte. Essas árvores fornecem uma visão estruturada do código, o que facilita a identificação de padrões, erros e dependências.
A integração é feita através do módulo grep_ast, que expande as funcionalidades do Tree-Sitter, permitindo uma análise contextual mais avançada e a extração de informações estruturais.
A classe RepoMap
é responsável por encapsular a análise sintática, com métodos pra construir e consultar essas representações do código. Ela é essencial pra entender a estrutura do código e garantir que as mudanças sejam feitas de forma precisa. Poir exemplo:
class RepoMap:
def get_tree_context(self, fname: str, code: Optional[str] = None) -> Optional[TreeContext]:
"""Constrói um contexto de árvore sintática para o arquivo especificado."""
if not code:
code = self.io.read_text(fname)
if not code:
return None
try:
# Configuração detalhada do contexto de análise
context = TreeContext(
fname,
code,
color=False, # Desativa coloração para processamento programático
line_number=True, # Inclui números de linha para referência precisa
child_context=False, # Omite contexto de nós filhos para reduzir verbosidade
last_line=False, # Não inclui última linha como contexto adicional
margin=0, # Sem margem adicional ao redor dos nós
mark_lois=True, # Marca linhas de interesse para priorização
loi_pad=3, # Adiciona 3 linhas de contexto ao redor das LOIs
show_top_of_file_parent_scope=False, # Omite escopo de arquivo completo
)
return context
except ValueError:
# Falha graciosamente se o parsing não for possível
return None
O TreeContext
gerado encapsula uma árvore sintática completa do código-fonte, enriquecida com metadados como números de linha, escopo de símbolos e relações hierárquicas. Essa estrutura de dados avançada permite que o sistema realize operações complexas, como:
- Extração de Símbolos: Identifica funções, classes, métodos, variáveis e suas definições, incluindo escopo e visibilidade.
- Análise de Dependências: Mapeia as relações entre diferentes partes do código, como chamadas de função, importações, herança e composição.
- Contextualização Semântica: Entende o papel e o significado de cada elemento no contexto geral do programa, essencial para fazer mudanças que façam sentido.
- Validação Estrutural: Verifica se o código continua válido após as modificações, garantindo que a árvore sintática resultante esteja correta.
O algoritmo de parsing do Tree-Sitter usa uma variante do GLR (Generalized LR), que traz várias vantagens para análise de código em um ambiente interativo:
- Parsing Incremental: Reanalisa apenas as partes do código que foram alteradas, o que é ótimo pra arquivos grandes.
- Tolerância a Erros: Consegue construir uma árvore sintática mesmo com erros no código, o que é crucial durante o desenvolvimento.
- Suporte Multi-linguagem: Usa gramáticas intercambiáveis, suportando várias linguagens de programação com a mesma infraestrutura.
O Tree-Sitter também é complementado por um sistema de cache que armazena árvores sintáticas já analisadas, melhorando o desempenho em sessões longas. Esse cache é invalidado seletivamente quando arquivos são modificados, mantendo um equilíbrio entre eficiência e precisão.
2.2 Integração com LiteLLM
O sistema se integra com modelos de linguagem através do LiteLLM, uma biblioteca que oferece uma interface unificada para vários provedores de LLMs.
A implementação usa um padrão de design proxy com carregamento preguiçoso pra otimizar o desempenho:
class LazyLiteLLM:
"""Proxy com carregamento preguiçoso para o módulo LiteLLM."""
_lazy_module = None
def __getattr__(self, name: str) -> Any:
"""Carrega o módulo só quando necessário."""
if name == "_lazy_module":
return super().__getattr__(name)
# Carrega o módulo na primeira vez que é usado
if self._lazy_module is None:
self._load_litellm()
# Delega o acesso ao módulo carregado
return getattr(self._lazy_module, name)
def _load_litellm(self) -> None:
"""Carrega o módulo LiteLLM e configura parâmetros."""
self._lazy_module = importlib.import_module("litellm")
# Configurações pra melhorar desempenho e reduzir logs desnecessários
self._lazy_module.suppress_debug_info = True
self._lazy_module.set_verbose = False
self._lazy_module.drop_params = True
self._lazy_module._logging._disable_debugging()
Essa implementação traz vários benefícios:
- Inicialização Otimizada: O carregamento preguiçoso reduz o tempo de inicialização, já que o módulo só é carregado quando realmente necessário.
- Abstração de Provedores: A interface unificada do LiteLLM permite usar diferentes LLMs (OpenAI, Anthropic, Cohere, etc.) sem precisar mudar o código base.
- Gerenciamento de Falhas: O LiteLLM tem mecanismos robustos de retry e fallback, essenciais pra manter a confiabilidade em sistemas que dependem de serviços externos.
- Normalização de Respostas: As diferentes APIs de LLMs são padronizadas em um formato consistente, simplificando o processamento.
A comunicação com os LLMs é gerenciada pela classe Model
, que cuida do envio de mensagens, processamento de respostas e tratamento de erros:
class Model:
def __init__(self, name: str, api_key: Optional[str] = None, **kwargs):
self.name = name
self.api_key = api_key
self.settings = ModelSettings.for_model(name, **kwargs)
self.token_count = self._get_token_counter()
def send_with_retries(self, messages: List[Dict], stream: bool = False) -> Union[str, Generator]:
"""Envia mensagens ao LLM com retry exponencial."""
retry_count = 0
max_retries = 5
while True:
try:
return self._send(messages, stream)
except RateLimitError as e:
retry_count += 1
if retry_count > max_retries:
raise
# Espera exponencial com jitter
delay = (2 ** retry_count) + random.uniform(0, 1)
time.sleep(min(delay, 60)) # Máximo de 60 segundos
O sistema de comunicação com LLMs implementa padrões avançados de resiliência:
- Retry Exponencial: Usa backoff exponencial com jitter pra lidar com erros transitórios e limites de taxa.
- Circuit Breaker: Detecta falhas persistentes e evita sobrecarregar serviços que estão indisponíveis.
- Timeout Adaptativo: Ajusta os timeouts com base no tamanho do prompt e na complexidade da tarefa.
- Streaming Eficiente: Processa respostas em streaming pra dar feedback em tempo real.
2.3 Fluxo de Análise e Edição
O processo de análise e edição segue um pipeline que integra análise sintática, processamento de linguagem natural e validação de código. Esse pipeline funciona em etapas: análise, contextualização, geração de sugestões, validação e aplicação.
-
Análise Inicial: O código-fonte é processado pelo Tree-Sitter pra gerar uma representação estrutural completa.
- Usa um algoritmo de parsing GLR otimizado pra código-fonte, criando uma árvore sintática que captura a estrutura gramatical do programa.
- A árvore é enriquecida com metadados como escopo de símbolos, dependências e informações de tipo.
-
Contextualização para o LLM: A representação estrutural é transformada em um formato textual otimizado pro LLM.
- Usa técnicas avançadas de serialização de árvores sintáticas, mantendo informações importantes e minimizando o uso de tokens.
- O algoritmo prioriza elementos relevantes pra tarefa atual, removendo detalhes desnecessários.
-
Geração de Edições: O LLM processa o contexto e gera sugestões de edição em um formato estruturado.
- Usa um protocolo de comunicação especializado pra guiar o LLM a produzir modificações bem formatadas.
- O protocolo inclui exemplos few-shot e instruções específicas pra gerar edições no formato desejado.
-
Parsing e Validação: As edições sugeridas são analisadas e validadas antes de serem aplicadas.
- Usa parsers especializados pra diferentes formatos de edição, extraindo as modificações e verificando se são aplicáveis.
- A validação inclui checagem sintática, semântica e de integridade.
-
Aplicação de Edições: As modificações validadas são aplicadas ao código-fonte.
- Usa algoritmos especializados pra diferentes tipos de edição, garantindo precisão e preservação da formatação.
- Para edições complexas, usa algoritmos de diff e merge sofisticados pra minimizar conflitos.
O fluxo de análise e edição é implementado como uma máquina de estados, com transições bem definidas entre as fases. Isso permite um tratamento robusto de erros e recuperação de falhas em qualquer ponto do processo.
A classe Linter
é essencial na validação de edições, implementando verificações sintáticas específicas pra cada linguagem:
class Linter:
def lint(self, fname: str) -> Optional[List[Dict[str, Any]]]:
"""Executa verificação sintática no arquivo especificado."""
lang = filename_to_lang(fname)
if not lang or lang not in self.languages:
return None
# Delega pra linter específico da linguagem
return self.languages[lang](fname)
def py_lint(self, fname: str) -> List[Dict[str, Any]]:
"""Implementa linting específico pra Python."""
try:
# Compila o código pra verificar erros sintáticos
with open(fname, "r", encoding=self.encoding) as f:
code = f.read()
compile(code, fname, "exec")
return []
except SyntaxError as e:
# Formata o erro de sintaxe com detalhes
return [{
"line": e.lineno,
"column": e.offset,
"message": str(e),
"source": "python"
}]
O sistema de linting é extensível, permitindo a adição de verificadores específicos pra diferentes linguagens e frameworks.
A arquitetura modular facilita a integração de ferramentas de análise estática existentes, como ESLint, Pylint ou Clippy, enriquecendo o processo de validação com verificações específicas de domínio.
3. Gerenciamento de Janela de Contexto
Um dos maiores desafios técnicos em ferramentas baseadas em LLMs é gerenciar a janela de contexto de forma eficiente.
O sistema usa um algoritmo inteligente pra otimizar o uso do espaço limitado de tokens, maximizando a eficácia das interações e reduzindo custos computacionais e financeiros.
3.1 Estrutura ChatChunks
No coração do sistema de gerenciamento de contexto está a classe ChatChunks
, uma estrutura de dados que organiza o contexto em categorias funcionais.
Ela usa um padrão de design composite, tratando diferentes partes do contexto como uma hierarquia unificada:
@dataclass
class ChatChunks:
"""Estrutura hierárquica pra gerenciar a janela de contexto."""
system: List[Dict] = field(default_factory=list) # Instruções do sistema
examples: List[Dict] = field(default_factory=list) # Exemplos demonstrativos
done: List[Dict] = field(default_factory=list) # Histórico de conversas
repo: List[Dict] = field(default_factory=list) # Mapa do repositório
readonly_files: List[Dict] = field(default_factory=list) # Arquivos só pra leitura
chat_files: List[Dict] = field(default_factory=list) # Arquivos que podem ser editados
cur: List[Dict] = field(default_factory=list) # Mensagem atual
reminder: List[Dict] = field(default_factory=list) # Lembrete final
def all_messages(self) -> List[Dict]:
"""Junta todas as categorias na ordem certa."""
return (
self.system +
self.examples +
self.readonly_files +
self.repo +
self.done +
self.chat_files +
self.cur +
self.reminder
)
Essa estrutura hierárquica prioriza as informações, dando mais importância aos componentes que estão mais pro final da lista quando o contexto precisa ser reduzido.
A ordem de concatenação foi pensada pra maximizar a eficiência do LLM:
- Instruções do Sistema: Definem como o modelo deve se comportar, estabelecendo o tom e as capacidades esperadas.
- Exemplos Demonstrativos: Mostram exemplos de interação e respostas no formato esperado.
- Arquivos Somente Leitura: Fornecem contexto de referência que não deve ser alterado, como dependências ou configurações.
- Mapa do Repositório: Dá uma visão geral da estrutura do projeto, essencial pra entender o contexto.
- Histórico de Conversas: Mantém o contexto das discussões anteriores, garantindo continuidade.
- Arquivos Editáveis: Contém o código que pode ser modificado, foco principal da interação atual.
- Mensagem Atual: É a instrução ou pergunta do usuário que vai gerar a resposta.
- Lembrete Final: Reforça instruções importantes, especialmente sobre o formato da resposta.
Essa estrutura não é só uma forma de organizar as coisas, mas também um algoritmo inteligente de alocação de tokens, que garante que o contexto limitado seja usado da melhor forma possível.
3.2 Controle de Cache e Tokens
É implementado um sistema avançado de controle de cache para otimizar o uso de tokens e melhorar a eficiência das interações com o LLM.
Este sistema utiliza metadados especiais para indicar quais partes do contexto podem ser reutilizadas entre chamadas consecutivas:
def add_cache_control_headers(self) -> None:
"""Adiciona metadados de cache a componentes apropriados do contexto."""
if self.examples:
self.add_cache_control(self.examples)
else:
self.add_cache_control(self.system)
if self.repo:
# Marca tanto o mapa do repositório quanto arquivos somente leitura como cacheáveis
self.add_cache_control(self.repo)
else:
# Se não houver mapa, apenas os arquivos somente leitura são cacheáveis
self.add_cache_control(self.readonly_files)
# Arquivos de chat são sempre cacheáveis
self.add_cache_control(self.chat_files)
def add_cache_control(self, messages: List[Dict]) -> None:
"""Adiciona metadados de cache a uma mensagem específica."""
if not messages:
return
content = messages[-1]["content"]
if isinstance(content, str):
# Converte para formato estruturado
content = {
"type": "text",
"text": content,
}
# Adiciona diretiva de cache
content["cache_control"] = {"type": "ephemeral"}
messages[-1]["content"] = [content]
Este mecanismo de cache implementa uma variante do padrão de design memoization, onde resultados de computações caras (neste caso, processamento de contexto pelo LLM) são armazenados e reutilizados quando possível.
A implementação utiliza um sistema de marcação que identifica componentes "efêmeros" do contexto - aqueles que o LLM pode reconhecer como já processados em interações anteriores.
O controle de tokens é implementado através de um sistema sofisticado de contagem e alocação, que monitora continuamente o uso de tokens e ajusta dinamicamente o contexto quando necessário:
def ensure_messages_within_context_window(self, messages: List[Dict]) -> Union[List[Dict], str]:
"""Garante que as mensagens estejam dentro da janela de contexto do modelo."""
# Calcula tokens totais
total_tokens = sum(self.model.token_count(msg) for msg in messages)
# Verifica se excede o limite
if total_tokens > self.model.settings.context_window:
# Tenta reduzir o contexto
if self.num_exhausted_context_windows < self.max_context_window_attempts:
self.num_exhausted_context_windows += 1
self.io.tool_error(
"Excedeu janela de contexto, tentando reduzir. Retentando."
)
return "retry"
return messages
Quando o limite de tokens é excedido, é implementada uma estratégia de redução adaptativa que prioriza a preservação de informações críticas:
Resumo de Histórico: Condensa conversas anteriores em resumos concisos, preservando informações essenciais enquanto reduz drasticamente o uso de tokens.
Poda de Mapa: Reduz o tamanho do mapa do repositório, focando apenas nos componentes mais relevantes para a tarefa atual.
Truncamento Seletivo: Remove seletivamente partes menos relevantes do contexto, como exemplos detalhados ou arquivos periféricos.
Compressão de Conteúdo: Aplica técnicas de compressão semântica para reduzir o tamanho de componentes essenciais sem perder informações críticas.
Estas estratégias são aplicadas sequencialmente até que o contexto se encaixe na janela disponível, priorizando as informações mais relevantes.
3.3 Resumo de Histórico
O sistema de resumo implementa uma solução elegante para o problema de contexto crescente em conversas prolongadas.
Ele usa o LLM para criar resumos semanticamente ricos que preservam informações críticas enquanto reduzem drasticamente o uso de tokens:
class ChatSummary:
def __init__(self, models: Union[Model, List[Model]], max_tokens: int = 1024):
"""Inicializa o sistema de resumo com modelos e limite de tokens."""
if not models:
raise ValueError("Pelo menos um modelo deve ser fornecido")
self.models = models if isinstance(models, list) else [models]
self.max_tokens = max_tokens
self.token_count = self.models[0].token_count
def summarize(self, messages: List[Dict], depth: int = 0) -> List[Dict]:
"""Resume mensagens que excedem o limite de tokens."""
messages = self.summarize_real(messages)
# Garante que a última mensagem seja do assistente para manter fluxo natural
if messages and messages[-1]["role"] != "assistant":
messages.append({"role": "assistant", "content": "Ok."})
return messages
def summarize_real(self, messages: List[Dict], depth: int = 0) -> List[Dict]:
"""Implementação real do algoritmo de resumo."""
sized = self.tokenize(messages)
total = sum(tokens for tokens, _msg in sized)
# Se estiver dentro do limite e não for recursivo, retorna sem modificação
if total <= self.max_tokens and depth == 0:
return messages
# Implementa resumo recursivo para conversas muito longas
if depth > 3:
# Trunca brutalmente se atingir profundidade máxima de recursão
return [{"role": "user", "content": "Continuando nossa conversa anterior..."}]
# Divide mensagens em grupos para resumo parcial
midpoint = len(messages) // 2
first_half = messages[:midpoint]
second_half = messages[midpoint:]
# Aplica recursivamente o algoritmo de resumo a cada metade
if total > self.max_tokens * 2:
first_half = self.summarize_real(first_half, depth + 1)
second_half = self.summarize_real(second_half, depth + 1)
return first_half + second_half
# Gera resumo usando o LLM quando a divisão recursiva não é necessária
for model in self.models:
try:
# Constrói prompt de resumo com instruções específicas
summarize_messages = [
{"role": "system", "content": prompts.summarize},
{"role": "user", "content": format_messages
]
# Solicita resumo ao modelo
summary = model.simple_send_with_retries(summarize_messages)
if summary:
# Formata e retorna o resumo como uma única mensagem do usuário
summary = prompts.summary_prefix + summary
return [{"role": "user", "content": summary}]
except Exception as e:
# Falha graciosamente e tenta o próximo modelo
continue
# Se todos os modelos falharem, levanta exceção
raise ValueError("Falha inesperada em todos os modelos de resumo")
Este algoritmo implementa uma estratégia de divisão e conquista com características notáveis:
Compressão Semântica Adaptativa: Ao invés de simplesmente truncar mensagens antigas, o LLM é utilizado para gerar resumos semanticamente ricos que preservam informações críticas enquanto reduzem drasticamente o uso de tokens.
Recursão Controlada: Para conversas extremamente longas, o algoritmo aplica recursivamente a estratégia de resumo, dividindo o histórico em segmentos gerenciáveis e resumindo cada um separadamente antes de combiná-los.
Degradação Graciosa: Implementa múltiplos níveis de fallback, incluindo tentativas com diferentes modelos e, em último caso, truncamento simples se a profundidade de recursão se tornar excessiva.
Preservação de Continuidade: Mantém a estrutura de diálogo natural ao garantir que a sequência de mensagens termine com uma resposta do assistente, preservando o fluxo conversacional.
O prompt de resumo (prompts.summarize
) instrui o LLM a condensar a conversa preservando informações essenciais como nomes de funções, bibliotecas e arquivos mencionados, enquanto omite detalhes menos relevantes. Esta abordagem garante que o contexto técnico crítico seja mantido mesmo após múltiplas rodadas de resumo.
3.4 Mapeamento do Repositório
Para fornecer contexto ao LLM sobre a estrutura do projeto, é usado o componente RepoMap
. Ele implementa um sistema sofisticado para criar uma representação compacta e informativa da estrutura do repositório.
Este mapa serve como um guia contextual para o LLM, permitindo que ele compreenda a organização do projeto sem necessitar do conteúdo completo de todos os arquivos:
class RepoMap:
def __init__(self, io: InputOutput, root: str, max_tags: int = 1000):
"""Inicializa o mapeador de repositório."""
self.io = io
self.root = root
self.max_tags = max_tags
self.cache = Cache(os.path.join(os.path.expanduser("~"), ".aider", "caches", "repomap"))
self.tree_context_cache = {}
def get_ranked_tags_map(self, chat_fnames: List[str], other_fnames: Optional[List[str]] = None) -> str:
"""Gera um mapa do repositório com tags classificadas por relevância."""
if not chat_fnames:
return ""
# Extrai tags (símbolos) de arquivos relevantes
tags = self.get_tags(chat_fnames, other_fnames)
if not tags:
return ""
# Formata as tags em uma representação textual estruturada
return self.format_tags_map(tags)
def get_tags(self, chat_fnames: List[str], other_fnames: Optional[List[str]] = None) -> List[Tag]:
"""Extrai tags (símbolos) de arquivos especificados."""
# Inicializa conjunto de arquivos a serem analisados
all_fnames = set(chat_fnames)
if other_fnames:
all_fnames.update(other_fnames)
# Coleta tags de cada arquivo
all_tags = []
for fname in all_fnames:
# Verifica cache para evitar reanálise desnecessária
cache_key = self._get_cache_key(fname)
cached_tags = self.cache.get(cache_key)
if cached_tags is not None:
# Usa tags em cache se disponíveis
all_tags.extend(cached_tags)
else:
# Extrai tags do arquivo e atualiza cache
tags = self._extract_tags_from_file(fname)
if tags:
self.cache.set(cache_key, tags)
all_tags.extend(tags)
# Classifica e filtra tags por relevância
ranked_tags = self._rank_tags(all_tags, chat_fnames)
return ranked_tags[:self.max_tags]
O algoritmo de mapeamento do repositório implementa várias técnicas sofisticadas:
Extração de Símbolos Baseada em AST: Utiliza a árvore sintática gerada pelo Tree-Sitter para identificar símbolos significativos (funções, classes, métodos, etc.) em cada arquivo, capturando sua estrutura hierárquica e relações.
Classificação por Relevância: Implementa um algoritmo de classificação inspirado no PageRank que atribui pontuações de relevância a cada símbolo com base em fatores como:
* Presença em arquivos atualmente em edição
* Frequência de referências em outros arquivos
* Proximidade semântica com o contexto atual
* Importância estrutural no projeto (ex: classes base vs. utilitários)
Caching Inteligente: Mantém um cache persistente de tags extraídas, invalidado seletivamente quando arquivos são modificados, otimizando o desempenho em sessões prolongadas.
Formatação Contextual: Gera uma representação textual estruturada que prioriza informações mais relevantes para o contexto atual, maximizando o valor informacional dentro das restrições de tokens.
O formato do mapa resultante é cuidadosamente projetado para maximizar a compreensão do LLM sobre a estrutura do projeto:
# Mapa do Repositório
## src/core/
- `class Database` (database.py:15): Gerencia conexões e transações com o banco de dados
- `method connect(config)` (database.py:28): Estabelece conexão com base na configuração
- `method execute_query(sql, params)` (database.py:42): Executa consulta SQL com parâmetros
## src/api/
- `function create_app()` (app.py:10): Cria e configura a aplicação Flask
- `class UserController` (controllers/user.py:8): Gerencia operações relacionadas a usuários
- `method get_user(user_id)` (controllers/user.py:15): Recupera usuário por ID
Esta representação hierárquica fornece ao LLM uma visão estruturada do projeto, permitindo que ele compreenda relações entre componentes e localize funcionalidades relevantes sem necessitar do código completo.
A combinação de caminhos de arquivo, números de linha, descrições concisas e relações hierárquicas cria um mapa mental do projeto que o LLM pode utilizar para contextualizar suas respostas e sugestões.
4. Integração com CI/CD e Qualidade de Código
O assistente de IA implementa integrações sofisticadas com sistemas modernos de CI/CD e ferramentas de qualidade de código, permitindo sua incorporação em fluxos de trabalho de desenvolvimento estabelecidos.
4.1 GitHub Actions
A integração com GitHub Actions permite automatizar tarefas de desenvolvimento assistidas por IA em pipelines de CI/CD. Esta integração é implementada através de workflows personalizados que invocam o assistente em pontos estratégicos do ciclo de desenvolvimento:
name: Aider Code Review
on:
pull_request:
types: [opened, synchronize]
paths-ignore:
- '**.md'
- '.github/**'
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Aider
run: pip install aider-chat
- name: Create .aiderignore
run: |
echo "node_modules/" > .aiderignore
echo "dist/" >> .aiderignore
echo "*.min.js" >> .aiderignore
- name: Run Aider review
run: |
PR_DIFF=$(gh pr diff ${{ github.event.pull_request.number }})
echo "$PR_DIFF" > pr_diff.txt
aider --yes --message "Analise este PR e sugira melhorias. Foque em problemas de segurança, performance e manutenibilidade. Aqui está o diff: $(cat pr_diff.txt)"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Post review comments
if: success()
run: |
REVIEW=$(cat aider_review.md)
gh pr comment ${{ github.event.pull_request.number }} -b "$REVIEW"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Esta integração implementa um fluxo de trabalho sofisticado que:
Análise Automatizada de PRs: Executa o assistente automaticamente quando um pull request é aberto ou atualizado, analisando as mudanças propostas.
Filtragem Inteligente: Utiliza o sistema
.aiderignore
para excluir arquivos irrelevantes ou problemáticos da análise, otimizando o uso de tokens e focando em código significativo.Contextualização de Mudanças: Fornece o diff completo do PR, permitindo análise contextual das modificações propostas.
Feedback Estruturado: Gera comentários detalhados que são automaticamente postados no PR, facilitando a revisão colaborativa.
A implementação suporta diversos casos de uso avançados, incluindo:
Revisão de Código Automatizada: Análise de PRs para identificar problemas de segurança, performance, manutenibilidade e conformidade com padrões.
Geração de Testes: Criação automática de testes unitários e de integração para código novo ou modificado.
Documentação Automática: Geração ou atualização de documentação técnica com base nas mudanças de código.
Refatoração Proativa: Sugestão de refatorações para melhorar a qualidade do código em áreas problemáticas.
4.2 SonarQube
Embora não possua uma integração nativa com o SonarQube, o assistente pode ser incorporado em fluxos de trabalho que utilizam esta ferramenta de análise de qualidade de código. Esta integração pode ser implementada de duas formas complementares:
- Pré-processamento: Utilizar a ferramenta para corrigir problemas antes da análise do SonarQube, reduzindo a dívida técnica identificada:
name: Code Quality Pipeline
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Aider
run: pip install aider-chat
- name: Run preliminary SonarQube scan
uses: sonarsource/sonarqube-scan-action@master
with:
args: >
-Dsonar.projectKey=my-project
-Dsonar.sources=src
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- name: Extract SonarQube issues
run: |
curl -s "$SONAR_HOST_URL/api/issues/search?componentKeys=my-project&resolved=false" \
-H "Authorization: Bearer $SONAR_TOKEN" > sonar_issues.json
jq -r '.issues[] | "- " + .message + " (" + .component + ":" + (.line|tostring) + ")"' sonar_issues.json > issues_summary.txt
- name: Run Aider to fix issues
run: |
aider --yes --message "Corrija os seguintes problemas de qualidade identificados pelo SonarQube: $(cat issues_summary.txt)"
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Run final SonarQube scan
uses: sonarsource/sonarqube-scan-action@master
with:
args: >
-Dsonar.projectKey=my-project
-Dsonar.sources=src
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- Remediação Pós-análise: Utilizar o software para corrigir problemas identificados pelo SonarQube após a análise: Ele pode ser usado para corrigir automaticamente problemas identificados, como vulnerabilidades de injeção de SQL ou código duplicado.
def process_sonarqube_issues(sonar_url, sonar_token, project_key):
"""Processa problemas do SonarQube e gera instruções para o Aider."""
# Configura cliente SonarQube
sonar = SonarQubeClient(sonar_url, token=sonar_token)
# Recupera problemas não resolvidos
issues = sonar.issues.search(
componentKeys=project_key,
resolved="false",
severities="BLOCKER,CRITICAL,MAJOR"
)
# Agrupa problemas por arquivo
issues_by_file = defaultdict(list)
for issue in issues['issues']:
component = issue['component']
if component.startswith(f"{project_key}:"):
# Remove prefixo do projeto
file_path = component[len(f"{project_key}:"):]
issues_by_file[file_path].append({
'rule': issue['rule'],
'message': issue['message'],
'line': issue.get('line', 1),
'severity': issue['severity']
})
# Gera instruções para o assistente
for file_path, file_issues in issues_by_file.items():
issues_text = "\n".join([
f"- Linha {issue['line']}: {issue['message']} ({issue['rule']})"
for issue in file_issues
])
# Executa para corrigir problemas no arquivo
subprocess.run([
"aider", "--yes",
"--message", f"Corrija os seguintes problemas no arquivo {file_path}:\n\n{issues_text}"
], env={**os.environ, "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY")})
Estas integrações demonstram sua flexibilidade como componente em pipelines de qualidade de código mais amplos. A capacidade de interpretar e remediar problemas identificados por ferramentas especializadas como o SonarQube representa uma sinergia poderosa entre análise estática tradicional e assistência baseada em IA.
5. Processamento e Validação de Edições
O sistema implementa um fluxo sofisticado para processar, validar e aplicar edições de código sugeridas pelo LLM, garantindo que as modificações sejam precisas, seguras e semanticamente válidas.
5.1 Formatos de Edição
São suportados múltiplos formatos de edição, cada um otimizado para diferentes cenários de modificação de código. Os dois principais formatos são implementados através de classes especializadas que herdam da classe base Coder
:
- Unified Diff (UnifiedDiffCoder): Implementa edições baseadas no formato de diff unificado, ideal para modificações precisas em seções específicas de código:
class UnifiedDiffCoder(Coder):
"""Implementa edições baseadas em diffs unificados."""
edit_format = "udiff"
def get_edits(self) -> List[Tuple[str, List[str]]]:
"""Extrai edições no formato de diff unificado da resposta do LLM."""
content = self.partial_response_content
raw_edits = list(find_diffs(content))
# Processa e valida cada diff
processed_edits = []
for path, diff_text in raw_edits:
# Normaliza caminho do arquivo
abs_path = self.abs_path(path)
if not abs_path:
self.io.tool_error(f"Arquivo não encontrado: {path}")
continue
# Extrai hunks (blocos de mudança) do diff
hunks = parse_unified_diff(diff_text)
if not hunks:
self.io.tool_error(f"Nenhum hunk válido encontrado no diff para {path}")
continue
processed_edits.append((abs_path, hunks))
return processed_edits
def apply_edits(self, edits: List[Tuple[str, List[str]]]) -> None:
"""Aplica edições baseadas em diff aos arquivos."""
for fname, hunks in edits:
# Lê conteúdo atual do arquivo
content = self.io.read_text(fname)
if content is None:
self.io.tool_error(f"Não foi possível ler {fname}")
continue
# Aplica hunks ao conteúdo
patched = self.apply_hunks(content, hunks)
if patched == content:
self.io.tool_error(f"Nenhuma mudança aplicada a {fname}")
continue
# Valida o resultado antes de escrever
if not self.validate_patched_content(fname, patched):
self.io.tool_error(f"Validação falhou para {fname}, abortando edições")
continue
# Escreve conteúdo modificado
self.io.write_text(fname, patched)
self.io.tool_output(f"Editado {fname}")
- Search and Replace (SearchReplaceCoder): Implementa edições baseadas em substituição de blocos de texto, ideal para refatorações mais amplas:
def search_and_replace(content: str, original: str, updated: str) -> Tuple[str, bool]:
"""Implementa algoritmo de busca e substituição com tolerância a diferenças de indentação."""
# Inicializa biblioteca diff-match-patch
dmp = diff_match_patch()
# Gera patches representando as diferenças entre original e atualizado
patches = dmp.patch_make(original, updated)
# Aplica patches ao conteúdo
new_content, results = dmp.patch_apply(patches, content)
# Verifica se todos os patches foram aplicados com sucesso
success = all(results)
return new_content, success
O algoritmo de search-and-replace implementa técnicas avançadas para lidar com desafios comuns em edição de código:
Indentação Relativa: Normaliza a indentação entre o código original e o código de substituição, permitindo que o LLM forneça snippets sem precisar replicar a indentação exata do arquivo.
Correspondência Flexível: Implementa correspondência aproximada que tolera pequenas diferenças em espaços em branco, comentários e formatação, aumentando a robustez das edições.
Detecção de Ambiguidade: Identifica quando um padrão de busca corresponde a múltiplas localizações no arquivo, solicitando clarificação ao invés de fazer substituições potencialmente incorretas.
Pré-processadores Especializados: Aplica transformações específicas de linguagem antes da correspondência, melhorando a precisão em construções sintáticas complexas.
5.2 Linting e Validação
É implementado um sistema robusto de validação que verifica a integridade das edições propostas antes de aplicá-las aos arquivos. Este sistema opera em múltiplas camadas:
class Linter:
"""Implementa validação sintática e semântica de código modificado."""
def __init__(self, encoding: str = "utf-8", root: Optional[str] = None):
self.encoding = encoding
self.root = root
# Registra validadores específicos por linguagem
self.languages = {
"python": self.py_lint,
"javascript": self.js_lint,
"typescript": self.ts_lint,
"java": self.java_lint,
"c": self.c_lint,
"cpp": self.cpp_lint,
"csharp": self.csharp_lint,
"go": self.go_lint,
"rust": self.rust_lint,
}
def lint(self, fname: str, code: Optional[str] = None) -> List[Dict[str, Any]]:
"""Executa validação completa em um arquivo."""
if code is None:
with open(fname, "r", encoding=self.encoding) as f:
code = f.read()
# Determina linguagem com base na extensão do arquivo
lang = filename_to_lang(fname)
if not lang or lang not in self.languages:
# Sem validador específico para esta linguagem
return []
# Executa validador específico da linguagem
return self.languages[lang](fname, code)
def find_syntax_errors(self, fname: str, code: str) -> List[int]:
"""Identifica erros sintáticos usando Tree-Sitter."""
try:
# Obtém parser para a linguagem
lang = filename_to_lang(fname)
parser = get_parser(lang)
if not parser:
return []
# Parseia código e identifica nós de erro
tree = parser.parse(code.encode())
return self._traverse_tree_for_errors(tree.root_node)
except Exception:
# Falha graciosamente em caso de erro no parser
return []
def _traverse_tree_for_errors(self, node) -> List[int]:
"""Percorre árvore sintática identificando nós de erro."""
errors = []
# Nós marcados como ERROR ou MISSING indicam problemas sintáticos
if node.type == "ERROR" or node.is_missing:
errors.append(node.start_point[0]) # Linha do erro
# Recursivamente verifica nós filhos
for child in node.children:
errors.extend(self._traverse_tree_for_errors(child))
return errors
O sistema de validação implementa verificações em múltiplos níveis:
Validação Sintática: Utiliza o Tree-Sitter para verificar se o código modificado mantém uma estrutura sintática válida, identificando erros de sintaxe introduzidos pelas edições.
Validação Semântica: Para linguagens suportadas, executa verificações semânticas como análise de tipos, verificação de referências não resolvidas e detecção de problemas de escopo.
Validação Específica de Linguagem: Implementa verificações especializadas para cada linguagem suportada, como verificação de importações em Python, tipagem em TypeScript ou gerenciamento de memória em C++.
Validação de Integridade de Projeto: Verifica se as modificações mantêm a integridade do projeto como um todo, incluindo compatibilidade com dependências e conformidade com padrões de arquitetura.
Quando problemas são detectados, é implementada uma estratégia de remediação em camadas:
Correção Automática: Para problemas simples e bem definidos, tenta aplicar correções automáticas baseadas em heurísticas.
Solicitação de Esclarecimento: Para problemas ambíguos ou complexos, solicita esclarecimento ao LLM, fornecendo detalhes específicos sobre o problema identificado.
Rejeição de Edição: Para problemas críticos que não podem ser resolvidos automaticamente, rejeita a edição e fornece feedback detalhado sobre o motivo.
Este sistema de validação multicamada garante que as modificações propostas pelo LLM sejam aplicadas apenas quando mantêm a integridade e qualidade do código, implementando um princípio de "primeiro, não prejudique" que é essencial para ferramentas de assistência à programação.
6. Conclusão
Essa ferramenta é um grande passo na integração de modelos de linguagem ao desenvolvimento de software, trazendo uma arquitetura bem pensada que combina análise sintática profunda, gerenciamento de contexto adaptativo e validação rigorosa de mudanças no código.
Essa mistura de tecnologias vai além das abordagens tradicionais, oferecendo uma assistência que é ao mesmo tempo relevante pro contexto e precisa tecnicamente.
O uso do Tree-Sitter como base pra análise sintática permite entender a estrutura do código de um jeito que vai muito além do simples processamento de texto. Isso abre portas pra operações semanticamente ricas, como refatoração, geração de testes e documentação automática.
Essa capacidade é ainda mais ampliada pelo sistema ChatChunks, que usa um algoritmo inteligente pra gerenciar o contexto, maximizando o uso da janela limitada de tokens disponível nos LLMs atuais.
A integração com ferramentas modernas de CI/CD, como GitHub Actions e SonarQube, mostra como a ferramenta é flexível e se encaixa bem em fluxos de trabalho de desenvolvimento mais amplos.
O sistema de edição, com seus vários formatos e algoritmos avançados de aplicação e validação, é uma solução elegante pra transformar sugestões de alto nível em mudanças precisas e seguras no código.
No fim das contas, essa ferramenta é um exemplo de uma nova geração de ferramentas de desenvolvimento com suporte de IA.
Ela não só automatiza tarefas repetitivas, mas também amplifica a criatividade e a capacidade analítica dos desenvolvedores, com o potencial de aumentar muito a produtividade e a qualidade do código.
Top comments (1)
Cara, fico muito foda seu artigo.parabens de maaais