目 录CONTENT

文章目录

LLM - RAG检索增强生成知识系统

PySuper
2025-04-13 / 0 评论 / 0 点赞 / 1 阅读 / 0 字
温馨提示:
所有牛逼的人都有一段苦逼的岁月。 但是你只要像SB一样去坚持,终将牛逼!!! ✊✊✊

RAG 检索增强生成知识系统

本文档系统阐述 RAG(Retrieval-Augmented Generation,检索增强生成)的完整技术链路,涵盖从文档解析到评测的十个核心环节

适合作为构建企业知识库和 RAG 系统的核心技术参考资料。


1. 文档解析

1.1 定义

文档解析是 RAG 系统的第一步,将 PDF、Word、Markdown、HTML 等不同格式的文档统一提取为纯文本加结构信息,为后续的清洗、分块和向量化做准备。解析质量直接决定整个 RAG 系统的知识上限。

1.2 核心原理

+----------------------------------------------------------+
|                    文档解析流程                            |
+----------------------------------------------------------+
|                                                          |
|  原始文档                                                 |
|  +----------+ +----------+ +----------+ +----------+    |
|  | Markdown | |   HTML   | |   PDF    | |   Word   |    |
|  +----+-----+ +----+-----+ +----+-----+ +----+-----+    |
|       |            |            |            |           |
|       v            v            v            v           |
|  +-----------------------------------------------+      |
|  |              格式特定的解析器                   |      |
|  |  (标题层级/段落/列表/表格/图片描述)            |      |
|  +----------------------+------------------------+      |
|                         |                                |
|                         v                                |
|  +-----------------------------------------------+      |
|  |  统一输出: sections = [                        |      |
|  |    {heading, level, content, metadata},       |      |
|  |    ...                                        |      |
|  |  ]                                            |      |
|  +-----------------------------------------------+      |
|                                                          |
+----------------------------------------------------------+

1.3 Markdown 解析

def parse_markdown(text: str) -> dict:
    """解析 Markdown 文本,提取标题层级和正文内容"""
    lines = text.strip().split("\n")
    sections = []
    current_heading = ""
    current_level = 0
    current_content = []

    for line in lines:
        if line.startswith("#"):
            # 保存上一个段落
            if current_content:
                sections.append({
                    "heading": current_heading,
                    "level": current_level,
                    "content": "\n".join(current_content).strip(),
                })
                current_content = []
            current_level = len(line) - len(line.lstrip("#"))
            current_heading = line.lstrip("# ").strip()
        else:
            current_content.append(line)

    if current_content:
        sections.append({
            "heading": current_heading,
            "level": current_level,
            "content": "\n".join(current_content).strip(),
        })

    return {"format": "markdown", "sections": sections}

1.4 HTML 解析

def parse_html(text: str) -> dict:
    """简易 HTML 解析:提取标题和正文,去除标签"""
    import re

    # 提取 <title>
    title_match = re.search(r"<title>(.*?)</title>", text, re.DOTALL)
    title = title_match.group(1).strip() if title_match else ""

    # 移除 script 和 style 标签
    text = re.sub(r"<script[^>]*>.*?</script>", "", text, flags=re.DOTALL)
    text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.DOTALL)

    # 提取 <h1>~<h3> 和 <p> 标签内容
    headings = re.findall(r"<h[1-3][^>]*>(.*?)</h[1-3]>", text, re.DOTALL)
    paragraphs = re.findall(r"<p[^>]*>(.*?)</p>", text, re.DOTALL)

    # 清除剩余 HTML 标签
    clean_headings = [re.sub(r"<[^>]+>", "", h).strip() for h in headings]
    clean_paragraphs = [re.sub(r"<[^>]+>", "", p).strip() for p in paragraphs]

    return {"format": "html", "title": title, "headings": clean_headings, "paragraphs": clean_paragraphs}

1.5 不同格式解析对比

格式

解析难度

结构信息

常用工具

挑战

Markdown

标题层级

正则/库

表格解析

HTML

标签结构

BeautifulSoup

噪声标签多

PDF

几乎无

PyPDF2/pdfplumber

排版复杂、表格图片

Word

样式信息

python-docx

公式和图表

纯文本

最低

直接读取

缺乏结构

1.6 解析中的关键决策

解析策略选择:

  1. 保留结构 vs 纯文本
     - 保留结构: 标题层级用于分块边界
     - 纯文本: 简单但丢失文档结构

  2. 表格处理
     - 转为文本描述: "表格:列A=值1, 列B=值2"
     - 保留 Markdown 表格: 保留结构
     - 跳过: 信息损失

  3. 图片处理
     - OCR 提取文字
     - 图片描述(VLM 生成)
     - 跳过

  4. 元数据保留
     - 来源文件名、页码、章节
     - 用于检索结果溯源

1.7 与其他概念的关联

  • -> 文本清洗与分块:解析输出是分块的输入

  • -> 向量化与存储:解析后的文本将被向量化

  • <- 无前置依赖:文档解析是 RAG 的入口


2. 文本清洗与分块策略

2.1 定义

文本清洗去除噪声(多余空白、特殊字符、页眉页脚),分块(Chunking)把长文档切成适合检索的证据片段。分块策略直接决定 RAG 的召回质量——块太大稀释语义、太小丢失上下文。

2.2 文本清洗

def clean_text(text: str) -> str:
    """清洗文本:去除多余空白、特殊字符、统一格式"""
    lines = text.split("\n")
    cleaned_lines = []
    for line in lines:
        line = line.strip()
        if not line:
            continue
        # 去除纯数字行(页码)
        if re.match(r"^\d{1,3}$", line):
            continue
        # 去除 HTML 残留标签
        line = re.sub(r"<[^>]+>", "", line)
        # 合并多个空格
        line = re.sub(r"[ \t]+", " ", line)
        cleaned_lines.append(line)
    return "\n".join(cleaned_lines)


def normalize_text(text: str) -> str:
    """归一化文本:全角转半角,统一标点"""
    text = text.replace("\u3000", " ")  # 全角空格
    text = re.sub(r"[\uff10-\uff19]", lambda m: chr(ord(m.group()) - 0xFFEE), text)
    text = re.sub(r"[\uff21-\uff3a]", lambda m: chr(ord(m.group()) - 0xFF00 + 0x41), text)
    text = re.sub(r"[\uff41-\uff5a]", lambda m: chr(ord(m.group()) - 0xFF00 + 0x61), text)
    return text

2.3 分块策略

2.3.1 固定长度分块

def chunk_by_fixed_size(text: str, chunk_size: int = 200, overlap: int = 50) -> list[str]:
    """固定长度分块:按字符数切割,带重叠窗口"""
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)
        if end >= len(text):
            break
        start = end - overlap  # 重叠部分
    return chunks

2.3.2 按句子分块

def chunk_by_sentence(text: str, sentences_per_chunk: int = 3) -> list[str]:
    """按句子分块:以句末标点为分割点,每 N 句一个块"""
    sentences = re.split(r"(?<=[。!?.!?\n])", text)
    sentences = [s.strip() for s in sentences if s.strip()]
    chunks = []
    for i in range(0, len(sentences), sentences_per_chunk):
        chunk = "".join(sentences[i : i + sentences_per_chunk])
        chunks.append(chunk)
    return chunks

2.3.3 按 Token 分块

def chunk_by_tokens(text: str, max_tokens: int = 256, overlap: int = 50) -> list[str]:
    """按 Token 数分块:使用 tiktoken 精确控制"""
    enc = tiktoken.get_encoding("cl100k_base")
    tokens = enc.encode(text)
    chunks = []
    start = 0
    while start < len(tokens):
        end = start + max_tokens
        chunk_tokens = tokens[start:end]
        chunks.append(enc.decode(chunk_tokens))
        if end >= len(tokens):
            break
        start = end - overlap
    return chunks

2.3.4 按分隔符分块

def chunk_by_separator(text: str, separator: str = "\n\n") -> list[str]:
    """按分隔符分块:保留文档自然段落结构"""
    chunks = text.split(separator)
    return [c.strip() for c in chunks if c.strip()]

2.4 分块策略对比

策略

块大小

上下文完整性

实现难度

适用场景

固定长度

均匀

差(可能截断句子)

通用基线

按句子

不均匀

好(保留完整句)

文本为主的文档

按 Token

精确

中(Token 边界)

精确控制成本

按分隔符

不均匀

最好(保留段落)

结构化文档

递归分块

自适应

复杂混合文档

2.5 重叠窗口的作用

无重叠 (可能丢失上下文):

  块1: "RAG 是一种检索增强生成技术。它的核心思想是"
  块2: "通过检索外部知识库来增强模型的回答能力。"

  -> 检索到块2时,缺少"RAG"的上下文

有重叠 (overlap=10):

  块1: "RAG 是一种检索增强生成技术。它的核心思想是"
  块2: "检索增强生成技术。它的核心思想是通过检索外部知识库..."

  -> 块2包含前文上下文,语义更完整

2.6 分块大小对 RAG 的影响

块大小的影响:

  太小 (50 tokens):
    - 语义不完整,检索准确率低
    - 需要检索更多块才能覆盖答案
    - 优点:定位精确

  适中 (200-500 tokens):
    - 语义完整,检索准确率高
    - 上下文充分,适合多数场景
    - 推荐默认值

  太大 (1000+ tokens):
    - 一个块包含多个主题,稀释语义
    - 检索准确率下降
    - 占用过多上下文窗口

2.7 与其他概念的关联

  • <- 文档解析:解析结果作为分块输入

  • -> 向量化与存储:分块结果将被向量化存入数据库

  • -> 向量检索:分块质量直接影响检索效果


3. 向量化与向量数据库

3.1 定义

向量化(Embedding)把文本变成高维向量,向量数据库存储这些向量并支持高效相似度搜索。这是 RAG 检索的基础设施——没有向量数据库,就无法实现毫秒级的语义检索。

对应 Demo: demos/02_RAG/03_向量化与存储.py

3.2 向量化流程

文本分块 -> Embedding 模型 -> 向量 -> 向量数据库

  "RAG 是检索增强生成"  ->  model  ->  [0.12, -0.34, ...]  ->  Qdrant
  "它通过检索知识库回答"  ->  model  ->  [0.15, -0.30, ...]  ->  Qdrant

  向量维度: 64 ~ 3072 (取决于模型)
  常用模型: text-embedding-ada-002 (1536维)
           text-embedding-3-small (1536维)
           text-embedding-3-large (3072维)
           BGE-zh (1024维)

3.3 Qdrant 向量数据库操作

def create_collection(client: QdrantClient, collection_name: str, vector_size: int) -> None:
    """创建向量集合(相当于数据库中的表)"""
    if client.collection_exists(collection_name):
        client.delete_collection(collection_name)
    client.create_collection(
        collection_name=collection_name,
        vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE),
    )


def upsert_points(client: QdrantClient, collection_name: str, chunks: list[dict], dim: int) -> None:
    """将文档分块向量化并写入向量数据库"""
    points = []
    for chunk in chunks:
        vector = mock_embedding(chunk["content"], dim)
        points.append(
            PointStruct(
                id=chunk["id"],
                vector=vector,
                payload={  # payload 存储原始文本和元数据
                    "content": chunk["content"],
                    "source": chunk["metadata"]["source"],
                    "chunk_index": chunk["metadata"]["chunk_index"],
                },
            )
        )
    client.upsert(collection_name=collection_name, points=points)

3.4 向量数据库核心概念

+----------------------------------------------------------+
|                Qdrant 数据模型                             |
+----------------------------------------------------------+
|                                                          |
|  Collection (集合) = 一类文档的向量表                      |
|    |                                                     |
|    +-- Point (点) = 一个文档块                             |
|          |                                               |
|          +-- Vector (向量): [0.12, -0.34, ...]            |
|          |                                               |
|          +-- Payload (元数据): {                          |
|                "content": "文档块文本",                    |
|                "source": "文件名",                         |
|                "chunk_index": 3,                          |
|                "category": "技术"                         |
|              }                                           |
|                                                          |
|  距离度量:                                                |
|    - Cosine (余弦): 适合文本语义匹配                       |
|    - Euclid (欧氏): 适合图像                               |
|    - Dot (点积): 适合已归一化向量                          |
|                                                          |
+----------------------------------------------------------+

3.5 向量数据库对比

数据库

类型

特点

适用场景

Qdrant

专用向量库

Rust 编写,高性能,过滤强

生产级 RAG

Chroma

嵌入式

轻量,Python 原生

原型开发

Pinecone

云服务

全托管,免运维

快速上线

Weaviate

专用向量库

支持多模态

多模态 RAG

Milvus

专用向量库

分布式,超大规模

十亿级向量

pgvector

PostgreSQL 扩展

与关系数据库共存

已有 PG 基础设施

3.6 向量索引算法

暴力搜索 (Flat):
  遍历所有向量计算相似度 -> O(n)
  精确但慢,适合小数据集 (<10万)

HNSW (Hierarchical Navigable Small World):
  构建层次图结构,近似最近邻搜索
  速度快,精度高,适合中等数据集 (10万~1000万)

IVF (Inverted File Index):
  聚类后只搜索最近的几个簇
  速度极快,精度略低,适合超大数据集

PQ (Product Quantization):
  向量压缩,减少内存占用
  牺牲精度换取存储效率

3.7 与其他概念的关联

  • <- 文本清洗与分块:分块结果是向量化的输入

  • -> 向量检索:向量数据库支持相似度搜索

  • -> 混合检索:向量检索是混合检索的组件之一


4. 向量检索

4.1 定义

向量检索(语义检索)是 RAG 的核心:把用户问题向量化,在向量库中找到最相似的文档块。关键在于相似度计算、Top-K 召回和元数据过滤。

对应 Demo: demos/02_RAG/04_向量检索.py

4.2 检索流程

用户问题: "如何训练大语言模型"
    |
    v
Embedding 模型 -> query_vector [0.12, -0.34, ...]
    |
    v
向量数据库 (Qdrant) 搜索
    |
    v
计算 query_vector 与所有文档块向量的余弦相似度
    |
    v
返回 Top-K 最相似的文档块
    |
    v
结果:
  1. [score=0.92] "大语言模型的训练包括预训练和微调..."
  2. [score=0.87] "预训练使用大规模无标注文本..."
  3. [score=0.85] "微调阶段使用监督学习..."
  4. [score=0.78] "模型评估指标包括..."
  5. [score=0.71] "训练成本与参数量..."

4.3 基本检索实现

def vector_search(client: QdrantClient, collection: str, query: str, dim: int, top_k: int = 5) -> list[dict]:
    """向量语义检索:返回 Top-K 结果"""
    query_vec = mock_embedding(query, dim)
    results = client.search(
        collection_name=collection,
        query_vector=query_vec,
        limit=top_k,
    )
    return [{"doc_id": int(r.id), "score": r.score, "content": r.payload["content"]} for r in results]

4.4 元数据过滤

def filtered_search(
    client: QdrantClient, collection: str, query: str, dim: int, source_filter: str = None, category_filter: str = None
) -> list[dict]:
    """带元数据过滤的向量检索"""
    query_vec = mock_embedding(query, dim)

    # 构建过滤条件
    conditions = []
    if source_filter:
        conditions.append(FieldCondition(key="source", match=MatchValue(value=source_filter)))
    if category_filter:
        conditions.append(FieldCondition(key="category", match=MatchValue(value=category_filter)))

    search_filter = Filter(must=conditions) if conditions else None

    results = client.search(
        collection_name=collection,
        query_vector=query_vec,
        query_filter=search_filter,
        limit=5,
    )
    return results

4.5 阈值过滤

def search_with_threshold(results: list[dict], min_score: float = 0.7) -> list[dict]:
    """阈值过滤:只保留相似度高于阈值的结果"""
    return [r for r in results if r["score"] >= min_score]
Top-K vs 阈值过滤:

  Top-K=5: 固定返回 5 条结果
    -> 可能包含低相关性的结果

  阈值=0.7: 只返回相似度 >= 0.7 的结果
    -> 结果数量不确定,但质量有保证

  组合: Top-K=10 + 阈值=0.7
    -> 最多 10 条,且都高于 0.7

4.6 检索质量影响因素

因素

影响

优化方向

Embedding 模型质量

决定语义理解能力

使用更好的模型

分块大小

影响语义粒度

选择合适的块大小

向量维度

维度越高信息越丰富

平衡精度和成本

Top-K 值

K 太小遗漏,太大噪声

通常 3-10

距离度量

不同度量适合不同场景

文本用余弦

元数据过滤

缩小搜索范围

按来源/类别过滤

4.7 与其他概念的关联

  • <- 向量化与存储:检索基于已存储的向量

  • -> 混合检索:向量检索是混合检索的语义组件

  • -> 重排序:向量检索结果是重排序的输入


5. BM25 关键词检索

5.1 定义

BM25(Best Matching 25)是经典的关键词检索算法,基于词频(TF)和逆文档频率(IDF)评分。它擅长精确匹配关键词,与向量检索互补——向量理解语义,BM25 精确匹配字面,两者结合大幅提升召回率。

对应 Demo: demos/02_RAG/05_关键词检索_BM25.py

5.2 BM25 评分公式

BM25(D,Q)=i=1nIDF(qi)f(qi,D)(k1+1)f(qi,D)+k1(1b+bDavgdl)\text{BM25}(D, Q) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)} {f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}

5.3 倒排索引

倒排索引结构:

  文档1: "Python 是编程语言"
  文档2: "Python 可以做数据分析"
  文档3: "编程语言有很多种"

  分词后:
  文档1: [python, 编程, 语言]
  文档2: [python, 数据, 分析]
  文档3: [编程, 语言, 很多, 种]

  倒排索引:
  python  -> [文档1, 文档2]
  编程    -> [文档1, 文档3]
  语言    -> [文档1, 文档3]
  数据    -> [文档2]
  分析    -> [文档2]
  很多    -> [文档3]
  种      -> [文档3]

  查询 "python 编程":
  python -> [文档1, 文档2]
  编程   -> [文档1, 文档3]
  合并   -> 文档1(2次命中), 文档2(1次), 文档3(1次)
  BM25评分 -> 文档1 > 文档2 > 文档3

5.4 BM25 实现

class BM25Index:
    """BM25 倒排索引"""

    def __init__(self, k1: float = 1.5, b: float = 0.75):
        """
        :param k1: 词频饱和参数(控制词频增长的影响)
        :param b: 长度归一化参数(控制文档长度的影响)
        """
        self.k1 = k1
        self.b = b
        self.documents: list[list[str]] = []
        self.doc_len: list[int] = []
        self.avg_doc_len: float = 0.0
        self.inverted_index: dict[str, list[int]] = {}
        self.idf: dict[str, float] = {}
        self.doc_count: int = 0

    def add_documents(self, docs: list[str]) -> None:
        """构建索引:分词 -> 统计词频 -> 构建倒排索引 -> 计算 IDF"""
        self.doc_count = len(docs)
        for doc_id, doc in enumerate(docs):
            tokens = tokenize(doc)
            self.documents.append(tokens)
            self.doc_len.append(len(tokens))
            tf = Counter(tokens)
            for term in tf:
                if term not in self.inverted_index:
                    self.inverted_index[term] = []
                self.inverted_index[term].append(doc_id)

        self.avg_doc_len = sum(self.doc_len) / self.doc_count if self.doc_count > 0 else 0

        # 计算 IDF
        for term, doc_ids in self.inverted_index.items():
            n = len(doc_ids)
            self.idf[term] = math.log((self.doc_count - n + 0.5) / (n + 0.5) + 1)

    def search(self, query: str, top_k: int = 5) -> list[tuple[int, float]]:
        """检索:计算查询与每个文档的 BM25 得分"""
        query_tokens = tokenize(query)
        scores = [0.0] * self.doc_count

        for term in query_tokens:
            if term not in self.inverted_index:
                continue
            for doc_id in self.inverted_index[term]:
                tf = self.documents[doc_id].count(term)
                # BM25 评分
                numerator = tf * (self.k1 + 1)
                denominator = tf + self.k1 * (1 - self.b + self.b * self.doc_len[doc_id] / self.avg_doc_len)
                scores[doc_id] += self.idf[term] * (numerator / denominator)

        ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
        return ranked[:top_k]

5.5 参数调优

参数

作用

推荐值

调优方向

词频饱和

1.2 ~ 2.0

大值:词频更重要;小值:抑制高频词

长度归一化

0.75

大值:更惩罚长文档;0:不考虑长度

k1 的影响:
  k1=0:   完全忽略词频(退化为布尔检索)
  k1=1.5: 标准值,词频有适度影响
  k1=10:  词频影响很大(几乎线性)

b 的影响:
  b=0:    不考虑文档长度
  b=0.75: 标准值,适度归一化
  b=1:    完全归一化(长文档严重被惩罚)

5.6 BM25 vs 向量检索

维度

BM25

向量检索

匹配方式

关键词精确匹配

语义相似度

理解同义词

不能

专有名词匹配

强(精确)

弱(可能语义偏移)

罕见词权重

高(IDF 大)

取决于训练数据

计算成本

索引大小

小(倒排索引)

大(高维向量)

5.7 与其他概念的关联

  • -> 混合检索:BM25 与向量检索融合是标准做法

  • <- 文本清洗与分块:分块结果构建 BM25 索引

  • -> RAG 评测:BM25 检索质量需要评测


6. 混合检索

6.1 定义

混合检索(Hybrid Search)融合语义检索(向量)和关键词检索(BM25)的结果。语义检索理解意思,关键词检索精确匹配,两者互补提升召回率。核心挑战是不同检索系统的分数不可直接比较,需要融合策略。

对应 Demo: demos/02_RAG/06_混合检索.py

6.2 为什么需要混合检索

场景1: 查询 "RAG 的 hallucination 问题"

  向量检索结果:
    1. "检索增强生成可以减少模型编造信息" (score=0.89)
    2. "RAG 系统的架构设计" (score=0.82)
    -> 找到了语义相关的内容,但可能遗漏包含 "hallucination" 关键词的文档

  BM25 检索结果:
    1. "Hallucination 是大模型的常见问题" (score=15.2)
    2. "减少 hallucination 的方法" (score=12.8)
    -> 精确匹配了关键词,但可能遗漏语义相关但用词不同的文档

  混合检索结果:
    1. "检索增强生成可以减少模型编造信息" (语义匹配 + 部分词匹配)
    2. "Hallucination 是大模型的常见问题" (词匹配)
    3. "减少 hallucination 的方法" (词匹配)
    -> 综合两者优势,召回率最高

6.3 分数融合策略

6.3.1 RRF(Reciprocal Rank Fusion,倒数排名融合)

其中

是常数(通常 60),

是文档

在检索系统

中的排名。

def rrf_fusion(vector_results: list[dict], bm25_results: list[dict], k: int = 60, top_n: int = 5) -> list[dict]:
    """RRF 融合:基于排名的融合,不依赖原始分数"""
    scores = {}

    # 向量检索结果按排名融合
    for rank, result in enumerate(vector_results):
        doc_id = result["doc_id"]
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)

    # BM25 检索结果按排名融合
    for rank, result in enumerate(bm25_results):
        doc_id = result["doc_id"]
        scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)

    # 排序并返回 Top-N
    ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
    return [{"doc_id": doc_id, "rrf_score": score} for doc_id, score in ranked[:top_n]]

6.3.2 加权融合

def weighted_fusion(
    vector_results: list[dict], bm25_results: list[dict], vector_weight: float = 0.7, bm25_weight: float = 0.3
) -> list[dict]:
    """加权融合:将分数归一化后按权重相加"""
    # 归一化向量检索分数到 [0, 1]
    max_vec_score = max(r["score"] for r in vector_results) if vector_results else 1
    # 归一化 BM25 分数到 [0, 1]
    max_bm25_score = max(r["score"] for r in bm25_results) if bm25_results else 1

    scores = {}
    for r in vector_results:
        doc_id = r["doc_id"]
        normalized = r["score"] / max_vec_score if max_vec_score > 0 else 0
        scores[doc_id] = scores.get(doc_id, 0) + vector_weight * normalized

    for r in bm25_results:
        doc_id = r["doc_id"]
        normalized = r["score"] / max_bm25_score if max_bm25_score > 0 else 0
        scores[doc_id] = scores.get(doc_id, 0) + bm25_weight * normalized

    ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
    return [{"doc_id": doc_id, "fused_score": score} for doc_id, score in ranked]

6.4 融合策略对比

策略

原理

优点

缺点

适用场景

RRF

基于排名融合

无需分数归一化,鲁棒

忽略分数差异

通用推荐

加权融合

分数归一化后加权

考虑分数差异

需调权重,对异常值敏感

分数可比场景

CombSUM

归一化分数求和

简单

对分数分布敏感

分数一致场景

RBO

有序排列融合

理论扎实

计算复杂

学术研究

6.5 权重选择

权重选择指南:

  语义检索为主 (vector=0.7, bm25=0.3):
    -> 适合自然语言问答、概念查询

  关键词检索为主 (vector=0.3, bm25=0.7):
    -> 适合精确查找、专有名词、代码搜索

  均衡 (vector=0.5, bm25=0.5):
    -> 通用场景的起点

  动态权重:
    -> 查询长度短: 提高 BM25 权重
    -> 查询长度长: 提高向量权重
    -> 包含专有名词: 提高 BM25 权重

6.6 与其他概念的关联

  • <- 向量检索 + BM25:混合检索融合两者结果

  • -> 重排序:混合检索结果是重排序的输入

  • -> 端到端 RAG:混合检索是完整 RAG 流程的核心组件


7. 重排序

7.1 定义

重排序(Reranking)对初步检索结果做精细化二次排序。向量检索是"双塔"模型(快但粗),Reranker 是"交叉"模型(慢但准)。先粗筛再精排是 RAG 检索的标准模式——用快速检索召回较多候选,再用精准模型选出最优。

对应 Demo: demos/02_RAG/07_重排序.py

7.2 两阶段检索架构

+----------------------------------------------------------+
|                两阶段检索架构                              |
+----------------------------------------------------------+
|                                                          |
|  用户查询                                                  |
|     |                                                    |
|     v                                                    |
|  +---------------+                                       |
|  | 第一阶段: 粗排 |  向量检索/BM25/混合检索               |
|  | (召回 Top-K)  |  K=20~50 (召回率高,速度快)           |
|  +-------+-------+                                       |
|          |                                               |
|          v                                               |
|  +---------------+                                       |
|  | 第二阶段: 精排 |  Cross-Encoder Reranker              |
|  | (精排 Top-N)  |  N=3~5 (精准度高,速度慢)            |
|  +-------+-------+                                       |
|          |                                               |
|          v                                               |
|     最终结果 (Top-N)                                     |
|                                                          |
+----------------------------------------------------------+

  双塔模型 (Bi-Encoder) vs 交叉模型 (Cross-Encoder):

  Bi-Encoder (向量检索):
    Query -> Encoder -> Vector
    Doc   -> Encoder -> Vector
    相似度 = cos(Query_Vector, Doc_Vector)
    -> 快 (可预计算), 但粗

  Cross-Encoder (Reranker):
    [Query, Doc] -> Encoder -> Score
    -> 慢 (不可预计算), 但准

7.3 重排序实现

def mock_reranker_score(query: str, document: str) -> float:
    """
    模拟 Cross-Encoder Reranker 评分
    真实场景应调用 Cohere Rerank / BGE-Reranker
    """
    q_tokens = set(re.findall(r"[\u4e00-\u9fff]|[a-zA-Z]+", query.lower()))
    d_tokens = re.findall(r"[\u4e00-\u9fff]|[a-zA-Z]+", document.lower())
    if not d_tokens:
        return 0.0
    # 关键词命中数
    hits = sum(1 for t in d_tokens if t in q_tokens)
    # 命中率
    hit_rate = hits / len(q_tokens) if q_tokens else 0
    # 文档中查询词的密度
    density = hits / len(d_tokens)
    # 综合评分
    return 0.6 * hit_rate + 0.4 * density


def first_stage_retrieve(client, collection, query, dim, top_k=10) -> list[dict]:
    """第一阶段:向量检索粗排,召回较多候选"""
    query_vec = mock_embedding(query, dim)
    results = client.search(collection_name=collection, query_vector=query_vec, limit=top_k)
    return [{"doc_id": int(r.id), "score": r.score, "content": r.payload["content"]} for r in results]


def second_stage_rerank(candidates: list[dict], query: str, top_n: int = 3) -> list[dict]:
    """第二阶段:Reranker 精排,对候选重新打分"""
    reranked = []
    for c in candidates:
        rerank_score = mock_reranker_score(query, c["content"])
        reranked.append({
            "doc_id": c["doc_id"],
            "vector_score": c["score"],
            "rerank_score": rerank_score,
            "content": c["content"],
        })
    # 按 Reranker 分数排序
    reranked.sort(key=lambda x: x["rerank_score"], reverse=True)
    return reranked[:top_n]

7.4 常用 Reranker 模型

模型

类型

特点

适用场景

Cohere Rerank

API 服务

效果好,付费

生产环境

BGE-Reranker

开源

中文友好,免费

本地部署

bge-reranker-large

开源

大模型,精度高

对精度要求高

Jina Reranker

开源

多语言

多语言 RAG

GPT-4 Rerank

LLM

最灵活,成本高

少量精排

7.5 两阶段检索参数

参数选择:

  第一阶段 (粗排):
    top_k = 20 ~ 50  (召回足够多候选)
    使用快速检索 (向量/BM25/混合)

  第二阶段 (精排):
    top_n = 3 ~ 5    (只保留最相关的)
    使用 Cross-Encoder (慢但准)

  权衡:
    top_k 太大 -> 精排计算量大
    top_k 太小 -> 可能遗漏正确答案
    top_n 太大 -> 上下文窗口占用多
    top_n 太小 -> 可能缺少必要信息

7.6 与其他概念的关联

  • <- 混合检索:重排序对混合检索结果做精排

  • -> 端到端 RAG:重排序是完整 RAG 流程的关键环节

  • -> RAG 评测:重排序提升检索质量,需评测验证


8. 查询改写

8.1 定义

查询改写(Query Rewrite)是 RAG 检索的前置优化。用户问题可能太短、口语化或缺少关键词,通过 LLM 改写为更适合检索的表述,提升召回率。主要策略包括查询扩展、查询分解和 HyDE。

对应 Demo: demos/02_RAG/08_查询改写.py

8.2 查询扩展

def query_expansion(query: str) -> list[str]:
    """
    查询扩展:为原始查询添加同义词和相关词
    生成多个改写查询,分别检索后合并结果
    """
    queries = [query]  # 原始查询始终保留
    for term, synonyms in SYNONYM_MAP.items():
        if term in query:
            for syn in synonyms:
                expanded = query.replace(term, syn)
                queries.append(expanded)
    return queries
查询扩展示例:

  原始查询: "如何解决幻觉问题"

  扩展后:
  1. "如何解决幻觉问题"           (原始)
  2. "如何解决 hallucination 问题" (英文同义词)
  3. "如何解决虚假信息问题"        (中文同义词)
  4. "如何解决编造内容问题"        (中文同义词)

  对每个查询分别检索,合并去重后排序

8.3 查询分解

def query_decomposition(query: str) -> list[str]:
    """
    查询分解:将复杂问题拆分为多个子问题
    每个子问题独立检索,最后综合答案
    """
    # 模拟 LLM 分解复杂查询
    if "和" in query or "与" in query:
        parts = re.split(r"[和与]", query)
        return [p.strip() for p in parts if p.strip()]
    return [query]
查询分解示例:

  原始查询: "比较 RAG 和微调的优缺点"

  分解后:
  1. "RAG 的优点是什么"
  2. "RAG 的缺点是什么"
  3. "微调的优点是什么"
  4. "微调的缺点是什么"

  分别检索 -> 分别生成答案 -> 综合为比较分析

8.4 HyDE(Hypothetical Document Embeddings)

def hyde(query: str, llm_handler) -> str:
    """
    HyDE: 先让 LLM 生成一个假设性答案文档,
    再用该文档做向量检索(而非用原始查询)
    """
    prompt = f"请为以下问题生成一段可能的答案(无需准确):\n{query}"
    hypothetical_doc = llm_handler(prompt)
    # 用假设文档而非原始查询做向量检索
    return hypothetical_doc
HyDE 原理:

  传统流程:
    查询 -> Embedding -> 检索文档
    问题: 查询(短问题)与文档(长答案)的向量分布不匹配

  HyDE 流程:
    查询 -> LLM 生成假设答案 -> 假设答案 Embedding -> 检索文档
    优势: 假设答案与文档长度和风格更接近,向量匹配更准确

  示例:
    查询: "什么是 RAG"
    假设答案: "RAG 是检索增强生成技术,通过检索外部知识库
              来增强大语言模型的回答能力,减少幻觉..."
    -> 用假设答案的向量去检索,比用短查询更准确

8.5 查询改写策略对比

策略

原理

适用场景

成本

查询扩展

添加同义词

查询缺少关键词

低(同义词表)

查询分解

拆分为子问题

复杂多部分问题

中(需 LLM)

HyDE

生成假设文档

短查询 vs 长文档

高(需 LLM)

查询重写

口语化转书面

用户输入不规范

中(需 LLM)

Step-back

提取更宽泛概念

需要背景知识

中(需 LLM)

8.6 与其他概念的关联

  • -> 向量检索 / 混合检索:改写后的查询用于检索

  • <- LLM API 调用:查询改写需要调用 LLM

  • -> 端到端 RAG:查询改写是 RAG 流程的可选前置步骤


9. 端到端 RAG 流程

9.1 定义

端到端 RAG 流程将文档解析、清洗分块、向量化、混合检索、重排序、上下文构建和生成回答串联成完整 Pipeline。这是前面所有环节的集大成,展示各组件如何协同工作构成可运行的 RAG 系统。

对应 Demo: demos/02_RAG/09_完整RAG流程.py

9.2 完整流程架构

+------------------------------------------------------------------+
|                    端到端 RAG Pipeline                            |
+------------------------------------------------------------------+
|                                                                  |
|  离线索引阶段:                                                    |
|                                                                  |
|  文档库 -> [解析] -> [清洗] -> [分块] -> [向量化] -> [存储]       |
|             |        |        |         |          |             |
|          Markdown   去噪    固定长度   Embedding   Qdrant         |
|            HTML     归一化   按句子     模型        向量库          |
|            PDF      去重    按 Token                             |
|                                                                  |
|  在线查询阶段:                                                    |
|                                                                  |
|  用户问题                                                         |
|     |                                                            |
|     v                                                            |
|  [查询改写] --(可选)--> 扩展/分解/HyDE                            |
|     |                                                            |
|     v                                                            |
|  +----------------+        +----------------+                    |
|  | 向量检索 (Top-K)|        | BM25 检索(Top-K)|                   |
|  +-------+--------+        +-------+--------+                    |
|          |                        |                              |
|          +--------+-------+-------+                              |
|                   |                                              |
|                   v                                              |
|          [混合融合 (RRF/加权)]                                    |
|                   |                                              |
|                   v                                              |
|          [重排序 (Reranker)]                                     |
|                   |                                              |
|                   v                                              |
|          [上下文构建]                                            |
|          System: "基于以下信息回答..."                            |
|          Context: [Top-N 检索结果]                               |
|          User: 原始问题                                          |
|                   |                                              |
|                   v                                              |
|          [LLM 生成回答]                                          |
|                   |                                              |
|                   v                                              |
|          最终回答 + 引用来源                                      |
|                                                                  |
+------------------------------------------------------------------+

9.3 RAG 类实现

class RAGPipeline:
    """端到端 RAG 流程"""

    def __init__(self, vector_dim: int = 64):
        """初始化各组件"""
        self.vector_dim = vector_dim
        self.client = QdrantClient(":memory:")
        self.collection = "rag_docs"
        self.bm25 = BM25Index()
        self.documents: list[Chunk] = []

    def index(self, raw_docs: list[str], source: str = "default") -> None:
        """索引阶段:解析 -> 清洗 -> 分块 -> 向量化 -> 存储"""
        all_chunks = []
        for i, doc in enumerate(raw_docs):
            # 1. 清洗
            cleaned = clean_text(doc)
            # 2. 分块
            chunks = chunk_by_fixed_size(cleaned, chunk_size=200, overlap=50)
            for j, chunk_text in enumerate(chunks):
                chunk = Chunk(
                    id=f"{source}_{i}_{j}",
                    content=chunk_text,
                    source=source,
                    metadata={"chunk_index": j},
                )
                all_chunks.append(chunk)

        self.documents = all_chunks

        # 3. 向量化 + 存储
        self._setup_vector_store(all_chunks)
        # 4. BM25 索引
        self.bm25.add_documents([c.content for c in all_chunks])

    def query(self, question: str, top_k: int = 5, top_n: int = 3) -> dict:
        """查询阶段:检索 -> 融合 -> 重排 -> 生成"""
        # 1. 向量检索
        vec_results = self._vector_search(question, top_k)
        # 2. BM25 检索
        bm25_results = self._bm25_search(question, top_k)
        # 3. RRF 融合
        fused = self._fuse(vec_results, bm25_results, top_k)
        # 4. 重排序
        reranked = self._rerank(fused, question, top_n)
        # 5. 构建上下文 + 生成
        context = self._build_context(reranked)
        answer = self._generate(question, context)
        return {"answer": answer, "sources": reranked}

9.4 上下文构建

def _build_context(self, results: list[RetrievalResult]) -> str:
    """构建 LLM 上下文"""
    context_parts = []
    for i, r in enumerate(results, 1):
        context_parts.append(f"[{i}] (来源: {r.chunk.source})\n{r.chunk.content}")
    return "\n\n".join(context_parts)


def _generate(self, question: str, context: str) -> str:
    """生成回答(模拟 LLM 调用)"""
    prompt = f"""请基于以下检索到的信息回答问题。如果信息不足以回答,请说明。

检索到的信息:
{context}

问题:{question}

回答:"""
    # 真实场景: return llm_client.chat(prompt)
    return f"[模拟回答] 基于 {len(context)} 字符的上下文回答了问题。"

9.5 RAG 的关键设计决策

决策点

选项

推荐值

说明

分块大小

100~1000 tokens

200~400

平衡语义完整性和检索精度

重叠窗口

0~100 tokens

50

避免上下文断裂

Top-K (粗排)

10~50

20

召回足够候选

Top-N (精排)

3~10

5

上下文窗口限制

检索方式

向量/BM25/混合

混合

综合语义和关键词

Reranker

有/无

提升精度

查询改写

有/无

可选

提升召回

9.6 常见问题与解决

问题1: 检索结果不相关
  原因: 分块太大/太小, Embedding 模型差, 查询表述差
  解决: 调整分块策略, 更换 Embedding, 查询改写

问题2: 回答不忠于检索结果(幻觉)
  原因: LLM 忽略上下文, 上下文不足
  解决: 强化 System Prompt, 增加上下文量, 忠实度评测

问题3: 检索速度慢
  原因: 向量库数据量大, 检索方式复杂
  解决: 使用 HNSW 索引, 减小 Top-K, 缓存常见查询

问题4: 知识更新不及时
  原因: 索引未更新
  解决: 增量索引, 定期重建, 文档变更监听

9.7 与其他概念的关联

  • <- 所有前置环节:RAG 流程整合所有组件

  • -> RAG 评测:完整流程需要系统性评测

  • -> Agent 核心能力:RAG 可作为 Agent 的工具


10. RAG 评测

10.1 定义

RAG 评测衡量检索系统和生成系统的质量。检索评测核心指标包括召回率(Recall)、精确率(Precision)、MRR、NDCG;生成质量评估包括忠实度(Faithfulness)和相关度(Relevance)。没有评测的 RAG 系统是盲目的——无法知道改进方向。

对应 Demo: demos/02_RAG/10_RAG评测.py

10.2 评测体系架构

+------------------------------------------------------------------+
|                    RAG 评测体系                                    |
+------------------------------------------------------------------+
|                                                                  |
|  检索评测 (Retrieval Metrics)                                     |
|  +-------------------+  +-------------------+                   |
|  | Recall@K          |  | Precision@K       |                   |
|  | 召回率: 相关文档   |  | 精确率: 结果中     |                   |
|  | 被检索到的比例     |  | 相关文档的占比     |                   |
|  +-------------------+  +-------------------+                   |
|  +-------------------+  +-------------------+                   |
|  | MRR               |  | NDCG              |                   |
|  | 第一个相关文档的   |  | 考虑排名位置的     |                   |
|  | 排名倒数           |  | 加权累积增益       |                   |
|  +-------------------+  +-------------------+                   |
|                                                                  |
|  生成评测 (Generation Metrics)                                    |
|  +-------------------+  +-------------------+                   |
|  | Faithfulness      |  | Relevance         |                   |
|  | 忠实度: 回答是否   |  | 相关度: 回答是否   |                   |
|  | 基于检索到的事实   |  | 切实回答了问题     |                   |
|  +-------------------+  +-------------------+                   |
|  +-------------------+  +-------------------+                   |
|  | Answer Correctness|  | Context Precision |                   |
|  | 答案正确性         |  | 上下文精确率       |                   |
|  +-------------------+  +-------------------+                   |
|                                                                  |
+------------------------------------------------------------------+

10.3 检索评测指标

10.3.1 Recall@K(召回率)

def recall_at_k(retrieved_ids: list[str], relevant_ids: set[str], k: int) -> float:
    """计算 Recall@K:Top-K 中命中相关文档的比例"""
    top_k = retrieved_ids[:k]
    hits = len(set(top_k) & relevant_ids)
    return hits / len(relevant_ids) if relevant_ids else 0.0

10.3.2 Precision@K(精确率)

10.3.3 MRR(Mean Reciprocal Rank)

其中

是第

个查询中第一个相关文档的排名。

10.3.4 NDCG(Normalized Discounted Cumulative Gain)

def evaluate_retrieval(eval_cases: list[EvalCase], search_results: list[list[str]], k: int = 5) -> RetrievalMetrics:
    """计算检索评测指标"""
    total_recall = 0.0
    total_precision = 0.0
    total_mrr = 0.0
    total_ndcg = 0.0
    hit_count = 0

    for case, results in zip(eval_cases, search_results):
        relevant = case.relevant_doc_ids
        top_k = results[:k]

        # Recall@K
        hits = len(set(top_k) & relevant)
        recall = hits / len(relevant) if relevant else 0
        total_recall += recall

        # Precision@K
        precision = hits / k if k > 0 else 0
        total_precision += precision

        # MRR
        for rank, doc_id in enumerate(top_k, 1):
            if doc_id in relevant:
                total_mrr += 1 / rank
                break

        # NDCG
        dcg = sum(1 / math.log2(i + 2) for i, d in enumerate(top_k) if d in relevant)
        idcg = sum(1 / math.log2(i + 2) for i in range(min(len(relevant), k)))
        total_ndcg += dcg / idcg if idcg > 0 else 0

        # Hit Rate
        if hits > 0:
            hit_count += 1

    n = len(eval_cases)
    return RetrievalMetrics(
        recall_at_k=total_recall / n,
        precision_at_k=total_precision / n,
        mrr=total_mrr / n,
        ndcg=total_ndcg / n,
        hit_rate=hit_count / n,
    )

10.4 指标对比

指标

关注点

值域

适用场景

Recall@K

是否找到所有相关文档

0~1

重视不遗漏

Precision@K

结果中有多少相关

0~1

重视结果质量

MRR

第一个相关文档的排名

0~1

用户只看第一个结果

NDCG

排名质量(考虑位置)

0~1

综合排名质量

Hit Rate

至少找到一个

0~1

最基本要求

10.5 生成评测

生成评测 (LLM-as-Judge):

  忠实度 (Faithfulness):
    问题: 回答是否完全基于检索到的上下文?
    评分: 0~1 (1=完全忠实, 0=全部编造)
    方法: LLM 检查回答中每个事实是否可在上下文中找到

  相关度 (Relevance):
    问题: 回答是否切实回答了用户的问题?
    评分: 0~1 (1=完全相关, 0=答非所问)
    方法: LLM 判断回答与问题的相关程度

  答案正确性 (Answer Correctness):
    问题: 回答是否与标准答案一致?
    评分: 0~1
    方法: 与人工标注的参考答案比较
def evaluate_generation(answer: str, context: str, question: str, expected: str = "") -> dict:
    """评估生成质量(模拟 LLM-as-Judge)"""
    # 忠实度: 回答中的事实是否在上下文中
    answer_facts = extract_facts(answer)
    context_facts = extract_facts(context)
    faithful = sum(1 for f in answer_facts if f in context_facts)
    faithfulness = faithful / len(answer_facts) if answer_facts else 0

    # 相关度: 回答是否涉及问题关键词
    q_tokens = set(tokenize(question))
    a_tokens = set(tokenize(answer))
    relevance = len(q_tokens & a_tokens) / len(q_tokens) if q_tokens else 0

    return {
        "faithfulness": faithfulness,
        "relevance": min(relevance, 1.0),
    }

10.6 评测数据集构建

评测数据集结构:

  EvalCase:
    query: "什么是 RAG?"               # 用户问题
    relevant_doc_ids: {"doc_1", "doc_5"}  # 人工标注的相关文档
    expected_answer: "RAG 是检索增强..."  # 参考答案(可选)

  构建方法:
  1. 人工标注: 领域专家标注相关文档和参考答案
  2. LLM 辅助: LLM 生成问题和候选答案,人工审核
  3. 真实查询: 从生产环境收集真实用户查询
  4. 合成数据: 基于文档自动生成问答对

  评测集要求:
  - 覆盖不同类型的问题(事实型/分析型/比较型)
  - 包含简单和困难查询
  - 定期更新以反映知识库变化

10.7 RAG 评测框架对比

框架

特点

检索指标

生成指标

适用场景

RAGAS

专注 RAG 评测

Context Recall/Precision

Faithfulness/Relevance

RAG 系统评估

TruLens

追踪+评测

支持

支持

开发调试

LangSmith

LangChain 生态

支持

支持

LangChain 项目

自建

完全可控

自定义

自定义

特殊需求

10.8 与其他概念的关联

  • <- 端到端 RAG:评测衡量完整 RAG 流程的质量

  • -> 持续优化:评测结果指导各环节优化方向

  • -> Agent 评测:RAG 评测是 Agent 评测的子集


概念关系总览

                    +--------------------------------------------------+
                    |              RAG 技术体系                         |
                    +--------------------------------------------------+

  数据处理层            检索层                  生成层              评测层
    |                   |                        |                  |
    v                   v                        v                  v
+-----------+   +---------------+        +---------------+   +-----------+
|文档解析    |   |向量检索        |        |上下文构建      |   |检索评测    |
|(MD/HTML/PDF)|   |(语义匹配)     |        |(Prompt 组装)  |   |(Recall等) |
+-----+-----+   +-------+-------+        +-------+-------+   +-----------+
      |                 |                        |
      v                 |                        |
+-----------+           |                        |
|文本清洗与  |---> +---------------+        |
|分块策略    |     |BM25 检索       |        |
|(分块/重叠) |     |(关键词匹配)    |        |
+-----+-----+     +-------+-------+        |
      |                   |                |
      v                   v                |
+-----------+   +---------------+          |
|向量化与    |   |混合检索        |          |
|向量数据库  |   |(RRF/加权融合)  |          |
|(Qdrant)   |   +-------+-------+          |
+-----------+           |                  |
                        v                  |
                +---------------+          |
                |重排序          |          |
                |(粗排+精排)     |          |
                +-------+-------+          |
                        |                  |
                        v                  |
                +---------------+   +------v-------+
                |查询改写        |   |LLM 生成回答   |
                |(扩展/分解/HyDE)|   |(基于上下文)   |
                +-------+-------+   +------+-------+
                        |                  |
                        v                  v
                +---------------+   +---------------+
                |端到端 RAG 流程 |-->| RAG 评测      |
                |(完整 Pipeline) |   |(检索+生成)    |
                +---------------+   +---------------+

概念间的依赖关系

文档解析 -> 文本清洗与分块 -> 向量化与存储 -> 向量检索
                                                      |
                                              BM25 检索
                                                      |
                                          混合检索 (融合两者)
                                                      |
                                                  重排序
                                                      |
查询改写 ----> (向量检索 / 混合检索)                   |
                                                      |
                                          端到端 RAG 流程
                                                      |
                                                  RAG 评测

概念

核心作用

依赖概念

下游概念

文档解析

格式统一化

文本清洗与分块

文本清洗与分块

文本标准化与切分

文档解析

向量化与存储

向量化与存储

向量索引构建

文本清洗与分块

向量检索

向量检索

语义匹配召回

向量化与存储

混合检索

BM25 检索

关键词匹配召回

文本清洗与分块

混合检索

混合检索

结果融合

向量检索、BM25

重排序

重排序

精细化排序

混合检索

端到端 RAG

查询改写

检索前置优化

LLM API

向量检索、混合检索

端到端 RAG

完整 Pipeline

所有前置环节

RAG 评测

RAG 评测

质量衡量

端到端 RAG

持续优化

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区