Cache em aplicações de IA: você precisa mesmo perguntar a mesma coisa de novo?

Em qualquer aplicação com perguntas e respostas, uma parte das perguntas costuma se repetir: a mesma dúvida de suporte, a mesma consulta de catálogo, o mesmo pedido de resumo do mesmo documento. Chamar o modelo de novo para cada uma delas significa pagar de novo por uma resposta que já existe.

Existem duas formas de evitar isso, e elas resolvem problemas diferentes.

Cache exato: a mesma entrada, a mesma saída

O cache exato usa a entrada completa da chamada como chave. Se a mesma sequência de mensagens chegar de novo, devolve a resposta guardada em vez de chamar o modelo. No Microsoft.Extensions.AI isso já vem como middleware: o DistributedCachingChatClient envolve qualquer IChatClient e usa um IDistributedCache para guardar as respostas.

builder.Services.AddDistributedMemoryCache();

builder.Services.AddChatClient(clienteBase)
    .UseDistributedCache();

Em produção com mais de uma instância, o cache em memória perde o sentido, porque cada instância teria o seu. A troca é só a implementação de IDistributedCache, por exemplo com Redis:

builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");

Dois pontos para checar. O primeiro é que um cache exato congela uma amostra: como as respostas de um LLM variam, a primeira resposta gerada vira “a” resposta até o cache expirar. O segundo é conferir, na versão que estiver em uso, o que entra na chave além das mensagens (modelo, temperatura, ferramentas), porque uma chave que ignora essas opções devolve respostas geradas em outras condições.

Cache semântico: perguntas parecidas, mesma resposta

“Como cancelo meu pedido?” e “Quero cancelar uma compra, como faço?” não são idênticas, mas provavelmente merecem a mesma resposta. O cache semântico gera o embedding da pergunta, procura no cache uma entrada com vetor próximo o bastante e, se achar, reaproveita a resposta.

Um esboço em memória, só para mostrar o mecanismo (os nomes exatos dos métodos de embedding variam entre versões da biblioteca):

public class CacheSemantico(
    IEmbeddingGenerator<string, Embedding<float>> embeddings,
    IChatClient chat)
{
    private readonly List<(ReadOnlyMemory<float> Vetor, string Resposta)> _entradas = [];
    private const float Limiar = 0.95f;

    public async Task<string> ResponderAsync(string pergunta, CancellationToken ct)
    {
        var geradas = await embeddings.GenerateAsync([pergunta], cancellationToken: ct);
        var vetor = geradas[0].Vector;

        foreach (var (v, resposta) in _entradas)
        {
            if (TensorPrimitives.CosineSimilarity(vetor.Span, v.Span) >= Limiar)
                return resposta;
        }

        var nova = (await chat.GetResponseAsync(pergunta, cancellationToken: ct)).Text;
        _entradas.Add((vetor, nova));
        return nova;
    }
}

O número que decide tudo é o limiar. Baixo demais, e perguntas diferentes recebem a resposta errada. Alto demais, e o cache quase nunca acerta. E existe um caso traiçoeiro: “cancelar o pedido 123” e “cancelar o pedido 124” ficam muito próximas em embedding e têm respostas diferentes. Cache semântico serve para perguntas cuja resposta não depende de detalhes específicos, e é um mau candidato para tudo que carrega identificador, valor ou data.

O que fica de fora do cache

Resposta personalizada não deveria ser compartilhada entre usuários. Uma pergunta que depende de dado atual (“qual é o saldo?”) fica velha assim que o dado muda. E respostas que usaram ferramentas dependem do estado do sistema no momento da chamada. Guardar qualquer uma dessas sem escopo (por usuário, por tenant) e sem expiração é a forma mais fácil de entregar a informação de uma pessoa para outra.

Não confundir com o cache do provedor

Alguns provedores oferecem prompt caching, que reaproveita o processamento de um prefixo repetido do prompt (instruções longas, documentos) para reduzir custo e latência daquela parte. Isso é diferente: o modelo ainda é chamado e ainda gera uma resposta nova. Os dois podem coexistir, e resolvem problemas distintos.