深入 ‘Small-to-Big Retrieval’:在向量库中存储句子级 Embedding,在返回时加载段落级上下文

各位同仁,各位对向量检索与大型语言模型(LLM)应用充满热情的开发者们,下午好!

今天,我们将深入探讨一种在检索增强生成(RAG)领域日益受到关注的精妙策略——“Small-to-Big Retrieval”。顾名思义,这种方法在向量库中存储粒度更小的单元(例如句子级)的Embedding,但在实际返回给LLM时,却加载并提供粒度更大的上下文(例如段落级)。这并非简单的技巧,而是对RAG系统核心挑战——召回精度与上下文完整性——的深刻理解与优雅应对。

1. RAG的挑战:语义鸿沟与上下文困境

在深入Small-to-Big之前,我们首先要回顾RAG系统面临的根本性挑战。RAG的初衷是赋予LLM访问外部知识的能力,以克服其知识截止日期、幻觉问题以及特定领域知识不足的限制。其基本流程是:用户提出问题 -> 将问题Embedding化 -> 在向量数据库中检索相关文档块(chunks)-> 将检索到的块作为上下文与用户问题一并提交给LLM。

这个看似简单的流程,却隐藏着一个核心难题:如何有效地切分文档?

  • 小块(例如句子、短语):

    • 优点: 语义粒度细,Embedding更精准地捕捉特定信息点。在向量搜索时,与查询的语义匹配度可能更高,从而提高召回精度
    • 缺点: 缺乏上下文。单个句子可能无法独立表达完整的意思,或者其含义需要前后文支撑。如果只返回几个孤立的句子,LLM可能难以理解其全貌,甚至产生误解,导致生成内容缺乏连贯性或准确性。
  • 大块(例如段落、多个段落):

    • 优点: 提供丰富的上下文信息,有助于LLM理解整体语境,生成更连贯、更全面的回答。
    • 缺点: 语义粒度粗。一个大块中可能包含许多与查询不直接相关的信息。这会导致Embedding的“稀释效应”,降低与查询的语义匹配度,从而降低召回率和精度。想象一下,你只想找一个关于“深度学习优化器”的句子,但却在一个包含机器学习发展史、神经网络结构、优化器原理等多个主题的巨大段落中搜索,相关性得分可能就不如直接在一个只讲优化器的句子中搜索来得高。

这种困境通常被称为“chunk size dilemma”,即块大小的选择总是在召回精度和上下文完整性之间进行权衡。我们希望找到最相关的“点”,但又需要提供足够多的“面”来支撑这个“点”。

2. Small-to-Big Retrieval:一种优雅的解决方案

Small-to-Big Retrieval正是为了解决上述困境而生。其核心思想是:

  1. 索引(Index)阶段: 将原始文档切分为细粒度的单元(通常是句子),并为每个句子生成Embedding,存储在向量数据库中。同时,每个句子Embedding会携带元数据,指向其所属的粗粒度上下文单元(通常是原始段落或自定义的更大块)。
  2. 检索(Retrieve)阶段:
    • 当用户查询到来时,对其进行Embedding。
    • 使用用户查询的Embedding在向量数据库中进行相似性搜索,召回与查询最相关的句子级Embedding
    • 根据召回的句子Embedding的元数据,识别出它们所属的粗粒度上下文单元的ID
    • 从原始文档存储中(或者预先存储的粗粒度文本块中)提取这些粗粒度上下文单元的完整文本
    • 将这些完整的、语义丰富的上下文单元提供给LLM。

2.1 为什么这种方法有效?

  • 高召回精度: 在搜索阶段,我们使用的是句子级的Embedding。句子通常语义单一、集中,与用户查询的匹配度更高。这意味着我们能更精确地找到用户真正关心的“信息点”。
  • 富上下文支持: 虽然搜索的是句子,但最终提供给LLM的是包含这些句子的完整段落。这确保了LLM能够获得充足的背景信息,避免因上下文不足而导致的误解或生成质量下降。
  • 解耦搜索与呈现: Small-to-Big方法巧妙地将“搜索最优匹配”和“提供最佳上下文”这两个目标解耦,分别用不同粒度的文本单元来实现,从而达到两全其美的效果。

2.2 类比理解

可以把这个过程想象成在图书馆找书:

  • 传统RAG(大块): 你想找一本关于“向量数据库优化”的书。图书馆的分类系统非常粗糙,只有“计算机科学”这一大类。你只能在整个“计算机科学”区里大海捞针,或者每次都抱走一大堆书来筛选,效率低下。
  • 传统RAG(小块): 图书馆的分类系统细致到“向量数据库优化”的每一个关键词,但每次你只能拿到一张写着关键词的纸条,而没有整本书。你虽然精准找到了关键词,但不知道这本书讲了什么。
  • Small-to-Big: 你依然想找关于“向量数据库优化”的书。图书馆的索引系统非常精细,能直接定位到包含“向量数据库优化”这个词的每一本书的每一页。系统会告诉你哪些页最相关。但最终,图书馆员递给你的不是那些孤立的页码,而是包含这些页码的整本书(或章节)。这样,你既能精准定位,又能获得完整的阅读体验。

3. 核心架构与数据模型

Small-to-Big Retrieval的实现需要一个清晰的数据模型来管理不同粒度的文本块及其之间的关联。

3.1 关键组件

  1. 原始文档存储: 存储原始、未经处理的完整文档。
  2. 段落/大块存储: 存储将原始文档切分后得到的段落或其他粗粒度文本块。每个块应有唯一的ID。
  3. 向量数据库: 存储句子级的Embedding。每个Embedding都应关联其所属的段落ID,以及可选的句子ID和句子文本本身。

3.2 数据模型示意

字段名 类型 描述 存储位置
document_id 字符串/UUID 原始文档的唯一标识符 段落存储、向量DB元数据
paragraph_id 字符串/UUID 段落的唯一标识符 段落存储、向量DB元数据
paragraph_text 字符串 完整的段落文本 段落存储
sentence_id 字符串/UUID 句子的唯一标识符 向量DB元数据
sentence_text 字符串 句子的原始文本 向量DB元数据
embedding 浮点数向量 句子文本的向量表示 向量数据库

元数据结构: 在向量数据库中,每个向量除了idembedding之外,通常还会有一个metadata字段。我们利用这个字段来存储document_idparagraph_idsentence_text

// 向量数据库中一个条目的示例
{
  "id": "sentence-uuid-123",
  "embedding": [0.1, 0.2, ..., 0.9],
  "metadata": {
    "document_id": "doc-uuid-abc",
    "paragraph_id": "para-uuid-xyz",
    "sentence_text": "Small-to-Big Retrieval 是一种在RAG中平衡召回精度与上下文完整性的策略。"
  }
}

3.3 总体工作流程

  1. 文档预处理:
    • 加载原始文档。
    • 将文档切分为段落。为每个段落生成一个paragraph_id。将paragraph_idparagraph_text存储在一个易于检索的地方(例如关系型数据库、NoSQL数据库或简单的Python字典/列表)。
    • 对于每个段落,再将其切分为句子。为每个句子生成一个sentence_id
  2. Embedding生成与索引:
    • 对每个句子生成其Embedding。
    • sentence_id、句子Embedding、paragraph_id(作为元数据)以及sentence_text(作为元数据)批量插入到向量数据库中。
  3. 查询处理:
    • 接收用户查询。
    • 对用户查询生成Embedding。
    • 使用查询Embedding在向量数据库中进行K近邻(K-NN)搜索,召回N个最相关的句子Embedding
  4. 上下文重构:
    • 从召回的N个句子Embedding中提取它们对应的paragraph_id
    • 对这些paragraph_id进行去重,得到一组唯一的unique_paragraph_ids
    • 根据unique_paragraph_ids,从段落存储中检索出完整的paragraph_text
    • 将这些完整的段落文本拼接起来,形成最终的上下文。
  5. LLM交互:
    • 将重构后的上下文与原始用户查询一同发送给LLM,请求生成回答。

4. 实践:代码实现与演示

接下来,我们将通过具体的Python代码来演示Small-to-Big Retrieval的实现过程。我们将使用以下库:

  • langchain_text_splitters:用于文档切分。
  • sentence_transformers:用于生成Embedding。
  • chromadb:一个轻量级的本地向量数据库,便于演示。

4.1 环境准备

首先,确保安装了所有必要的库:

pip install langchain_text_splitters sentence_transformers chromadb

4.2 模拟文档数据

我们将使用一段关于“RAG架构”的虚构文本作为我们的文档。

document_content = """
大型语言模型(LLM)在理解和生成人类语言方面展现出惊人的能力,但它们也存在固有的局限性。首先,LLM的知识是静态的,受限于其训练数据的截止日期,无法获取最新信息。其次,它们可能出现“幻觉”,即生成看似合理但实际上是虚构的内容。为了解决这些问题,检索增强生成(RAG)架构应运而生。

RAG的核心思想是在生成答案之前,先从外部知识库中检索相关信息,然后将这些信息作为上下文提供给LLM。这使得LLM能够基于事实生成更准确、更可靠的回答。一个典型的RAG流程包括三个主要阶段:索引(Indexing)、检索(Retrieval)和生成(Generation)。

在索引阶段,原始文档被切分成小的、可管理的块(chunks)。这些块经过Embedding模型转换成高维向量,并存储在向量数据库中。Embedding模型的选择对检索性能至关重要,例如使用Sentence-BERT或OpenAI的text-embedding-ada-002。

检索阶段是RAG的关键一步。当用户提出查询时,查询本身也被Embedding化。然后,这个查询Embedding被用来在向量数据库中进行相似性搜索,找出与查询最相关的文档块。常见的搜索算法包括近似最近邻(ANN)搜索,如HNSW或IVF。

最后,在生成阶段,检索到的文档块与原始用户查询一同被送入LLM。LLM利用这些上下文信息来生成一个连贯、准确且信息丰富的回答。这个过程有效地将LLM的语言理解与外部知识的准确性结合起来。
"""

4.3 文本切分与预处理

我们将文档切分为段落,然后每个段落再切分为句子。我们需要维护paragraph_idsentence_id之间的映射。

import uuid
from typing import List, Dict, Any
from langchain_text_splitters import RecursiveCharacterTextSplitter
from sentence_transformers import SentenceTransformer
import chromadb

# 1. 初始化文本切分器
# 段落切分器:以双换行符(常见于Markdown和纯文本)作为段落分隔符
paragraph_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000, # 实际段落通常不会超过这个长度,这里只是一个上限
    chunk_overlap=0,
    separators=["nn", "n", " ", ""]
)

# 句子切分器:LangChain的RecursiveCharacterTextSplitter也能处理句子,
# 但为了更精确的句子分割,我们通常会结合NLTK或spaCy。
# 这里为了简化,我们先用一个简单的分句逻辑,实际应用中推荐NLTK的sent_tokenize。
# 也可以直接使用LangChain的SentenceSplitter
from langchain_text_splitters import SentenceSplitter
sentence_splitter = SentenceSplitter()

# 2. 初始化Embedding模型
# 使用一个预训练的Sentence-BERT模型,例如'all-MiniLM-L6-v2'
embedding_model = SentenceTransformer('all-MiniLM-L6-v2')

# 3. 存储段落的字典 (模拟持久化存储)
paragraph_store: Dict[str, str] = {}

4.4 文档处理与索引函数

这个函数将执行:

  1. 切分文档为段落。
  2. 存储段落文本。
  3. 切分每个段落为句子。
  4. 为每个句子生成Embedding。
  5. 将句子Embedding及其元数据存入向量数据库。
def ingest_document_small_to_big(
    document_id: str,
    document_content: str,
    paragraph_store: Dict[str, str],
    vector_db_collection: chromadb.Collection,
    embedding_model: SentenceTransformer
) -> None:
    """
    将文档处理为Small-to-Big格式并索引到向量数据库。
    """
    print(f"--- 正在处理文档: {document_id} ---")

    # 1. 将文档切分为段落
    paragraphs: List[str] = paragraph_splitter.split_text(document_content)

    # 准备存储到向量数据库的数据
    sentence_embeddings: List[List[float]] = []
    sentence_ids: List[str] = []
    sentence_metadatas: List[Dict[str, str]] = []

    for para_idx, paragraph_text in enumerate(paragraphs):
        if not paragraph_text.strip():
            continue # 跳过空段落

        paragraph_id = f"{document_id}-para-{para_idx}"
        paragraph_store[paragraph_id] = paragraph_text # 存储完整的段落文本

        # 2. 将每个段落切分为句子
        sentences_in_paragraph: List[str] = sentence_splitter.split_text(paragraph_text)

        for sent_idx, sentence_text in enumerate(sentences_in_paragraph):
            if not sentence_text.strip():
                continue # 跳过空句子

            sentence_id = f"{paragraph_id}-sent-{sent_idx}"

            # 生成句子Embedding
            embedding = embedding_model.encode(sentence_text).tolist()

            sentence_embeddings.append(embedding)
            sentence_ids.append(sentence_id)
            sentence_metadatas.append({
                "document_id": document_id,
                "paragraph_id": paragraph_id,
                "sentence_text": sentence_text # 也存储句子文本,方便调试和二次确认
            })

    print(f"生成了 {len(sentence_ids)} 个句子Embedding。")

    # 3. 批量存储到向量数据库
    if sentence_ids:
        vector_db_collection.add(
            embeddings=sentence_embeddings,
            ids=sentence_ids,
            metadatas=sentence_metadatas
        )
        print(f"成功将 {len(sentence_ids)} 个句子Embedding索引到向量数据库。")
    else:
        print("没有句子可供索引。")

4.5 向量数据库初始化

我们将使用ChromaDB作为本地向量数据库。

# 初始化ChromaDB客户端
client = chromadb.PersistentClient(path="./chroma_db") # 数据将保存在当前目录下的chroma_db文件夹中

# 获取或创建集合 (Collection)
# 这是一个逻辑上的分组,用于存储具有相同Embedding模型生成的向量
collection_name = "small_to_big_rag_collection"
vector_db_collection = client.get_or_create_collection(name=collection_name)

# 清空集合(可选,每次运行时重新开始)
# try:
#     client.delete_collection(name=collection_name)
#     vector_db_collection = client.get_or_create_collection(name=collection_name)
#     print(f"集合 '{collection_name}' 已清空并重建。")
# except Exception as e:
#     print(f"无法清空集合 (可能不存在): {e}. 正在创建新集合。")
#     vector_db_collection = client.get_or_create_collection(name=collection_name)

4.6 执行索引

# 执行文档索引
ingest_document_small_to_big("doc-rag-architecture", document_content, paragraph_store, vector_db_collection, embedding_model)

print(f"n当前向量数据库中Embedding数量: {vector_db_collection.count()}")
print(f"存储的段落数量: {len(paragraph_store)}")

4.7 检索函数

这个函数将执行:

  1. 将用户查询Embedding化。
  2. 在向量数据库中搜索最相关的句子。
  3. 提取这些句子所属的paragraph_id
  4. paragraph_store中获取完整的段落文本。
  5. 返回这些段落文本作为上下文。
def retrieve_context_small_to_big(
    query: str,
    top_k_sentences: int,
    paragraph_store: Dict[str, str],
    vector_db_collection: chromadb.Collection,
    embedding_model: SentenceTransformer
) -> List[str]:
    """
    根据查询执行Small-to-Big检索,返回完整的段落上下文。
    """
    print(f"n--- 正在检索查询: '{query}' ---")

    # 1. 对查询生成Embedding
    query_embedding = embedding_model.encode(query).tolist()

    # 2. 在向量数据库中搜索最相关的句子Embedding
    # `query_embeddings` 期待一个列表的列表,即使只有一个查询
    results = vector_db_collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k_sentences,
        include=['metadatas', 'distances'] # 包含元数据和距离以便分析
    )

    retrieved_metadatas: List[Dict[str, str]] = results['metadatas'][0]
    retrieved_distances: List[float] = results['distances'][0]

    print(f"召回了 {len(retrieved_metadatas)} 个句子Embedding。")
    # for i, meta in enumerate(retrieved_metadatas):
    #     print(f"  - 句子: '{meta['sentence_text']}' (距离: {retrieved_distances[i]:.4f})")

    # 3. 提取所有召回句子对应的unique_paragraph_ids
    unique_paragraph_ids = set()
    for metadata in retrieved_metadatas:
        unique_paragraph_ids.add(metadata['paragraph_id'])

    print(f"这些句子涉及 {len(unique_paragraph_ids)} 个唯一段落。")

    # 4. 从paragraph_store中获取这些完整段落的文本
    retrieved_paragraphs: List[str] = []
    for para_id in unique_paragraph_ids:
        if para_id in paragraph_store:
            retrieved_paragraphs.append(paragraph_store[para_id])
        else:
            print(f"警告: 找不到段落ID '{para_id}' 的文本。")

    # 可以选择对段落进行排序,例如按它们第一次被引用的顺序,或者按原始文档顺序
    # 这里我们简单返回
    return retrieved_paragraphs

4.8 模拟LLM调用

def mock_llm_response(query: str, context: List[str]) -> str:
    """
    模拟LLM根据上下文生成回答。
    """
    if not context:
        return f"抱歉,我没有找到与 '{query}' 相关的信息。"

    combined_context = "nn".join(context)

    print("n--- 模拟LLM输入 ---")
    print("查询:", query)
    print("提供给LLM的上下文:n", combined_context)
    print("-------------------n")

    # 简单的模拟逻辑,实际LLM会更智能地总结
    response = f"根据提供的上下文,关于 '{query}' 的信息如下:n"
    response += "上下文提到了...n"
    response += combined_context[:200] + "...n" # 仅截取部分展示
    response += "LLM会综合这些信息生成一个详细的答案。"

    return response

4.9 完整的运行示例

if __name__ == "__main__":
    # --- 1. 索引阶段 ---
    ingest_document_small_to_big(
        "doc-rag-architecture",
        document_content,
        paragraph_store,
        vector_db_collection,
        embedding_model
    )

    # --- 2. 检索并模拟LLM交互 ---
    queries = [
        "RAG架构的目的是什么?",
        "索引阶段具体做了哪些工作?",
        "LLM有哪些局限性?",
        "如何选择Embedding模型?"
    ]

    for q in queries:
        retrieved_context = retrieve_context_small_to_big(
            q,
            top_k_sentences=3, # 召回3个最相关的句子
            paragraph_store=paragraph_store,
            vector_db_collection=vector_db_collection,
            embedding_model=embedding_model
        )

        llm_answer = mock_llm_response(q, retrieved_context)
        print("模拟LLM回答:n", llm_answer)
        print("n" + "="*80 + "n")

    # --- 清理 (可选) ---
    # client.delete_collection(name=collection_name)
    # print(f"集合 '{collection_name}' 已删除。")

代码运行输出示例(部分):

--- 正在处理文档: doc-rag-architecture ---
生成了 10 个句子Embedding。
成功将 10 个句子Embedding索引到向量数据库。

当前向量数据库中Embedding数量: 10
存储的段落数量: 5

--- 正在检索查询: 'RAG架构的目的是什么?' ---
召回了 3 个句子Embedding。
这些句子涉及 2 个唯一段落。

--- 模拟LLM输入 ---
查询: RAG架构的目的是什么?
提供给LLM的上下文:
 大型语言模型(LLM)在理解和生成人类语言方面展现出惊人的能力,但它们也存在固有的局限性。首先,LLM的知识是静态的,受限于其训练数据的截止日期,无法获取最新信息。其次,它们可能出现“幻觉”,即生成看似合理但实际上是虚构的内容。为了解决这些问题,检索增强生成(RAG)架构应运而生。

RAG的核心思想是在生成答案之前,先从外部知识库中检索相关信息,然后将这些信息作为上下文提供给LLM。这使得LLM能够基于事实生成更准确、更可靠的回答。一个典型的RAG流程包括三个主要阶段:索引(Indexing)、检索(Retrieval)和生成(Generation)。
-------------------

模拟LLM回答:
 根据提供的上下文,关于 'RAG架构的目的是什么?' 的信息如下:
上下文提到了...
大型语言模型(LLM)在理解和生成人类语言方面展现出惊人的能力,但它们也存在固有的局限性。首先,LLM的知识是静态的,受限于其训练数据的截止日期,无法获取最新信息。其次,它们可能出现“幻觉”,即生成看似合理但实际上是虚构的内容。为了解决这些问题,检索增强生成(RAG)架构应运而生。

RAG的核心思想是在生成答案之前,先从外部知识库中检索相关信息,然后将这些信息作为上下文提供给LLM。这使得LLM能够基于事实生成更准确、更可靠的回答。一个典型的RAG流程包括三个主要阶段:索引(Indexing)、检索(Retrieval)和生成(Generation)。
LLM会综合这些信息生成一个详细的答案。

================================================================================

--- 正在检索查询: '索引阶段具体做了哪些工作?' ---
召回了 3 个句子Embedding。
这些句子涉及 1 个唯一段落。

--- 模拟LLM输入 ---
查询: 索引阶段具体做了哪些工作?
提供给LLM的上下文:
 在索引阶段,原始文档被切分成小的、可管理的块(chunks)。这些块经过Embedding模型转换成高维向量,并存储在向量数据库中。Embedding模型的选择对检索性能至关重要,例如使用Sentence-BERT或OpenAI的text-embedding-ada-002。
-------------------

模拟LLM回答:
 根据提供的上下文,关于 '索引阶段具体做了哪些工作?' 的信息如下:
上下文提到了...
在索引阶段,原始文档被切分成小的、可管理的块(chunks)。这些块经过Embedding模型转换成高维向量,并存储在向量数据库中。Embedding模型的选择对检索性能至关重要,例如使用Sentence-BERT或OpenAI的text-embedding-ada-002。
LLM会综合这些信息生成一个详细的答案。

================================================================================

从输出中我们可以清晰地看到,即使我们只检索了3个句子,但最终提供给LLM的上下文是包含这些句子的完整段落,这保证了LLM获得的信息是完整且语义连贯的。

5. 进阶考量与优化

Small-to-Big Retrieval虽然强大,但在实际部署时仍需考虑一些进阶问题和优化策略。

5.1 粒度选择:如何定义“小”和“大”?

  • “小”的粒度: 句子通常是默认选择。但有时,“短语”或“子句”可能更小,而“两三个句子”的组合也可能被视为“小”。选择的原则是:确保其语义足够集中,能够精准匹配查询意图。
  • “大”的粒度: 默认是“段落”。但一个段落可能非常长,或者一个概念跨越多个段落。
    • 固定大小块: 将文档切分成固定token数量(例如256、512 token)的块,并允许一定程度的重叠。
    • 语义块: 使用更智能的算法(如基于标题、章节、语义相似度聚类)来定义逻辑上的“大块”。
    • 动态扩展: 初始检索一个段落,如果LLM反馈信息不足,可以尝试扩展到邻近的段落。

5.2 上下文窗口与Token限制

LLM的上下文窗口是有限的。即使我们返回了完整的段落,如果召回的段落过多,或者单个段落本身非常长,也可能超出LLM的输入限制。

  • 截断策略: 如果拼接后的上下文超过LLM的token限制,需要有策略地截断。是截断最不相关的段落?还是截断每个段落的尾部?
  • 摘要与压缩: 在将上下文发送给LLM之前,可以先用一个小型LLM或摘要模型对召回的段落进行摘要,提取核心信息,从而减少token数量。
  • Re-ranking: 在获取到初步的粗粒度上下文后,可以对这些上下文进行二次排序。例如,使用一个更强大的交叉编码器(Cross-Encoder)来评估查询与每个召回段落的实际相关性,将最相关的段落排在前面。

5.3 性能与成本

  • 存储成本: 存储句子级别的Embedding数量会显著多于段落级Embedding。例如,一个文档有100个段落,每个段落平均10个句子,那么你将存储1000个句子Embedding而不是100个段落Embedding。这意味着更高的存储成本和潜在的内存需求。
  • 索引时间: 生成更多Embedding并将其索引到向量数据库所需的时间也会更长。
  • 检索时间: 虽然向量搜索本身很快,但从向量数据库中提取paragraph_id后,还需要额外一步去paragraph_store中检索完整的文本。这引入了额外的I/O延迟,尤其是在paragraph_store不是内存数据库或优化良好的情况下。
  • Embedding模型选择: 强大的Embedding模型通常更大、更慢,但效果更好。需要在效果和性能之间权衡。

5.4 混合检索(Hybrid Search)

Small-to-Big可以与混合检索结合,进一步提升效果:

  • 关键词搜索 + 向量搜索: 除了句子Embedding搜索,还可以对查询进行关键词提取,并在向量数据库的元数据(sentence_textparagraph_text)中进行关键词过滤,或者结合传统搜索引擎,先筛选出包含特定关键词的段落,再进行向量搜索。
  • 多个召回器: 可以同时使用Small-to-Big和传统的Chunk-based RAG进行召回,然后将两者的结果合并、去重、排序,以获得更全面的上下文。

5.5 异常处理与边缘情况

  • 非常短的文档: 如果文档本身只有一两个段落,Small-to-Big的优势可能不明显。
  • 无意义的句子: 句子切分器有时会产生非常短、缺乏语义的句子(例如“好的。”“是。”)。这些句子生成的Embedding质量可能不高,需要过滤。
  • 跨段落的语义: 某些关键信息可能需要跨越多个段落才能理解。Small-to-Big通过召回多个段落来解决,但如果这些段落不连续,LLM理解起来可能仍有挑战。Re-ranking和动态上下文扩展可以帮助缓解此问题。

5.6 实时更新与增量索引

对于需要频繁更新知识库的场景,Small-to-Big方法需要更精细的增量索引策略。当一个文档被修改时,可能需要重新切分、Embedding并更新受影响的句子和段落。向量数据库的增删改查能力将是关键。

6. 构建更智能的检索系统

Small-to-Big Retrieval并非RAG的银弹,但它确实提供了一种强大而灵活的策略,以更精细地控制检索过程。它通过解耦搜索粒度与呈现粒度,有效解决了传统RAG中召回精度与上下文完整性的两难困境。

理解并掌握Small-to-Big Retrieval,意味着我们能够构建更智能、更鲁棒的RAG系统,为LLM提供更精准、更具上下文的外部知识,从而使其在处理复杂查询和生成高质量回答方面表现出色。未来的RAG系统将继续向多模态、自适应、可解释的方向发展,而这种对文本粒度精细化控制的思想,无疑将是其中不可或缺的一部分。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注