Corretor de Redação do ENEM

Bot multicanal (WhatsApp + Telegram) que corrige redações do ENEM a partir de uma foto do manuscrito: o aluno manda a foto e recebe a nota estimada por competência (0–1.000), com feedback pedagógico que cita trechos exatos do próprio texto. Sem instalar app, com privacidade pensada para um público menor de idade.

FastAPIRedis embutidoMistral OCRLLM plugável💬 WhatsApp✈️ Telegram
Concepção, arquitetura e desenvolvimento — Ygor Carvalho · Engenharia de IA
5
competências (matriz INEP)
0–1000
nota estimada
2
canais sem app
45min
TTL — nada persiste depois

Corretor ENEM 📝

Bot multicanal (WhatsApp + Telegram) que corrige redações do ENEM a partir de uma foto do manuscrito: o aluno informa o tema, cola os textos motivadores (se tiver) e manda a foto — e recebe de volta a nota estimada (0 a 1000) por competência, com feedback pedagógico citando trechos exatos do próprio texto.

Como funciona

Toda a correção acontece dentro de uma conversa de WhatsApp ou Telegram, a partir de uma foto do manuscrito — o aluno não sai do mensageiro:


Aluno                    Bot
  │  /corrigir            │
  │◀── "digite o tema" ───┤   tema e motivadores: SÓ texto digitado
  │  tema (texto)         │   (OCR é reservado para a redação)
  │◀── "textos motivad.?"─┤
  │  textos / Pular       │
  │◀── "manda a foto" ────┤
  │  📷 foto da redação   │──▶ download (background) → Mistral OCR
  │◀── preview do OCR ────┤   foto descartada; só o texto segue
  │  [Editar texto]       │   opcional: aluno cola o texto ajustado
  │◀── eco p/ confirmar ──┤   (letra difícil → OCR imperfeito)
  │  [Corrigir agora]     │──▶ LLM + matriz de correção do ENEM
  │◀── nota + feedback ───┤   5 competências, trechos citados

O fluxo, etapa por etapa

  • Tema e textos motivadores entram como texto digitado (o OCR fica reservado para a redação); os motivadores são opcionais.
  • A foto da redação (até 3 imagens, frente/verso) chega ao webhook, que apenas valida, responde 200 em <200 ms (SLA da Meta) e despacha o trabalho pesado — download da mídia, OCR e LLM — para uma task asyncio. Nunca no handler HTTP.
  • O OCR (Mistral mistral-ocr-latest) transcreve só a foto da redação. Em seguida a foto é descartada: apenas o texto segue adiante.
  • Preview e edição manual: o aluno vê a transcrição com as palavras de baixa confiança destacadas e pode ajustar o que a letra difícil fez o OCR errar; o bot ecoa a versão final para confirmação antes de corrigir.
  • A correção vai para um LLM com prompt de corretor especialista baseado na Matriz de Referência do INEP (5 competências, triagem de nota zero, checklist da proposta de intervenção). Volta a nota estimada por competência (0–200 cada; 0–1000 no total) e o feedback citando trechos, formatado para o mensageiro (sem tabelas/#).

Arquitetura

  • Mono-container: FastAPI + Redis no MESMO contêiner. O entrypoint.sh sobe o redis-server (só em 127.0.0.1, nunca exposto fora do contêiner), espera o PING responder e só então executa o uvicorn. Um único deploy, sem Redis gerenciado à parte.
  • Estado 100% em Redis (sem banco relacional): máquina de estados da conversa (TTL 45 min), consentimento LGPD, rate-limit diário e idempotência de webhook. O AOF em /data (volume) sobrevive a restart do contêiner.
  • Réplica única, sempre: como o Redis vive dentro do contêiner, ele não é compartilhado entre réplicas — replicas > 1 fragmentaria rate-limit, consentimento e estado de fluxo (cada réplica veria um Redis diferente). Mantenha 1 réplica (folgado para o tráfego de uma escola).
  • Correção plugável: o provedor do LLM troca por env var (CORRECTION_PROVIDER: gemini | claude | openai | deepseek | maritaca; CORRECTION_MODEL define o modelo dentro dele — default deepseek / deepseek-v4-flash, adotado por custo × qualidade), o que permite comparar modelos rápido. O prompt é dividido em bloco estático (matriz, ~6.000 tokens) + bloco dinâmico (tema/motivadores/redação) para aproveitar cache de contexto — explícito no Gemini e no Claude, automático no OpenAI/DeepSeek (a Maritaca não documenta cache; o bloco estático é reenviado sem desconto confirmado).

Por que mensageria, e não um app de IA

WhatsApp e Telegram costumam continuar acessíveis em redes escolares que bloqueiam ChatGPT/Gemini/Claude — a correção chega por um canal que já não é bloqueado. E como a entrada é uma foto do manuscrito (não texto digitado), o treino continua fiel ao formato real da prova: o aluno escreve à mão, no papel, como no dia do ENEM.

LGPD (o público é menor de idade)

  • Consentimento explícito no primeiro contato, antes de qualquer processamento.
  • A foto é descartada logo após o OCR; o texto transcrito vive apenas no estado Redis, que expira em 45 min. Nada persiste após a correção (não há histórico).
  • O feedback deixa claro que a correção é feita por IA e que a nota é uma estimativa (no ENEM oficial são dois corretores humanos).
  • Rate-limit diário (ESSAY_DAILY_LIMIT, default 2) — controle de custo do fluxo OCR + LLM.

Rodando local

Requisitos: Python 3.12, Docker, chaves MISTRAL_API_KEY e GOOGLE_API_KEY.

O Makefile embrulha os comandos multi-passo (make help lista tudo). O único passo manual é preencher as chaves:


cp .env.example .env       # preencha as chaves
make up                    # 1 único contêiner: app + redis embutido, porta 8000

Sem provedor de mensageria configurado, teste o fluxo completo pelo simulador de terminal (OCR e correção reais). Como o Redis normalmente vive dentro da imagem, para rodar o simulador FORA do Docker suba um Redis solto só para o dev:


make setup                 # cria o venv e instala deps de dev (só na 1ª vez)
make redis-dev             # redis avulso, só para dev sem Docker Compose
make simulate
# 👤 > /corrigir
# 👤 > Desafios para a valorização de comunidades tradicionais no Brasil
# 👤 > btn skip_motivators
# 👤 > foto /caminho/para/redacao.jpg
# 👤 > btn confirm_correct

Testes e lint


make test                  # puros, sem I/O (Redis fake, LLM/OCR mockados)
make lint                  # ruff check; `make fmt` para auto-corrigir e formatar

Conectando os canais

Telegram é o canal recomendado. A partir de 01/10/2026, o WhatsApp Business Platform passa a cobrar por mensagem de serviço (a categoria de conversa que este bot usa) — o Telegram Bot API continua gratuito. Para manter o custo operacional no menor patamar possível, prefira o Telegram como canal principal; o WhatsApp fica como opção para quem já opera nesse canal e absorve o custo por mensagem.

O teste real mais barato é o Telegram (token grátis no @BotFather) com um túnel local:


make dev             # sobe o app + túnel ngrok persistente, imprime a URL pública
curl "https://api.telegram.org/bot$TOKEN/setWebhook" \
  -d "url=https://SEU-TUNEL/v1/webhook/telegram" \
  -d "secret_token=$TELEGRAM_WEBHOOK_SECRET"      # OBRIGATÓRIO em produção

O túnel do make dev fica de pé mesmo depois de Ctrl+C (só o app cai) — reiniciar o app com make dev de novo reaproveita a mesma URL, sem precisar refazer o setWebhook. Para encerrar o túnel de vez: make tunnel-stop.

WhatsApp (Meta Cloud API): crie um app no Meta for Developers, aponte o webhook para https://SEU-HOST/v1/webhook/whatsapp usando o META_VERIFY_TOKEN do .env, e preencha PHONE_NUMBER_ID, WHATSAPP_TOKEN e META_APP_SECRET.

Em produção, ENVIRONMENTlocal liga a verificação de assinatura: HMAC SHA-256 (X-Hub-Signature-256) no WhatsApp e secret token no Telegram.

Deploy (Azure Container Apps)

O docker-compose é a forma canônica de rodar; para produção o alvo é o Azure Container Apps, no mesmo padrão do projeto pai:

  • 1 único Container App (imagem deste Dockerfile — Redis já embutido, sem Azure Cache for Redis nem serviço separado).
  • minReplicas=1 e maxReplicas=1 (fixo, não escala) — o Redis embutido não é compartilhável entre réplicas; ver nota acima. Para o volume de uma escola isso é folgado.
  • Monte um Azure Files em /data (mesmo mount point da VOLUME do Dockerfile) para o AOF do Redis sobreviver a um redeploy/restart do contêiner.
  • Segredos (tokens, chaves) via Key Vault/secret refs — nunca env var em texto plano no manifesto.

Estrutura

Só o que é essencial para rodar, testar e fazer deploy do bot. O portal de

documentação renderizado fica em docs/index.html; a fonte que o gera e os

scripts de laboratório não fazem parte deste repositório.


.
├── Dockerfile              # imagem única: app + redis embutido (ver entrypoint.sh)
├── docker-compose.yml      # `make up` — forma canônica de rodar (app + redis)
├── entrypoint.sh           # sobe redis-server (127.0.0.1) → espera PING → uvicorn
├── Makefile                 # `make help` lista todos os atalhos de dev
├── pyproject.toml            # config de ruff (lint/format) e pytest
├── requirements.txt           # dependências de runtime
├── requirements-dev.txt        # + pytest/ruff (dependências de dev)
├── .env.example                 # todas as env vars documentadas — copiar para .env
│
├── src/
│   ├── main.py                    # FastAPI: webhooks (valida → enfileira task → 200)
│   ├── config.py                   # pydantic-settings — todas as env vars
│   ├── messages.py                  # toda a copy de UX (nenhuma string solta no flow/)
│   ├── prompts.py                    # prompt do corretor ENEM (Matriz do INEP; str.format, nunca f-string)
│   ├── rate_limit.py                  # limite diário de correções por aluno
│   ├── redis_client.py                 # conexão + idempotência de webhook
│   │
│   ├── channels/                        # IMessagingChannel — canais plugáveis
│   │   ├── interfaces.py                   # contrato agnóstico (InboundMessage, IMessagingChannel)
│   │   ├── factory.py                       # get_channel(name) — troca via CHANNEL_BACKEND
│   │   ├── whatsapp.py                       # WhatsApp Cloud API (Meta)
│   │   ├── telegram.py                        # Telegram Bot API
│   │   ├── markdown.py                         # *negrito*/_itálico_ → formato de cada canal
│   │   └── text_utils.py                        # chunking de mensagem longa
│   │
│   ├── flow/                             # máquina de estados da conversa (Redis)
│   │   ├── states.py                        # FlowState, FlowData, load/save/clear
│   │   └── handlers.py                       # um handler por estado do fluxo
│   │
│   └── correction/                       # OCR + correção por LLM
│       ├── ocr.py                           # Mistral OCR + palavras de baixa confiança
│       ├── corrector.py                      # lógica comum de chamada ao provedor
│       ├── interfaces.py                      # contrato ICorrector
│       ├── factory.py                          # get_corrector(provider) — troca via CORRECTION_PROVIDER
│       └── providers/                           # gemini.py, claude.py, openai_compatible.py (openai/deepseek/maritaca)
│
├── scripts/
│   └── simulate.py            # REPL de conversa com canal fake — E2E local sem provedor real
│
└── tests/
    └── unit/                     # 52 testes puros — FakeRedis + FakeChannel, sem I/O

1. Diagnóstico do Problema e Objetivos

1.1 Contexto: a redação do ENEM e o acesso ao ensino superior

A redação é a prova de maior peso simbólico do ENEM: vale de 0 a 1.000 pontos e é, para muitos cursos concorridos, o fiel da balança na disputa por vagas via SISU, ProUni e FIES. O exame reúne, a cada edição, milhões de participantes — a maioria estudantes de ensino médio ou recém-egressos — e a nota da redação costuma ser o item que mais separa aprovados de reprovados em cursos de alta demanda.

A correção segue a Matriz de Referência do INEP, que divide o texto dissertativo-argumentativo em cinco competências, cada uma pontuada de 0 a 200:

CompetênciaO que avalia
C1Domínio da norma-padrão da escrita formal
C2Compreensão do tema e repertório sociocultural
C3Seleção, organização e projeto de texto (argumentação)
C4Coesão textual (mecanismos coesivos)
C5Proposta de intervenção que respeite os direitos humanos

A nota mil é rara — algumas dezenas de redações por edição, entre milhões — justamente porque atingir o nível máximo nas cinco competências ao mesmo tempo exige treino sistemático e feedback qualificado.

Fonte: Matriz de Referência para Redação do ENEM e A Redação do ENEM — Cartilha do Participante (INEP). O material de correção usado neste projeto é derivado diretamente dessa matriz oficial.


1.2 O problema real: o gargalo é a correção, não a escrita

Praticar redação é, do ponto de vista do aluno, a parte fácil: dá para escrever um texto por dia. O gargalo está do outro lado — receber uma correção detalhada, por competência e ancorada no próprio texto — e é esse retorno que efetivamente faz a nota subir.

Corrigir bem uma única redação exige:

  • Decompor o tema e verificar aderência (fuga, tangenciamento ou abordagem completa).
  • Aplicar a triagem de nota zero (fuga total, tipo textual errado, cópia, desrespeito aos direitos humanos etc.).
  • Avaliar as cinco competências com um olho treinado, citando trechos exatos.
  • Explicar como corrigir cada problema — não apenas apontá-lo.

Feito com rigor, isso consome 15 a 20 minutos por redação e depende de alguém que conheça a matriz a fundo. Na prática:

  • O professor de redação da escola pública acompanha, sozinho, dezenas a centenas de alunos — é fisicamente impossível dar retorno detalhado e frequente a todos.
  • A correção particular ou o cursinho preparatório entregam esse feedback, mas custam caro — o que concentra o acesso a quem pode pagar.
  • O aluno que escreve muito, mas recebe pouco (ou nenhum) retorno específico, estagna: repete os mesmos erros sem saber que os comete.

O resultado é uma assimetria de acesso ao feedback que se traduz diretamente em desigualdade de desempenho na prova que mais pesa no acesso ao ensino superior.

E a IA genérica (ChatGPT, GPTs de terceiros) já não resolveria isso? Só em parte — e não no contexto onde o aluno mais precisa. Colar a redação num chatbot de propósito geral esbarra em três problemas concretos:

  • A rede da escola costuma bloquear justamente essas ferramentas. ChatGPT, Gemini e Claude são alvos comuns de bloqueio institucional (ver §1.5); um GPT customizado de terceiros herda o mesmo bloqueio, porque ainda roda dentro do domínio do ChatGPT.
  • Exige digitar a redação inteira — o aluno perde o treino no formato real da prova (escrita à mão, sob o tempo e o cansaço de redigir no papel) só para conseguir uma correção.
  • Sem garantia de rigor nem de privacidade adequada a menores. Um chatbot genérico não segue a Matriz de Referência do INEP por padrão (a menos que o próprio aluno monte um prompt elaborado), não é obrigado a citar trechos exatos, e os termos de uso padrão de uma conta de IA generalista não foram desenhados pensando em dados de adolescentes.

Ou seja: a IA generativa reduziu o custo técnico de gerar uma correção, mas não resolveu o acesso no ambiente onde o aluno de escola pública efetivamente está — dentro da rede da escola, com o celular na mão, sem querer digitar um texto de 30 linhas.


1.3 Análise de Causas-Raiz

#Causa-RaizDescrição
C1Correção qualificada é escassa e caraUm retorno por competência, com trechos citados, exige tempo e um corretor treinado — recurso caro e mal distribuído
C2Relação professor–aluno desfavorávelUm único professor de redação para dezenas/centenas de alunos inviabiliza feedback individual e frequente
C3Feedback genérico não ensina"Melhore a argumentação" sem apontar o trecho e o porquê não diz ao aluno o que fazer diferente
C4Latência do retornoQuando a correção chega (dias/semanas depois), o aluno já perdeu a conexão com o que escreveu
C5Barreira de ferramentaSoluções que exigem instalar app, criar conta e digitar o texto criam atrito — o aluno escreve no papel, à mão
C6IA genérica não é acessível no contexto escolarChatGPT/GPTs de terceiros costumam ser bloqueados na rede da escola, exigem digitar e não garantem rigor nem privacidade para menores

1.4 Consequências Mensuráveis

ConsequênciaImpacto
Aluno sem feedback detalhadoRepete os mesmos desvios; a nota estaciona apesar do volume de treino
Desigualdade de acessoQuem paga cursinho/correção evolui mais rápido do que quem depende só da escola
Tempo do professor consumido em correção repetitivaMenos tempo para ensino, planejamento e atendimento individual
Retorno lento e genéricoBaixa aderência do aluno; o esforço de escrever não se converte em aprendizado

1.5 A Solução: correção instantânea por foto, no canal que o aluno já usa

O produto é um bot multicanal (WhatsApp + Telegram) que corrige a redação a partir de uma foto do manuscrito. O fluxo foi desenhado para ter o menor atrito possível:

  • Sem instalar nada — o aluno usa o WhatsApp ou o Telegram que já tem no celular.
  • A partir do papel — ele fotografa a redação escrita à mão; um OCR (Mistral) transcreve o texto.
  • Confere e corrige a transcrição — o bot mostra o que leu; se a letra difícil confundiu o OCR, o aluno ajusta o texto antes de corrigir.
  • Recebe a correção completa — nota estimada de 0 a 1.000 por competência, com feedback pedagógico que cita trechos exatos do próprio texto e mostra como reescrever.

O motor de correção é um prompt de corretor especialista construído sobre a Matriz de Referência do INEP: faz a triagem de nota zero, avalia as cinco competências ancorando cada observação em citações e entrega o resultado formatado para leitura no WhatsApp.

Por que WhatsApp/Telegram, e não um app de IA direto — dois diferenciais que não são só conveniência:

  • Contorna o bloqueio de IA nas escolas. Muitas redes de ensino bloqueiam, na rede da escola, o acesso a ferramentas de IA generalistas (ChatGPT, Gemini, Claude etc.) — mas WhatsApp e Telegram, por serem apps de mensagens de uso cotidiano, normalmente não entram nesse bloqueio. Ao entregar a correção por um canal de mensageria em vez de um app de IA, o produto continua acessível dentro do ambiente escolar, onde o aluno mais precisaria dele.
  • Preserva o treino no formato real da prova. O ENEM é escrito à mão, no papel. Corretores online genéricos pedem o texto digitado, o que já é um exercício diferente do que o aluno vai enfrentar no dia da prova (a caligrafia, o tempo de escrita à mão, a ausência de corretor automático). Aqui o aluno fotografa o que escreveu no papel — o treino continua fiel ao formato oficial, e o OCR é que se adapta ao aluno, não o contrário.

Planejamento Macro (PMC)

A estruturação executiva do projeto foi concebida com a metodologia Project Model Canvas (PMC). Para a visão completa e interativa dos treze blocos fundamentais (Por Quê, O Quê, Quem, Como, Quando e Quanto), acesse a página Canvas de Projeto.

Em resumo: o pipeline transforma uma foto de uma redação manuscrita em um diagnóstico pedagógico acionável, em cerca de um minuto e no aplicativo de mensagens que o aluno já usa todos os dias.


1.6 Contextualização do Desafio

O público-alvo são estudantes de ensino médio em preparação para o ENEM — em boa parte, menores de idade. Isso coloca a conformidade com a LGPD e o cuidado ético no centro do projeto, não como acessório (ver página Ética e LGPD):

  • Consentimento explícito antes de qualquer processamento.
  • Descarte da foto imediatamente após o OCR; o texto vive apenas no estado temporário (TTL de 45 min) e nada persiste após a correção.
  • Transparência: a nota é sempre apresentada como estimativa — a nota oficial do ENEM depende de dois corretores humanos.

1.7 Objetivos SMART

Objetivo Principal

Desenvolver um bot multicanal que, a partir de uma foto de redação manuscrita, entregue uma correção estimada por competência (0–1.000) com feedback ancorado em trechos exatos, em cerca de um minuto e sem instalação de aplicativo, respeitando a LGPD para um público majoritariamente menor de idade.

Objetivos Específicos

#ObjetivoEspecíficoMensurávelAlcançávelRelevanteFase
O1Transcrever a redação manuscrita por fotoOCR (Mistral) da foto, com preview e edição manual opcionalTexto transcrito fielmente; parte ilegível é recusada com orientaçãoAPI de OCR madura; foto é o documento inteiroElimina a digitação — o aluno escreve à mão, como na provaNúcleo de IA
O2Estimar a nota por competência com feedbackPrompt de corretor sobre a Matriz do INEP (5 competências + triagem de zero)Saída com nota 0–1.000, notas por competência e trechos citadosLLM plugável com cache de contexto no bloco estáticoEntrega o feedback específico que faz a nota subirNúcleo de IA
O3Operar em multicanal sem atritoWebhooks de WhatsApp e Telegram sobre uma interface de canal agnósticaFluxo idêntico nos dois canais; sem app nem cadastroReuso da arquitetura do projeto paiEncontra o aluno onde ele já estáIntegração
O4Garantir conformidade e controle de custoConsentimento LGPD, descarte da foto, TTL de 45 min, rate-limit diárioNada persiste após a correção; limite de correções/aluno/diaEstado 100% em Redis com TTL; contador diárioProtege um público menor de idade e contém o custo de OCR+LLMGovernança


POR QUÊ? 1. Justificativa

A redação do ENEM vale 0 a 1.000 pontos e é decisiva no acesso ao ensino superior (SISU, ProUni, FIES). O que faz a nota subir não é escrever mais — é receber feedback detalhado por competência, ancorado no próprio texto. Esse feedback é escasso e caro pela via humana (professor de escola pública com dezenas/centenas de alunos, correção particular que custa dinheiro) — e a IA genérica (ChatGPT, GPTs de terceiros) não preenche essa lacuna no contexto escolar: costuma ser bloqueada na rede da escola, exige digitar a redação (perdendo o treino manuscrito) e não garante rigor da Matriz do INEP nem privacidade adequada a menores.

O que motivou este projeto: dar a qualquer aluno, a partir de uma foto da redação manuscrita e sem instalar nada, uma correção estimada por competência com trechos citados — em cerca de um minuto, no WhatsApp ou Telegram que ele já usa (e que a escola não bloqueia).

POR QUÊ? 2. Objetivo SMART

Desenvolver um bot multicanal que, a partir de uma foto de redação manuscrita, entregue uma correção estimada por competência (0–1.000) com feedback ancorado em trechos exatos, em cerca de um minuto e sem instalação de aplicativo, respeitando a LGPD para um público majoritariamente menor de idade.

#Objetivo EspecíficoMétricaFase
O1Transcrever a redação manuscrita (OCR)Transcrição fiel; ilegível recusadaNúcleo de IA
O2Estimar nota por competência com feedback0–1.000, notas por competência, trechosNúcleo de IA
O3Operar em WhatsApp e TelegramFluxo idêntico, sem app/cadastroIntegração
O4Conformidade e controle de custoConsentimento, TTL 45min, rate-limitGovernança

POR QUÊ? 3. Benefícios

#BenefícioValor Gerado
B1Feedback específico e imediatoNota por competência com trechos citados, em ~1 min
B2Acesso democratizadoCorreção qualificada sem custo de cursinho
B3Zero atritoFoto no WhatsApp/Telegram — sem app, sem cadastro
B4Escreve à mão, como na provaTreino realista no papel — diferente de corretores online que exigem digitar
B5Privacidade por designFoto descartada após o OCR; nada persiste
B6Alívio para o professorDescarrega a correção repetitiva
B7Funciona onde a escola bloqueia IAWhatsApp/Telegram costumam escapar do bloqueio a ChatGPT/Gemini/Claude

O QUÊ? 4. Produto

Um bot conversacional multicanal composto por:

  • Borda HTTP (FastAPI): webhooks de WhatsApp e Telegram, resposta em <200ms, processamento em background.
  • Máquina de estados em Redis: tema → motivadores → foto → confirmação → correção (TTL 45 min).
  • Motor de OCR: transcrição da foto via Mistral OCR.
  • Motor de correção: prompt de corretor (Matriz do INEP) sobre um LLM plugável.
  • Arquitetura mono-contêiner: app + Redis embutido num único deploy.

O QUÊ? 5. Requisitos

Funcionais

  • RF01 — Transcrever a foto da redação via OCR.
  • RF02 — Preview do OCR e edição manual do texto.
  • RF04 — Estimar nota 0–1.000 por competência com trechos citados.
  • RF05 — Operar em WhatsApp e Telegram na mesma interface.
  • RF06 — Consentimento LGPD antes de qualquer processamento.

Não Funcionais

  • RNF01 — Responder o webhook em < 200 ms (SLA da Meta).
  • RNF02 — Foto descartada após OCR; estado com TTL de 45 min.
  • RNF03 — Conter o custo de OCR + LLM (rate-limit + cache).

QUEM? 6. Stakeholders

StakeholderTipoInteresseInfluência
Aluno de ensino médioUsuário diretoFeedback rápido e específicoAlta
Professor de redaçãoUsuário indiretoDescarregar correção; acompanhar turmasAlta
Responsável legalRegulatórioPrivacidade dos dados do menorAlta
Escola / CursinhoInstitucionalApoio pedagógico de baixo custoMédia
Comunidade / PortfólioExternoReprodutibilidade e referência técnicaBaixa

QUEM? 7. Equipe

Ygor (Autor): Engenheiro de Sistemas de IA / Full-Stack responsável pela arquitetura, fluxo conversacional, integração de canais, OCR, provedores de LLM, prompt de correção e documentação. Projeto individual, derivado de um assistente de ensino WhatsApp/Telegram.

COMO? 8. Premissas

  • P1 — O aluno tem WhatsApp ou Telegram e consegue fotografar a redação.
  • P2 — As APIs de OCR (Mistral) e de LLM manterão disponibilidade e termos de uso.
  • P3 — O LLM aplica a Matriz do INEP com aderência a trechos citados.
  • P4 — A caligrafia é, em geral, legível (com edição manual como rede de segurança).
  • P5 — O tráfego (escala de escola) é atendido por uma única réplica.

COMO? 9. Entregas (EAP)

Corretor de Redação do ENEM
├── 1. Diagnóstico e Planejamento
├── 2. Borda e Canais (FastAPI + WhatsApp/Telegram)
├── 3. Fluxo Conversacional (Redis + consentimento)
├── 4. Núcleo de IA (OCR + correção + provedores)
└── 5. Qualidade e Documentação

COMO? 10. Restrições

  • R1 — Réplica única (Redis embutido não é compartilhável).
  • R2 — Cada correção consome OCR + LLM pagos → rate-limit obrigatório.
  • R3 — Público majoritariamente menor de idade → LGPD no centro.
  • R4 — A nota é estimativa por IA, não a nota oficial do INEP.
  • R6 — Saída formatada para WhatsApp (sem tabelas markdown, sem #).

QUANDO E QUANTO? 11. Riscos

RiscoMitigação
OCR erra a transcrição (letra difícil)Preview obrigatório + edição manual + piso de legibilidade
LLM superestima/subestima competênciaPrompt calibrado por níveis; ancoragem em trechos; anti-inflação
Custo aberto de OCR + LLMRate-limit; cota espiada antes do OCR; cache de contexto
Vazamento de dado de menorConsentimento; foto descartada; TTL 45 min; sem histórico
Indisponibilidade de provedorRetry com backoff; provedor plugável; devolução de crédito

QUANDO E QUANTO? 12. Cronograma

  • Diagnóstico: investigação do problema, canvas e arquitetura.
  • Borda e canais: FastAPI, webhooks, interface de canal agnóstica.
  • Fluxo: máquina de estados em Redis, consentimento, rate-limit.
  • Núcleo de IA: OCR, prompt de correção, provedores e cache.
  • Qualidade e docs: testes unitários, análise crítica e portal.

QUANDO E QUANTO? 13. Custos

Diferente de um projeto 100% local, o custo não é zero: cada correção consome APIs pagas (OCR + LLM). O custo é variável, por uso, e contido pelo rate-limit diário, pela cota espiada antes do OCR e pelo cache de contexto no bloco estático (~6.000 tokens). A infraestrutura é enxuta: um único Container App (app + Redis embutido, 1 réplica fixa). A modelagem detalhada está na aba Métricas e ROI.

2. Arquitetura e Fluxo Conversacional

A arquitetura é deliberadamente enxuta: um único contêiner que embute a aplicação (FastAPI) e o estado (Redis), sem banco relacional e sem serviços gerenciados à parte. A escolha prioriza um deploy simples, barato e reprodutível — adequado ao volume de tráfego de uma escola — sobre a elasticidade de uma arquitetura distribuída que o caso de uso não exige.

2.1 Topologia: mono-contêiner (app + Redis embutido)


┌─────────────────────── Container único ───────────────────────┐
│                                                                │
│   entrypoint.sh                                                │
│     ├─ redis-server  (127.0.0.1, AOF em /data, nunca exposto)  │
│     └─ aguarda o PING responder → exec uvicorn                 │
│                                                                │
│   FastAPI (src/main.py)                                        │
│     ├─ POST /v1/webhook/{whatsapp|telegram}                    │
│     ├─ GET  /v1/webhook/{...}  (verificação/challenge)         │
│     └─ GET  /health                                            │
│                                                                │
│   Redis (estado)                                               │
│     ├─ flow:{canal}:{sender}     máquina de estados (TTL 45m)  │
│     ├─ consent:{canal}:{sender}  consentimento LGPD (sem TTL)  │
│     ├─ essay_count:{...}:{dia}    rate-limit diário (TTL 24h)  │
│     └─ idempotência de webhook                                 │
└────────────────────────────────────────────────────────────────┘

O entrypoint.sh sobe o redis-server em modo daemonsó em 127.0.0.1, nunca exposto para fora do contêiner — espera o PING responder e só então executa o uvicorn. Isso evita a corrida de a aplicação tentar conectar antes de o Redis terminar o fork de inicialização.

Por que Redis embutido, e não um Redis gerenciado? Um único Container App carrega tudo. Não há Azure Cache for Redis nem serviço separado para provisionar, pagar e monitorar. O AOF em /data (volume) faz o consentimento LGPD sobreviver a restart do contêiner.

2.2 A borda HTTP: responder rápido, processar depois

O invariante central — herdado do projeto pai — é que o handler do webhook nunca faz trabalho pesado. A Meta exige resposta 200 em menos de 200 ms; um não-2xx dispara uma enxurrada de retries. Então o handler apenas:

  • Valida a assinatura (HMAC SHA-256 no WhatsApp, secret token no Telegram — em produção).
  • Faz o parse do payload para uma InboundMessage normalizada (agnóstica de canal).
  • Descarta duplicatas (idempotência: Meta/Telegram reenviam o mesmo update em timeout/restart).
  • Despacha o processamento para uma task asyncio e retorna 200 imediatamente.

Todo o trabalho caro — download da mídia, OCR e LLM — acontece nessa task de background, nunca no caminho da resposta HTTP. Exceções na task são isoladas e registradas; nunca escapam para o webhook.


Meta/Telegram ──POST──▶ verify_signature ──▶ parse_inbound ──▶ is_duplicate?
                                                                   │ não
                                                                   ▼
                                            200 OK ◀── create_task(_process) ──▶ (background)
                                                                                    handle_message

2.3 Detalhamento dos componentes

ComponenteArquivoResponsabilidade
Borda HTTPsrc/main.pyRecebe webhooks, valida, responde 200 em <200ms, despacha a task
Canaissrc/channels/IMessagingChannel: WhatsApp e Telegram; parse de entrada, envio de texto/botões, download de mídia, verificação de assinatura
Máquina de estadossrc/flow/Fluxo da conversa (states.py) e handlers (handlers.py), com estado em Redis
OCRsrc/correction/ocr.pyTranscrição da foto da redação via Mistral OCR (mistral-ocr-latest)
Correçãosrc/correction/corrector.pyMonta o bloco dinâmico do prompt e delega ao provedor de LLM
Provedoressrc/correction/providers/Implementações plugáveis: Gemini, Claude, OpenAI, DeepSeek, Maritaca
Promptsrc/prompts.pyPrompt do corretor ENEM (bloco estático + bloco dinâmico)
Rate-limitsrc/rate_limit.pyLimite diário de correções por aluno
Configsrc/config.pypydantic-settings — todas as variáveis de ambiente

Interface de canal agnóstica

Toda a lógica de fluxo opera sobre a abstração IMessagingChannel e a InboundMessage normalizada. WhatsApp e Telegram são apenas duas implementações da mesma interface (send_text, send_buttons, download_media, parse_inbound, verify_signature). O resultado: o mesmo fluxo conversacional roda idêntico nos dois canais, e adicionar um terceiro canal é implementar uma classe, não reescrever o fluxo.

2.4 O fluxo conversacional (máquina de estados)

O estado da conversa é a única memória do sistema — não há banco. Uma chave flow:{canal}:{sender} guarda o JSON do fluxo com TTL de 45 minutos: fluxo abandonado evapora sozinho, junto com o texto do OCR (exigência de LGPD). Ausência de chave = IDLE.


Aluno                         Bot
  │  (1º contato)              │
  │◀── consentimento LGPD ─────┤   nada é processado antes do aceite
  │  [Aceito]                  │
  │  /corrigir                 │
  │◀── "digite o tema" ────────┤   AWAITING_THEME   (tema: só texto)
  │  tema (texto)              │
  │◀── "textos motivadores?" ──┤   AWAITING_MOTIVATORS
  │  textos / [Pular]          │
  │◀── "manda a foto" ─────────┤   AWAITING_ESSAY_PHOTO
  │  📷 foto da redação        │──▶ download → Mistral OCR (a foto morre aqui)
  │◀── preview do OCR ─────────┤   CONFIRMING
  │  [Editar texto]            │──▶ EDITING_TEXT (aluno ajusta a letra difícil)
  │◀── eco p/ confirmar ───────┤
  │  [Corrigir agora]          │──▶ LLM + Matriz de correção do ENEM
  │◀── nota + feedback ────────┤   estado é limpo ANTES da entrega

Estados (FlowState): AWAITING_THEMEAWAITING_MOTIVATORSAWAITING_ESSAY_PHOTOCONFIRMING → (opcional) EDITING_TEXT → correção.

Regras de projeto embutidas no fluxo:

  • OCR só na redação. Tema e textos motivadores são texto digitado; uma foto nesses passos é recusada com orientação, para não queimar créditos de OCR à toa.
  • A redação pode vir em até 2 fotos (frente/verso) — cada parte é transcrita e concatenada. O texto definitivo do ENEM tem 30 linhas numa face só e cabe em uma foto; a segunda existe para o aluno que treina em folha comum (letra maior, verso da folha).
  • Rate-limit é consumido na correção, não no passeio pelo fluxo. Uma falha técnica na correção devolve o crédito do dia, e o estado é preservado para o aluno tentar de novo sem repetir tudo.
  • O estado morre antes da entrega. Se o envio falhar no meio dos chunks, o aluno recomeça limpo em vez de ficar preso num CONFIRMING fantasma.

2.5 O motor de correção: prompt em dois blocos e cache de contexto

O prompt (src/prompts.py) é dividido em duas partes para aproveitar cache de contexto e cortar custo:

  • Bloco estático (ENEM_CORRECTION_SYSTEM, ~6.000 tokens): a matriz de correção inteira — identidade do corretor, triagem de nota zero, as cinco competências com tabelas de pontuação, e o formato de saída para WhatsApp. É idêntico em toda chamada.
  • Bloco dinâmico (ENEM_CORRECTION_INPUT): apenas TEMA / TEXTOS MOTIVADORES / REDAÇÃO — a única parte que muda por correção.

Nota de implementação: a formatação é sempre via str.format(), nunca f-string — o corpo do prompt tem chaves literais (tabelas, exemplos) que um f-string tentaria avaliar como expressão.

Cada provedor combina os dois blocos do jeito mais barato disponível:

Provedor (CORRECTION_PROVIDER)Cache do bloco estático
geminiCache explícito (client.caches.create), com fallback para bloco inline se a criação falhar
claudeCache explícito (cache_control)
openai, deepseek (default)Cache automático de prefixo
maritacaSem cache documentado — o bloco estático é reenviado, sem desconto confirmado

A troca de provedor é uma variável de ambiente (CORRECTION_PROVIDER + CORRECTION_MODEL), o que permite comparar modelos rapidamente sem tocar no código. Tanto o OCR quanto a correção usam retry com backoff exponencial (tenacity) — comum a sobrecarga (503) e limite de cota (429) em qualquer provedor.

2.6 Deploy (Azure Container Apps)

O docker-compose é a forma canônica de rodar; para produção o alvo é o Azure Container Apps, no mesmo padrão do projeto pai:

  • 1 único Container App (imagem deste Dockerfile — Redis já embutido).
  • minReplicas=1 e maxReplicas=1 (fixo, não escala). O Redis embutido não é compartilhável entre réplicas: rodar com mais de uma fragmentaria o rate-limit, o consentimento e o estado do fluxo. Para o volume de uma escola, uma réplica é folgado (ver Análise e Limitações e Robustez e Escala).
  • Azure Files montado em /data (mesmo mount point da VOLUME do Dockerfile) para o AOF do Redis sobreviver a redeploy/restart.
  • Segredos (tokens, chaves) via Key Vault / secret refs — nunca env var em texto plano no manifesto.

3. Análise Crítica e Limitações Estruturais

Um sistema honesto declara onde ele não é perfeito. Abaixo, as fragilidades reais do projeto, cada uma com a mitigação já implementada e o que ainda fica em aberto para uma evolução Enterprise-Grade.


3.1 OCR de manuscrito: o limite da letra difícil

Risco. A entrada é uma foto de redação manuscrita. Caligrafia difícil, foto tremida, sombra ou papel amassado degradam a transcrição do OCR (Mistral). Uma palavra lida errado pode virar um "desvio" que o aluno não cometeu — e uma correção baseada em texto errado é uma correção injusta.

Mitigação implementada.

  • Preview obrigatório antes de corrigir. O bot mostra a transcrição do OCR e só avança com a confirmação do aluno — que já vê a leitura da foto antes de qualquer nota.
  • Destaque das palavras de baixa confiança. O OCR é chamado com confidence_scores_granularity="word", e o Mistral devolve a confiança (0–1) por palavra. As palavras abaixo de um limiar (OCR_CONFIDENCE_THRESHOLD, default 0,85) são mostradas no preview — as de menor confiança primeiro — direcionando a atenção do aluno aos pontos onde a leitura ficou incerta. É triagem de atenção, não veredito: confiança baixa não significa erro, apenas "vale checar". O comportamento é adaptativo: poucas palavras incertas → o bot as aponta uma a uma; muitas (letra difícil no geral) → em vez de despejar uma lista enorme, ele pede a releitura do texto inteiro e mostra apenas as mais incertas.

Calibração nas redações reais (6 fotos com nota conhecida): a confiança do OCR acompanha a legibilidade, que por sua vez acompanha a nota. As redações nota 1000 (letra caprichada) tiveram 0 a 1 palavra sinalizada a 0,85; a de 920, apenas 2; já as de 800 (letra mais difícil) tiveram ~50 palavras cada. O limiar de 0,85 separa os dois regimes com folga — nenhuma redação cai perto da fronteira —, o que valida a decisão de trocar de mensagem por quantidade de palavras sinalizadas.

  • Edição manual opcional. Se a letra confundiu o OCR, o aluno toca em Editar texto, recebe o texto lido numa mensagem própria (fácil de copiar), ajusta o que estiver errado, e o bot ecoa a versão final para nova confirmação. Ao editar, o destaque de baixa confiança é zerado (a leitura foi conferida à mão).
  • Piso de legibilidade por foto. Cada parte precisa render um mínimo de caracteres (MIN_ESSAY_CHARS); abaixo disso, a foto é tratada como ilegível e recusada com orientação — em vez de mandar lixo para o LLM.

Em aberto. O OCR é uma transcrição fiel da foto — ele não "conserta" nem interpreta a redação. Se o aluno não revisar o preview, um erro de leitura entra na correção como se fosse do texto original. A responsabilidade pela fidelidade do texto é compartilhada com o aluno, e o produto deixa isso explícito na etapa de confirmação.

Alternativa avaliada e descartada: "segundo olhar" de uma LLM multimodal

Antes de fixar a edição manual como a única rede de segurança, testamos se uma LLM multimodal (recebendo a foto + a transcrição do OCR + as palavras de baixa confiança) conseguiria decifrar os trechos incertos automaticamente — poupando o aluno de reler e corrigir o texto à mão. A hipótese era razoável: um modelo de visão "olhando" a imagem deveria acertar onde o OCR só viu um traço ambíguo.

Método. Um script de laboratório (scripts/ocr_second_look.py, fora do fluxo de produção) roda o experimento em duas versões por foto — v1 (OCR cru) e v2 (revisão da LLM) — para comparação lado a lado com a imagem original. Testamos três eixos, isoladamente e combinados, nas redações reais de redacoes/:

Eixo testadoVariações
Modelogemini-3.1-flash-lite, gemini-3.5-flash
Modo de promptocr-anchored (revisa o texto do OCR) · image-first (transcreve direto da imagem, usando o OCR só como apoio)
Resolução de imagempadrão · media_resolution=ultra (mais tiles internos do Gemini) · ultra + upscale físico 3× (LANCZOS)

Em todas as combinações, o prompt instruía explicitamente transcrição literal — preservar erros de ortografia/concordância do aluno, nunca "consertar" para a norma culta (a redação é avaliada exatamente por esses desvios, na Competência 1).

Metodologia de verificação. Comparar apenas as saídas de diferentes rodadas de LLM entre si não prova qual delas está certa — nenhuma rodada é "gabarito" só por parecer mais coerente. Por isso, as conclusões abaixo separam explicitamente o que foi conferido contra a foto original (evidência forte) do que era só divergência entre modelos (evidência fraca, tratada como pergunta em aberto, não como falha).

Confirmado contra a foto: acertos genuínos do segundo olhar. Em fotos de letra difícil, o modelo decifrou corretamente vários trechos que o OCR embaralhou — incluindo dois casos em que o OCR não só errou a palavra como mudou o sentido da frase, e a revisão corrigiu:

O aluno escreveu (confirmado na foto)O que o OCR leu (errado)
"...além de transmitir sabedoria, evitando a desinformação...""...além de transmitir sobre a vida e a vida, evitando..."
"...caso que infelizmente ainda são vistos na atualidade.""...caso que infelizmente ainda não visto na atualidade."
reibérvinhoribeirinhos / Defreende-seDepreende-se / se deponiramse depararamgarbles óbvios do OCR

No modo image-first, o segundo olhar também recuperou a divisão em parágrafos que o OCR havia achatado num bloco só.

Confirmado contra a foto: o risco que se manteve.

CasoO que a foto mostraO que aconteceu
Substituição silenciosa de um erro realO aluno escreveu "hordiena" (erro real)Em algumas rodadas (mesmo modelo, mesmo prompt, variando só a resolução da imagem) o modelo "corrigiu" para "hodierna" — mesmo com instrução explícita para não fazer isso
Erro do OCR que soa gramatical passa despercebidoO trecho correto é "são fundamentais"O OCR leu "não fundamentam" (também gramatical) — nenhuma combinação de modelo/modo/resolução testada corrigiu isso

Em aberto (não verificado contra a foto). Outras divergências entre rodadas (Autrorrim/Outrossim, Sob em vez/Sob esse viés, geração/gerenciação de leis, estereótipos,/esteriótipos) apareceram ao comparar modelos e modos entre si, mas não foram conferidas contra o manuscrito — podem ser tanto erros do modelo quanto acertos que o OCR também errava. Ficam registradas como incerteza, não como falha.

O achado mais importante: aumentar a resolução da imagem (ultra + upscale 3×, ~9× mais pixels de entrada) não eliminou o risco confirmado — a mesma inconsistência no caso "hordiena" (preservado em algumas rodadas, normalizado em outras) apareceu em resoluções diferentes do mesmo modelo. Isso indica que a causa não é falta de detalhe visual: é variância do modelo e, possivelmente, um viés para uma leitura que "soa bem" em português — mesmo vendo a imagem com nitidez.

Decisão. O saldo do experimento é misto: o segundo olhar acertou dois trechos em que o OCR mudava o sentido da frase (evidência forte de valor real), mas também normalizou um erro real do aluno em algumas rodadas, de forma inconsistente entre execuções idênticas. Esse segundo ponto é o que desqualifica a autoaceitação: como o mesmo input pode gerar ora uma transcrição fiel, ora uma "corrigida", não há como confiar no resultado sem checar contra a foto — e checar contra a foto é exatamente o trabalho que já cabe ao aluno na edição manual. Por isso, mantivemos a integração ao fluxo de produção descartada por ora, mas o resultado positivo nos dois casos confirmados deixa em aberto uma evolução futura: usar o segundo olhar como sugestão opcional dentro da própria edição manual (ex.: "o modelo lê como X aqui — confere?"), sempre com a palavra final do aluno, nunca substituindo o texto sozinho. Os scripts (scripts/ocr_calibration.py e scripts/ocr_second_look.py) permanecem no repositório como ferramentas de laboratório documentadas — não fazem parte do fluxo do bot hoje.


3.2 A nota é uma estimativa, não a nota oficial

Risco. A avaliação é feita por um LLM, não por dois corretores humanos credenciados pelo INEP (com critério de desempate), como no ENEM real. Modelos de linguagem têm variância (a mesma redação pode oscilar alguns pontos entre execuções) e podem, em casos limítrofes, superestimar (por complacência) ou subestimar uma competência.

Mitigação implementada.

  • Prompt calibrado pela matriz oficial. Cada competência tem a tabela de níveis (0/40/80/120/160/200) transcrita da Matriz de Referência; o modelo é instruído a ancorar toda nota em um nível específico e toda observação em um trecho citado — nada de "impressão solta".
  • Regras de conduta anti-inflação. O prompt proíbe explicitamente inflar nota "para não desanimar", inventar desvio inexistente, e elogiar de forma genérica sem citar o trecho.
  • Triagem de nota zero antes de tudo. Fuga total, tipo textual errado, cópia, desrespeito aos direitos humanos etc. são verificados antes da pontuação por competência, evitando notas altas em textos que o ENEM zeraria.
  • Transparência de estimativa. O feedback deixa claro que a nota é uma estimativa por IA e que a nota oficial depende de humanos.

Medido (piloto) — deixou de ser um vazio total de dados. Como o INEP nunca publica o texto das redações (só a nota), medir "vs. nota oficial" é inviável para qualquer projeto. O proxy mais próximo é o corpus público Essay-BR (redações reais corrigidas segundo os critérios do ENEM). Duas medições independentes convergiram para o mesmo intervalo: um teste interno menor (5 redações de nota conhecida) mediu MAE de 120,0 pontos para este prompt; um piloto maior (30 redações do Essay-BR) mediu MAE de 129,3 pontos e viés médio de -1,3 (praticamente neutro — a favor das regras anti-inflação acima). Detalhes completos, metodologia e limitações em Métricas e ROI, §8.2.

Em aberto. A amostra do piloto (n=30) ainda é pequena para uma leitura estatística apertada, e não há como descartar que o LLM tenha visto o corpus Essay-BR (público desde 2021) durante o treino. Ampliar a amostra e repetir com outro provedor é o próximo passo natural (ver Métricas e ROI, §8.5).


3.3 Réplica única: o Redis embutido não escala horizontalmente

Risco. Como o Redis vive dentro do contêiner, ele não é compartilhado entre réplicas. Subir replicas > 1 fragmentaria o estado: cada réplica veria um Redis diferente, quebrando o rate-limit (o aluno burlaria o limite caindo em réplicas distintas), o consentimento e a continuidade do fluxo.

Mitigação implementada.

  • Réplica fixa em 1. O deploy usa minReplicas=1 e maxReplicas=1 — a arquitetura assume e documenta a restrição, em vez de deixá-la como uma armadilha silenciosa.
  • Dimensionamento adequado ao caso. Para o volume de uma escola, uma única réplica com Redis local é folgada — o gargalo real é a latência de OCR+LLM (I/O de rede), não CPU do contêiner.

Em aberto. Se o produto precisar escalar para muitas escolas simultâneas, o caminho é externalizar o Redis (um Redis gerenciado compartilhado) e então liberar réplicas — uma mudança contida, já que todo o estado passa por src/redis_client.py.


3.4 Custo por correção: OCR + LLM a cada redação

Risco. Diferente de um serviço puramente local, cada correção consome APIs pagas: uma chamada de OCR (Mistral) por foto e uma chamada de LLM (~6.000 tokens de bloco estático + o texto da redação). Acesso aberto (qualquer número da escola) sem contenção viraria um custo aberto.

Mitigação implementada.

  • Rate-limit diário por aluno (ESSAY_DAILY_LIMIT, default 2) — a única contenção de custo, já que o acesso é aberto. Contado por (canal, remetente, dia), com a chave expirando sozinha em 24h.
  • Cota espiada antes do OCR. O sistema verifica a cota antes de chamar o Mistral (has_quota), para não gastar OCR quando a correção nem vai acontecer.
  • Cache de contexto no bloco estático. ~6.000 tokens idênticos a cada chamada são cacheados (explícito no Gemini/Claude, automático no OpenAI/DeepSeek), cortando o custo por correção.
  • Tetos de entrada. Tema, motivadores e redação têm limites de caracteres — contêm abuso/acidente (ex.: foto de um livro inteiro) sem afetar uma redação real (~2.500 caracteres).

Custo medido em uso real (ver Métricas e ROI, §8.4.1): OCR (Mistral) tem free tier generoso — só ~1% da cota mensal consumida em testes extensivos — e, acima dela, US$ 0,004/foto; o LLM (DeepSeek V4 Flash) mediu US$ 0,00375/correção. Somados, menos de 1 centavo de dólar por correção de uma foto, fora da cota gratuita de OCR. Em escala (§8.4.1, "Custo em escala") — um teto conservador que ignora o free tier do OCR — isso é menos de US$ 2/ano para um aluno individual e ≈ US$ 21/ano para uma escola de 400 alunos com adoção parcial; considerando a cota gratuita real, a escola cai para ≈ US$ 10/ano. Baixo o bastante para ser bancado do próprio bolso, sem modelo de negócio formal.

Em aberto. O custo de hospedagem em nuvem (Azure Container Apps) ainda não foi medido — falta para fechar o custo operacional total. A comparação de custo × qualidade entre os demais provedores de LLM (Gemini, Claude, OpenAI, Maritaca) também segue em aberto (a troca é uma variável de ambiente — ver Arquitetura, §2.5).


3.5 Sem histórico: o preço (deliberado) da privacidade

Risco. Por decisão de privacidade (público menor de idade), nada persiste após a correção — não há histórico de redações do aluno. O efeito colateral é que o produto não mostra a evolução do aluno ao longo do tempo, um recurso pedagógico valioso.

Mitigação / racional.

  • É uma escolha de design, não um esquecimento. O texto do OCR vive só no estado Redis com TTL de 45 minutos; a foto é descartada logo após o OCR. Isso minimiza a superfície de dados pessoais de menores (ver Ética e LGPD).
  • O consentimento é o único dado sem TTL — e é um aceite, não conteúdo de redação.

Em aberto. Um histórico opcional (com consentimento específico e responsável legal, e possivelmente anonimizado/agregado) permitiria acompanhar a evolução por competência — mas só com uma base legal e um modelo de retenção bem definidos. Hoje, a ausência de histórico é o trade-off consciente que mantém o projeto no lado seguro da LGPD.


3.6 Custo de mensageria: o WhatsApp deixa de ser gratuito

Risco. A partir de 01/10/2026, o WhatsApp Business Platform (Meta) passa a cobrar por mensagem de serviço — a categoria de conversa usada por este bot (respostas a um contato que iniciou a interação, dentro da janela de atendimento). Até então, esse tipo de mensagem era gratuito; a mudança adiciona um custo por correção que não existia, além do já existente custo de OCR + LLM (§3.4). Para um produto de acesso aberto voltado a escolas — onde orçamento é justamente a maior restrição —, um custo por mensagem no canal mais usado é um risco real ao modelo de custo zero/baixo custo do projeto.

Mitigação implementada.

  • Telegram como canal recomendado e default. O Telegram Bot API é gratuito e sem sinal de mudança nesse sentido; CHANNEL_BACKEND agora tem telegram como valor padrão (era whatsapp), e o README recomenda explicitamente o Telegram como canal principal.
  • Arquitetura já é multicanal por design. Como os dois canais implementam a mesma interface (IMessagingChannel — ver Arquitetura, §2.3), a escolha de canal é uma decisão de configuração (variáveis de ambiente + qual webhook é registrado no provedor), não uma reescrita de código. Migrar uma implantação de WhatsApp para Telegram (ou operar as duas em paralelo, uma por escola) não exige tocar no fluxo conversacional.
  • WhatsApp continua suportado. Para instituições que já operam via WhatsApp (base de contatos existente, preferência da comunidade escolar), o canal continua funcional — a recomendação é sobre custo, não uma remoção de suporte.

Em aberto. O valor exato cobrado por mensagem de serviço (e se há alguma cota gratuita mensal, como o WhatsApp já pratica para outras categorias de conversa) ainda precisa ser confirmado na documentação oficial da Meta mais perto da data de vigência, para orçar o custo por correção em quem optar por manter o WhatsApp como canal.


Testes de Stress — Pipeline de Correção

🔬 Status: pilotos locais rodados, teste em nuvem pendente. Latência por etapa (n=5, Telegram + túnel ngrok, DeepSeek V4 Flash, CORRECTION_EFFORT=max), ganho do cache (agregado real do DeepSeek) e rajada na borda HTTP (até 100 webhooks concorrentes) já foram medidos localmente — ver seções abaixo. O que resta é rodar o mesmo em ambiente de nuvem (Azure) para confirmar os números em produção. Comparar outros provedores/efforts saiu de escopo por decisão de custo × qualidade (DeepSeek/max adotado — ver Métricas e ROI, §8.2 e §8.4.1), não por falta de medição.


Objetivo

Diferente de um pipeline local, o gargalo aqui é I/O de rede para APIs externas (OCR e LLM), não CPU/GPU. Os testes de stress precisam responder a três perguntas:

  • Qual é a latência real de cada etapa (download da mídia, OCR, LLM) e onde está o gargalo?
  • Como cada provedor de LLM se comporta sob a mesma redação (latência × custo × qualidade)?
  • A borda HTTP aguenta rajada de webhooks mantendo a resposta em <200ms enquanto o trabalho pesado roda em background?

Componentes a Medir

Piloto local instrumentado em src/flow/handlers.py (time.perf_counter() nos pontos de download/OCR/LLM, logado via structlog como essay_photo_timing e essay_flow_completed) — canal Telegram real, túnel ngrok, container Docker local (não nuvem).

ComponenteO que medirPayload de testeRodadasMín (s)Méd (s)Máx (s)Status
Download da mídiaBaixar a foto do provedor de canalfoto real, 90–212 KB51,492,213,03
OCR (Mistral)Transcrever 1 foto de redaçãoredação real, foto única51,381,701,97
OCR (Mistral)Redação em 2 fotos (frente/verso)2 fotos➖ fora do caso ideal
LLM — DeepSeekCorreção completa, CORRECTION_EFFORT=max~6.000 + ~800 tokens583,0092,0098,98
LLM — DeepSeekCorreção com CORRECTION_EFFORT=highidem⏳ latência não cronometrada (só qualidade, §8.2)
LLM — outros provedoresGemini / Claude / OpenAI / Maritaca~6.000 + ~800 tokens➖ fora de escopo (custo, §8.4.1)
Borda HTTPTempo de resposta do webhook (<200ms, SLA da Meta)update mínimo
Redisget/set do estado, isoladoFlowData JSON➖ coberto pelo overhead combinado
Overhead por tarefa (Redis+borda+envio, combinado)task_ms − download_ms − ocr_ms (etapa foto) / task_ms − llm_ms (etapa correção) — não isola Redis de borda de envio de mensagem, só confirma que a soma é pequena5+5~0 (foto) / 2,34 (correção)~0 (foto) / 3,09 (correção)~0 (foto) / 4,40 (correção)

Nota de escopo: "Overhead por tarefa" é uma medição combinada (Redis + despacho da task + chamadas de envio de mensagem ao canal) — confirma que a soma desses três é pequena frente ao LLM, mas não isola quanto é Redis puro vs. borda pura vs. envio. Isolar cada um exigiria instrumentação mais fina, não feita ainda.


Ganho do Cache de Contexto (medido — DeepSeek)

O bloco estático do prompt (~6.000 tokens) é reenviado a cada correção, mas é cacheável. O painel de uso do DeepSeek separa quantos tokens de entrada bateram no cache — e o agregado real do período de testes confirma que a alavanca funciona:

Tipo de tokenQuantidadeFatia
Input — cache hit200.96075% do input
Input — cache miss67.16825% do input
Output (gerado)401.863

Três de cada quatro tokens de entrada vieram do cache. É a confirmação direta de que o bloco estático de ~6.000 tokens não é recobrado a cada correção — exatamente o que a alavanca de custo nº 1 (Métricas e ROI, §8.4.1) previa. Como o DeepSeek cobra o token de cache hit por uma fração do preço do cache miss, isso derruba a maior parte do custo de entrada.

Nuance honesta: o gargalo de custo é a saída, não a entrada. Os 401.863 tokens de output superam os 268.128 de input somados — e, como o token de saída é o mais caro da tabela do DeepSeek, é ele que domina o custo por correção. O cache faz seu trabalho (75% de acerto no input), mas a alavanca de custo que ainda sobra para explorar é o tamanho da resposta gerada (competências + feedback + reescrita), não o input já cacheado.

Ainda não isolado: a comparação por-chamada (1ª fria vs. subsequentes quentes) não foi feita — o número acima é o agregado real do DeepSeek em uso, suficiente para confirmar que o cache opera, não para um teste controlado frio-vs-quente. Os demais provedores (Gemini/Claude/OpenAI) não foram medidos porque saíram de escopo (DeepSeek adotado — §8.4.1).


Teste de Rajada (Burst) na Borda HTTP — medido (piloto local)

Objetivo: confirmar que N webhooks simultâneos não quebram a resposta <200ms, já que o trabalho pesado é despachado para tasks de background.

Método. _local/scripts/burst_test.py dispara N POSTs concorrentes direto no container local (127.0.0.1:8000, sem o túnel ngrok no meio) com updates de texto (zero OCR/LLM — o trabalho de background é o fluxo de consentimento) e chat_id inválido (nenhuma mensagem chega a alguém real). Container com um worker Uvicorn (default — um event loop só).

N simultâneoHTTP 200<200msEdge p50Edge p95Edge máx
1010/1010/1054ms78ms78ms
2020/2020/2058ms119ms125ms
5050/5030/50127ms393ms410ms
100100/10024/100408ms830ms989ms

O invariante crítico se manteve em 100% dos casos: toda requisição retornou 200 — nunca um 4xx/5xx que dispararia retry-storm da Meta/Telegram (invariante 2). O SLA soft de <200ms, por outro lado, segura até ~20 concorrentes e começa a ceder a partir de 50.

Diagnóstico: a culpa é da contenção no event loop, não do edge. Repetindo a rajada com todos os updates deduplicando (mesmo message_id → nenhuma task de background é enfileirada, flag --same-id), o edge sozinho fica plano mesmo em N=100:

N simultâneo (sem background)<200msEdge p95Edge máx
5050/50134ms135ms
100100/100130ms137ms

Ou seja: parsear + validar assinatura + checar idempotência no Redis + enfileirar + responder 200 escala tranquilo (100 concorrentes a p95 130ms). Quem empurra a latência pra cima são as tasks de background rodando no mesmo event loop (worker único): sob dezenas delas simultâneas, as respostas do edge esperam atrás das continuações das tasks.

Ressalvas honestas:

  • O background do teste não é o de produção. Aqui as tasks fazem consentimento + um envio que falha rápido; em produção fazem OCR/LLM, que ficam segundos aguardando rede (cedem o loop mais), mas também vivem muito mais tempo. O teste caracteriza o padrão de contenção do loop, não prevê o número exato de produção.
  • 50–100 webhooks simultâneos é muito além do realista para um bot único de baixo volume: as mensagens chegam pingadas conforme os alunos escrevem, não em lote. Na faixa realista (≤20 concorrentes) o SLA segura com folga.
  • Alavanca, se um dia importar: uvicorn --workers N no mesmo container — todos os workers falam com o mesmo Redis embutido (localhost), então não fere o invariante de réplica única (worker ≠ réplica). Alternativamente, mover o trabalho pesado para um processo/fila separada.

Reflexão Crítica

Perguntas que a análise deve responder, no mesmo espírito do restante da documentação — a maioria já respondida com dado real; sobra o que só a nuvem fecha:

  • Qual é o gargalo real? ✅ Respondida. Não é "OCR + LLM" como hipótese original — é o LLM sozinho: ~93% do tempo total (92s médios de correção contra 3,9s médios de foto). OCR (1,7s) e download (2,2s) são rápidos; é a geração do LLM que domina, de forma bem mais extrema do que a hipótese inicial previa. Borda e Redis são de fato desprezíveis (ver "Overhead por tarefa" na tabela acima).
  • Existe correlação entre tamanho do payload e latência? Não isolado ainda — o piloto variou o tamanho da foto (90–212 KB) sem controle experimental rígido; a correlação com download_ms não foi testada estatisticamente (n=5 é pouco para isso). Para o bloco dinâmico do prompt (tema+motivadores+redação) × llm_ms, não há dados pareados ainda.
  • Qual provedor/configuração oferece o melhor equilíbrio latência × custo × qualidade? ✅ Decidida (por custo × qualidade, não por varredura exaustiva). A configuração adotada é DeepSeek V4 Flash + CORRECTION_EFFORT=max. Comparar outros provedores foi descartado por custo — o Gemini 3 Flash sai a ~R$ 0,31/redação contra o custo marginal do DeepSeek (§8.4.1), sem indício de ganho de qualidade que justifique. Reduzir o effort para highpiorou o alinhamento com o proxy de acurácia (§8.2) — e não há nível abaixo de high no DeepSeek (low/medium mapeiam para high), então os dois efforts efetivos já foram os dois comparados. Não é uma varredura de todos os provedores, mas o espaço de effort do DeepSeek está completo — é uma decisão de engenharia informada pelos pilotos de custo e qualidade.
  • A borda aguenta rajada sem violar o SLA de 200ms? ✅ Testada (piloto local). O edge sozinho aguenta 100 webhooks concorrentes a p95 130ms; o que degrada sob rajada agressiva (≥50) é a contenção das tasks de background no worker único, não o edge. E o invariante que mais importa se manteve: toda requisição retornou 200 em todas as rajadas (sem retry-storm). Na faixa realista (≤20 concorrentes) o SLA segura. Ver seção "Teste de Rajada" acima.
  • O sistema está pronto para o volume de uma escola? Parcialmente. Cada correção consome ~99s de processamento assíncrono (majoritariamente LLM, I/O externo — não CPU/rede do contêiner). A rajada mostrou que o edge não é o gargalo (escala a 100 concorrentes); o limite prático é quantas tasks pesadas (OCR/LLM) o worker único sustenta em paralelo sem degradar o tempo de resposta — mensurável de verdade só sob carga de produção em nuvem, com a alavanca --workers/fila à mão se precisar.

Nota metodológica: por consumir OCR e LLM pagos, os testes de stress devem rodar com um orçamento de chamadas controlado (ou em modo simulado/mock para as partes que não exigem a API real), respeitando o mesmo rate-limit que protege a produção.

6. Robustez, Escalabilidade e Boas Práticas

Para um bot que atende alunos em tempo real e depende de APIs externas (canal, OCR, LLM), robustez não é enfeite: uma falha mal tratada vira um webhook em loop de retries, um crédito de correção perdido, ou uma resposta que nunca chega. Abaixo, as decisões que levam o projeto do patamar de script ao de serviço resiliente.


6.1 Tratamento de Erros e Resiliência

A resiliência foi projetada em camadas, do webhook até a chamada de LLM.

Como foi implementado

  • A borda nunca devolve não-2xx nas rotas de webhook. Um status ≠ 2xx faz a Meta/Telegram reenviarem o update em enxurrada. Por isso, assinatura inválida, payload desconhecido e até exceções não tratadas degradam para 200 (e são logadas). Um exception handler global garante isso mesmo para erros inesperados.
  • A task de background isola exceções. Todo o processamento pesado roda numa task asyncio cujo corpo captura qualquer exceção — nenhuma escapa para derrubar o processo. Referências fortes às tasks impedem que o GC mate uma task em andamento.
  • Retry com backoff exponencial (tenacity) tanto no OCR quanto na correção — cobre sobrecarga (503) e limite de cota (429), comuns a qualquer provedor. OCR: até 3 tentativas; correção: até 4.
  • Devolução de crédito na falha técnica. O rate-limit é consumido na correção; se a chamada de LLM falhar depois de esgotar os retries, o crédito do dia é devolvido (decr) e o estado é preservado — o aluno tenta de novo sem repetir todo o fluxo.
  • Falha de OCR não queima cota. A cota é espiada (has_quota, sem consumir) antes de chamar o Mistral; e o rate-limit só é consumido na correção. Passear pelo fluxo é grátis.

Justificativa Arquitetural

O aluno está numa conversa síncrona: ele mandou a foto e espera a nota. Uma falha silenciosa que o deixe travado num estado CONFIRMING fantasma é pior do que um erro claro. Por isso o estado morre antes da entrega da correção: se o envio quebrar no meio dos chunks, o aluno recomeça limpo em vez de ficar preso.


6.2 Observabilidade: logs estruturados

O monitoramento não depende de "olhar o terminal": a telemetria é estruturada para ingestão externa.

Como foi implementado

  • structlog com eventos nomeados. Cada passo relevante emite um evento com campos (channel, sender, provider, model, chars): essay_corrected, essay_ocr_failed, essay_correction_failed, webhook_duplicate_dropped, webhook_signature_failed, flow_unhandled_exception, essay_flow_completed.
  • Mensagens de erro truncadas. Strings de erro são cortadas (ex.: [:300]) para não vazar conteúdo sensível nem inflar o log.
  • Nível configurável via LOG_LEVEL (env), sem alteração de código.

Justificativa Arquitetural

Logs em formato estruturado permitem que qualquer coletor (ELK, Datadog, Grafana Loki) faça a ingestão e construa alertas — taxa de falha de OCR, latência por provedor, volume de correções por dia — sem instrumentar o código de novo.


6.3 Configuração via ambiente (Twelve-Factor)

Código e configuração são estritamente separados.

Como foi implementado

  • Ponto único de verdade. Todas as chaves (canal, OCR, LLM), o provedor de correção, o modelo, os limites do fluxo e o TTL vivem em variáveis de ambiente, centralizadas em src/config.py (pydantic-settings) e documentadas em .env.example.
  • Provedor de LLM plugável sem código. CORRECTION_PROVIDER (gemini | claude | openai | deepseek | maritaca) + CORRECTION_MODEL trocam o motor de correção por env var — o factory.py resolve a implementação sob demanda.
  • Segredos fora do versionamento. .env é ignorado pelo Git; em produção, os segredos vêm de Key Vault / secret refs, nunca em texto plano no manifesto.
  • Verificação de assinatura ligada por ambiente. ENVIRONMENTlocal liga HMAC SHA-256 (WhatsApp) e secret token (Telegram); em local, o simulador roda sem esse atrito.

Justificativa Arquitetural

A mesma imagem roda para uma escola, para um cursinho ou para o simulador de dev — muda só o .env. Comparar dois LLMs na mesma redação é trocar uma variável e reiniciar, não editar código.


6.4 Estado, persistência e escalabilidade

Como foi implementado

  • Estado 100% em Redis, com TTL. A conversa vive em flow:{canal}:{sender} com TTL de 45 min; o rate-limit em essay_count:{...}:{dia} com TTL de 24h. Chaves com TTL fazem o sistema se auto-limpar — fluxo abandonado (e o texto do OCR junto) evapora sozinho.
  • Consentimento durável. consent:{canal}:{sender} é o único dado sem TTL (é um aceite, não um passo do fluxo) e sobrevive a restart via AOF do Redis em /data (volume).
  • Idempotência de webhook. Meta/Telegram reenviam o mesmo update em timeout/restart; uma chave de deduplicação por message_id descarta a repetição antes de processar.

O limite de escala — e o caminho para superá-lo

O Redis é embutido no contêiner, então não é compartilhável entre réplicas. Consequência: o deploy roda com 1 réplica fixa (minReplicas = maxReplicas = 1). Subir mais réplicas fragmentaria rate-limit, consentimento e fluxo (cada réplica veria um Redis diferente).

AspectoHoje (1 réplica)Caminho para escalar
EstadoRedis embutido no contêinerExternalizar para um Redis gerenciado compartilhado
RéplicasFixo em 1Liberar maxReplicas > 1 após externalizar o Redis
GargaloI/O de OCR + LLM (rede), não CPUConcorrência de tasks já é assíncrona
Superfície de mudançaContida em src/redis_client.py (todo acesso passa por lá)

Justificativa: para o volume de uma escola, uma réplica com Redis local é folgada — o custo dominante é a latência de rede das APIs, não CPU do contêiner. A restrição é documentada e assumida, não uma armadilha silenciosa; e o caminho para escalar (externalizar o Redis) é uma mudança contida, porque todo o acesso ao estado já está centralizado.


6.5 Qualidade: testes puros

A suíte de testes unitários (tests/unit/) roda sem I/O: Redis fake, LLM e OCR mockados. Cobre o fluxo conversacional (transições de estado, consentimento, rate-limit, edição de texto), os canais (parse de entrada, verificação de assinatura, chunking) e o prompt (montagem do bloco dinâmico, tetos de entrada). Testes puros rodam rápido e não gastam OCR/LLM pagos — make test a qualquer momento.

8. Métricas de Performance, Confiabilidade e ROI

Esta seção modela latência, acurácia, confiabilidade e viabilidade econômica do corretor. Latência (§8.1), acurácia (§8.2) e custo (§8.4.1) já têm uma primeira medição real — todas como piloto local, não como número fechado de regime de produção. Confiabilidade em regime real (taxa de sucesso, distribuição de falhas) ainda depende dos testes de stress em nuvem (ver Testes de Stress) e segue marcada como ⏳ a medir; a lógica de ROI segue como estimativa paramétrica com premissas explícitas.


8.1 Latência: tempo até a nota chegar

Ao contrário de um pipeline em lote, aqui a latência é sentida pelo aluno: ele mandou a foto e espera a correção. O tempo total decompõe-se em duas etapas assíncronas — cada uma disparada por um webhook (foto recebida; botão "Corrigir agora" clicado) — e só o processamento dentro de cada etapa é medido: o tempo que o aluno leva digitando o tema ou decidindo se confirma fica de fora por construção (src/flow/handlers.py, instrumentado com time.perf_counter() nos pontos de download/OCR/LLM).

Medido — piloto local (n=5, Telegram, DeepSeek V4 Flash, CORRECTION_EFFORT=max):

EtapaNaturezaMínMédMáx
Download da mídia (foto)Rede (provedor de canal)1,49s2,21s3,03s
OCR (Mistral)Rede (API de visão)1,38s1,70s1,97s
Overhead etapa foto (Redis + borda)Local~0s~0s~0s
LLM (correção)Rede (API de LLM)83,00s92,00s98,98s
Overhead etapa correção (Redis + borda + envio)Local2,34s3,09s4,40s
Total combinado (foto + correção)89,02s99,00s105,16s

A hipótese se confirma, com folga — e de forma mais extrema do que o previsto. O LLM sozinho responde por ~93% do tempo total. O overhead de infra é desprezível na etapa da foto (~0s) e pequeno mesmo na etapa da correção (2-4s, ~3% do total) — o "borda + Redis desprezível" está bem confirmado. Toda alavanca de latência real está, portanto, na camada de modelos — e dentro dela, CORRECTION_EFFORT (ver §8.2) já se mostrou uma variável de peso: os ~92s médios acima são específicos do effort max. Não medimos a latência com high/medium/low ainda — a comparação high feita até agora foi só de qualidade da correção (§8.2), não de tempo.

Nota de UX: o fluxo mascara a latência com sinais de progresso — mensagem de "processando", indicador de "digitando" e o preview do OCR (que já dá ao aluno algo para revisar enquanto ainda não há nota). A latência percebida é menor que a medida — mas quase 100 segundos de espera na correção é tempo real, não só percepção, e vale considerar ao escolher o effort de produção.

Em aberto. Amostra pequena (n=5) e ambiente local — não nuvem. O que já foi medido localmente (decomposição de latência, rajada na borda até 100 concorrentes, ganho do cache) está em Testes de Stress; o que falta é confirmar tudo em ambiente de nuvem (Azure). A rajada, aliás, mostrou que o edge não é o gargalo — ele escala a 100 requisições concorrentes; o limite é a contenção das tasks de background no worker único (ver Testes de Stress, seção Rajada).


8.2 Acurácia da nota estimada — medido contra o Essay-BR

Diferente do restante desta seção, a acurácia da nota já tem uma primeira medição real — não fica mais em aberto no sentido de "zero dados", ainda que a amostra atual seja um piloto, não uma medição definitiva.

Por que não dá para medir contra a nota oficial do INEP. O INEP publica a nota da redação de cada participante nos microdados oficiais, mas nunca o texto do manuscrito — por privacidade, a redação em si não é disponibilizada. Sem o texto, não há como alimentar o corretor: não existe caminho para medir "vs. nota oficial do INEP", nem para este projeto, nem para nenhum outro.

O proxy público mais próximo é o Essay-BR: ~6.500 redações reais de estudantes brasileiros, corrigidas por especialistas seguindo os critérios do ENEM — na mesma escala oficial (0/40/80/120/160/200 por competência, 0–1.000 no total), sem conversão necessária. Não é a nota oficial (o INEP usa dois corretores humanos, com um terceiro em caso de forte discordância — ver nota de contexto abaixo); é uma aproximação séria, com texto real e nota por competência.

Fonte: Essay-BR — github.com/lplnufpi/essay-br (licença MIT, Laboratório de Processamento de Linguagem Natural, UFPI).

Metodologia do piloto. 30 redações sorteadas aleatoriamente (seed fixa) do subconjunto do corpus com nota > 0 (exclui ~1,4% do corpus com todas as competências zeradas — provável triagem de nota zero, um teste diferente de "acurácia"). Cada uma passou pelo caminho de correção real (src/correction/factory.get_corrector) com o prompt de produção sem variantes (src/prompts.py) — só o texto de entrada muda: tema e proposta do Essay-BR no lugar da foto transcrita por OCR. Provedor: DeepSeek V4 Flash com CORRECTION_EFFORT=max, o mesmo já medido em custo (§8.4.1).

MétricaValor
MAE geral (erro médio absoluto vs. nota do corpus)129,3 pontos (escala 0–1.000)
Viés médio (estimado − corpus)-1,3 pontos — praticamente neutro
MAE por competênciaC1: 30,7 · C2: 27,6 · C3: 35,9 · C4: 29,3 · C5: 45,3

O viés próximo de zero é o achado mais importante: o corretor não infla nem deflaciona a nota sistematicamente — evidência a favor das regras anti-inflação do prompt (ver Análise e Limitações, §3.2). C5 (proposta de intervenção) concentra o maior erro médio, consistente com ser a competência mais subjetiva de avaliar, mesmo entre corretores humanos.

Ponto de referência, não prova de equivalência. O próprio material do Essay-BR descreve que, no processo oficial do ENEM, dois corretores humanos podem divergir em até 80 pontos por competência antes de um terceiro avaliador ser acionado. Todas as médias de erro por competência acima (27–45) ficam abaixo desse limiar — o que não significa que o corretor "iguala" um humano (a comparação aqui é contra um único avaliador do Essay-BR, não dois corretores do INEP em consenso), mas mostra que o erro medido está na mesma ordem de grandeza da variância que o próprio sistema oficial trata como aceitável.

Consistência com uma medição anterior e independente. O teste A/B do prompt (ver Análise e Limitações, §3.2 — n=5, redações de nota conhecida) havia medido MAE de 120,0 pontos para este mesmo prompt de produção — muito próximo dos 129,3 medidos aqui, em corpus e metodologia diferentes. Duas medições convergindo para o mesmo intervalo (~120–130) é evidência de que o número é estável, não um acidente de amostra pequena.

Limitações do piloto — honestas, não descartáveis:

  • Amostra pequena (n=30). Suficiente para uma primeira leitura, não para afirmar acurácia com confiança estatística apertada.
  • Risco de vazamento de dado. O Essay-BR é público desde 2021 — não há garantia de que o LLM usado nunca "viu" pares redação↔nota desse corpus durante o treino. Se houver, o MAE medido fica artificialmente bom; não é possível descartar essa hipótese sem um corpus inédito.
  • Qualidade de dado da fonte, não do corretor. 12,2% do corpus (52 de 151 temas, concentrados num lote específico de ids) tem o texto do tema sem nenhuma acentuação — bug de origem no Essay-BR, não introduzido por este projeto. Não afeta o texto da redação em si, só o enunciado do tema em parte dos casos; 4 das 30 redações da amostra foram afetadas, sem um padrão claro de piorar o erro.
  • Comparação contra um avaliador único, não o consenso de dois corretores do INEP — o Essay-BR é um proxy, nunca o gabarito oficial.

Sensibilidade ao CORRECTION_EFFORT: max vs. high, mesmas 30 redações. O piloto foi repetido inteiro com CORRECTION_EFFORT=high (era max), mesma seed — garantindo comparação pareada, essay a essay:

MétricamaxhighΔ
MAE geral129,3162,7+33,4
Viés médio-1,3 (neutro)-64,0 (subestima)
MAE C1 / C2 / C3 / C4 / C530,7 / 27,6 / 35,9 / 29,3 / 45,332,9 / 26,4 / 27,4 / 33,6 / 49,3misto

Não é ruído: comparando as 30 redações par a par (mesmo texto, effort diferente), high deu nota em média 62,7 pontos mais baixa que max na mesma redação — e a diferença persiste em -61,4 pontos mesmo excluindo os dois maiores outliers, ou seja, é uma tendência ampla, não dois casos isolados puxando a média. Os dois outliers mais extremos foram triagens de nota zero que só aconteceram no high (as mesmas redações tinham nota normal — 240 e 400 — no max): uma por fuga total ao tema, outra por cópia de texto motivador, ambas com trechos do próprio texto citados corretamente na justificativa — não parecem alucinação, mas um julgamento mais rigoroso na aplicação da triagem.

Não dá para saber se high está mais certo ou mais errado. max estava melhor calibrado contra este proxy específico (viés ≈ 0); isso não prova que max é mais correto em termos absolutos, só que combinou melhor com as notas do Essay-BR nesta amostra. A mesma limitação epistêmica do restante desta seção se aplica: high pode estar sendo mais rigoroso do que um corretor humano exigente seria (o que aproximaria da realidade), ou superpunindo (o que afastaria) — sem um terceiro corretor de referência, não dá para desempatar. Na prática, CORRECTION_EFFORT se mostrou um eixo de calibração de comportamento tão relevante quanto a escolha de provedor, e também afeta a latência (ver §8.1) — vale documentar qual valor está em produção e por quê, não só qual é mais rápido.

Por que a amostra fica em n=30 (decisão, não pendência). Ampliar para 100+ apertaria o intervalo de confiança, mas o MAE de ~130 pontos no max já é aceitável para o propósito do produto — a própria correção oficial do ENEM admite divergência entre dois avaliadores humanos, e uma nota estimada nunca prometeu equivalência exata (ver Análise e Limitações, §3.2). Também não repetimos com outro provedor: o Gemini 3 Flash custa ~R$ 0,31/redação contra o custo marginal do DeepSeek (§8.4.1) — a diferença de preço não se justifica para um ganho de qualidade que o piloto não sugere existir. E não há effort intermediário a testar no DeepSeek: a API expõe só dois níveis efetivos, high e max (low/medium são mapeados para high internamente) — ou seja, o espectro completo de effort do DeepSeek já foi coberto pelos dois pilotos, não é uma varredura pela metade. Em resumo: DeepSeek + CORRECTION_EFFORT=max é a configuração adotada, e ampliar a amostra ou trocar de provedor foram avaliados e descartados por custo/benefício, não deixados pendentes.


8.3 Confiabilidade

A correção só é bem-sucedida se as três etapas (download → OCR → LLM) concluírem. O sistema falha graciosamente: retry com backoff em OCR e LLM, devolução de crédito na falha técnica, e estado preservado para o aluno tentar de novo.

Fator de riscoMitigaçãoEfeito
Sobrecarga/cota do provedor (429/503)Retry com backoff exponencial (OCR ×3, LLM ×4)Absorve instabilidade transitória
Falha definitiva na correçãoDevolução do crédito do dia + estado preservadoAluno não perde a cota nem recomeça o fluxo
Webhook duplicadoIdempotência por message_idNão processa (nem cobra) duas vezes
Foto ilegívelPiso de caracteres por foto + edição manualNão manda lixo ao LLM
Exceção inesperadaException handler global → 200 + logNão dispara retries da Meta

Piloto informal. Ao longo do desenvolvimento, mais de 20 correções reais (Telegram, DeepSeek) rodaram de ponta a ponta sem nenhuma falha — as três etapas concluíram e a nota chegou em todas. É um sinal encorajador de que o caminho feliz é estável, mas não é uma taxa de confiabilidade: a amostra é pequena, sem controle sobre fatores externos (instabilidade momentânea de OCR/LLM, cota, rede) que só aparecem em regime de volume e ao longo do tempo. Os retries com backoff existem justamente para absorver esses eventos raros, que o piloto pequeno não teve a chance de provocar.

A medir: a taxa de sucesso por correção em regime real (concluídas ÷ tentativas, com N grande) e a distribuição de falhas por etapa (download × OCR × LLM) — dados que virão da observabilidade (§6.2) e dos testes de stress. Um piloto de 20+ correções sem falha não substitui isso.


8.4 Custo e ROI

8.4.1 Custo por correção (estrutura)

Diferente de um projeto 100% local, cada correção tem custo marginal — duas chamadas pagas:


Custo por correção  ≈  custo_OCR(nº de fotos)  +  custo_LLM(tokens_entrada + tokens_saída)

tokens_entrada  ≈  ~6.000 (bloco estático, CACHEÁVEL)  +  ~500–1.500 (tema + motivadores + redação)
tokens_saída    ≈  a correção formatada (competências + feedback + reescrita)

As três alavancas que mantêm esse custo previsível:

  • Cache de contexto no bloco estático (~6.000 tokens): cobrado uma vez por janela de cache, não a cada correção (explícito no Gemini/Claude, automático no OpenAI/DeepSeek). Medido: 75% dos tokens de entrada vieram do cache no uso real com DeepSeek (ver Testes de Stress) — a alavanca funciona; a ressalva é que o custo por correção acaba dominado pela saída, não pela entrada já cacheada.
  • Rate-limit diário por aluno (ESSAY_DAILY_LIMIT, default 2): teto de custo por usuário.
  • Cota espiada antes do OCR: não gasta Mistral quando a correção não vai acontecer.

Custo de execução (OCR + LLM) — medido, não estimado

Diferente da seção anterior deste documento (que era projeção), os números abaixo vêm de uso real do projeto, e cobrem a parte do custo que independe de onde a aplicação roda (a chamada às APIs de OCR e LLM). O que falta somar é o custo de hospedagem em nuvem (Azure Container Apps — ver Arquitetura, §2.6), ainda não medido.

ItemObservaçãoValor
OCR (Mistral)free tierEm uso real de testes (múltiplas correções), consumiu ~1% da cota gratuita mensalefetivamente US$ 0 dentro da cota
OCR (Mistral) — acima da cotaPreço de tabelaUS$ 4,00 / 1.000 páginasUS$ 0,004 / foto
LLM (DeepSeek V4 Flash)Medido em uso real: US$ 0,03 para 8 correções completas (bloco estático + dinâmico)US$ 0,00375 / correção

Nota de escopo: o valor do LLM foi medido com CORRECTION_PROVIDER=deepseek — que é também o provedor adotado como default do projeto, justamente por esse custo. A troca de provedor é uma variável de ambiente (ver Arquitetura, §2.5); os demais (Gemini, Claude, OpenAI, Maritaca) não foram medidos porque o custo do DeepSeek já resolve o requisito — o Gemini 3 Flash, por exemplo, sai a ~R$ 0,31/redação, ordens de grandeza acima.

Custo total por correção, somando OCR (acima da cota gratuita) + LLM (DeepSeek):

Fotos da redaçãoCusto OCRCusto LLMCusto total / correção
1 foto (caso comum)US$ 0,004US$ 0,00375≈ US$ 0,0078
2 fotos (máximo)US$ 0,008US$ 0,00375≈ US$ 0,0118

Ou seja: menos de 1 centavo de dólar por correção (caso comum, 1 foto) acima da cota gratuita do OCR — e, dentro da cota gratuita (como tem sido a operação até agora), o custo por correção cai para ≈ US$ 0,004 (só o LLM). Em escala, isso é ≈ US$ 7,80 por 1.000 correções (1 foto, fora da cota gratuita) — a referência útil para orçar qualquer volume, multiplicando pelo número de correções esperado.

O câmbio USD→BRL não está fixado aqui de propósito: Mistral e DeepSeek cobram em dólar, e a conversão exata depende da cotação no momento da compra de créditos — para orçamento, use a cotação comercial vigente.

Em aberto. O custo de hospedagem em nuvem (Container App no Azure) ainda não foi medido — é o item que falta para fechar o custo operacional total do projeto (execução + infraestrutura). A comparação custo × qualidade entre os demais provedores de LLM (Gemini, Claude, OpenAI, Maritaca) também segue em aberto.

Custo em escala: de um aluno a uma escola inteira

Com o custo de execução medido (§ anterior), dá para colocar em perspectiva o que ele significa em cenários reais de uso — de um projeto pessoal a uma escola inteira. Nem todo aluno de uma turma ou escola vai efetivamente usar a ferramenta (adoção parcial é a norma em qualquer produto opcional), o que reduz ainda mais o custo agregado frente a uma conta "todos os alunos, todos os dias". As premissas abaixo são ilustrativas e ajustáveis — o objetivo é mostrar a ordem de grandeza, não cravar um número exato.

CenárioPremissa (adoção + uso)Correções estimadasCusto total estimado
Aluno individual — uso típico1 aluno, ~3× por semana ao longo de 4 meses de preparação~50 correções≈ US$ 0,40
Aluno individual — uso máximo1 aluno, no teto diário (ESSAY_DAILY_LIMIT=2) todo dia por 4 meses~240 correções≈ US$ 1,90
Uma turma (30 alunos)~50% adotam o bot, ~20 correções/aluno ao longo do ano~300 correções≈ US$ 2,40
Uma escola (400 alunos)~1/3 adotam o bot, ~20 correções/aluno ao longo do ano~2.660 correções≈ US$ 21,30

Isto é um teto, não uma média — dois fatores empurram o custo real para baixo:

1. A tabela acima IGNORA o free tier do OCR de propósito (assume que 100% das fotos são cobradas ao preço de tabela, US$ 0,004/foto). Na prática, a operação real consumiu só ~1% da cota gratuita mensal da Mistral em testes extensivos (§ anterior) — enquanto a cota durar, o custo de OCR é US$ 0, e a escola de 400 alunos cai de ≈ US$ 21,30/ano para ≈ US$ 10,00/ano (só o custo do LLM).

2. Uso menor que a premissa reduz o custo na mesma proporção. Se a adoção real for menor que 1/3, ou os alunos corrigirem menos que 20 redações/ano, o custo cai linearmente — os números acima são o teto de um cenário de uso relativamente intenso, não o piso.

>

Mesmo no cenário mais caro da tabela (escola inteira, ano letivo completo, adoção de 1/3, sem considerar o free tier), o custo de execução fica na casa de dezenas de dólares por ano — não milhares, não centenas. Isso muda a leitura da seção a seguir (§8.4.3): para uso pessoal, de turma ou de escola, o custo é baixo o bastante para ser bancado do próprio bolso por um aluno, professor ou desenvolvedor entusiasta, sem precisar de nenhum modelo de negócio. Só na escala de múltiplas escolas simultâneas — onde a soma de correções e o custo (ainda não medido) de hospedagem em nuvem começam a pesar — um custeio institucional passaria a fazer sentido, e como viabilização de acesso, não como fim comercial.

8.4.2 Valor gerado (a lógica)

O valor não é "tempo de engenheiro economizado" (como num projeto de automação corporativa), e sim acesso a um recurso pedagógico escasso:

  • Para o aluno: substitui uma correção particular (que custa por redação) ou supre a ausência de qualquer correção detalhada — a um custo marginal de centavos por correção. O ganho é a evolução da nota, que se traduz em acesso a cursos mais concorridos.
  • Para o professor/escola: descarrega a correção repetitiva, liberando tempo docente para ensino e atendimento individual.
  • Para a rede pública: aproxima o feedback de quem hoje depende só da escola, reduzindo a assimetria de acesso.

8.4.3 Por que isto não precisa ser um produto pago

Existe correção de redação por IA de graça — colar o texto num ChatGPT gratuito faz isso. Então "quem pagaria por isto?" é a pergunta errada: a resposta honesta é provavelmente ninguém, e tudo bem. O valor deste projeto não está em cobrar pelo que a IA genérica já faz de graça, e sim em entregar isso onde a IA genérica não chega:

  • Sem app, sem login, sem digitar. A rede da escola costuma bloquear o ChatGPT; aqui o aluno usa o WhatsApp/Telegram que já tem, fotografa o manuscrito e não redigita nada — preservando o treino da letra à mão da prova real.
  • Rigor da matriz, não "dar uma nota". A correção segue a Matriz do INEP transcrita em regras (triagem de zero, cinco competências, trecho citado), não um chute genérico de chatbot.
  • Privacidade pensada para menor de idade. Foto descartada após o OCR, sem histórico, TTL curto — cuidado que uma conversa avulsa num chatbot genérico não oferece.

O enquadramento honesto é portfólio + acesso, não receita. Como o custo de execução é de centavos (§8.4.1) e fica em dezenas de dólares por ano mesmo para uma escola inteira, um projeto assim se sustenta do próprio bolso em escala pessoal, de turma ou de escola — não precisa de modelo de negócio para existir e cumprir seu propósito social e demonstrar competência técnica. Se um dia escalasse para muitas escolas ao mesmo tempo, aí a conta de nuvem (ainda não medida) pesaria, e um custeio institucional — convênio com rede pública, patrocínio de edital — faria sentido como viabilização de acesso, não como fim comercial. Vender assinatura contra um ChatGPT gratuito não é a estratégia; fingir que seria só enfraqueceria a análise honesta que o resto desta seção construiu.


8.5 Roadmap Priorizado

Melhorias derivadas das limitações (aba Análise e Limitações) e das lacunas de medição.

Prioridade 1 — Medir para decidir

  • Confirmar os pilotos locais em nuvem (aba Testes de Stress): latência, rajada na borda e cache já foram medidos localmente; falta repetir em produção (Azure) — inclusive testar o efeito de uvicorn --workers na contenção de background que a rajada expôs.
  • Instrumentar a observabilidade para a taxa de sucesso em regime real (§8.3) — o único número de confiabilidade que ainda falta, já que o piloto informal de 20+ correções sem falha não substitui uma medição com N grande.

Prioridade 2 — Escala e produto

  • Externalizar o Redis para liberar réplicas quando o volume exigir (ver §6.4).
  • Acompanhamento de evolução (opt-in, com base legal e retenção definida) — histórico por competência sem ferir a LGPD.
  • Onboarding institucional (escola/cursinho) com painel de turma para o professor.

Fora do roadmap (decisões tomadas, não pendências): ampliar a amostra de acurácia além de n=30 e medir outros provedores de LLM (Gemini/Claude/OpenAI/Maritaca) foram avaliados e descartadosDeepSeek + CORRECTION_EFFORT=max é a configuração adotada por custo × qualidade (ver §8.2 e §8.4.1). Os efforts de DeepSeek (high e max, os dois únicos efetivos) já foram os dois comparados. Não são lacunas de medição em aberto.


Resumo executivo: o corretor troca "custo zero, latência de madrugada" (típico de automação em lote) por custo marginal por uso e latência sentida em tempo real — o que desloca o foco de otimização para a camada de modelos (provedor, modelo, effort, cache) e para a contenção de custo (rate-limit, cache de contexto). Custo, latência e acurácia da nota já têm uma primeira medição real (todas como piloto local); a mesma variável (CORRECTION_EFFORT) que domina a latência (§8.1) também muda o comportamento da nota (§8.2) — não é só um botão de "mais rápido". Taxa de sucesso em regime real e testes em nuvem seguem pendentes de medição.

Síntese Visual do Produto

O corretor transforma uma foto de redação manuscrita em feedback por competência com trechos citados, no WhatsApp/Telegram, em cerca de um minuto. Os indicadores abaixo resumem o que já está entregue; o custo de execução (OCR + LLM) já foi medido em uso real — a latência e o custo de hospedagem em nuvem seguem com o selo a medir até a execução dos testes de stress.

5
Competências avaliadas (Matriz INEP)
0–1000
Escala da nota estimada
2
Canais (WhatsApp + Telegram)
<200ms
Resposta do webhook (SLA da Meta)
45 min
TTL do estado — nada persiste depois
<1¢
Custo de execução por correção (OCR + LLM, medido)
a medir
Latência e custo de hospedagem em nuvem

Anatomia da nota

Cinco competências, 0–200 cada, somando 1.000

C1 — Escrita formal
0–200
C2 — Tema e repertório
0–200
C3 — Argumentação
0–200
C4 — Coesão
0–200
C5 — Proposta de intervenção
0–200
Cada observação do corretor é ancorada num trecho citado do próprio texto — nada de "impressão solta". A triagem de nota zero roda antes da pontuação.

Onde o tempo é gasto hipótese · a medir

Decomposição esperada da latência até a nota chegar

LLM (correção)
domina
OCR (Mistral)
segundos
Download da foto
baixo
Borda + Redis
~0
Hipótese: OCR + LLM respondem por quase todo o tempo. Toda alavanca de otimização está na camada de modelos. Números reais virão dos Testes de Stress.

O caminho de uma correção

Do primeiro contato à nota — com a foto morrendo logo após o OCR

Consentimento Tema Motivadores 📷 Foto OCR + preview Confirma/edita Correção (LLM)

O gargalo que o produto ataca

O problema nunca foi escrever — foi receber correção detalhada onde o aluno realmente está

Bloqueado
ou digitado
professor/particular: caro e escasso · IA genérica: barrada na escola e exige digitar
~1 min
por foto
no papel, no WhatsApp/Telegram liberado, com a Matriz do INEP

Privacidade por design

Minimização radical para um público menor de idade

  • A foto é descartada logo após o OCR — nunca é gravada
  • O texto vive 45 min em Redis e some sozinho (TTL)
  • Não pede o nome do aluno — só o identificador do canal
  • Sem histórico: nada persiste após a correção
  • Consentimento explícito antes de qualquer processamento
Em uma frase: um corretor que transforma uma foto de manuscrito em feedback por competência com trechos citados — no WhatsApp/Telegram, sem app, com privacidade pensada para menores de idade. O núcleo está pronto; as métricas de performance são o próximo passo.

9. Data Storytelling e Síntese dos Resultados

Esta seção conta a história por trás do projeto — conectando cada decisão de engenharia ao objetivo central: transformar uma foto de redação manuscrita em um diagnóstico pedagógico acionável, no canal que o aluno já usa. O painel visual no topo desta página resume o produto; as métricas de performance nele marcadas como a medir serão preenchidas após os testes de stress.


9.1 A História do Projeto

Boas histórias de dados têm começo, meio e fim — e a deste projeto começa por uma assimetria: milhões de alunos escrevem redações, mas pouquíssimos recebem feedback detalhado o bastante para melhorar.

O problema. A redação do ENEM vale até 1.000 pontos e é decisiva no acesso ao ensino superior. O que faz a nota subir não é escrever mais — é receber um retorno por competência, ancorado no próprio texto. Só que corrigir bem uma redação leva 15–20 minutos e exige um olho treinado na Matriz do INEP. Um professor de escola pública, com dezenas ou centenas de alunos, não tem como dar esse retorno a todos; e a correção particular, que dá, custa caro. A IA generativa reduziu o custo técnico de gerar uma correção — mas colar a redação num ChatGPT ou GPT de terceiros esbarra justamente onde o aluno está: a rede da escola costuma bloquear esses apps, o fluxo exige digitar (perdendo o treino manuscrito da prova real) e nada garante rigor com a matriz oficial nem cuidado com dado de menor. O gargalo, portanto, nunca foi a escrita, nem a existência de IA — é o acesso a uma correção rigorosa, no lugar certo, sem atrito.

A solução e a prova. O bot ataca exatamente esse gargalo. O aluno fotografa a redação escrita à mão — sem instalar app, sem cadastro, sem digitar — e recebe de volta a nota estimada por competência, com trechos citados e uma sugestão de reescrita, em cerca de um minuto. Por trás da simplicidade, decisões de engenharia deliberadas: a borda HTTP responde ao webhook em menos de 200ms e joga o trabalho pesado para background (nada de travar o SLA da Meta); o estado vive 45 minutos em Redis e some sozinho; a foto é descartada logo após o OCR; e o motor de correção usa um prompt de ~6.000 tokens cacheado para conter custo. A correção não é um "chatbot que dá uma nota": é a Matriz de Referência do INEP transcrita em regras — triagem de nota zero, cinco competências com tabelas de níveis, e a exigência de citar o trecho em cada observação.

O valor e o futuro. O que este projeto entrega é acesso: um recurso pedagógico que hoje é caro e escasso, oferecido a um custo marginal de centavos por correção, no aplicativo que o aluno já tem no bolso. Para a escola, é alívio da correção repetitiva; para a rede pública, é redução de uma desigualdade que se converte diretamente em nota de vestibular. À frente, o roadmap confirma em nuvem o piloto local de latência (hoje: ~99s médios até a nota chegar, ~93% disso só na chamada ao LLM) e instrumenta a observabilidade para a taxa de sucesso em regime real; a acurácia da nota já foi validada num piloto contra o Essay-BR (o INEP nunca publica o texto das redações, só a nota — medir "contra o oficial" não é viável para nenhum projeto). E o enquadramento é honesto: contra um ChatGPT gratuito, o valor não é cobrar pela correção, e sim entregá-la onde a IA genérica não chega — sem app, sobre o manuscrito, com o rigor da matriz e privacidade de menor de idade. É portfólio e acesso, não receita.


9.2 Conexão entre Decisão, História e Objetivo

Cada decisão de engenharia serve à narrativa — nenhuma existe por si só.

Decisão de engenhariaO que conta na históriaObjetivo conectado
Foto do manuscrito + OCR"Escreve à mão, como na prova — e não precisa digitar"O1 — transcrever a redação (RF01)
Preview + edição do OCR"O aluno confere a leitura antes da nota; a letra difícil não injustiça"O1 — transcrição fiel
Prompt sobre a Matriz do INEP"Não é um chatbot que chuta nota — é a matriz oficial em regras"O2 — nota por competência com trechos
Multicanal sem app"Encontra o aluno onde ele já está"O3 — WhatsApp + Telegram
Foto descartada + TTL 45 min"Privacidade por design, para um público menor de idade"O4 — conformidade LGPD
Rate-limit + cache de contexto"Custo por uso previsível e contido"O4 — controle de custo
Borda 200ms + background"Responde rápido, processa depois — sem quebrar o SLA da Meta"Robustez
Latência real por etapa✅ piloto medido (n=5, DeepSeek)Métricas e ROI (§8.1)
Acurácia da nota por competência✅ piloto medido (n=30)Métricas e ROI (§8.2)
Taxa de sucesso por correção🔬 20+ sem falha (informal); taxa real ⏳Confiabilidade (§8.3)
Custo por correção✅ medido (DeepSeek)ROI (§8.4)

9.3 O Que Já Dá Para Afirmar (e o que falta)

Já entregue e verificável no código:

  • Fluxo conversacional completo (consentimento → tema → motivadores → foto → preview/edição → correção), com testes unitários puros.
  • OCR da foto com preview e edição manual; redação em até 2 fotos (frente/verso).
  • Correção por competência sobre a Matriz do INEP, com provedor de LLM plugável (5 opções) e cache de contexto.
  • Conformidade de base: consentimento LGPD, descarte da foto, TTL de 45 min, sem histórico, rate-limit diário.
  • Arquitetura mono-contêiner (app + Redis embutido), borda com resposta <200ms e processamento em background.
  • Custo efetivo por correção medido em uso real (DeepSeek V4 Flash) — ver Métricas e ROI, §8.4.1.
  • Acurácia da nota — piloto medido (30 redações do Essay-BR, MAE 129,3 pontos, viés praticamente neutro com CORRECTION_EFFORT=max) — ver Métricas e ROI, §8.2. Não é a nota oficial do INEP (que não publica o texto das redações), e a amostra ainda é pequena para fechar o número. Repetir com CORRECTION_EFFORT=high mudou bastante a nota (MAE subiu para 162,7, viés para -64) — o effort não é só um botão de velocidade, muda o comportamento do corretor.
  • Latência por etapa — piloto local medido (n=5, Telegram): ~99s médios até a nota chegar, dos quais ~93% é só a chamada ao LLM (~92s) — download+OCR somam só ~4s. Confirma com folga a hipótese de que o gargalo é a camada de modelos, não a infraestrutura — ver Métricas e ROI, §8.1.
  • Rajada na borda — piloto local medido (até 100 webhooks concorrentes): o edge escala (100 concorrentes a p95 130ms) e toda requisição retorna 200 (sem retry-storm); na faixa realista (≤20 concorrentes) o <200ms segura. Sob rajada agressiva (≥50), o que degrada é a contenção das tasks de background no worker único, não o edge — ver Testes de Stress.

Pendente de medição (marcado como ⏳ ao longo da documentação):

  • Confirmar a latência em ambiente de nuvem (o piloto acima rodou local, via túnel).
  • Taxa de sucesso e distribuição de falhas em regime real — o caminho feliz rodou 20+ vezes sem falha em testes, mas isso não é uma taxa de confiabilidade com N grande (ver Métricas e ROI, §8.3).
  • Custo de hospedagem em nuvem (Azure Container Apps) — só o custo de execução OCR+LLM foi medido (§8.4.1).

Ampliar a amostra de acurácia, testar outros provedores de LLM e varrer os efforts intermediários não estão nesta lista: foram avaliados e descartados por custo × qualidade (DeepSeek/max adotado), não são pendências — ver Métricas e ROI, §8.2 e §8.5.

Em uma frase: um corretor de redação que transforma uma foto de manuscrito em feedback por competência com trechos citados, no WhatsApp/Telegram, com privacidade pensada para menores de idade — pronto no núcleo, com as métricas de performance como próximo passo.

10. Impacto Ético, LGPD e IA Responsável

Esta seção analisa os impactos éticos, legais e sociais do corretor. Ela parte de um fato que define tudo o que vem a seguir: o público-alvo é majoritariamente menor de idade e o conteúdo tratado (uma redação pessoal) pode revelar opiniões, crenças e circunstâncias do autor. Logo, conformidade e responsabilidade não são acessórios — são requisitos de projeto. A LGPD (Lei nº 13.709/2018) tem, aliás, regra específica para o tratamento de dados de crianças e adolescentes (art. 14).


10.1 Mapeamento de Dados Pessoais no Pipeline (Data Mapping)

O que o sistema realmente trata, e por quanto tempo:

DadoOnde estáÉ dado pessoal?Retenção
Foto da redação (manuscrito)Memória, durante o processamentoSim (pode conter nome, letra, conteúdo pessoal)Descartada logo após o OCR — nunca é gravada
Texto transcrito (OCR)Estado Redis (flow:...)Sim (o conteúdo da redação é do titular)TTL de 45 min; some com o fim do fluxo
Identificador do remetenteChaves Redis (flow:, consent:, essay_count:)Sim (telefone no WhatsApp / chat_id no Telegram)Fluxo/contador expiram; consentimento persiste
Consentimentoconsent:{canal}:{sender}Vinculado ao identificadorSem TTL (é o registro do aceite)
Tema e textos motivadoresEstado RedisGeralmente não-pessoal (dados da proposta)TTL de 45 min
Credenciais (tokens, chaves de API).env / secret storeSegredo (exige proteção)Fora do Git

⚠️ O ponto sensível: conteúdo de uma redação de menor

Uma redação pode revelar, no texto, opinião política, convicção religiosa, origem, circunstâncias familiares — potencialmente dados sensíveis (art. 5º, II) de um adolescente. É por isso que o projeto minimiza ao extremo a retenção: a foto morre no OCR, o texto vive 45 minutos, e nada é gravado após a correção. O sistema também não pede o nome do aluno — o identificador é o do canal, e o produto não precisa de mais nada.


10.2 Aplicação da LGPD ao Projeto

10.2.1 Papéis e bases legais

Conceito LGPDAplicação no projeto
Controlador (art. 5º, VI)Quem opera o bot e decide as finalidades (o autor/instituição que oferece o serviço)
Operador (art. 5º, VII)A infraestrutura de execução e os subprocessadores (provedores de OCR e LLM, plataforma de canal)
Titular (art. 5º, V)O aluno (frequentemente adolescente) que envia a redação
Base legal (art. 7º / art. 14)Consentimento — coletado explicitamente antes de qualquer processamento. Para menores de 16 anos, o art. 14 exige consentimento específico e destacado, dado por um dos pais ou responsável (ver lacuna em §10.5)

10.2.2 Princípios do art. 6º — autoavaliação

PrincípioComo o projeto atendeLacuna / ação
FinalidadeDados usados só para gerar a correção daquela redação✅ Documentar por escrito (aviso de privacidade)
Necessidade (minimização)Não pede nome; foto descartada no OCR; texto com TTL✅ Um dos pontos mais fortes do projeto
Livre acessoEstado é efêmero; pouco há para "acessar" depois⚠️ Definir canal para pedidos do titular/responsável
TransparênciaConsentimento no 1º contato; nota rotulada como estimativa por IA⚠️ Publicar aviso de privacidade completo
Segurança (art. 46)Redis só em 127.0.0.1; segredos fora do Git; HTTPS/HMAC⚠️ Reforçar (ver §10.2.4)
PrevençãoRetry, degradação graciosa, piso de legibilidade
Não discriminaçãoO sistema avalia um texto, não decide sobre a pessoa✅ (baixo risco — ver §10.3)
ResponsabilizaçãoLogs estruturados de cada correção (sem conteúdo sensível)✅ Trilha de auditoria (ver doc 06)

10.2.3 Direitos dos titulares (art. 18) — como atender

DireitoImplementação
Confirmação e acessoO estado é efêmero (TTL 45 min); após a correção, não há dado do aluno armazenado a consultar
Eliminação/cancelar limpa o fluxo na hora; o TTL elimina o resto automaticamente
Revogação do consentimento⚠️ Implementar comando de revogação que apague a chave consent: (ver §10.5)
Informação sobre compartilhamento⚠️ Declarar no aviso de privacidade o uso de OCR e LLM de terceiros (ver §10.2.5)

10.2.4 Segurança da informação (art. 46)

MedidaEstadoMelhoria recomendada
Redis não exposto✅ Só escuta em 127.0.0.1, dentro do contêiner
Segredos fora do versionamento.gitignore cobre .env; produção via Key Vault
Dados em trânsito✅ HTTPS (APIs) + HMAC/secret na verificação de webhook
Minimização em repouso✅ Foto descartada; texto com TTL; sem histórico
Verificação de assinatura✅ Ligada quando ENVIRONMENTlocalGarantir que produção nunca rode como local
AOF do Redis (consentimento)⚠️ Persistido em volume, sem criptografia explícitaCriptografar o volume / restringir acesso ao host

10.2.5 Transferência internacional de dados (art. 33) — o ponto que exige honestidade

Aqui está a diferença mais importante em relação a um projeto de IA local: este sistema envia dados a serviços de IA de terceiros, no exterior.

  • ⚠️ A foto vai para o OCR (Mistral) e o texto da redação vai para o provedor de LLM (Gemini/Claude/OpenAI/DeepSeek/Maritaca, conforme configurado). Isso configura transferência internacional (art. 33) e compartilhamento com operadores/subprocessadores — e precisa ser declarado ao titular no aviso de privacidade.
  • Mitigação por minimização: a foto é descartada logo após o OCR; o texto não é persistido pelo nosso sistema após a correção; não coletamos o nome do aluno. Reduz-se ao máximo o que trafega e por quanto tempo.
  • ⚠️ Dependência das políticas dos provedores: é preciso escolher provedores com salvaguardas contratuais adequadas e, idealmente, com compromisso de não usar os dados para treinamento. Essa avaliação deve constar da documentação de conformidade.
  • 🔧 Alavanca de projeto: como o provedor é plugável por variável de ambiente, é possível selecionar (ou trocar) o provedor com base em critérios de privacidade — inclusive migrar para uma opção nacional (ex.: Maritaca) ou, no futuro, um modelo self-hosted para eliminar a transferência.

Síntese honesta: ao contrário de um pipeline 100% local, aqui a redação sai para modelos de terceiros — este é o principal ponto de exposição do projeto, e ele é declarado, não escondido. A arquitetura o mitiga por minimização radical (foto descartada, texto efêmero, sem nome) e o mantém sob controle pela troca de provedor por configuração. As lacunas remanescentes (consentimento parental do art. 14, revogação, aviso de privacidade) estão no plano de ação (§10.5).


10.3 Análise de IA Responsável

O sistema é de risco limitado: ele avalia um texto e sugere melhorias — não decide automaticamente sobre a vida de ninguém (não define aprovação, crédito, contratação). Ainda assim, aplicam-se princípios consagrados (OCDE, UNESCO, e o PL 2.338/2023 — Marco Legal da IA brasileiro em tramitação).

PrincípioSituação no projetoMitigação implementada / proposta
Supervisão humanaA nota é consultiva; o aluno e o professor decidem o que fazer com ela✅ Posicionada como estimativa; nunca substitui a correção oficial
TransparênciaO aluno pode não saber que é uma correção por IA✅ Consentimento explícito + rótulo de "estimativa por IA" no feedback
RobustezRisco de super/subestimar uma competência (variância do LLM)✅ Prompt calibrado por níveis da matriz; ancoragem em trechos; anti-inflação (ver doc 03)
Justiça / não-discriminaçãoRisco de viés do modelo ao avaliar temas sensíveis⚠️ Monitorar; a proibição de "inventar desvio" e a exigência de citar trecho reduzem arbitrariedade
Privacidade desde a concepçãoMinimização é decisão de arquitetura✅ Foto descartada, texto efêmero, sem nome, sem histórico
ResponsabilizaçãoPreciso saber quem responde por um erro de nota✅ Logs de auditoria; ⚠️ deixar claro que a nota não é oficial

O risco mais importante: a nota tratada como verdade absoluta

Um aluno pode desanimar com uma nota subestimada, ou se acomodar com uma superestimada — e uma correção baseada num OCR que leu errado pode ser injusta. Por isso o produto insiste, em vários pontos, que a nota é uma estimativa (a oficial tem dois corretores humanos e desempate), mostra o preview do OCR antes de corrigir, e apresenta o feedback como orientação de estudo, não veredito. O melhor uso é com um professor no circuito, usando a estimativa como ponto de partida.


10.4 Impacto Social

Impactos positivos

  • Democratização do feedback: leva correção detalhada por competência a quem hoje depende só da escola ou não tem acesso a cursinho — atacando diretamente uma desigualdade que pesa no acesso ao ensino superior.
  • Baixíssimo atrito: sem app, sem cadastro, a partir de uma foto do papel — encontra o aluno no aplicativo que ele já usa.
  • Valorização do trabalho docente: descarrega a correção repetitiva, devolvendo ao professor tempo para ensino e atendimento individual.
  • Aprender com o próprio texto: ao citar trechos exatos e mostrar como reescrever, o feedback ensina — não apenas pontua.

Riscos sociais e mitigação

Risco socialAnáliseMitigação
Excesso de confiança na IAAluno pode tomar a nota como oficialRótulo de estimativa; incentivo ao uso com professor
Exclusão digitalRequer smartphone e internetFormato em canal de baixo consumo já usado por quase todos; sem app extra
Viés do modeloLLMs carregam vieses de treinoDomínio restrito (correção de texto); ancoragem em trechos; monitoramento
Dependência da ferramentaSubstituir o esforço de aprender a autoavaliarFeedback explicativo, que ensina o critério — não só entrega a nota

10.5 Plano de Ação de Conformidade (Checklist Acionável)

Itens de alta prioridade são de baixo esforço e alto retorno de conformidade — vários já têm o mecanismo técnico pronto, faltando só o wiring e o texto legal.

#AçãoPrioridadeEsforço
1Consentimento parental (art. 14): fluxo de aceite específico e destacado por responsável, para menores de 16🔴 AltaMédio
2Aviso de privacidade declarando finalidade, base legal, uso de OCR/LLM de terceiros e transferência internacional🔴 AltaMédio
3Comando de revogação de consentimento que apague a chave consent: e encerre o uso🔴 AltaBaixo
4Escolha documentada de provedores com salvaguarda de privacidade e, idealmente, opt-out de treinamento🟠 MédiaBaixo
5Rótulo permanente de "estimativa por IA" e incentivo ao uso com professor no rodapé do feedback🟠 MédiaBaixo
6Criptografia/restrição do volume onde o AOF do Redis persiste o consentimento🟡 BaixaMédio
7Avaliar provedor nacional ou self-hosted para eliminar a transferência internacional🟡 FuturoAlto

Conclusão da seção: O corretor nasce com uma vantagem estrutural de privacidade — minimização radical: não pede nome, descarta a foto após o OCR, mantém o texto por 45 minutos e não guarda histórico. Sua principal exposição é honesta e declarada: a redação é enviada a modelos de IA de terceiros no exterior, um ponto que a arquitetura mitiga (minimização + provedor plugável) mas não elimina. As lacunas remanescentes são pontuais e conhecidas — com destaque para o consentimento parental do art. 14, incontornável para um público de adolescentes. Eticamente, o sistema se mantém no lugar certo: uma estimativa que orienta o estudo do aluno e apoia o professor, sem jamais se apresentar como a nota oficial nem substituir o julgamento humano.