08-RAG技术详解:让大模型拥有"外挂知识库"

48 阅读9分钟

RAG技术详解:让大模型拥有"外挂知识库"

理解检索增强生成的原理与实践,让大模型访问最新、最准确的信息。

前言

大语言模型虽然强大,但存在一个致命缺陷:知识截止。GPT-4的知识只到2023年,无法回答关于最新事件的问题。而且,模型可能对某些专业知识了解不足。

**RAG(Retrieval-Augmented Generation,检索增强生成)**就是为了解决这个问题而生——让大模型能够"查阅资料"后再回答问题。


一、RAG的核心概念

为什么需要RAG?

大模型的局限性:

1. 知识截止
   用户:2024年奥运会金牌榜是怎样的?
   模型:我的知识截止于2023年... ❌

2. 幻觉问题
   用户:介绍一下《时间简史》这本书
   模型:《时间简史》是霍金写的...作者是张三 ❌(错误信息)

3. 私有数据
   用户:根据公司内部文档回答这个问题
   模型:我无法访问您的私有数据 ❌

4. 专业领域
   用户:这个医学影像显示什么问题?
   模型:我不是医学专家... ❌

RAG的解决思路

RAG工作流程:

用户提问
    ↓
┌─────────────────┐
│   检索阶段       │  在知识库中搜索相关内容
└────────┬────────┘
         ↓
┌─────────────────┐
│   增强阶段       │  将检索结果与问题组合
└────────┬────────┘
         ↓
┌─────────────────┐
│   生成阶段       │  大模型基于上下文生成回答
└────────┬────────┘
         ↓
    最终回答

RAG vs 微调

维度RAG微调
知识更新实时更新知识库需要重新训练
成本较低较高
可解释性可追溯知识来源黑盒
适用场景知识密集型任务特定能力学习
私有数据天然支持需要训练数据

二、RAG架构详解

基础架构

┌─────────────────────────────────────────────────────────────┐
│                      RAG系统架构                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    离线阶段                          │   │
│  │  文档 → 分块 → 向量化 → 存入向量数据库                 │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    在线阶段                          │   │
│  │                                                     │   │
│  │  用户问题 ─→ 向量化 ─→ 向量检索 ─→ 获取相关文档      │   │
│  │                                        ↓            │   │
│  │  问题 + 相关文档 ─→ Prompt构建 ─→ LLM生成回答        │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

代码实现

from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.llms import OpenAI
from langchain.chains import RetrievalQA

class RAGSystem:
    def __init__(self, documents, embedding_model="text-embedding-ada-002"):
        self.embeddings = OpenAIEmbeddings(model=embedding_model)
        self.llm = OpenAI(temperature=0)
        self.vectorstore = None
        self.documents = documents

    def build_index(self, chunk_size=1000, chunk_overlap=200):
        """构建向量索引"""
        # 文档分块
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap
        )
        chunks = text_splitter.split_documents(self.documents)

        # 创建向量存储
        self.vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=self.embeddings
        )

        print(f"已索引 {len(chunks)} 个文档块")
        return self.vectorstore

    def query(self, question, k=4):
        """查询并生成回答"""
        # 检索相关文档
        docs = self.vectorstore.similarity_search(question, k=k)

        # 构建Prompt
        context = "\n\n".join([doc.page_content for doc in docs])
        prompt = f"""根据以下参考资料回答问题。如果资料中没有相关信息,请说"根据现有资料无法回答"。

参考资料:
{context}

问题:{question}

回答:"""

        # 生成回答
        response = self.llm(prompt)

        return {
            "answer": response,
            "sources": docs
        }

# 使用示例
from langchain.document_loaders import TextLoader

# 加载文档
loader = TextLoader("knowledge_base.txt")
documents = loader.load()

# 创建RAG系统
rag = RAGSystem(documents)
rag.build_index()

# 查询
result = rag.query("什么是机器学习?")
print(f"回答:{result['answer']}")
print(f"来源:{len(result['sources'])} 个文档块")

三、文档处理与分块

分块策略

from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    CharacterTextSplitter,
    TokenTextSplitter,
    MarkdownHeaderTextSplitter
)

# 策略1:递归字符分块(推荐)
recursive_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)

# 策略2:按Token分块
token_splitter = TokenTextSplitter(
    chunk_size=500,
    chunk_overlap=50
)

# 策略3:Markdown按标题分块
markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[
        ("#", "header1"),
        ("##", "header2"),
        ("###", "header3")
    ]
)

# 策略4:语义分块(按句子边界)
def semantic_chunk(text, min_chunk_size=100, max_chunk_size=500):
    """按语义边界分块"""
    import re
    # 按句子分割
    sentences = re.split(r'[。!?\n]', text)

    chunks = []
    current_chunk = ""

    for sentence in sentences:
        if len(current_chunk) + len(sentence) < max_chunk_size:
            current_chunk += sentence
        else:
            if len(current_chunk) >= min_chunk_size:
                chunks.append(current_chunk)
            current_chunk = sentence

    if current_chunk:
        chunks.append(current_chunk)

    return chunks

分块最佳实践

分块考虑因素:

1. 块大小
   ├── 太小:语义不完整
   └── 太大:检索精度下降

   推荐:500-1500字符

2. 块重叠
   ├── 避免关键信息被截断
   └── 推荐重叠:10-20%

3. 分块边界
   ├── 自然语言:按段落/句子
   ├── 代码:按函数/类
   └── Markdown:按标题层级

4. 元数据
   ├── 来源文件名
   ├── 页码/位置
   └── 创建时间

四、向量数据库

主流向量数据库对比

数据库特点适用场景
Pinecone全托管,易用生产环境
Milvus开源,高性能大规模部署
Chroma轻量,本地运行开发测试
Weaviate支持混合检索复杂查询
FAISSMeta开源,纯向量检索研究原型
QdrantRust实现,高性能生产环境

向量检索原理

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

def vector_search(query_embedding, document_embeddings, top_k=5):
    """
    向量相似度检索

    query_embedding: 查询向量 (d,)
    document_embeddings: 文档向量矩阵 (n, d)
    """
    # 计算余弦相似度
    similarities = cosine_similarity(
        [query_embedding],
        document_embeddings
    )[0]

    # 获取top-k索引
    top_indices = np.argsort(similarities)[-top_k:][::-1]

    return top_indices, similarities[top_indices]

# 示例
query = np.array([0.1, 0.2, 0.3])
docs = np.array([
    [0.1, 0.2, 0.35],  # 相似度最高
    [0.5, 0.6, 0.7],   # 相似度较低
    [0.15, 0.25, 0.32] # 相似度较高
])

indices, scores = vector_search(query, docs, top_k=2)
print(f"最相似的文档索引: {indices}")
print(f"相似度分数: {scores}")

混合检索

def hybrid_search(query, vectorstore, keyword_index, alpha=0.5, k=10):
    """
    混合检索:向量检索 + 关键词检索

    alpha: 向量检索权重 (0-1)
    """
    # 向量检索
    vector_results = vectorstore.similarity_search(query, k=k*2)
    vector_scores = {r.id: r.score for r in vector_results}

    # 关键词检索 (BM25)
    keyword_results = keyword_index.search(query, k=k*2)
    keyword_scores = {r.id: r.score for r in keyword_results}

    # 分数归一化
    vector_scores = normalize_scores(vector_scores)
    keyword_scores = normalize_scores(keyword_scores)

    # 加权融合
    all_ids = set(vector_scores.keys()) | set(keyword_scores.keys())
    final_scores = {}

    for doc_id in all_ids:
        v_score = vector_scores.get(doc_id, 0)
        k_score = keyword_scores.get(doc_id, 0)
        final_scores[doc_id] = alpha * v_score + (1 - alpha) * k_score

    # 排序返回
    sorted_ids = sorted(final_scores.keys(),
                        key=lambda x: final_scores[x],
                        reverse=True)

    return sorted_ids[:k]

def normalize_scores(scores):
    """分数归一化到0-1"""
    if not scores:
        return scores
    max_score = max(scores.values())
    min_score = min(scores.values())
    if max_score == min_score:
        return {k: 1.0 for k in scores}
    return {k: (v - min_score) / (max_score - min_score)
            for k, v in scores.items()}

五、高级RAG技术

1. 多查询检索

def multi_query_retrieval(query, llm, vectorstore, n_queries=3):
    """
    多查询检索:生成多个相关查询,扩展检索范围
    """
    # 生成多个查询变体
    prompt = f"""请生成{ n_queries}个与以下问题语义相似但表述不同的问题:

原始问题:{query}

要求:
1. 保持核心语义不变
2. 使用不同的表述方式
3. 每行一个问题

问题列表:"""

    response = llm(prompt)
    queries = [query] + response.strip().split('\n')

    # 对每个查询进行检索
    all_docs = []
    for q in queries:
        docs = vectorstore.similarity_search(q, k=3)
        all_docs.extend(docs)

    # 去重
    unique_docs = list({doc.id: doc for doc in all_docs}.values())

    return unique_docs

2. 重排序

from sentence_transformers import CrossEncoder

def rerank_results(query, documents, top_k=5):
    """
    使用Cross-Encoder重排序检索结果
    """
    # 加载重排序模型
    reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

    # 构建查询-文档对
    pairs = [(query, doc.page_content) for doc in documents]

    # 计算相关性分数
    scores = reranker.predict(pairs)

    # 按分数排序
    scored_docs = list(zip(documents, scores))
    scored_docs.sort(key=lambda x: x[1], reverse=True)

    return [doc for doc, score in scored_docs[:top_k]]

3. 自查询检索

def self_query_retrieval(query, llm, vectorstore):
    """
    自查询:从问题中提取过滤条件
    """
    prompt = f"""分析以下问题,提取检索相关信息:

问题:{query}

请输出JSON格式:
{{
    "search_query": "核心检索内容",
    "filters": {{
        "author": "作者名(如果有)",
        "date_range": {{"start": "开始日期", "end": "结束日期"}},
        "category": "分类(如果有)"
    }}
}}"""

    response = llm(prompt)
    parsed = parse_json(response)

    # 使用过滤条件检索
    results = vectorstore.similarity_search(
        parsed["search_query"],
        filter=parsed.get("filters", {})
    )

    return results

4. 知识图谱增强RAG

def graph_enhanced_rag(query, vectorstore, knowledge_graph, llm):
    """
    结合知识图谱的RAG
    """
    # 1. 向量检索
    vector_docs = vectorstore.similarity_search(query, k=5)

    # 2. 从知识图谱中检索相关实体和关系
    entities = extract_entities(query)
    graph_info = []

    for entity in entities:
        # 获取实体的邻居节点和关系
        neighbors = knowledge_graph.get_neighbors(entity)
        relations = knowledge_graph.get_relations(entity)
        graph_info.append({
            "entity": entity,
            "neighbors": neighbors,
            "relations": relations
        })

    # 3. 构建增强Prompt
    context = format_context(vector_docs, graph_info)

    prompt = f"""基于以下信息回答问题:

向量检索结果:
{format_docs(vector_docs)}

知识图谱信息:
{format_graph_info(graph_info)}

问题:{query}

回答:"""

    return llm(prompt)

六、RAG评估

评估指标

RAG评估维度:

1. 检索质量
   ├── 召回率 (Recall)
   ├── 精确率 (Precision)
   └── MRR (Mean Reciprocal Rank)

2. 生成质量
   ├── 准确性:回答是否正确
   ├── 相关性:回答是否切题
   ├── 完整性:信息是否完整
   └── 忠实性:是否基于检索内容

3. 端到端指标
   ├── 响应时间
   └── 用户满意度

评估代码

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_recall,
    context_precision
)

def evaluate_rag(test_data):
    """
    使用RAGAS框架评估RAG系统

    test_data: DataFrame包含
    - question: 问题
    - answer: 生成的回答
    - contexts: 检索的文档
    - ground_truth: 标准答案
    """
    results = evaluate(
        test_data,
        metrics=[
            faithfulness,      # 忠实性
            answer_relevancy,  # 回答相关性
            context_recall,    # 上下文召回率
            context_precision  # 上下文精确率
        ]
    )

    return results

七、RAG最佳实践

实践建议

1. 文档处理
   ├── 选择合适的分块策略
   ├── 保留文档元数据
   └── 定期更新知识库

2. 检索优化
   ├── 使用混合检索
   ├── 添加重排序步骤
   └── 调整chunk数量

3. 提示工程
   ├── 明确指示基于检索内容回答
   ├── 处理"不知道"的情况
   └── 添加引用来源

4. 系统设计
   ├── 缓存常见查询
   ├── 异步处理长文档
   └── 监控检索和生成质量

Prompt模板

rag_prompt_template = """
你是一个专业的问答助手。请根据提供的参考资料回答用户问题。

要求:
1. 仅基于参考资料回答,不要使用外部知识
2. 如果参考资料中没有相关信息,请明确说明
3. 引用具体的参考来源(如[文档1])
4. 回答要简洁、准确、完整

参考资料:
{context}

用户问题:{question}

回答:
"""

小结

组件功能关键技术
文档处理知识库构建分块、清洗、元数据
向量化文本→向量Embedding模型
向量存储向量索引与检索Pinecone、Milvus等
检索找到相关文档向量检索、混合检索
生成生成最终回答LLM + Prompt

思考与练习

  1. 思考题

    • RAG和微调各适合什么场景?
    • 如何选择合适的分块大小?
  2. 动手练习

    • 使用LangChain构建一个简单的RAG系统
    • 尝试不同的分块策略,比较检索效果
  3. 延伸阅读


下期预告

下一篇文章,我们将深入探讨:Agent智能体:大模型的"手"和"脚"

会解答这些问题:

  • 什么是AI Agent?
  • Agent如何规划和执行任务?
  • 有哪些主流的Agent框架?

关注专栏,不错过后续更新!


作者:ECH00O00 本文首发于掘金专栏《AI科普实验室》 欢迎评论区交流讨论,点赞收藏就是最大的鼓励 ❤️