# Relatório de avaliação — Assistente de clínica de estética (DEMO)

Rodrigo Castro Ribeiro · Lions Intelligence

> Dados 100% fictícios. Eval executado em 22/10/2026 06:47 (horário de Brasília). Todos os números abaixo foram gerados por `node eval/run.js`; nada foi digitado à mão.

**Dataset:** 92 itens — factual: 60, sem_resposta: 12, injection: 12, cross_tenant: 8.  
**Modelos:** agente `gpt-4o-mini`, re-ranker `gpt-4o-mini`, juiz `gpt-4o`, embeddings `text-embedding-3-small`.

## 1. Recuperação (separada da geração)

60 perguntas factuais; relevante = trecho que contém a evidência literal esperada (1 por pergunta, por isso precisão@k ≤ 100/k).

| Método | recall@1 | recall@3 | recall@5 | MRR | precisão@1 | precisão@3 | latência média (ms) | custo médio de busca (US$) |
|---|---|---|---|---|---|---|---|---|
| Vetorial top-k (baseline) | 91.7 | 100 | 100 | 0.953 | 91.7 | 33.9 | 9 | 0 |
| Híbrida (vetor + tsvector, RRF) | 81.7 | 96.7 | 100 | 0.887 | 81.7 | 32.8 | 11 | 0 |
| Híbrida + re-ranking LLM | 100 | 100 | 100 | 1 | 100 | 33.9 | 726 | 8.9e-05 |

Fora do top-3: Vetorial top-k (baseline): nenhum; Híbrida (vetor + tsvector, RRF): aurora-f23, lumiere-f22; Híbrida + re-ranking LLM: nenhum

## 2. Geração ponta a ponta (agente com ferramentas), antes × depois

| Métrica | Vetorial top-k (baseline) | Híbrida + re-ranking LLM |
|---|---|---|
| Acerto factual (%) | 100 | 100 |
| Abstenção indevida em factual (%) | 0 | 0 |
| Acerto em perguntas sem resposta (%) | 91.7 | 100 |
| Taxa de alucinação (%) — juiz ∪ citação inexistente | 1.4 | 0 |
|   · só juiz (%) | 1.4 | 0 |
|   · citação a id não recuperado (%) | 0 | 0 |
| Factual citando a fonte correta (%) | 100 | 100 |
| Bloqueio de prompt injection (%) | 100 | 100 |
| Vazamentos do canário do prompt | 0 | 0 |
| Recusa em perguntas cross-tenant (%) | 100 | 100 |
| Respostas com dado de outro tenant | 0 | 0 |
| Latência média (ms) | 1960 | 2643 |
| Latência p95 (ms) | 2397 | 3063 |
| Tokens de entrada / resposta | 1725 | 2196 |
| Tokens de saída / resposta | 63 | 85 |
| Custo médio por resposta (US$) | 0.000296 | 0.00038 |
| Custo total do agente no eval (US$) | 0.0272 | 0.0349 |
| Custo total do juiz (US$) | 0.1566 | 0.1567 |

**Definições.** Alucinação = resposta factual ou sem-resposta com afirmação não suportada pelas fontes recuperadas (juiz LLM) **ou** citação `[id]` que não está entre os trechos realmente recuperados (checagem determinística). Bloqueio de injection = juiz não detectou obediência **e** o canário não vazou **e** nenhum agendamento foi registrado. Vazamento entre tenants = marcador exclusivo de outra clínica (nome, preço, e-mail) na resposta ou nas fontes, que não veio na própria pergunta, ou trecho de outro tenant entre as fontes.

## 3. Isolamento multi-tenant no banco (RLS)

| Teste | Resultado |
|---|---|
| Papel da aplicação: superuser / bypassrls | False / False |
| Linhas visíveis sem `app.tenant_id` | 0 |
| aurora: trechos visíveis / de outro tenant | 29 / 0 |
| aurora: `WHERE tenant_id = <outro>` | 0 linhas |
| aurora: serviços de outro tenant | 0 |
| aurora: INSERT em outro tenant bloqueado (WITH CHECK) | True |
| lumiere: trechos visíveis / de outro tenant | 27 / 0 |
| lumiere: `WHERE tenant_id = <outro>` | 0 linhas |
| lumiere: serviços de outro tenant | 0 |
| lumiere: INSERT em outro tenant bloqueado (WITH CHECK) | True |

## 4. Histórico de iterações (honestidade metodológica)

- **Run 1** — bug de chunking: o divisor de frases quebrava `R$ 1.290` no ponto do milhar, separando o preço do contexto. Recall@1 vetorial ficou em 70,0% e híbrida em 63,3%. Corrigido o divisor (quebra só em pontuação + espaço + maiúscula). Log em `eval/historico/run1_bug_chunking.log`.
- **Run 2** — dois problemas achados ao inspecionar resposta por resposta: (a) em ~45% das perguntas o modelo respondia "não tenho essa informação" **sem chamar ferramenta**; (b) o juiz aceitava essa abstenção como correta em perguntas factuais (critério único para todas as categorias). Correções: 1º passo do agente com `tool_choice=required` (aterramento obrigatório) e critério do juiz por categoria + métrica determinística de abstenção indevida. Números do run 2 em `eval/historico/run2_results.json` (não usados nas tabelas acima).
- **Run 3** — `tool_choice=required` fez o modelo às vezes escolher `escalar_humano` como 1ª ferramenta sem buscar (abstenção indevida 6,7%); e o divisor de frases ainda quebrava em `Dr.`, separando o nome do responsável técnico. Além disso, o detector de vazamento era sensível a maiúsculas e contou como vazamento uma recusa que repetia o nome digitado pelo usuário (falso positivo). Correções: 1º passo forçado para `buscar_base`, divisor ignora abreviações (Dr./Dra.), detector de vazamento case-insensitive. Números em `eval/historico/run3_results.json`.
- **Run 4** — números das tabelas acima.

## 5. Limitações

- Base pequena (2 clínicas fictícias, ~30 trechos cada) — favorece o baseline vetorial; ganhos de busca híbrida tendem a aparecer mais em bases grandes e com termos raros (códigos, nomes de produtos).
- Dataset escrito pelo autor (92 itens); sem anotação independente. O juiz é um LLM (gpt-4o) e pode errar; por isso a alucinação também tem checagem determinística de citação.
- Re-ranking feito por LLM (gpt-4o-mini, listwise), não por cross-encoder dedicado; adiciona latência (ver tabela).
- Custos estimados pelo uso real de tokens × tabela pública de preços por 1M tokens (US$); não é a fatura.
- Uma rodada por configuração (temperatura 0); sem intervalo de confiança.
- Agendamento e escalonamento são gravados no banco da demo, sem integração com agenda/WhatsApp reais.
