Aula 4, Busca semântica e montagem de contexto

Esta aula junta as peças do RAG em um fluxo de resposta completo. Vamos recuperar os trechos relevantes, montar com cuidado o contexto que vai ao modelo, e gerar a resposta citando as fontes. É aqui que o assistente começa a responder de verdade.

Já temos os embeddings e o banco vetorial. Falta a parte que transforma a busca em uma resposta útil, e ela é mais sutil do que parece. Recuperar os trechos certos é metade do trabalho. A outra metade é montar o contexto de um jeito que o modelo use bem, respeitando o limite de tamanho, deixando claro o que é fonte e o que é pergunta, e instruindo o modelo a responder apenas com base no que foi fornecido.

Uma montagem descuidada estraga uma boa busca. Se o contexto é confuso, ou se o prompt não deixa claro que o modelo deve se ater às fontes, ele pode ignorar os trechos e voltar a inventar. Nesta aula você vai aprender a montar o contexto com cuidado, a controlar o tamanho, a pedir respostas fundamentadas com citação das fontes, e a lidar com o caso em que nada relevante é encontrado.


Objetivos

Ao final desta aula, você deve ser capaz de:

  • Recuperar os top-k trechos e montar um contexto bem estruturado.
  • Instruir o modelo a responder apenas com base no contexto.
  • Pedir respostas com citação das fontes recuperadas.
  • Tratar o caso em que nenhum trecho relevante é encontrado.

Teoria

A montagem do contexto é a ponte entre a busca e a geração. Recuperados os top-k trechos, nós os organizamos no prompt, em geral numerados ou marcados como fontes, seguidos da pergunta e de uma instrução clara. A instrução é decisiva. Pedir responda apenas com base no contexto e, se a resposta não estiver no contexto, diga que não sabe reduz drasticamente as alucinações, porque orienta o modelo a não extrapolar.

Três cuidados melhoram muito o resultado. O primeiro é o limite de tamanho, pois o contexto mais a resposta precisam caber na janela do modelo, então selecionamos só os trechos mais relevantes. O segundo é a citação das fontes, pedindo ao modelo que indique de qual trecho tirou cada afirmação, o que torna a resposta verificável. O terceiro é o tratamento da ausência, decidir o que fazer quando a busca não encontra nada suficientemente relevante, em vez de forçar uma resposta.

flowchart LR
    Q[Pergunta] --> R[Recupera top-k trechos]
    R --> M{Trechos relevantes?}
    M -- sim --> C[Monta contexto + instrução]
    C --> L[LLM responde com citação]
    M -- não --> N[Responde que não sabe]

Repare que tudo isso é prompt engineering aplicado, do Módulo 8. A diferença é que parte do prompt agora vem da busca, e não de você. O RAG é, em essência, prompt engineering com um contexto recuperado dinamicamente.

Explicação Intuitiva

Pense em um assistente de pesquisa que você instrui assim, aqui estão três trechos do material, responda à minha pergunta usando apenas eles, e diga de qual trecho tirou cada informação. Se ele seguir a instrução, você ganha uma resposta confiável e verificável, porque pode conferir cada afirmação na fonte. Se você não der essa instrução, ele pode misturar o que leu com o que acha que sabe, e aí a confiança cai.

O cuidado com a ausência é o que separa um assistente honesto de um que inventa. Um bom assistente, ao não encontrar nada relevante, diz que não sabe, em vez de chutar. Para a educação, isso é essencial, é melhor o assistente admitir que aquela dúvida não está coberta pelo material do que dar ao aluno uma resposta errada com cara de certa.

Explicação Matemática

A seleção dos trechos é um problema de top-k sobre as similaridades. Calculamos a similaridade do cosseno da pergunta com cada pedaço e escolhemos os $k$ maiores. Um refinamento comum é aplicar um limiar, só incluir trechos com similaridade acima de um valor mínimo, o que ajuda a tratar a ausência, se nenhum trecho passa do limiar, consideramos que a base não cobre a pergunta.

O limite de tamanho transforma a seleção em um pequeno problema de orçamento. Cada trecho ocupa um número de tokens, e há um teto, a janela de contexto menos o espaço reservado para a pergunta e a resposta. Escolhemos os trechos de maior similaridade que cabem nesse orçamento, priorizando relevância. É uma versão simples de seleção sob restrição, sempre favorecendo o mais relevante.

Exemplo Prático

Vamos montar o fluxo completo de resposta sobre o banco vetorial, recuperar os top-k trechos, verificar se algum é relevante o bastante, e montar um prompt que peça uma resposta fundamentada com citação das fontes, ou que reconheça quando a base não cobre a pergunta. Geramos a resposta com o Ollama quando disponível.

A montagem do contexto e o tratamento da ausência são determinísticos e rodam sem o modelo. O código está no notebook notebooks/modulo-09/04-busca-semantica-contexto.ipynb, então abra-o ao lado para acompanhar.

Código Comentado

def montar_contexto(trechos_pontuados, limiar=0.05):
    """Seleciona trechos acima do limiar e os numera como fontes."""
    relevantes = [(s, t) for s, t in trechos_pontuados if s >= limiar]
    if not relevantes:
        return None
    linhas = [f"[{i+1}] {t}" for i, (s, t) in enumerate(relevantes)]
    return "\n".join(linhas)


def montar_prompt(pergunta, contexto):
    if contexto is None:
        # Nada relevante encontrado: instruímos o modelo a admitir.
        return (
            f"Não há material sobre a pergunta a seguir. Responda exatamente: "
            f"'Não encontrei isso no material disponível.'\nPergunta: {pergunta}"
        )
    return (
        "Responda à pergunta usando APENAS o contexto numerado abaixo. "
        "Cite as fontes entre colchetes, como [1]. Se a resposta não estiver no "
        "contexto, diga que não encontrou.\n\n"
        f"Contexto:\n{contexto}\n\n"
        f"Pergunta: {pergunta}\nResposta:"
    )


# Exemplo de trechos já recuperados, com as suas similaridades.
recuperados = [
    (0.39, "A derivada mede a taxa de variação de uma função."),
    (0.21, "A regra da cadeia deriva funções compostas."),
]
contexto = montar_contexto(recuperados)
print(montar_prompt("O que é a derivada?", contexto))

print("\n--- Caso sem material relevante ---")
print(montar_prompt("Qual a capital da Mongólia?", montar_contexto([(0.01, "trecho irrelevante")])))

Ao rodar, o primeiro caso monta um prompt com as duas fontes numeradas e instrui o modelo a responder citando-as, o que produz uma resposta fundamentada e verificável. O segundo caso, em que nada passou do limiar, gera um prompt que orienta o modelo a admitir que não encontrou, evitando a alucinação. Esse tratamento honesto da ausência é o que dá ao assistente a confiança necessária para a educação.

Exercícios

1) Conceitual: Por que a instrução de responder apenas com base no contexto reduz as alucinações? 2) Conceitual: Como o limiar de similaridade ajuda a tratar perguntas que a base não cobre? 3) Prático: Mude o limiar e observe a partir de quando perguntas fora do tema passam a ser recusadas. 4) Prático: Peça ao modelo, via Ollama, uma resposta com citação das fontes e verifique se ela é conferível nos trechos. 5) Extensão: Pesquise técnicas de reordenação dos trechos recuperados, o re-ranking, e explique o que elas melhoram.

Projeto da Aula

Implemente o fluxo de resposta completo do assistente. A entrega é a parte do sistema que recebe uma pergunta, recupera os trechos, monta o contexto com citação e trata a ausência, gerando a resposta com o LLM.

Considere o projeto pronto quando o assistente responder perguntas cobertas pela base citando as fontes, e recusar de forma honesta perguntas fora do material, e quando você escrever um parágrafo sobre como o tratamento da ausência aumenta a confiança do sistema. Com isso, o assistente está quase completo, faltando apenas escalar para produção, tema da próxima aula.

Leituras Recomendadas

  • O artigo de Lewis e colegas sobre RAG, para a fundamentação da geração com recuperação.
  • Guias de RAG com LangChain, sobre montagem de contexto e citação de fontes.
  • Materiais sobre avaliação de RAG, como fidelidade da resposta às fontes.

Referências Científicas

As referências abaixo são reais e estão registradas em references/referencias.bib. As chaves entre parênteses são as do BibTeX.

  • Lewis, P., et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS. (lewis2020rag)
  • Karpukhin, V., et al. (2020). Dense Passage Retrieval for Open-Domain Question Answering. EMNLP. (karpukhin2020dpr)
  • Liu, P., et al. (2023). Pre-train, Prompt, and Predict. ACM Computing Surveys. (liu2023prompt)