# ADR-002 — Utilizar Qwen3.5 2B local com fallback operacional Groq e OmniRoute ## Status Accepted --- # Contexto A classificação entre: ```text text ``` e: ```text media ``` exige uma pequena decisão semântica multilíngue. O problema não exige um modelo grande. A tarefa é extremamente restrita: 1. receber texto e informações estruturais; 2. decidir se o texto é conteúdo jornalístico substancial ou apenas descrição/contextualização da mídia; 3. quando for mídia, identificar uma entre cinco categorias. O sistema precisa ser production-ready sem introduzir infraestrutura ou complexidade desnecessária. --- # Decisão Utilizar a seguinte cadeia sequencial: ```text Qwen3.5 2B / Ollama ↓ falha operacional GPT-OSS 20B / Groq ↓ falha operacional cgpt-web/gpt-5.5 / OmniRoute ``` --- # Provider primário ```text Qwen3.5 2B ``` Runtime: ```text Ollama ``` Motivos da decisão: * execução local; * modelo pequeno; * suficiente para classificação restrita; * adequado ao cenário multilíngue; * elimina custo por chamada no caminho normal; * não depende de serviço externo durante operação normal. --- # Primeiro fallback ```text GPT-OSS 20B ``` Provider: ```text Groq ``` O Groq somente é chamado quando o provider primário não consegue fornecer uma classificação tecnicamente utilizável. --- # Segundo fallback Modelo: ```text cgpt-web/gpt-5.5 ``` Provider: ```text OmniRoute ``` O OmniRoute somente é utilizado quando: ```text Ollama falhou E Groq falhou ``` --- # Tipo de fallback O fallback é exclusivamente operacional. Exemplos de falha que justificam fallback: ```text conexão recusada timeout erro HTTP provider indisponível erro de execução resposta que não pode ser consumida resposta incompatível com o schema obrigatório ``` --- # O que NÃO provoca fallback Não executar fallback quando: ```text modelo retorna text modelo retorna media resultado parece improvável resultado parece ambíguo modelo parece estar inseguro outro modelo talvez respondesse diferente ``` Não existe: * score de confiança; * votação; * consenso; * LLM-as-a-judge; * segunda opinião; * comparação de respostas. Uma resposta válida do provider atual encerra a cadeia. --- # Execução sequencial A cadeia deve ser executada sequencialmente. ```text try Ollama se resposta válida: usar resposta finalizar se falha operacional: try Groq se resposta válida: usar resposta finalizar se falha operacional: try OmniRoute ``` Não executar providers em paralelo. --- # Contrato do modelo Todos os providers devem produzir o mesmo contrato lógico. ```json { "content_type": "text", "media_type": null } ``` ou: ```json { "content_type": "media", "media_type": "video" } ``` Valores permitidos: ```text content_type: - text - media ``` ```text media_type: - video - image - images - embed - mixed - null ``` Regra: ```text text → media_type = null media → media_type obrigatório ``` --- # Structured output Sempre que suportado pelo provider, a resposta deve utilizar JSON Schema/structured output nativo. Independentemente do mecanismo do provider, a aplicação deve validar o resultado antes de aceitá-lo. Não adicionar parsing tolerante complexo. O contrato é pequeno e fechado. Se a resposta não puder ser validada, aquela tentativa é considerada falha do provider e a cadeia avança. --- # Prompt O prompt deve ser único e pequeno. Não criar prompts diferentes por idioma. A instrução deve explicar apenas: 1. classificar `text` ou `media`; 2. `media` significa que o texto é essencialmente uma introdução ou descrição da mídia; 3. `text` significa que o artigo possui conteúdo jornalístico textual substancial; 4. quando `media`, selecionar `video`, `image`, `images`, `embed` ou `mixed`; 5. retornar exclusivamente o schema estabelecido. Não solicitar: * resumo; * justificativa; * reasoning; * confiança; * evidências; * tradução; * keywords. --- # Configuração do modelo A classificação deve utilizar comportamento determinístico sempre que o provider permitir. Não habilitar reasoning desnecessário. A configuração precisa privilegiar: ```text baixa variabilidade structured output baixa latência resposta curta ``` --- # Falha dos três providers Se: ```text Ollama falhar Groq falhar OmniRoute falhar ``` o sistema deve retornar internamente um estado de falha de classificação. Exemplo lógico: ```json { "classification_status": "failed", "error_message": "media classification providers unavailable" } ``` Não gerar automaticamente: ```text text ``` Não gerar automaticamente: ```text media ``` --- # Destino da falha O artigo com falha total: * permanece registrado no JSON principal; * não passa pelo multimotor; * não entra no `*_media.json`; * não interrompe os outros artigos do lote. Não criar: ```text *_failed.json ``` Não criar: * dead-letter queue; * banco para falhas; * worker de retry; * processo assíncrono de recuperação. --- # Configuração e secrets As informações específicas de cada provider devem ser externas ao código. Incluem, quando aplicável: ```text endpoint model API key timeout ``` Credenciais não podem aparecer: * no código-fonte; * no JSON de saída; * nos logs; * nas mensagens de erro persistidas. --- # Observabilidade Para cada classificação deve ser possível identificar operacionalmente qual caminho foi utilizado: ```text ollama groq omniroute failed ``` Devem existir logs para: ```text fallback Ollama → Groq fallback Groq → OmniRoute falha final ``` O conteúdo integral do artigo não deve ser logado por padrão. Não criar um novo sistema de observabilidade especificamente para esta feature. --- # Testabilidade A cadeia de providers deve ser testável sem depender dos serviços externos reais. Os testes devem conseguir simular: ```text Ollama success Ollama fail + Groq success Ollama fail + Groq fail + OmniRoute success todos falham provider retorna schema inválido ``` Não é necessário executar chamadas reais aos três providers na suíte normal de CI. --- # Alternativas consideradas ## Apenas Ollama Rejeitada. O sistema é production-ready e precisa continuar funcionando caso o runtime local esteja indisponível. --- ## Ollama + Groq apenas Rejeitada. Foi definido um terceiro fallback já disponível através do OmniRoute. --- ## Chamar vários modelos e escolher maioria Rejeitada. Não existe requisito que justifique consenso. Aumentaria: * custo; * código; * latência; * pontos de falha. --- ## Fallback baseado em confidence Rejeitada. Não existe requisito de score de confiança e não deve existir interpretação adicional da resposta. --- ## Modelo grande como principal Rejeitada. A tarefa é pequena e fechada. Qwen3.5 2B atende ao objetivo com custo operacional mínimo. --- ## Criar serviço separado de classificação Rejeitada. A classificação pertence ao fluxo atual e não exige um novo deployable. --- # Consequências positivas * custo marginal mínimo no caminho principal; * operação local normalmente; * alta disponibilidade através de dois fallbacks externos; * implementação pequena; * comportamento previsível; * nenhum acoplamento a framework de agentes; * fácil teste; * fácil troca futura de provider através da mesma interface lógica. --- # Trade-off aceito Em uma indisponibilidade simultânea de Ollama, Groq e OmniRoute, o artigo não será processado como texto nem mídia. Essa é uma decisão intencional. É preferível registrar explicitamente: ```text classification_failed ``` a mascarar uma falha operacional tomando uma decisão que o sistema não conseguiu realizar. --- # Invariantes ```text Ollama é sempre o primeiro provider. Groq só é chamado após falha operacional do Ollama. OmniRoute só é chamado após falha operacional de Ollama e Groq. Resposta válida encerra a cadeia. Nunca existe votação. Nunca existe fallback por confiança. Falha dos três nunca gera classificação presumida. ```