
Escalando o gold-standard de 72 para 300+ claims
Workflow pre-annotation (Gemma 4) + revisão humana (Shiny)
Hugo Rodrigues
Source:vignettes/llm-annotation-workflow.Rmd
llm-annotation-workflow.RmdPor que escalar o gold-standard
O benchmark de v1.8.0 (vignette("llm-kg-benchmark"))
roda em 30 abstracts sintéticos × 72 claims — suficiente para validar a
infraestrutura, mas não para um paper. Com 300 claims anotadas em
abstracts reais do OpenAlex / SciELO:
- O intervalo de confiança de Wilson 95% sobre o F1 passa de
[0,69, 0,88]para[0,75, 0,84]— estreito o suficiente para declarar diferenças entre backends. - Com subgrupos por tópico (manejo, clima, textura, …) cada célula tem ≥ 30 claims, o mínimo estatístico para análise por subgrupos.
- Claims em abstracts reais expõem as idiosincrasias de cada LLM no jargão pedológico real (Latim para espécies de gramíneas, siglas de WRB, etc.).
Anotar 300 claims do zero levaria ~2 dias de pedólogo. A Ferramenta 2 reduz para ~3 horas via pre-annotation + revisão.
O workflow em três passos
┌────────────────────────┐
│ corpus.jsonl │ (fetched from OpenAlex/SciELO)
│ abstracts only │
└───────────┬────────────┘
│ llm_preannotate()
▼
┌────────────────────────┐
│ draft_gold.jsonl │ (Gemma 4 extractions, status="draft")
└───────────┬────────────┘
│ llm_annotation_launch()
▼
┌────────────────────────┐
│ reviewed.jsonl │ (human accept / edit / reject / add)
└───────────┬────────────┘
│ llm_annotation_export()
▼
┌────────────────────────┐
│ gold_v2_final.jsonl │ (publication-grade, drafts removed)
└────────────────────────┘
Passo 1 — Construir o corpus
Opção A (produção): buscar 100-300 abstracts reais sobre pedogênese Cerrado no OpenAlex:
library(edaphos)
# OpenAlex — queries ortogonais para cobrir o domínio
queries <- c(
"cerrado AND (pedogenesis OR soil formation)",
"cerrado AND soil organic carbon",
"(cerrado OR savanna) AND (clay OR texture) AND soil",
"cerrado AND (land use change OR deforestation) AND soil"
)
corpus_list <- lapply(queries, function(q) {
causal_corpus_openalex(q, max_results = 50L)
})
corpus <- do.call(rbind, corpus_list)
corpus <- causal_corpus_deduplicate(corpus) # drop DOI duplicates
nrow(corpus) # ~180-200 unique abstracts
# Converter para o schema esperado pelo pre-annotator
corpus_records <- lapply(seq_len(nrow(corpus)), function(i) {
list(
abstract_id = paste0("OA_", sprintf("%04d", i)),
title = corpus$title[i],
abstract_text = corpus$abstract[i],
year = corpus$year[i],
doi = corpus$doi[i]
)
})
# Grava JSONL
con <- file("cerrado_corpus_openalex_2026.jsonl", "w")
for (r in corpus_records) writeLines(
jsonlite::toJSON(r, auto_unbox = TRUE), con)
close(con)Opção B (demo): usar os 30 abstracts sintéticos já disponíveis:
# Produz inst/extdata/cerrado_gold_standard_v1_draft.jsonl
source("data-raw/annotation_tool_demo.R")Passo 2 — Pre-annotation com Gemma 4
A função llm_preannotate() lê o corpus e produz um draft
em que cada claim tem status = "draft" (para o reviewer
saber que veio do LLM e não do humano):
llm_preannotate(
corpus = "cerrado_corpus_openalex_2026.jsonl",
backend = "ollama", # local, gratuito
model = "gemma4:latest",
output_path = "draft_gold_v2.jsonl",
cache_dir = "~/.cache/edaphos_annotation",
verbose = TRUE
)Com Ollama rodando + gemma4:latest pulled, 100 abstracts
são processados em ~25 minutos. O cache_dir torna a
execução resume-safe — se o processo morrer, basta re-rodar e ele pula
os hits do cache.
Sem Ollama? Use o fallback determinístico:
llm_preannotate(corpus, backend = "simulator",
output_path = "draft_gold_v2.jsonl")O simulator não entende os abstracts — apenas amostra pares vocabulário-a-vocabulário — mas produz um draft em schema válido, útil para testar a ferramenta de revisão.
Passo 3 — Revisão humana (Shiny)
Essa é a parte de 3 horas. Abra o reviewer:
llm_annotation_launch(
draft_path = "draft_gold_v2.jsonl",
output_path = "reviewed_gold_v2.jsonl",
keyboard_shortcuts = TRUE
)O app abre no seu navegador padrão. A interface tem três abas:
Aba 1 — Review
- Cabeçalho: abstract ID + título + metadados + texto completo do resumo numa caixa destacada.
-
Tabela de claims: cada claim ocupa uma linha com
dropdowns editáveis para
cause/effect(restritos ao vocabulário canônico), radio parapolarity, slider paraconfidence, campo de texto pararationale, e dois botões: ✓ Accept ✗ Reject. -
Botões de navegação: “+ Add missed claim” (adiciona
linha nova com
status = "added"), “← Previous”, “Accept all →”, “Save & Next →”. - Sidebar: contadores de progresso + lembrete dos atalhos.
Atalhos de teclado (modo rápido)
| Tecla | Ação |
|---|---|
n |
Save & Next |
p |
Previous |
a |
Accept all visíveis |
+ |
Add missed claim |
1–9
|
Toggle Accept no claim n |
Com atalhos você consegue ~2 abstracts por minuto em claims que Gemma
4 acertou (simples a + n). Claims complexas
levam 30 s para editar pause.
Aba 2 — Stats
- Tabela por abstract: n_draft · n_accepted · n_edited · n_added · n_rejected.
- Barplot de cobertura do vocabulário canônico nos claims aceitos — útil para perceber se o corpus está cobrindo todos os 22 conceitos ou se há lacunas.
Aba 3 — Export
Clicar “Validate & export” aciona a lógica de
llm_annotation_export():
- Drops todos os claims
rejected. - Drops claims ainda com
status = "draft"(o humano não os tocou — conservadoramente excluímos). - Remove o campo
statusinterno. - Valida vocabulary + polarity + confidence.
- Escreve
reviewed_gold_v2_final.jsonl.
Passo 4 — Re-rodar o benchmark
Com o gold-standard expandido, basta apontar o runner do v1.8.0 para o arquivo novo:
# Edit data-raw/llm_benchmark_run.R line 22:
# gold_path <- "inst/extdata/cerrado_gold_standard_v2_final.jsonl"
source("data-raw/llm_benchmark_run.R")Os números de P/R/F1/κ agora são publication-grade.
Boas práticas de anotação
Sugestões de critério de anotação (internal consistency dentro do seu gold-standard):
-
Seja conservador com confidence.
0,90+só quando o texto cita estudo controlado ou magnitude quantificada;0,70para claims bem suportadas pelo mecanismo mas sem número;0,50para sugestivo. -
Canonical vocabulary first. Se o texto menciona
“tree cover”, use
vegetation. Se menciona “annual rainfall”, usemean_annual_precipitation. Consistência importa mais que fidelidade literal. -
Polaridade é um sinal, não magnitude. “Reduz SOC” →
-. “Aumenta”, “eleva”, “acelera” →+. Claims condicionais (“reduz se > 2 AU/ha”) ainda são-no gold-standard; a confidence pode cair. -
Uma claim por mecanismo direto. Se o texto diz
“rainfall → NPP → SOC”, registre duas claims
(
precip → vegetation,vegetation → soc), não uma claim indireta. - Rationale = 1 linha. Um fragmento do texto que suporta a claim. Útil para auditorias posteriores e para treinar a próxima iteração do prompt.
v1.8.2 upgrades
Corpus fetcher em produção
# Exige rede. Gratuito. ~40s para 6 queries × 60 results.
Sys.setenv(EDAPHOS_CORPUS_MAILTO = "rodrigues.machado.hugo@gmail.com")
source("data-raw/cerrado_corpus_v2_fetch.R")
# -> inst/extdata/cerrado_corpus_openalex_v2.jsonl (~150 abstracts)DAG preview ao vivo
No app, a aba DAG renderiza um
DiagrammeR::grViz() do grafo agregado de todas as claims
com status = accepted | edited | added:
- Slider min_support filtra arestas por número de ocorrências (útil quando o corpus é grande e você quer só as arestas confirmadas por múltiplos abstracts).
- Cores de aresta: verde = +, vermelha = −.
- Largura de aresta ∝ confidence médio.
- Toggle de labels para visualizar estrutura sem nomes (útil para figuras de paper).
Dark mode
Toggle no sidebar (usa bslib::input_dark_mode()).
Respeita a preferência do sistema operacional.
Publicação no Zenodo
A aba Publish empacota tudo num diretório Zenodo-ready:
# Mesma coisa programaticamente
edaphos::llm_annotation_to_zenodo(
reviewed_path = "reviewed_gold_v2_final.jsonl",
output_dir = "~/Desktop/zenodo_cerrado_kg",
title = "Cerrado Pedogenesis KG (edaphos, v2, 2026-04)",
authors = data.frame(
family_name = "Rodrigues", given_name = "Hugo",
orcid = "0000-0002-8070-8126",
affiliation = "University of São Paulo"
),
keywords = c("Cerrado", "pedometrics", "soil organic carbon",
"knowledge graph", "causal inference"),
license = "CC-BY-4.0",
zip = TRUE
)Conteúdo do bundle:
| Arquivo | Papel |
|---|---|
gold_standard.jsonl |
Gold-standard limpo (draft/rejected removidos) |
kg.ttl |
Grafo serializado em RDF 1.1 Turtle (cada claim vira um
rdf:Statement reificado com predicados
eda:polarity, eda:confidence,
dct:source) |
metadata.json |
DataCite-compatible; pronto para a Zenodo REST API |
README.md |
Descrição, contagens, provenance, citação |
Upload manual em https://zenodo.org/deposit/new, cola os campos do
metadata.json no formulário, publica, registra o DOI
mintado no CITATION.cff do seu paper.
Próximas sessões (v1.8.3 → v1.8.4)
- v1.8.3: production run completo — fetcher → Gemma 4 → anotação humana → Zenodo deposit com DOI real. ~3 horas do pedólogo + ~40 min de máquina.
-
v1.8.4: integrar voting ponderado (Gemma + Claude)
como default de
causal_llm_ingest_abstract_voted(), calibrado pelo κ backend-vs-gold derivado do benchmark.
Referências
- Landis, J. R. & Koch, G. G. (1977). “The measurement of observer agreement for categorical data.” Biometrics 33, 159-174.
- Ollama (2026). https://ollama.com
- OpenAlex (2026). https://openalex.org