使用 Python 中的强化学习最大化简单 RAG 性能

本篇文章:Maximizing Simple RAG Performance Using RL in Python适合对强化学习和信息检索有兴趣的读者,亮点在于通过自定义的强化学习奖励系统,将简单的检索增强生成(RAG)模型的检索质量从53%提升至84%。作者详细介绍了如何从零开始实现该模型,且代码清晰易懂。


文章目录

  • [1 GitHub 仓库与性能结果](#1 GitHub 仓库与性能结果)
  • [2 概述](#2 概述)
    • [2.1 环境设置](#2.1 环境设置)
    • [2.2 数据预处理](#2.2 数据预处理)
    • [2.3 文档嵌入生成](#2.3 文档嵌入生成)
    • [2.4 向量存储实现](#2.4 向量存储实现)
    • [2.5 简单检索实现](#2.5 简单检索实现)
    • [2.6 LLM 响应生成](#2.6 LLM 响应生成)
    • [2.7 基础 RAG 流水线](#2.7 基础 RAG 流水线)
    • [2.8 评估基础 RAG 流水线](#2.8 评估基础 RAG 流水线)
  • [3 RAG 的强化学习](#3 RAG 的强化学习)
    • [3.1 状态、动作空间和奖励方法](#3.1 状态、动作空间和奖励方法)
    • [3.2 动作函数逻辑](#3.2 动作函数逻辑)
    • [3.3 策略网络](#3.3 策略网络)
    • [3.4 单步 RL](#3.4 单步 RL)
    • [3.5 训练参数与策略更新](#3.5 训练参数与策略更新)
    • [3.6 训练循环](#3.6 训练循环)
    • [3.7 性能比较逻辑](#3.7 性能比较逻辑)
    • [3.8 评估(RL 对比简单)RAG](#3.8 评估(RL 对比简单)RAG)
  • [4 结论](#4 结论)

本文将介绍如何使用我们自己的 RL 奖励系统 来改进最简单的 RAG 实现,这将把事实查询的检索质量从 53% 提升到 84%

我们将从零开始编写所有代码,包括 RL 算法,不使用任何 Python 库

并且以Jupyter Notebook 风格保持所有内容清晰简单,使其易于理解和学习。

本文将使用三个重要内容:

  1. 用于响应生成:google/gemma-2--2b-it
  2. 用于嵌入生成:BAAI/bge-en-icl
  3. 用于 RL 奖励:贪婪算法(寻找最大值)

1 GitHub 仓库与性能结果

逐步的 Notebook 可在此处获取:
https://github.com/FareedKhan-dev/rag-with-rl

让我们来看看我们将获得的简单 RAG 与基于 RL 的 RAG 的结果对比。

我的查询:

超导体中量子比特的数学表示是什么?

复制代码
My Query:
What is the mathematical representation of a qubit in superposition?

(Non RL Response): ψ  α0  β1

经过 5 个训练回合后的响应是:

复制代码
(RL Response):
The mathematical equation describing a qubit in superposition is: 
|ψ⟩ = α|0⟩ + β|1⟩

Where:

* |ψ⟩ represents the superposition state of the qubit.
* α and β are complex coefficients representing the probabilities of finding the qubit in the |0⟩ and |1⟩ states, respectively. 
* |0⟩ and |1⟩ are the basis states of the qubit.

Evaluation Results:
----------------------------------------
Simple RAG similarity to ground truth: 0.5326
RL-enhanced RAG similarity to ground truth: 0.8652
Improvement: 33.26%

然而,计算时间很重要,显然取决于我们托管嵌入/大语言模型的基础设施。

2 概述

看看我们的简单 RAG 和基于 RL 的 RAG 方法之间的区别的概述。

我们最简单的 RAG 从查询和文档开始,这些文档被分块。嵌入模型根据查询找到相关块,并将前 K 个 块作为上下文传递给 LLM,以生成该查询的响应。

我们的基于 RL 的 RAG 使用一个 RL 代理,该代理根据 LLM 的响应采取行动。行动可以包括重写查询、检索更多块或删除不相关的块。代理会持续此过程(多个回合),直到达到最佳结果。

2.1 环境设置

首先,我们需要克隆仓库。

python 复制代码
!git clone https://github.com/FareedKhan-dev/rag-with-rl.git

安装所需的库。

python 复制代码
!pip install -r rag-with-rl/requirements.txt

然后我们需要导入必要的库并设置环境。

我们将使用托管在 Nebius 平台下的 HuggingFace 模型。显然,您可以使用自己的模型,只要它们与 OpenAI 的 API 兼容。

python 复制代码
import os
from openai import OpenAI
import numpy as np
import json
from typing import Dict, List, Tuple, Optional, Union

接下来,我们需要初始化负责响应和嵌入生成的客户端。

python 复制代码
client = OpenAI(
    base_url="https://api.studio.nebius.com/v1/",
    api_key= os.environ["OPENAI_API_KEY"]
)

2.2 数据预处理

现在我们已经进入数据预处理阶段,我们需要加载数据并对其进行预处理。

让我们创建一个函数,该函数将从目录中加载所有 .txt 文件并返回文档列表。

python 复制代码
def load_documents(directory_path: str) -> List[str]:
    """
    Load all text documents from the specified directory.

    Args:
        directory_path (str): Path to the directory containing text files.

    Returns:
        List[str]: A list of strings, where each string is the content of a text file.
    """

    documents = []
    for filename in os.listdir(directory_path):
        if filename.endswith(".txt"):
            with open(os.path.join(directory_path, filename), 'r', encoding='utf-8') as file:
                documents.append(file.read())
    return documents

我们需要创建一个函数,在文档加载后对其进行分块。

我们使用 chunk_size100 个字符(约 30 个单词),但您可以根据需要进行调整。

python 复制代码
def split_into_chunks(documents: List[str], chunk_size: int = 30) -> List[str]:
    """
    Split documents into smaller chunks of specified size.

    Args:
        documents (List[str]): A list of document strings to be split into chunks.
        chunk_size (int): The maximum number of words in each chunk. Default is 100.

    Returns:
        List[str]: A list of chunks, where each chunk is a string containing up to `chunk_size` words.
    """

    chunks = []
    for doc in documents:
        words = doc.split()
        for i in range(0, len(words), chunk_size):
            chunk = " ".join(words[i:i + chunk_size])
            chunks.append(chunk)
    return chunks

此步骤是可选的,我们通过删除特殊字符、转换为小写等方式预处理每个块。

python 复制代码
def preprocess_text(text: str) -> str:
    """
    Preprocess the input text by converting it to lowercase and removing special characters.

    Args:
        text (str): The input text to preprocess.

    Returns:
        str: The preprocessed text with only alphanumeric characters and spaces.
    """

    text = text.lower()
    text = ''.join(char for char in text if char.isalnum() or char.isspace())
    return text

但是,如果您使用之前的预处理步骤,您可以简单地创建一个函数来预处理整个文档。

python 复制代码
def preprocess_chunks(chunks: List[str]) -> List[str]:
    """
    Apply preprocessing to all text chunks.

    Args:
        chunks (List[str]): A list of text chunks to preprocess.

    Returns:
        List[str]: A list of preprocessed text chunks.
    """

    return [preprocess_text(chunk) for chunk in chunks]

现在我们已经实现了所有用于数据预处理的函数,我们可以从目录中加载文档,将它们分成块,并预处理这些块。

python 复制代码
directory_path = "data"
documents = load_documents(directory_path)
chunks = split_into_chunks(documents)
preprocessed_chunks = preprocess_chunks(chunks)

打印前两个块的前 200 个字符。

python 复制代码
for i in range(2):
    print(f"Chunk {i+1}: {preprocessed_chunks[i][:50]} ... ")
    print("-" * 50)
复制代码
OUTPUT:
Chunk 1: quantum computing principles progress and possibil ...
--------------------------------------------------
Chunk 2: process information in binary digits bits quantum ...
--------------------------------------------------

2.3 文档嵌入生成

在上一步中,我们对文档进行了分块。现在是时候为分块数据集生成嵌入了。

在使用 RAG 时,我们的知识库通常非常大。因此,我们需要分批生成嵌入。让我们创建一个核心函数来分批生成块的嵌入。

我们使用的嵌入模型是 BAAI/bge-en-icl

python 复制代码
def generate_embeddings_batch(chunks_batch: List[str], model: str = "BAAI/bge-en-icl") -> List[List[float]]:
    """
    Generate embeddings for a batch of text chunks using the OpenAI client.

    Args:
        chunks_batch (List[str]): A batch of text chunks to generate embeddings for.
        model (str): The model to use for embedding generation. Default is "BAAI/bge-en-icl".

    Returns:
        List[List[float]]: A list of embeddings, where each embedding is a list of floats.
    """

    response = client.embeddings.create(
        model=model,
        input=chunks_batch
    )
    embeddings = [item.embedding for item in response.data]
    return embeddings

接下来,我们将定义一个函数,用于分批生成所有文本块的嵌入。

此函数将文本块列表作为输入,并使用 OpenAI 客户端为每批块生成嵌入。

该函数将返回与所有文本块对应的嵌入列表。

python 复制代码
def generate_embeddings(chunks: List[str], batch_size: int = 10) -> np.ndarray:
    """
    Generate embeddings for all text chunks in batches.

    Args:
        chunks (List[str]): A list of text chunks to generate embeddings for.
        batch_size (int): The number of chunks to process in each batch. Default is 10.

    Returns:
        np.ndarray: A NumPy array containing embeddings for all chunks.
    """

    all_embeddings = []
    for i in range(0, len(chunks), batch_size):
        batch = chunks[i:i + batch_size]
        embeddings = generate_embeddings_batch(batch)
        all_embeddings.extend(embeddings)
    return np.array(all_embeddings)

让我们创建另一个函数,将嵌入保存到 JSON 文件中。

python 复制代码
def save_embeddings(embeddings: np.ndarray, output_file: str) -> None:
    """
    Save embeddings to a JSON file.

    Args:
        embeddings (np.ndarray): A NumPy array containing the embeddings to save.
        output_file (str): The path to the output JSON file where embeddings will be saved.

    Returns:
        None
    """

    with open(output_file, 'w', encoding='utf-8') as file:
        json.dump(embeddings.tolist(), file)

现在我们已经实现了所有用于嵌入生成的函数,我们可以继续为预处理的文本块生成嵌入并将其保存到 JSON 文件中。

python 复制代码
preprocessed_chunks = preprocess_chunks(chunks)
embeddings = generate_embeddings(preprocessed_chunks)
save_embeddings(embeddings, "embeddings.json")

2.4 向量存储实现

由于我们不使用任何 Python 库进行向量存储,我们将使用字典实现一个简单的向量存储。

python 复制代码
vector_store: dict[int, dict[str, object]] = {}

def add_to_vector_store(embeddings: np.ndarray, chunks: List[str]) -> None:
    """
    Add embeddings and their corresponding text chunks to the vector store.

    Args:
        embeddings (np.ndarray): A NumPy array containing the embeddings to add.
        chunks (List[str]): A list of text chunks corresponding to the embeddings.

    Returns:
        None
    """

    for embedding, chunk in zip(embeddings, chunks):
        vector_store[len(vector_store)] = {"embedding": embedding, "chunk": chunk}

2.5 简单检索实现

我们知道,为了检索与给定查询最相似的文本块,我们可以使用查询嵌入和所有文本块嵌入之间的余弦相似度。

余弦相似度越高,文本块越相似。

然后我们可以根据相似度分数对块进行排序,并返回前 k 个最相似的块。

因此,让我们实现一个简单的基于余弦相似度的检索函数。两个向量 A 和 B 之间的余弦相似度计算公式为:

其中:

  • A ⋅ B A \cdot B A⋅B 是向量 A 和 B 的点积
  • ∣ ∣ A ∣ ∣ ||A|| ∣∣A∣∣ 和 ∣ ∣ B ∣ ∣ ||B|| ∣∣B∣∣ 是向量的欧几里得范数(大小)
  • n n n 是向量的维度
python 复制代码
def cosine_similarity(vec1: np.ndarray, vec2: np.ndarray) -> float:
    """
    Compute the cosine similarity between two vectors.

    Args:
        vec1 (np.ndarray): The first vector.
        vec2 (np.ndarray): The second vector.

    Returns:
        float: The cosine similarity between the two vectors, ranging from -1 to 1.
    """

    dot_product = np.dot(vec1, vec2)
    norm_vec1 = np.linalg.norm(vec1)
    norm_vec2 = np.linalg.norm(vec2)
    return dot_product / (norm_vec1 * norm_vec2)

当我们计算查询与所有块之间的余弦相似度时,我们可以执行相似度搜索。

根据 top_k 参数,我们检索前 k 个最相似的块。

python 复制代码
def similarity_search(query_embedding: np.ndarray, top_k: int = 5) -> List[str]:
    """
    Perform similarity search in the vector store and return the top_k most similar chunks.

    Args:
        query_embedding (np.ndarray): The embedding vector of the query.
        top_k (int): The number of most similar chunks to retrieve. Default is 5.

    Returns:
        List[str]: A list of the top_k most similar text chunks.
    """

    similarities = []
    for key, value in vector_store.items():
        similarity = cosine_similarity(query_embedding, value["embedding"])
        similarities.append((key, similarity))
    similarities = sorted(similarities, key=lambda x: x[1], reverse=True)
    return [vector_store[key]["chunk"] for key, _ in similarities[:top_k]]

一旦我们有了相似度搜索函数,我们就可以在其之上简单地编写一个检索函数,该函数将根据查询提供相关的块。

python 复制代码
def retrieve_relevant_chunks(query_text: str, top_k: int = 5) -> List[str]:
    """
    Retrieve the most relevant document chunks for a given query text.

    Args:
        query_text (str): The query text for which relevant chunks are to be retrieved.
        top_k (int): The number of most relevant chunks to retrieve. Default is 5.

    Returns:
        List[str]: A list of the top_k most relevant text chunks.
    """

    query_embedding = generate_embeddings([query_text])[0]
    relevant_chunks = similarity_search(query_embedding, top_k=top_k)
    return relevant_chunks

现在我们已经实现了所有用于检索的函数,我们可以继续使用示例查询测试检索系统。

python 复制代码
add_to_vector_store(embeddings, preprocessed_chunks)
query_text = "What is Quantum Computing?"
relevant_chunks = retrieve_relevant_chunks(query_text)

for idx, chunk in enumerate(relevant_chunks):
    print(f"Chunk {idx + 1}: {chunk[:50]} ... ")
    print("-" * 50)
复制代码
OUTPUT:
Chunk 1: quantum computing principles progress and possibil ...
--------------------------------------------------
Chunk 2: through distinct stages 1 nisq era current 2 error ...
--------------------------------------------------
Chunk 3: quantum advantage and practical applications quant ...
--------------------------------------------------
Chunk 4: process information in binary digits bits quantum ...
--------------------------------------------------
Chunk 5: measuring the correct answer quantum gates and cir ...
--------------------------------------------------

2.6 LLM 响应生成

当我们有一个查询和一组相关的文档块时,我们可以使用大型语言模型(LLM)根据查询和检索到的信息生成响应。

在本节中,我们将使用 OpenAI API 通过向 LLM 提供查询文本和相关文档块作为上下文来生成对查询的响应。

首先,我们需要一个函数来构建 LLM 的输入提示,其中包括查询文本和相关文档块作为上下文。

python 复制代码
def construct_prompt(query: str, context_chunks: List[str]) -> str:
    """
    Construct a prompt by combining the query with the retrieved context chunks.

    Args:
        query (str): The query text for which the prompt is being constructed.
        context_chunks (List[str]): A list of relevant context chunks to include in the prompt.

    Returns:
        str: The constructed prompt to be used as input for the LLM.
    """

    context = "\n".join(context_chunks)
    system_message = (
        "You are a helpful assistant. Only use the provided context to answer the question. "
        "If the context doesn't contain the information needed, say 'I don't have enough information to answer this question.'"
    )
    prompt = f"System: {system_message}\n\nContext:\n{context}\n\nQuestion:\n{query}\n\nAnswer:"
    return prompt

为了生成 LLM 响应,我们需要实现一个函数,该函数接收构建的输入提示并将其发送到 OpenAI API 进行响应生成。

python 复制代码
def generate_response(
    prompt: str,
    model: str = "google/gemma-2-2b-it",
    max_tokens: int = 512,
    temperature: float = 1,
    top_p: float = 0.9,
    top_k: int = 50) -> str:
    """
    Generate a response from the OpenAI chat model based on the constructed prompt.

    Args:
        prompt (str): The input prompt to provide to the chat model.
        model (str): The model to use for generating the response. Default is "google/gemma-2-2b-it".
        max_tokens (int): Maximum number of tokens in the response. Default is 512.
        temperature (float): Sampling temperature for response diversity. Default is 0.5.
        top_p (float): Probability mass for nucleus sampling. Default is 0.9.
        top_k (int): Number of highest probability tokens to consider. Default is 50.

    Returns:
        str: The generated response from the chat model.
    """

    response = client.chat.completions.create(
        model=model,
        max_tokens=max_tokens,
        temperature=temperature,
        top_p=top_p,
        extra_body={
            "top_k": top_k
        },
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": prompt
                    }
                ]
            }
        ]
    )
    return response.choices[0].message.content

2.7 基础 RAG 流水线

我们不能重复运行小段代码。因此,我们需要创建一个简单的 RAG 流水线,它只接受一个参数,即我们的查询,并返回 LLM 响应。

python 复制代码
def basic_rag_pipeline(query: str) -> str:
    """
    Implement the basic Retrieval-Augmented Generation (RAG) pipeline:
    retrieve relevant chunks, construct a prompt, and generate a response.

    Args:
        query (str): The input query for which a response is to be generated.

    Returns:
        str: The generated response from the LLM based on the query and retrieved context.
    """

    relevant_chunks: List[str] = retrieve_relevant_chunks(query)
    prompt: str = construct_prompt(query, relevant_chunks)
    response: str = generate_response(prompt)
    return response

2.8 评估基础 RAG 流水线

现在我们已经编写了基础 RAG 流水线,我们可以用它进行评估。

我们的评估查询包含不同的目标段,例如 factual_queriescomplex_nature。我们将测试 RAG 流水线的事实知识。

让我们加载我们的评估查询及其预期答案。

python 复制代码
with open('data/val.json', 'r') as file:
    validation_data = json.load(file)

sample_query = validation_data['basic_factual_questions'][0]['question']
expected_answer = validation_data['basic_factual_questions'][0]['answer']

print(f"Sample Query: {sample_query}\n")
print(f"Expected Answer: {expected_answer}\n")
复制代码
Sample Query: What is the mathematical representation of a
qubit in superposition?

Expected Answer: |ψ⟩ = α|0⟩ + β|1⟩, where α and β are complex
numbers satisfying |α|² + |β|² = 1, representing the probability
amplitudes for measuring the qubit in state |0⟩ or |1⟩ respectively.

让我们用这个评估查询测试基础 RAG 流水线,看看它的表现如何。

python 复制代码
print("🔍 Running the Retrieval-Augmented Generation (RAG) pipeline...")
print(f"✉️ Query: {sample_query}\n")

response = basic_rag_pipeline(sample_query)

print("🤖 AI Response:")
print("-" * 50)
print(response.strip())
print("-" * 50)

print("✅ Ground Truth Answer:")
print("-" * 50)
print(expected_answer)
print("-" * 50)
复制代码
OUTPUT:
🔍 Running the Retrieval-Augmented Generation (RAG) pipeline...
✉️ Query: What is the mathematical representation of a qubit in superposition?

🤖 AI Response:
--------------------------------------------------
ψ α0 β1
--------------------------------------------------
✅ Ground Truth Answer:
--------------------------------------------------
|ψ⟩ = α|0⟩ + β|1⟩, where α and β are complex numbers satisfying |α|² + |β|² = 1, representing the probability amplitudes for measuring the qubit in state |0⟩ or |1⟩ respectively.
--------------------------------------------------

简单的 RAG 流水线在当前状态下表现不佳。生成的响应不仅与真实值无关,而且还缺少关键信息。

但别担心!在接下来的步骤中,我们将实现一个基于强化学习的 RAG 流水线来解决这些缺点。

这将帮助我们改进检索和生成过程,使响应更加准确和上下文相关。

敬请期待,我们将把我们的 RAG 流水线提升到一个新的水平!🚀

3 RAG 的强化学习

强化学习 (RL) 是一种机器学习类型,其中代理通过在环境中采取行动来学习做出决策,以最大化某种累积奖励。

与监督学习不同,代理不会被明确告知要采取哪些行动,而是必须通过试错来发现哪些行动能带来最大的奖励。

强化学习系统的主要组成部分如下:

  1. 代理:学习者或决策者
  2. 环境:代理与之交互的世界
  3. 状态 (S):代理在环境中的当前情况
  4. 动作 (A):代理可以采取的一组可能的行动
  5. 奖励 ®:每次行动后环境的反馈
  6. 策略 (π):代理遵循的确定下一个行动的策略

强化学习的目标是学习一个策略 [\pi],使其最大化预期累积奖励:

π ∗ = arg ⁡ max ⁡ π E [ ∑ t = 0 T γ t R t ∣ π ] \pi^* = \arg\max_{\pi} E[\sum_{t=0}^{T} \gamma^t R_t | \pi] π∗=argmaxπE[∑t=0TγtRt∣π]

其中:

  • π ∗ \pi^* π∗ 是最优策略
  • γ \gamma γ 是折扣因子 ( 0 ≤ γ ≤ 1 0 \leq \gamma \leq 1 0≤γ≤1)
  • R t R_t Rt 是时间步 t t t 的奖励
  • T T T 是最终时间步

在 RAG 系统的背景下,强化学习可用于:

  • 通过学习哪些文档最有帮助来改进检索
  • 根据用户反馈完善提示构建
  • 通过从成功的响应中学习来优化生成过程

3.1 状态、动作空间和奖励方法

编写 RL 算法的第一步是定义三件事:

  • 状态: 它是环境的当前情况。在我们的例子中,初始状态是我们的简单 RAG 流水线(查询、上下文、响应)。
  • 动作空间: 它是代理根据状态采取的决策。在我们的例子中,动作可以包括更改模型、修改上下文、更改查询等。
  • 奖励: 它是代理在采取行动后收到的反馈。在我们的例子中,奖励可以是生成的响应与真实答案之间的相似度。

我们的状态将随着训练的进行而不断变化。为此,我们需要在每个 训练回合 后保存状态,以便我们的 RL 代理可以从中学习并避免再次犯同样的错误。

python 复制代码
def define_state(
    query: str,
    context_chunks: List[str],
    rewritten_query: str = None,
    previous_responses: List[str] = None,
    previous_rewards: List[float] = None) -> dict:
    """
    Define the state representation for the reinforcement learning agent.

        Args:
        query (str): The original user query.
        context_chunks (List[str]): Retrieved context chunks from the knowledge base.
        rewritten_query (str, optional): A reformulated version of the original query.
        previous_responses (List[str], optional): List of previously generated responses.
        previous_rewards (List[float], optional): List of rewards received for previous actions.

        Returns:
        dict: A dictionary representing the current state with all relevant information.
    """

    state = {
        "original_query": query,
        "current_query": rewritten_query if rewritten_query else query,
        "context": context_chunks,
        "previous_responses": previous_responses if previous_responses else [],
        "previous_rewards": previous_rewards if previous_rewards else []
    }
    return state

我们已经定义了 RL 代理的状态表示,包括用户查询、检索到的上下文块、重写后的查询(如果有)以及响应和奖励的历史记录。

此状态将指导代理生成更好的响应。

接下来我们需要定义强化学习代理的动作空间。

动作空间由代理在每个步骤中可以采取的一组可能的动作组成。在这种情况下,我们定义了四个动作:

  • rewrite_query:重新 формуulate 原始查询以改进检索
  • expand_context:检索额外的上下文块
  • filter_context:删除不相关的上下文块
  • generate_response:根据当前查询和上下文生成响应
python 复制代码
def define_action_space() -> List[str]:
    """
    Define the set of possible actions the reinforcement learning agent can take.

        Actions include:
    - rewrite_query: Reformulate the original query to improve retrieval
    - expand_context: Retrieve additional context chunks
    - filter_context: Remove irrelevant context chunks
    - generate_response: Generate a response based on current query and context

        Returns:
        List[str]: A list of available actions.
    """

    actions = ["rewrite_query", "expand_context", "filter_context", "generate_response"]
    return actions

显然,当我们的 RL 代理采取行动时,它将基于当前状态和动作空间。

它将根据 RAG 流水线生成的响应质量获得奖励。

奖励函数将基于生成的响应与真实答案之间的余弦相似度。

python 复制代码
def calculate_reward(response: str, ground_truth: str) -> float:
    """
    Calculate a reward value by comparing the generated response to the ground truth.

        Uses cosine similarity between the embeddings of the response and ground truth
    to determine how close the response is to the expected answer.

        Args:
        response (str): The generated response from the RAG pipeline.
        ground_truth (str): The expected correct answer.

        Returns:
        float: A reward value between -1 and 1, where higher values indicate
               greater similarity to the ground truth.
    """

    response_embedding = generate_embeddings([response])[0]
    ground_truth_embedding = generate_embeddings([ground_truth])[0]
    similarity = cosine_similarity(response_embedding, ground_truth_embedding)
    return similarity

我们的目标是通过生成与真实答案相似的响应来最大化奖励。更高的奖励值表示生成的响应与预期答案更一致。

3.2 动作函数逻辑

现在我们已经定义了动作空间,我们需要为每个动作实现逻辑。

此逻辑将确定如何根据 RL 代理采取的动作修改 RAG 流水线。

回顾一下,四个动作是:

  • rewrite_query:重新 формуulate 原始查询以改进检索
  • expand_context:检索额外的上下文块
  • filter_context:删除不相关的上下文块
  • generate_response:根据当前查询和上下文生成响应

让我们为代理创建第一个动作逻辑。我们将实现的第一个动作是 rewrite_query 动作,它涉及重新 формуulate 原始用户查询以提高检索性能。

此操作对于增强检索到的上下文的相关性并生成更准确的响应至关重要。

python 复制代码
def rewrite_query(
    query: str,
    context_chunks: List[str],
    model: str = "google/gemma-2-2b-it",
    max_tokens: int = 100,
    temperature: float = 0.3) -> str:
    """
    Use the LLM to rewrite the query for better document retrieval.

    Args:
        query (str): The original query text.
        context_chunks (List[str]): A list of context chunks retrieved so far.
        model (str): The model to use for generating the rewritten query. Default is "google/gemma-2-2b-it".
        max_tokens (int): Maximum number of tokens in the rewritten query. Default is 100.
        temperature (float): Sampling temperature for response diversity. Default is 0.3.

    Returns:
        str: The rewritten query optimized for document retrieval.
    """

    rewrite_prompt = f"""
    You are a query optimization assistant. Your task is to rewrite the given query to make it more effective
    for retrieving relevant information. The query will be used for document retrieval.

        Original query: {query}

        Based on the context retrieved so far:
    {' '.join(context_chunks[:2]) if context_chunks else 'No context available yet'}

        Rewrite the query to be more specific and targeted to retrieve better information.
    Rewritten query:
    """

    response = client.chat.completions.create(
        model=model,
        max_tokens=max_tokens,
        temperature=temperature,
        messages=[
            {
                "role": "user",
                "content": rewrite_prompt
            }
        ]
    )
    rewritten_query = response.choices[0].message.content.strip()
    return rewritten_query

此操作对于增强检索到的上下文的相关性并生成更准确的响应至关重要。

让我们编写下一个动作逻辑,即通过检索额外的块来扩展上下文。

我们将使用现有函数 retrieve_relevant_chunks 来获取更多上下文块,然后从当前上下文中过滤掉任何重复项。

我们将限制添加到上下文中的新块的数量为指定的 top_k 值。

python 复制代码
def expand_context(query: str, current_chunks: List[str], top_k: int = 3) -> List[str]:
    """
    Expand the context by retrieving additional chunks.

    Args:
        query (str): The query text for which additional context is needed.
        current_chunks (List[str]): The current list of context chunks.
        top_k (int): The number of additional chunks to retrieve. Default is 3.

    Returns:
        List[str]: The expanded list of context chunks including new unique chunks.
    """

    additional_chunks = retrieve_relevant_chunks(query, top_k=top_k + len(current_chunks))
    new_chunks = []
    for chunk in additional_chunks:
        if chunk not in current_chunks:
            new_chunks.append(chunk)

    expanded_context = current_chunks + new_chunks[:top_k]
    return expanded_context

我们需要过滤上下文,只保留与查询最相关的块。

此过滤步骤对于确保提供给语言模型的上下文简洁并专注于最相关的信息至关重要。

python 复制代码
def filter_context(query: str, context_chunks: List[str]) -> List[str]:
    """
    Filter the context to keep only the most relevant chunks.

    Args:
        query (str): The query text for which relevance is calculated.
        context_chunks (List[str]): The list of context chunks to filter.

    Returns:
        List[str]: A filtered list of the most relevant context chunks.
    """

    if not context_chunks:
        return []

    query_embedding = generate_embeddings([query])[0]
    chunk_embeddings = [generate_embeddings([chunk])[0] for chunk in context_chunks]

    relevance_scores = []
    for chunk_embedding in chunk_embeddings:
        score = cosine_similarity(query_embedding, chunk_embedding)
        relevance_scores.append(score)

    sorted_chunks = [x for _, x in sorted(zip(relevance_scores, context_chunks), reverse=True)]
    filtered_chunks = sorted_chunks[:min(5, len(sorted_chunks))]
    return filtered_chunks

此操作将帮助代理探索与查询相关的更多信息。

3.3 策略网络

之前,我们定义了状态、动作和奖励逻辑。接下来,我们需要创建一个策略网络,它将根据当前状态选择一个动作。

策略网络是一个函数,它将当前状态和动作空间作为输入,并根据状态返回所选动作。

策略网络可以使用简单的启发式方法根据当前状态选择动作。

例如,如果没有先前的响应,策略网络可以优先重写查询。如果上下文包含太多块,策略网络可以选择过滤上下文。

python 复制代码
def policy_network(
    state: dict,
    action_space: List[str],
    epsilon: float = 0.2) -> str:
    """
    Define a policy network to select an action based on the current state using an epsilon-greedy strategy.

    Args:
        state (dict): The current state of the environment, including query, context, responses, and rewards.
        action_space (List[str]): The list of possible actions the agent can take.
        epsilon (float): The probability of choosing a random action for exploration. Default is 0.2.

    Returns:
        str: The selected action from the action space.
    """

    if np.random.random() < epsilon:
        action = np.random.choice(action_space)
    else:
        if len(state["previous_responses"]) == 0:
            action = "rewrite_query"
        elif state["previous_rewards"] and max(state["previous_rewards"]) < 0.7:
            action = "expand_context"
        elif len(state["context"]) > 5:
            action = "filter_context"
        else:
            action = "generate_response"
    return action

因此,我们的策略网络的工作方式如下:

  • 如果没有先前的响应,优先重写查询。
  • 如果存在先前的响应但奖励较低,尝试扩展上下文。
  • 如果上下文包含太多块,尝试过滤上下文。
  • 否则,生成响应。

3.4 单步 RL

我们已经编写了 RL 流水线的重要组成部分。

对于任何进行过任何类型训练的开发人员来说,都存在一个训练循环,其中每次迭代都是一个单步,RL 代理在此步骤中采取行动,计算奖励,更新状态等等。

因此,我们需要编写训练循环的单步。让我们来做。

python 复制代码
def rl_step(
    state: dict,
    action_space: List[str],
    ground_truth: str) -> tuple[dict, str, float, str]:
    """
    Perform a single RL step: select an action, execute it, and calculate the reward.

    Args:
        state (dict): The current state of the environment, including query, context, responses, and rewards.
        action_space (List[str]): The list of possible actions the agent can take.
        ground_truth (str): The expected correct answer to calculate the reward.

    Returns:
        tuple: A tuple containing:
            - state (dict): The updated state after executing the action.
            - action (str): The action selected by the policy network.
            - reward (float): The reward received for the action.
            - response (str): The response generated (if applicable).
    """

    action: str = policy_network(state, action_space)
    response: str = None
    reward: float = 0

    if action == "rewrite_query":
        rewritten_query: str = rewrite_query(state["original_query"], state["context"])
        state["current_query"] = rewritten_query
        new_context: List[str] = retrieve_relevant_chunks(rewritten_query)
        state["context"] = new_context

    elif action == "expand_context":
        expanded_context: List[str] = expand_context(state["current_query"], state["context"])
        state["context"] = expanded_context

    elif action == "filter_context":
        filtered_context: List[str] = filter_context(state["current_query"], state["context"])
        state["context"] = filtered_context

    elif action == "generate_response":
        prompt: str = construct_prompt(state["current_query"], state["context"])
        response: str = generate_response(prompt)
        reward: float = calculate_reward(response, ground_truth)
        state["previous_responses"].append(response)
        state["previous_rewards"].append(reward)

    return state, action, reward, response

在我们的单步函数中,我们首先使用策略网络选择一个动作。策略网络使用 epsilon-greedy 策略来平衡探索和利用。

如果随机数小于 epsilon,我们从动作空间中选择一个随机动作进行探索。否则,我们使用简单的启发式方法根据当前状态选择最佳动作。

3.5 训练参数与策略更新

我们需要为训练循环定义一些训练参数,并定义一个函数来根据收到的奖励更新策略。

尽管训练参数函数是可选的,但它可以用于 RL 流水线的高级实现。

python 复制代码
def initialize_training_params() -> Dict[str, Union[float, int]]:
    """
    Initialize training parameters such as learning rate, number of episodes, and discount factor.

    Returns:
        Dict[str, Union[float, int]]: A dictionary containing the initialized training parameters.
    """

    params = {
        "learning_rate": 0.01,
        "num_episodes": 100,
        "discount_factor": 0.99
    }
    return params

与 RL 过程中每一步之后状态变化的方式类似,策略也需要根据收到的奖励进行更新。

update_policy 函数以当前策略、状态、动作、奖励和学习率作为输入,并返回更新后的策略。

python 复制代码
def update_policy(
    policy: Dict[str, Dict[str, Union[float, str]]],
    state: Dict[str, object],
    action: str,
    reward: float,
    learning_rate: float) -> Dict[str, Dict[str, Union[float, str]]]:
    """
    Update the policy based on the reward received.

    Args:
        policy (Dict[str, Dict[str, Union[float, str]]]): The current policy to be updated.
        state (Dict[str, object]): The current state of the environment.
        action (str): The action taken by the agent.
        reward (float): The reward received for the action.
        learning_rate (float): The learning rate for updating the policy.

    Returns:
        Dict[str, Dict[str, Union[float, str]]]: The updated policy.
    """

    policy[state["query"]] = {
        "action": action,
        "reward": reward
    }
    return policy

在上述 update_policy 逻辑中,我们将每个查询的已采取行动和收到的奖励存储在策略字典中。

在更高级的 RL 算法中,策略更新将涉及更复杂的方法,例如策略梯度或 Q 学习。

最后,我们需要实现进度跟踪逻辑来监控训练过程。

这将帮助我们了解模型如何学习和随着时间的推移而改进。

python 复制代码
def track_progress(
    episode: int,
    reward: float,
    rewards_history: List[float]) -> List[float]:
    """
    Track the training progress by storing rewards for each episode.

    Args:
        episode (int): The current episode number.
        reward (float): The reward received in the current episode.
        rewards_history (List[float]): A list to store the rewards for all episodes.

    Returns:
        List[float]: The updated rewards history.
    """

    rewards_history.append(reward)
    print(f"Episode {episode}: Reward = {reward}")
    return rewards_history

3.6 训练循环

现在我们已经编写了训练循环的每个部分,我们可以将它们组合在一个函数中,该函数实现了 RL 增强 RAG 系统的训练循环。

python 复制代码
def training_loop(
    query_text: str,
    ground_truth: str,
    params: Optional[Dict[str, Union[float, int]]] = None) -> Tuple[Dict[str, Dict[str, Union[float, str]]], List[float], List[List[str]], Optional[str]]:
    """
    Implement the training loop for RL-enhanced RAG.

    Args:
        query_text (str): The input query text for the RAG pipeline.
        ground_truth (str): The expected correct answer for the query.
        params (Optional[Dict[str, Union[float, int]]]): Training parameters such as learning rate,
            number of episodes, and discount factor. If None, default parameters are initialized.

    Returns:
        Tuple: A tuple containing:
            - policy (Dict[str, Dict[str, Union[float, str]]]): The updated policy after training.
            - rewards_history (List[float]): A list of rewards received in each episode.
            - actions_history (List[List[str]]): A list of actions taken in each episode.
            - best_response (Optional[str]): The best response generated during training.
    """

    if params is None:
        params = initialize_training_params()

    rewards_history: List[float] = []
    actions_history: List[List[str]] = []
    policy: Dict[str, Dict[str, Union[float, str]]] = {}
    action_space: List[str] = define_action_space()
    best_response: Optional[str] = None
    best_reward: float = -1

    simple_response: str = basic_rag_pipeline(query_text)
    simple_reward: float = calculate_reward(simple_response, ground_truth)
    print(f"Simple RAG reward: {simple_reward:.4f}")

    for episode in range(params["num_episodes"]):
        context_chunks: List[str] = retrieve_relevant_chunks(query_text)
        state: Dict[str, object] = define_state(query_text, context_chunks)
        episode_reward: float = 0
        episode_actions: List[str] = []

        for step in range(10):
            state, action, reward, response = rl_step(state, action_space, ground_truth)
            episode_actions.append(action)

            if response:
                episode_reward = reward

                if reward > best_reward:
                    best_reward = reward
                    best_response = response
                break

        rewards_history.append(episode_reward)
        actions_history.append(episode_actions)

        if episode % 5 == 0:
            print(f"Episode {episode}: Reward = {episode_reward:.4f}, Actions = {episode_actions}")

    improvement: float = best_reward - simple_reward
    print(f"\nTraining completed:")
    print(f"Simple RAG reward: {simple_reward:.4f}")
    print(f"Best RL-enhanced RAG reward: {best_reward:.4f}")
    print(f"Improvement: {improvement:.4f} ({improvement * 100:.2f}%)")

    return policy, rewards_history, actions_history, best_response

此函数将接收输入查询文本、预期真实答案以及可选的训练参数。

它将返回更新后的策略、每个回合中收到的奖励列表、每个回合中采取的行动列表以及训练过程中生成的最佳响应。

更详细地说,training_loop 函数将:

  • 如果未提供,则初始化训练参数。
  • 获取简单 RAG 流水线的初始性能以进行比较。
  • 开始指定回合数的训练循环。
  • 在每个回合中执行一个 RL 步骤。
  • 更新每个回合的奖励和行动历史。
  • 每 5 个回合打印一次进度。
  • 比较最佳 RL 增强 RAG 奖励与简单 RAG 奖励。
  • 返回更新后的策略、奖励历史、行动历史以及训练过程中生成的最佳响应。

3.7 性能比较逻辑

尽管我们可以手动比较简单 RAG 流水线与基于 RL 的 RAG 流水线,但一个函数肯定可以帮助我们。

因此,让我们定义一个函数来比较简单 RAG 流水线与 RL 增强 RAG 流水线的性能。

python 复制代码
def compare_rag_approaches(query_text: str, ground_truth: str) -> Tuple[str, str, float, float]:
    """
    Compare the outputs of simple RAG versus RL-enhanced RAG.

    Args:
        query_text (str): The input query text for the RAG pipeline.
        ground_truth (str): The expected correct answer for the query.

    Returns:
        Tuple[str, str, float, float]: A tuple containing:
            - simple_response (str): The response generated by the simple RAG pipeline.
            - best_rl_response (str): The best response generated by the RL-enhanced RAG pipeline.
            - simple_similarity (float): The similarity score of the simple RAG response to the ground truth.
            - rl_similarity (float): The similarity score of the RL-enhanced RAG response to the ground truth.
    """

    print("=" * 80)
    print(f"Query: {query_text}")
    print("=" * 80)

    simple_response: str = basic_rag_pipeline(query_text)
    simple_similarity: float = calculate_reward(simple_response, ground_truth)

    print("\nSimple RAG Output:")
    print("-" * 40)
    print(simple_response)
    print(f"Similarity to ground truth: {simple_similarity:.4f}")

    print("\nTraining RL-enhanced RAG model...")
    params: Dict[str, float | int] = initialize_training_params()
    params["num_episodes"] = 5

    _, rewards_history, actions_history, best_rl_response = training_loop(
        query_text, ground_truth, params
    )

    if best_rl_response is None:
        context_chunks: List[str] = retrieve_relevant_chunks(query_text)
        prompt: str = construct_prompt(query_text, context_chunks)
        best_rl_response: str = generate_response(prompt)

    rl_similarity: float = calculate_reward(best_rl_response, ground_truth)

    print("\nRL-enhanced RAG Output:")
    print("-" * 40)
    print(best_rl_response)
    print(f"Similarity to ground truth: {rl_similarity:.4f}")

    improvement: float = rl_similarity - simple_similarity

    print("\nEvaluation Results:")
    print("-" * 40)
    print(f"Simple RAG similarity to ground truth: {simple_similarity:.4f}")
    print(f"RL-enhanced RAG similarity to ground truth: {rl_similarity:.4f}")
    print(f"Improvement: {improvement * 100:.2f}%")

    if len(rewards_history) > 1:
        try:
            import matplotlib.pyplot as plt

            plt.figure(figsize=(10, 6))
            plt.plot(rewards_history)
            plt.title('Reward History During RL Training')
            plt.xlabel('Episode')
            plt.ylabel('Reward')
            plt.grid(True)
            plt.show()
        except ImportError:
            print("Matplotlib not available for plotting rewards")

    return simple_response, best_rl_response, simple_similarity, rl_similarity

因此,我们的性能比较逻辑并不复杂,但基于 4 个步骤:

  1. 使用简单 RAG 流水线生成响应。
  2. 使用训练循环训练 RL 增强 RAG 模型。
  3. 评估并比较结果。
  4. 绘制奖励历史(如果可用)。

3.8 评估(RL 对比简单)RAG

让我们评估简单 RAG 流水线与 RL 增强 RAG 流水线在我们的事实查询上的性能,简单 RAG 之前未能提供正确答案。让我们看看 RL 增强 RAG 流水线是否能表现更好。

让我们重新审视我们的评估查询,看看简单 RAG 流水线为其生成了什么。

复制代码
OUTPUT:
🔍 Running the Retrieval-Augmented Generation (RAG) pipeline...
✉️ Query: What is the mathematical representation of a qubit in superposition?

🤖 AI Response:
--------------------------------------------------
ψ α0 β1
--------------------------------------------------
✅ Ground Truth Answer:
--------------------------------------------------
|ψ⟩ = α|0⟩ + β|1⟩, where α and β are complex numbers satisfying |α|² + |β|² = 1, representing the probability amplitudes for measuring the qubit in state |0⟩ or |1⟩ respectively.
--------------------------------------------------
python 复制代码
simple_response, rl_response, simple_sim, rl_sim = compare_rag_approaches(sample_query, expected_answer)
复制代码
OUTPUT:
================================================================================
Query: What is the mathematical representation of a qubit in superposition?
================================================================================

Simple RAG Output:
----------------------------------------
ψ α0 β1

Similarity to ground truth: 0.6726

Training RL-enhanced RAG model...
Simple RAG reward: 0.6772
Episode 0: Reward = 0.0000, Actions = ['rewrite_query', 'rewrite_query', np.str_('rewrite_query'), 'rewrite_query', np.str_('rewrite_query'), 'rewrite_query', 'rewrite_query', 'rewrite_query', np.str_('expand_context'), 'rewrite_query']

Training completed:
Simple RAG reward: 0.6772
Best RL-enhanced RAG reward: 0.8652
Improvement: 0.1879 (18.79%)

RL-enhanced RAG Output:
----------------------------------------
The mathematical representation of a qubit in superposition is:
ψ = α0 + β1

Where:

* α and β are complex numbers.
* α² + β² = 1

Let me know if you would like a deeper explanation of any of these terms!

Similarity to ground truth: 0.8652

Evaluation Results:
----------------------------------------
Simple RAG similarity to ground truth: 0.5326
RL-enhanced RAG similarity to ground truth: 0.8652
Improvement: 33.26%

奖励图

您可以清楚地看到,RL 增强 RAG 模型生成的响应比简单 RAG 流水线更准确和相关。

与真实值相似度的提高显而易见,这表明 RL 增强模型通过训练学会了生成更好的响应。

4 结论

  • 在事实查询上,简单 RAG 的性能低于 RL 增强 RAG。
  • RL 增强 RAG 在 5 个回合内将相似度分数提高了 19.5%。

可以通过以下方式进一步改进:

  • 训练更多回合。
  • 调整超参数。
  • 时间是训练的关键限制。
  • RL 算法的并行实现有助于减少训练时间。
相关推荐
max5006006 小时前
图像处理:实现多图点重叠效果
开发语言·图像处理·人工智能·python·深度学习·音视频
AI原吾6 小时前
玩转物联网只需十行代码,可它为何悄悄停止维护
python·物联网·hbmqtt
云动雨颤6 小时前
Python单元测试入门:3个核心断言方法,帮你快速定位代码bug
python·单元测试
SunnyDays10117 小时前
Python 实现 HTML 转 Word 和 PDF
python·html转word·html转pdf·html转docx·html转doc
RickyWasYoung7 小时前
【代码】matlab-遗传算法工具箱
开发语言·matlab
跟橙姐学代码7 小时前
Python异常处理:告别程序崩溃,让代码更优雅!
前端·python·ipython
蓝纹绿茶8 小时前
Python程序使用了Ffmpeg,结束程序后,文件夹中仍然生成音频、视频文件
python·ubuntu·ffmpeg·音视频
mahuifa8 小时前
OpenCV 开发 -- 图像基本处理
人工智能·python·opencv·计算机视觉
土了个豆子的8 小时前
03.缓存池
开发语言·前端·缓存·visualstudio·c#