Pular para o conteúdo

ADR-0002: Abstração de fornecedor de LLM

  • Status: Accepted
  • Data: 2026-03-18
  • Responsáveis pela decisão: Waldemar Szemat

O agente chama um endpoint de chat-completion várias vezes por turno (classificador de escopo, redator, verificação de segurança, juiz). A tese do projeto inclui a afirmação de que o agente é neutro em relação a fornecedores e de que a mesma base de código pode rodar contra OpenAI, Anthropic ou Groq com uma única variável de ambiente. Essa afirmação tem que ser honrada por código, não por texto de documentação.

Ao mesmo tempo, o projeto opera sob um orçamento rigidamente limitado, então o caminho implantado da demo roda uma cadeia de modelos padrão econômica — OpenAI gpt-4o-mini como primário, com Anthropic claude-haiku-4-5 como fallback automático — e o caminho do juiz de avaliação em CI roda em sua própria cadeia de provedores. Qualquer um dos três provedores suportados deve ser plugável a partir das chaves do usuário sem mudanças de código, de modo que trocar o provedor ativo seja uma única mudança de ambiente.

Como expomos uma interface única e estável para completions de LLM dentro do agente e dentro do harness de avaliação, mantendo o acesso a três fornecedores e a opção de adicionar mais depois?

  • Uma superfície de chamada coerente para o agente e o harness de avaliação; sem ramificação por fornecedor no código dos nós
  • Consciente de custo por padrão: a demo implantada roda uma cadeia de modelos padrão econômica (OpenAI gpt-4o-mini, Anthropic como fallback)
  • Economia de CI: os avaliadores determinísticos que bloqueiam PRs não precisam de LLM nenhum; o juiz apoiado em LLM roda fora do caminho crítico que bloqueia PRs
  • Realismo de produção: um usuário com chaves pagas de OpenAI ou Anthropic obtém uma experiência quase idêntica ao alternar o LLM_PROVIDER
  • Evitar lock-in pesado de framework: queremos a liberdade de descartar os adaptadores de provedor da LangChain depois sem reescrever os nós do agente
  • Tipagem forte em requisições e respostas (mensagens tipadas com Pydantic), consistente com a postura mypy --strict
  • Protocol LLMClient fino sobre LangChain langchain-openai + langchain-anthropic mais um adaptador direto Groq via um cliente REST compatível com OpenAI, alternado por uma variável de ambiente LLM_PROVIDER
  • ChatModel direto da LangChain em todo lugar: usar langchain_openai.ChatOpenAI, langchain_anthropic.ChatAnthropic, etc. diretamente dentro dos nós do agente
  • Proxy / SDK do LiteLLM: chamar todos os provedores pela camada no formato OpenAI do LiteLLM
  • Somente SDKs nativos dos fornecedores: ignorar qualquer abstração, escrever três conjuntos de chamadas específicas de fornecedor
  • OpenRouter (ou roteador similar): um endpoint HTTP, muitos provedores selecionados por nome de modelo

Opção escolhida: Protocol LLMClient fino, com adaptadores concretos que envolvem os clientes de provedor da LangChain para OpenAI e Anthropic e que chamam o Groq diretamente pelo seu endpoint REST compatível com OpenAI. O Protocol expõe um pequeno conjunto de métodos (chat completion, chat completion com streaming, contagem de tokens). A seleção de fornecedor é uma única variável de ambiente LLM_PROVIDER in {openai, anthropic, groq}, resolvida por fábrica na inicialização do processo.

O padrão implantado resolve para um gerador OpenAI gpt-4o-mini com um fallback Anthropic claude-haiku-4-5; o Groq é um fornecedor suportado de primeira classe e serve como um nível de fallback na cascata do juiz de avaliação.

Esta opção preserva o valor de opção de trocar a LangChain depois (o agente nunca importa tipos da LangChain diretamente), dá ao harness de avaliação uma interface estável e testável, e corresponde ao realismo que o projeto precisa: um usuário com chaves pagas alterna uma variável de ambiente e o mesmo agente roda contra o fornecedor de sua preferência.

  • Todo nó do agente e todo avaliador de avaliação que precise de um LLM importa o Protocol LLMClient, não uma classe de provedor
  • Um smoke test de CI importa cada adaptador (OpenAI, Anthropic, Groq) e verifica que eles implementam LLMClient
  • Um teste de integração de CI exercita pelo menos dois provedores de ponta a ponta com um prompt enlatado curto para validar a afirmação “agnóstico de fornecedor”
  • O LLM_PROVIDER é documentado na referência de configuração do projeto e no arquivo de ambiente de exemplo
  • O código do agente e da avaliação conversa com um Protocol; a troca de fornecedor é uma mudança de ambiente, não uma mudança de código
  • A cadeia de modelos padrão econômica mantém baixo o custo por turno enquanto ainda demonstra padrões realistas de produção
  • Realismo de produção: um leitor tecnicamente rigoroso pode colar sua chave de OpenAI ou Anthropic e rodar o mesmo fluxo
  • O Protocol é pequeno (seis métodos ou menos) e trivialmente mockável, o que mantém enxuta a superfície de testes unitários
  • A LangChain permanece um detalhe de implementação de dois adaptadores, não um framework tecido por toda a base de código
  • Dois dos adaptadores dependem de pacotes de provedor da LangChain (langchain-openai, langchain-anthropic); aceitamos isso em troca de não reimplementar nuances de uso de ferramentas, function-calling e streaming
  • O adaptador REST compatível com OpenAI para Groq tem que lidar com casos de borda (cabeçalhos de rate-limit, formato de chunk de streaming) que a LangChain trata para os provedores nativos
  • A superfície do Protocol precisa evoluir com cuidado; uma mudança incompatível no Protocol significa tocar cada adaptador e cada nó
  • O projeto carrega três adaptadores; as cadeias ativas do gerador e do juiz os usam como níveis primário e de fallback
  • A instrumentação de tokens-por-turno e ms-por-turno fica na camada de adaptador, não no ponto de chamada
  • O streaming é opcional: o Protocol expõe um método de streaming mas o fluxo padrão não o exige
  • Boa, porque dá uma interface inspecionável para os três provedores
  • Boa, porque a troca de fornecedor é uma única variável de ambiente
  • Boa, porque mockar o Protocol torna os testes unitários baratos
  • Ruim, porque somos donos do código de adaptador para Groq
  • Ruim, porque a evolução do Protocol é um custo de coordenação
  • Boa, porque a LangChain já envolve todo fornecedor importante
  • Ruim, porque os nós importam tipos da LangChain diretamente, o que acopla o agente à hierarquia de classes da LangChain e quebra a postura “neutra em relação a fornecedores, leve em framework”
  • Boa, porque o LiteLLM dá uma API uniforme no formato OpenAI entre muitos provedores
  • Ruim, porque adiciona uma camada de tradução de terceiros entre o agente e os modelos upstream, com sua própria superfície de bugs e peculiaridades de observabilidade
  • Ruim, porque a semântica de rate-limit do Groq é mais fácil de honrar conversando diretamente com ele
  • Boa, porque zero sobrecarga de abstração
  • Ruim, porque cada nó carregaria código específico de fornecedor; a afirmação “agnóstico de fornecedor” se torna falsa no código
  • Boa, porque um endpoint para muitos modelos
  • Ruim, porque adiciona um intermediário que não é gratuito no volume que um harness de avaliação consegue gerar, e obscurece qual provedor de fato atendeu um dado turno