AetherMap / docs /METRICS.md
Madras1's picture
Update docs/METRICS.md
fa6d517 verified
|
Raw History Blame Contribute Delete
30.1 kB
# 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
```