# 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/.json --custom-json metrics/results/.json ```