Clean Architecture para aplicações com LLM: o que realmente precisa mudar?

Em algum momento, alguém decide que um caso de uso precisa chamar um modelo de linguagem, e a forma mais rápida de fazer isso funcionar é instanciar o client da OpenAI (ou de qualquer outro provedor) direto ali dentro, no mesmo método que já contém a regra de negócio daquele caso de uso. Funciona. Compila. E é exatamente o tipo de acoplamento que Clean Architecture existe para evitar, só que agora aplicado a uma dependência que praticamente todo mundo está adicionando de um jeito ou de outro.

Um provedor de IA é só mais um detalhe de infraestrutura

A Dependency Rule de Clean Architecture diz que código de domínio não deveria depender de detalhe externo: banco de dados, framework web, biblioteca de terceiros. Um provedor de LLM se encaixa exatamente nessa categoria. É um serviço externo, com SDK próprio, formato de mensagem próprio, forma própria de lidar com erro e limite de taxa. Nada disso é regra de negócio. É infraestrutura, do mesmo jeito que uma chamada a um banco de dados ou a uma API de pagamento é.

O caso de uso que decide “gerar um resumo desse pedido” é regra de aplicação. Como esse resumo é gerado, se é via GPT, via Claude, via um modelo rodando localmente, é detalhe de implementação que não deveria vazar para dentro do caso de uso.

Onde a interface mora importa mais do que parece

A interface que representa “preciso conversar com um modelo de linguagem” (algo como IChatClient, do Microsoft.Extensions.AI, ou uma abstração própria do projeto) deveria ser definida na camada de Application, como uma porta que a Application declara precisar, não como algo que a Infrastructure impõe de fora. Quem implementa essa porta, o adapter que efetivamente fala com o SDK do provedor escolhido, fica na Infrastructure.

// Application
public interface IResumoService
{
    Task<string> GerarResumoAsync(Pedido pedido, CancellationToken cancellationToken);
}

// Infrastructure
public class ResumoService(IChatClient chatClient) : IResumoService
{
    public async Task<string> GerarResumoAsync(Pedido pedido, CancellationToken cancellationToken)
    {
        var prompt = $"Resuma o pedido {pedido.Id} em uma frase.";
        var resposta = await chatClient.GetResponseAsync(prompt, cancellationToken: cancellationToken);
        return resposta.Text;
    }
}

O caso de uso depende de IResumoService, não de IChatClient e muito menos do SDK específico de um provedor. Trocar de provedor, ou até trocar de estratégia (chamar um modelo local em vez de um serviço externo), vira um problema isolado na Infrastructure, sem exigir tocar em nada que representa regra de negócio.

O que isso muda na prática, para quem testa

Esse isolamento paga a conta na hora de testar. Um caso de uso que depende de IResumoService pode ser testado com uma implementação fake que devolve um texto fixo, sem chamar nenhum provedor de verdade, sem gastar token, sem depender de rede disponível durante o teste. O comportamento do caso de uso (o que ele faz com o resumo depois de recebido, que validação aplica, que evento dispara) fica testável isoladamente da parte que efetivamente conversa com um LLM.

O que realmente muda, então

Estruturalmente, quase nada. Se um projeto já segue Clean Architecture de forma razoável, adicionar um provedor de IA não pede uma camada nova, nem um padrão diferente do que já seria usado para qualquer outra dependência externa. Pede só a mesma disciplina de sempre: caso de uso não conhece SDK de fornecedor, interface pertence a quem precisa dela, e implementação concreta fica isolada onde detalhe de infraestrutura já mora.

A parte que exige mais cuidado não é arquitetural, é de comportamento: decidir o que fazer quando aquele provedor externo demora, falha ou responde algo inesperado, o que já é assunto para outro texto.