Pular para o conteúdo principal

Rotas LLM explícitas por node: por que "provider default" é um bug de arquitetura

· 8 min para ler
Gui
AI Engineer · Engenharia de Sistemas Multiagentes
Cinco dimensões de uma rota LLM (provider, identidade, deployment, protocolo e SDK) ligadas a um router fail-closed por node

A dor em produção

O incidente começa sem stack trace útil. Um node de classificação passa a devolver texto livre em vez de JSON; outro node de planning "pensa" demais e estoura latência; o embedder muda a dimensão do vetor e o índice deixa de aceitar writes. Ninguém deployou um modelo novo de propósito. O que mudou foi um default: OPENAI_API_KEY apontando para um gateway, um SDK que escolheu outro deployment, um fallback que "só usou o client global".

Em sistemas agênticos com vários nodes, a rota LLM é contrato. Se o contrato for implícito - "usa o provider default" - você não tem arquitetura; tem sorte operacional.

Tese

Provider default é bug de arquitetura, não conveniência. Cada node que chama um modelo deve declarar, de forma explícita e versionada, qual rota LLM usa. Um router central valida compatibilidade entre as dimensões da rota e falha fechado (fail-closed) se o node não estiver cadastrado ou se a combinação for inválida. Não há fallback silencioso para "qualquer modelo que responda".

Essa regra vale para chat/completion, structured output, reasoning e embeddings. Capability não é atributo da marca do provider; é atributo do deployment concreto.

As cinco dimensões (não colapse nenhuma)

Tratar "OpenAI" ou "Anthropic" como um único botão mistura cinco eixos distintos. Colapsá-los é a raiz da maioria dos bugs de roteamento.

  1. Provider (credencial / endpoint). Quem autentica e para onde a request vai: API key, project, base URL, tenant, region. Provider é conta e endpoint, não o nome do modelo. O mesmo "modelo comercial" pode existir em cloud do vendor, em Azure/Bedrock/Vertex, ou atrás de um gateway interno - com credenciais e SLAs diferentes.
  2. Identidade do LLM. O que você quer semanticamente: família e papel ("classificador barato", "reasoner forte", "summarizer"). Identidade é intenção de produto/engenharia, estável no desenho do grafo. Não é string de API.
  3. Model / deployment. A unidade real de runtime: model id + versão + região + quota + flags (context window, multimodal, defaults de sampling do deployment, etc.). Temperature na request não é a mesma coisa que capability pinada no cadastro da rota. Em clouds gerenciados, "deployment name" ≠ "model family name". É aqui que vivem custo, latência e disponibilidade.
  4. Protocolo de transporte. O wire format da conversa: Chat Completions OpenAI-compatible, Anthropic Messages, Responses API, gRPC interno, etc. Protocolo define headers, schema de messages/tools, streaming e códigos de erro. Protocolo ≠ provider. Um gateway OpenAI-compatible pode falar protocolo OpenAI contra backends que não são OpenAI.
  5. Engine / SDK. A biblioteca que monta a request: SDK oficial, cliente HTTP fino, abstração LangChain (BaseChatModel), runtime próprio. O SDK traduz capabilities para parâmetros; se ele esconde defaults, ele vira parte do bug.

Regra prática: se você só consegue nomear "provider = openai", falta mapear as outras quatro dimensões. Sem isso, logs e feature flags mentem.

Config por node + router fail-closed

Config explícita. Cada node do grafo (ou cada role reutilizável) carrega uma referência imutável a uma rota cadastrada - por exemplo route_id: classifier.v3. A definição da rota declara as cinco dimensões e as capabilities exigidas:

  • provider + endpoint
  • identidade lógica
  • model/deployment pinado
  • protocolo
  • engine/SDK
  • capabilities: structured_output, tools, reasoning, vision, embeddings, dimensão do vetor, etc.

Não existe "se não especificar, usa o default do ambiente". Ambiente fornece secrets e endpoints disponíveis; não escolhe a rota do node.

Router fail-closed. Na inicialização (preferível) e em cada resolve:

  1. Node pede route_id.
  2. Router busca o cadastro; ausência = erro duro.
  3. Valida compatibilidade: provider × deployment × protocolo × capabilities pedidas pelo node.
  4. Só então instancia/recupera o client.

Fail-closed significa: node não cadastrado, deployment sem a capability pedida, ou protocolo incompatível com o SDK → a aplicação não sobe, ou a chamada aborta com erro explícito. Nunca "tenta outro modelo". Fallback silencioso transforma incidente de config em drift de comportamento - pior classe de falha em agentes, porque o grafo continua "funcionando" com semântica errada.

Em grafos (LangGraph e equivalentes), o node é a unidade de trabalho; a escolha do modelo deve ser input de configuração do node, não efeito colateral de um singleton global. A abstração de chat model (ex.: BaseChatModel no LangChain) ajuda a invocar de forma uniforme; ela não autoriza um único client compartilhado a decidir qual deployment roda em qual etapa.

Structured output e reasoning são do deployment

A documentação da OpenAI descreve Structured Outputs como garantia de aderência a um JSON Schema em modelos/APIs que suportam o recurso (via response_format / modo strict, conforme a superfície da API). A Anthropic documenta a Messages API com parâmetros e capacidades por modelo (tools, thinking, etc.). Em ambos os casos, o ponto de engenharia é o mesmo:

Capability ≠ logo do provider.

Distinga duas superfícies que costumam ser confundidas: Structured Outputs (aderência a um JSON Schema - tipicamente via modo strict) versus JSON mode (garante JSON válido, mas não que o payload respeite o schema). Um gateway OpenAI-compatible pode aceitar o parâmetro de structured/strict e, no backend, degradar para JSON mode ou pior - o protocolo "parece certo"; a garantia some.

  • Structured Outputs / schema adherence pode existir no deployment A e não no B, mesmo sob o mesmo vendor comercial.
  • "Reasoning" / extended thinking depende de modelo e de flags de API - não de provider: anthropic no YAML.
  • Se o node exige schema tipado, a rota precisa declarar capability de Structured Outputs (não só "JSON"). Aceitar o parâmetro sem a garantia é o mesmo anti-padrão do fallback silencioso.

Portanto, o cadastro da rota deve marcar capabilities no deployment, e o node que exige JSON tipado deve falhar se a rota não declarar structured_output: true (ou o equivalente versionado). Prompt engineering do tipo "responda só em JSON" e JSON mode puro não substituem essa checagem: são contratos mais fracos.

O mesmo raciocínio vale para tool calling: suporte a tools, parallel tools, strict schemas e streaming de tool calls variam por deployment. Tratar "o provider X tem tools" como booleano global é o mesmo anti-padrão com outro nome.

Embeddings: o mesmo padrão, com dimensão

Embeddings são rotas LLM com contrato ainda mais rígido: modelo + dimensão + normalização + task type (quando existir). Trocar silenciosamente text-embedding-3-small por outro deployment "compatível" quebra:

  • índices vetoriais (dimensão),
  • thresholds de similaridade,
  • caches de embedding por chave de conteúdo.

Cadastre rotas de embedding como nodes/roles (embed.docs.v2, embed.query.v2), pinando deployment e dimensão. O router falha se a dimensão declarada na rota ≠ dimensão esperada pelo store. Aqui, fallback silencioso não é só bug de qualidade - é corrupção de dados.

Anti-padrões

  • Env escolhendo a rota. LLM_PROVIDER=openai + MODEL=gpt-4o no .env de staging "para facilitar". Resultado: o mesmo código em produção resolve outra rota porque o secret manager injetou outro base URL. Env deve injetar credenciais e catálogo de endpoints; a ligação node→rota vive em config versionada (código ou manifesto), revisável em PR.
  • Fallback silencioso. "Se Claude falhar, tenta GPT". Útil como política explícita de resiliência com orçamento, métricas e semântica aceita. Perigoso como default do client. Se a política existir, ela é uma rota composta nomeada (planner.primary_with_failover), não um except: use_default().
  • Um client global. Singleton openai.OpenAI() usado por todos os nodes. Além de esconder base URL/headers, impede isolation de timeout, retry e quota por papel. Prefira factory via router: mesmo código de invocação, clients distintos por rota.
  • Protocolo = provider. Assumir que "fala OpenAI API" implica "é a OpenAI" é o erro que gateways OpenAI-compatible amplificam. O protocolo unifica transporte; não unifica billing, data residency, model cards nem guarantees de structured output. Roteie por deployment cadastrado; use o protocolo só como dimensão de wiring.
  • Nome comercial como capability. if provider == "openai": use_json_schema(). Substitua por if route.capabilities.structured_output. O teste de integração sobe um deployment sem a flag e deve vermelhar o node que a exige.

Referências públicas

  • OpenAI - Structured Outputs: o guia oficial descreve aderência a JSON Schema (incl. modo strict) e a superfície por modelo/API - trate suporte como propriedade do deployment exposto, não como premissa do SDK. Ver Structured Outputs. Para o contraste com JSON mode (JSON válido sem garantia de schema), veja o mesmo conjunto de guias de formato de resposta na documentação da plataforma.
  • Anthropic - Messages API: a overview e a referência de Messages documentam endpoint, model IDs e capacidades por modelo (tools, thinking, etc.). Ver API overview e a referência de Messages.
  • LangChain / LangGraph - abstração de modelo e Graph API: o núcleo LangChain separa a interface de chat models (BaseChatModel em langchain_core.language_models) das integrações concretas por provider; LangGraph trata nodes como unidades do grafo que invocam um model interface - a escolha de qual integração/deployment usar permanece responsabilidade da configuração do node, não da abstração em si. Ver langchain-core language models e a Graph API (LangGraph).

Conclusão acionável

  1. Modele cinco dimensões (provider, identidade, deployment, protocolo, engine) no cadastro de rotas - nunca um enum único Provider.
  2. Pin cada node a um route_id versionado; remova defaults silenciosos de modelo/protocolo/provider.
  3. Router fail-closed: node ausente ou incompatível aborta; zero fallback implícito.
  4. Capabilities no deployment: structured output, reasoning, tools, visão, embeddings (+ dimensão).
  5. Mesmo rigor para embeddings - dimensão e modelo pinados.
  6. Trate gateways OpenAI-compatible como protocolo, não como identidade de provider.
  7. Proíba client global único e proíba env como seletor de rota; env só entrega secrets e catálogo.

Se um node não consegue explicar, em uma linha, qual deployment e com quais guarantees ele chama, a rota ainda é um default - e default, em arquitetura de LLM, é bug.