Engenharia · · 10 min de leitura · Time Catcher Agents

De 71% a 100% de hit@5:
como afinamos a recuperação do RAG

Um agente que "sabe" seus documentos só é útil se a busca entrega o trecho certo para o modelo. Contamos como o re-ranking sobre uma base híbrida levou o hit@5 do nosso RAG de 5/7 (71%) para 7/7 (100%) num benchmark determinístico — e por que os braços semântico e léxico precisam trabalhar juntos.

Semântica encontra o assunto; léxico resgata o identificador; o re-ranker escolhe o contexto final.

Pipeline medido

Dois braços, vinte candidatos, cinco evidências

  1. Denso + léxicoConceito e correspondência literal.
  2. Fusão RRFRankings combinados por posição.
  3. Top 20Over-fetch preserva candidatos.
  4. Top 5Re-ranking listwise fecha o contexto.
5/7Híbrido RRF, re-rank desligado.
7/7Mesmo pipeline, re-rank ligado.

RAG (retrieval-augmented generation) tem duas metades: a recuperação e a geração. Quase toda a atenção vai para a segunda — qual modelo, qual prompt. Mas se a recuperação entrega o chunk errado, o melhor modelo do mundo responde com confiança sobre o contexto errado. A métrica que importa aqui é o hit@k: em que fração das perguntas o trecho correto aparece entre os k primeiros resultados que você injeta no prompt.

Medimos o nosso com um benchmark determinístico — um conjunto fixo de perguntas com o chunk-alvo conhecido para cada uma. Sem essa fixação, "melhorar o RAG" vira opinião. Com ela, cada mudança tem um número antes e depois.

O ponto de partida do A/B: 71% com híbrido, sem re-ranking

A configuração A do experimento já usava recuperação híbrida com RRF, mas deixava o re-ranking desligado. Em sete perguntas com chunk-alvo conhecido, acertou cinco: 5/7, ou 71% de hit@5. Os quatro identificadores exatos estavam recuperados entre as primeiras posições; as três perguntas de grounding mais profundas produziram as duas falhas.

O A/B também mostrou por que a base híbrida era indispensável. Embeddings capturam significado, mas podem diluir tokens raros e literais. Na pergunta por AVX-039, o braço denso deixou o trecho certo em 14º; o braço léxico o colocou em 1º, e a fusão RRF o trouxe para o topo. O salto de 71% para 100% veio depois, quando o re-ranker corrigiu as três consultas de grounding — mas sem o braço léxico o exact-ID nem chegaria bem posicionado àquela etapa.

A busca semântica é ótima para "sobre o que isso fala" e ruim para "onde está exatamente este código". Produtos reais precisam das duas.

A base do pipeline: busca léxica e densa fundidas com RRF

A busca léxica (BM25) faz exatamente o que falta: casa tokens literais. "AVX-006" na pergunta encontra "AVX-006" no chunk, com peso alto porque é um token raro no corpus. O problema é combinar as duas listas — a densa e a léxica — porque os scores vivem em escalas incomparáveis: a similaridade de cosseno fica num intervalo fixo e estreito, o score do BM25 é ilimitado e depende do tamanho do corpus e da frequência dos termos. Somar os dois é somar metros com graus Celsius; normalizar exige calibração manual que quebra a cada corpus novo.

A solução é o Reciprocal Rank Fusion (RRF), que descarta os scores e usa só as posições: cada documento ganha 1/(k + rank_denso) + 1/(k + rank_léxico), onde k é uma constante de suavização da fórmula (não confundir com o k do top-k) que evita que o primeiro lugar de uma lista atropele tudo. Rank é adimensional — primeiro lugar é primeiro lugar em qualquer escala — então a fusão dispensa qualquer normalização. Um trecho bem posicionado nas duas listas soma duas contribuições altas e sobe; um que só aparece numa delas recebe um único termo e fica atrás. E a fusão não precisa ser simétrica: um peso léxico (lexical_weight, de 0 a 1, default 0.5) inclina o resultado para o braço léxico quando o corpus é denso em códigos e tabelas, ou para o denso quando é prosa conceitual.

É o híbrido com RRF que resgata os exact-ID lookups. O chunk do AVX-039 que o ranking denso deixou em 14º ranqueou em 1º no braço léxico — e a fusão o promoveu ao topo do conjunto de candidatos. Essa recuperação já estava verde na configuração de 71%; o re-ranking da camada seguinte foi o que resolveu as perguntas de grounding restantes.

A correção, camada 2: over-fetch e re-ranking listwise

A fusão melhora a ordem, mas continua sendo uma heurística barata sobre dois rankings aproximados. A segunda camada começa com um over-fetch: em vez de buscar só os 5 finais, o pipeline puxa um conjunto maior de candidatos (k_overfetch, default 20). Esse número importa porque nenhum re-ranker salva um trecho que não chegou à mesa — com 20 candidatos, o chunk certo que a fusão deixou em 12º ainda está no jogo e pode ser promovido.

Sobre esses 20 roda um re-rank listwise via LLM: o modelo recebe a pergunta e a lista inteira de candidatos de uma vez e reordena o conjunto — diferente de um re-ranker pointwise, que pontua cada par pergunta-trecho isoladamente e não enxerga redundância nem complementaridade entre candidatos. Só depois vem o corte no top-k final (default 5). É a diferença entre "quais 20 trechos são plausíveis" (rápido, aproximado) e "quais 5 desses 20 respondem de fato à pergunta" (mais caro, mais preciso, aplicado a poucos candidatos — por isso o custo não explode). Com o re-rank ligado sobre o híbrido, o hit@5 fechou em 100% no benchmark.

O caminho completo de uma pergunta até o contexto do modelo fica assim:

                        pergunta
                            |
            +---------------+---------------+
            |                               |
            v                               v
      braço denso                     braço léxico
 (embeddings, cosseno)           (BM25, tokens literais)
            |                               |
            v                               v
     ranking denso                   ranking léxico
            |                               |
            +---------------+---------------+
                            |
                            v
          fusão RRF — por posição, não por score
          1/(k + rank_denso) + 1/(k + rank_léxico)
                            |
                            v
         over-fetch: ~20 candidatos (k_overfetch)
                            |
                            v
        re-rank listwise via LLM (pergunta + lista)
                            |
                            v
                 corte no top-k (default 5)

A terceira peça: cards de Q&A destilados

Além dos chunks crus, mantemos uma camada de Q&A destilado (o que chamamos de Refinery): perguntas e respostas curtas geradas a partir do corpus. Para consultas diretas, um card "qual o EC50 do AVX-006 → 22,4 µM" bate mais forte e mais limpo do que um parágrafo do PDF original. O modo qa_first prioriza esses cards na recuperação; o corpus cru continua lá para o que os cards não cobrem. E dá para forçar os dois extremos: qa_only responde só com cards, chunks ignora os cards e busca só no corpus cru. Os três modos estão expostos na API — o que nos leva ao próximo ponto.

Os botões reais: inspecionando a busca pela API

Tudo isso seria caixa-preta se você não pudesse ver o que a recuperação devolve antes de ela virar resposta. Cada agente expõe a própria busca em GET /v1/agents/{id}/knowledge/search — o mesmo pipeline que alimenta o chat, chamável direto. São quatro parâmetros: q (a pergunta, obrigatório), k (quantos trechos retornar, default 5, teto 20), source_id (restringe a busca a um único arquivo do corpus) e mode (qa_first, qa_only ou chunks).

curl -s "https://agents-api.catcher.one/v1/agents/AGENT_ID/knowledge/search?q=qual%20o%20EC50%20do%20AVX-006&mode=qa_first" \
  -H "X-API-Key: ctc_SUA_CHAVE"

A resposta ecoa os parâmetros e devolve cada trecho com conteúdo, arquivo de origem e score à vista:

{
  "query": "qual o EC50 do AVX-006",
  "k": 5,
  "source_id": "",
  "mode": "qa_first",
  "chunks": [
    {
      "content": "AVX-006: EC50 de 22,4 µM no ensaio de inibição...",
      "source": "avx-006-relatorio-preclinico.pdf",
      "score": 0.97
    }
  ]
}

Debaixo desse endpoint ficam os botões internos que este post descreveu, com os defaults que o benchmark fixou: hybrid (liga a fusão RRF dos braços denso e léxico), lexical_weight (0 a 1, default 0.5 — inclina a fusão para o braço léxico), k_overfetch (quantos candidatos puxar antes do re-rank, default 20), rerank (o passe listwise, ligado por padrão) e qa_first (prioriza os cards do Refinery). Há ainda structured, um roteamento analítico gateado que só entra quando a pergunta pede agregação em vez de trechos. Na prática, você afina por chamada com k, mode e source_id, e usa o score e o source devolvidos para validar a recuperação no seu próprio corpus — antes de culpar o modelo pela resposta. No Console, a tela de testar busca mostra ainda o debug por braço: em qual dos dois o trecho casou e o rank em cada um (foi assim que enxergamos o "léxico 1º, denso 14º" lá de cima).

Por que um benchmark determinístico importa tanto

Nada disso seria confiável sem a régua fixa: o mesmo conjunto de sete perguntas, cada uma com o chunk-alvo conhecido, rodado antes e depois de cada mudança — um A/B real sobre a recuperação, não impressão de quem testou três perguntas no chat. "Melhorou a busca" é achismo; "o hit@5 saiu de 5/7 para 7/7 ao ligar o re-rank sobre o mesmo híbrido" é engenharia. E como o alvo é conhecido, o benchmark revela o mecanismo: RRF resgatou o exact-ID AVX-039; o re-ranker corrigiu as consultas de grounding. Quando alguém propõe a próxima mudança, a régua diz na hora se ela ajudou, atrapalhou ou não mudou nada.

O que isso significa na prática

Um hit@5 de 100% no benchmark não quer dizer que o agente nunca erra — quer dizer que, para as perguntas medidas, o trecho certo sempre chega ao modelo. A partir daí, a qualidade da resposta é do modelo e do prompt, não da busca. E como o agente cita a fonte de cada afirmação, você vê de onde a resposta veio — e quando a informação não está no corpus, ele responde "não consta" em vez de inventar. Foi essa combinação — recuperação forte + citação + recusa honesta — que validamos também com um corpus de 40 papers fictícios, impossível de acertar por conhecimento prévio do modelo.

Perguntas frequentes

O que é hit@5?

É a fração de perguntas em que o trecho correto aparece entre os 5 primeiros resultados da busca. Um hit@5 de 100% significa que, para todas as perguntas do benchmark, o chunk certo estava no top-5 entregue ao modelo.

Por que busca semântica sozinha falha?

Embeddings capturam significado, mas diluem tokens raros e exatos como códigos, SKUs e identificadores. Uma pergunta por "AVX-006" pode ranquear trechos semanticamente próximos acima do trecho que contém exatamente "AVX-006". A busca léxica resolve isso.

O que é RRF?

Reciprocal Rank Fusion combina duas listas ranqueadas (a densa e a léxica) somando 1/(k+posição) de cada documento. É robusto porque usa posições, não scores de escalas diferentes.

Como testo a recuperação do meu agente?

Pelo endpoint GET /v1/agents/{id}/knowledge/search, passando q (a pergunta) e, opcionalmente, k (default 5, teto 20), source_id (restringe a um arquivo) e mode (qa_first, qa_only ou chunks). A resposta devolve os chunks com content, source e score — o suficiente para ver exatamente o que chegaria ao modelo.

Suba seus documentos e veja a busca funcionando

A recuperação híbrida com re-ranking está pronta em cada agente da Catcher Agents. Crie um agente, suba um PDF e teste a busca no Console — com o score de cada trecho à vista.

Continue lendo

Engenharia

Validando o cérebro do agente com 40 papers fictícios

Por que testamos anti-alucinação com um corpus que não existe no mundo real.

Conceitos

Memória de agentes em camadas

L3, L4, verbatim e o ciclo de sono que consolida o que importa.