这篇文章上次修改于 442 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

概念解析

RAG(Retrieval-Augmented Generation,检索增强生成)是一种结合信息检索(Retrieval)和文本生成(Generation)的技术,常用于大模型(如GPT、LLM)中,以提升回答的准确性和可控性。

其中信息检索涉及到知识库的构建和知识库检索

而文本生成涉及大模型的使用,通常是使用现成的大模型,例如qwen,deepseek等,主要利用了大模型的逻辑推理能力。

简单的来说,RAG应用实际并没有听着那么高级,更像是大模型的先进使用,不拘泥于只使用大模型进行问答,而是提供了参考文档,让大模型根据文档进行检索和回答。

例如构建校园问答应用,只需要构建校园信息的知识库,再在每次提问的时候,将初始提问和校园信息知识库进行结合为新提问,将新提问提供给大模型,大模型会结合新提问里面给予的参考信息进行回答。

信息检索

概念解析

Embedding

Embedding(嵌入) 是一种将文本、图像或其他数据转换为高维数值向量的技术,使其能够在向量空间中表示语义相似性。

例如,语义相近的句子会被映射到相近的向量。例如,queen和king在Embedding里面的相似度高于queen和apple。

现在的Embedding大多基于句子级别,根据整个句子的语义动态生成。句意思更相近的,两个向量的余弦相似度较高。

Embedding 通常用于自然语言处理(NLP)、推荐系统 和 检索增强生成(RAG) 等任务,常见模型包括 OpenAI Embeddings、BERT、SBERT 等。

对于以上我们可以得到一种新的方式,用于判断两个句子的相近程度,而许多厂商都有提供Embedding API,即使用同一个Embedding模型的情况下,两个句子的高维向量可以表示两个句子之间的联系。

对于langchain,有一定的预备接口以满足

os.environ["OPENAI_API_BASE"] = "<https://api.gptsapi.net/v1>" # 可以修改基础url
embedding = OpenAIEmbeddings(
    model="text-embedding-3-large",
    openai_api_key=key)

或者以下是一个构建Embeddings实例,此处使用自定义的Embeddings

import torch
from transformers import AutoTokenizer, AutoModel
from langchain_core.embeddings import Embeddings
from tqdm import tqdm

class MathBERTEmbeddings(Embeddings):
    def __init__(self, model_name="tbs17/MathBERT", device=None):
        self.model_name = model_name
        self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
        # 加载模型和分词器
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModel.from_pretrained(model_name).to(self.device)
        print("MathBERT模型加载完成")
    
    def embed_documents(self, texts):
        """为文档列表创建嵌入"""
        embeddings = []
        
        # 使用tqdm显示进度
        for text in tqdm(texts, desc="创建嵌入"):
            embeddings.append(self._embed_text(text))
            
        return embeddings
    
    def embed_query(self, text):
        return self._embed_text(text)
    
    def _embed_text(self, text):
        inputs = self.tokenizer(
            text, 
            return_tensors="pt", 
            max_length=512,
            padding="max_length",
            truncation=True
        ).to(self.device)
        with torch.no_grad():
            outputs = self.model(**inputs, output_hidden_states=True)
            last_hidden_state = outputs.last_hidden_state
            cls_embedding = last_hidden_state[:, 0, :].squeeze().cpu().numpy()
            
            return cls_embedding.tolist()

使用例:

vector = embedding.embed_query("hello")
print(vector[:3])
# [-0.024605071172118187, -0.0075481850653886795, 0.004001544788479805]

向量数据库

是知识库的基础,也就是用于存放我们参考信息的数据库,由于参考信息经过Embedding变成了多个高维向量集合,将其都存储进向量数据库,接下来的需求就是:

输入一个句子,根据Embedding的原理找到相近的数据作为参考依据。

为了查找和检索效率,产生了向量数据库,对相近向量的检索有特别的优化,提高了效率。

构建知识库

在 检索增强生成(RAG) 框架下,向量数据库主要用于存储和高效检索知识。典型流程如下:

数据准备

  • 采集知识(如文档、论文、对话记录)
  • 预处理文本(分段、去噪、去重)

生成向量(Embedding)

  • 使用 Embedding API(如 OpenAI Embeddings、Hugging Face、BERT)
  • 将文本转换为向量并存入向量数据库

存入向量数据库(Vector DB)

  • 选择合适的数据库(如 FAISS、Milvus、Pinecone)
  • 存储向量及其关联的原始文本

以下是一个使用例

def create_faiss_index_from_limo():
    try:
        with open(r"limo_test.json", "r", encoding="utf-8") as f:
            data = json.load(f)
            documents = [
                Document(
                    page_content=f"问题: {item['instruction']}\\n\\n解答: {item['output']}",
                ) 
                for item in tqdm(data, desc="处理数据")
            ]
    except Exception as e:
        print(f"加载数据时出错: {e}")
        return None
    
    try:
        embedding = OpenAIEmbeddings(
            model="text-embedding-3-large",
            openai_api_key=key)
    except Exception as e:
        print(f"创建Embeddings模型时出错: {e}")
        return None
    
    try:
        vectorstore = FAISS.from_documents(documents, embedding)
    except Exception as e:

        print(f"构建FAISS索引时出错: {e}")
        return None
    
    try:
        vectorstore.save_local("faiss_index")
        return vectorstore
    except Exception as e:
        print(f"保存索引时出错: {e}")
        return None

使用知识库

查询 & 召回(Retrieval)

  • 用户输入问题(转换为向量)
  • 在向量数据库中进行相似度搜索(如余弦相似度)
  • 召回最相关的知识片段

最后将检索到的知识提供给大模型作为参考。

使用Langchain进行实践

Langchain集成了RAG的整个流程,

同时,为了满足langchain中模型的使用,修改了llm的类方法

class QwenMath(LLM):
    model: object = None
    tokenizer: object = None
    max_new_tokens: int = 512
    device: object = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    def __init__(self, model_name: str):
        super().__init__()

        # 加载模型和tokenizer
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModelForCausalLM.from_pretrained(
            model_name,
            torch_dtype="auto",
            device_map={"": 0}
        )

    def chat(self, model, tokenizer, msgs):
        text = tokenizer.apply_chat_template(
            msgs,
            tokenize=False,
            add_generation_prompt=True
        )

        model_inputs = tokenizer(text, return_tensors="pt", max_length=512, truncation=True).to(self.device)

        generated_ids = model.generate(
            **model_inputs,
            max_new_tokens=self.max_new_tokens,
        )
        generated_ids = [
            output_ids[len(input_ids):]
            for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
        ]
        response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]

        return response

    def _call(self, prompt: str, stop=["<|user|>"], run_manager=None, **kwargs):
        msgs = []
        for line in prompt.splitlines():
            if line.startswith("System: "):
                msgs.append({'role': 'system', 'content': line[8:]})
            elif line.startswith("Human: "):
                msgs.append({'role': 'user', 'content': line[7:]})

        response = self.chat(self.model, self.tokenizer, msgs)
        return response

    @property
    def _llm_type(self) -> str:
        return "Qwen2"

以下是一个完整使用例:

from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from QwenMath import QwenMath
from langchain_community.vectorstores import FAISS

from ed import MathBERTEmbeddings
import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"

embeddings = MathBERTEmbeddings()

# ✅ 1. 创建 LLM
llm = QwenMath("Qwen2.5-Math-7B")

# ✅ 2. 创建知识库检索器
vectorstore = FAISS.load_local("faiss_mathbert_index", embeddings,
                               allow_dangerous_deserialization=True)  # 你的向量存储
retriever = vectorstore.as_retriever()

# ✅ 3. 改写查询,使其带有历史上下文
contextualize_q_system_prompt = (
    "Given a chat history and the latest user question "
    "which might reference context in the chat history, "
    "formulate a standalone question which can be understood "
    "without the chat history. Do NOT answer the question, just "
    "reformulate it if needed and otherwise return it as is."
)
contextualize_q_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", contextualize_q_system_prompt),
        MessagesPlaceholder("chat_history"),  # 这里用于存储聊天历史
        ("human", "{input}"),  # 用户的新问题
    ]
)
history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)

# ✅ 4. 让 LLM 结合知识库回答问题
qa_system_prompt = (
    "你是一个数学方面的助手,需要帮助用户解决数学问题,要求答案正确,并且要提供思考步骤。\\n\\n"
    "{context}"
)
qa_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", qa_system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)

# ✅ 5. 创建 RAG(检索增强生成)链
rag_chain = create_retrieval_chain(
    history_aware_retriever, question_answer_chain
)

# ✅ 6. 运行
chat_history = []  # 用于存储历史消息
query = "求解Inx=x-e的解"

result = rag_chain.invoke({"input": query, "chat_history": chat_history})
print(result["answer"])