RAG检索增强生成系统构建完全教程

教程简介

RAG(检索增强生成)是将信息检索与大语言模型相结合的核心技术架构。本教程系统讲解RAG系统的完整构建流程,包括文档加载与预处理(PDF/Word/网页/代码)、文本分块策略(固定/语义/递归)、Embedding模型选择与对比、向量数据库选型(Chroma/Milvus/Qdrant)、检索策略(混合检索/重排序)、Prompt模板设计、系统评估指标,以及HyDE、Self-RATE、CRAG等高级RAG技术,最终通过企业知识库问答系统实战案例串联所有知识点。

RAG检索增强生成系统构建完全教程

一、RAG概述与原理

1.1 什么是RAG

RAG(Retrieval-Augmented Generation,检索增强生成)是一种将信息检索与大语言模型(LLM)相结合的技术架构。它的核心思想是:在LLM生成回答之前,先从外部知识库中检索相关信息,然后将这些信息作为上下文提供给LLM,从而生成更准确、更有依据的回答。

RAG解决了一个根本性问题:LLM的知识是静态的,它只能基于训练数据中的信息回答问题。当面对企业内部文档、最新资讯、专业领域知识等训练数据之外的内容时,LLM会产生"幻觉"——编造看似合理但实际错误的答案。RAG通过引入外部知识源,让LLM能够基于真实数据回答问题。

1.2 RAG的核心原理

RAG的工作流程可以概括为三个阶段:

索引阶段(Indexing):离线处理。将原始文档加载、分块、向量化后存入向量数据库。这是RAG系统的基础准备工作。

检索阶段(Retrieval):在线处理。当用户提出问题时,将问题向量化,在向量数据库中搜索最相关的文档片段。

生成阶段(Generation):将检索到的相关文档片段与用户问题组合成Prompt,发送给LLM生成最终回答。

用户问题
    │
    ▼
┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│  文本向量化   │───▶│  向量检索     │───▶│  相关文档     │
│  (Embedding) │    │ (Similarity) │    │  (Top-K)     │
└─────────────┘    └──────────────┘    └──────┬──────┘
                                              │
                                              ▼
                                       ┌──────────────┐
                                       │  Prompt组装    │
                                       │  (Template)    │
                                       └──────┬───────┘
                                              │
                                              ▼
                                       ┌──────────────┐
                                       │  LLM生成回答   │
                                       │  (Generation)  │
                                       └──────────────┘

1.3 RAG vs 微调 vs 长上下文

维度 RAG 微调(Fine-tuning) 长上下文(Long Context)
知识更新 实时更新,无需重训 需要重新训练 每次输入
成本 中等 高(token费用)
可解释性 高(可追溯来源)
准确性 高(有据可查) 中(可能过拟合) 中(注意力分散)
适用场景 知识密集型问答 风格/格式迁移 文档分析总结

二、文档加载与预处理

2.1 文档加载器设计

RAG系统需要处理多种格式的文档。设计一个统一的文档加载接口:

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
import hashlib

@dataclass
class Document:
    """统一文档格式"""
    content: str           # 文本内容
    metadata: dict         # 元数据(来源、页码、时间等)
    doc_id: Optional[str] = None  # 文档唯一标识

    def __post_init__(self):
        if not self.doc_id:
            self.doc_id = hashlib.md5(
                self.content.encode()
            ).hexdigest()

class BaseLoader(ABC):
    """文档加载器基类"""

    @abstractmethod
    def load(self, source: str) -> list[Document]:
        """加载文档,返回Document列表"""
        pass

    @abstractmethod
    def supported_extensions(self) -> list[str]:
        """返回支持的文件扩展名"""
        pass

2.2 PDF文档加载

import fitz  # PyMuPDF

class PDFLoader(BaseLoader):
    def __init__(self, extract_images: bool = False):
        self.extract_images = extract_images

    def supported_extensions(self):
        return [".pdf"]

    def load(self, source: str) -> list[Document]:
        documents = []
        pdf = fitz.open(source)

        for page_num in range(len(pdf)):
            page = pdf[page_num]
            text = page.get_text("text")

            # 基本清理
            text = self._clean_text(text)

            if text.strip():
                documents.append(Document(
                    content=text,
                    metadata={
                        "source": source,
                        "page": page_num + 1,
                        "total_pages": len(pdf),
                        "format": "pdf"
                    }
                ))

            # 提取图片描述(如果启用)
            if self.extract_images:
                images = page.get_images()
                for img_idx, img in enumerate(images):
                    img_text = self._extract_image_text(page, img)
                    if img_text:
                        documents.append(Document(
                            content=img_text,
                            metadata={
                                "source": source,
                                "page": page_num + 1,
                                "type": "image_description"
                            }
                        ))

        pdf.close()
        return documents

    def _clean_text(self, text: str) -> str:
        """清理文本:去除多余空白、修复断行"""
        import re
        # 合并多个空格
        text = re.sub(r'\s+', ' ', text)
        # 修复被断开的单词
        text = re.sub(r'(\w)-\s+(\w)', r'\1\2', text)
        return text.strip()

2.3 Word文档加载

from docx import Document as DocxDocument

class WordLoader(BaseLoader):
    def supported_extensions(self):
        return [".docx"]

    def load(self, source: str) -> list[Document]:
        documents = []
        doc = DocxDocument(source)

        current_section = ""
        for para in doc.paragraphs:
            # 检测标题层级
            if para.style.name.startswith("Heading"):
                level = para.style.name.replace("Heading ", "")
                current_section = para.text

            if para.text.strip():
                documents.append(Document(
                    content=para.text,
                    metadata={
                        "source": source,
                        "section": current_section,
                        "style": para.style.name,
                        "format": "docx"
                    }
                ))

        # 处理表格
        for table_idx, table in enumerate(doc.tables):
            table_text = self._table_to_text(table)
            documents.append(Document(
                content=table_text,
                metadata={
                    "source": source,
                    "type": "table",
                    "table_index": table_idx,
                    "format": "docx"
                }
            ))

        return documents

    def _table_to_text(self, table) -> str:
        """将表格转换为结构化文本"""
        rows = []
        headers = [cell.text.strip() for cell in table.rows[0].cells]
        rows.append(" | ".join(headers))
        rows.append("-" * len(rows[0]))

        for row in table.rows[1:]:
            cells = [cell.text.strip() for cell in row.cells]
            rows.append(" | ".join(cells))

        return "\n".join(rows)

2.4 网页内容加载

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse

class WebLoader(BaseLoader):
    def __init__(self, timeout: int = 30):
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (RAG-Bot/1.0)"
        })

    def supported_extensions(self):
        return [".html", ".htm"]

    def load(self, url: str) -> list[Document]:
        response = self.session.get(url, timeout=self.timeout)
        response.raise_for_status()

        soup = BeautifulSoup(response.text, "html.parser")

        # 移除脚本和样式
        for tag in soup(["script", "style", "nav", "footer", "header"]):
            tag.decompose()

        # 提取主要内容
        main_content = (
            soup.find("main") or
            soup.find("article") or
            soup.find("div", class_="content") or
            soup.find("body")
        )

        if not main_content:
            return []

        # 分段提取
        documents = []
        current_heading = ""

        for element in main_content.find_all(["h1", "h2", "h3", "p", "li"]):
            if element.name.startswith("h"):
                current_heading = element.get_text(strip=True)
            elif element.get_text(strip=True):
                documents.append(Document(
                    content=element.get_text(strip=True),
                    metadata={
                        "source": url,
                        "heading": current_heading,
                        "tag": element.name,
                        "format": "html"
                    }
                ))

        return documents

    def load_sitemap(self, sitemap_url: str) -> list[Document]:
        """加载站点地图中的所有页面"""
        response = self.session.get(sitemap_url)
        soup = BeautifulSoup(response.text, "xml")

        all_docs = []
        for loc in soup.find_all("loc"):
            url = loc.text.strip()
            try:
                docs = self.load(url)
                all_docs.extend(docs)
            except Exception as e:
                print(f"加载失败 {url}: {e}")

        return all_docs

2.5 代码文件加载

import ast
import re

class CodeLoader(BaseLoader):
    def __init__(self):
        self.language_map = {
            ".py": "python", ".js": "javascript",
            ".ts": "typescript", ".java": "java",
            ".go": "go", ".rs": "rust",
        }

    def supported_extensions(self):
        return list(self.language_map.keys())

    def load(self, source: str) -> list[Document]:
        import os
        ext = os.path.splitext(source)[1]
        language = self.language_map.get(ext, "unknown")

        with open(source, "r", encoding="utf-8") as f:
            code = f.read()

        documents = []

        # 提取模块级文档
        if ext == ".py":
            module_doc = self._extract_python_docstring(code)
            if module_doc:
                documents.append(Document(
                    content=module_doc,
                    metadata={
                        "source": source,
                        "type": "module_doc",
                        "language": language
                    }
                ))

        # 按函数/类分块
        chunks = self._split_by_function(code, language)
        for chunk in chunks:
            documents.append(Document(
                content=chunk["code"],
                metadata={
                    "source": source,
                    "type": "function",
                    "name": chunk["name"],
                    "language": language
                }
            ))

        return documents

    def _extract_python_docstring(self, code: str) -> str:
        try:
            tree = ast.parse(code)
            return ast.get_docstring(tree) or ""
        except SyntaxError:
            return ""

    def _split_by_function(self, code: str, language: str) -> list[dict]:
        if language == "python":
            return self._split_python_functions(code)
        # 其他语言使用正则分块
        return self._split_by_regex(code)

    def _split_python_functions(self, code: str) -> list[dict]:
        chunks = []
        try:
            tree = ast.parse(code)
            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
                    start = node.lineno - 1
                    end = node.end_lineno or start + 1
                    lines = code.splitlines()[start:end]
                    chunks.append({
                        "name": node.name,
                        "code": "\n".join(lines)
                    })
        except SyntaxError:
            pass
        return chunks

三、文本分块策略

3.1 为什么分块很重要

分块(Chunking)是RAG系统中最关键的预处理步骤之一。分块质量直接影响检索精度:

  • 块太大:包含过多无关信息,稀释了关键内容的相关性
  • 块太小:丢失上下文,语义不完整
  • 分块位置不当:在句子中间断开,破坏语义连贯性

3.2 固定大小分块

最简单的分块方式,按固定字符数或token数切分:

class FixedSizeChunker:
    def __init__(self, chunk_size: int = 500, overlap: int = 50):
        self.chunk_size = chunk_size
        self.overlap = overlap

    def chunk(self, text: str) -> list[str]:
        chunks = []
        start = 0

        while start < len(text):
            end = start + self.chunk_size

            # 尝试在句子边界切分
            if end < len(text):
                # 找最近的句号、问号、感叹号
                for punct in ['。', '!', '?', '. ', '! ', '? ']:
                    last_punct = text[start:end].rfind(punct)
                    if last_punct > self.chunk_size * 0.5:
                        end = start + last_punct + len(punct)
                        break

            chunk = text[start:end].strip()
            if chunk:
                chunks.append(chunk)

            # 下一块从overlap位置开始
            start = end - self.overlap

        return chunks

3.3 递归字符分块

更智能的分块方式,按层级分隔符递归切分:

class RecursiveChunker:
    def __init__(
        self,
        chunk_size: int = 500,
        chunk_overlap: int = 50,
        separators: list[str] = None
    ):
        self.chunk_size = chunk_size
        self.chunk_overlap = chunk_overlap
        self.separators = separators or [
            "\n\n",  # 段落
            "\n",    # 换行
            "。",    # 中文句号
            ". ",    # 英文句号
            " ",     # 空格
            ""       # 字符
        ]

    def chunk(self, text: str) -> list[str]:
        return self._recursive_split(text, self.separators)

    def _recursive_split(
        self, text: str, separators: list[str]
    ) -> list[str]:
        if len(text) <= self.chunk_size:
            return [text] if text.strip() else []

        # 选择当前层级的分隔符
        separator = separators[0] if separators else ""
        remaining_separators = separators[1:] if len(separators) > 1 else [""]

        # 按分隔符切分
        if separator:
            splits = text.split(separator)
        else:
            # 无分隔符时按字符切分
            splits = [text[i:i+self.chunk_size]
                      for i in range(0, len(text), self.chunk_size)]

        # 合并小块
        chunks = []
        current = ""

        for split in splits:
            candidate = (
                current + separator + split if current else split
            )

            if len(candidate) <= self.chunk_size:
                current = candidate
            else:
                if current:
                    chunks.append(current)
                # 如果单个split仍然太大,递归处理
                if len(split) > self.chunk_size:
                    sub_chunks = self._recursive_split(
                        split, remaining_separators
                    )
                    chunks.extend(sub_chunks[:-1])
                    current = sub_chunks[-1] if sub_chunks else ""
                else:
                    current = split

        if current:
            chunks.append(current)

        # 添加重叠
        if self.chunk_overlap > 0:
            chunks = self._add_overlap(chunks)

        return chunks

    def _add_overlap(self, chunks: list[str]) -> list[str]:
        overlapped = [chunks[0]]
        for i in range(1, len(chunks)):
            # 从前一块的末尾取overlap个字符
            overlap_text = chunks[i-1][-self.chunk_overlap:]
            # 找到完整的词边界
            space_idx = overlap_text.find(" ")
            if space_idx != -1:
                overlap_text = overlap_text[space_idx+1:]
            overlapped.append(overlap_text + chunks[i])
        return overlapped

3.4 语义分块

基于语义相似度的分块方式,在语义变化点切分:

import numpy as np
from sentence_transformers import SentenceTransformer

class SemanticChunker:
    def __init__(
        self,
        model_name: str = "all-MiniLM-L6-v2",
        threshold: float = 0.5,
        min_chunk_size: int = 100
    ):
        self.model = SentenceTransformer(model_name)
        self.threshold = threshold
        self.min_chunk_size = min_chunk_size

    def chunk(self, text: str) -> list[str]:
        # 先按句子切分
        sentences = self._split_sentences(text)
        if len(sentences) <= 1:
            return [text]

        # 计算句子嵌入
        embeddings = self.model.encode(sentences)

        # 计算相邻句子的相似度
        similarities = []
        for i in range(len(embeddings) - 1):
            sim = np.dot(embeddings[i], embeddings[i+1]) / (
                np.linalg.norm(embeddings[i]) *
                np.linalg.norm(embeddings[i+1])
            )
            similarities.append(sim)

        # 在相似度低于阈值的位置切分
        chunks = []
        current_chunk = [sentences[0]]

        for i, sim in enumerate(similarities):
            if sim < self.threshold:
                chunk_text = " ".join(current_chunk)
                if len(chunk_text) >= self.min_chunk_size:
                    chunks.append(chunk_text)
                    current_chunk = [sentences[i+1]]
                else:
                    current_chunk.append(sentences[i+1])
            else:
                current_chunk.append(sentences[i+1])

        if current_chunk:
            chunks.append(" ".join(current_chunk))

        return chunks

    def _split_sentences(self, text: str) -> list[str]:
        import re
        # 中英文混合句子切分
        sentences = re.split(r'(?<=[。!?.!?])\s*', text)
        return [s.strip() for s in sentences if s.strip()]

3.5 分块策略对比

策略 优点 缺点 适用场景
固定大小 简单快速 可能破坏语义 快速原型、大批量处理
递归字符 保持结构完整 需要调参 通用文档处理
语义分块 语义连贯性好 计算成本高 高质量知识库

四、Embedding模型选择与对比

4.1 Embedding模型概述

Embedding模型将文本转换为固定维度的向量表示,是RAG系统的核心组件。模型选择直接影响检索质量。

4.2 主流模型对比

模型 维度 中文支持 最大Token 特点
OpenAI text-embedding-3-small 1536 8191 性价比高
OpenAI text-embedding-3-large 3072 8191 精度最高
BGE-M3 1024 8192 多语言、开源
GTE-large 1024 8192 中文优化
E5-large-v2 1024 512 轻量高效
Jina-embeddings-v2 768 8192 长文本支持

4.3 使用Embedding模型

from sentence_transformers import SentenceTransformer
import numpy as np

class EmbeddingService:
    def __init__(self, model_name: str = "BAAI/bge-m3"):
        self.model = SentenceTransformer(model_name)
        self.dimension = self.model.get_sentence_embedding_dimension()

    def encode(
        self,
        texts: list[str],
        batch_size: int = 32,
        show_progress: bool = False
    ) -> np.ndarray:
        """批量编码文本为向量"""
        return self.model.encode(
            texts,
            batch_size=batch_size,
            show_progress=show_progress,
            normalize_embeddings=True  # L2归一化
        )

    def encode_query(self, query: str) -> np.ndarray:
        """编码查询(可能需要添加前缀)"""
        # BGE模型建议查询添加前缀
        query = "为这个句子生成表示以用于检索相关文章:" + query
        return self.model.encode(
            [query],
            normalize_embeddings=True
        )[0]

    def similarity(self, vec1: np.ndarray, vec2: np.ndarray) -> float:
        """计算两个向量的余弦相似度"""
        return float(np.dot(vec1, vec2))

4.4 使用OpenAI Embedding API

from openai import OpenAI
import numpy as np

class OpenAIEmbedding:
    def __init__(
        self,
        model: str = "text-embedding-3-small",
        api_key: str = None
    ):
        self.client = OpenAI(api_key=api_key)
        self.model = model

    def encode(self, texts: list[str]) -> np.ndarray:
        """批量编码"""
        response = self.client.embeddings.create(
            model=self.model,
            input=texts
        )
        return np.array([
            item.embedding for item in response.data
        ])

    def encode_with_cache(
        self,
        texts: list[str],
        cache: dict
    ) -> np.ndarray:
        """带缓存的编码"""
        uncached = [t for t in texts if t not in cache]
        if uncached:
            new_embeddings = self.encode(uncached)
            for text, emb in zip(uncached, new_embeddings):
                cache[text] = emb

        return np.array([cache[t] for t in texts])

五、向量数据库选型

5.1 向量数据库概述

向量数据库是RAG系统的存储层,负责高效存储和检索高维向量。选择合适的向量数据库需要考虑:数据规模、查询性能、运维成本、功能特性。

5.2 Chroma - 轻量级首选

import chromadb
from chromadb.config import Settings

class ChromaVectorStore:
    def __init__(
        self,
        collection_name: str = "default",
        persist_directory: str = "./chroma_db"
    ):
        self.client = chromadb.PersistentClient(
            path=persist_directory,
            settings=Settings(anonymized_telemetry=False)
        )
        self.collection = self.client.get_or_create_collection(
            name=collection_name,
            metadata={"hnsw:space": "cosine"}
        )

    def add_documents(
        self,
        documents: list[str],
        embeddings: list[list[float]],
        metadatas: list[dict] = None,
        ids: list[str] = None
    ):
        """添加文档"""
        if ids is None:
            import hashlib
            ids = [hashlib.md5(d.encode()).hexdigest() for d in documents]

        self.collection.add(
            documents=documents,
            embeddings=embeddings,
            metadatas=metadatas,
            ids=ids
        )

    def query(
        self,
        query_embedding: list[float],
        top_k: int = 5,
        filter_dict: dict = None
    ) -> dict:
        """查询相似文档"""
        kwargs = {
            "query_embeddings": [query_embedding],
            "n_results": top_k,
            "include": ["documents", "metadatas", "distances"]
        }
        if filter_dict:
            kwargs["where"] = filter_dict

        return self.collection.query(**kwargs)

    def delete(self, ids: list[str]):
        """删除文档"""
        self.collection.delete(ids=ids)

    def count(self) -> int:
        """返回文档数量"""
        return self.collection.count()

5.3 Milvus - 大规模生产级

from pymilvus import (
    connections, Collection, FieldSchema,
    CollectionSchema, DataType, utility
)

class MilvusVectorStore:
    def __init__(
        self,
        collection_name: str = "default",
        host: str = "localhost",
        port: int = 19530,
        dimension: int = 1024
    ):
        connections.connect(host=host, port=port)
        self.collection_name = collection_name
        self.dimension = dimension
        self._create_collection()

    def _create_collection(self):
        if utility.has_collection(self.collection_name):
            self.collection = Collection(self.collection_name)
            return

        fields = [
            FieldSchema("id", DataType.VARCHAR, is_primary=True, max_length=64),
            FieldSchema("embedding", DataType.FLOAT_VECTOR, dim=self.dimension),
            FieldSchema("content", DataType.VARCHAR, max_length=65535),
            FieldSchema("metadata", DataType.JSON),
        ]
        schema = CollectionSchema(fields)
        self.collection = Collection(self.collection_name, schema)

        # 创建索引
        self.collection.create_index(
            field_name="embedding",
            index_params={
                "metric_type": "COSINE",
                "index_type": "HNSW",
                "params": {"M": 16, "efConstruction": 256}
            }
        )

    def add_documents(
        self,
        ids: list[str],
        embeddings: list[list[float]],
        documents: list[str],
        metadatas: list[dict] = None
    ):
        data = [ids, embeddings, documents, metadatas or [{}] * len(ids)]
        self.collection.insert(data)
        self.collection.flush()

    def query(
        self,
        query_embedding: list[float],
        top_k: int = 5,
        filter_expr: str = None
    ) -> list[dict]:
        self.collection.load()

        search_params = {"metric_type": "COSINE", "params": {"ef": 128}}
        results = self.collection.search(
            data=[query_embedding],
            anns_field="embedding",
            param=search_params,
            limit=top_k,
            output_fields=["content", "metadata"],
            expr=filter_expr
        )

        return [
            {
                "id": hit.id,
                "score": hit.score,
                "content": hit.entity.get("content"),
                "metadata": hit.entity.get("metadata")
            }
            for hit in results[0]
        ]

5.4 Qdrant - 高性能选择

from qdrant_client import QdrantClient
from qdrant_client.models import (
    Distance, VectorParams, PointStruct,
    Filter, FieldCondition, MatchValue
)

class QdrantVectorStore:
    def __init__(
        self,
        collection_name: str = "default",
        host: str = "localhost",
        port: int = 6333,
        dimension: int = 1024
    ):
        self.client = QdrantClient(host=host, port=port)
        self.collection_name = collection_name
        self.dimension = dimension
        self._create_collection()

    def _create_collection(self):
        collections = self.client.get_collections().collections
        names = [c.name for c in collections]

        if self.collection_name not in names:
            self.client.create_collection(
                collection_name=self.collection_name,
                vectors_config=VectorParams(
                    size=self.dimension,
                    distance=Distance.COSINE
                )
            )

    def add_documents(
        self,
        ids: list[str],
        embeddings: list[list[float]],
        documents: list[str],
        metadatas: list[dict] = None
    ):
        points = []
        for i, (id_, emb, doc) in enumerate(zip(ids, embeddings, documents)):
            payload = {"content": doc}
            if metadatas and i < len(metadatas):
                payload.update(metadatas[i])

            points.append(PointStruct(
                id=id_,
                vector=emb,
                payload=payload
            ))

        self.client.upsert(
            collection_name=self.collection_name,
            points=points
        )

    def query(
        self,
        query_embedding: list[float],
        top_k: int = 5,
        filter_dict: dict = None
    ) -> list[dict]:
        query_filter = None
        if filter_dict:
            conditions = [
                FieldCondition(
                    key=k,
                    match=MatchValue(value=v)
                )
                for k, v in filter_dict.items()
            ]
            query_filter = Filter(must=conditions)

        results = self.client.search(
            collection_name=self.collection_name,
            query_vector=query_embedding,
            limit=top_k,
            query_filter=query_filter
        )

        return [
            {
                "id": r.id,
                "score": r.score,
                "content": r.payload.get("content"),
                "metadata": {k: v for k, v in r.payload.items() if k != "content"}
            }
            for r in results
        ]

5.5 选型建议

场景 推荐方案 理由
原型开发/小数据量 Chroma 零配置、嵌入式、Python友好
中等规模生产 Qdrant 高性能、Rust实现、丰富过滤
大规模企业级 Milvus 分布式、高可用、生态完善
云原生/托管服务 Pinecone 全托管、免运维

六、检索策略

6.1 基础相似度检索

最简单的检索方式,直接按向量相似度返回Top-K结果:

class BasicRetriever:
    def __init__(self, vector_store, embedding_service):
        self.vector_store = vector_store
        self.embedding_service = embedding_service

    def retrieve(self, query: str, top_k: int = 5) -> list[dict]:
        query_embedding = self.embedding_service.encode_query(query)
        results = self.vector_store.query(
            query_embedding=query_embedding.tolist(),
            top_k=top_k
        )
        return results

6.2 混合检索

结合向量检索和关键词检索(BM25),综合利用语义匹配和精确匹配的优势:

from rank_bm25 import BM25Okapi
import jieba
import numpy as np

class HybridRetriever:
    def __init__(
        self,
        vector_store,
        embedding_service,
        alpha: float = 0.7  # 向量检索权重
    ):
        self.vector_store = vector_store
        self.embedding_service = embedding_service
        self.alpha = alpha
        self.bm25 = None
        self.corpus = []

    def build_bm25_index(self, documents: list[str]):
        """构建BM25索引"""
        self.corpus = documents
        tokenized = [list(jieba.cut(doc)) for doc in documents]
        self.bm25 = BM25Okapi(tokenized)

    def retrieve(self, query: str, top_k: int = 5) -> list[dict]:
        # 向量检索
        query_embedding = self.embedding_service.encode_query(query)
        vector_results = self.vector_store.query(
            query_embedding=query_embedding.tolist(),
            top_k=top_k * 2
        )

        # BM25检索
        tokenized_query = list(jieba.cut(query))
        bm25_scores = self.bm25.get_scores(tokenized_query)
        bm25_top_indices = np.argsort(bm25_scores)[::-1][:top_k * 2]

        # 分数融合(RRF - Reciprocal Rank Fusion)
        scores = {}
        k = 60  # RRF常数

        for rank, result in enumerate(vector_results):
            doc_id = result["id"]
            scores[doc_id] = scores.get(doc_id, 0) + self.alpha / (k + rank + 1)

        for rank, idx in enumerate(bm25_top_indices):
            doc_id = str(idx)
            scores[doc_id] = scores.get(doc_id, 0) + (1 - self.alpha) / (k + rank + 1)

        # 按融合分数排序
        sorted_ids = sorted(scores.keys(), key=lambda x: scores[x], reverse=True)

        # 返回结果
        id_to_result = {r["id"]: r for r in vector_results}
        results = []
        for doc_id in sorted_ids[:top_k]:
            if doc_id in id_to_result:
                result = id_to_result[doc_id]
                result["hybrid_score"] = scores[doc_id]
                results.append(result)

        return results

6.3 重排序(Reranking)

使用交叉编码器对检索结果进行二次排序,提高精度:

from sentence_transformers import CrossEncoder

class Reranker:
    def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
        self.model = CrossEncoder(model_name)

    def rerank(
        self,
        query: str,
        documents: list[dict],
        top_k: int = 5
    ) -> list[dict]:
        if not documents:
            return []

        # 构造query-document对
        pairs = [(query, doc["content"]) for doc in documents]

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

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

        results = []
        for score, doc in scored_docs[:top_k]:
            doc["rerank_score"] = float(score)
            results.append(doc)

        return results

6.4 多查询检索

通过生成多个查询变体来提高召回率:

class MultiQueryRetriever:
    def __init__(self, retriever, llm_client):
        self.retriever = retriever
        self.llm = llm_client

    def retrieve(self, query: str, top_k: int = 5) -> list[dict]:
        # 生成查询变体
        variations = self._generate_variations(query)

        # 收集所有结果
        all_results = {}
        for q in [query] + variations:
            results = self.retriever.retrieve(q, top_k=top_k)
            for r in results:
                doc_id = r["id"]
                if doc_id not in all_results or r["score"] > all_results[doc_id]["score"]:
                    all_results[doc_id] = r

        # 按分数排序返回
        sorted_results = sorted(
            all_results.values(),
            key=lambda x: x["score"],
            reverse=True
        )
        return sorted_results[:top_k]

    def _generate_variations(self, query: str) -> list[str]:
        prompt = f"""请为以下问题生成3个不同角度的查询变体,
每个变体单独一行,不要编号:

原始问题:{query}

查询变体:"""
        response = self.llm.generate(prompt)
        return [q.strip() for q in response.strip().split("\n") if q.strip()]

七、Prompt模板设计

7.1 基础RAG Prompt

RAG_PROMPT_TEMPLATE = """你是一个专业的知识助手。请基于以下参考文档回答用户的问题。

要求:
1. 只使用参考文档中的信息回答问题
2. 如果参考文档中没有相关信息,请明确说明"根据现有资料无法回答该问题"
3. 回答要准确、完整、有条理
4. 适当引用文档来源

参考文档:
{context}

用户问题:{question}

回答:"""

7.2 带来源引用的Prompt

RAG_CITATION_PROMPT = """你是一个严谨的知识助手。请基于参考文档回答问题,并在回答中标注信息来源。

规则:
1. 每个关键信息点都需要标注来源,格式为 [来源X]
2. 只使用参考文档中的信息
3. 如果信息不足,明确指出
4. 保持客观中立

参考文档:
{context}

其中每个文档的来源信息:
{sources}

用户问题:{question}

请提供带来源标注的回答:"""

7.3 对话式RAG Prompt

CONVERSATIONAL_RAG_PROMPT = """你是一个友好的知识助手,正在与用户进行对话。

对话历史:
{chat_history}

参考文档:
{context}

当前用户问题:{question}

要求:
1. 结合对话历史理解用户问题的上下文
2. 基于参考文档提供准确回答
3. 如果需要更多信息才能回答,请向用户提问
4. 保持对话的自然流畅

回答:"""

7.4 Prompt构建器

class PromptBuilder:
    def __init__(self, template: str = None):
        self.template = template or RAG_PROMPT_TEMPLATE

    def build(
        self,
        question: str,
        documents: list[dict],
        chat_history: list[dict] = None
    ) -> str:
        # 构建上下文
        context_parts = []
        for i, doc in enumerate(documents, 1):
            source = doc.get("metadata", {}).get("source", "未知来源")
            context_parts.append(
                f"[文档{i}] (来源: {source})\n{doc['content']}"
            )
        context = "\n\n".join(context_parts)

        # 构建对话历史
        history = ""
        if chat_history:
            history = "\n".join([
                f"{msg['role']}: {msg['content']}"
                for msg in chat_history[-5:]  # 只保留最近5轮
            ])

        # 填充模板
        return self.template.format(
            context=context,
            question=question,
            chat_history=history,
            sources="\n".join([
                f"[文档{i+1}]: {doc.get('metadata', {}).get('source', '未知')}"
                for i, doc in enumerate(documents)
            ])
        )

八、RAG系统评估指标

8.1 评估维度

RAG系统的评估需要从多个维度进行:

检索质量:检索到的文档是否与问题相关 生成质量:生成的回答是否准确、完整 端到端质量:最终回答是否满足用户需求

8.2 检索评估指标

import numpy as np

class RetrievalEvaluator:
    @staticmethod
    def precision_at_k(retrieved: list, relevant: set, k: int) -> float:
        """P@K: 前K个结果中相关文档的比例"""
        top_k = retrieved[:k]
        hits = sum(1 for doc in top_k if doc in relevant)
        return hits / k

    @staticmethod
    def recall_at_k(retrieved: list, relevant: set, k: int) -> float:
        """R@K: 前K个结果覆盖了多少相关文档"""
        top_k = retrieved[:k]
        hits = sum(1 for doc in top_k if doc in relevant)
        return hits / len(relevant) if relevant else 0

    @staticmethod
    def mrr(retrieved: list, relevant: set) -> float:
        """MRR: 第一个相关文档的排名倒数"""
        for i, doc in enumerate(retrieved):
            if doc in relevant:
                return 1.0 / (i + 1)
        return 0.0

    @staticmethod
    def ndcg_at_k(retrieved: list, relevance: dict, k: int) -> float:
        """NDCG@K: 归一化折损累积增益"""
        dcg = sum(
            relevance.get(doc, 0) / np.log2(i + 2)
            for i, doc in enumerate(retrieved[:k])
        )
        ideal = sorted(relevance.values(), reverse=True)[:k]
        idcg = sum(
            rel / np.log2(i + 2)
            for i, rel in enumerate(ideal)
        )
        return dcg / idcg if idcg > 0 else 0.0

8.3 生成评估指标

from collections import Counter

class GenerationEvaluator:
    @staticmethod
    def faithfulness(answer: str, context: str) -> float:
        """忠实度: 回答是否基于给定上下文"""
        # 简单实现:检查回答中的关键信息是否在上下文中出现
        answer_words = set(answer.split())
        context_words = set(context.split())
        overlap = answer_words & context_words
        return len(overlap) / len(answer_words) if answer_words else 0

    @staticmethod
    def answer_relevancy(answer: str, question: str) -> float:
        """答案相关性: 回答是否与问题相关"""
        # 使用简单的词重叠作为近似
        q_words = set(question.split())
        a_words = set(answer.split())
        overlap = q_words & a_words
        return len(overlap) / len(q_words) if q_words else 0

    @staticmethod
    def completeness(answer: str, reference: str) -> float:
        """完整性: 回答是否涵盖了参考答案的要点"""
        ref_words = set(reference.split())
        ans_words = set(answer.split())
        overlap = ref_words & ans_words
        return len(overlap) / len(ref_words) if ref_words else 0

8.4 端到端评估框架

class RAGEvaluator:
    def __init__(self, rag_system):
        self.rag = rag_system
        self.retrieval_eval = RetrievalEvaluator()
        self.generation_eval = GenerationEvaluator()

    def evaluate(self, test_cases: list[dict]) -> dict:
        """
        test_cases: [{
            "question": "...",
            "reference_answer": "...",
            "relevant_doc_ids": [...]
        }]
        """
        results = {
            "precision@5": [],
            "recall@5": [],
            "mrr": [],
            "faithfulness": [],
            "relevancy": []
        }

        for case in test_cases:
            # 执行RAG查询
            rag_result = self.rag.query(case["question"])

            # 检索评估
            retrieved_ids = [r["id"] for r in rag_result["documents"]]
            relevant = set(case.get("relevant_doc_ids", []))

            results["precision@5"].append(
                self.retrieval_eval.precision_at_k(retrieved_ids, relevant, 5)
            )
            results["recall@5"].append(
                self.retrieval_eval.recall_at_k(retrieved_ids, relevant, 5)
            )
            results["mrr"].append(
                self.retrieval_eval.mrr(retrieved_ids, relevant)
            )

            # 生成评估
            context = " ".join([d["content"] for d in rag_result["documents"]])
            results["faithfulness"].append(
                self.generation_eval.faithfulness(rag_result["answer"], context)
            )
            results["relevancy"].append(
                self.generation_eval.answer_relevancy(
                    rag_result["answer"], case["question"]
                )
            )

        # 计算平均值
        return {k: np.mean(v) for k, v in results.items()}

九、高级RAG技术

9.1 HyDE(假设文档嵌入)

HyDE的核心思想:先让LLM生成一个假设性答案,然后用这个假设答案去检索,而不是直接用问题检索。

class HyDERetriever:
    def __init__(self, retriever, llm_client):
        self.retriever = retriever
        self.llm = llm_client

    def retrieve(self, query: str, top_k: int = 5) -> list[dict]:
        # 生成假设性文档
        hyde_prompt = f"""请针对以下问题,写一段可能包含答案的文档内容。
不需要准确,只需要看起来合理即可。

问题:{query}

假设文档:"""
        hypothetical_doc = self.llm.generate(hyde_prompt)

        # 用假设文档进行检索
        return self.retriever.retrieve(hypothetical_doc, top_k=top_k)

9.2 Self-RATE(自适应检索增强)

Self-RATE让LLM先判断是否需要检索,避免对简单问题进行不必要的检索:

class SelfRATERetriever:
    def __init__(self, retriever, llm_client):
        self.retriever = retriever
        self.llm = llm_client

    def query(self, question: str) -> dict:
        # 第一步:判断是否需要检索
        decision_prompt = f"""判断以下问题是否需要外部知识来回答。

问题:{question}

如果这个问题可以基于常识回答,回复 "NO_RETRIEVAL"
如果需要查询特定信息才能准确回答,回复 "NEED_RETRIEVAL"

判断:"""
        decision = self.llm.generate(decision_prompt).strip()

        if "NO_RETRIEVAL" in decision:
            # 直接回答
            answer = self.llm.generate(f"请回答以下问题:{question}")
            return {"answer": answer, "documents": [], "retrieved": False}
        else:
            # 执行检索增强回答
            results = self.retriever.retrieve(question)
            context = "\n".join([r["content"] for r in results])
            prompt = f"基于以下信息回答问题:\n{context}\n\n问题:{question}"
            answer = self.llm.generate(prompt)
            return {"answer": answer, "documents": results, "retrieved": True}

9.3 CRAG(纠正性RAG)

CRAG在检索后增加一个评估和纠正步骤,过滤掉不相关的检索结果:

class CRAGRetriever:
    def __init__(self, retriever, llm_client):
        self.retriever = retriever
        self.llm = llm_client

    def retrieve(self, query: str, top_k: int = 5) -> list[dict]:
        # 初始检索
        initial_results = self.retriever.retrieve(query, top_k=top_k * 2)

        # 评估每个文档的相关性
        relevant_docs = []
        for doc in initial_results:
            eval_prompt = f"""判断以下文档是否与问题相关。

问题:{query}

文档内容:
{doc['content'][:500]}

只回答 "相关" 或 "不相关":"""
            evaluation = self.llm.generate(eval_prompt).strip()

            if "相关" in evaluation and "不相关" not in evaluation:
                relevant_docs.append(doc)

        # 如果没有相关文档,使用网络搜索补充
        if not relevant_docs:
            web_results = self._web_search(query)
            return web_results[:top_k]

        return relevant_docs[:top_k]

    def _web_search(self, query: str) -> list[dict]:
        """网络搜索作为后备方案"""
        # 这里可以集成搜索引擎API
        # 返回格式与向量检索一致
        return []

9.4 上下文压缩

在生成前压缩检索到的文档,去除无关内容:

class ContextCompressor:
    def __init__(self, llm_client):
        self.llm = llm_client

    def compress(self, query: str, documents: list[dict]) -> list[dict]:
        compressed = []
        for doc in documents:
            compress_prompt = f"""从以下文档中提取与问题最相关的关键信息。
去除无关内容,保留核心信息。

问题:{query}

文档:
{doc['content']}

关键信息:"""
            key_info = self.llm.generate(compress_prompt)
            compressed.append({
                **doc,
                "content": key_info,
                "original_length": len(doc["content"]),
                "compressed_length": len(key_info)
            })
        return compressed

十、实战案例:企业知识库问答系统

10.1 系统架构

class EnterpriseRAGSystem:
    """企业级RAG问答系统"""

    def __init__(self, config: dict):
        self.config = config

        # 初始化组件
        self.embedding_service = EmbeddingService(
            model_name=config.get("embedding_model", "BAAI/bge-m3")
        )

        self.vector_store = ChromaVectorStore(
            collection_name=config.get("collection", "enterprise_kb"),
            persist_directory=config.get("persist_dir", "./data/chroma")
        )

        self.retriever = HybridRetriever(
            vector_store=self.vector_store,
            embedding_service=self.embedding_service,
            alpha=config.get("hybrid_alpha", 0.7)
        )

        self.reranker = Reranker()
        self.prompt_builder = PromptBuilder()
        self.llm = LLMClient(model=config.get("llm_model", "gpt-4"))

    def ingest_documents(self, source_dir: str):
        """批量导入文档"""
        loaders = {
            ".pdf": PDFLoader(),
            ".docx": WordLoader(),
            ".html": WebLoader(),
            ".py": CodeLoader(),
        }

        chunker = RecursiveChunker(
            chunk_size=self.config.get("chunk_size", 500),
            chunk_overlap=self.config.get("chunk_overlap", 50)
        )

        all_chunks = []
        all_metadata = []

        import glob
        for filepath in glob.glob(f"{source_dir}/**/*", recursive=True):
            import os
            ext = os.path.splitext(filepath)[1].lower()
            loader = loaders.get(ext)

            if loader:
                print(f"处理文件: {filepath}")
                documents = loader.load(filepath)

                for doc in documents:
                    chunks = chunker.chunk(doc.content)
                    for chunk in chunks:
                        all_chunks.append(chunk)
                        all_metadata.append(doc.metadata)

        # 批量向量化
        print(f"正在向量化 {len(all_chunks)} 个文本块...")
        embeddings = self.embedding_service.encode(
            all_chunks, batch_size=64, show_progress=True
        )

        # 存入向量数据库
        import hashlib
        ids = [hashlib.md5(c.encode()).hexdigest() for c in all_chunks]

        self.vector_store.add_documents(
            ids=ids,
            embeddings=embeddings.tolist(),
            documents=all_chunks,
            metadatas=all_metadata
        )

        # 构建BM25索引
        self.retriever.build_bm25_index(all_chunks)

        print(f"导入完成,共 {len(all_chunks)} 个文本块")

    def query(
        self,
        question: str,
        top_k: int = 5,
        use_reranker: bool = True,
        chat_history: list[dict] = None
    ) -> dict:
        """执行RAG查询"""
        # 第一步:混合检索
        retrieved = self.retriever.retrieve(question, top_k=top_k * 2)

        # 第二步:重排序(可选)
        if use_reranker and retrieved:
            retrieved = self.reranker.rerank(question, retrieved, top_k=top_k)

        # 第三步:构建Prompt
        prompt = self.prompt_builder.build(
            question=question,
            documents=retrieved,
            chat_history=chat_history
        )

        # 第四步:生成回答
        answer = self.llm.generate(prompt)

        return {
            "answer": answer,
            "documents": retrieved,
            "prompt": prompt
        }

10.2 Web API接口

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="企业知识库问答系统")
rag_system = None

class QueryRequest(BaseModel):
    question: str
    top_k: int = 5
    use_reranker: bool = True
    chat_history: list[dict] = None

class QueryResponse(BaseModel):
    answer: str
    sources: list[dict]

@app.on_event("startup")
async def startup():
    global rag_system
    config = {
        "embedding_model": "BAAI/bge-m3",
        "collection": "enterprise_kb",
        "persist_dir": "./data/chroma",
        "hybrid_alpha": 0.7,
        "chunk_size": 500,
        "chunk_overlap": 50,
        "llm_model": "gpt-4"
    }
    rag_system = EnterpriseRAGSystem(config)

@app.post("/query", response_model=QueryResponse)
async def query(request: QueryRequest):
    try:
        result = rag_system.query(
            question=request.question,
            top_k=request.top_k,
            use_reranker=request.use_reranker,
            chat_history=request.chat_history
        )
        return QueryResponse(
            answer=result["answer"],
            sources=[
                {
                    "content": doc["content"][:200],
                    "source": doc.get("metadata", {}).get("source", ""),
                    "score": doc.get("rerank_score", doc.get("score", 0))
                }
                for doc in result["documents"]
            ]
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/ingest")
async def ingest(source_dir: str):
    try:
        rag_system.ingest_documents(source_dir)
        return {"status": "success"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

10.3 前端界面

<!DOCTYPE html>
<html>
<head>
    <title>企业知识库</title>
    <style>
        body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .chat-container { border: 1px solid #ddd; border-radius: 8px; padding: 20px; }
        .message { margin: 10px 0; padding: 10px; border-radius: 8px; }
        .user { background: #e3f2fd; text-align: right; }
        .assistant { background: #f5f5f5; }
        .sources { font-size: 0.9em; color: #666; margin-top: 5px; }
        input, button { padding: 10px; margin: 5px 0; }
        input { width: 80%; }
        button { background: #1976d2; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>
    <h1>📚 企业知识库问答</h1>
    <div class="chat-container" id="chat"></div>
    <div>
        <input type="text" id="question" placeholder="输入您的问题..." />
        <button onclick="ask()">发送</button>
    </div>

    <script>
    async function ask() {
        const input = document.getElementById('question');
        const question = input.value.trim();
        if (!question) return;

        // 显示用户消息
        appendMessage('user', question);
        input.value = '';

        // 调用API
        const response = await fetch('/query', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({question, top_k: 5})
        });
        const data = await response.json();

        // 显示回答
        let sourcesHtml = '<div class="sources">来源:' +
            data.sources.map(s => s.source).join(', ') + '</div>';
        appendMessage('assistant', data.answer + sourcesHtml);
    }

    function appendMessage(role, content) {
        const chat = document.getElementById('chat');
        const div = document.createElement('div');
        div.className = `message ${role}`;
        div.innerHTML = content;
        chat.appendChild(div);
        chat.scrollTop = chat.scrollHeight;
    }
    </script>
</body>
</html>

十一、最佳实践

11.1 分块策略调优

  • 从500字开始:大多数场景下,500字符的块大小是一个好的起点
  • 保留50-100字的重叠:确保上下文连贯性
  • 按文档类型调整:代码文档可能需要更大的块,FAQ可以按条目分块
  • 测试不同策略:用真实查询评估不同分块方案的效果

11.2 检索优化

  • 混合检索是标配:向量检索+BM25的效果通常优于单一检索
  • 重排序提升明显:使用交叉编码器重排序可以显著提高Top-5精度
  • 调整返回数量:检索时多取一些结果(如Top-10),重排序后再精选Top-5

11.3 Prompt设计

  • 明确指令:告诉LLM"只使用提供的文档回答"
  • 设置兜底:当文档不足以回答时,让LLM明确说明
  • 控制长度:根据应用场景设定回答长度要求
  • 来源引用:要求LLM标注信息来源,提高可信度

11.4 生产部署

# 关键配置项
PRODUCTION_CONFIG = {
    # 分块
    "chunk_size": 500,
    "chunk_overlap": 50,

    # Embedding
    "embedding_model": "BAAI/bge-m3",
    "embedding_batch_size": 64,

    # 向量数据库
    "vector_db": "milvus",
    "index_type": "HNSW",
    "ef_construction": 256,
    "m": 16,

    # 检索
    "retrieval_top_k": 10,
    "final_top_k": 5,
    "hybrid_alpha": 0.7,
    "use_reranker": True,

    # 生成
    "llm_model": "gpt-4",
    "temperature": 0.1,
    "max_tokens": 2000,

    # 缓存
    "enable_cache": True,
    "cache_ttl": 3600,
}

十二、常见问题

Q1: 检索结果不相关怎么办?

排查步骤:

  1. 检查分块是否合理——块是否太大或太小
  2. 检查Embedding模型是否适合你的语言和领域
  3. 尝试混合检索,结合关键词匹配
  4. 添加重排序步骤
  5. 优化查询——使用HyDE或多查询技术

Q2: 如何处理多语言文档?

建议:

  1. 使用多语言Embedding模型(如BGE-M3、multilingual-e5)
  2. 为不同语言建立独立的向量集合
  3. 查询时自动检测语言并路由到对应集合

Q3: 知识库如何更新?

实现增量更新:

class IncrementalIndexer:
    def update(self, source_dir: str):
        # 计算文件哈希,检测变更
        current_files = self._scan_files(source_dir)
        indexed_files = self._get_indexed_files()

        # 新增文件
        new_files = current_files - indexed_files
        # 修改文件
        modified = self._detect_modifications(current_files, indexed_files)
        # 删除文件
        deleted = indexed_files - current_files

        # 执行增量更新
        for f in new_files | modified:
            self._reindex_file(f)
        for f in deleted:
            self._remove_file(f)

Q4: 如何控制成本?

  • 使用本地Embedding模型替代API调用
  • 实施查询缓存,对相似查询返回缓存结果
  • 使用较小的LLM生成回答(如GPT-3.5而非GPT-4)
  • 限制检索文档数量和上下文长度

Q5: 如何评估RAG系统效果?

  1. 构建测试数据集(100+条带标准答案的问答对)
  2. 使用自动化评估脚本定期运行
  3. 关注指标:检索召回率、回答准确率、忠实度
  4. 收集用户反馈,持续优化

十三、总结

RAG系统是当前最实用的AI应用架构之一,它让LLM能够基于最新、最准确的外部知识回答问题。构建高质量的RAG系统需要关注以下关键环节:

  1. 数据质量:好的文档预处理和分块策略是基础
  2. 检索精度:混合检索+重排序是当前最佳实践
  3. 生成质量:精心设计的Prompt模板确保回答准确可靠
  4. 持续优化:通过评估指标驱动系统迭代改进

随着技术的发展,RAG系统正在向更智能的方向演进:自适应检索、多模态RAG、图谱增强RAG等新技术不断涌现。掌握RAG核心技术,将帮助你构建更加强大的AI知识应用。

推荐学习资源

  • LangChain官方文档 - RAG章节
  • LlamaIndex官方文档
  • 向量数据库(Chroma/Milvus/Qdrant)官方文档
  • "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" 原始论文
  • MTEB Embedding模型排行榜

本教程基于2024年RAG技术生态编写,相关工具和最佳实践持续演进中。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录