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