以一个简单案例来讲解RAG

0 阅读7分钟

RAG简介

如果你需要一个客服来应对客户对产品的提问,你会怎么做?

  1. 你想到的第一个点就是花钱请一个人专门做客服,但是工资+五险一金的成本又太高了。

  2. 如果不请人,那么用一个脚本来回复问题吧,但是脚本程序只能固定死if-else,几个选项,肯定不能给客户灵活的回复问题。

  3. 那么使用大语言模型,这个可以进行“活人感”的对话服务,但是大模型并不知道我的产品是怎样的,使用手册和注意事项什么的呀。

  4. 于是我们使用一种技术,让大模型可以根据特定的知识库进行学习和归纳,而这个技术叫做rag。

    rag的本质,就是一个模型大脑+一个随时可查阅的“外部知识库”。把大模型比作一个学生,他自己训练过的知识,就是自己平时学到的,而我们需要特别定制的内容,对于学生来说,是超纲的内容。这时候我们直接把书拿来给他,开卷考试即可。

技术准备

语言为python,以langchain框架为主。

langchain是目前最为火热的大模型应用开发框架,其核心是将各个模型与工具和数据源结合,使得一个干巴巴的只懂聊天的模型,可以调用数据和程序,更像一个真正的ai。

  1. 模型调用

langchain已经内置了一套规范的模型调用api,不需要自己去每个模型网站上一个个看接口,自己组装网络报文,各种调试了。

llm = ChatOpenAI(
    model="deepseek-chat",
    temperature=0.7,
    api_key=os.getenv("DEEPSEEK_API_KEY"),  
    base_url="https://api.deepseek.com/v1"
)

messages = [
    SystemMessage(content="你是一个友好的助手,用简洁语言回答问题。"),
    HumanMessage(content=question)
]

response = llm.invoke(messages)
return response

重点讲一下base_url="https://api.deepseek.com/v1",指定的事模型的网络地址,如果没有声明,那么默认走openai,但是openai没有这个模型,就会返回错误。

image.png

其次是模型的入参messages,可以看到,其参数是一个数组,其顺序是 system|human|system|human的方式来排列的,system表达的是模型方的信息,human是用户的信息。这里只写两个信息,表达的是第一轮交互。而第一个system指的是对模型的预定义。

  1. 构建向量数据库

image.png

rag的核心,就是从向量数据库中获取对应的数据,然后对其进行解析,返还。相当于我们为大模型创建的“移动硬盘”。这里我们以一个香蕉手机使用说明书.txt做rag。

数据处理流程:

  1. 将非文本数据转换为统一的文档数据。
# 加载txt文档
def load_txt(txt_path):
    """加载 TXT 文档并返回文档内容(适配LangChain分块格式)"""
    try:
        # 使用LangChain的文本加载器,兼容后续分块流程
        loader = TextLoader(txt_path, encoding="utf-8")
        documents = loader.load()
        print(f"成功加载 TXT,共 {len(documents)} 段")
        return documents
    except Exception as e:
        print(f"加载 TXT 失败:{e}")
        return None

2. 文本切割。

因为大模型的输入有限,不能一次性大段输入文本。这样就算没超出文本限制,也会导致token费用过多。先将长文本切割为文本块(chunks)。

# 文本分割
def split_documents(documents):
    """将文档分割为文本块"""
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,          # 每个文本块的最大字符数
        chunk_overlap=50,        # 文本块之间的重叠字符数(保持上下文连贯)
        length_function=len,     # 长度计算函数
        separators=["\n\n", "\n", "。", "!", "?", ",", "、", " "]  # 分割符优先级
    )
    splits = text_splitter.split_documents(documents)
    print(f"文档分割完成,共生成 {len(splits)} 个文本块")
    return splits

上面这是ai告诉我的默认切割方式,但是其效果十分拉胯,因为是按照字符长度来固定切分的,导致每个语义块都可能被切割开。我需要将其优化一下,先做一步预操作,将文本按照语义用“##”分割,然后使用代码切开。

def split_documents(documents):
    """
    按 ## 分割文档(简单、稳定、绝不乱切)
    """
    splits = []

    for doc in documents:
        # 核心:按 ## 把文本切开
        chunks = doc.page_content.split("##")

        for chunk in chunks:
            chunk = chunk.strip()  # 去掉多余空格/换行
            if len(chunk) > 10:  # 过滤空块
                new_doc = doc.copy()
                new_doc.page_content = chunk
                splits.append(new_doc)

    print(f"✅ 按 ## 分割完成,共生成 {len(splits)} 个文本块")
    return splits

这样的效果要好得多。

  1. 向量库创建。

向量库本身也是一种数据库,向量库存储数据的本质,每条数据由三个关键部分组成: 向量、原始文本串、元数据

image.png

所谓的向量,也就是让文本向量化。这个任务由嵌入模型负责,他的工作是经过训练后,使得语义相似的文本在向量空间中距离更近,反之更远。

Snipaste_2026-03-23_21-52-24.png

余弦相似定理的值越小,那么其文本越相似。

当我们取出向量数据库中的数据使用时,用户问题同样被转化为查询向量,数据库通过索引快速找到最相似的k个文本块ID,这里的k由我们设置。根据文本块ID,取出对应的文本块和元数据返还。

注意:无论是客户提问的大语言模型,还是创建向量数据库的模型,都可以选择使用远程调用,也可以将模型下载到本地调用,推荐后者,省去了网络请求的损耗和可能出现的网络问题。

# 创建向量库
def create_vector_db(splits):
    """创建 FAISS 向量数据库(使用本地 HuggingFace 嵌入模型)"""
    # 使用本地 sentence-transformers 模型(首次运行会自动下载,之后使用缓存)
    model_name = "sentence-transformers/all-MiniLM-L6-v2"
    embeddings = HuggingFaceEmbeddings(
        model_name="sentence-transformers/all-MiniLM-L6-v2",
        model_kwargs={"local_files_only": True}  # 强制本地模型
    )
    # 将文本块存入 FAISS 向量库
    vector_db = FAISS.from_documents(documents=splits, embedding=embeddings)
    vector_db.save_local("./faiss_db")
    print("向量数据库创建完成(使用本地嵌入模型)")
    return vector_db

tips:使用huggingface下载远端模型太慢,可以改变其来源。

os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'

从镜像网站中下载模型。

问题

暴露的一些问题,例如人工员工是可以根据照片和视觉信息给予反馈的,但是ai目前就不行了,只能根据文本来。

image.png

  1. 构建答疑链
# rag答疑链
def build_rag_chain(vector_db):
    """构建检索增强生成的问答链"""
    # 1. 创建检索器(返回最相关的3个文本块)
    retriever = vector_db.as_retriever(search_kwargs={"k": 2})

    # 加上这段!测试检索到了什么
    test_docs = retriever.invoke("怎么使用一起听功能")
    print("====检索到的内容====")
    for d in test_docs:
        print(d.page_content)
    # ----------------------


    # 2. 定义提示模板
    prompt_template = ChatPromptTemplate.from_template("""
    请根据以下提供的上下文信息回答用户的问题。
    如果你无法从上下文中找到答案,请明确说明"无法从知识库中找到相关答案",不要编造信息。

    上下文信息:
    {context}

    用户问题:
    {question}
    """)

    # 3. 初始化大模型
    llm = ChatOpenAI(model="deepseek-chat",
                     temperature=0,
                     api_key=os.getenv("DEEPSEEK_API_KEY"),  # DeepSeek API Key,
                     base_url="https://api.deepseek.com/v1"  # DeepSeek API 基础地址
    )

    # 4. 构建 RAG 链,核心,链式语法是langchain的特色
    rag_chain = (
            {"context": retriever, "question": RunnablePassthrough()}  # 检索上下文 + 传递用户问题
            | prompt_template  # 格式化提示
            | llm  # 调用大模型
            | StrOutputParser()  # 解析输出为字符串
    )

    return rag_chain

注意链式语法是langchain的特色,必须要熟悉。 其实有的同学可能看出来了,说是rag这个特有名词,其实本质上是一个检索相关语义块+构建prompt让模型来做开卷题,而其中的核心技术细节其实是检索chunks,这里做的约精细,那么效果越准确。

最终测试:

image.png