--- title: "Classificação com LLM Local em Escala" output: rmarkdown::html_vignette: toc: true toc_depth: 2 vignette: > %\VignetteIndexEntry{Classificação com LLM Local em Escala} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` ## Visão geral Esta vignette cobre o fluxo de `ac_qual_code()` quando o modelo roda **localmente** (LM Studio, llama.cpp, vLLM, Ollama em modo OpenAI) em vez de um provedor em nuvem: o cenário mais comum quando os documentos não podem sair do ambiente do pesquisador (dados sensíveis, acordos de confidencialidade institucional) ou quando o volume de documentos tornaria o custo por token de um provedor pago proibitivo. A orientação aqui vem de uso em produção classificando atos normativos administrativos, com documentos de até 1,4 milhão de caracteres e rodadas de milhares de documentos. Classificar localmente troca custo monetário e risco de vazamento de dados por gerência própria de infraestrutura: tamanho de contexto, throughput e estabilidade do servidor passam a ser problema do pesquisador, não do provedor. As seções abaixo cobrem exatamente os pontos onde isso dá errado na prática. ## 1. Conectando a um servidor local [ac_chat_local()] cria um `Chat` do `ellmer` apontando para qualquer servidor compatível com a API da OpenAI, sem exigir uma chave de API real: ```{r} library(acR) # LM Studio, servindo um modelo na porta padrao (1234) chat_local <- ac_chat_local(model = "qwen2.5-14b-instruct") # llama.cpp (llama-server), porta customizada chat_llama <- ac_chat_local( model = "modelo-local", base_url = "http://localhost:8080/v1" ) ``` Use `chat = chat_local` em [ac_qual_code()] exatamente como usaria qualquer outro `Chat` do `ellmer`: ```{r} coded <- ac_qual_code(corpus, codebook, chat = chat_local) ``` ## 2. Dimensionando o contexto antes de gastar uma chamada Servidores locais costumam ter janelas de contexto menores que provedores em nuvem (4k–32k tokens é comum, contra 128k+ de modelos de ponta). Antes de rodar um corpus inteiro, estime quantos tokens cada documento vai consumir: ```{r} tamanhos <- ac_estimate_tokens(corpus$text) summary(tamanhos) # Quantos documentos provavelmente excedem uma janela de 8k tokens? sum(tamanhos > 8000) ``` `ac_estimate_tokens()` é uma heurística (caracteres por token): não faz nenhuma chamada de rede. Para uma estimativa calibrada contra o modelo e tokenizador real, rode `ac_calibrate_tokens()` numa amostra pequena antes da rodada completa: ```{r} chat_factory <- function(temperature, system_prompt) { ac_chat_local(model = "qwen2.5-14b-instruct", temperature = temperature, system_prompt = system_prompt) } razao <- ac_calibrate_tokens(chat_factory, sample(corpus$text, 10)) tamanhos_calibrados <- ac_estimate_tokens(corpus$text, chars_per_token = razao) ``` Documentos que excedem a janela de contexto do servidor não devem ser enviados como estão: considere dividir manualmente em partes menores antes de montar o `ac_corpus()`, ou reduzir `reasoning_length` para liberar espaço para a resposta. ## 3. Rodando em lotes com checkpoint Uma rodada de milhares de documentos contra um servidor local pode levar horas, e qualquer interrupção (queda de energia, servidor reiniciado, notebook suspenso) sem checkpoint significa reclassificar tudo. Use `checkpoint_dir`: ```{r} coded <- ac_qual_code( corpus, codebook, chat = chat_local, checkpoint_dir = "checkpoints/rodada_2026_10", max_retries = 2L, live = "progress" ) ``` Cada documento é salvo (gravação atômica) assim que classificado. Se a rodada for interrompida, rodar o mesmo comando de novo pula os documentos já concluídos e continua de onde parou. `live = "progress"` mostra uma barra enxuta (documento atual + status) em vez de ecoar categoria e raciocínio a cada uma das `k_consistency` rodadas: importante para não poluir o console numa rodada de milhares de documentos. Se os parâmetros da rodada mudarem (codebook revisado, modelo diferente, `k_consistency` diferente), o `checkpoint_dir` é invalidado e recomeça do zero automaticamente (com aviso), para nunca misturar classificações de codebooks diferentes sem que o pesquisador perceba. ## 4. Lidando com falhas técnicas Servidores locais sob carga sustentada falham de formas que provedores em nuvem bem estabelecidos raramente mostram: timeout por sobrecarga de memória, resposta truncada, conexão recusada depois de muitas horas rodando. A coluna `status` do resultado diferencia os tipos de falha: ```{r} table(coded$status) # ok -- classificado normalmente # resposta_invalida -- o servidor respondeu, mas não em JSON valido # estouro_contexto -- documento excedeu a janela de contexto # timeout -- servidor não respondeu a tempo # erro_servidor -- outro erro de rede/servidor ``` `max_retries` tenta de novo (com espera exponencial) para `timeout` e `erro_servidor`, mas não para `estouro_contexto`: repetir a mesma chamada com o mesmo texto não vai resolver um documento que simplesmente não cabe na janela do modelo. Se mais da metade dos últimos documentos falhar, a rodada é interrompida automaticamente: sinal de que o servidor está instável e insistir só desperdiçaria tempo. ## 5. Validação humana por categoria (multilabel) Quando o codebook é `multilabel = TRUE`, a métrica agregada de [ac_qual_reliability()] mistura decisões binárias independentes num único número: uma concordância "boa" pode esconder uma categoria específica indo mal. Use `by_variable = TRUE`: ```{r} rel <- ac_qual_reliability( llm = coded, human = humano_df, by_variable = TRUE, exclude_status = c("estouro_contexto", "timeout", "erro_servidor") ) attr(rel, "by_variable") ``` `exclude_status` remove da comparação documentos cuja rodada principal falhou: uma categoria vinda de uma resposta inválida ou um timeout não é um dado confiável sobre concordância, e misturá-la nas métricas por variável distorceria o diagnóstico. Para montar a amostra de validação humana de forma representativa da população (não só de cada categoria igualmente, como `strategy = "stratified"` faz), use `strategy = "proportional"`: ```{r} amostra <- ac_qual_sample( coded, n = 100, strategy = "proportional", strata = "especie_normativa" ) ``` A amostra ganha `peso_amostral`, útil se você for agregar a validação numa estimativa populacional ponderada. ## 6. Auditando a configuração da execução Toda chamada de [ac_qual_code()] anexa um atributo `run_info` ao resultado, com a configuração efetiva da rodada: ```{r} info <- attr(coded, "run_info") info$model info$params_efetivos$temperature # a temperatura que o PROVEDOR confirmou info$temperatura_pedida # a temperatura que você PEDIU info$codebook_hash ``` Compare `params_efetivos$temperature` com `temperatura_pedida` antes de reportar `confidence_score` num artigo: se um `Chat` pré-configurado não confirmar a temperatura pedida, `ac_qual_code()` já emite um aviso explícito no momento da rodada, mas `run_info` permite auditar isso depois, inclusive em rodadas de terceiros. ## Referências Wang, X. et al. (2023). Self-Consistency Improves Chain of Thought Reasoning in Language Models. *ICLR*. Gilardi, F.; Alizadeh, M.; Kubli, M. (2023). ChatGPT Outperforms Crowd Workers for Text-Annotation Tasks. *PNAS*, 120(30). Gwet, K. L. (2014). *Handbook of Inter-Rater Reliability* (4th ed.). Advanced Analytics.