DEV Community

Cover image for Aider: Integração Avançada de LLMs no Desenvolvimento de Software
Vitor Lobo
Vitor Lobo

Posted on • Edited on

Aider: Integração Avançada de LLMs no Desenvolvimento de Software

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

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 
Enter fullscreen mode Exit fullscreen mode

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.

Image description

Na imagem acima, dá pra ver a arquitetura com essas características.

  1. 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.

  2. 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.).
  3. 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.

  4. 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.

  5. 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 
Enter fullscreen mode Exit fullscreen mode

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:

  1. Extração de Símbolos: Identifica funções, classes, métodos, variáveis e suas definições, incluindo escopo e visibilidade.
  2. 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.
  3. 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.
  4. 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() 
Enter fullscreen mode Exit fullscreen mode

Essa implementação traz vários benefícios:

  1. Inicialização Otimizada: O carregamento preguiçoso reduz o tempo de inicialização, já que o módulo só é carregado quando realmente necessário.
  2. Abstração de Provedores: A interface unificada do LiteLLM permite usar diferentes LLMs (OpenAI, Anthropic, Cohere, etc.) sem precisar mudar o código base.
  3. 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.
  4. 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 
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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" }] 
Enter fullscreen mode Exit fullscreen mode

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 ) 
Enter fullscreen mode Exit fullscreen mode

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:

  1. Instruções do Sistema: Definem como o modelo deve se comportar, estabelecendo o tom e as capacidades esperadas.
  2. Exemplos Demonstrativos: Mostram exemplos de interação e respostas no formato esperado.
  3. Arquivos Somente Leitura: Fornecem contexto de referência que não deve ser alterado, como dependências ou configurações.
  4. Mapa do Repositório: Dá uma visão geral da estrutura do projeto, essencial pra entender o contexto.
  5. Histórico de Conversas: Mantém o contexto das discussões anteriores, garantindo continuidade.
  6. Arquivos Editáveis: Contém o código que pode ser modificado, foco principal da interação atual.
  7. Mensagem Atual: É a instrução ou pergunta do usuário que vai gerar a resposta.
  8. 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] 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Quando o limite de tokens é excedido, é implementada uma estratégia de redução adaptativa que prioriza a preservação de informações críticas:

  1. Resumo de Histórico: Condensa conversas anteriores em resumos concisos, preservando informações essenciais enquanto reduz drasticamente o uso de tokens.

  2. Poda de Mapa: Reduz o tamanho do mapa do repositório, focando apenas nos componentes mais relevantes para a tarefa atual.

  3. Truncamento Seletivo: Remove seletivamente partes menos relevantes do contexto, como exemplos detalhados ou arquivos periféricos.

  4. 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") 
Enter fullscreen mode Exit fullscreen mode

Este algoritmo implementa uma estratégia de divisão e conquista com características notáveis:

  1. 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.

  2. 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.

  3. 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.

  4. 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] 
Enter fullscreen mode Exit fullscreen mode

O algoritmo de mapeamento do repositório implementa várias técnicas sofisticadas:

  1. 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.

  2. 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) 
Enter fullscreen mode Exit fullscreen mode
  1. 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.

  2. 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 
Enter fullscreen mode Exit fullscreen mode

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 }} 
Enter fullscreen mode Exit fullscreen mode

Esta integração implementa um fluxo de trabalho sofisticado que:

  1. Análise Automatizada de PRs: Executa o assistente automaticamente quando um pull request é aberto ou atualizado, analisando as mudanças propostas.

  2. 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.

  3. Contextualização de Mudanças: Fornece o diff completo do PR, permitindo análise contextual das modificações propostas.

  4. 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:

  1. 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 }} 
Enter fullscreen mode Exit fullscreen mode
  1. 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")}) 
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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}") 
Enter fullscreen mode Exit fullscreen mode
  1. 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 
Enter fullscreen mode Exit fullscreen mode

O algoritmo de search-and-replace implementa técnicas avançadas para lidar com desafios comuns em edição de código:

  1. 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.

  2. 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.

  3. 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.

  4. 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 
Enter fullscreen mode Exit fullscreen mode

O sistema de validação implementa verificações em múltiplos níveis:

  1. 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.

  2. 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.

  3. 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++.

  4. 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:

  1. Correção Automática: Para problemas simples e bem definidos, tenta aplicar correções automáticas baseadas em heurísticas.

  2. Solicitação de Esclarecimento: Para problemas ambíguos ou complexos, solicita esclarecimento ao LLM, fornecendo detalhes específicos sobre o problema identificado.

  3. 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)

Collapse
 
mateusc__ profile image
Mateus Fonseca

Cara, fico muito foda seu artigo.parabens de maaais