NLTagger é a API da Apple para análise de sentimento. Está integrada no iOS e macOS, roda on-device, não precisa de servidor, e está a três linhas de código de distância. É a primeira coisa que você encontra quando busca “sentiment analysis Swift”.

Isso é o que ela retorna para texto que qualquer desenvolvedor escreveria num dia normal:

MensagemNLTaggerRealidade
“delete the temp file”-0.8Instrução neutra
“ok”-0.8Confirmação neutra
“run make test”-0.6Instrução neutra
“commit and push”-0.4Instrução neutra
“great job, thanks!”+1.0Positivo (correto)
“this is fucking broken”-1.0Negativo (correto)

A escala vai de -1.0 (muito negativo) a +1.0 (muito positivo). Segundo a Apple, “delete the temp file” tem quase a mesma carga emocional que “this is fucking broken”. E “ok” – a resposta mais neutra da língua inglesa – pontua -0.8.

Esses não são números inventados. São resultados reproduzíveis em qualquer Mac com macOS 14+.

Como reproduzir

import NaturalLanguage

let tagger = NLTagger(tagSchemes: [.sentimentScore])
tagger.string = "delete the temp file"
let (tag, _) = tagger.tag(
    at: tagger.string!.startIndex,
    unit: .paragraph,
    scheme: .sentimentScore
)
print(tag?.rawValue ?? "nil")
// "-0.8"

Oito linhas de Swift. O resultado é determinístico: o mesmo texto produz o mesmo score a cada execução, em cada dispositivo. Não é um erro aleatório. É um viés sistemático.

Por que acontece

NLTagger usa um modelo treinado com texto de consumo: reviews de produtos, comentários de redes sociais, opiniões sobre restaurantes. Nesse domínio, funciona razoavelmente bem. “This product is terrible” pontua -0.9 e “I love this app” pontua +0.9. Correto em ambos os casos.

O problema é que o vocabulário técnico compartilha palavras com o vocabulário emocional, mas com significados completamente distintos:

  • kill, terminate, abort – operações normais sobre processos
  • fatal, critical, panic – níveis de log
  • crash, dead, zombie – estados do sistema
  • delete, destroy, drop – operações de dados
  • reject, deny, block – controle de acesso

Para um modelo treinado com reviews da Amazon, “delete” é destrutivo, “kill” é violento, e “fatal” é catastrófico. Não tem contexto para saber que “kill the background process” é tão emocional quanto “fecha a porta ao sair”.

Isso se chama lexical bias: o modelo atribui polaridade a palavras individuais sem entender o domínio. É o equivalente a um tradutor automático interpretar “I’m killing it” como um homicídio em curso.

Ninguém percebeu

E aqui está o interessante: esse viés não aparece em nenhum paper acadêmico. Há centenas de artigos sobre sentiment analysis em software engineering – análise de code reviews, detecção de toxicidade em commits, classificação de issues – mas nenhum menciona NLTagger. A comunidade de NLP ignora completamente.

A razão é simples: ninguém usa NLTagger para produção. Os pesquisadores usam modelos do Hugging Face. As empresas usam APIs da OpenAI ou Google. NLTagger é o que usa o desenvolvedor indie que busca uma solução rápida para seu app de iOS, implementa numa tarde, e nunca valida os resultados contra dados reais.

Isso significa que há um número desconhecido de apps na App Store que estão classificando texto técnico com um modelo que acha que “ok” é quase tão negativo quanto um insulto explícito. E nenhuma delas sabe disso.

Um caso real: monitoramento de sessões de código

O problema não é teórico. Construí Tokamak, um app de macOS que monitora sessões do Claude Code. Uma das funcionalidades que queria adicionar era detecção de frustração: se você passa duas horas brigando com um bug, o app deveria conseguir detectar isso pelo tom das suas mensagens.

A abordagem óbvia era NLTagger. Três linhas de código, zero dependências, roda on-device. O protótipo funcionou em 20 minutos.

E classificou “apaga o arquivo temporário e executa os testes” como -0.7. Uma instrução completamente neutra que você daria a um assistente de código. Segundo NLTagger, eu estava à beira de uma crise emocional.

Como resolvemos: camadas determinísticas primeiro

A solução não é “usar um modelo melhor” (embora isso também ajude). A solução é não depender de um único modelo opaco para tudo.

SentimentKit é a biblioteca que escrevi para resolver isso. Usa um pipeline de 4 camadas onde a análise determinística roda primeiro e o ML só intervém quando há ambiguidade:

Mensagem
  │
  ├─ Camada 1: Detector de keywords (determinística, ~20KB)
  │  Dicionários curados de profanidade, frustração e expressões positivas.
  │  8 idiomas (ES, EN, PT, DE, FR, ZH, JA, KO).
  │
  ├─ Camada 2: Regras VADER (determinística, ~500KB)
  │  Negação ("not good"), intensificadores ("very bad"),
  │  MAIÚSCULAS, pontuação.
  │
  ├─ Camada 3: CoreML DistilBERT (opcional)
  │  Só para mensagens longas e ambíguas.
  │
  └─ Camada 4: LLM scorer (opcional, requer API)
     Só para casos que as camadas anteriores não resolvem.

A chave é que as camadas 1 e 2 são determinísticas. Produzem o mesmo resultado toda vez. Não há modelo opaco. Você pode auditar cada dicionário, cada regra. E o mais importante: os comandos técnicos neutros pontuam 0.0, não -0.8.

import SentimentKit

let analyzer = SentimentAnalyzer()

let result = analyzer.analyze("delete the temp file and run make test")
// result.score = 0.0 -- neutro, como deve ser

let angry = analyzer.analyze("que porra é essa, não funciona merda nenhuma")
// angry.score = -2.0 -- dois hits de profanidade detectados

VADER: a ferramenta que deveria ser mais conhecida

As regras da camada 2 são inspiradas no VADER (Valence Aware Dictionary and sEntiment Reasoner), um sistema criado por C.J. Hutto e Eric Gilbert no Georgia Tech em 2014. VADER é um analisador de sentimento baseado em regras, não em ML, que usa um dicionário de ~7500 palavras anotadas por humanos com scores de valência.

O interessante do VADER é que ele lida com modificadores linguísticos que os modelos baseados em bag of words ignoram:

  • Negação: “not good” inverte a polaridade
  • Intensificadores: “very good” amplifica o score
  • Maiúsculas: “GOOD” pontua mais que “good”
  • Mas: “the food was great BUT the service was terrible” – o but dá mais peso à segunda cláusula

VADER tem limitações (não entende sarcasmo, funciona melhor em inglês), mas sua transparência é sua maior virtude. Quando VADER classifica algo mal, você pode abrir o dicionário, encontrar a entrada e corrigi-la. Com NLTagger, você só pode dar de ombros.

A ironia: a Apple criou o problema e a solução

Desde macOS 26 / iOS 26, a Apple oferece o framework Foundation Models, um LLM de ~3B parâmetros que roda on-device. É incomparavelmente melhor que NLTagger para análise de sentimento porque entende contexto, não só palavras soltas.

A mesma Apple que te vendeu um classificador que acha que “ok” é negativo, agora te oferece um modelo de linguagem que entende que “kill the process” é uma instrução técnica. A solução correta existia dentro do mesmo ecossistema; só chegou uma década atrasada.

SentimentKit pode usar Foundation Models como camada 4 (LLM scorer). A ironia de usar Apple Intelligence para corrigir os erros do NLTagger não me escapa.

Como a Anthropic detecta frustração (spoiler: regex)

Um dado curioso. No código do Claude Code (o CLI da Anthropic para Claude), a detecção de frustração do usuário é implementada com expressões regulares. Não com NLTagger. Não com um modelo de ML. Com regex que buscam padrões como “this is broken”, “doesn’t work”, “what the”.

É uma abordagem crua mas efetiva: sem falsos positivos em comandos técnicos, porque os padrões são explícitos. A Anthropic, a empresa por trás de um dos LLMs mais avançados do mundo, usa regex para detectar frustração. Se isso não te diz algo sobre o estado da análise de sentimento em produção, nada dirá.

Golden tests: 144 fixtures e contando

SentimentKit inclui 144 golden messages com asserções exatas: 35 em espanhol, 35 em inglês, 20 em português, 19 em alemão, 20 em francês e 15 em chinês. Os dados vêm de datasets publicados (cardiffnlp/tweet_sentiment_multilingual, sepidmnorozy/Chinese_sentiment) e de code reviews reais de repositórios públicos.

Cada fixture tem um range esperado de score, as expressões que devem ser detectadas, e as que não. O sistema de testes inclui detecção PHANTOM (entradas do dicionário que nunca coincidem com dados reais) e detecção UNCONSUMED (expressões reais que faltam nos dicionários). É a mesma abordagem que uso no Tokamak para validar DTOs contra dados de produção: se o LLM inventou um dado, o teste falha.

Teste você mesmo

git clone https://github.com/frr149/SentimentKit
cd SentimentKit
swift test

Ou como dependência Swift Package Manager:

.package(url: "https://github.com/frr149/SentimentKit.git", from: "1.0.0")

O pacote funciona sem CoreML e sem conexão à internet. As camadas determinísticas (keywords + VADER) são suficientes para a maioria dos casos de uso em texto técnico.

A lição

Se você usa NLTagger para qualquer texto que não sejam reviews de produtos, está obtendo dados lixo com duas casas decimais de falsa precisão. O modelo não está quebrado; está fora de domínio. E a API não te avisa.

A comunidade NLP ignora. A Apple não documenta como limitação. E os desenvolvedores que usam em produção nunca validam os resultados, porque um Double entre -1.0 e 1.0 parece rigoroso mesmo não significando nada.

Antes de confiar num score de sentimento, tem que se perguntar: “ok” deveria ser -0.8? Se sua ferramenta diz que sim, o problema não está no usuário. Está na ferramenta.