Pesquisa

Aprimorando continuamente nosso harness de agente

Stefan Heule & Jediah Katz11 min de leitura

Abordamos o desenvolvimento do harness de agente do Cursor da mesma forma que abordaríamos qualquer produto de software ambicioso. Boa parte do trabalho é orientada por uma visão: começamos com uma opinião sobre como deve ser a experiência ideal de agente.

A partir daí, formulamos hipóteses sobre como chegar mais perto dessa visão, executamos experimentos para testá-las e iteramos com base em sinais quantitativos e qualitativos de evals e do uso real. Esse processo depende de termos a instrumentação online e offline ideal, para sabermos quando uma alteração realmente melhora o harness.

Quando temos acesso antecipado a novos modelos, todas essas abordagens convergem. Passamos semanas personalizando nosso harness aos pontos fortes e às peculiaridades de um modelo, até que esse mesmo modelo, dentro do nosso harness especialmente ajustado, fique visivelmente mais rápido, mais inteligente e mais eficiente.

Ocasionalmente, descobrimos melhorias de grande impacto. Mais frequentemente, porém, aprimorar o harness é uma questão de empilhar obsessivamente pequenas otimizações que, juntas, tornam os agentes melhores em desenvolver software.

A evolução da janela de contexto

No cerne da interação com modelos de linguagem de grande porte está a janela de contexto. Ao pedir ao agente para construir algo, a janela de contexto começa com o prompt do sistema e as descrições das ferramentas, seguida do estado atual da conversa e, por fim, da solicitação do usuário.

A forma como preenchemos e gerenciamos essa janela evoluiu significativamente ao longo da história do Cursor.

Quando desenvolvemos pela primeira vez nosso agente de programação, no fim de 2024, os modelos eram muito piores em escolher seu próprio contexto, e investimos muito trabalho de engenharia de contexto para criar proteções — por exemplo, exibindo erros de lint e de tipagem ao agente após cada edição, reescrevendo suas leituras de arquivo quando ele solicitava linhas de menos e até limitando o número máximo de ferramentas que ele podia chamar em uma única rodada.

Também fornecíamos grandes quantidades de contexto estático, sempre disponíveis para o agente no início de cada sessão. Em vários momentos, isso incluía a estrutura de pastas da base de código, trechos de código semanticamente relacionados à consulta e versões comprimidas de arquivos anexados manualmente pelo usuário.

Isso em grande parte ficou para trás.

Ainda incluímos algum contexto estático útil (por exemplo, sistema operacional, status do git, arquivos atuais e visualizados recentemente). Mas nos adaptamos ao aumento da capacidade dos modelos reduzindo essas proteções e fornecendo mais contexto dinâmico, que pode ser obtido pelo agente enquanto trabalha. Em uma postagem anterior, fizemos uma análise aprofundada de algumas das nossas técnicas por trás do contexto dinâmico, muitas das quais desde então foram adotadas por outros agentes de programação. Grande parte do nosso trabalho agora se concentra em oferecer mais formas para o agente buscar contexto dinamicamente e interagir com o mundo.

Com contexto dinâmico, o modelo pode decidir quando trazer informações adicionais para a janela de contexto, como conversas anteriores, sessões ativas do terminal ou ferramentas relevantes.Com contexto dinâmico, o modelo pode decidir quando trazer informações adicionais para a janela de contexto, como conversas anteriores, sessões ativas do terminal ou ferramentas relevantes.

Duas formas de avaliar mudanças no harness

O harness e o modelo, juntos, determinam quão bom é o agente, mas é difícil definir exatamente o que significa "bom". Para chegar lá, desenvolvemos várias camadas de medição.

Mantemos benchmarks públicos junto com nosso próprio conjunto de evals, o CursorBench, que nos dá uma leitura rápida e padronizada da qualidade e nos permite comparar os resultados ao longo do tempo. Mas mesmo os melhores benchmarks só aproximam o uso real, o que significa que perderíamos sinais importantes se dependêssemos exclusivamente deles.

Por isso, também executamos experimentos online em que implementamos lado a lado duas ou mais variantes de harness e fazemos testes A/B com uso real. Medimos a qualidade do agente nesses testes por meio de várias métricas. Algumas são diretas, como latência, eficiência de tokens, número de chamadas de ferramenta e taxa de acerto de cache. Elas são úteis como indicativos, mas ainda não capturam as questões mais sutis — e mais importantes — sobre se o agente realmente fez um bom trabalho.

Medimos isso de duas formas.

A primeira é a "Keep Rate" do código gerado pelo agente. Para um determinado conjunto de alterações de código propostas pelo agente, acompanhamos que fração delas permanece na base de código do usuário após intervalos fixos de tempo. Isso nos permite entender quando os usuários precisam ajustar manualmente a saída do agente ou iterar mais e pedir que o agente corrija as coisas, indicando que a resposta inicial do agente foi de qualidade inferior.

Em segundo lugar, usamos um modelo de linguagem para ler as respostas do usuário à saída inicial do agente e captar, semanticamente, se ele ficou satisfeito ou não. Um usuário passar para o próximo recurso é um forte sinal de que o agente cumpriu seu papel, enquanto um usuário colar um stack trace é um sinal confiável de que não cumpriu.

Às vezes, esses testes online nos mostram que devemos deixar de lado uma ideia que parecia promissora. Em um experimento, testamos um modelo mais caro para sumarização de contexto e observamos que isso gerou uma diferença desprezível na qualidade do agente, que não compensava o custo mais alto.

Rastreando e corrigindo degradações

À medida que adicionamos mais modelos e capacidade, o harness fica mais complexo, com mais estados possíveis, como qualquer software. Com isso, aumenta também a superfície onde bugs podem surgir, muitos dos quais só conseguimos detectar em escala.

As ferramentas do agente são uma das maiores superfícies para bugs, e erros em chamadas de ferramenta podem ser extremamente prejudiciais para uma sessão no Cursor. Embora o agente muitas vezes consiga se autocorrigir, os erros permanecem no contexto, desperdiçando tokens e causando “deterioração do contexto”, em que erros acumulados degradam a qualidade das decisões subsequentes do modelo.

Às vezes, o agente pode ficar bloqueado ou sair totalmente dos trilhos após uma falha em uma chamada de ferramenta. Embora métricas como volume de chamadas de ferramenta e taxa de erro não meçam diretamente se o agente fez um bom trabalho, elas funcionam como indicadores que podem apontar para um problema mais amplo.

Qualquer erro desconhecido representa um bug no harness, e nós o tratamos como tal. Mas muitos erros são “esperados” — por exemplo, quando o modelo ocasionalmente propõe uma edição incorreta ou tenta ler um arquivo que não existe. Classificamos esses erros esperados por causa. InvalidArguments e UnexpectedEnvironment capturam erros do modelo e contradições na janela de contexto, enquanto ProviderError captura indisponibilidades de provedores em ferramentas como GenerateImage ou WebSearch.

Também temos várias outras classificações, como UserAborted e Timeout, que juntas abrangem a maioria dos erros esperados.

Em um sprint focado no início deste ano, levamos todas as chamadas de ferramenta a pelo menos 2 e muitas vezes 3 noves de fiabilidade.Em um sprint focado no início deste ano, levamos todas as chamadas de ferramenta a pelo menos 2 e muitas vezes 3 noves de fiabilidade.

Definimos alertas com base nessas métricas para detectar regressions significativas que chegam à production. Como erros desconhecidos são sempre bugs, geramos um alerta sempre que a taxa de erro desconhecido de qualquer ferramenta ultrapassa um limite fixo. Mas pode ser difícil dizer se erros esperados representam um bug no harness ou um comportamento esperado.

Por exemplo, um timeout de busca com grep pode acontecer por causa de um problema de performance na ferramenta, ou a base de código pode simplesmente ser enorme e o modelo ter feito uma consulta ineficiente. Para lidar com isso, temos alertas de detecção de anomalias que disparam quando erros esperados ficam significativamente acima da linha de base. Calculamos linhas de base por ferramenta e por modelo, porque modelos diferentes podem errar chamadas de ferramenta em taxas diferentes.

Também executamos uma Automação semanal equipada com uma skill que ensina o modelo a vasculhar nossos logs, identificar problemas novos ou que tiveram aumento recente e criar ou atualizar tickets em um backlog com uma investigação. Dependemos bastante de Agente na nuvem para iniciar correções de muitos problemas de uma só vez, e até podemos acioná-los diretamente no Linear.

Esse processo faz parte da forma como estamos concretizando uma “fábrica de software” automatizada para nosso harness de agente. Ao longo de um sprint focado no início deste ano, reduzimos os erros inesperados de chamada de ferramenta em uma ordem de magnitude.

Personalizando o harness para diferentes modelos

Todas as nossas abstrações de harness são agnósticas em relação ao modelo e podem ser amplamente personalizadas para cada modelo compatível. Por exemplo, os modelos da OpenAI são treinados para editar arquivos usando um formato baseado em patch, enquanto os modelos da Anthropic são treinados para substituir strings. Qualquer um desses modelos poderia usar qualquer uma das ferramentas, mas apresentar uma ferramenta que ele não conhece consome tokens extras de raciocínio e gera mais erros. Por isso, no nosso harness, fornecemos a cada modelo o formato de ferramenta com o qual ele foi treinado.

Essa personalização é bastante profunda e inclui prompts personalizados para diferentes provedores e até para diferentes versões de modelo. Os modelos da OpenAI tendem a ser mais literais e precisos ao seguir instruções, enquanto o Claude é um pouco mais intuitivo e mais tolerante a instruções imprecisas.

Quando temos acesso antecipado a um novo modelo antes do lançamento, começamos com o harness do modelo existente mais próximo e passamos a iterar. Executamos evals offline para identificar onde o modelo se confunde, pedimos para pessoas da nossa equipe usá-lo e relatar problemas, e ajustamos o harness em resposta. Iteramos assim até chegar a uma combinação de modelo e harness que consideramos boa para entregar.

Grande parte desse processo de ajuste envolve personalizar o harness para os pontos fortes de um novo modelo, mas às vezes encontramos peculiaridades reais do modelo que conseguimos mitigar com o harness. Por exemplo, observamos um modelo desenvolver o que passamos a chamar de ansiedade de contexto: à medida que sua janela de contexto ia enchendo, ele começava a recusar tarefas, alegando que a tarefa parecia grande demais. Conseguimos reduzir esse comportamento com ajustes no prompt.

Facilitando a alternância de modelos no meio do chat

É especialmente difícil projetar o harness para dar suporte à alternância de modelos no meio de uma conversa, porque modelos diferentes têm comportamentos, prompts e formatos de ferramentas distintos.

Quando um usuário alterna de modelo, o Cursor muda automaticamente para o harness apropriado, com o conjunto personalizado de prompts e ferramentas desse modelo. No entanto, o modelo ainda precisa aplicar essas ferramentas a um histórico de conversa gerado por um modelo diferente e que está fora da distribuição na qual foi treinado.

Para resolver isso, adicionamos instruções personalizadas que informam ao modelo quando ele está assumindo uma conversa no meio do chat a partir de outro modelo. Essas instruções também o orientam a evitar chamar ferramentas que aparecem no histórico da conversa, mas não fazem parte do seu próprio conjunto de ferramentas.

Impedindo que os modelos chamem ferramentas que não fazem parte do seu conjunto de ferramentasImpedindo que os modelos chamem ferramentas que não fazem parte do seu conjunto de ferramentas

Um segundo desafio é que os caches são específicos de cada provedor e modelo, então alternar significa um cache miss e uma primeira interação mais lenta e mais cara. Mitigamos isso resumindo a conversa no momento da alternância, o que fornece ao modelo um resumo limpo que reduz a penalidade de cache. Mas, se o usuário estiver avançado em uma tarefa complexa, o resumo pode perder detalhes importantes, por isso geralmente recomendamos manter um único modelo durante toda a conversa, a menos que você tenha um motivo para alternar.

Outra forma de contornar os desafios da alternância de modelos no meio da conversa é usar um subagente, que começa com uma nova janela de contexto. Recentemente, adicionamos ao harness a capacidade de os usuários pedirem diretamente que um subagente seja executado com um modelo específico.

A harness e o futuro do desenvolvimento de software

O futuro da engenharia de software assistida por IA será multiagente. Em vez de passar cada subtarefa por um único agente, o sistema aprenderá a delegar entre agentes e subagentes especializados: um para planejamento, outro para edições rápidas e um terceiro para depuração, cada um focado no que faz melhor.

Fazer isso funcionar bem é, fundamentalmente, um desafio da harness. O sistema precisa saber qual agente acionar, como estruturar a tarefa de acordo com os pontos fortes desse agente e como integrar os resultados em um fluxo de trabalho coeso. A capacidade de orquestrar esse tipo de coordenação estará na harness, e não em um agente isolado. Isso significa que, embora a engenharia dessa harness sempre tenha sido importante para o sucesso do agente, ela só vai se tornar ainda mais crítica daqui para a frente.

Publicado em: Pesquisa

Autors: Stefan Heule & Jediah Katz