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:
- Selecionar o contexto mais relevante a ser incluído na janela de contexto.
- Maximizar o uso da janela de contexto disponível, sem exceder os limites.
- Minimizar o custo (em termos de tokens) das interações com o LLM.
- 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.
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 usandoself.fmt_system_prompt
noBaseCoder
.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 peloRepoMap
(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 doreminder
pode variar de acordo com o modelo e oedit-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
.
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.
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 bibliotecalitellm
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
: quandotext
é muito grande, a funçãotoken_count
doRepoMap
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.
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:
-
summarize(messages)
: É o ponto de entrada. Chamasummarize_real
para realizar o trabalho pesado. -
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 demax_tokens
).
- Resumo Recursivo: A parte
head
é resumida recursivamente chamandosummarize_all
. - Combinação: O resumo da parte
head
é concatenado com a partetail
(não resumida). - Verificação Final: Se o resultado combinado ainda for muito longo, o processo é repetido recursivamente.
- Verificação Inicial: Se o número total de tokens nas mensagens for menor que
-
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ê".
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:
-
get_ranked_tags_map(...)
: Esta função é o coração doRepoMap
. 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
(etree-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
) usandodiskcache.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 que estão na conversa (
- Arquivos Especiais: Arquivos como
README.md
,requirements.txt
, etc., são automaticamente incluídos no mapa do repositório, independentemente do ranking (usandofilter_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
. Oto_tree
usa a classeTreeContext
para mostrar o contexto (linhas de código próximas) para cada tag.
- Seleciona os arquivos/tags mais relevantes com base no ranking, limitando o tamanho do mapa do repositório para caber em
- 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 (usandomtime
) e os parâmetros de geração do mapa.
- Coleta de Tags (defs e refs): Usa a biblioteca
-
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.
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 (oCoder
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.
- Incrementa um contador (
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.
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.
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 paramap-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 serauto
,always
,files
oumanual
.
-
Inicialização:
- O
main
emaider/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 eedit-format
) é criado. OCoder
é responsável por interagir com o LLM. - O
RepoMap
é inicializado (se habilitado). ChatChunks
é inicializado dentro doCoder
.
- O
-
Loop Principal (dentro de
Coder.run
):- Entrada do Usuário: O usuário fornece uma instrução (prompt).
- Formatação das Mensagens:
- O
Coder
usaformat_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).
- Mensagens do sistema (
- O
- 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.
-
Otimização contínua:
- O Aider monitora as respostas, buscando por
ContextWindowExceededError
. Se ocorrer, o processo é refeito com um prompt reduzido.
- O Aider monitora as respostas, buscando por
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.
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 🤷♂️