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
```