File size: 30,118 Bytes
20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 20f9e2e 971cb75 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 | # AetherMap - Métricas, Arquitetura e Avaliação do RAG
Atualizado em: 2026-08-22
Snapshot principal dos benchmarks: 2026-05-11
Este documento é a fonte principal de métricas do AetherMap. Ele descreve a stack,
a arquitetura do RAG, os modos de ablação, os benchmarks executados contra o
Space e a leitura crítica dos resultados. Os números abaixo são um snapshot
histórico, não medições contínuas da versão atualmente publicada.
## 1. O que é o AetherMap
O AetherMap é uma aplicação de **cartografia semântica + RAG híbrido**. Ele
recebe um corpus em TXT/CSV, transforma os textos em embeddings, cria um mapa
3D navegável, agrupa documentos por similaridade, detecta duplicatas e responde
perguntas com citações para os documentos recuperados.
O projeto tem duas frentes principais:
| Frente | Entrega |
| --- | --- |
| Mapa semântico | Visualização 3D, clusters, top termos, riqueza lexical, entropia e duplicatas |
| RAG híbrido | Busca semântica + busca lexical + reranking + resposta LLM com citações |
- Backend principal: `app.py`
- Frontend: `frontend/`
- Experimentos e benchmarks: `metrics/`
## 2. Stack de tecnologia
### Backend e API
| Tecnologia | Uso no projeto |
| --- | --- |
| Python | Linguagem principal do backend e dos scripts de avaliação |
| FastAPI | API HTTP para upload, processamento, busca RAG, busca web e grafo de entidades |
| Uvicorn | Servidor ASGI para execução da API |
| python-multipart | Upload de arquivos via formulário |
| OpenAI SDK | Cliente OpenAI-compatible para chamar OpenRouter |
| Tavily | Busca web opcional em `/search_web/` |
### Machine Learning, NLP e busca
| Tecnologia | Uso no projeto |
| --- | --- |
| SentenceTransformers | Runtime para carregar o modelo de embeddings e gerar vetores de corpus/query |
| `Madras1/minilm-gooaq-mnr-v5` | Embedding fine-tunado por Gabriel Yogi no GooAQ, usado como recuperador semântico customizado do AetherMap |
| CrossEncoder | Reranking dos documentos candidatos |
| `mmarco-mMiniLMv2-L12-H384-v1` | Reranker multilíngue treinado para recuperação |
| FAISS | Índice vetorial para busca semântica por produto interno |
| BM25 customizado | Busca lexical sem dependência externa de `rank_bm25` |
| RRF | Fusão de rankings FAISS e BM25 |
| UMAP | Projeção 3D semântica mais rica |
| PCA | Projeção 3D rápida em `fast_mode` |
| HDBSCAN | Clustering por densidade, sem número fixo de clusters |
| scikit-learn | TF-IDF, CountVectorizer, StandardScaler, PCA |
| NLTK + `stopwords.txt` | Stopwords PT/EN e stopwords customizadas |
| spaCy | NER para grafo de entidades em PT/EN |
| langdetect | Detecção de idioma para escolher modelo spaCy |
### Observabilidade e avaliação
| Tecnologia | Uso no projeto |
| --- | --- |
| Prometheus FastAPI Instrumentator | Métricas HTTP automáticas |
| prometheus-client | Histogramas de latência do LLM, busca total e etapas internas por modo de ablação |
| Hugging Face Datasets | Carregamento de SQuAD-PT nos benchmarks |
| requests | Execução dos benchmarks contra o Space |
| Mistral API | LLM-as-judge externo para avaliar respostas geradas |
| JSON/Markdown | Persistência dos resultados brutos e relatórios |
### Frontend
| Tecnologia | Uso no projeto |
| --- | --- |
| HTML/CSS/JavaScript | Interface do usuário |
| Plotly/Three.js | Visualização 3D e exploração semântica |
| Chart.js | Gráficos auxiliares de métricas |
## 3. Arquitetura do RAG
O RAG do AetherMap é um pipeline em camadas:
```text
Camada 1 - Ingestão
TXT/CSV
-> extração dos textos
-> seleção de coluna textual
Camada 2 - Representação
textos
-> embeddings com modelo fine-tuned para RAG
-> normalização dos vetores
-> índices FAISS e BM25
Camada 3 - Exploração semântica
embeddings
-> UMAP ou PCA para 3D
-> HDBSCAN para clusters
-> TF-IDF por corpus e por cluster
-> duplicatas exatas e semânticas
Camada 4 - Retrieval
query
-> embedding da query
-> busca FAISS
-> busca BM25
-> fusão RRF
Camada 5 - Reranking
candidatos recuperados
-> CrossEncoder(query, documento)
-> top documentos finais
Camada 6 - Geração
contexto com [ID: n]
-> prompt com regra de citação, honestidade e idioma
-> LLM via OpenRouter
-> resposta final citada
```
### Por que o RAG é híbrido
| Recuperador | Força | Fraqueza |
| --- | --- | --- |
| FAISS/embeddings | Sinônimos, paráfrases e similaridade semântica | Pode errar nomes próprios, códigos e termos exatos |
| BM25 | Termos raros, nomes, siglas e correspondência literal | Pode falhar com sinônimos e perguntas parafraseadas |
| RRF | Combina rankings sem depender de escala comum de score | Depende da qualidade dos candidatos iniciais |
| CrossEncoder | Reordena olhando o par query-documento | Custa mais latência que retrieval puro |
A arquitetura favorece **recall amplo primeiro, precisão no topo depois**.
FAISS e BM25 trazem candidatos por sinais diferentes; RRF junta os rankings; o
CrossEncoder decide a ordem final dos melhores documentos.
### Query expansion
O modo `full` ativa a expansão de query via LLM antes da recuperação. A ideia é
aumentar o recall, mas ela tem custo: aumenta a latência e pode introduzir ruído.
No snapshot principal, `full` empatou com `hybrid_rerank` em qualidade e foi mais
lento; portanto, a recomendação operacional é usar `hybrid_rerank`.
## 4. Endpoints principais
| Endpoint | Papel |
| --- | --- |
| `GET /` | Verificação de integridade simples |
| `POST /csv_columns/` | Lista colunas de um CSV antes do processamento |
| `POST /process/` | Processa corpus, gera embeddings, clusters, índices e métricas |
| `POST /search/` | Executa RAG com `ablation_mode` configurável |
| `POST /describe_clusters/` | Usa LLM para descrever clusters |
| `POST /search_web/` | Busca web via Tavily e cria corpus a partir dos resultados |
| `POST /entity_graph/` | Extrai entidades e cria grafo |
| `POST /analyze_graph/` | Análise LLM do grafo de entidades |
| `GET /metrics` | Métricas Prometheus |
## 5. Modos de ablação
| Modo | Componentes ativos | Uso recomendado |
| --- | --- | --- |
| `faiss_only` | FAISS | Baseline semântico |
| `bm25_only` | BM25 | Baseline lexical |
| `hybrid` | FAISS + BM25 + RRF | Modo rápido com recuperação híbrida |
| `hybrid_rerank` | FAISS + BM25 + RRF + CrossEncoder | Melhor compromisso atual entre qualidade e latência |
| `full` | Hybrid rerank + query expansion | Teto experimental de qualidade, mais lento |
Configuração atual de busca:
| Parâmetro | Normal | `turbo_mode=true` |
| --- | ---: | ---: |
| `top_k_retrieval` | até `100` candidatos | até `30` candidatos |
| `final_top_k` | `10` documentos | `5` documentos |
| RRF `k` | `60` | `60` |
| Query expansion | apenas em `full` | desativada |
## 6. Métricas usadas
As métricas foram separadas em retrieval, geração, citação, abstenção e
latência. Isso evita conclusões erradas: um sistema pode recuperar o documento
certo e gerar mal, ou recuperar mal e ainda parecer plausível.
| Área | Métricas |
| --- | --- |
| Retrieval | `Hit@1`, `Hit@3`, `Hit@5`, `Hit@10`, `MRR` |
| Geração literal | `Generation success`, `Strict answer contains gold` |
| Geração semântica | `Semantic answer quality`, `Correctness pass rate` |
| Citação | `Citation valid`, `Citation supports gold doc` |
| Abstenção | `Refusal accuracy` |
| LLM-as-judge | `correctness`, `faithfulness`, `citation_quality`, `refusal_quality`, `verdict_pass_rate` |
| Latência | média, mediana, p95, `llm_api_latency_seconds` |
| Corpus | documentos, clusters, ruído, riqueza lexical, entropia, duplicatas |
Definições importantes:
| Métrica | Definição |
| --- | --- |
| `Hit@k` | Documento correto apareceu entre os `k` primeiros resultados |
| `MRR` | Média de `1/rank` do primeiro documento correto |
| `Generation success` | A resposta foi gerada, sem cair no fallback |
| `Strict answer contains gold` | A resposta contém literalmente a resposta esperada após normalização |
| `Semantic answer quality` | `Correctness avg / 5.0` no juiz Mistral; aproxima qualidade semântica da resposta |
| `Correctness pass rate` | Proporção de respostas com `correctness >= 4` no juiz Mistral |
| `Citation valid` | Toda citação `[ID: n]` aponta para um resultado existente |
| `Citation supports gold doc` | A citação aponta para o documento correto |
| `Refusal accuracy` | O sistema recusa quando o tópico correto não foi fornecido ao corpus |
## 7. Observabilidade implementada
O backend expõe `/metrics` via Prometheus FastAPI Instrumentator:
```bash
curl https://madras1-aethermap.hf.space/metrics
```
Além das métricas HTTP automáticas, o backend agora expõe métricas customizadas
para latência do LLM e para as etapas internas do `/search/`.
| Métrica | Tipo | Mede |
| --- | --- | --- |
| `llm_api_latency_seconds` | Histograma | Tempo da chamada externa ao LLM |
| `aethermap_search_total_latency_seconds` | Histograma | Latência total do endpoint `/search/`, por `ablation_mode` |
| `aethermap_search_stage_latency_seconds` | Histograma | Latência por etapa interna, com os rótulos `stage` e `ablation_mode` |
Etapas instrumentadas em `aethermap_search_stage_latency_seconds`:
| Stage | O que mede |
| --- | --- |
| `model_load` | Carregamento de modelos sob demanda |
| `query_expansion` | Expansão de query do modo `full` |
| `query_embedding` | Embedding da pergunta |
| `faiss_search` | Busca vetorial no índice FAISS |
| `bm25_search` | Busca lexical BM25 |
| `rank_fusion` | Fusão RRF |
| `candidate_prep` | Preparação dos documentos candidatos |
| `rerank` | CrossEncoder reranking |
| `prompt_assembly` | Montagem do contexto e prompt |
| `llm_call` | Chamada externa ao LLM |
O endpoint `/search/` também devolve `timings_ms` na resposta JSON. Isso foi
usado para medir o gargalo real do Space sem instalar FAISS localmente.
Buckets do histograma legado do LLM:
```text
0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0 segundos
```
## 8. Benchmark principal: SQuAD-PT estrito
Este é o benchmark principal para portfólio porque está em português, usa um
dataset real e mede o caminho completo da API do Space: processamento, recuperação
e resposta LLM citada.
| Item | Valor |
| --- | --- |
| Dataset | `nunorc/squad_v1_pt` |
| Split | `validation` |
| API medida | `https://madras1-aethermap.hf.space` |
| Embedding no Space | `Madras1/minilm-gooaq-mnr-v5` |
| Contextos enviados | `120` |
| Perguntas respondíveis | `40` |
| Perguntas controladas sem resposta | `10` |
| Total por modo | `50` queries |
| Total de buscas | `250` |
| Modos testados | `faiss_only`, `bm25_only`, `hybrid`, `hybrid_rerank`, `full` |
| Script | `metrics/run_aethermap_squad_llm_metrics.py` |
| Resultado bruto | `metrics/results/squad_llm_metrics_squadpt-50q-strict-specific-fullanswer-minilm-gooaq-mnr-v5_20260511T025206Z.json` |
Como o SQuAD-PT não possui exemplos nativos sem resposta, essas perguntas foram
criadas de forma controlada. Na versão final do benchmark, os controles sem
resposta usam três filtros:
| Filtro | Motivo |
| --- | --- |
| `source_title` ausente do corpus | Evita vazamento por outros trechos do mesmo artigo |
| título específico, não tópico genérico | Evita controles amplos como `Construction`, que podem ser parcialmente respondidos por documentos relacionados |
| pergunta com âncora nominal | Prefere perguntas com entidades como BSkyB, Sky, Virgin ou Microsoft |
Na rodada final, os controles sem resposta vieram de `Sky_(United_Kingdom)`,
título ausente do corpus enviado.
### Resultados automáticos
| Modo | Hit@1 | Hit@3 | MRR | Generation success | Strict answer contains gold | Citation valid | Citation supports gold doc | Refusal accuracy | Mean latency | p95 latency |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| `faiss_only` | 0.57 | 0.80 | 0.69 | 1.00 | 0.60 | 0.88 | 0.88 | 1.00 | 9.31s | 24.18s |
| `bm25_only` | 0.80 | 0.82 | 0.83 | 1.00 | 0.72 | 0.93 | 0.93 | 1.00 | 10.30s | 26.94s |
| `hybrid` | 0.72 | 0.90 | 0.82 | 1.00 | 0.70 | 0.88 | 0.88 | 1.00 | 8.11s | 18.45s |
| `hybrid_rerank` | 0.97 | 1.00 | 0.99 | 1.00 | 0.68 | 0.97 | 0.97 | 1.00 | 22.34s | 36.26s |
| `full` | 0.97 | 1.00 | 0.99 | 1.00 | 0.70 | 1.00 | 1.00 | 1.00 | 24.82s | 35.61s |
### Qualidade semântica da resposta
O `Strict answer contains gold` é uma métrica propositalmente dura: ela procura
a string do gabarito dentro da resposta. Isso é bom para detectar falhas óbvias,
mas subestima respostas corretas que parafraseiam, traduzem, flexionam ou
respondem com uma formulação equivalente. Por isso a métrica de portfólio para
qualidade de resposta deve ser a qualidade semântica julgada pelo Mistral, junto
com fidelidade e citação.
Artefato:
```text
metrics/results/answer_quality_summary_squadpt_50q_strict_specific_20260511T191047Z.md
```
| Modo | Strict answer contains gold | Semantic answer quality | Mistral correctness avg | Correctness pass rate | Faithfulness avg | Verdict pass rate |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `faiss_only` | 0.60 | 0.92 | 4.60 | 0.88 | 4.82 | 0.90 |
| `bm25_only` | 0.72 | 0.97 | 4.86 | 0.97 | 4.92 | 0.98 |
| `hybrid` | 0.70 | 0.95 | 4.76 | 0.93 | 4.80 | 0.94 |
| `hybrid_rerank` | 0.68 | 0.98 | 4.88 | 0.97 | 4.98 | 0.98 |
| `full` | 0.70 | 0.98 | 4.92 | 0.97 | 5.00 | 0.96 |
Leitura: o número literal `0.68` do `hybrid_rerank` não significa que o sistema
só acerta 68% das respostas. Ele significa que 68% das respostas continham
literalmente a string esperada do SQuAD-PT. Quando o julgamento considera
equivalência semântica, o mesmo modo chega a `Semantic answer quality = 0.98`,
`Faithfulness avg = 4.98` e `Verdict pass rate = 0.98`.
### Leitura técnica
O melhor modo medido é `hybrid_rerank`.
Ele empatou com `full` em retrieval (`Hit@1 = 0.97`, `Hit@3 = 1.00`,
`MRR = 0.99`, refusal `1.00`), mas teve menor latência média
(`22.34s` contra `24.82s`) e melhor pass rate no juiz Mistral. O `full`
continua útil como teto experimental, mas não é o modo recomendado para
portfólio porque a query expansion não trouxe ganho de retrieval nessa rodada.
O `hybrid` foi o modo rápido mais interessante: `Hit@3 = 0.90`,
`MRR = 0.82`, `Refusal accuracy = 1.00` e `p95 = 18.45s`. Ele é bom para uma demonstração
rápida, mas o modo de portfólio deve ser `hybrid_rerank`, porque ele estabiliza
muito melhor o top 1.
`Strict answer contains gold` ficou abaixo das demais métricas porque é uma
métrica literal. Ela penaliza respostas semanticamente corretas que parafraseiam
o gabarito, mudam flexão, usam tradução alternativa ou incluem uma forma
equivalente da entidade. Por isso ela deve ser lida junto com citação e
LLM-as-judge, não isoladamente.
## 9. Avaliação LLM-as-judge com Mistral
Foi rodada uma avaliação LLM-as-judge usando a API da Mistral sobre a rodada
SQuAD-PT estrita. O juiz avaliou todos os cinco modos de ablação, com `50`
respostas por modo (`40` respondíveis e `10` controladas sem resposta),
totalizando `250` julgamentos.
Arquivos:
| Artefato | Caminho |
| --- | --- |
| Script | `metrics/run_mistral_llm_judge.py` |
| Resultado JSON | `metrics/results/mistral_llm_judge_squadpt_50q_strict_specific_fullanswer_all_modes_20260511T025836Z.json` |
| Relatório Markdown | `metrics/results/mistral_llm_judge_squadpt_50q_strict_specific_fullanswer_all_modes_20260511T025836Z.md` |
Correção metodológica importante: a versão anterior do juiz lia
`summary_preview`, truncado em `700` caracteres. Isso foi corrigido. O benchmark
agora salva `generated_answer` com a resposta completa, e o juiz usa
`answer_text_source = generated_answer`. Respostas muito longas ainda podem ser
recortadas apenas pelo limite de prompt do juiz (`max_answer_chars = 6000`), o
que não ocorreu de forma relevante nesta rodada.
| Modo | Julgados | Correctness avg | Faithfulness avg | Citation avg | Refusal avg | Verdict pass rate |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `faiss_only` | 50 | 4.60 | 4.82 | 4.64 | 5.00 | 0.90 |
| `bm25_only` | 50 | 4.86 | 4.92 | 4.70 | 5.00 | 0.98 |
| `hybrid` | 50 | 4.76 | 4.80 | 4.56 | 5.00 | 0.94 |
| `hybrid_rerank` | 50 | 4.88 | 4.98 | 4.92 | 5.00 | 0.98 |
| `full` | 50 | 4.92 | 5.00 | 4.94 | 5.00 | 0.96 |
Leitura: o juiz confirma que `hybrid_rerank` é o melhor modo de apresentação.
Ele empatou com o melhor pass rate (`0.98`), teve `Faithfulness avg = 4.98`,
`Citation avg = 4.92` e manteve `Refusal avg = 5.00`. O `full` teve médias
ligeiramente maiores em correctness/faithfulness, mas menor pass rate (`0.96`)
e maior latência média.
Os casos reprovados restantes foram todos em perguntas respondíveis; nenhum modo falhou
nos controles sem resposta:
| Modo | Casos reprovados | Padrão observado |
| --- | ---: | --- |
| `faiss_only` | 5 | Recuperação/citação mais fraca e algumas recusas indevidas |
| `bm25_only` | 1 | Recusa indevida em uma pergunta sobre Victoria/Koori |
| `hybrid` | 3 | Recusas indevidas e uma citação parcial |
| `hybrid_rerank` | 1 | Resposta parcial sobre oxigênio livre |
| `full` | 2 | Respostas semanticamente válidas, mas parciais frente ao gabarito literal |
## 10. Investigação da precisão de recusa
A rodada ampliada anterior tinha `Refusal accuracy = 0.83`. A investigação
mostrou que isso era um artefato do benchmark, não uma conclusão limpa sobre o
sistema.
O problema era este: os exemplos controlados sem resposta vinham do título
`Super_Bowl_50`. O benchmark removia o contexto exato da pergunta, mas ainda
permitia que outros trechos do mesmo artigo entrassem no corpus. Assim, a
pergunta era marcada como "sem resposta", mas o retriever encontrava documentos
semanticamente próximos sobre Broncos, Panthers, MVP e evento do Super Bowl.
Caso que falhou na rodada antiga:
```text
Qual time da NFL representou o AFC no Super Bowl 50?
```
O sistema recuperava trechos sobre Denver Broncos e Super Bowl 50. O texto
citado não dizia literalmente "representou a AFC", mas o LLM completava a
lacuna por inferência/conhecimento externo. Havia dois problemas:
| Problema | Efeito |
| --- | --- |
| Controle sem resposta por contexto, não por título | O corpus ainda tinha documentos do mesmo tema |
| Prompt permissivo a inferência indireta | O LLM respondia a partir de pista parcial |
Correções aplicadas:
| Arquivo | Correção |
| --- | --- |
| `metrics/run_aethermap_squad_llm_metrics.py` | Controles sem resposta agora exigem que o `source_title` da pergunta esteja ausente do corpus enviado |
| `app.py` | Prompt do RAG foi endurecido contra conhecimento externo, inferência indireta e citação apenas relacionada |
Depois disso, uma rodada com `200` contextos revelou um segundo problema: ao
aumentar demais o corpus, sobravam controles sem resposta vindos de
`Construction`, que é um tópico genérico. Mesmo removendo o título `Construction`,
outros documentos relacionados a prédios, reformas e infraestrutura podiam
sustentar respostas parciais. Isso não era um bom controle de abstenção.
A versão final do script filtra esses casos: controles sem resposta precisam
vir de títulos específicos e perguntas com âncora nominal. Na rodada final, os
controles vieram de `Sky_(United_Kingdom)`, ausente do corpus. Resultado:
`Refusal accuracy = 1.00` em todos os modos e `Refusal avg = 5.00` no juiz
Mistral em todos os modos.
Observação: essas medições foram feitas contra o Space em 2026-05-11. O `app.py`
local também foi endurecido para proibir conhecimento externo, inferência indireta
e vazamento de raciocínio interno em respostas finais. Em 2026-08-22, a configuração
local do build foi corrigida para fixar versões compatíveis do PyTorch e permitir
que o instalador resolva dependências no PyPI. Até a nova versão ser publicada e
os testes serem executados novamente, os números desta seção devem ser tratados
como um snapshot histórico, não como validação do commit atual.
## 11. Análise arquitetural: escala, RAM e Space
O AetherMap foi desenhado como um sistema de **processamento de corpus inteiro**
antes da busca. Isso muda o comportamento do RAG: em vez de calcular tudo a cada
pergunta, ele faz uma fase pesada de ingestão e depois consulta estruturas já
materializadas.
Fluxo real:
```text
/process/
CSV/TXT completo
-> extração da coluna textual
-> embeddings em batch
-> normalização vetorial
-> índice FAISS
-> índice BM25
-> mapa 3D
-> clusters HDBSCAN
-> TF-IDF global/por cluster
-> detecção de duplicatas
-> cache por job_id
/search/
query
-> consulta o job_id já indexado
-> recupera candidatos
-> funde rankings
-> rerankeia
-> envia apenas top documentos ao LLM
```
Por isso a arquitetura aguenta melhor datasets reais do que uma demo que faz
retrieval improvisado a cada pergunta:
| Mecanismo | Por que ajuda |
| --- | --- |
| Indexação uma vez por `job_id` | O custo pesado fica no `/process/`; várias perguntas reaproveitam embeddings, FAISS, BM25 e DataFrame |
| Embeddings em batch | Vetoriza o corpus de forma mais eficiente do que documento a documento |
| FAISS com vetores normalizados | Busca semântica sobre índice pronto por produto interno/cosseno |
| Troca para HNSW em corpus grande | Acima de `FAISS_HNSW_MIN_SIZE`, usa `IndexHNSWFlat`, reduzindo custo de busca em bases maiores |
| Reranker sobre os principais candidatos | O CrossEncoder não vê o corpus inteiro; ele reordena até `100` candidatos recuperados |
Trade-offs atuais da demo no Hugging Face Space:
| Trade-off | Leitura técnica |
| --- | --- |
| Cache em RAM | Decisão adequada para demo em Space: evita depender de armazenamento persistente |
| Sem persistência de jobs/índices | Escolha pragmática, porque o armazenamento local do Space não deve ser tratado como banco durável |
| BM25 custom atual | Percorre documentos para pontuar; em corpus muito grande pode virar gargalo por query |
| `plot_data` retorna todos os textos | Bom para demo visual imediata, mas respostas de `/process/` podem ficar pesadas com datasets inteiros |
| UMAP fora do `fast_mode` | Pode ficar caro em datasets grandes; `fast_mode` com PCA é o caminho prático |
| Query expansion do `full` | Aumenta latência e, nas medições, não melhorou qualidade sobre `hybrid_rerank` |
### Perfil de latência do `/search/` no Space
Para investigar a latência sem instalar FAISS localmente, foi criado um
profiler que usa a própria API hospedada no Hugging Face Space. O script envia
o corpus pelo `/process/`, executa buscas no `/search/` e coleta o campo
`timings_ms` devolvido pelo backend instrumentado.
Artefatos:
| Artefato | Caminho |
| --- | --- |
| Script | `metrics/profile_space_search_timings.py` |
| Resultado JSON | `metrics/results/space_search_timing_profile_squadpt_5q_20260511T204758Z.json` |
| Relatório Markdown | `metrics/results/space_search_timing_profile_squadpt_5q_20260511T204758Z.md` |
Estas medições incluem efeitos reais do Space: CPU compartilhada, poucos
workers/threads, fila, rede e chamada externa ao LLM. Elas descrevem a latência
da demo pública, não uma implantação de produção otimizada.
Latência ponta a ponta do endpoint:
| Modo | Queries | Mean | Median | p95 |
| --- | ---: | ---: | ---: | ---: |
| `faiss_only` | 5 | 10.62s | 10.97s | 16.45s |
| `bm25_only` | 5 | 5.19s | 3.00s | 11.17s |
| `hybrid` | 5 | 4.82s | 3.66s | 8.11s |
| `hybrid_rerank` | 5 | 16.47s | 15.24s | 22.48s |
| `full` | 5 | 18.94s | 18.47s | 25.73s |
Média das etapas internas mais relevantes:
| Modo | Query embedding | FAISS | BM25 | RRF | Rerank | Query expansion | LLM call | Total backend |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| `faiss_only` | 12.29ms | 0.28ms | 0.00ms | 0.02ms | 0.10ms | 0.00ms | 10223.82ms | 10240.65ms |
| `bm25_only` | 15.63ms | 0.00ms | 2.90ms | 0.02ms | 0.10ms | 0.00ms | 4999.80ms | 5022.49ms |
| `hybrid` | 13.95ms | 0.30ms | 2.69ms | 0.17ms | 0.10ms | 0.00ms | 4639.20ms | 4660.91ms |
| `hybrid_rerank` | 13.56ms | 0.29ms | 2.41ms | 0.15ms | 8018.13ms | 0.00ms | 8076.30ms | 16115.11ms |
| `full` | 24.20ms | 0.29ms | 2.81ms | 0.14ms | 7937.09ms | 3252.05ms | 7552.86ms | 18773.65ms |
Leitura técnica: no corpus medido (`120` documentos), FAISS e BM25 são baratos.
FAISS fica abaixo de `0.4ms`; BM25 fica em torno de `2.4ms` a `2.9ms`; RRF fica
abaixo de `0.2ms`. Portanto, a latência alta do portfólio não vem do retrieval
base. O custo vem principalmente de três pontos:
| Gargalo | Evidência |
| --- | --- |
| Chamada externa ao LLM | De `4.6s` a `10.2s` de média nos modos sem rerank/full |
| CrossEncoder reranking | Aproximadamente `8.0s` em `hybrid_rerank` e `full` |
| Query expansion | Aproximadamente `3.25s` adicionais no `full` |
O BM25 customizado merece ser substituído em um corpus grande, mas ele não é o gargalo desta
demonstração. A decisão correta é manter a medição por etapa e revisitar o BM25 quando
o corpus passar para milhares/dezenas de milhares de documentos ou quando o
perfil mostrar `bm25_search` como componente dominante.
Portanto, o AetherMap já é mais do que um protótipo simples: ele tem arquitetura de
RAG de corpus real, com ingestão, indexação, cache por job, recuperação híbrida,
reranking e resposta citada. No formato atual, ele está corretamente otimizado
para uma demo interativa no Space. Para produção fora do Space, os próximos
passos seriam persistir índices/jobs em armazenamento durável, paginar `plot_data`,
otimizar reranking/LLM e trocar o BM25 manual por uma estrutura lexical mais
eficiente quando a escala justificar.
## 12. Ablação do embedding: customizado com GooAQ vs. all-MiniLM-L6-v2
Foi rodada uma comparação A/B com o mesmo dataset, mesma seed, mesmas queries e
mesmos modos. O baseline foi `sentence-transformers/all-MiniLM-L6-v2`; a rodada
customizada usa o embedding fine-tunado no GooAQ.
Arquivo de comparação:
```text
metrics/results/embedding_ablation_custom_vs_minilm.md
```
Resumo dos deltas do embedding customizado contra o baseline:
| Modo | Delta Hit@1 | Delta Hit@3 | Delta MRR | Delta citation support | Delta p95 latency |
| --- | ---: | ---: | ---: | ---: | ---: |
| `faiss_only` | +0.125 | +0.125 | +0.135 | +0.000 | +6.097s |
| `bm25_only` | +0.000 | +0.000 | +0.000 | +0.000 | +8.452s |
| `hybrid` | +0.125 | +0.000 | +0.083 | +0.000 | +4.684s |
| `hybrid_rerank` | +0.000 | +0.000 | +0.000 | +0.000 | +3.887s |
| `full` | +0.000 | +0.000 | +0.000 | +0.000 | +6.908s |
Leitura: nesse benchmark controlado pequeno, o embedding customizado melhorou
os modos em que o recuperador semântico aparece com menos compensação do
reranker, especialmente `faiss_only` e `hybrid`. Nos modos com CrossEncoder
(`hybrid_rerank` e `full`), a qualidade empatou porque o reranker corrigiu o
topo do ranking nos dois casos.
O ganho não deve ser generalizado para qualquer dataset. Em uma rodada pequena
separada sobre SQuAD em inglês, o baseline foi melhor no `faiss_only`, enquanto
os modos com reranker empataram em recuperação. A conclusão sustentada é que o
projeto implementa um embedding próprio e uma medição A/B capaz de revelar onde
ele ajuda ou regride. Para afirmar superioridade por domínio, a comparação deve
ser repetida de forma pareada em SQuAD-PT com 50 a 100 perguntas por modo.
## 13. Nota sobre benchmark controlado antigo
Existe um benchmark sintético pequeno em `metrics/run_aethermap_portfolio_metrics.py`.
Ele não é usado como evidência principal neste documento, porque a rodada antiga
misturava um dataset toy com comportamento de geração/fallback do `full` que não
representa o benchmark real em SQuAD-PT. Para apresentação pública, a evidência
principal deve ser a rodada SQuAD-PT estrita com `50` queries por modo e
LLM-as-judge sobre resposta completa.
O benchmark controlado continua útil como teste de sanidade do harness, mas deve
ser executado novamente depois que a correção do build e o prompt endurecido forem
publicados no Space.
## 14. Como reproduzir
Benchmark principal em português, modo estrito:
```bash
python metrics/run_aethermap_squad_llm_metrics.py --dataset-id nunorc/squad_v1_pt --split validation --n-contexts 120 --n-answerable 40 --n-unanswerable 10 --modes faiss_only bm25_only hybrid hybrid_rerank full --run-label squadpt-50q-strict-specific-fullanswer --embedding-label minilm-gooaq-mnr-v5 --timeout 300 --pause 0.15
```
LLM-as-judge com Mistral:
```bash
python metrics/run_mistral_llm_judge.py --input-json metrics/results/squad_llm_metrics_squadpt-50q-strict-specific-fullanswer-minilm-gooaq-mnr-v5_20260511T025206Z.json --modes faiss_only bm25_only hybrid hybrid_rerank full --model mistral-small-latest --max-answer-chars 6000 --output-prefix metrics/results/mistral_llm_judge_squadpt_50q_strict_specific_fullanswer_all_modes
```
Resumo de qualidade semântica da resposta:
```bash
python metrics/summarize_answer_quality.py --benchmark-json metrics/results/squad_llm_metrics_squadpt-50q-strict-specific-fullanswer-minilm-gooaq-mnr-v5_20260511T025206Z.json --judge-json metrics/results/mistral_llm_judge_squadpt_50q_strict_specific_fullanswer_all_modes_20260511T025836Z.json --output-prefix metrics/results/answer_quality_summary_squadpt_50q_strict_specific
```
Perfil de latência por etapa no Space, sem instalar FAISS local:
```bash
python metrics/profile_space_search_timings.py --api-url https://madras1-aethermap.hf.space --contexts-csv metrics/results/squad_contexts_latest.csv --queries-json metrics/results/squad_queries_latest.json --modes faiss_only bm25_only hybrid hybrid_rerank full --max-queries 5 --timeout 300 --pause 0.15 --output-prefix metrics/results/space_search_timing_profile_squadpt_5q
```
Benchmark controlado:
```bash
python metrics/run_aethermap_portfolio_metrics.py
```
Ablação do embedding:
```bash
python metrics/compare_embedding_runs.py --baseline-json metrics/results/<baseline>.json --custom-json metrics/results/<custom>.json
```
|