首页 / 文章 / RAG、无向量RAG与GraphRAG的对比

RAG、无向量RAG与GraphRAG的对比

经典向量RAG、词汇无向量检索与GraphRAG:分块处理、嵌入技术、BM25算法、多跳图结构,以及每种方法适用的场景。

1834 词

当今的语言模型是如何根据训练数据中从未出现过的资料来回答问题的?

RAG 一句话解释

检索增强生成正如其缩写所示。首先获取与用户问题相关的资料,然后模型利用这些资料加上提示词来撰写答案。其流程是先检索,再生成。

为何要这么做?

模型只了解其训练数据截止时的内容,其他一切对它们来说都是不可见的。

想象一本仅有3,000页且极少在网上出现、从未被纳入任何训练语料库的稀有书籍。向模型询问它,你会得到空白答案;即便使用爬虫工具,也依然得不到任何信息——因为根本没有可爬取的内容。

RAG填补了这一空白。将PDF加载到处理流程中,在查询时提取最相关的部分并将其作为上下文附加。模型的任务便简化为:利用语言能力从这些文本中作答。

无需微调,只需在恰当的时间提供合适的上下文即可。

处理流程框架

首先进行索引构建,而索引构建始于分块处理。

分块策略

数千页的PDF不能作为一个整体存储在向量数据库中,应将其拆分,以便每个片段都能独立嵌入和检索。

常见的拆分方式:

按页拆分。一页对应一个片段(3,000页则对应3,000个片段)。当需要精确匹配时此方法效果最佳。

按段落处理。在段落边界处进行更细致的切割。虽然可能让结果更加清晰,但长度差异极大(200个标记与2,000个标记并存)。不均匀的长度会导致嵌入向量也不均匀,进而悄悄影响检索效果。

固定窗口。无论切割点位于何处,标记数量都保持不变——通常为512。统一的长度能生成更具可比性的嵌入向量。在不确定时,大多数团队都会选择这种方式。

from langchain_text_splitters import CharacterTextSplitter
from langchain_core.documents import Document

def perform_fixed_size_chunking(document, chunk_size=1000, chunk_overlap=200😞
    """
    Performs fixed-size chunking on a document with specified overlap.

    Args:
        document (str): The text document to process
        chunk_size (int): The target size of each chunk in characters
        chunk_overlap (int): The number of characters of overlap between chunks

    Returns:
        list: The chunked documents with metadata
    """
    # Create the text splitter with optimal parameters
    text_splitter = CharacterTextSplitter(
        separator="\n\n",
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        length_function=len
    )

    # Split the text into chunks
    chunks = text_splitter.split_text(document)
    print(f"Document split into {len(chunks)} chunks")

    # Convert to Document objects with metadata
    documents = []
    for i, chunk in enumerate(chunks):
        doc = Document(
            page_content=chunk,
            metadata={
                "chunk_id": i,
                "total_chunks": len(chunks),
                "chunk_size": len(chunk),
                "chunk_type": "fixed-size"
            }
        )
        documents.append(doc)

    return documents

# Example usage
if __name__ == "__main__":

    # Create the dummy document
    document = create_dummy_document()

    # Process with fixed-size chunking
    chunked_docs = perform_fixed_size_chunking(
        document,
        chunk_size=1000,
        chunk_overlap=200
    )

    # Display results
    print("\n----- CHUNKING RESULTS -----")
    print(f"Total chunks: {len(chunked_docs)}")

    # Print an example chunk
    print("\n----- EXAMPLE CHUNK -----")
    middle_chunk_idx = len(chunked_docs) // 2
    example_chunk = chunked_docs[middle_chunk_idx]
    print(f"Chunk {middle_chunk_idx} content ({len(example_chunk.page_content)} characters):")
    print("-" * 40)
    print(example_chunk.page_content)
    print("-" * 40)
    print(f"Metadata: {example_chunk.metadata}")

    # For integration with Databricks Vector Search
    print("\nThese documents are ready for embedding and storage in Databricks Vector Search")
    print("Example next steps:")
    print("1. Create embeddings using the Databricks embedding endpoint")
    print("2. Store documents and embeddings in Delta table")
    print("3. Create Vector Search index for retrieval")

嵌入向量

嵌入向量将文本(单词、句子、页面)转换为密集的高维向量,以此编码含义,使相似的概念能够聚集在一起。

RAG中的四个阶段

  1. 语料库端。对每个文本块进行嵌入处理,并存储索引与向量。
  2. 查询端。对用户输入的提示语进行嵌入,使其具备语义特征指纹。
  3. 查找。在存储库中搜索最近的相似项——通常采用余弦相似度作为判断标准。
  • 增强功能。将检索到的段落作为额外上下文附加;模型会根据提示词与这些上下文来回答。
  • 向量存储的位置

    这是一种专门用于存储嵌入向量的数据库,而非通用的关系型或对象数据库。AlloyDB、Pinecone和Qdrant是常见的选择;许多团队则在PostgreSQL上使用pgvector。

    传统向量RAG的局限性

    1. 截取片段时可能会忽略语义含义;相关的文本窗口可能被分开;虽然较大的重叠度有时会有帮助,但并不能解决所有问题。
    2. 相似性计算可能无法识别同义表达——“销售额下降”与“公司处于衰退期”在向量空间中可能不会被归为相近内容。
    3. 当因果关系分布在不同的文本片段中且仅能检索到一个片段时,多跳事实关联就会失效。
    4. 嵌入向量的构建、存储、索引以及重新索引都需要耗费大量资源。

    另外:向量数据库与RAG并非同义词,它只是其中一种检索后端。无向量RAG仍采用“先获取再生成”的方式,但去掉了嵌入搜索功能。

    为何要放弃向量技术?因为构建/重新索引嵌入需要成本,对ID、数字或错误代码的精确匹配效果较差,且还需要额外的基础设施来支持运行。

    无向量方案其实是一类技术,并非单一固定模式:

    1. 词汇搜索。 BM25、Postgres的tsvector功能、Elasticsearch——对于SKU、引用内容及日志行而言,精确匹配的文本检索效果优于基于语义的模糊搜索。
    from rank_bm25 import BM25Okapi
    
    def vectorless_retrieve(query, corpus_chunks, top_k=3):
        """
        Lexical retrieval over raw text chunks - no embeddings, no vector DB.
        """
        tokenized_corpus = [chunk.lower().split() for chunk in corpus_chunks]
        bm25 = BM25Okapi(tokenized_corpus)
    
        tokenized_query = query.lower().split()
        scores = bm25.get_scores(tokenized_query)
    
        ranked = sorted(zip(corpus_chunks, scores), key=lambda x: x[1], reverse=True)
        return [chunk for chunk, score in ranked[:top_k]]
    
    1. 智能代理/工具检索。无需预先建立索引;模型会根据需求自行查找、调用搜索API或打开相关内容,就像编程代理在代码库中检索信息一样,属于实时、基于推理的查询方式。
  • 长上下文填充。凭借巨大的上下文窗口,较小的语料库也能被纳入提示词中。虽非传统的检索方式,但对规模较小的语料库也能取得类似效果。
  • 混合重排序。先通过廉价的词汇列表筛选,再由模型按相关性重新排序——在无需预先构建完整嵌入索引的情况下,兼顾关键词的快速检索与一定的语义细微差别。
  • 无向量方法的局限

    关键词仍无法处理同义表达——有时甚至比嵌入模型表现更差。代理循环会增加每次查询的延迟和token消耗。对于大规模语料库,构建完善的向量索引依然更为有效。无向量方法多在中小规模场景下,或当精确性优于模糊匹配时更具优势。

    GraphRAG

    传统RAG可能在不同文本块之间混淆因果关系。无向量方法则用关键词或长上下文替代嵌入向量,但两者都无法体现概念之间的关联。Graph RAG正是为弥补这一缺陷而设计的。

    不要问“哪个片段最接近?”,而应问“这些概念之间如何关联?”答案就是知识图谱。

    索引——扩展图谱

    先跳过片段/嵌入步骤,将文档输入模型以提取实体和关系。输出结果为节点和边。

    对于那本厚厚的理论著作,可能会得到诸如马克思、恩格斯、《资本论》、《共产党宣言》、剩余价值、辩证唯物主义之类的节点,以及“撰写”、“合著”、“引入概念”之类的边。

    实体变为节点,关系变为边。你是在映射意义,而非分割页面。

    将紧密关联的节点聚类为社群(莱顿聚类法很常用),然后通过另一次模型处理来总结每个社群的内容。

    在两个层面进行操作:

    • 节点用于具体事实及直接关联
    • 社群用于主题归纳与总结

    具体问题 → 节点。宽泛的“核心思想?”类问题 → 社区总结。一个索引,两种检索模式。

    查询——遍历图结构

    问题:“谁与马克思合著过作品,他们共同写了什么?”

    传统RAG方法面临困境:合著信息集中在同一部分,而相关作品可能在数百页之外,只能依赖嵌入向量是否一致。

    图结构RAG的处理方式如下:

    1. 将马克思标识为锚点
    2. 加载马克思对应的节点及关联边
    3. 通过“合著于”关系找到恩格斯
    4. 再通过恩格斯的作品关联找到《共产党宣言》《工人阶级状况》等相关作品

    有向边保持了各元素之间的关联关系。传统RAG会拆分的因果关系在图结构中则表现为相连的节点。这种多跳关联模式对于传统RAG和无需向量的RAG来说都很难准确处理。

    将节点、边及社区总结整合为上下文,然后像平常一样生成结果。

    from graphrag import GraphRAGPipeline
    
    # Indexing — runs once
    pipeline = GraphRAGPipeline(llm="claude-3", graph_store="neo4j")
    pipeline.index(documents=["book.pdf"])
    # Under the hood: entity extraction → graph build → community detection → summaries
    
    # Querying
    result = pipeline.query(
        "Who co-wrote with Marx and what did they write together?",
        mode="global"   # uses community summaries for broad questions
        # mode="local"  # uses node-level traversal for specific facts
    )
    print(result.answer)
    print(result.sources)   # returns actual nodes + edges used, fully traceable
    

    mode并非仅用于美观目的。各种实现方案(包括微软的开源技术栈)会区分全局与局部模式,因为这是两种不同的处理策略。

    GraphRAG的成本

    信息提取成本很高。必须读取整个文档;长篇书籍会消耗大量计算资源;那些隐含的引用(“如前文所述”)可能永远无法转化为连接边。

    图的质量决定了整体性能。提取质量差→图结构不佳→检索效果差。解决办法往往是需要重新读取所有内容,这在处理大规模数据时效率极低。

    实际应用中,应根据查询类型选择合适的检索方式,而非对所有问题都强制使用同一种方法。

    总结:进行语义检索时使用传统RAG,需要高精确度或希望减少基础设施投入时选择非向量方法,而当关注点在于数据之间的关系时则选用Graph RAG。

    无需固执己见地选择检索方式

    一个实用的决策流程如下所示。

    当语料库规模庞大、语言种类繁多,且通常需要近似的语义相似项时,应从经典的向量RAG技术开始。务必提前投入精力提升分块质量与嵌入更新流程,因为这些因素才是决定性能的关键。

    在精确标识符比语义改写更为重要、语料库规模小到适合使用词汇搜索或处理长上下文,或是不愿承担嵌入基础设施成本的情况下,可选择非向量技术。BM25及其同类算法并非“过时”技术——对于产品编号、引用信息及错误代码而言,它们正是合适的工具。

    当产品相关问题涉及复杂关系时——比如谁与谁有关联、哪个概念引出了哪个想法、哪个社群总结了某个主题——应采用图结构RAG技术。这类方法会导致索引成本上升,因此需将信息提取质量视为首要考量因素。

    许多团队最终采用混合方式:先进行词汇级处理,再做向量级处理,对于部分多跳域则使用图结构。关键不在于选择某一种方法,而在于让检索器与实际出现的故障模式相匹配。

    演示中省略的运营注意事项

    重新索引的时间安排很重要。即使模型本身没有变化,过时的嵌入也会悄悄降低向量RAG的性能。当相邻窗口具有相似含义时,重叠设置十分重要。如果不同租户绝不能看到彼此的数据块,元数据过滤器就不可或缺。在没有标签的情况下宣称“性能更优”时,评估集也很重要。

    对于图结构RAG,需要为文档变更时的提取重试、部分图更新以及社区重新总结做好准备。对于智能检索系统,则需设定工具使用预算和超时策略,防止好奇的模型在单次查询中耗尽所有令牌。

    这些笔记都并非什么精彩绝伦的内容。它们只是图表与能够承受一个月实际流量考验的系统之间的区别而已。