Lab 85 — Recuperação híbrida e reranking
O problema, e a Cadência que o revisita
O L83 deixou o RAG de atendimento da Cadência em 92,5% de acerto contra um golden set de 40 perguntas — um salto real sobre os 35% do LLM sem recuperação. Ficaram 3 erros residuais, todos medidos e diagnosticados naquele laboratório como problema de chunking (2 casos de tabela cortada no limite do chunk) e de higiene do acervo (1 caso de documento duplicado). O diagnóstico parecia fechado.
Três semanas depois, um atendente escalou um caso: perguntou pelo prazo de garantia do item "EL-4471-B" e recebeu uma resposta citando — com trecho e tudo — a política genérica de eletrônicos, não a de material elétrico específico daquele código. A resposta parecia correta, tinha citação, e estava errada mesmo assim. O time de dados reabriu os 3 erros residuais do L83 para investigar de novo, desta vez inspecionando não só se a resposta batia com o gabarito, mas ONDE o chunk certo ficava no ranking de recuperação antes de qualquer coisa ser cortada ou descartada.
A descoberta mudou o diagnóstico: em pelo menos 2 dos 3 casos, o chunk correto estava no índice inteiro, sem corte de tabela nenhum — só não aparecia entre os 5 mais próximos que a busca vetorial pura devolvia. Ele ficava na posição 8, na posição 11, sempre fora do que entrava no prompt. O problema nunca foi o tamanho do chunk. Foi que um embedding semântico comprime SENTIDO, e um código de produto como "EL-4471-B" carrega pouco sentido — ele carrega uma STRING exata, e busca vetorial pura generaliza exatamente o que uma string exata não deveria generalizar.
Citação errada é pior do que ausência de citação
Uma resposta com citação e uma resposta certa não são a mesma coisa quando a citação vem do trecho errado. "Não encontrei" é um erro visível — o atendente desconfia e escala. Uma citação da política ERRADA sobre garantia de material elétrico é um erro invisível: parece verificável, tem link, tem trecho — e o atendente repassa ao cliente uma informação de segurança incorreta sem saber que está errada. Este é o defeito que a busca vetorial pura esconde melhor que qualquer outro.
O que este laboratório NÃO é
Não é sobre qual banco vetorial usar — isso já foi decidido no tema do L84, e este laboratório assume o resultado dele. Não é sobre guardrails filtrando saída — isso é o L86. Não é sobre um agente decidindo qual ferramenta chamar — isso é o L87. Este laboratório ataca uma coisa: o trecho certo existe no acervo, e a busca não o traz no topo. É o problema de RECUPERAÇÃO que o L19 (BM25 léxico) e o L83 (vetor semântico) resolveram cada um pela metade, sem nunca terem sido combinados.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com uma medição contra o mesmo golden set do L83, ou com um comando na seção de implantação.
- Reproduzir os 3 erros residuais do L83 e diagnosticar a posição real do chunk certo no ranking vetorial puro (rank 8, rank 11 e um empate de versões).
- Configurar um domínio OpenSearch Service com busca híbrida: sub-busca BM25 (léxica) e k-NN (vetorial) executadas juntas sobre o mesmo índice.
- Fundir os dois rankings por Reciprocal Rank Fusion (RRF), sem depender do valor bruto de score de nenhum motor.
- Rerankear o top-N fundido com um cross-encoder (Bedrock Rerank), e explicar por que ele pontua com mais precisão que os embeddings da recuperação inicial.
- Medir o ganho de acerto no golden set do L83 mudando SÓ a recuperação — modelo de geração e prompt de fundamentação inalterados.
- Diagnosticar quando um erro residual NÃO é resolvido por melhor ranking (o caso de documento duplicado) e reconhecer isso como fora do escopo deste laboratório.
- Configurar um domínio OpenSearch Multi-AZ (zone awareness) e explicar a troca de custo em relação ao Serverless do L83/L84.
- Restringir IAM ao modelo de rerank e ao domínio específicos, sem `"Resource": "*"`.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Busca híbrida (léxica + semântica) | MLA-C01 | sub-busca BM25 (match) e k-NN executadas juntas no mesmo domínio OpenSearch | por que léxico e semântico cobrem falhas complementares, não redundantes |
| Reciprocal Rank Fusion (RRF) | MLA-C01 | fórmula 1/(k+rank) fundindo os dois rankings sem depender de score bruto | por que fusão por POSIÇÃO resolve o problema de escalas de score incompatíveis |
| Cross-encoder vs bi-encoder | MLA-C01, AIF-C01 | Bedrock Rerank pontuando o par pergunta+trecho junto, depois da fusão | por que cross-encoder é mais preciso e mais caro que o bi-encoder da recuperação inicial |
| Recall vs precisão em recuperação | MLA-C01 | golden set segmentado por tipo de pergunta (com/sem identificador exato) | reconhecer se o gargalo é RECALL (o certo não está nos candidatos) ou PRECISÃO (está, mas não no topo) |
| Custo e latência de reranking | MLA-C01, SAP-C02 | orçamento de latência decomposto em busca + fusão + rerank + geração | por que reranking só entra sobre um conjunto já filtrado (top-20/30), nunca o índice inteiro |
| Disponibilidade de índice de busca | SAP-C02 | OpenSearch Service com zone awareness (Multi-AZ) em vez de zona única | trade-off de custo fixo por instância contra sobreviver à perda de uma AZ |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um RAG que funciona bem em teste geral, mas erra sistematicamente em perguntas com um dado específico — código, número de processo, nome próprio — e pede a causa. A resposta esperada não é "aumentar k" nem "trocar o modelo de embedding por um melhor": é reconhecer que busca puramente semântica sub-representa string exata, e que a correção estrutural é somar um sinal léxico (BM25) à busca, não só ajustar parâmetro da busca vetorial.
Requisitos, e como cada um muda o desenho
Requisito que não vira uma linha do pipeline de recuperação é intenção. A coluna da direita é onde cada um deixou marca — e o oitavo é o requisito que organiza todos os outros: nada aqui pode tocar geração.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Resposta mantém citação do trecho, mesmo com pipeline de recuperação custom | obrigatório | o prompt final continua exigindo citação; a montagem manual do prompt preserva o metadado de origem por chunk |
| Termo exato (código, artigo, nome próprio) não pode ser perdido pela busca | obrigatório nas perguntas que o citam | inclui sub-busca BM25 (léxica) em paralelo à vetorial — não troca uma pela outra |
| Fusão não pode depender da escala de score de cada motor | obrigatório | RRF (fusão por posição no ranking), nunca soma de score bruto entre motores diferentes |
| Reranking cabe no orçamento de latência do atendimento | até 3 s no total, herdado do L83 | cross-encoder reordena só o top-20/30 já fundido — nunca o índice inteiro |
| Domínio de busca sobrevive à perda de uma zona de disponibilidade | obrigatório em produção | OpenSearch Service provisionado com zone awareness (Multi-AZ), não Serverless de capacidade mínima única |
| Credencial do domínio OpenSearch não fica fixa no código | obrigatório | Secrets Manager com rotação, mais fine-grained access control por role do IAM |
| Auditoria sabe se a citação veio do ranking BM25, do vetorial ou dos dois | obrigatório | log estruturado no CloudWatch com a origem de cada chunk que entrou no rerank |
| Mudança de recuperação não pode alterar o modelo nem o prompt de geração | obrigatório — é o contrato do próprio experimento | Claude e a instrução de fundamentação ficam IDÊNTICOS ao L83; só a pipeline de retrieval muda, para o ganho medido ser atribuível só a ela |
Arquitetura mínima: busca só vetorial — o defeito residual de L83
Este é exatamente o desenho que saiu do L83, sem nenhuma simplificação de propósito. Ele responde 92,5% das perguntas do golden set corretamente — e o defeito só aparece quando a pergunta carrega um identificador exato que a busca semântica generaliza.
- → pergunta com identificador exato: código de produto, artigo de norma, nome de fornecedor
- → encaminha a chamada HTTP
- → RetrieveAndGenerate — a mesma chamada gerenciada do L83, sem mudança
- → busca k-NN pura: distância de cosseno entre o vetor da pergunta e o de cada chunk
- → top-5 por proximidade semântica — o chunk do código exato pode ficar na posição 8
- → pergunta + os 5 chunks recuperados, nem sempre o certo
- → resposta com citação — mas a citação pode ser do chunk errado
- → sugestão pronta para copiar, sem sinal de incerteza
- Fora da AWS
- Rede e entrega
- Compute
- IA e machine learning
- Analytics
É o desenho que a Cadência já tinha em produção depois do L83 e do L84: uma única busca por proximidade semântica entre o vetor da pergunta e o vetor de cada chunk. Percorra os passos e repare no quinto: o chunk com o código exato do produto existe no índice, mas não está entre os 5 mais próximos — a diferença entre "não achou" e "achou o errado" é zero para quem lê a resposta.
- A pergunta carrega um identificador exato. Diferente da maioria das perguntas do golden set do L83, esta cita um código de produto — o pior caso possível para uma busca que só entende sentido, não string.
- A API só repassa, como sempre. Nenhuma mudança em relação ao L83 até aqui — a diferença inteira mora na recuperação, ainda não alcançada.
- RetrieveAndGenerate segue idêntico ao L83. A função delega busca e geração para a mesma chamada gerenciada — é exatamente a peça que este laboratório vai trocar, e ainda não trocou aqui.
- A busca compara só sentido, nunca string. O embedding do código "EL-4471-B" comprime esse texto num vetor que carrega pouco sentido semântico — ele se parece, no espaço vetorial, com qualquer trecho genérico sobre garantia de eletrônicos.
- O chunk certo perde para chunks "parecidos". Medido depois: o trecho com o código exato fica na posição 8 do ranking vetorial. Só os 5 primeiros entram no prompt — a posição 8 nunca chega ao modelo.
- O modelo gera com o que recebeu, mesmo sendo o trecho errado. Claude não sabe que existe um trecho mais correto fora do que foi recuperado — ele fundamenta a resposta nos 5 chunks que chegaram, com a mesma confiança de sempre.
- A resposta sai citada — e citada errado. O atendente recebe um trecho, uma fonte, um link para conferir — só que aponta para a política genérica de eletrônicos, não para a de material elétrico do código perguntado.
A resposta parece tão citável quanto uma resposta certa
Uma citação de norma de segurança elétrica ERRADA não é um erro de UX — é informação de conformidade incorreta saindo com aparência de verificada. O atendente que recebe essa resposta não tem como desconfiar: tem trecho, tem fonte, tem o mesmo formato de toda resposta correta que já viu. É o motivo de este laboratório existir antes de qualquer melhoria de prompt ou de modelo.
Arquitetura para produção: busca híbrida, RRF e reranking
Esta topologia se justifica pelos dois requisitos que a Figura 1 deixa em aberto: não perder termo exato na recuperação, e não deixar a fusão refém da escala de score de cada motor. Cada peça nova abaixo rastreia a um desses dois — se você não consegue apontar qual, ela é adorno.
- → mesma pergunta com o mesmo código de produto, para comparar com a Figura 1
- → encaminha a chamada HTTP
- → sub-busca léxica: match query no campo texto, favorece o termo exato
- → sub-busca vetorial: k-NN no campo embedding, favorece o sentido da pergunta
- → ranking por relevância de termo, até 50 candidatos
- → ranking por proximidade semântica, até 50 candidatos
- → top-20 fundidos por RRF — nenhum score bruto de motor sobrevive à fusão
- → top-5 reordenados pelo cross-encoder, cada par pergunta+trecho pontuado junto
- → pergunta + os 5 chunks reordenados, no mesmo prompt de fundamentação do L83
- → resposta citando o trecho — agora o trecho certo, na maioria dos casos
- → sugestão pronta, com citação
- → loga o chunk citado e a origem do sinal que o trouxe
- → credencial de acesso granular ao domínio, rotacionada automaticamente
- Fora da AWS
- Rede e entrega
- Compute
- Conceito de arquitetura
- Analytics
- IA e machine learning
- Segurança e identidade
- Gestão e governança
Nada muda em Claude, no prompt de fundamentação ou na interface do atendente — é exatamente por isso que o ganho medido na seção de prova pode ser atribuído só à recuperação. O que muda é tudo entre a API e a chamada ao modelo: duas sub-buscas no mesmo domínio, fundidas por RRF, reordenadas por um cross-encoder antes de virar prompt.
- Pergunta chega, autenticação já resolvida. A interface não muda nada em relação às Figuras 1 e 2 anteriores; a credencial que a função usa para autenticar no domínio OpenSearch já vem pronta do Secrets Manager, nunca fixa no código.
- A busca vira duas consultas, não uma. A função dispara duas sub-buscas no MESMO domínio: uma léxica (BM25), uma vetorial (k-NN) — nenhuma substitui a outra.
- Cada motor pontua do seu jeito, e nenhum é "o certo" sozinho. BM25 devolve um score de relevância de termo que pode ir de 0 a dezenas; k-NN devolve uma distância limitada por definição — os dois não são comparáveis na mesma escala.
- RRF funde sem depender da escala de nenhum score. A fusão usa só a POSIÇÃO de cada documento em cada ranking — 1/(k+posição) somado entre os dois motores — o que elimina o problema de normalizar duas escalas diferentes.
- O cross-encoder lê pergunta e trecho juntos, não em vetores separados. A recuperação inicial comparou o VETOR da pergunta com o VETOR do chunk, calculados de forma independente. O cross-encoder recebe os dois textos JUNTOS numa única passada, o que captura relação fina que embeddings separados perdem.
- O prompt final leva os chunks na nova ordem. A função monta o prompt manualmente com o top-5 já reordenado — o modelo, os parâmetros e a instrução de fundamentação são IDÊNTICOS ao L83.
- A resposta sai citando o trecho certo, e a origem fica registrada. O log no CloudWatch guarda se o chunk citado veio do ranking BM25, do vetorial ou dos dois — é o que torna a melhoria auditável, não só "parece melhor agora".
O ganho inteiro acontece antes do modelo de geração
O que muda entre a Figura 1 e esta não é "mais um passo" — é que dois sinais independentes (termo exato e sentido) contribuem para a mesma decisão de ranking, e um terceiro estágio (cross-encoder) confere o resultado antes de qualquer coisa virar prompt. Claude nunca sabe que essa engenharia existe — ele só recebe chunks melhores, na mesma forma de sempre.
Como funciona, ponta a ponta
O payload abaixo é o que a função monta para o rerank e o que recebe de volta — é o material bruto da decisão de reordenação, com o código de produto do caso real que abriu este laboratório.
{
"requisicaoRerank": {
"query": "qual a garantia do item EL-4471-B?",
"sources": [
{"chunkId": "garantia-eletronicos-geral.md#chunk-2", "texto": "Garantia padrão de itens eletrônicos: 90 dias, sem cobrir mau uso."},
{"chunkId": "garantia-material-eletrico.md#chunk-7", "texto": "Item EL-4471-B: garantia de 180 dias por exigência do fornecedor, mediante nota fiscal."},
{"chunkId": "politica-frete-sul.md#chunk-1", "texto": "Frete para a região Sul segue tabela própria, revisada trimestralmente."}
],
"numberOfResults": 5
},
"respostaRerank": {
"resultados": [
{"chunkId": "garantia-material-eletrico.md#chunk-7", "score": 0.94, "posicaoOriginal": 8},
{"chunkId": "garantia-eletronicos-geral.md#chunk-2", "score": 0.31, "posicaoOriginal": 1},
{"chunkId": "politica-frete-sul.md#chunk-1", "score": 0.02, "posicaoOriginal": 14}
]
},
"_comentario": "posicaoOriginal e a posicao no ranking fundido por RRF, antes do rerank. O chunk certo (chunk-7) estava na posicao 8 do ranking so-vetorial, subiu para o topo depois da fusao com BM25, e o cross-encoder confirma com score 0.94 — muito acima do segundo colocado."
}
RRF e reranking não fazem o mesmo trabalho
Repare no `posicaoOriginal`: o chunk certo já tinha subido para dentro do top-N só com a fusão RRF (BM25 encontrou o código exato, mesmo a busca vetorial não tendo achado). O cross-encoder não "resgatou" o chunk do nada — ele confirmou, com um score bem mais alto que o segundo colocado, que a fusão já tinha acertado a direção. As duas peças fazem trabalho diferente: RRF traz candidato pro jogo, reranking decide a ordem final entre eles.
As decisões, e o que se perde em cada uma
📋 O RAG de atendimento da Cadência responde 92,5% do golden set do L83 corretamente, mas erra sistematicamente perguntas com identificador exato — código de produto, artigo de norma, nome de fornecedor. O time não pode tocar no modelo de geração nem no prompt de fundamentação (já calibrados e testados) sem arriscar regressão em tudo que já funciona.
BM25 resolve exatamente o ponto cego do embedding semântico — string exata — sem exigir re-treino nem troca de modelo. RRF funde os dois rankings sem depender de normalizar escalas de score incompatíveis. E o reranking confere a ordem final com um modelo que lê pergunta e trecho JUNTOS, mais preciso que a comparação de vetores separados da recuperação inicial. Nenhuma das três peças toca o modelo de geração — é o que torna o ganho medido atribuível só à recuperação.
Alt: Aumentar k (número de chunks recuperados) na busca vetorial pura — mais chunks no prompt custam mais tokens e mais chance de o modelo se confundir entre trechos — e não resolve o problema estrutural: o chunk certo pode nem estar entre os top-50, não só fora do top-5.
Alt: Trocar o modelo de embedding por um mais recente — melhora recall médio, mas todo bi-encoder semântico comprime identificador exato em representação que prioriza significado, não string — o ganho nesse tipo específico de pergunta é marginal, porque não ataca a causa.
Alt: Fine-tuning do modelo de embedding com os termos da Cadência — mais caro e mais lento de manter atualizado a cada produto novo ou norma revisada — e ainda compete contra a mesma limitação estrutural do bi-encoder. BM25 resolve o mesmo problema sem re-treinar nada.
Alt: Deixar o próprio LLM de geração escolher o trecho certo entre mais candidatos — desloca a responsabilidade de recuperação para geração — exatamente o antipadrão que este laboratório existe para corrigir: RAG erra na recuperação, não na geração, e resolver lá é resolver no lugar errado.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Onde roda a fusão de rankings | RRF (posição), calculado na função | soma de score bruto; média ponderada de score normalizado | imune à escala de cada motor, sem precisar normalizar nada | não pondera "quão relevante" um motor achou o documento, só a ordem — dois documentos em posições próximas ficam quase empatados mesmo com scores brutos bem diferentes |
| Tipo de reranking | Cross-encoder (Bedrock Rerank) | bi-encoder de embedding melhor; heurística de boost por campo (título pesa mais) | lê pergunta e trecho JUNTOS numa passada, captura relação fina que vetores separados perdem | mais caro por par avaliado — por isso reordena só o top-N já filtrado, nunca o índice inteiro |
| Quantos candidatos o rerank recebe | top-20 a 30, vindos da fusão RRF | rerankear os 5 primeiros só; rerankear centenas de candidatos | espaço suficiente pra fusão errar um pouco sem que o cross-encoder perca o certo, sem explodir custo | candidato fora do top-30 fundido nunca chega ao cross-encoder, mesmo que fosse o certo |
| Domínio de busca | OpenSearch Service provisionado, zone awareness | OpenSearch Serverless (o do L83/L84); Aurora pgvector com extensão de busca textual | controle fino sobre o search pipeline de fusão e sobre Multi-AZ explícito | custo fixo por instância-hora, mesmo em tráfego baixo — o Serverless do L83 não cobrava isso |
| O que fica igual ao L83 | Modelo de geração, prompt de fundamentação, formato de citação | ajustar o prompt para "escolher entre os candidatos"; trocar de modelo junto com a recuperação | isola a variável — o ganho medido na prova é só efeito da recuperação | nenhuma melhoria de geração entra nesta rodada, mesmo que uma fosse óbvia — é decisão deliberada, não esquecimento |
Construir: o domínio OpenSearch com busca híbrida e fusão RRF
O domínio troca o Serverless do L83/L84 por OpenSearch Service provisionado — a diferença que compra zone awareness explícito e um search pipeline configurável, ao custo de instância ligada o tempo todo.
# dominio-hibrido.tf — OpenSearch Service Multi-AZ, com o campo de texto (BM25) e o
# campo de vetor (k-NN) no MESMO índice. O search pipeline de fusão é criado por
# fora do Terraform — a API do OpenSearch para pipelines não tem recurso nativo no
# provider; confira a versão do provider antes de assumir isso mudou.
resource "aws_opensearch_domain" "recuperacao_hibrida" {
domain_name = "cadencia-recuperacao-hibrida"
engine_version = "OpenSearch_2.19" # confira a versão GA mais recente antes de aplicar
cluster_config {
instance_type = "r6g.large.search"
instance_count = 4 # 2 por AZ — sobrevive à perda de 1 zona
zone_awareness_enabled = true
zone_awareness_config {
availability_zone_count = 2
}
dedicated_master_enabled = true
dedicated_master_type = "r6g.medium.search"
dedicated_master_count = 3
}
vpc_options {
subnet_ids = [aws_subnet.privada[0].id, aws_subnet.privada[1].id]
security_group_ids = [aws_security_group.opensearch_hibrido.id]
}
encrypt_at_rest { enabled = true }
node_to_node_encryption { enabled = true }
# Sem isto, quem alcança a porta (security group) e quem pode ler cada documento
# (autorização) seriam a mesma pergunta — e não são. O usuário mestre vem do
# Secrets Manager, nunca de senha fixa em variável de ambiente.
advanced_security_options {
enabled = true
internal_user_database_enabled = false
master_user_options {
master_user_arn = aws_iam_role.opensearch_admin.arn
}
}
}
O search pipeline que funde BM25 e k-NN por RRF é um recurso do próprio OpenSearch, criado via API REST — não existe recurso nativo do Terraform para ele até a data desta escrita.
# pipeline-rrf.sh — cria o search pipeline de fusão no domínio já provisionado.
# O nome do processor de RRF mudou entre versões do OpenSearch — confira o nome
# exato disponível na versão do seu domínio antes de aplicar isto em produção.
DOMINIO="https://$(terraform output -raw opensearch_endpoint)"
curl -s -X PUT "$DOMINIO/_search/pipeline/hibrido-rrf" \
-H "Content-Type: application/json" \
--aws-sigv4 "aws:amz:us-east-1:es" \
-d '{
"description": "funde BM25 (campo texto) e k-NN (campo embedding) por RRF",
"phase_results_processors": [
{
"score-ranker-processor": {
"combination": { "technique": "rrf", "rank_constant": 60 }
}
}
]
}'
# Confirma que o pipeline existe antes de referenciá-lo numa consulta
curl -s "$DOMINIO/_search/pipeline/hibrido-rrf" | jq .
// BuscaHibridaService.cs — dispara a consulta hybrid contra o mesmo índice,
// combinando a sub-busca BM25 (campo "texto") com a sub-busca k-NN (campo
// "embedding"), usando o search pipeline "hibrido-rrf" criado acima.
public sealed class BuscaHibridaService
{
private readonly OpenSearchClient _cliente;
public BuscaHibridaService(OpenSearchClient cliente) => _cliente = cliente;
public async Task<IReadOnlyList<ChunkCandidato>> BuscarAsync(
string pergunta, float[] embeddingPergunta, int candidatosPorMotor = 50)
{
var resposta = await _cliente.SearchAsync<ChunkDocumento>(s => s
.Index("politica-atendimento")
.Pipeline("hibrido-rrf") // aplica a fusao RRF no proprio OpenSearch
.Size(candidatosPorMotor)
.Query(q => q
.Hybrid(h => h.Queries(
// Sub-busca 1: BM25 léxica — o que resolve código exato
qq => qq.Match(m => m.Field(f => f.Texto).Query(pergunta)),
// Sub-busca 2: k-NN vetorial — o que resolve paráfrase
qq => qq.Knn(k => k
.Field(f => f.Embedding)
.Vector(embeddingPergunta)
.K(candidatosPorMotor))
))));
if (!resposta.IsValid)
throw new BuscaIndisponivelException(resposta.DebugInformation);
// A ordem já vem fundida por RRF — nao ha score bruto comparavel aqui,
// so a posicao relativa que o pipeline calculou.
return resposta.Hits
.Select((h, i) => new ChunkCandidato(h.Source.ChunkId, h.Source.Texto, posicaoFundida: i))
.ToList();
}
}
O nome do processor depende da versão do domínio
O nome do processor de fusão (`score-ranker-processor`, técnica `rrf`) é uma API que evoluiu entre versões do OpenSearch — versões mais antigas expõem só um `normalization-processor` com técnicas de combinação diferentes (`arithmetic_mean`, por exemplo), que não é RRF de verdade. Confira a documentação da versão exata do seu domínio antes de assumir que o nome deste laboratório está disponível.
Construir: o cross-encoder da Cadência (Bedrock Rerank)
O Rerank é uma chamada separada de RetrieveAndGenerate — recebe a pergunta e uma lista de textos candidatos, devolve os mesmos candidatos reordenados por um modelo que pontua o par inteiro, não um vetor de cada lado.
// ServicoRerank.cs — chama o Bedrock Rerank sobre o top-N já fundido por RRF, e
// devolve só os 5 melhores para entrar no prompt de geração.
public class ServicoRerank
{
private readonly AmazonBedrockAgentRuntimeClient _cliente = new();
// Confira o ID do modelo de rerank disponível na sua região antes de fixar
// este ARN em produção — a AWS adiciona modelo de rerank novo com frequência.
private const string ModeloRerankArn =
"arn:aws:bedrock:us-east-1::foundation-model/amazon.rerank-v1:0";
public async Task<IReadOnlyList<ChunkReordenado>> RerankearAsync(
string pergunta, IReadOnlyList<ChunkCandidato> candidatosFundidos)
{
var pedido = new RerankRequest
{
Queries = new List<RerankQuery> { new() { TextQuery = new() { Text = pergunta } } },
Sources = candidatosFundidos.Select(c => new RerankSource
{
InlineDocumentSource = new() { TextDocument = new() { Text = c.Texto } },
}).ToList(),
RerankingConfiguration = new RerankingConfiguration
{
BedrockRerankingConfiguration = new()
{
ModelConfiguration = new() { ModelArn = ModeloRerankArn },
// So os 5 melhores voltam — rerankear e descartar o resto e
// deliberado: o cross-encoder ja fez o trabalho caro sobre TODOS
// os candidatos, o corte e so no que entra no prompt final.
NumberOfResults = 5,
},
},
};
var resposta = await _cliente.RerankAsync(pedido);
// O indice de origem aponta de volta para o candidato ANTES do rerank —
// e o que permite logar "posicaoOriginal" para auditoria depois.
return resposta.Results
.Select(r => new ChunkReordenado(
candidatosFundidos[r.Index].ChunkId,
candidatosFundidos[r.Index].Texto,
r.RelevanceScore))
.ToList();
}
}
Rerankear o índice inteiro queima orçamento sem melhorar nada
Rerankear o índice inteiro em vez do top-N já filtrado pela fusão RRF é o erro mais caro que este desenho pode cometer: o cross-encoder pontua par a par, e o custo cresce linearmente com o número de candidatos. Rerankear 50 mil chunks por pergunta em vez de 20-30 já fundidos multiplica a fatura por mil e ainda assim não melhora a resposta final — o rerank só reordena o que já está bem posicionado; ele não substitui uma busca que nunca trouxe o candidato certo para perto.
O código de geração em si não muda de forma: continua montando o mesmo prompt de fundamentação do L83, só que agora com os 5 chunks que saíram do rerank, não os 5 que a busca vetorial pura teria escolhido sozinha.
Segurança: quem chama o quê, e com qual credencial
Três credenciais distintas participam deste desenho, cada uma restrita ao que usa de verdade — o mesmo princípio do L41 e do L83, aplicado a mais uma peça: o domínio de busca híbrida.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BuscarNoDominioHibridoEspecifico",
"Effect": "Allow",
"Action": ["es:ESHttpPost", "es:ESHttpGet"],
"Resource": "arn:aws:es:us-east-1:111122223333:domain/cadencia-recuperacao-hibrida/*"
},
{
"Sid": "ChamarRerankRestritoAoModelo",
"Effect": "Allow",
"Action": ["bedrock:Rerank"],
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/amazon.rerank-v1:0"
},
{
"Sid": "ChamarModeloDeGeracaoRestrito",
"Effect": "Allow",
"Action": ["bedrock:InvokeModel"],
"Resource": "arn:aws:bedrock:us-east-1:111122223333:inference-profile/us.anthropic.claude-sonnet-4-20250514-v1:0"
},
{
"Sid": "LerCredencialDoDominioHibrido",
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:us-east-1:111122223333:secret:cadencia/opensearch-hibrido-*"
}
]
}
Alcançar a porta não é o mesmo que ter permissão
O security group do domínio limita QUEM alcança a porta 443 — não decide QUEM pode ler cada documento depois de alcançá-la. Sem `advanced_security_options` com fine-grained access control ligado, qualquer credencial de rede válida dentro da VPC lê o índice inteiro, inclusive documentos que outro time indexou ali por engano. É a mesma distinção rede-vs-autorização que o L83 já tratava para o Bedrock; aqui ela vale para o OpenSearch também.
| Risco | Probabilidade | Impacto | Prevenção |
|---|---|---|---|
| Credencial do domínio fixa em variável de ambiente | Média | Alto — acesso de leitura ao índice inteiro se vazar | Secrets Manager com rotação automática; nunca literal no código ou no Terraform |
| `Resource: "*"` na policy de `bedrock:Rerank` | Baixa, se revisão de PR pegar | Médio — função reranquearia com qualquer modelo, inclusive um mais caro por engano | ARN específico do modelo de rerank, igual ao de `bedrock:InvokeModel` |
| Fine-grained access control desligado no domínio | Baixa em ambiente novo, alta em domínio "herdado" de outro laboratório | Alto — qualquer credencial de rede lê tudo, sem distinção de índice | `advanced_security_options.enabled = true` desde a criação do domínio, nunca como retrofit |
Implantar, e provar com número
A prova reusa o MESMO golden set de 40 perguntas do L83 — nenhuma pergunta nova, nenhum critério novo de acerto. O que muda é o diagnóstico: desta vez, a posição do chunk certo no ranking é medida ANTES de qualquer coisa ser descartada.
# prova.sh — reusa o golden set de 40 perguntas do L83; mede a POSIÇÃO do chunk
# certo no ranking antes de medir taxa de acerto, para separar "nao achou" de
# "achou, mas nao no topo".
GOLDEN_SET=golden-set-politica-cadencia.jsonl # mesmo arquivo de 40 perguntas do L83
# ── Diagnostico: onde os 3 erros residuais do L83 realmente estavam no ranking ──
python3 diagnosticar_ranking.py --golden "$GOLDEN_SET" --modo vetor-puro \
--saida diagnostico-vetor-puro.json
# Pergunta "garantia do item EL-4471-B": chunk certo no INDICE, posicao 8 do
# ranking vetorial puro — so o top-5 entra no prompt, fica de fora
# Pergunta "artigo 14 da norma de instalacao eletrica": chunk certo, posicao 11
# Pergunta "politica de frete Sul, versao vigente": DUAS versoes empatadas nas
# posicoes 1 e 2 — nao e problema de RANKING, sao duas versoes vigentes no indice
# ── Rodada 1: busca so vetorial (a mesma pipeline do L83/L84, sem mudanca) ──────
python3 avaliar.py --endpoint responder-hibrido --modo vetor-puro \
--golden "$GOLDEN_SET" --saida resultado-vetor-puro.json
# Resultado medido: 37 de 40 corretas (92,5%) — identico ao L83, porque nada
# mudou ainda na recuperacao
# ── Rodada 2: busca hibrida (BM25+vetor) + RRF + reranking cross-encoder ────────
python3 avaliar.py --endpoint responder-hibrido --modo hibrido-rerank \
--golden "$GOLDEN_SET" --saida resultado-hibrido-rerank.json
# Resultado medido: 39 de 40 corretas (97,5%) — os 2 casos de identificador exato
# sobem ao topo do ranking fundido; o caso de versao duplicada de politica CONTINUA
# errado, porque nao e um problema de ranking — e duas versoes vigentes no acervo,
# fora do escopo deste laboratorio
# ── Confirmacao: modelo e prompt de geracao nao mudaram ─────────────────────────
diff <(jq -S .configuracaoModelo resultado-vetor-puro.json) \
<(jq -S .configuracaoModelo resultado-hibrido-rerank.json)
# Esperado: sem diferenca — se este diff mostrar algo, o experimento nao isolou a
# recuperacao, e o ganho medido nao e atribuivel so a ela
| Prova | Antes — busca só vetorial | Depois — híbrida + RRF + rerank | O que mudou |
|---|---|---|---|
| Taxa de acerto no golden set (40 perguntas) | 37/40 — 92,5% | 39/40 — 97,5% | os 2 casos de identificador exato subiram ao topo do ranking fundido |
| Posição do chunk certo (item EL-4471-B) | posição 8 — fora do top-5 que entra no prompt | posição 1, após rerank | BM25 encontra o código exato; RRF preserva o candidato; cross-encoder confirma com score alto |
| Posição do chunk certo (artigo 14 da norma) | posição 11 | posição 2 | mesmo padrão: termo exato pesa na busca léxica, quase não pesa na vetorial |
| Erro de versão duplicada de política (frete Sul) | errado | continua errado | não é problema de recuperação — as duas versões estão vigentes no acervo, fora do escopo deste laboratório |
| Latência p95 por pergunta | ≈1,6 s (herdado do L83) | ≈2,3 s | soma da sub-busca BM25 (paralela, quase grátis) e do reranking cross-encoder (≈500–700 ms) |
| Configuração do modelo de geração | Claude, prompt de fundamentação do L83 | idêntica — mesmo modelo, mesmo prompt | o ganho inteiro veio da recuperação, e é exatamente isso que a prova precisa isolar |
O erro que a recuperação não resolve, mesmo perfeita
O erro de versão duplicada de política não desaparece com busca melhor — nem deveria. Fusão e reranking decidem QUAL chunk é mais relevante entre os que EXISTEM no índice; quando duas versões da mesma política estão indexadas ao mesmo tempo, as duas são igualmente "relevantes" para o ranker, e a decisão certa (qual é a VIGENTE) exige metadado de data e disciplina de arquivamento — o mesmo problema que o L83 já tinha nomeado como fora do seu próprio escopo.
Quebrar de propósito: quatro falhas, e o limite que nenhuma delas vence
A busca híbrida com reordenação subiu o acerto e trouxe três peças novas que podem falhar em silêncio: o pipeline de fusão, o reordenador, e o número de candidatos entre os dois. As quatro injeções abaixo derrubam cada uma delas — e a terceira expõe o limite estrutural desta arquitetura.
| Falha injetada | Como injetar | Sintoma enganoso | Diagnóstico correto |
|---|---|---|---|
| Pipeline de fusão não aplicado na consulta | Remover a referência ao search pipeline do corpo da requisição e repetir o golden set | Tudo responde. O acerto volta para perto dos 92,5% do L83, o que ainda parece um bom número | A busca voltou a ser vetorial pura e o BM25 deixou de participar, sem nenhum erro. O código de reordenação continua rodando e reordenando — candidatos piores. O sintoma "voltou ao número anterior" é a assinatura desta falha, e só existe porque há uma linha de base medida: sem o golden set do L83, essa regressão seria invisível para sempre |
| Reordenador indisponível | Negar a permissão de invocação do Rerank na role da aplicação | Dependendo de como o código trata o erro, ou a pergunta falha inteira, ou responde com qualidade um pouco pior | As duas opções são defensáveis e precisam ser ESCOLHIDAS. Falhar aberto — usar a ordem que a fusão já produziu — mantém o atendimento de pé com qualidade degradada, e é quase sempre o certo aqui; falhar fechado esconde a degradação atrás de um erro. O que não é aceitável é falhar aberto em silêncio: se o reordenador está fora, o painel precisa dizer, senão o time passa semanas atribuindo a piora do acerto a outra coisa |
| Poucos candidatos entrando na reordenação | Baixar o número de candidatos recuperados para 5 e reordenar esses 5 | A latência melhora, o custo de reordenação cai, e o acerto quase não muda nas perguntas fáceis | É o limite estrutural desta arquitetura, e vale escrever na parede: **o reordenador só reordena o que a recuperação trouxe.** O caso "EL-4471-B" que originou este laboratório tinha o trecho certo na posição 8 e na posição 11 — recuperar 5 candidatos torna aquele caso impossível de resolver, por mais bom que seja o reordenador. O número de candidatos é a alavanca que decide o TETO de qualidade; a reordenação só decide o quanto desse teto você alcança |
| Termo do domínio virando ruído no BM25 | Perguntar usando uma palavra que aparece em quase todo documento do acervo — "Cadência", por exemplo | A metade lexical da busca devolve praticamente qualquer coisa, e a fusão promove documentos irrelevantes que a busca vetorial tinha descartado | BM25 pondera por raridade no acervo, e num acervo de domínio único as palavras do domínio são as menos raras de todas. Híbrido não é sempre melhor que vetorial: é melhor quando o termo exato importa — código de produto, número de norma, SKU. A fusão por posição em vez de por score absoluto já protege bastante, e o resto é higiene de termos do domínio |
A terceira injeção reordena a lista de prioridades
O instinto ao ver acerto insuficiente é trocar o reordenador por um melhor. A terceira falha mostra que, quase sempre, aumentar o número de candidatos recuperados dá mais resultado e custa menos esforço — porque nenhum reordenador conserta a ausência. Meça a posição do trecho certo ANTES do corte, que é justamente o instrumento que este laboratório introduziu, e deixe o número de candidatos ser consequência dessa medida.
Investigando os 3 erros residuais do L83 (92,5% de acerto), o time descobre que o chunk correto ESTAVA no índice inteiro — chunking não era o problema — mas não aparecia entre os 5 mais próximos de uma busca puramente vetorial, quando a pergunta citava um código de produto específico. Qual é a causa estrutural desse tipo de erro?
Observabilidade: as perguntas que o painel tem de responder
O painel do L83 media se a resposta tinha citação. Este precisa medir uma coisa mais difícil e mais útil: ONDE o trecho certo estava antes de qualquer corte. É a métrica que transforma "a resposta melhorou" em diagnóstico.
- Qual a posição média do trecho correto no ranking, medida ANTES do corte de candidatos? É o instrumento central deste laboratório — sem ele, não dá para distinguir "o reordenador é ruim" de "o trecho nunca chegou".
- Em que proporção das perguntas a reordenação MUDA o primeiro colocado? Perto de zero significa que ela não está agregando e está sendo paga à toa; muito alto sugere que a fusão está entregando candidatos ruins.
- Dos trechos que sobrevivem ao corte final, quantos vieram da busca lexical e quantos da vetorial? A resposta diz para que tipo de pergunta o híbrido está realmente servindo.
- Quanto a reordenação acrescenta à latência ponta a ponta, em p50 e em p99?
- Qual a saúde do domínio provisionado — pressão de memória, estado dos shards, distribuição entre zonas? Diferente da base gerenciada do L83, aqui existe um cluster com estado de saúde próprio.
| Alarme | Limiar inicial | O que ele pega |
|---|---|---|
| PosicaoMediaDoTrechoCerto | acima da linha de base medida na implantação | degradação da recuperação conforme o acervo cresce — antes de o acerto cair |
| ReordenacaoSemEfeito | abaixo de 10% de troca do primeiro colocado na semana | a reordenação virou custo sem benefício, ou a fusão já está boa o bastante |
| ReordenadorIndisponivel | qualquer falha de invocação | a segunda injeção — degradação silenciosa quando o código falha aberto |
| ClusterAmarelo | qualquer shard não alocado | perda de réplica; com zone awareness, costuma ser sinal de perda de zona |
| LatenciaTotalP99 | acima do orçamento de latência do canal que consome | a reordenação empurrando a cauda para fora do prazo — crítico se este RAG servir o canal de voz do L91 |
Escala: 40 documentos, 40 mil, e a perda de uma zona
| Ordem de grandeza | O que muda no desenho | O que NÃO muda |
|---|---|---|
| O acervo de hoje, 40 perguntas de golden set | Nada. O domínio menor absorve, e a reordenação sobre poucos candidatos é barata | O piso: instância ligada 24 horas cobra igual com uma ou com mil perguntas |
| 400 documentos, tráfego do atendimento | O número de candidatos precisa subir para manter o teto de qualidade, e isso multiplica o custo de reordenação diretamente. Passa a valer reordenar só quando a fusão estiver indecisa — quando os primeiros colocados têm scores próximos | A estrutura do pipeline. Fundir por posição continua sendo a escolha certa, independentemente de escala |
| 40 mil documentos | O dimensionamento de shard passa a importar de verdade, e a escolha de instância deixa de ser detalhe. A reordenação sobre muitos candidatos vira o termo dominante de latência, e o desenho passa a pedir dois estágios: um reordenador barato afunilando, o caro decidindo o topo | O limite da terceira injeção de falha: o reordenador continua sem poder consertar o que a recuperação não trouxe, em qualquer escala |
| Perda de uma zona de disponibilidade | Com zone awareness e réplica, o domínio continua servindo com capacidade reduzida. É exatamente isto que a troca do Serverless pelo provisionado comprou, e é a hora de verificar se a réplica realmente existe | A reordenação, que é um serviço gerenciado regional e não tem noção de zona |
| Perda da região | O domínio e o índice moram numa região. Reconstruir exige reindexar o acervo inteiro em outra, e o tempo disso é o RTO real | Vale a mesma observação do L83, agora mais cara: reindexar um domínio provisionado é mais lento que reingerir uma base gerenciada |
Custo: o piso que a mudança de backend comprou, e o multiplicador escondido
Este laboratório troca um serviço sem servidor por um cluster provisionado e acrescenta uma chamada de modelo por pergunta. As duas decisões mudam a forma da conta, não só o valor — e a segunda tem um multiplicador que passa despercebido.
| Cenário | O que domina | O que ninguém nota |
|---|---|---|
| Piloto — o acervo atual, tráfego de teste | A instância ligada, com folga. A reordenação some diante dela | Nesta escala o desenho do L83 é mais barato E quase tão bom. A justificativa do provisionado aqui é o caso "EL-4471-B", não a economia — e vale dizer isso em voz alta antes que alguém peça o retorno financeiro |
| Produção — 1.200 perguntas por dia, 20 candidatos | A instância ainda domina; a reordenação passa a ser visível na conta | Reordenação tem custo proporcional ao uso e a instância não. Conforme o tráfego cresce, a fatia variável cresce e a fixa se dilui — a métrica que importa é custo por pergunta, e ela MELHORA com volume neste desenho |
| Alta escala — 50 candidatos por pergunta | A reordenação ultrapassa a instância e vira a maior linha | É o ponto em que o desenho de dois estágios se paga: afunilar com um reordenador barato e reservar o caro para os poucos finalistas. Antes desse ponto, dois estágios é complexidade sem retorno |
A comparação honesta
Preço de instância, de reordenação por consulta e por 1.000 tokens muda por região e por modelo. Use os valores abaixo como ordem de grandeza para dimensionar; confirme o vigente na página de preços antes de levar a proposta. Compare este desenho com o do L83 pelo custo por pergunta CORRETA, não por pergunta — é a única forma de o ganho de acerto aparecer no denominador. Uma resposta errada custa barato de produzir e caro de consertar, e nenhuma planilha de infraestrutura registra isso.
Well-Architected nos seis pilares
| Pilar | Situação hoje | Risco | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | O pipeline de fusão é criado por chamada de API, fora do Terraform | Recurso vivo que o estado da infraestrutura não conhece: recriar o domínio perde o pipeline, e a busca volta a ser vetorial pura em silêncio — a primeira injeção de falha | Registrar a criação do pipeline como passo versionado do provisionamento e conferir a existência dele numa verificação pós-implantação | Alta |
| Segurança | Papel escopado ao domínio e ao reordenador; acervo interno em bucket privado | O domínio provisionado tem superfície de rede própria, diferente do serviço gerenciado que ele substituiu | Domínio em sub-rede privada com política de acesso restrita à aplicação, sem endpoint público | Alta |
| Confiabilidade | Zone awareness com réplica — a razão declarada da troca de backend | Zone awareness configurado e réplica ausente é uma combinação que passa despercebida até a zona cair | Alarme de shard não alocado, e o experimento de queda de zona do L57 apontado para este domínio | Alta |
| Eficiência de desempenho | Reordenação sobre um número fixo de candidatos, para toda pergunta | Paga-se reordenação cara em perguntas que a fusão já resolveu com folga | Reordenar condicionalmente, quando os primeiros scores estiverem próximos; medir contra o golden set antes de adotar | Média |
| Otimização de custo | Cluster ligado 24 horas para tráfego de horário comercial | Capacidade ociosa fora do expediente, e o número de candidatos definido sem medir a relação custo-benefício | Dimensionar o número de candidatos pela curva de acerto medida, não por valor redondo | Média |
| Sustentabilidade | Nós provisionados permanentes | Recurso alocado sem uso durante a maior parte do dia | Reavaliar contra os backends medidos no L84 quando o padrão de tráfego se estabilizar | Baixa |
Onde mais IA entra nesta recuperação, e onde ela não paga
A reordenação já é um modelo dentro do caminho de busca. As três ideias abaixo são as que o time propõe em seguida, e as respostas são diferentes entre si.
| Ideia | Vale a pena? | Por quê |
|---|---|---|
| Reescrever a pergunta antes de buscar | Sim, com medição | Resolve um problema real e diferente do que a reordenação resolve: a pergunta do cliente frequentemente não usa as palavras do documento. Expandir "quanto tempo de garantia" para incluir "prazo", "vigência" e o código do produto ajuda o lado lexical do híbrido justamente onde ele é forte. O risco é expandir para o lado errado e piorar — por isso entra com o mesmo golden set e uma comparação lado a lado, nunca por convicção |
| Treinar um reordenador próprio com os dados da Cadência | Não nesta escala | Um reordenador treinado no domínio bate o genérico quando há dezenas de milhares de exemplos de relevância rotulados. A Cadência tem 40 perguntas de golden set. O caminho até lá começa por REGISTRAR o sinal — qual trecho o atendente usou de fato — e esse registro é barato de começar hoje, anos antes de valer treinar qualquer coisa |
| Gerar um documento hipotético e buscar por ele | Interessante, e provavelmente redundante aqui | A técnica ataca o mesmo desalinhamento de vocabulário que a reescrita de pergunta ataca, e custa uma geração completa por consulta em vez de uma expansão barata. Se a reescrita já resolver, isto é dinheiro a mais pelo mesmo ganho — meça a reescrita primeiro |
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Sintoma em produção | Forma correta |
|---|---|---|---|
| Só busca vetorial, porque parece mais moderno que BM25 | embedding e IA generativa dominam a conversa técnica atual, e BM25 soa como tecnologia antiga de busca por palavra-chave — abandoná-la parece progresso | pergunta com código de produto, número de processo ou nome próprio erra sistematicamente, mesmo com o RAG funcionando bem no resto | tratar léxico e semântico como COMPLEMENTARES, não como um substituindo o outro — cada um cobre o ponto cego do outro |
| Somar score bruto de BM25 com score de k-NN | parece a forma mais direta de "combinar dois números" que já se tem em mãos | a fusão vira refém de qual motor produz números maiores por acidente de escala, não de qual é mais relevante | RRF — fusão por posição no ranking, imune à escala de cada motor |
| Rerankear TODOS os candidatos, sem limitar a um top-N pequeno | parece mais seguro rerankear mais candidatos, "para não perder nenhum bom" | latência e custo do cross-encoder crescem linearmente e o orçamento de tempo real estoura | cross-encoder só sobre o top-20/30 que a fusão já filtrou, nunca o índice inteiro |
| Trocar reranking por prompt engineering ("ordene os trechos por relevância") | parece mais simples pedir pro próprio LLM ordenar do que integrar um serviço novo | o modelo de geração ordena de forma inconsistente entre chamadas, e todos os chunks entram no prompt de qualquer forma, gastando token igual | cross-encoder dedicado pontua o par pergunta+trecho de forma determinística e mais barata que usar o LLM de geração pra isso |
| Medir só a taxa de acerto agregada, sem segmentar por tipo de pergunta | um número só é mais simples de reportar num painel | a melhoria de recuperação em perguntas com identificador exato fica escondida dentro de uma média que quase não se moveu no total | segmentar o golden set por tipo de falha (com/sem identificador exato) mede o efeito real da mudança |
Evolução em níveis: da busca só vetorial à plataforma de recuperação
A terceira arquitetura não é mais um desenho — é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco medido neste laboratório e compra outro no lugar.
Busca só vetorial, herdada do L83/L84. Responde 92,5% do golden set, mas erra sistematicamente perguntas com identificador exato.Busca híbrida (BM25 + k-NN) com fusão RRF e reranking por cross-encoder, sobre OpenSearch Service Multi-AZ.Guardrails como controle real de tópico e conteúdo (L86), e agente com ferramenta quando a resposta exige AGIR, não só responder (L87).O golden set vira suíte de CI, segmentada por tipo de falha (recuperação vs geração), com LLM-como-juiz complementando a checagem manual (L88).A mesma recuperação híbrida atendendo voz em tempo real (L91) e respeitando residência de dado multi-região (L99), com orçamento de latência bem menor que o chat.A mesma técnica de recuperação híbrida e reranking decide o ranking de BUSCA DE PRODUTO (L94), não só de atendimento — geração passa a só apresentar, a recuperação é quem decide, e dados de conversão substituem o golden set como métrica.Por que este laboratório vem depois do L83
A ordem não é negociável: busca híbrida e reranking (nível 2) pressupõem que existe um golden set medido e uma pipeline funcionando — é exatamente o que o L83 deixou pronto. Quem tenta rerank antes de ter RAG básico funcionando está otimizando um número que ainda não mediu.
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Log ou métrica | Correção |
|---|---|---|---|---|
| O acerto voltou ao número do L83 | O pipeline de fusão não está sendo aplicado — a busca é vetorial pura de novo | Consultar a definição do pipeline no domínio e conferir se a requisição realmente o referencia | Proporção de trechos finais vindos da busca lexical: perto de zero é a assinatura | Recriar o pipeline e transformar a existência dele em verificação de implantação, não em suposição |
| Código de produto exato continua não sendo encontrado | O campo do código está sendo analisado como texto comum, e o termo se fragmenta antes de chegar ao índice | Consultar diretamente pelo código no índice lexical, sem passar pela fusão | Retorno da consulta lexical isolada | Mapear o campo do código como termo exato. É a metade do problema que a busca híbrida existe para resolver, e ela não resolve nada se o campo estiver mal mapeado |
| A reordenação não muda quase nada | Os candidatos que chegam já estão bem ordenados, ou chegam poucos demais | Comparar a posição do trecho certo antes e depois da reordenação, na mesma consulta | Proporção de perguntas em que o primeiro colocado muda | Se os candidatos já estão bons, a reordenação é custo — reordene condicionalmente. Se são poucos, aumente o número de candidatos: é a terceira injeção de falha |
| A latência p99 estourou o prazo do canal | A reordenação acrescenta um salto de rede e um modelo ao caminho crítico | Separar no traço o tempo de recuperação, de reordenação e de geração | Latência das três etapas em séries separadas | Reduzir candidatos, reordenar condicionalmente, ou aceitar que este RAG não serve canal com prazo apertado sem cache — a decisão do L91 |
| O domínio ficou amarelo depois de uma implantação | Réplica não alocada, frequentemente por não haver zona disponível para ela | Verificar a alocação dos shards e a distribuição entre zonas | Estado do cluster e contagem de shards não alocados | Conferir que o número de réplicas é compatível com o número de zonas configuradas. Cluster amarelo responde normalmente, e é por isso que ele sobrevive semanas sem ninguém notar |
Limpeza: o que o destroy não leva
# limpeza.sh — ordem importa: o search pipeline referencia o dominio, e o
# dominio provisionado com instancia dedicada de master NAO e gratuito parado.
# 1) Apagar o search pipeline antes do dominio
DOMINIO="https://$(terraform output -raw opensearch_endpoint)"
curl -s -X DELETE "$DOMINIO/_search/pipeline/hibrido-rrf" --aws-sigv4 "aws:amz:us-east-1:es"
# 2) terraform destroy cuida do dominio, das roles do IAM, do security group e do
# grupo de logs — mas confira o item 4 antes de fechar o navegador
terraform destroy -auto-approve
# 3) Confirma que o dominio nao existe mais
aws opensearch describe-domain --domain-name cadencia-recuperacao-hibrida 2>&1 | grep -i "not found"
# 4) O secret do Secrets Manager tem retencao de 7-30 dias por padrao mesmo apos
# "excluido" — force-delete se nao precisar de janela de recuperacao
aws secretsmanager delete-secret \
--secret-id cadencia/opensearch-hibrido-master \
--force-delete-without-recovery
O que continua cobrando depois de "destruir"
OpenSearch Service provisionado com dedicated master (3 instâncias `r6g.medium.search` sempre ligadas, fora das 4 instâncias de dado) é o item que mais cobra parado deste laboratório — diferente do Serverless do L83/L84, aqui não existe "capacidade mínima quando ocioso": a instância cobra hora ligada independente de tráfego. Confira o passo 3 antes de considerar o laboratório encerrado, e o secret do passo 4 antes de assumir que ele já parou de existir — o padrão do Secrets Manager é reter por dias, não excluir na hora.
Resumo: problema, peça e motivo
| Problema | Peça | Motivo |
|---|---|---|
| Chunk certo existe mas não aparece no top-k vetorial | Busca híbrida (BM25 + k-NN) | termo exato pesa na busca léxica, sentido pesa na semântica — juntas cobrem o ponto cego uma da outra |
| Score de motores diferentes não é comparável | RRF (fusão por posição) | ranking, não valor bruto de score, decide a ordem final |
| Top-N fundido ainda pode estar em ordem imprecisa | Bedrock Rerank (cross-encoder) | pontua o PAR pergunta+trecho junto, mais preciso que embeddings comparados separadamente |
| Índice de busca não pode cair com a perda de 1 zona | OpenSearch Service Multi-AZ (zone awareness) | domínio sobrevive à perda de uma AZ, ao custo de instância sempre ligada |
| Ganho precisa ser atribuível só à recuperação | LLM e prompt de geração inalterados | mesma pergunta, mesmo modelo — só a pipeline de retrieval muda |
| "Melhorou" sem prova segmentada | Golden set do L83, com diagnóstico de posição no ranking | mostra o efeito onde ele realmente acontece, não escondido numa média agregada |
Perguntas frequentes
❓ Por que a Knowledge Base do L83 não resolveu isso sozinha, se ela já usa embedding?
❓ RRF e reranking resolvem o mesmo problema — por que usar os dois?
❓ O Bedrock Rerank substitui o modelo de embedding usado na Knowledge Base?
❓ Por que não aumentar k na busca vetorial em vez de somar BM25?
❓ Este laboratório muda o prompt ou o modelo de geração da Cadência?
❓ A Knowledge Base já não tem busca híbrida gerenciada pronta?
❓ O custo do reranking compensa, ou é caro demais para aplicar em toda pergunta?
❓ Como saber se a busca híbrida realmente ajudou, e não só reordenou sem importância?
Fixando
Por que a fusão de busca híbrida usa Reciprocal Rank Fusion (posição no ranking) em vez de simplesmente somar o score do BM25 com o score do k-NN?
Depois da fusão RRF, o Bedrock Rerank (cross-encoder) reordena o top-20 fundido antes do prompt final. Se a fusão RRF já combina dois sinais de relevância diferentes, por que ainda é preciso um cross-encoder para reordenar?
Próximo laboratório
Próximo passo: L94 (busca de produto com IA em escala)
Este laboratório atacou a recuperação de UMA aplicação: o atendimento da Cadência, com um golden set de 40 perguntas e um orçamento de latência de segundos. O L94 pega a MESMA técnica — busca híbrida, RRF, reranking por cross-encoder — e aplica em BUSCA DE PRODUTO: milhões de consultas por dia, onde a geração deixa de decidir e passa só a apresentar, e o ganho não se mede em taxa de acerto de golden set, se mede em conversão, com teste A/B. A engenharia de recuperação é a mesma que você acabou de construir; o que muda é a escala e o que está em jogo quando ela erra.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…