Construindo o Ecossistema RAG Completo e Otimizando Cada Componente

Tenho lido os posts e artigos do Fareed e os acho incrivelmente detalhados e bem escritos. Neste artigo originalmente publicado em seu blog, você pode encontrar seu repositório para este post e links para seu outro conteúdo!

Construindo todo o Ecossistema RAG e Otimizando Cada Componente

Roteamento, Indexação, Recuperação, Transformação e muito mais.

Leia esta história gratuitamente: link

A maioria das equipes, ao criar um sistema RAG pronto para produção em seus dados, passa por muitas rodadas de experimentação e depende de vários componentes diferentes, cada um exigindo sua própria configuração, ajuste e manipulação cuidadosa. Esses componentes incluem…

  1. Transformações de Consulta: Reescrita de perguntas do usuário para serem mais eficazes na recuperação.

  2. Roteamento Inteligente: Direcionamento de uma consulta para a fonte de dados correta ou uma ferramenta especializada.

  3. Indexação: Criação de uma base de conhecimento em múltiplas camadas.

  4. Recuperação e Re-ranking: Filtragem de ruído e priorização do contexto mais relevante.

  5. Fluxos Agentic Auto-Corretivos: Construção de sistemas que podem avaliar e melhorar seu próprio trabalho.

  6. Avaliação End-to-End: Medição objetiva do desempenho de todo o pipeline.

e muito mais…

Aprenderemos e codificaremos cada parte do ecossistema RAG junto com visuais para facilitar a compreensão, começando do básico até técnicas avançadas.

Todo o código (Teoria + Notebook) está disponível no meu Repositório GitHub:

GitHub - FareedKhan-dev/rag-ecosystem: Entenda e codifique cada componente importante do RAG…

Meu Índice está dividido em várias seções. Dê uma olhada.

Entendendo o Sistema RAG Básico

Transformações Avançadas de Consulta

Roteamento & Construção de Consulta

Estratégias de Indexação

Recuperação & Geração

Avaliação Manual de RAG

Avaliação com Frameworks

Resumindo Tudo


Entendendo o Sistema RAG Básico

Antes de analisarmos o básico do RAG, precisamos definir as variáveis de ambiente para rastreamento e outras tarefas, como o provedor de API de LLMs que usaremos.

import os

# Defina o endpoint da API do LangChain e a chave da API
os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'
os.environ['LANGCHAIN_API_KEY'] = <your-api-key>
```Substitua pela sua chave de API do LangChain

# Defina a chave de API do OpenAI
os.environ['OPENAI_API_KEY'] =`<your-api-key>`  # Substitua pela sua chave de API do OpenAI

Você pode obter sua chave de API LangSmith na documentação oficial deles para rastrear nosso produto RAG ao longo deste blog. Para o LLM, usaremos a API OpenAI, mas como você já pode saber, LangChain também suporta uma variedade de provedores de LLM.

O pipeline RAG principal é a base de qualquer sistema avançado, e entender seus componentes é importante. Portanto, antes de entrar nos detalhes dos componentes avançados, primeiro precisamos entender a lógica principal de como um sistema RAG funciona, mas você pode pular esta seção se já souber como um sistema RAG funciona.

Este RAG mais simples pode ser dividido em três componentes:

  • Indexação: Organize e armazene dados em um formato estruturado para permitir buscas eficientes.

  • Recuperação: Pesquise e busque dados relevantes com base em uma consulta ou entrada.

  • Geração: Crie uma resposta ou saída final usando os dados recuperados.

Vamos construir este pipeline simples do zero para ver como cada peça funciona.

Fase de Indexação

Antes que nosso sistema RAG possa responder a qualquer pergunta, ele precisa de conhecimento para extrair. Para isso, usaremos um WebBaseLoader para extrair conteúdo diretamente do excelente post do blog de Lilian Weng sobre agentes alimentados por LLM.

import bs4
from langchain_community.document_loaders import WebBaseLoader

# Inicialize um carregador de documentos da web com instruções de análise específicas
loader = WebBaseLoader(
    web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),  # URL do post do blog a carregar
    bs_kwargs=dict(
        parse_only=bs4.SoupStrainer(
            class_=("post-content", "post-title", "post-header")  # Analise apenas as classes HTML especificadas
        )
    ),
)

# Carregue o conteúdo filtrado da página da web em documentos
docs = loader.load()

O argumento bs_kwargs nos ajuda a direcionar apenas as tags HTML relevantes (post-content, post-title, etc.), limpando nossos dados desde o início.

Agora que temos o documento, enfrentamos nosso primeiro desafio. Alimentar um documento massivo diretamente em um LLM é ineficiente e muitas vezes impossível devido aos limites da janela de contexto.

É por isso que chunking é uma etapa crítica. Precisamos quebrar o documento em pedaços menores e semanticamente significativos.

O RecursiveCharacterTextSplitter é a ferramenta recomendada para este trabalho porque tenta inteligentemente manter parágrafos e frases intactos.

from langchain.text_splitter import RecursiveCharacterTextSplitter

# Crie um divisor de texto para dividir o texto em chunks de 1000 caracteres com sobreposição de 200 caracteres
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)

# Divida os documentos carregados em chunks menores
splits = text_splitter.split_documents(docs)

Com chunk_size=1000, estamos criando chunks de 1000 caracteres, e chunk_overlap=200 garante que haja continuidade entre eles, o que ajuda a preservar o contexto.

Nosso texto agora está dividido, mas ainda é apenas texto. Para realizar buscas de similaridade, precisamos converter esses chunks em representações numéricas chamadas embeddings. Então armazenaremos esses embeddings em um vector store, que é um banco de dados especializado projetado para busca eficiente de vetores.

O vector store Chroma e OpenAIEmbeddings tornam isso incrivelmente simples. A linha a seguir lida com embedding e indexação em uma única operação.

from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# Incorpore os chunks de texto e armazene-os em um vector store Chroma para busca de similaridade
vectorstore = Chroma.from_documents(
    documents=splits, 
    embedding=OpenAIEmbeddings()  # Use o modelo de embedding do OpenAI para converter texto em vetores
)

Com nosso conhecimento indexado, agora estamos prontos para começar a fazer perguntas.

Recuperação

O vector store é nossa biblioteca, e o retriever é nosso bibliotecário inteligente. Ele pega a consulta de um usuário, a incorpora e busca os chunks mais semanticamente similares do vector store.

Criar um retriever a partir de nosso vectorstore é uma linha.

# Crie um retriever a partir do vector store
retriever = vectorstore.as_retriever()

Vamos testá-lo. Faremos uma pergunta e veremos o que nosso retriever encontra.

# Recupere documentos relevantes para uma consulta
docs = retriever.get_relevant_documents("What is Task Decomposition?")

# Imprima o conteúdo do primeiro documento recuperado
print(docs[0].page_content)

#### SAÍDA ####
Task decomposition can be done (1) by LLM with simple prompting ...
Tree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple ...

Como você pode ver, o retriever extraiu com sucesso o chunk mais relevante do post do blog que discute diretamente “Task decomposition”. Este pedaço de contexto é exatamente o que o LLM precisa para formar uma resposta precisa.

Geração

Temos nosso contexto, mas precisamos de um LLM para lê-lo e formular uma resposta amigável ao usuário. Esta é a etapa “Geração” em RAG.

Primeiro, precisamos de um bom template de prompt. Isso instrui o LLM sobre como se comportar. Em vez de escrever o nosso próprio, podemos extrair um pré-otimizado do LangChain Hub.

from langchain import hub

# Extraia um prompt RAG pré-feito do LangChain Hub
prompt = hub.pull("rlm/rag-prompt")

# imprimindo o prompt
print(prompt)

#### SAÍDA ####
human
You are an assistant for question-answering tasks. Use the following pieces
of retrieved context to answer the question. If you dont know the answer,
just say that you dont know. Use three sentences maximum and keep the
answer concise.

Question: {question} 
Context: {context} 
Answer:

Em seguida, inicializamos nosso LLM. Usaremos gpt-3.5-turbo.

from langchain_openai import ChatOpenAI

# Inicialize o LLM
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

Agora para a etapa final: encadear tudo junto. Usando a Linguagem de Expressão LangChain (LCEL), podemos canalizar a saída de um componente para a entrada do próximo.

from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# Função auxiliar para formatar documentos recuperados
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# Defina a cadeia RAG completa
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

Vamos dividir esta cadeia:

  1. {"context": retriever | format_docs, "question": RunnablePassthrough()}: Esta parte é executada em paralelo. Ela envia a pergunta do usuário para o retriever para obter documentos, que são então formatados em uma única string por format_docs. Simultaneamente, RunnablePassthrough passa a pergunta original inalterada.

  2. | prompt: O contexto e a pergunta são alimentados em nosso template de prompt.

  3. | llm: O prompt formatado é enviado para o LLM.

  4. | StrOutputParser(): Isso limpa a saída do LLM em uma string simples.

Agora, vamos invocar a cadeia inteira.

# Faça uma pergunta usando a cadeia RAG
response = rag_chain.invoke("What is Task Decomposition?")
print(response)

#### OUTPUT ####
Task decomposition is a technique used to break down large tasks
into smaller, more manageable subgoals. This can be achieved by using a
Large Language Model (LLM) with simple prompts, task-specific instructions,
or human inputs. For example, ...

E pronto, nosso pipeline RAG recuperou com sucesso informações relevantes sobre “Task Decomposition” e as utilizou para gerar uma resposta concisa e precisa. Esta cadeia simples forma a base sobre a qual construiremos capacidades mais avançadas e poderosas.

Transformações de Consulta Avançadas

Então, agora que entendemos os fundamentos do pipeline RAG. Mas os sistemas de produção frequentemente revelam as limitações dessa abordagem básica. Um dos pontos de falha mais comuns é a própria consulta do usuário.

Uma consulta pode ser muito específica, muito ampla ou usar vocabulário diferente do nosso documentos de origem, levando a resultados de recuperação pobres.

A solução não é culpar o usuário, é tornar nosso sistema mais inteligente. Transformação de Consulta é um conjunto de técnicas poderosas projetadas para reescrever, expandir ou decompor a pergunta original para melhorar significativamente a precisão da recuperação.

Em vez de confiar em uma única consulta, vamos engenheirar múltiplas consultas mais bem informadas para lançar uma rede mais ampla e precisa.

Para testar essas novas técnicas, usaremos a mesma base de conhecimento indexada da seção do pipeline RAG básico que acabamos de percorrer. Isso garante que possamos comparar diretamente os resultados e ver as melhorias.

Como um rápido lembrete, aqui está como configuramos nosso recuperador:

# Carregue o post do blog
loader = WebBaseLoader(
    web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
    bs_kwargs=dict(
        parse_only=bs4.SoupStrainer(
            class_=("post-content", "post-title", "post-header")
        )
    ),
)
blog_docs = loader.load()

# Divida os documentos em pedaços
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=300, 
    chunk_overlap=50
)
splits = text_splitter.split_documents(blog_docs)

# Indexe os pedaços em um armazenamento de vetores Chroma
vectorstore = Chroma.from_documents(documents=splits, 
                                    embedding=OpenAIEmbeddings())

# Crie nosso recuperador
retriever = vectorstore.as_retriever()

Agora, com nosso recuperador pronto, vamos explorar nossa primeira técnica de transformação de consulta.

Geração de Múltiplas Consultas

Uma única consulta do usuário representa apenas uma perspectiva. A busca por similaridade baseada em distância pode perder documentos relevantes que usam sinônimos ou discutem conceitos relacionados.

A abordagem Multi-Query aborda isso usando um LLM para gerar várias versões diferentes da pergunta do usuário, efetivamente pesquisando de múltiplos ângulos.

Começaremos criando um prompt que instrui o LLM a gerar essas perguntas alternativas.

from langchain.prompts import ChatPromptTemplate

# Prompt para gerar múltiplas consultas
template = """You are an AI language model assistant. Your task is to generate five 
different versions of the given user question to retrieve relevant documents from a vector 
database. By generating multiple perspectives on the user question, your goal is to help
the user overcome some of the limitations of the distance-based similarity search. 
Provide these alternative questions separated by newlines. Original question: {question}"""
prompt_perspectives = ChatPromptTemplate.from_template(template)

# Cadeia para gerar as consultas
generate_queries = (
    prompt_perspectives 
    | ChatOpenAI(temperature=0) 
    | StrOutputParser() 
    | (lambda x: x.split("\n"))
)

Vamos testar essa cadeia e ver que tipo de consultas ela gera para nossa pergunta.

question = "What is task decomposition for LLM agents?"
generated_queries_list = generate_queries.invoke({"question": question})

# Imprima as consultas geradas
for i, q in enumerate(generated_queries_list):
    print(f"{i+1}. {q}")

#### OUTPUT ####
1. How can LLM agents break down complex tasks?
2. What is the process of task decomposition in the context of large language model agents?
3. What are the methods for decomposing tasks for LLM-powered agents?
4. Explain the concept of task decomposition as it applies to AI agents using LLMs.
5. In what ways do LLM agents handle task decomposition?

Isso é excelente. O LLM reformulou nossa pergunta original usando palavras-chave diferentes como “break down complex tasks”, “methods” e “process”. Agora, podemos recuperar documentos para todas essas consultas e combinar os resultados. Uma maneira simples de combiná-los é pegar o conjunto único de todos os documentos recuperados.

from langchain.load import dumps, loads

def get_unique_union(documents: list[list]):
    """ Uma função simples para obter a união única de documentos recuperados """
    # Achate a lista de listas e converta cada Documento em uma string para exclusividade
    flattened_docs = [dumps(doc) for sublist in documents for doc in sublist]
    unique_docs = list(set(flattened_docs))
    return [loads(doc) for doc in unique_docs]

# Construa a cadeia de recuperação
retrieval_chain = generate_queries | retriever.map() | get_unique_union

# Invoque a cadeia e verifique o número de documentos recuperados
docs = retrieval_chain.invoke({"question": question})
print(f"Total unique documents retrieved: {len(docs)}")

#### OUTPUT ####
Total unique documents retrieved: 6

Ao pesquisar com cinco consultas diferentes, recuperamos um total de 6 documentos únicos, provavelmente capturando um conjunto mais abrangente de informações do que uma única consulta teria. Agora podemos alimentar esse contexto em nossa cadeia RAG final.

from operator import itemgetter

# A cadeia RAG final
template = """Answer the following question based on this context:

{context}

Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
llm = ChatOpenAI(temperature=0)

final_rag_chain = (
    {"context": retrieval_chain, "question": itemgetter("question")} 
    | prompt
    | llm
    | StrOutputParser()
)

final_rag_chain.invoke({"question": question})

#### OUTPUT ####
Task decomposition for LLM agents involves breaking down large,
complex tasks into smaller, more manageable sub-goals. This allows
the agent to work through a problem systematically. Methods for
decomposition include using the LLM itself with simple prompts ...

Esta resposta é mais robusta porque é baseada em um conjunto mais amplo de documentos relevantes.

RAG-Fusion

Multi-Query é um ótimo começo, mas simplesmente pegar uma união de documentos os trata igualmente. E se um documento foi classificado altamente por três de nossas consultas, enquanto outro era um resultado de baixa classificação de apenas uma?

O primeiro é claramente mais importante. RAG-Fusion melhora o Multi-Query não apenas buscando documentos, mas também…

re-classificando eles usando uma técnica chamada Reciprocal Rank Fusion (RRF).

RRF combina inteligentemente resultados de múltiplas buscas. Ele aumenta a pontuação de documentos que aparecem consistentemente altos em diferentes listas de resultados, empurrando o conteúdo mais relevante para o topo.

O código é muito semelhante, mas vamos trocar nossa função get_unique_union por uma implementação de RRF.

def reciprocal_rank_fusion(results: list[list], k=60):
    """Fusão de Rank Recíproco que combina inteligentemente múltiplas listas classificadas"""
    fused_scores = {}

    # Iterar através de cada lista de documentos classificados
    for docs in results:
        for rank, doc in enumerate(docs):
            doc_str = dumps(doc)
            if doc_str not in fused_scores:
                fused_scores[doc_str] = 0
            # O núcleo do RRF: documentos classificados mais altos (valor de rank menor) recebem uma pontuação maior
            fused_scores[doc_str] += 1 / (rank + k)

    # Classificar documentos por suas novas pontuações fundidas em ordem decrescente
    reranked_results = [
        (loads(doc), score)
        for doc, score in sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
    ]
    return reranked_results
```

A função acima irá reclassificar os documentos após serem obtidos através da busca por similaridade, mas ainda não a inicializamos, então vamos fazer isso agora.

```python
# Use um prompt ligeiramente diferente para RAG-Fusion
template = """Você é um assistente útil que gera múltiplas consultas de busca baseadas em uma única consulta de entrada. \n\nGere múltiplas consultas de busca relacionadas a: {question} \n\nSaída (4 consultas):"""
prompt_rag_fusion = ChatPromptTemplate.from_template(template)

generate_queries = (
    prompt_rag_fusion 
    | ChatOpenAI(temperature=0)
    | StrOutputParser() 
    | (lambda x: x.split("\n"))
)

# Construir a nova cadeia de recuperação com RRF
retrieval_chain_rag_fusion = generate_queries | retriever.map() | reciprocal_rank_fusion
docs = retrieval_chain_rag_fusion.invoke({"question": question})

print(f"Total de documentos reclassificados recuperados: {len(docs)}")

#### SAÍDA ####
Total de documentos reclassificados recuperados: 7
```

A cadeia final permanece a mesma, mas agora recebe um contexto classificado de forma mais inteligente. RAG-Fusion é uma maneira poderosa e de baixo esforço para aumentar a qualidade da sua recuperação.

### Decomposição

Algumas perguntas são muito complexas para serem respondidas em uma única etapa. Por exemplo, **"Quais são os componentes principais de um agente alimentado por LLM e como eles interagem?"** Isso é realmente duas perguntas em uma.

![Responder Recursivamente (Criado por [Fareed Khan](None))](https://miro.medium.com/1*oYttQUN_G0J_TZtigWjsGQ.png)

![Responder Recursivamente (Criado por [Fareed Khan](None))](https://miro.medium.com/1*bVJzw49ji0Qsd-wF0KR6gg.png)

A técnica de Decomposição usa um LLM para dividir uma consulta complexa em um conjunto de sub-perguntas mais simples e autossuficientes. Podemos então responder cada uma e sintetizar uma resposta final.

Começaremos com um prompt projetado para esse propósito.

```python
# Prompt de decomposição
template = """Você é um assistente útil que gera múltiplas sub-perguntas relacionadas a uma pergunta de entrada. \n\nO objetivo é dividir a entrada em um conjunto de sub-problemas / sub-perguntas que podem ser respondidas isoladamente. \n\nGere múltiplas consultas de busca relacionadas a: {question} \n\nSaída (3 consultas):"""
prompt_decomposition = ChatPromptTemplate.from_template(template)

# Cadeia para gerar sub-perguntas
generate_queries_decomposition = (
    prompt_decomposition 
    | ChatOpenAI(temperature=0) 
    | StrOutputParser() 
    | (lambda x: x.split("\n"))
)

# Gerar e imprimir as sub-perguntas
question = "Quais são os componentes principais de um sistema de agente autônomo alimentado por LLM?"
sub_questions = generate_queries_decomposition.invoke({"question": question})
print(sub_questions)

#### SAÍDA ####
[
 '1. Quais são os componentes principais ... agente?',
 '2. Como a memória é implementada em agentes alimentados por LLM?',
 '3. Qual é o papel do planejamento e decomposição de tarefas ... LLMs?'
]
```

O LLM decompôs com sucesso nossa pergunta complexa. Agora, podemos responder cada uma individualmente e combinar os resultados. Um método eficaz é responder cada sub-pergunta e usar os pares de Q&A resultantes como contexto para sintetizar uma resposta final e abrangente.

```python
# Prompt RAG
prompt_rag = hub.pull("rlm/rag-prompt")

# Uma lista para manter as respostas às nossas sub-perguntas
rag_results = []
for sub_question in sub_questions:
    # Recuperar documentos para cada sub-pergunta
    retrieved_docs = retriever.get_relevant_documents(sub_question)
    
    # Usar nossa cadeia RAG padrão para responder a sub-pergunta
    answer = (prompt_rag | llm | StrOutputParser()).invoke({"context": retrieved_docs, "question": sub_question})
    rag_results.append(answer)

def format_qa_pairs(questions, answers):
    """Formatar pares de P e R"""
    formatted_string = ""
    for i, (question, answer) in enumerate(zip(questions, answers), start=1):
        formatted_string += f"Pergunta {i}: {question}\nResposta {i}: {answer}\n\n"
    return formatted_string.strip()

# Formatar os pares de P&R em uma única string de contexto
context = format_qa_pairs(sub_questions, rag_results)

# Prompt de síntese final
template = """Aqui está um conjunto de pares de P+R:

{context}

Use estes para sintetizar uma resposta à pergunta original: {question}
"""
prompt = ChatPromptTemplate.from_template(template)

final_rag_chain = (
    prompt
    | llm
    | StrOutputParser()
)

final_rag_chain.invoke({"context": context, "question": question})

#### SAÍDA ####
Um sistema de agente autônomo alimentado por LLM consiste principalmente em três
componentes principais: planejamento, memória e uso de ferramentas. ...
```

Ao dividir o problema, construímos uma resposta muito mais detalhada e estruturada do que teríamos conseguido de outra forma.

### Prompting de Passo Atrás

Às vezes, a consulta de um usuário é muito específica, enquanto nossos documentos contêm as informações mais gerais e subjacentes necessárias para respondê-la.

![Prompting de Passo Atrás (Criado por [Fareed Khan](None))](https://miro.medium.com/1*6lrhGv1fdcmLKMVu5tU3uQ.png)

> Por exemplo, um usuário pode perguntar: "Os membros do The Police poderiam realizar prisões legais?"

Uma busca direta por isso pode falhar. A técnica de Passo Atrás usa um LLM para dar um "passo atrás" e formular uma pergunta mais geral, como "Quais são os poderes e deveres da banda The Police?" Recuperamos então contexto tanto para as perguntas específicas quanto gerais, fornecendo um contexto mais rico para a resposta final.

Podemos ensinar ao LLM esse padrão usando exemplos de poucos disparos.

```python
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate

# Exemplos de poucos disparos para ensinar ao modelo como gerar perguntas de passo atrás (mais genéricas)
examples = [
    {
        "input": "Os membros do The Police poderiam realizar prisões legais?",
        "output": "o que os membros do The Police podem fazer?",
    },
    {
        "input": "Jan Sindel nasceu em qual país?",
        "output": "qual é o histórico pessoal de Jan Sindel?",
    },
]

# Definir como cada exemplo é formatado no prompt
example_prompt = ChatPromptTemplate.from_messages([
    ("human", "{input}"),  # Entrada do usuário
    ("ai", "{output}")     # Resposta do modelo
])

# Envolver os exemplos de poucos disparos em um modelo de prompt reutilizável
few_shot_prompt = FewShotChatMessagePromptTemplate(
    example_prompt=example_prompt,
    examples=examples,
)

# O prompt completo inclui instrução do sistema, exemplos de poucos disparos e a pergunta do usuário
prompt = ChatPromptTemplate.from_messages([
    ("system", 
     "Você é um especialista em conhecimento mundial. Sua tarefa é dar um passo atrás e reformular uma pergunta "
     "para uma pergunta de passo atrás mais genérica, que é mais fácil de responder. Aqui estão alguns exemplos:"),
    few_shot_prompt,
    ("user", "{question}"),
])
```

Agora, podemos simplesmente definir a cadeia para a abordagem de passo atrás, então vamos fazer isso.

```makefile
# Define a chain to generate step-back questions using the prompt and an OpenAI model
generate_queries_step_back = prompt | ChatOpenAI(temperature=0) | StrOutputParser()

# Run the chain on a specific question
question = "What is task decomposition for LLM agents?"
step_back_question = generate_queries_step_back.invoke({"question": question})

# Output the original and generated step-back question
print(f"Original Question: {question}")
print(f"Step-Back Question: {step_back_question}")

#### OUTPUT ####
Original Question: What is task decomposition for LLM agents?
Step-Back Question: What are the different approaches to task decomposition 
                    in software engineering?
```

Esta é uma pergunta de recuo importante. Ela amplia o escopo para engenharia de software geral, o que provavelmente trará documentos fundamentais que podem ser combinados com o contexto específico sobre agentes LLM. Agora podemos construir uma cadeia que usa ambos.

```python
from langchain_core.runnables import RunnableLambda

# Prompt para a resposta final
response_prompt_template = """You are an expert of world knowledge. I am going to ask you a question. Your response should be comprehensive and not contradicted with the following context if they are relevant. Otherwise, ignore them if they are not relevant.

# Normal Context
{normal_context}

# Step-Back Context
{step_back_context}

# Original Question: {question}
# Answer:"""
response_prompt = ChatPromptTemplate.from_template(response_prompt_template)

# A cadeia completa
chain = (
    {
        # Recuperar contexto usando a pergunta normal
        "normal_context": RunnableLambda(lambda x: x["question"]) | retriever,
        # Recuperar contexto usando a pergunta de recuo
        "step_back_context": generate_queries_step_back | retriever,
        # Passar a pergunta original
        "question": lambda x: x["question"],
    }
    | response_prompt
    | ChatOpenAI(temperature=0)
    | StrOutputParser()
)

chain.invoke({"question": question})
```

Esta é a saída que obtemos ao executar esta cadeia de prompt de recuo com nossa consulta.

```python
####### OUTPUT OF STEP BACK ###########
Task decomposition is a fundamental concept in software engineering
where a complex problem is broken down into smaller, more manageable parts.
In the context of LLM agents, this principle is applied to enable them to 
handle large tasks. By decomposing a task into sub-goa ....
```

### HyDE

Esta técnica final é uma das mais inteligentes. O problema central da recuperação é que a consulta de um usuário pode usar palavras diferentes do documento (o problema de "incompatibilidade de vocabulário").

![HyDE (Criado por [Fareed Khan](None))](https://miro.medium.com/1*YQVJMOpDBU6l54atHoFpJg.png)

**HyDE (Hypothetical Document Embeddings)** propõe uma solução radical: Primeiro, fazer um LLM gerar uma resposta _hipotética_ para a pergunta. Este documento falso, embora não seja factualmente correto, será semanticamente rico e usará o tipo de linguagem que esperamos encontrar em uma resposta real.

Em seguida, incorporamos este documento hipotético e usamos sua incorporação para realizar a recuperação. O resultado é que encontramos documentos reais que são semanticamente muito semelhantes a uma resposta ideal.

Vamos começar criando um prompt para gerar este documento hipotético.

```python
# Prompt HyDE
template = """Please write a scientific paper passage to answer the question
Question: {question}
Passage:"""
prompt_hyde = ChatPromptTemplate.from_template(template)

# Cadeia para gerar o documento hipotético
generate_docs_for_retrieval = (
    prompt_hyde 
    | ChatOpenAI(temperature=0) 
    | StrOutputParser() 
)

# Gerar e imprimir o documento hipotético
hypothetical_document = generate_docs_for_retrieval.invoke({"question": question})
print(hypothetical_document)

#### OUTPUT ####
Task decomposition in large language model (LLM) agents refers to the
process of breaking down a complex, high-level task ...
```

Esta passagem é uma resposta perfeita, ao estilo de um livro didático. Agora, usamos sua incorporação para encontrar documentos reais.

```bash
# Recuperar documentos usando a abordagem HyDE
retrieval_chain = generate_docs_for_retrieval | retriever 
retrieved_docs = retrieval_chain.invoke({"question": question})

# Usar nossa cadeia RAG padrão para gerar a resposta final a partir do contexto recuperado
final_rag_chain.invoke({"context": retrieved_docs, "question": question})

#### OUTPUT ####
Task decomposition for LLM agents involves breaking down a larger task
into smaller, more manageable subgoals. This can be done using techni ...
```

Ao usar um documento hipotético como **isca**, HyDE nos ajudou a focar nos trechos mais relevantes em nossa base de conhecimento, demonstrando outra ferramenta poderosa em nosso kit de ferramentas RAG.

## Roteamento & Construção de Consultas

Nosso sistema RAG está ficando mais inteligente, mas em um cenário do mundo real, o conhecimento não é armazenado em uma única biblioteca uniforme.

> Frequentemente temos múltiplas fontes de dados: documentação para diferentes linguagens de programação, wikis internos, sites públicos ou bancos de dados com metadados estruturados.

![Roteamento e Transformação de Consultas (Criado por [Fareed Khan](None))](https://miro.medium.com/1*cost0_AWB8NKp0WxZlH7fA.png)

Enviar cada consulta para cada fonte é extremamente ineficiente e pode levar a resultados barulhentos e irrelevantes.

É aqui que nosso sistema RAG precisa evoluir de um simples bibliotecário para um **operador de central inteligente**. Ele precisa da capacidade de primeiro _analisar_ uma consulta recebida e depois _rotear_ para o destino correto ou _construir_ uma consulta mais precisa e estruturada para recuperação. Esta seção mergulha nas técnicas que tornam isso possível.

### Roteamento Lógico

Roteamento é um problema de classificação. Dada uma pergunta do usuário, precisamos classificá-la em uma de várias categorias predefinidas. Embora modelos de ML tradicionais possam fazer isso, podemos aproveitar o poderoso mecanismo de raciocínio que já temos: o próprio LLM.

![Roteamento Lógico (Criado por [Fareed Khan](None))](https://miro.medium.com/1*PK9xKW0o-72xmmLaAAozeA.png)

Ao fornecer ao LLM um esquema claro (um conjunto de categorias possíveis), podemos pedir que ele tome a decisão de classificação para nós.

Começaremos definindo o "contrato" para a saída do nosso LLM usando um modelo Pydantic. Este esquema diz explicitamente ao LLM os possíveis destinos para uma consulta.

```python
from typing import Literal
from langchain_core.pydantic_v1 import BaseModel, Field

# Definir o modelo de dados para a saída do nosso roteador
class RouteQuery(BaseModel):
    """Um modelo de dados para rotear uma consulta do usuário para a fonte de dados mais relevante."""

    # O campo 'datasource' deve ser uma das três strings literais especificadas.
    # Isso impõe um conjunto rigoroso de escolhas para o LLM.
    datasource: Literal["python_docs", "js_docs", "golang_docs"] = Field(
        ...,  # O '...' indica que este campo é obrigatório.
        description="Dada uma pergunta do usuário, escolha qual fonte de dados seria mais relevante para responder sua pergunta.",
    )
```

Com nosso esquema definido, agora podemos construir a cadeia do roteador. Usaremos um prompt para dar as instruções do LLM e então usaremos o método `.with_structured_output()` para garantir que sua resposta corresponda perfeitamente ao nosso modelo `RouteQuery`.

```python
# Inicializar nosso LLM
llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)

# Criar uma nova instância de LLM que é "estruturada" para produzir nosso modelo Pydantic
structured_llm = llm.with_structured_output(RouteQuery)

# O prompt do sistema fornece a instrução central para a tarefa do LLM.
system = """You are an expert at routing a user question to the appropriate data source.

Based on the programming language the question is referring to, route it to the relevant data source."""

# O modelo de prompt completo combina a mensagem do sistema e a pergunta do usuário.
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system),
        ("human", "{question}"),
    ]
)

# Definir a cadeia completa do roteador
router = prompt | structured_llm
```

Agora, vamos testar nosso roteador. Passaremos uma pergunta que é claramente sobre Python e inspecionaremos a saída.

```python
question = """Por que o seguinte código não funciona:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages(["human", "speak in {language}"])
prompt.invoke("french")
"""

# Invoke the router and check the result
result = router.invoke({"question": question})

print(result)

#### OUTPUT ####
datasource='python_docs'
```

A saída é uma instância do nosso modelo `RouteQuery`, e o LLM identificou corretamente `python_docs` como a fonte de dados apropriada. Esta saída estruturada agora é algo que podemos usar de forma confiável em nosso código para implementar lógica de ramificação.

```python
def choose_route(result):
    """Uma função para determinar a lógica downstream com base na saída do roteador."""
    if "python_docs" in result.datasource.lower():
        # Em um aplicativo real, esta seria uma cadeia RAG completa para documentos Python
        return "chain for python_docs"
    elif "js_docs" in result.datasource.lower():
        # Esta seria a cadeia para documentos JavaScript
        return "chain for js_docs"
    else:
        # E esta para documentos Go
        return "chain for golang_docs"

# A cadeia completa agora inclui a lógica de roteamento e ramificação
full_chain = router | RunnableLambda(choose_route)

# Vamos executar a cadeia completa
final_destination = full_chain.invoke({"question": question})

print(final_destination)

#### OUTPUT ####
chain for python_docs
```

Nossa central de comutação roteou corretamente a consulta relacionada a Python. Esta abordagem é incrivelmente poderosa para construir sistemas RAG multi-fonte.

### Roteamento Semântico

O roteamento lógico funciona perfeitamente quando você tem categorias claramente definidas. Mas e se você quiser fazer o roteamento com base no _estilo_ ou _domínio_ de uma pergunta? Por exemplo, você pode querer responder perguntas de física com um tom sério e acadêmico e perguntas de matemática com uma abordagem passo a passo e pedagógica. É aqui que entra o **Roteamento Semântico**.

![Roteamento Semântico (Criado por [Fareed Khan](None))](https://miro.medium.com/1*mzz-ncmrzdwQU37GFgPeTw.png)

> Em vez de classificar a consulta, definimos múltiplos prompts de especialistas.

Em seguida, incorporamos a consulta do usuário e cada um de nossos modelos de prompt, e usamos similaridade de cosseno para encontrar o prompt que está mais semanticamente alinhado com a consulta.

Primeiro, vamos definir nossas duas personas de especialistas.

```python
from langchain_core.prompts import PromptTemplate

# Um prompt para um professor de física
physics_template = """You are a very smart physics professor. \
You are great at answering questions about physics in a concise and easy to understand manner. \
When you don't know the answer to a question you admit that you don't know.

Here is a question:
{query}"""

# Um prompt para um especialista em matemática
math_template = """You are a very good mathematician. You are great at answering math questions. \
You are so good because you are able to break down hard problems into their component parts, \
answer the component parts, and then put them together to answer the broader question.

Here is a question:
{query}"""
```

Agora, vamos criar a função de roteamento que realiza a incorporação e comparação de similaridade.

```python
from langchain.utils.math import cosine_similarity

# Inicializar o modelo de incorporação
embeddings = OpenAIEmbeddings()

# Armazenar nossos modelos e suas incorporações para comparação
prompt_templates = [physics_template, math_template]
prompt_embeddings = embeddings.embed_documents(prompt_templates)

def prompt_router(input):
    """Uma função para rotear a consulta de entrada para o modelo de prompt mais similar."""
    # 1. Incorporar a consulta do usuário recebida
    query_embedding = embeddings.embed_query(input["query"])
    
    # 2. Calcular a similaridade de cosseno entre a consulta e todos os modelos de prompt
    similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
    
    # 3. Encontrar o índice do prompt mais similar
    most_similar_index = similarity.argmax()
    
    # 4. Selecionar o modelo de prompt mais similar
    chosen_prompt = prompt_templates[most_similar_index]
    
    print(f"DEBUG: Using {'MATH' if most_similar_index == 1 else 'PHYSICS'} template.")
    
    # 5. Retornar o objeto de prompt escolhido
    return PromptTemplate.from_template(chosen_prompt)
```

Com a lógica de roteamento em vigor, podemos construir a cadeia completa que seleciona dinamicamente o especialista certo para o trabalho.

```python
# A cadeia final que combina o roteador com o LLM
chain = (
    {"query": RunnablePassthrough()}
    | RunnableLambda(prompt_router)  # Selecionar dinamicamente o prompt
    | ChatOpenAI()
    | StrOutputParser()
)

# Fazer uma pergunta de física
print(chain.invoke("What's a black hole"))

#### OUTPUT ####
DEBUG: Using PHYSICS template.
A black hole is a region of spacetime where gravity is so strong
that nothing—no particles or even electromagnetic radiation such as
light—can escape from it. The boundary of no escape is called the
event horizon. Although it has a great effect on the fate
and circumstances of an object crossing it, it has no locally
detectable features. In many ways, a black hole acts as an ideal black body,
as it reflects no light.
```

Perfeito. O roteador identificou corretamente a pergunta como relacionada a física e usou o prompt do professor de física, resultando em uma resposta concisa e precisa. Esta técnica é excelente para criar agentes especializados que adaptam sua persona às necessidades do usuário.

### Estruturação de Consultas

Até agora, nos concentramos em recuperar de texto não estruturado. Mas a maioria dos dados do mundo real é _semi-estruturada_; contém metadados valiosos como datas, autores, contagens de visualizações ou categorias. Uma simples busca vetorial não pode aproveitar essas informações.

> **Estruturação de Consultas** é a técnica de converter uma pergunta em linguagem natural em uma consulta estruturada que pode usar esses filtros de metadados para recuperação altamente precisa.

Para ilustrar, vamos examinar os metadados disponíveis de uma transcrição de vídeo do YouTube.

```python
from langchain_community.document_loaders import YoutubeLoader

# Carregar uma transcrição do YouTube para inspecionar seus metadados
docs = YoutubeLoader.from_youtube_url(
    "https://www.youtube.com/watch?v=pbAd8O1Lvm4", add_video_info=True
).load()

# Imprimir os metadados do primeiro documento
print(docs[0].metadata)

#### OUTPUT ####
{ 
  'source': 'pbAd8O1Lvm4',
  'title': 'Self-reflective RAG with LangGraph: Self-RAG and CRAG',
  'description': 'Unknown',
  'view_count': 11922,
  'thumbnail_url': 'https://i.ytimg.com/vi/pbAd8O1Lvm4/hq720.jpg',
  'publish_date': '2024-02-07 00:00:00',
  'length': 1058,
  'author': 'LangChain'
}
```

Este documento possui metadados ricos: `view_count`, `publish_date`, `length`. Queremos que nossos usuários possam filtrar nesses campos usando linguagem natural. Para fazer isso, vamos definir outro esquema Pydantic, desta vez para uma consulta de busca de vídeo estruturada.

``````python
import datetime
from typing import Optional

class TutorialSearch(BaseModel):
    """Um modelo de dados para pesquisar em um banco de dados de vídeos tutoriais."""

    # A consulta principal para uma pesquisa de similaridade sobre a transcrição do vídeo.
    content_search: str = Field(..., description="Consulta de pesquisa de similaridade aplicada às transcrições de vídeos.")
    
    # Uma consulta mais sucinta para pesquisar apenas o título do vídeo.
    title_search: str = Field(..., description="Versão alternativa da consulta de pesquisa de conteúdo para aplicar aos títulos de vídeos.")
    
    # Filtros de metadados opcionais
    min_view_count: Optional[int] = Field(None, description="Filtro de contagem mínima de visualizações, inclusivo.")
    max_view_count: Optional[int] = Field(None, description="Filtro de contagem máxima de visualizações, exclusivo.")
    earliest_publish_date: Optional[datetime.date] = Field(None, description="Filtro de data de publicação mais antiga, inclusivo.")
    latest_publish_date: Optional[datetime.date] = Field(None, description="Filtro de data de publicação mais recente, exclusivo.")
    min_length_sec: Optional[int] = Field(None, description="Comprimento mínimo do vídeo em segundos, inclusivo.")
    max_length_sec: Optional[int] = Field(None, description="Comprimento máximo do vídeo em segundos, exclusivo.")

    def pretty_print(self) -> None:
        """Uma função auxiliar para imprimir os campos preenchidos do modelo."""
        for field in self.__fields__:
            if getattr(self, field) is not None:
                print(f"{field}: {getattr(self, field)}")
```

Este é nosso esquema alvo. Agora criaremos uma cadeia que recebe uma pergunta do usuário e preenche este modelo.

```python
# Prompt do sistema para o analisador de consultas
system = """Você é um especialista em converter perguntas de usuários em consultas de banco de dados. \
Você tem acesso a um banco de dados de vídeos tutoriais sobre uma biblioteca de software para construir aplicações alimentadas por LLMs. \
Dada uma pergunta, retorne uma consulta de banco de dados otimizada para recuperar os resultados mais relevantes.

Se houver acrônimos ou palavras que você não conhece, não tente reformulá-los."""

prompt = ChatPromptTemplate.from_messages([("system", system), ("human", "{question}")])
structured_llm = llm.with_structured_output(TutorialSearch)

# A cadeia final do analisador de consultas
query_analyzer = prompt | structured_llm
```

Vamos testar isso com algumas perguntas diferentes para ver seu poder.

```python
# Teste 1: Uma consulta simples
query_analyzer.invoke({"question": "rag from scratch"}).pretty_print()

#### SAÍDA ####
content_search: rag from scratch
title_search: rag from scratch
```

Como esperado, ele preenche os campos de pesquisa de conteúdo e título. Agora para uma consulta mais complexa.

```python
# Teste 2: Uma consulta com filtro de data
query_analyzer.invoke(
    {"question": "vídeos sobre chat langchain publicados em 2023"}
).pretty_print()

#### SAÍDA ####
content_search: chat langchain
title_search: chat langchain 2023
earliest_publish_date: 2023-01-01
latest_publish_date: 2024-01-01
```

Isso é brilhante. O LLM interpretou corretamente "em 2023" e criou um filtro de intervalo de datas. Vamos tentar mais um com uma restrição de tempo.

```python
# Teste 3: Uma consulta com filtro de comprimento
query_analyzer.invoke(
    {
        "question": "como usar modelos multimodais em um agente, apenas vídeos com menos de 5 minutos"
    }
).pretty_print()

#### SAÍDA ####
content_search: multi-modal models agent
title_search: multi-modal models agent
max_length_sec: 300
```

Ele converteu perfeitamente "menos de 5 minutos" para `max_length_sec: 300`. Esta consulta estruturada agora pode ser passada para um armazenamento vetorial que suporta filtragem de metadados, permitindo recuperação incrivelmente precisa e eficiente que vai muito além da simples busca semântica.

## Estratégias Avançadas de Indexação

Até agora, nossa abordagem para indexação foi direta: dividir documentos em pedaços e incorporá-los. Isso funciona, mas tem uma limitação fundamental.

Pedaços pequenos e focados são ótimos para a precisão da recuperação (contêm menos ruído), mas frequentemente carecem do contexto mais amplo necessário para o LLM gerar uma resposta abrangente.

![Estratégias de Indexação (Criado por [Fareed Khan](None))](https://miro.medium.com/1*PrdpYBmw3-ln5AaZLjyUaw.png)

Inversamente, pedaços grandes fornecem um ótimo contexto, mas têm desempenho ruim na recuperação porque seu significado central fica diluído.

> Este é o dilema clássico do "tamanho do pedaço". Como podemos obter o melhor dos dois mundos?

A resposta está em estratégias de indexação mais avançadas que separam a representação do documento usada para _recuperação_ daquela usada para _geração_. Vamos nos aprofundar.

### Indexação Multi-Representação

A ideia central da Indexação Multi-Representação é simples, mas poderosa: em vez de incorporar os pedaços completos do documento, criamos uma representação menor e mais focada de cada pedaço (como um resumo) e incorporamos _isso_ em vez disso.

![Indexação Multi-Representação (Criado por [Fareed Khan](None))](https://miro.medium.com/1*1TbTDTSvgVbpKxSW7feMng.png)

Durante a recuperação, pesquisamos sobre esses resumos concisos. Uma vez que encontramos o melhor resumo, usamos seu ID para procurar e recuperar o pedaço de documento original completo.

Dessa forma, obtemos a precisão de pesquisar sobre resumos pequenos e densos e o contexto rico dos documentos pai maiores para geração.

Primeiro, precisamos carregar alguns documentos para trabalhar. Vamos pegar dois posts do blog de Lilian Weng.

```python
from langchain_community.document_loaders import WebBaseLoader

# Carregar dois posts de blog diferentes para criar uma base de conhecimento mais diversa
loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
docs = loader.load()

loader = WebBaseLoader("https://lilianweng.github.io/posts/2024-02-05-human-data-quality/")
docs.extend(loader.load())

print(f"Carregados {len(docs)} documentos.")

#### SAÍDA ####
Carregados 2 documentos.
```

Em seguida, criaremos uma cadeia para gerar um resumo para cada um desses documentos.

```python
import uuid

# A cadeia para gerar resumos
summary_chain = (
    # Extrair o page_content do objeto do documento
    {"doc": lambda x: x.page_content}
    # Canalizá-lo em um modelo de prompt
    | ChatPromptTemplate.from_template("Resuma o seguinte documento:\n\n{doc}")
    # Usar um LLM para gerar o resumo
    | ChatOpenAI(model="gpt-3.5-turbo", max_retries=0)
    # Analisar a saída em uma string
    | StrOutputParser()
)

# Usar .batch() para executar a sumarização em paralelo para eficiência
summaries = summary_chain.batch(docs, {"max_concurrency": 5})

# Vamos inspecionar o primeiro resumo
print(summaries[0])

#### SAÍDA ####
O documento discute a construção de agentes autônomos alimentados por Modelos de Linguagem Grande (LLMs). Ele descreve os componentes-chave de tal sistema, ...
```

Agora vem a parte crucial. Precisamos de um `MultiVectorRetriever` que requer dois componentes principais:

1. Um `vectorstore` para armazenar as incorporações de nossos resumos.

2. Um `docstore` (um armazenamento simples de chave-valor) para manter os documentos originais completos.

```python
from langchain.storage import InMemoryByteStore
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain_core.documents import Document

# O armazenamento vetorial para indexar as incorporações de resumo
vectorstore = Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings())

# A camada de armazenamento para os documentos pai
store = InMemoryByteStore()
id_key = "doc_id" # Esta chave vinculará resumos aos seus documentos pai

# O recuperador que orquestra todo o processo
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    byte_store=store,
    id_key=id_key,
)

# Gerar IDs únicos para cada um de nossos documentos originais
doc_ids = [str(uuid.uuid4()) for _ in docs]

# Criar novos objetos Document para os resumos, adicionando o 'doc_id' aos seus metadados
summary_docs = [
    Document(page_content=s, metadata={id_key: doc_ids[i]})
    for i, s in enumerate(summaries)
]

# Adicionar os resumos ao armazenamento vetorial
retriever.vectorstore.add_documents(summary_docs)

# Adicionar os documentos originais ao docstore, vinculando-os pelos mesmos IDs
retriever.docstore.mset(list(zip(doc_ids, docs)))
```

Nosso índice avançado agora está construído. Vamos testar o processo de recuperação. Faremos uma pergunta sobre "Memória em agentes" e veremos o que acontece.

``````python
query = "Memória em agentes"

# Primeiro, vamos ver o que o vectorstore encontra ao pesquisar os resumos
sub_docs = vectorstore.similarity_search(query, k=1)
print("--- Resultado da pesquisa de resumos ---")
print(sub_docs[0].page_content)
print("\n--- Metadados mostrando o link para o documento pai ---")
print(sub_docs[0].metadata)

#### SAÍDA ####
--- Resultado da pesquisa de resumos ---
O documento discute o conceito de construção de agentes autônomos alimentados por Modelos de Linguagem Grande (LLMs) como seus controladores principais. Ele cobre componentes como planejamento, memória e uso de ferramentas, juntamente com estudos de caso e exemplos de prova de conceito como AutoGPT e GPT-Engineer. Desafios como comprimento de contexto finito, dificuldades de planejamento e confiabilidade de interfaces de linguagem natural também são destacados. O documento fornece referências a artigos de pesquisa relacionados e oferece uma visão geral abrangente de agentes autônomos alimentados por LLM.

--- Metadados mostrando o link para o documento pai ---
{'doc_id': 'cf31524b-fe6a-4b28-a980-f5687c9460ea'}
```

Como você pode ver, a pesquisa encontrou o resumo que menciona "memória". Agora, o `MultiVectorRetriever` usará o `doc_id` dos metadados deste resumo para buscar automaticamente o documento pai completo do `docstore`.

```python
# Deixe o recuperador completo fazer seu trabalho
retrieved_docs = retriever.get_relevant_documents(query, n_results=1)

# Imprima o início do documento completo recuperado
print("\n--- O documento completo recuperado pelo MultiVectorRetriever ---")
print(retrieved_docs[0].page_content[0:500])

#### SAÍDA ####
"\n\n\n\n\n\nLLM Powered Autonomous Agents | Lil'Log\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n ...."
```

Isso é exatamente o que queríamos! Pesquisamos resumos concisos, mas recuperamos o documento completo e rico em contexto, resolvendo o dilema do tamanho do chunk.

### Indexação Hierárquica (RAPTOR) Árvore de Conhecimento

**A Teoria:** RAPTOR (Recursive Abstractive Processing for Tree-Organized Retrieval) leva a ideia de multi-representação um passo adiante. Em vez de apenas uma camada de resumos, RAPTOR constrói uma árvore multinível de resumos. Começa agrupando pequenos chunks de documentos. Em seguida, resume cada cluster.

![RAPTOR (de [LangChain Docs](https://github.com/langchain-ai/langsmith-cookbook))](https://miro.medium.com/1*95v0K13O2rvsAYJ96ldhew.png)

Então, ele pega esses resumos, agrupa _eles_, e resume os novos clusters. Este processo se repete, criando uma hierarquia de conhecimento desde detalhes refinados até conceitos de alto nível. Quando você faz uma consulta, pode pesquisar em diferentes níveis desta árvore, permitindo recuperação que pode ser tão específica ou geral quanto necessário.

Esta é uma técnica mais avançada, e embora não implementemos o algoritmo completo aqui, você pode encontrar uma análise profunda e código completo no [RAPTOR Cookbook](https://github.com/langchain-ai/langchain/blob/a2529cd805b18230f187a8c49b2d2453540afe62/cookbook/RAPTOR.ipynb). Representa o estado da arte da indexação estruturada.

### Precisão em Nível de Token (ColBERT)

**A Teoria:** Modelos de embedding padrão criam um único vetor para um chunk inteiro de texto (isso é chamado de abordagem "bag-of-words"). Isso pode perder muita nuance.

![Embeddings especializados (Criado por [Fareed Khan](None))](https://miro.medium.com/1*VL6Ny9Z8S9kRqgYyFhhdsA.png)

> **ColBERT (Contextualized Late Interaction over BERT)** oferece uma abordagem mais granular. Ele gera um embedding separado e consciente do contexto para _cada token individual_ no documento.

Quando você faz uma consulta, ColBERT também incorpora cada token da sua consulta. Então, em vez de comparar um vetor de documento com um vetor de consulta, ele encontra a similaridade máxima entre cada token de consulta e _qualquer_ token de documento.

Esta "interação tardia" permite uma compreensão muito mais refinada da relevância, excelendo em buscas estilo palavra-chave.

Podemos usar facilmente ColBERT através da biblioteca `RAGatouille`.

```python
# Instale a biblioteca necessária
!pip install -U ragatouille

from ragatouille import RAGPretrainedModel

# Carregue um modelo ColBERT pré-treinado
RAG = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")
```

Agora, vamos indexar uma página da Wikipedia usando a abordagem única em nível de token do ColBERT.

```python
import requests

def get_wikipedia_page(title: str):
    """Uma função auxiliar para recuperar conteúdo da Wikipedia."""
    # Endpoint da API Wikipedia e parâmetros
    URL = "https://en.wikipedia.org/w/api.php"
    params = { "action": "query", "format": "json", "titles": title, "prop": "extracts", "explaintext": True }
    headers = {"User-Agent": "MyRAGApp/1.0"}
    response = requests.get(URL, params=params, headers=headers)
    data = response.json()
    page = next(iter(data["query"]["pages"].values()))
    return page.get("extract")

full_document = get_wikipedia_page("Hayao_Miyazaki")

# Indexe o documento com RAGatouille. Ele lida com o chunking e embedding em nível de token internamente.
RAG.index(
    collection=[full_document],
    index_name="Miyazaki-ColBERT",
    max_document_length=180,
    split_documents=True,
)
```

O processo de indexação é mais complexo, pois está criando embeddings para cada token, mas `RAGatouille` lida com isso perfeitamente. Agora, vamos pesquisar nosso novo índice.

```python
# Pesquise o índice ColBERT
results = RAG.search(query="Qual estúdio de animação Miyazaki fundou?", k=3)
print(results)

#### SAÍDA ####
[{'content': 'Em abril de 1984, ...', 'score': 25.9036, 'rank': 1, ...}, 
 {'content': 'Hayao Miyazaki ...', 'score': 25.5716, 'rank': 2, ...},
 {'content': 'Glen Keane disse ...', 'score': 24.8411, 'rank': 3, ...}]
```

O resultado principal menciona diretamente a fundação do Studio Ghibli. Também podemos envolver isso facilmente como um recuperador padrão do LangChain.

```python
# Converter o modelo RAGatouille em um retriever compatível com LangChain
colbert_retriever = RAG.as_langchain_retriever(k=3)

# Use-o como qualquer outro retriever
retrieved_docs = colbert_retriever.invoke("What animation studio did Miyazaki found?")
print(retrieved_docs[0].page_content)

#### OUTPUT ####
In April 1984, Miyazaki opened his own office in Suginami Ward, naming it Nibariki.

=== Studio Ghibli ===
==== Early films (1985–1996) ====
In June 1985, Miyazaki, Takahata, Tokuma and Suzuki founded the animation production company Studio Ghibli, with funding from Tokuma Shoten. Studio Ghibli's first film, Laputa: Castle in the Sky (1986), employed the same production crew of Nausicaä. Miyazaki's designs for the film's setting were inspired by Greek architecture and "European urbanistic templates".
```

O ColBERT fornece uma alternativa poderosa e refinada à busca vetorial tradicional, demonstrando que a forma como construímos nossa biblioteca é tão importante quanto a forma como a pesquisamos.

## Recuperação e Geração Avançadas

Criamos um sistema RAG sofisticado com roteamento inteligente e indexação avançada. Agora, chegamos à etapa final: recuperação e geração. É aqui que garantimos que o contexto que fornecemos ao LLM seja da mais alta qualidade possível e que a resposta final do LLM seja relevante, precisa e fundamentada nesse contexto.

![Recuperação/Geração (Criado por [Fareed Khan](None))](https://miro.medium.com/1*RJzBqSbw8V0LPpzYN7VFjA.png)

Mesmo com a melhor indexação, nossa recuperação inicial ainda pode conter ruído — documentos menos relevantes que passam despercebidos. E os LLMs, por mais poderosos que sejam, às vezes podem compreender mal o contexto ou alucinar.

Esta seção apresenta as técnicas avançadas que atuam como a camada final de controle de qualidade para nosso pipeline.

### Re-ranking Dedicado

Os métodos de recuperação padrão nos fornecem uma lista classificada de documentos, mas essa classificação inicial nem sempre é perfeita. **Re-ranking** é uma etapa crucial de segunda passagem onde pegamos o conjunto inicial de documentos recuperados e usamos um modelo mais sofisticado (e frequentemente mais caro) para reordená-los com base em sua relevância para a consulta.

![Re-ranking Dedicado (Criado por [Fareed Khan](None))](https://miro.medium.com/1*rnQCpniADswmhbTFiCN1Gg.png)

> Isso garante que os documentos mais relevantes sejam colocados no topo do contexto que fornecemos ao LLM.

Já vimos um método poderoso de re-ranking: Reciprocal Rank Fusion (RRF) em nossa seção RAG-Fusion. É uma ótima maneira sem modelo para combinar resultados. Mas para uma abordagem ainda mais poderosa, podemos usar um modelo de re-ranking dedicado, como o fornecido pela Cohere.

Vamos configurar um retriever padrão primeiro. Usaremos o mesmo post de blog de nossos exemplos anteriores.

```python
# Carregar, dividir e indexar o documento
loader = WebBaseLoader(web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",))
blog_docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(chunk_size=300, chunk_overlap=50)
splits = text_splitter.split_documents(blog_docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())

# Retriever de primeira passagem: obter os 10 documentos potencialmente relevantes
retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
```

Agora, apresentamos o `ContextualCompressionRetriever`. Este retriever especial envolve nosso retriever base e adiciona uma etapa de "compressão". Aqui, nosso compressor será o modelo `CohereRerank`.

Ele pegará os 10 documentos do nosso retriever base e os reordenará, retornando apenas os mais relevantes.

```python
# Você precisará instalar cohere: pip install cohere
# E definir sua variável de ambiente COHERE_API_KEY
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CohereRerank

# Inicializar o modelo Cohere Rerank
compressor = CohereRerank()

# Criar o recuperador de compressão
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor, 
    base_retriever=retriever
)

# Vamos testá-lo com nossa consulta
question = "What is task decomposition for LLM agents?"
compressed_docs = compression_retriever.get_relevant_documents(question)

# Imprimir os documentos re-classificados
print("--- Re-ranked and Compressed Documents ---")
for doc in compressed_docs:
    print(f"Relevance Score: {doc.metadata['relevance_score']:.4f}")
    print(f"Content: {doc.page_content[:150]}...\\n")

#### OUTPUT ####
--- Re-ranked and Compressed Documents ---
Relevance Score: 0.9982
Content: Task decomposition can be done (1) by LLM with simple prompting like "Steps for XYZ.", "What are the subgoals for achieving XYZ?", (2) by using task...

Relevance Score: 0.9851
Content: Tree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into mult...

Relevance Score: 0.9765
Content: LLM-powered autonomous agents have been an exciting concept. They can be used for task decomposition by prompting, using task-specific instructions, or ...
```

A saída é notável. O modelo `CohereRerank` não apenas reordenou os documentos, mas também atribuiu uma `relevance_score` a cada um. Agora podemos estar muito mais confiantes de que o contexto que passamos para o LLM é da mais alta qualidade, levando diretamente a respostas melhores e mais precisas.

### Auto-Correção usando Agentes de IA

E se nosso sistema RAG pudesse verificar seu próprio trabalho antes de dar uma resposta? Essa é a ideia por trás de arquiteturas RAG auto-corrigíveis como **CRAG (Corrective RAG)** e **Self-RAG**.

![Self Correction RAG (Do [blog Langchain](https://blog.langchain.com/agentic-rag-with-langgraph/))](https://miro.medium.com/1*LpQrsvNj09aJPMhhh4fc-A.png)

Estas não são apenas cadeias simples, são gráficos dinâmicos (frequentemente construídos com LangGraph) que podem raciocinar sobre a qualidade das informações recuperadas e decidir sobre um curso de ação.

- **CRAG:** Se os documentos recuperados forem irrelevantes ou ambíguos para uma determinada consulta, um sistema CRAG não apenas os passará para o LLM. Em vez disso, ele dispara uma nova busca na web mais robusta para encontrar informações melhores, corrige os documentos recuperados e depois prossegue com a geração.

- **Self-RAG:** Esta abordagem vai um passo além. A cada etapa, ela usa um LLM para gerar "tokens de reflexão" que criticam o processo. Ela classifica os documentos recuperados quanto à relevância. Se não forem relevantes, ela recupera novamente. Depois de ter bons documentos, ela gera uma resposta e depois classifica essa resposta quanto à consistência factual, garantindo que esteja fundamentada nos documentos de origem.

Essas técnicas representam o estado da arte na construção de RAG confiável e em nível de produção. Implementá-las do zero envolve construir uma máquina de estado ou gráfico. Embora a implementação completa seja extensa, você pode encontrar excelentes e detalhados passo a passos aqui:

- [CRAG Notebook](github.com/langchain-ai/langgraph/blob/8ccead9560f6cd76537f632d7a310ba41e38f28b/examples/rag/langgraph_crag.ipynb)

- [Self-RAG Notebook](github.com/langchain-ai/langgraph/blob/8ccead9560f6cd76537f632d7a310ba41e38f28b/examples/rag/langgraph_self_rag_mistral_nomic.ipynb)

Esses frameworks agentic são a chave para ir além de simples bots de perguntas e respostas para criar verdadeiros mecanismos de raciocínio robustos.

### Impacto do Contexto Longo

Um tema recorrente em RAG tem sido as janelas de contexto limitadas dos LLMs. Mas com o surgimento de modelos com janelas de contexto massivas (128k, 200k, ou até 1 milhão de tokens), surge uma questão:

![Long Context (Criado por [Fareed Khan](None))](https://miro.medium.com/1*g3NCw9EzZcylHpOJMlGr8A.png)

> **Ainda precisamos de RAG?** Podemos apenas colocar todos os nossos documentos no prompt?

A resposta é nuançada. Embora os modelos de contexto longo sejam incrivelmente poderosos, eles não são uma solução milagrosa.

Pesquisas mostraram que seu desempenho pode degradar quando a informação crucial está enterrada no meio de um contexto muito longo (o problema da "agulha no palheiro").

- **Vantagem do RAG:** RAG se destaca em _encontrar_ a agulha primeiro e apresentar apenas isso ao LLM. É uma ferramenta de precisão.

- **Vantagem do Contexto Longo:** Os modelos de contexto longo são fantásticos para tarefas que exigem sintetizar informações de _muitas partes diferentes_ de um documento simultaneamente, algo que o RAG pode perder.

O futuro provavelmente será uma abordagem híbrida: usar RAG para realizar uma recuperação inicial e precisa dos documentos mais relevantes e depois alimentar esse contexto de alta qualidade e pré-filtrado em um modelo de contexto longo para síntese final.

Para uma análise aprofundada deste tópico, esta apresentação é um excelente recurso:

- **Slides sobre Contexto Longo:** [The Impact of Long Context on RAG](https://docs.google.com/presentation/d/1mJUiPBdtf58NfuSEQ7pVSEQ2Oqmek7F1i4gBwR6JDss/edit#slide=id.g26c0cb8dc66_0_0)

## Avaliação Manual de RAG

Construímos um pipeline RAG cada vez mais sofisticado, adicionando técnicas avançadas para recuperação, indexação e geração. Mas uma questão crucial permanece: **como provamos que realmente funciona?**

Em um ambiente de produção, "parece funcionar" não é suficiente. Precisamos de métricas objetivas e repetíveis para medir o desempenho, identificar fraquezas e orientar melhorias.

É aqui que entra a avaliação. É a ciência de responsabilizar nosso sistema RAG. Nesta parte, exploraremos como medir quantitativamente a qualidade do nosso sistema construindo nossos próprios avaliadores do zero.

### As Métricas Principais: O Que Devemos Medir?

Antes de mergulharmos no código, vamos definir como uma "boa" resposta RAG se parece. Podemos dividi-la em alguns princípios principais:

1. **Fidelidade:** A resposta se mantém estritamente ao contexto fornecido? Uma resposta fiel não inventa informações ou usa o conhecimento pré-treinado do LLM para responder. Esta é a métrica mais importante para evitar alucinações.

2. **Correção:** A resposta é factualmente correta quando comparada a uma resposta de "verdade fundamental" ou referência?

3. **Relevância Contextual:** O contexto que recuperamos foi realmente relevante para a pergunta do usuário? Isso avalia o desempenho do nosso recuperador, não do gerador.

Vamos explorar como medir esses, começando com o método mais transparente: construir os avaliadores nós mesmos.

### Construindo Avaliadores do Zero com LangChain

A melhor maneira de entender avaliação é construí-la. Usando componentes básicos do LangChain, podemos criar cadeias personalizadas que instruem um LLM a agir como um "juiz" imparcial, classificando a saída do nosso sistema RAG com base em critérios que definimos em um prompt. Isso nos dá controle e transparência máximos.

Vamos começar com **Correção**. Nosso objetivo é criar uma cadeia que compare a generated_answer com uma resposta ground_truth e retorne uma pontuação de 0 a 1.

``````python
from langchain.prompts import PromptTemplate

# Usaremos um LLM poderoso como gpt-4o para atuar como nosso "juiz" para avaliação confiável.
llm = ChatOpenAI(temperature=0, model_name="gpt-4o", max_tokens=4000)

# Defina o esquema de saída para nossa pontuação de avaliação para garantir saída consistente e estruturada.
class ResultScore(BaseModel):
    score: float = Field(..., description="A pontuação do resultado, variando de 0 a 1, onde 1 é a melhor pontuação possível.")

# Este modelo de prompt instrui claramente o LLM sobre como pontuar a correção da resposta.
correctness_prompt = PromptTemplate(
    input_variables=["question", "ground_truth", "generated_answer"],
    template="""
    Question: {question}
    Ground Truth: {ground_truth}
    Generated Answer: {generated_answer}

    Avalie a correção da resposta gerada em comparação com a verdade fundamental.
    Pontue de 0 a 1, onde 1 é perfeitamente correto e 0 é completamente incorreto.
    
    Score:
    """
)

# Construímos a cadeia de avaliação canalizando o prompt para o LLM com saída estruturada.
correctness_chain = correctness_prompt | llm.with_structured_output(ResultScore)
```

Agora, vamos envolver isso em uma função simples e testá-la. E se a verdade fundamental for "Paris e Madrid", mas nosso sistema RAG respondeu apenas parcialmente com "Paris"?

```python
def evaluate_correctness(question, ground_truth, generated_answer):
    """Uma função auxiliar para executar nossa cadeia de avaliação de correção personalizada."""
    result = correctness_chain.invoke({
        "question": question, 
        "ground_truth": ground_truth, 
        "generated_answer": generated_answer
    })
    return result.score

# Teste a cadeia de correção com uma resposta parcialmente correta.
question = "Qual é a capital da França e da Espanha?"
ground_truth = "Paris e Madrid"
generated_answer = "Paris"
score = evaluate_correctness(question, ground_truth, generated_answer)

print(f"Correctness Score: {score}")

### OUTPUT ###
Correctness Score: 0.5
```

Este é um resultado perfeito. Nosso LLM juiz raciocinou corretamente que a resposta gerada era apenas metade correta e atribuiu uma pontuação apropriada de 0,5.

A seguir, vamos construir um avaliador para **Fidelidade**. Isso é argumentavelmente mais importante do que a correção para RAG, pois é nossa defesa principal contra alucinação.

Aqui, o LLM juiz deve ignorar se a resposta é factualmente correta e _apenas_ se importar se a resposta pode ser derivada do `context` fornecido.

```python
# O modelo de prompt para fidelidade inclui vários exemplos (few-shot prompting)
# para deixar as instruções para o LLM juiz cristalinas.
faithfulness_prompt = PromptTemplate(
    input_variables=["question","context", "generated_answer"],
    template="""
    Question: {question}
    Context: {context}
    Generated Answer: {generated_answer}

    Avalie se a resposta gerada para a pergunta pode ser deduzida do contexto.
    Pontuação de 0 ou 1, onde 1 é perfeitamente fiel *E PODE SER DERIVADA DO CONTEXTO* e 0 caso contrário.
    Você não se importa se a resposta está correta; tudo o que você se importa é se a resposta pode ser deduzida do contexto.
    
    [... alguns exemplos do notebook para guiar o LLM ...]

    Exemplo:
    Question: O que é 2+2?
    Context: 4.
    Generated Answer: 4.
    Neste caso, o contexto afirma '4', mas não fornece informações para deduzir a resposta para 'O que é 2+2?', então a pontuação deve ser 0.
    """
)

# Construa a cadeia de fidelidade usando o mesmo LLM estruturado.
faithfulness_chain = faithfulness_prompt | llm.with_structured_output(ResultScore)
```

Fornecemos vários exemplos no prompt para guiar o raciocínio do LLM, especialmente para casos extremos complicados. Vamos testá-lo com o exemplo "2+2", que é um teste clássico de fidelidade.

```python
def evaluate_faithfulness(question, context, generated_answer):
    """Uma função auxiliar para executar nossa cadeia de avaliação de fidelidade personalizada."""
    result = faithfulness_chain.invoke({
        "question": question, 
        "context": context, 
        "generated_answer": generated_answer
    })
    return result.score

# Teste a cadeia de fidelidade. A resposta está correta, mas é fiel?
question = "o que é 3+3?"
context = "6"
generated_answer = "6"
score = evaluate_faithfulness(question, context, generated_answer)

print(f"Faithfulness Score: {score}")

#### OUTPUT ####
Faithfulness Score: 0.0
```

Isso demonstra o poder e a precisão de uma métrica de fidelidade bem definida. Embora a resposta **6** seja factualmente correta, ela não pôde ser logicamente deduzida do contexto fornecido "6".

O contexto não disse **3+3 é igual a 6**. Nosso sistema sinalizou corretamente isso como uma resposta infiel, que é provavelmente uma alucinação onde o LLM usou seu próprio conhecimento pré-treinado em vez do contexto fornecido.

Construir esses avaliadores do zero fornece uma visão profunda do que estamos medindo. No entanto, pode ser demorado. Na próxima parte, veremos como alcançar os mesmos resultados de forma mais eficiente usando estruturas de avaliação especializadas.

## Avaliação com Estruturas

Na parte anterior, construímos nossas próprias cadeias de avaliação do zero. Esta é uma maneira fantástica de entender os princípios fundamentais das métricas de RAG.

> No entanto, para testes mais rápidos e robustos, estruturas de avaliação dedicadas são o caminho a seguir.

![Avaliação usando Estruturas (Criado por [Fareed Khan](None))](https://miro.medium.com/1*uBn-2vN1Bz--NXfaeR2hyw.png)

Essas bibliotecas fornecem métricas pré-construídas e ajustadas que lidam com a complexidade da avaliação para nós, permitindo que nos concentremos em analisar os resultados.

Exploraremos três estruturas populares: `deepeval`, `grouse` e o powerhouse específico para RAG, `RAGAS`.

### Avaliação Rápida com `deepeval`

`deepeval` é uma estrutura poderosa e de código aberto projetada para tornar a avaliação de LLM simples e intuitiva. Ela fornece um conjunto de métricas bem definidas que podem ser facilmente aplicadas aos resultados do seu pipeline RAG.

O fluxo de trabalho envolve criar objetos `LLMTestCase` e medi-los em relação a métricas pré-construídas como `Correctness`, `Faithfulness` e `ContextualRelevancy`.

```python
# Você precisará instalar deepeval: pip install deepeval
from deepeval import evaluate
from deepeval.metrics import GEval, FaithfulnessMetric, ContextualRelevancyMetric
from deepeval.test_case import LLMTestCase

# Crie casos de teste
test_case_correctness = LLMTestCase(
    input="Qual é a capital da Espanha?",
    expected_output="Madrid é a capital da Espanha.",
    actual_output="MadriD."
)

test_case_faithfulness = LLMTestCase(
    input="o que é 3+3?",
    actual_output="6",
    retrieval_context=["6"]
)

# A função evaluate() executa todos os casos de teste em relação a todas as métricas especificadas
evaluation_results = evaluate(
    test_cases=[test_case_correctness, test_case_faithfulness],
    metrics=[GEval(name="Correctness", model="gpt-4o"), FaithfulnessMetric()]
)

print(evaluation_results)

#### OUTPUT ####
✨ Evaluation Results ✨
-------------------------
Overall Score: 0.50
-------------------------
Metrics Summary:
- Correctness: 1.00
- Faithfulness: 0.00
-------------------------
```

A visualização agregada do `deepeval` nos dá imediatamente uma visão de alto nível do desempenho do nosso sistema, facilitando a identificação de áreas que precisam de melhoria.

### Outra Alternativa Poderosa com `grouse`

`grouse` é outra excelente opção de código aberto, oferecendo um conjunto semelhante de métricas, mas com um foco único em permitir personalização profunda dos prompts do "juiz". Isso é útil para ajustar os critérios de avaliação para um domínio específico.

```python
# Você precisará instalar grouse: pip install grouse-eval
from grouse import EvaluationSample, GroundedQAEvaluator

evaluator = GroundedQAEvaluator()
unfaithful_sample = EvaluationSample(
    input="Onde fica a Torre Eiffel?",
    actual_output="A Torre Eiffel fica localizada na Rue Rabelais em Paris.",
    references=[
        "A Torre Eiffel é uma torre de treliça de ferro forjado no Champ de Mars em Paris, França",
        "Gustave Eiffel morreu em seu apartamento na Rue Rabelais em Paris."
    ]
)

result = evaluator.evaluate(eval_samples=[unfaithful_sample]).evaluations[0]
print(f"Pontuação de Fidelidade Grouse (0 ou 1): {result.faithfulness.faithfulness}")

#### SAÍDA ####
Pontuação de Fidelidade Grouse (0 ou 1): 0
```

Como `deepeval`, `grouse` captura efetivamente erros sutis, fornecendo outra ferramenta robusta para nosso kit de ferramentas de avaliação.

### Avaliação com `RAGAS`

Enquanto `deepeval` e `grouse` são ótimos avaliadores de propósito geral, **`RAGAS` (Retrieval-Augmented Generation Assessment)** é um framework construído especificamente para avaliar pipelines RAG. Ele fornece um conjunto abrangente de métricas que medem cada componente do seu sistema, desde o recuperador até o gerador.

Para usar `RAGAS`, primeiro precisamos preparar nossos dados de avaliação em um formato específico. Requer quatro informações-chave para cada caso de teste:

- `question`: A consulta de entrada do usuário.

- `answer`: A resposta final gerada pelo nosso sistema RAG.

- `contexts`: A lista de documentos recuperados pelo nosso recuperador.

- `ground_truth`: A resposta correta de referência.

Vamos preparar um conjunto de dados de amostra.

```python
# 1. Preparar os dados de avaliação
questions = [
    "Qual é o nome do cão de três cabeças que guarda a Pedra Filosofal?",
    "Quem deu a Harry Potter sua primeira vassoura?",
    "Qual casa o Chapéu Seletor considerou inicialmente para Harry?",
]

# Estas seriam as respostas geradas pelo nosso pipeline RAG
generated_answers = [
    "O cão de três cabeças se chama Fluffy.",
    "A Professora McGonagall deu a Harry sua primeira vassoura, uma Nimbus 2000.",
    "O Chapéu Seletor considerou fortemente colocar Harry em Sonserina.",
]

# A verdade fundamental, ou respostas "perfeitas"
ground_truth_answers = [
    "Fluffy",
    "Professora McGonagall",
    "Sonserina",
]

# O contexto recuperado pelo nosso sistema RAG para cada pergunta
retrieved_documents = [
    ["Um cão massivo de três cabeças guardava uma alçapão. Hagrid mencionou que seu nome era Fluffy."],
    ["Alunos do primeiro ano não podem ter vassouras, mas a Professora McGonagall, chefe da Grifinória, fez uma exceção para Harry."],
    ["O Chapéu Seletor sussurrou no ouvido de Harry, 'Você poderia ser grande, sabe, tudo está aqui em sua cabeça, e Sonserina o ajudará no caminho para a grandeza...'"],
]
```

Em seguida, estruturamos esses dados usando a biblioteca `datasets` do Hugging Face, que `RAGAS` se integra perfeitamente.

```python
# Você precisará instalar ragas e datasets: pip install ragas datasets
from datasets import Dataset

# 2. Estruturar os dados em um objeto Dataset do Hugging Face
data_samples = {
    'question': questions,
    'answer': generated_answers,
    'contexts': retrieved_documents,
    'ground_truth': ground_truth_answers
}

dataset = Dataset.from_dict(data_samples)
```

Agora, podemos definir nossas métricas e executar a avaliação. `RAGAS` oferece várias métricas poderosas específicas para RAG prontas para uso.

```python
from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_recall,
    answer_correctness,
)

# 3. Definir as métricas que queremos usar para avaliação
metrics = [
    faithfulness,       # Quão consistente factualmente é a resposta com o contexto? (Previne alucinação)
    answer_relevancy,   # Quão relevante é a resposta para a pergunta?
    context_recall,     # Recuperamos todo o contexto necessário para responder a pergunta?
    answer_correctness, # Quão precisa é a resposta em comparação com a verdade fundamental?
]

# 4. Executar a avaliação
result = evaluate(
    dataset=dataset, 
    metrics=metrics
)

# 5. Exibir os resultados em um formato de tabela limpo
results_df = result.to_pandas()
print(results_df)
```
<script src="https://gist.github.com/khalid557557/7a4cf3b5e00280389b8a7394bf52001b.js"></script>

Podemos ver que nosso sistema é altamente fiel e recupera contexto relevante bem (`faithfulness` e `context_recall` são perfeitos). As respostas também são altamente relevantes e corretas, com apenas pequenos desvios.

`RAGAS` torna incrivelmente fácil executar esse tipo de avaliação abrangente de ponta a ponta, fornecendo os dados que precisamos para implantar e melhorar com confiança nossas aplicações RAG.

## Resumindo Tudo

Então, vamos resumir o que fizemos até agora no caminho para construir um sistema RAG pronto para produção.

1. Na **Parte 1**, construímos um sistema RAG fundamental do zero, cobrindo os três componentes principais: **Indexação** de nossos dados, **Recuperação** de contexto relevante e **Geração** de uma resposta final.

2. Na **Parte 2**, passamos para **Transformações Avançadas de Consultas**, usando técnicas como RAG-Fusion, Decomposição e HyDE para reescrever e expandir perguntas do usuário para recuperação muito mais precisa.

3. Na **Parte 3**, transformamos nosso pipeline em uma central inteligente, adicionando **Roteamento** para direcionar consultas para a fonte de dados correta e **Estruturação de Consultas** para aproveitar poderosos filtros de metadados.

4. Na **Parte 4**, focamos em **Indexação Avançada**, explorando estratégias como Indexação Multi-Representação e ColBERT em nível de token para criar uma biblioteca de conhecimento mais inteligente e eficiente.

5. Na **Parte 5**, polimos a saída final com técnicas de **Recuperação Avançada** como re-ranking para priorizar o melhor contexto e introduzimos conceitos agênticos e autocorretivos como CRAG e Self-RAG.

6. Finalmente, nas **Partes 6 e 7**, abordamos a etapa crucial de **Avaliação**. Aprendemos como medir o desempenho do nosso sistema com métricas-chave como fidelidade e correção, tanto construindo avaliadores do zero quanto usando frameworks poderosos como deepeval, grouse e RAGAS.

> Caso você goste deste blog, sinta-se livre para **[me seguir no Medium](https://medium.com/@fareedkhandev)** - só escrevo lá.