Strategy Pattern para trocar de modelo de IA sem desmontar sua aplicação

A versão mais comum de “suportar mais de um provedor de IA” começa com um if. Depois vira um switch. Depois vira um switch dentro de outro switch, decidindo não só qual provedor usar, mas em qual ambiente. Chega um ponto em que ninguém mais quer mexer naquele método.

O problema que Strategy Pattern resolve aqui

Strategy Pattern define uma família de algoritmos intercambiáveis atrás de uma interface comum, deixando o código que consome esse comportamento livre de saber qual implementação específica está sendo usada. Aplicado a provedores de IA, isso significa: uma interface (IChatClient, do Microsoft.Extensions.AI, serve perfeitamente para esse papel) e implementações diferentes por trás dela, uma para cada provedor.

public interface IModeloEstrategia
{
    Task<string> ExecutarAsync(string prompt, CancellationToken cancellationToken);
}

public class OpenAiEstrategia(IChatClient chatClient) : IModeloEstrategia
{
    public async Task<string> ExecutarAsync(string prompt, CancellationToken cancellationToken)
    {
        var resposta = await chatClient.GetResponseAsync(prompt, cancellationToken: cancellationToken);
        return resposta.Text;
    }
}

Até aqui, isso é só a abstração de sempre. A parte que resolve o problema do if gigante é como a estratégia certa é escolhida em tempo de execução, sem que quem chama precise conhecer a lógica de decisão.

Keyed Services: registrar mais de uma implementação da mesma interface

Um obstáculo comum antes do .NET 8 era registrar múltiplas implementações da mesma interface na injeção de dependência e escolher entre elas de forma limpa. Keyed Services, introduzido no .NET 8, resolve exatamente isso: permite registrar várias implementações da mesma interface, cada uma associada a uma chave, e resolver a implementação certa por essa chave em vez de por tipo.

builder.Services.AddKeyedSingleton<IModeloEstrategia, OpenAiEstrategia>("producao");
builder.Services.AddKeyedSingleton<IModeloEstrategia, OllamaEstrategia>("desenvolvimento");
builder.Services.AddKeyedSingleton<IModeloEstrategia, ClaudeEstrategia>("tarefa-complexa");

Cada chave representa um critério de escolha diferente: ambiente de execução, tipo de tarefa, ou qualquer outro eixo de decisão que o projeto precise. A resolução acontece via [FromKeyedServices] ou pelos métodos de extensão de resolução por chave, sem precisar de fábrica manual escrita à mão para decidir qual implementação injetar.

public class RoteadorDeModelo(IServiceProvider serviceProvider)
{
    public IModeloEstrategia Obter(string chave) =>
        serviceProvider.GetRequiredKeyedService<IModeloEstrategia>(chave);
}

Onde mora a decisão de qual chave usar

Keyed Services resolve o registro e a resolução, não decide sozinho qual chave usar em cada chamada. Essa decisão continua sendo lógica de aplicação: pode vir de configuração (ambiente atual), de uma característica da tarefa (complexidade estimada, tamanho esperado da resposta) ou de uma regra de negócio explícita. O ganho aqui não é eliminar a decisão, é isolar essa decisão num único lugar, em vez de espalhar condicional por cada trecho de código que precisa chamar um modelo.

O que isso evita, na prática

Sem essa estrutura, é comum ver o mesmo tipo de condicional repetido em vários pontos do código, cada chamador decidindo de novo, muitas vezes de forma um pouco diferente, qual provedor usar naquele contexto. Isso cria inconsistência (um lugar decide por variável de ambiente, outro por configuração, outro hardcoded) e torna trocar a regra de decisão um trabalho de caça a todos os lugares que precisam ser atualizados.

Com Strategy Pattern e Keyed Services, a decisão de qual implementação usar fica centralizada, o comportamento de cada provedor fica isolado na própria implementação da estratégia, e adicionar um provedor novo significa criar uma nova classe e registrar uma nova chave, sem tocar em nenhum código que já funciona.

Isso não substitui abstração de provedor, complementa

Vale deixar claro que Strategy Pattern e Keyed Services não competem com abstrações como IChatClient. Eles resolvem um problema diferente: IChatClient abstrai o SDK específico de cada provedor. Strategy Pattern e Keyed Services resolvem qual implementação concreta dessa abstração deve ser usada em cada situação. As duas coisas trabalham juntas, não uma no lugar da outra.