什么是RAG?

RAG(Retrieval-Augmented Generation,检索增强生成)是当前自然语言处理领域的重要技术范式,它通过结合信息检索和文本生成的能力,让大语言模型能够基于特定知识库生成准确、可靠的答案。与传统的纯生成模型相比,RAG系统具有以下优势:

  • 知识实时性:可以访问最新或特定领域的知识

  • 答案可追溯:每个回答都有明确的来源依据

  • 减少幻觉:大幅降低模型编造信息的可能性

  • 成本效益:不需要为每个特定任务重新训练模型

RAG系统核心组件

一个完整的RAG系统通常包含以下关键组件:

1. 文档加载与处理

from langchain_community.document_loaders import TextLoader

def load_text_file(file_path: str) -> list[Document]:
    text_loader = TextLoader(file_path, encoding="utf-8")
    documents = text_loader.load()
    return documents

文档加载是RAG流水线的第一步。我们使用LangChain的TextLoader来加载文本文件,支持UTF-8编码确保中文文本正确处理。

2. 文本分割策略

from langchain_text_splitters import RecursiveCharacterTextSplitter

def split_documents(documents: list[Document],
                    split_way: list[str] = None,
                    chunk_size: int = 800,
                    chunk_overlap: int = 200) -> list[str]:
    # 实现文本分割逻辑

文本分割对RAG性能至关重要。合适的chunk大小和重叠策略能平衡信息的完整性和检索的精准度。实践中,我们使用递归字符文本分割器,支持自定义分隔符。

3. 向量化与检索

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

emb = OpenAIEmbeddings(
    model="Qwen/Qwen3-Embedding-8B",
    openai_api_base="https://api.siliconflow.cn/v1",
    openai_api_key="your-api-key",
)

vector_store = FAISS.from_texts(split_docs, emb)

我们使用FAISS作为向量数据库,它提供了高效的相似性搜索能力。结合强大的Qwen嵌入模型,将文本转换为高维向量表示。

4. 智能生成与提示工程

prompt = PromptTemplate.from_template(
    "- Role: 人事客服助理"
    "- Background: 用户在寻求与人事相关的问题解答..."
    # 详细的提示词设计
)

提示工程是RAG系统的灵魂。我们设计了详细的角色设定、背景描述、技能要求和约束条件,确保模型生成符合预期的回答。

完整RAG流水线实现

chain = (
        {
            "docs": vector_store.as_retriever() | (lambda rag_docs: display_rag_result(rag_docs, False)),
            "question": RunnablePassthrough()
        }
        | prompt
        | llm
        | StrOutputParser()
)

这个链式调用实现了完整的RAG流程:

  1. 检索相关文档片段

  2. 组合检索结果和用户问题

  3. 使用精心设计的提示词

  4. 调用语言模型生成答案

  5. 解析输出格式

实战应用:人事问答系统

基于上述技术,我们构建了一个人事管理问答系统。系统能够准确回答关于公司政策、流程和规章制度的问题:

# 使用示例
result = chain.invoke("员工入职多久内必须签订劳务合同?")
print(result)

系统严格遵循"不知道"原则:当参考信息中没有相关内容时,会明确告知用户无法回答,而不是编造信息。

然而运行结果,往往不尽人意

  • 有时无法获取到正确的答案

  • 在对分块chunk_size做了大小调整,增加topK数值后,终于可以正确回答

性能优化建议

1. 分块策略优化:平衡信息的完整性与检索精度

分块是RAG的基础,直接影响检索质量。

  • 语义完整性:对于技术文档或法律条文,应保持逻辑段落的完整性(如一个完整的步骤、一个条款),可适当增大 chunk_size (如 800-1200 tokens)。

  • 事实精准性:对于问答对或知识条目,应采用更小的 chunk_size (如 200-500 tokens) 来确保检索到最相关的事实片段,减少噪声。

  • 高级策略:超越简单的递归分割,尝试按标题分层、按语义分割或使用LLM辅助分割,确保块边界在语义上合理。

2. 检索优化:提升相关文档的召回率

  • 相似度算法:根据任务特性选择算法。cosine 相似度通用性最好;max marginal relevance (MMR) 在保证相关性的同时兼顾多样性,避免返回多个重复内容的片段。

  • 检索阈值:设置相似度分数或相关度阈值,过滤掉低置信度的检索结果,减少无关上下文对生成环节的干扰。

  • 元数据过滤:为文档块添加元数据(如来源章节、文档类型、日期),检索时结合元数据过滤器进行混合查询,实现更精准的筛选。

3. 重排序(Re-Ranking):精排检索结果,提升TOP-K质量

这是连接“粗筛”检索和“精加工”生成的关键桥梁,能显著改善答案质量。

  • 问题所在:向量检索返回的TOP-K文档,可能包含与问题语义相似但实际不相关的片段(例如,都提到“合同”但讨论的是不同方面)。直接将这些全部塞给LLM会引入噪声。

  • 解决方案:使用一个专门的、更精细的重排序模型对初步检索到的文档列表进行重新打分和排序。

  • 如何实现
    • 使用交叉编码器模型:相比双塔式向量模型(快速但精度相对较低),交叉编码器(Cross-Encoder)将问题和每个文档片段进行深度交互计算,能得到更精确的相关性分数,但计算成本更高。

    • 工作流向量检索 -> 返回Top N个候选片段 (e.g., N=20) -> 重排序模型精排 -> 选择Top K个最相关的片段 (e.g., K=5) -> 填入Prompt

    • 推荐工具
      • LangChain集成:使用 LangChainContextualCompressionRetrieverCrossEncoderReranker(例如基于BAAI/bge-reranker系列或Cohere的在线重排序API)。

      • 代码示例
        from langchain.retrievers import ContextualCompressionRetriever
        from langchain.retrievers.document_compressors import CrossEncoderReranker
        from langchain_community.utilities import BingSearchAPIWrapper
        from langchain_community.cross_encoders import HuggingFaceCrossEncoder
        
        # 初始化重排序模型
        model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-large")
        compressor = CrossEncoderReranker(model=model, top_n=5) # 精排后只保留Top5
        # 包装原有的检索器
        compression_retriever = ContextualCompressionRetriever(
            base_compressor=compressor,
            base_retriever=vector_store.as_retriever(search_kwargs={"k": 20}) # 先检索20个
        )
        # 然后在chain中使用 compression_retriever 代替原来的 retriever
        

4. 提示词优化:精确引导LLM生成高质量答案

  • 结构化提示:采用清晰的模块化结构(如Role/Context/Task/Constraints),便于LLM理解和遵循。

  • 少样本学习:在提示词中包含几个高质量的输入-输出示例(Few-shot Learning),明确教导LLM所需的回答格式和风格。

  • 强制约束:在提示词中明确强调“仅根据上下文回答”和“拒绝回答未知问题”,并使用XMLJSON等格式来结构化输出,减少模型幻觉。

5. 多检索器融合:结合不同检索算法的优势

  • 混合检索:将向量检索(擅长语义匹配)和关键词检索(如BM25,擅长精确术语匹配)的结果进行融合,综合两者的召回优势。例如,使用LangChainEnsembleRetriever

  • 工作流融合:可以并行执行多种检索策略,然后对结果集进行去重、加权或使用重排序模型统一排序,再输入给LLM。

6. 迭代与评估:构建优化闭环

  • 建立评估体系:定义关键指标(如答案准确性、相关性、忠实度)并构建一个包含多种问题的测试集。

  • 持续迭代:任何优化策略(调整分块参数、更换模型、修改提示词)都应通过评估集进行量化评估,用数据驱动决策,形成一个“优化-评估-迭代”的闭环。

完整代码示例

from typing import List, Type

from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_text_splitters import RecursiveCharacterTextSplitter

emb = OpenAIEmbeddings(
    model="Qwen/Qwen3-Embedding-8B",
    openai_api_base="https://api.siliconflow.cn/v1",
    openai_api_key="your-api-key",
)

llm = ChatOpenAI(
    model="deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
    openai_api_base="https://api.siliconflow.cn",
    openai_api_key="your-api-key",
    temperature=0.1,  # 控制生成多样性
    max_tokens=2000  # 限制输出长度
)


# 加载文本文件
def load_text_file(file_path: str) -> list[Document]:
    text_loader = TextLoader(file_path, encoding="utf-8")
    documents = text_loader.load()
    return documents


# 文本分割
def split_documents(documents: list[Document],
                    split_ways: list[str] = None,
                    chunk_size: int = 800,
                    chunk_overlap: int = 200) -> list[str]:
    """
    将文档切分为多个文本块
    Args:
        documents: 需要切分的文档列表
        split_ways: 分隔符列表,默认使用RecursiveCharacterTextSplitter的默认分隔符
        chunk_size: 每个文本块的最大长度
        chunk_overlap: 文本块之间的重叠长度
    Returns:
        切分后的文本字符串列表
    """
    if split_ways is None:
        # 使用RecursiveCharacterTextSplitter的默认分隔符
        separators = None
    else:
        separators = split_ways

    text_splitter = RecursiveCharacterTextSplitter(
        separators=separators,
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap
    )

    split_documents = text_splitter.split_documents(documents)
    res = []
    for doc in split_documents:
        content = doc.page_content
        for sep in separators:
            content = content.replace(sep, "")
            res.append(content)
    return res


def display_rag_result(search_docs, is_show_log=True) -> str:
    """
    显示RAG检索结果并返回合并的上下文内容
    
    参数:
        search_docs: 检索到的文档列表,每个文档应包含page_content属性
        is_show_log: 是否显示日志信息,默认为True
    
    返回值:
        str: 合并后的文档内容字符串
    """
    context = ''
    # 遍历检索到的文档,构建合并的上下文内容
    for doc in search_docs:
        if is_show_log:
            print("-" * 50)
            print(doc.page_content)
        # 将当前文档内容追加到上下文字符串中
        if context:
            context += "\n" + doc.page_content
        else:
            context = doc.page_content
    return context


if __name__ == '__main__':
    # 加载文本文件
    docs = load_text_file("公司人事管理流程章程.txt")
    split_way = ["---#*split*#---"]
    # 文本分割
    split_docs = split_documents(docs, split_way, 300, 100)
    # 向量化存储
    vector_store = FAISS.from_texts(split_docs, emb)
    # 定义提示词
    prompt = PromptTemplate.from_template(
        "- Role: 人事客服助理"
        "- Background: 用户在寻求与人事相关的问题解答,需要基于提供的参考信息({docs})获取准确答案。用户可能正在处理与人力资源相关的事务,如招聘、员工关系、薪酬福利等,需要明确、准确的信息来解决问题。"
        "- Profile: 你是一位经验丰富的人事客服助理,对人力资源管理的各个模块有着扎实的了解,熟悉招聘流程、员工关系维护、薪酬福利政策等。你擅长从提供的文件或信息中快速提取关键内容,并以简洁明了的方式回答用户的问题。"
        "- Skills: 你具备快速阅读和理解文件的能力,能够准确识别与问题相关的关键信息。你擅长逻辑分析,能够将复杂的人事政策或流程简化为易于理解的答案。同时,你具备良好的沟通能力,能够以礼貌、专业的态度回应用户。"
        "- Goals: 基于提供的参考信息({docs}),准确回答用户的问题({question}),确保回答内容严格遵循参考信息,不使用任何其他信息。如果无法从参考信息中找到答案,明确告知用户“不知道”,不编造答案。"
        "- Constrains: 你只能使用提供的参考信息({docs})作为回答的依据,不能使用任何外部资源或编造答案。如果参考信息中没有相关内容,必须明确告知用户“不知道”。"
        "- OutputFormat: 以简洁明了的语言回答用户的问题,确保回答内容直接、准确。"
        "- Workflow:"
        " 1. 仔细阅读用户提供的参考信息({docs}),提取与问题({question})相关的关键内容。"
        " 2. 分析问题({question}),确定其核心要点。"
        " 3. 根据提取的关键内容,结合问题的核心要点,给出准确的回答。如果参考信息中没有相关内容,明确告知用户“不知道”。"
        "- Examples:"
        " - 例子1:"
        "   - 参考信息::公司规定,员工每月可享受3天带薪病假。"
        "   - 问题::员工每月可以享受多少天带薪病假?"
        "   - 回答:根据公司规定,员工每月可享受3天带薪病假。"
        " - 例子2:"
        "   - 参考信息::公司目前没有关于远程工作的政策。"
        "   - 问题:公司是否有远程工作的政策?"
        "   - 回答:不知道。"
    )

    chain = (
            {
                "docs": vector_store.as_retriever(search_kwargs={"k": 10})
                        | (lambda rag_docs: display_rag_result(rag_docs, False)),
                "question": RunnablePassthrough()
            }
            | prompt
            | llm
            | StrOutputParser()
    )

    print("正在生成答案...")
    result = chain.invoke("员工入职多久内必须签订劳务合同?")
    print("..." * 100)

    print(result)

总结

RAG技术为构建领域特定的智能问答系统提供了强大而灵活的框架。通过合理的组件选择和参数调优,我们可以创建出既准确又可靠的AI助手。本文展示的实现方案使用了流行的LangChain框架和高效的FAISS向量数据库,为开发者提供了一个可扩展的RAG系统基础架构。

随着大语言模型和嵌入技术的不断发展,RAG系统的性能和应用场景将会进一步扩展,为各行各业带来更智能的知识管理和问答解决方案。


注:本文代码示例中的API密钥仅为演示用途,实际使用时请替换为您自己的密钥,并确保遵循相关API的使用条款和安全性要求。

下一篇将引入重排序模型,提升Rag准确度

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐