Skip to content

Instantly share code, notes, and snippets.

@scovl
Last active February 26, 2025 20:54
Show Gist options
  • Save scovl/d8146ecb935e35e2d1becb3961d6e327 to your computer and use it in GitHub Desktop.
Save scovl/d8146ecb935e35e2d1becb3961d6e327 to your computer and use it in GitHub Desktop.
Estudo sobre o aider

Gerenciamento de Janela de Contexto no Aider

O Aider implementa um sistema sofisticado para gerenciar a janela de contexto nas interações com modelos de linguagem (LLMs). Este documento detalha como isso funciona internamente, cobrindo a estrutura de dados, algoritmos, estratégias de otimização e configurações.

Por que o Gerenciamento da Janela de Contexto é Crucial?

Modelos de linguagem, como os da OpenAI (GPT-3, GPT-4), Anthropic (Claude) e outros, operam com uma janela de contexto limitada. Essa janela representa a quantidade máxima de texto (medida em tokens) que o modelo pode "ver" ao gerar uma resposta. Quanto maior a janela, mais contexto o modelo tem, mas também maior o custo computacional e financeiro.

Ferramentas como o Aider, que usam LLMs para auxiliar na programação, enfrentam um desafio particular:

  • Código-fonte é extenso: Projetos de software podem ter milhares de linhas de código, distribuídas em muitos arquivos. É impossível incluir todo o código na janela de contexto.
  • Contexto é fundamental: Para entender e modificar código, o LLM precisa ter acesso a informações relevantes, como:
    • O código que está sendo editado.
    • Outras partes do código que interagem com ele (funções, classes, etc.).
    • O histórico da conversa com o usuário (comandos, perguntas, respostas anteriores).
    • A estrutura geral do projeto (nomes de arquivos, diretórios, etc.).

O Aider precisa, portanto, de um sistema inteligente para:

  1. Selecionar o contexto mais relevante a ser incluído na janela de contexto.
  2. Maximizar o uso da janela de contexto disponível, sem exceder os limites.
  3. Minimizar o custo (em termos de tokens) das interações com o LLM.
  4. Manter a consistência ao longo da conversa, mesmo com a janela de contexto limitada.

Abaixo está uma visão geral do sistema de gerenciamento de janela de contexto do Aider.

Estrutura Básica: ChatChunks

A janela de contexto é gerenciada principalmente através da classe ChatChunks em aider/coders/chat_chunks.py. Esta classe organiza as mensagens em diferentes seções, cada uma com um propósito específico:

@dataclass
class ChatChunks:
    system: List = field(default_factory=list)
    examples: List = field(default_factory=list)
    done: List = field(default_factory=list)
    repo: List = field(default_factory=list)
    readonly_files: List = field(default_factory=list)
    chat_files: List = field(default_factory=list)
    cur: List = field(default_factory=list)
    reminder: List = field(default_factory=list)
  • system: Mensagens do sistema que definem o comportamento geral do modelo. Essas mensagens geralmente incluem instruções sobre como o modelo deve formatar as respostas, usar as ferramentas disponíveis e interagir com o usuário. Essas mensagens são formatadas usando self.fmt_system_prompt no BaseCoder.
  • examples: Exemplos de interações (perguntas e respostas) para few-shot learning. Esses exemplos ajudam o modelo a entender o formato desejado das respostas e o tipo de tarefa a ser realizada.
  • done: Mensagens de chat anteriores (usuário e assistente) que já foram processadas. O Aider mantém um histórico das interações para fornecer contexto ao LLM.
  • repo: Uma representação textual do repositório, gerada pelo RepoMap (mais detalhes abaixo). Isso fornece ao LLM uma visão geral da estrutura do projeto, incluindo nomes de arquivos, funções, classes, etc.
  • readonly_files: O conteúdo de arquivos que são relevantes para o contexto, mas que o LLM não deve editar. Esses arquivos são incluídos para fornecer informações adicionais, como código de bibliotecas ou arquivos de configuração.
  • chat_files: O conteúdo dos arquivos que o LLM está autorizado a editar. Esses são os arquivos principais com os quais o Aider trabalhará.
  • cur: Mensagens da interação atual do usuário. É a pergunta/comando que o usuário acabou de enviar.
  • reminder: Uma mensagem de lembrete que é anexada no final do prompt, geralmente para reforçar instruções importantes ou o formato de saída desejado. O conteúdo do reminder pode variar de acordo com o modelo e o edit-format.

As mensagens dentro de cada seção são formatadas como texto plano, seguindo convenções específicas (ex: uso de cercas de código Markdown). A formatação é feita principalmente pela função format_messages em aider/utils.py, e métodos como fmt_msg em BaseCoder.

Controle de Cache

O Aider implementa um sistema de cache para otimizar o uso da janela de contexto e reduzir a latência. O objetivo é evitar reenviar informações que o LLM já viu e processou em turnos anteriores da conversa. O método add_cache_control_headers da classe ChatChunks adiciona metadados de cache às mensagens. Esses metadados não são headers HTTP, mas sim comentários especiais dentro do próprio texto da mensagem:

# aider/coders/chat_chunks.py
def add_cache_control_headers(self):
    if self.examples:
        self.add_cache_control(self.examples)
    else:
        self.add_cache_control(self.system)

    if self.repo:
        self.add_cache_control(self.repo)
    else:
        self.add_cache_control(self.readonly_files)

    self.add_cache_control(self.chat_files)

def add_cache_control(self, msgs):
    for msg in msgs:
        msg["content"] = "###### example.py\n" + msg["content"]

Exemplo prático de como o header é adicionado (saída de show_messages):

USER ###### example.py
      def hello():
          print("hello")

O Aider usa esses headers para identificar partes do prompt que podem ser cacheadas, evitando o reenvio desnecessário de informações estáticas (como prompts do sistema, exemplos e o mapa do repositório) entre as chamadas ao LLM.

Gerenciamento de Tokens

O Aider monitora cuidadosamente o uso de tokens para evitar exceder os limites da janela de contexto do modelo. A classe Model (em aider/models.py) fornece métodos para contar tokens:

# aider/models.py
def token_count(self, messages):
    if type(messages) is list:
        try:
            return litellm.token_counter(model=self.name, messages=messages)
        except Exception as err:
            print(f"Unable to count tokens: {err}")
            return 0

    if not self.tokenizer:
        return

    if type(messages) is str:
        msgs = messages
    else:
        msgs = json.dumps(messages)

    try:
        return len(self.tokenizer(msgs))
    except Exception as err:
        print(f"Unable to count tokens: {err}")
        return 0

def token_count_for_image(self, fname):
    # ... calcula o custo de tokens para uma imagem ...
  • token_count(messages): Conta o número de tokens em uma lista de mensagens ou em uma string. Usa a biblioteca litellm para contar tokens para diversos modelos de linguagem. Se ocorrer um erro durante a contagem, retorna 0.
  • token_count_for_image(fname): Calcula o custo em tokens para incluir uma imagem no prompt, considerando as dimensões da imagem.
  • Aproximação em repomap.py: quando text é muito grande, a função token_count do RepoMap faz amostragem para não gastar muitos tokens.

O Aider também define max_chat_history_tokens para cada modelo, que controla quantos tokens do histórico da conversa são mantidos. max_chat_history_tokens é calculado como sendo 1/16 de max_input_tokens, com um mínimo de 1024 e máximo de 8192 tokens.

Resumo de Histórico

Quando a conversa fica muito longa e excede max_chat_history_tokens, o Aider usa a classe ChatSummary (em aider/history.py) para resumir o histórico da conversa. O objetivo é condensar as informações mais antigas, liberando espaço na janela de contexto para mensagens mais recentes.

# aider/history.py
class ChatSummary:
    def __init__(self, models=None, max_tokens=1024):
        # ...

    def summarize(self, messages, depth=0):
        messages = self.summarize_real(messages)
        if messages and messages[-1]["role"] != "assistant":
            messages.append(dict(role="assistant", content="Ok."))
        return messages

    def summarize_real(self, messages, depth=0):
        # ... implementação do resumo ...

O processo de resumo funciona da seguinte forma:

  1. summarize(messages): É o ponto de entrada. Chama summarize_real para realizar o trabalho pesado.

  2. summarize_real(messages, depth):

    • Verificação Inicial: Se o número total de tokens nas mensagens for menor que max_tokens (ou se a profundidade da recursão for muito alta), o resumo não é necessário. As mensagens originais são retornadas.
    • Divisão Head/Tail: Se a conversa for muito longa, ela é dividida em duas partes:
      • head: As mensagens mais antigas.
      • tail: As mensagens mais recentes. A divisão é feita de forma a manter a maior parte possível do histórico recente (tail) dentro de um limite de tokens (metade de max_tokens).
    • Resumo Recursivo: A parte head é resumida recursivamente chamando summarize_all.
    • Combinação: O resumo da parte head é concatenado com a parte tail (não resumida).
    • Verificação Final: Se o resultado combinado ainda for muito longo, o processo é repetido recursivamente.
  3. summarize_all(messages):

    • Formata as mensagens em um único bloco de texto.
    • Cria um prompt para o LLM, usando prompts.summarize como instrução do sistema.
    • Usa o weak_model para gerar o resumo (modelos menores e mais rápidos, como gpt-3.5-turbo).
    • O resumo gerado é então retornado como uma única mensagem do usuário.

O prompt de resumo (prompts.summarize) instrui o LLM a:

  • Ser breve.
  • Incluir mais detalhes sobre as partes mais recentes da conversa.
  • Não concluir o resumo (já que a conversa continuará).
  • Incluir nomes de funções, bibliotecas, pacotes e nomes de arquivos.
  • Escrever o resumo na perspectiva do usuário, referindo-se ao assistente como "você".

Mapa do Repositório (RepoMap)

O Aider mantém um mapa do repositório (RepoMap, em aider/repomap.py) para fornecer ao LLM um contexto sobre a estrutura do projeto, mesmo que nem todos os arquivos estejam incluídos na janela de contexto. O RepoMap ajuda o LLM a entender as relações entre diferentes partes do código.

# aider/repomap.py
class RepoMap:
    def __init__(
        self,
        map_tokens=1024,
        root=None,
        main_model=None,
        io=None,
        repo_content_prefix=None,
        verbose=False,
        max_context_window=None,
        map_mul_no_files=8,
        refresh="auto",
    ):
        # ...

    def get_ranked_tags_map(
        self,
        chat_fnames,
        other_fnames=None,
        max_map_tokens=None,
        mentioned_fnames=None,
        mentioned_idents=None,
        force_refresh=False,
    ):
        # ...

O RepoMap funciona da seguinte forma:

  1. get_ranked_tags_map(...): Esta função é o coração do RepoMap. Ela gera o mapa do repositório, que é uma string contendo uma representação textual da estrutura do código.

    • Coleta de Tags (defs e refs): Usa a biblioteca grep-ast (e tree-sitter) para analisar os arquivos do repositório e extrair:
      • Definições (defs): Declarações de funções, classes, métodos, etc. (onde os símbolos são definidos).
      • Referências (refs): Locais onde os símbolos são usados.
      • Essa informação é armazenada em cache (TAGS_CACHE) usando diskcache.Cache, para acelerar processos subsequentes.
    • Construção do Grafo: Cria um grafo direcionado (usando networkx) onde:
      • Os nós são os arquivos.
      • As arestas representam as relações entre os arquivos (definidor/usuário). O peso das arestas é determinado pelo número de referências e pela "importância" do identificador (nomes que começam com _ têm peso menor, nomes mencionados no chat têm peso maior).
    • Ranking (PageRank): Aplica o algoritmo PageRank ao grafo para determinar a importância relativa de cada arquivo.
    • Personalização: O PageRank é personalizado para dar maior peso a:
      • Arquivos que estão na conversa (chat_fnames).
      • Arquivos mencionados no chat (mentioned_fnames).
      • Identificadores mencionados no chat (mentioned_idents).
    • Arquivos Especiais: Arquivos como README.md, requirements.txt, etc., são automaticamente incluídos no mapa do repositório, independentemente do ranking (usando filter_important_files).
    • Seleção e Formatação:
      • Seleciona os arquivos/tags mais relevantes com base no ranking, limitando o tamanho do mapa do repositório para caber em max_map_tokens.
      • Usa uma heurística de busca binária (lower_bound, upper_bound, middle) para encontrar o número ideal de arquivos/tags a serem incluídos, maximizando o uso de tokens dentro do limite.
      • Formata o mapa do repositório como uma string, usando to_tree. O to_tree usa a classe TreeContext para mostrar o contexto (linhas de código próximas) para cada tag.
    • Cache: Os resultados são armazenados em cache (map_cache, tree_cache, tree_context_cache) para otimizar o desempenho. O cache leva em consideração o conteúdo dos arquivos (usando mtime) e os parâmetros de geração do mapa.
  2. repo_content_prefix: Uma string opcional que é adicionada antes do mapa do repositório. Pode ser usada para fornecer instruções adicionais ao LLM sobre como interpretar o mapa.

Tratamento de Excedentes de Contexto

Quando o tamanho combinado das mensagens e do mapa do repositório excede a janela de contexto do modelo, o Aider toma medidas para lidar com a situação. A lógica principal está em aider/coders/base_coder.py:

# aider/coders/base_coder.py
    def handle_exception(self, ex):
        from aider.sendchat import send_with_retries

        if self.stream:
            self.io.tool_output()
        self.io.tool_error(f"{type(ex).__name__}: {ex}")

        if isinstance(ex, ContextWindowExceededError):
            self.num_exhausted_context_windows += 1
            if self.num_exhausted_context_windows > 3:
                self.io.tool_error("Too many context window exhaustions, something is wrong")
                return False

            self.io.tool_error(
                "Exceeded context window, trying a smaller context window size. Retrying."
            )
            return "retry"

        return False
  • ContextWindowExceededError: Se o LLM retornar um erro indicando que a janela de contexto foi excedida, o Aider:
    • Incrementa um contador (num_exhausted_context_windows).
    • Imprime uma mensagem de erro.
    • Retorna "retry", indicando que a operação deve ser tentada novamente (o Coder irá reduzir o tamanho do contexto e reenviar a solicitação).
    • Se o erro persistir após várias tentativas, o Aider aborta a operação.

Além de ContextWindowExceededError, o Aider trata outras exceções relacionadas ao LLM (ver aider/exceptions.py) e à comunicação com a API (aider/llm.py). O método send_with_retries em aider/llm.py (e usado em aider/models.py) implementa retentativas com espera exponencial para lidar com erros de taxa limite (RateLimitError) e outros erros temporários.

Estratégias de Otimização

O Aider emprega várias estratégias para otimizar o uso da janela de contexto:

  • Cache Eficiente: Usa headers de controle de cache (comentários especiais) para evitar o reenvio de informações estáticas (prompts do sistema, exemplos, mapa do repositório) entre as chamadas ao LLM.
  • Resumo Adaptativo: Resume o histórico da conversa quando ele se torna muito longo, priorizando informações mais recentes e relevantes. O resumo é feito de forma recursiva, se necessário.
  • Gerenciamento de Tokens: Monitora continuamente o uso de tokens e ajusta dinamicamente o tamanho do contexto (ex: reduzindo o número de arquivos no mapa do repositório, resumindo o histórico) para evitar exceder os limites do modelo.
  • Mapeamento Inteligente do Repositório (RepoMap): Cria uma representação compacta e informativa da estrutura do código, focando nos arquivos e símbolos mais relevantes para a tarefa atual. O PageRank e a personalização ajudam a identificar o código mais importante.
  • Priorização de Arquivos/Tags: Arquivos na conversa, arquivos/tags mencionados, e arquivos especiais tem prioridade.
  • Truncamento de Linhas Longas: Evita que linhas de código excessivamente longas (ex: arquivos minificados) consumam muitos tokens.

Configuração

A janela de contexto pode ser configurada através de argumentos de linha de comando, definidos em aider/args.py:

  • --model <MODEL>: Especifica o modelo de linguagem a ser usado (ex: gpt-4, gpt-3.5-turbo). Cada modelo tem uma janela de contexto máxima diferente. Atalhos como -4, -3, etc., também podem ser usados.
  • --map-tokens <NUM>: Define o número máximo de tokens a serem usados para o mapa do repositório. O padrão depende do modelo.
  • --max-chat-history-tokens <NUM>: Define o número máximo de tokens do histórico a ser incluído.
  • --map-multiplier-no-files <NUM>: Multiplicador para map-tokens quando não houver arquivos na conversa.
  • --edit-format <FORMAT>: Define o formato de edição a ser usado (ex: diff, udiff, ...), que pode influenciar a quantidade de contexto necessária.
  • --refresh: Controla a frequência de atualização do mapa do repositório. Pode ser auto, always, files ou manual.

Fluxo de Processamento Detalhado

  1. Inicialização:

    • O main em aider/main.py carrega as configurações a partir de argumentos de linha de comando, arquivos de configuração e variáveis de ambiente.
    • O Coder (selecionado com base no modelo e edit-format) é criado. O Coder é responsável por interagir com o LLM.
    • O RepoMap é inicializado (se habilitado).
    • ChatChunks é inicializado dentro do Coder.
  2. Loop Principal (dentro de Coder.run):

    • Entrada do Usuário: O usuário fornece uma instrução (prompt).
    • Formatação das Mensagens:
      • O Coder usa format_messages para construir a lista de mensagens a serem enviadas ao LLM. Essa lista inclui:
        • Mensagens do sistema (ChatChunks.system).
        • Exemplos (ChatChunks.examples, se aplicável).
        • Histórico da conversa (ChatChunks.done).
        • Mapa do repositório (ChatChunks.repo).
        • Conteúdo de arquivos somente leitura (ChatChunks.readonly_files).
        • Conteúdo de arquivos editáveis (ChatChunks.chat_files).
        • A instrução atual do usuário (ChatChunks.cur).
        • Um lembrete (ChatChunks.reminder, se aplicável).
    • Contagem de Tokens: O Coder verifica se o tamanho total das mensagens excede o limite da janela de contexto do modelo.
    • Resumo (se necessário): Se a janela de contexto for excedida, o histórico da conversa é resumido usando ChatSummary.
    • Atualização do Mapa do Repositório (se necessário): Se o mapa do repositório estiver habilitado e as condições para atualização forem atendidas (ex: novos arquivos foram adicionados, o usuário solicitou uma atualização), o RepoMap é atualizado.
    • Envio para o LLM: As mensagens formatadas são enviadas ao LLM (usando litellm).
    • Processamento da Resposta: O Coder processa a resposta do LLM, que pode incluir:
      • Texto simples.
      • Edições em arquivos (no formato especificado por edit-format).
      • Chamadas de funções (não usadas no Aider).
    • Aplicação das Edições: As edições sugeridas pelo LLM são aplicadas aos arquivos (se houver).
    • Repetição: O loop continua, aguardando a próxima instrução do usuário.
  3. Otimização contínua:

    • O Aider monitora as respostas, buscando por ContextWindowExceededError. Se ocorrer, o processo é refeito com um prompt reduzido.

Conclusão

O sistema de gerenciamento de janela de contexto do Aider é algo que chama a atenção pela forma como lida com desafios que, à primeira vista, parecem complicados demais. Quando você está trabalhando em projetos de código extensos e complexos, é fácil se perder no meio de tantas informações.

O que mais impressiona é como ele consegue manter a interação com os LLMs (modelos de linguagem) fluida, mesmo quando o volume de dados é grande. Não é sobre fazer propaganda da ferramenta, mas sobre reconhecer que, em um cenário onde a complexidade pode ser um obstáculo, ter algo que simplifica e direciona o foco faz toda a diferença. É como se ele ajudasse a "traduzir" a bagunça em algo mais digerível, sem perder o essencial. Achei bastante inteligente como a janela de contexto foi implementada aqui.

@infoslack
Copy link

Ta muito bom!
Minha sugestão é só para adicionar uma breve introdução, tipo explicando por que o gerenciamento de contexto é importante para essas tools. E na conclusão senti falta sobre a sua visão (com um pouco mais detalhe) aqui vale o achismo! Tipo o que vc acha que vai rolar de melhoria no futuro? Talvez uma breve comparação com abordagens de ferramentas similares 🤷‍♂️

@scovl
Copy link
Author

scovl commented Feb 26, 2025

Ta muito bom! Minha sugestão é só para adicionar uma breve introdução, tipo explicando por que o gerenciamento de contexto é importante para essas tools. E na conclusão senti falta sobre a sua visão (com um pouco mais detalhe) aqui vale o achismo! Tipo o que vc acha que vai rolar de melhoria no futuro? Talvez uma breve comparação com abordagens de ferramentas similares 🤷‍♂️

Ajustado! Obrigado!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment