如果在过去一年中你构建过RAG(检索增强生成)系统,那么你很可能遇到过这样的问题:你的大语言模型会给出错误的答案,引用并不存在的信息,或者完全忽略那些其实存在于向量数据库中的相关上下文信息。

问题并不出在你的嵌入模型或向量存储机制上。大多数RAG系统的实现方式都将上下文视为关键词搜索问题来处理,而实际上这应该是一个语义理解问题

传统的RAG系统会将文档分割成若干片段,对这些片段进行嵌入处理,然后检索出“最相关”的片段并将其传递给大语言模型。但实际上,当这些片段失去了它们所处的上下文环境时,这种处理方式就会失效。例如,“它的增幅达到了40%”这样的句子,在不知道“它”指的是什么或这一事件发生在何时的情况下,是毫无意义的。

基于上下文的检索方法会明确保留并利用各个片段之间的关系、它们在文档中的结构以及它们所蕴含的语义意义,而不是将每个片段视为孤立的文本单元。

在这份指南中,我们将详细解释RAG系统中“上下文”的含义,分析为什么简单的片段分割方法会失败,以及现代的基于上下文的检索技术是如何解决这些问题的——同时避免对基础设施进行过度设计。

目录

  • RAG系统中的上下文是什么?

  • 简单片段分割方法存在的问题

  • 基于上下文的检索机制是如何工作的

  • 更智能的片段分割策略

  • 两阶段检索与排序机制

  • 基于图结构的上下文检索

    RAG系统中的“上下文”究竟指的是什么?

    在讨论信息检索机制之前,我们首先需要明确:“上下文”在RAG系统中到底包含哪些含义。实际上,上下文并非单一的概念,而是由多个相互关联的层面构成的。

    1. 本地块级上下文

    任何一块文本都与其周围的直接文本内容紧密相关。如果没有这些上下文信息,“如上所述”或“这种方法”之类的表述就会失去意义。

    可能出现的问题:假设某块文本声称“这样做可以将延迟降低60%”,但却没有说明这里的“这样做”具体指的是从EBS切换到本地NVMe存储方案,而这一细节其实早在前两段文字中就已经被提及过。

    2. 文档结构级上下文

    文档结构级上下文涉及该文本所属文档的类型、章节位置、内容性质(例如是API文档还是博客文章)、用途以及目标读者群体等信息。

    可能出现的问题:当用户询问当前的最佳实践方案时,如果大型语言模型从2023年的淘汰通知中检索相关内容,虽然这些信息曾经是相关的,但由于时间背景的变化,现在获取到的信息可能会完全错误。

    3. 语义级上下文

    语义级上下文指的是整个知识体系中各个概念之间的关联关系。例如,某一块文本在语义上如何与其他文本相互关联,即使这些文本分布在不同的文档中也是如此。

    可能出现的问题:当用户询问“如何优化冷启动过程”时,系统可能会检索到关于Lambda函数的资料,但却忽略了与VPC配置、并发处理能力以及SnapStart技术相关的关键信息,因为这些内容分别存在于不同的文档中,且没有共同的关键词来连接它们。

    大多数RAG系统的实现仅关注第一种类型的上下文,而真正的智能检索系统则会同时考虑这三种类型的上下文信息。

    简单的分块方法存在的问题

    传统的RAG系统遵循一种简单的处理流程:

    1. 将文档分割成固定大小的块(例如每个块包含512个词条,相邻块之间有50个词条的重叠部分)

    2. 为每个分割出的块生成嵌入向量

    3. 将这些嵌入向量存储在向量数据库中

    4. 在查询时:将查询语句转换为嵌入向量,找到最接近的匹配结果,然后返回前k个结果

    5. 将这些结果插入到大型语言模型的输入框中

    这种处理方法在早期的演示中效果还算不错,但在实际应用中却会遇到很多问题。

    为什么固定大小的分块方法会失效?

    以技术文档的分割为例,如果使用简单的固定大小分块方法,可能会得到如下结果:

    块1:

    我们在z3-highmem-14实例上进行的测试结果。MongoDB配置使用了WiredTiger存储引擎,并配备了100GB的缓存空间。
    
    测试方法:
    我们使用了YCSB 0.18.0测试工具,测试数据包含10亿条记录,这些记录在数据库中均匀分布。每次测试都会执行200万次操作,同时会调整线程数量来进行测试。

    块2:

    当线程数量不同时,读取吞吐量会发生变化:对于MongoDB而言,当线程数为8,000时吞吐量达到峰值;而对于FerretDB来说,这一数值为10,000 QPS。然而,由于EloqDoc使用了本地的NVMe存储设备而非网络连接的磁盘,因此在512个线程的情况下,其吞吐量竟然达到了129,000 QPS。

    看到这个问题了吗?

    • 第1部分包含了关键的设置信息,但这些信息在描述过程中被截断了。

    • 第2部分以“不同的线程数量”这一表述开头(如果没有第1部分,这部分内容就毫无意义),并且提到了“使用本地的NVMe存储设备”,但却没有解释到底是指什么。

    • 最关键的信息(即EloqDoc在性能上比其他系统高出16倍)却是用一个代词来指代的,而这个代词实际上引用了第3部分中的内容。

    当有人搜索“数据库性能对比”时,他们可能会看到第2部分的内容,其中自信地提到了“129,000 QPS”这一数值,但却没有任何关于该数据对应的是哪种系统、测试了什么样的工作负载,或者它与其他方案相比有何优劣的说明。

    为什么仅仅依靠部分内容重叠无法解决问题

    许多开发者认为在各个数据块之间设置10%到20%的内容重叠就能解决这些问题,但实际上这并不能起到任何作用。内容重叠确实有助于处理边界划分问题(比如避免将句子分割得支离破碎),但对于语义上的连贯性却没有任何帮助。如果相关的上下文信息距离当前所在的位置有200个或500个词条那么远,内容重叠仍然无法起到任何作用。

    常见的错误模式

    在实际应用中,RAG系统经常会遇到以下这些错误模式,你的系统也可能会遇到类似的问题:

    1. 代词使用混乱:“它同时支持这两种模式”——但“它”到底指的是什么?

    2. 缺乏上下文的比较

      :“这种方法的效率是原来的3倍”——但是它是相对于什么来说效率更高的呢?

    3. 步骤顺序混乱

      :教程中的第3步被放在了与第1步和第2步不同的数据块中。

    4. 时间线索丢失

      :“从上个季度开始……”——但是是哪个季度呢?

    5. 前提条件缺失

      :代码假设某些库已经存在于其他数据块中,但实际上这些库并没有被导入。

    根本问题在于,固定大小的数据分割方式将文档仅仅视为需要被切割的字符串,而不是具有明确语义界限的结构化信息。

    上下文检索的工作原理

    上下文检索通过在创建数据块时就明确保留并利用相关上下文信息,从而解决了这些问题。关键在于:你无法在事后重新获取丢失的上下文信息——必须在将数据块嵌入索引之前,就将这些上下文信息直接包含到数据块中。

    可以这样理解:简单的文本分割就像是从书中随意撕下一些页面;而上下文检索则更像是在阅读一本书的同时,为每一章写一份摘要,这样每一页的内容在单独来看时都是通顺易懂的。

    Anthropics公司的上下文嵌入技术

    Anthropic在2024年底发布了一种名为上下文检索的技术详见此处,该技术的目的在于提升RAG系统的准确率。其实现方法是在对文本片段进行嵌入处理之前,先为该片段添加一段简短的上下文摘要,用以说明该片段的含义及其在文档中的位置。

    以下是其在实际应用中的运作方式:

    原始数据块(朴素型RAG):

    在不同的线程数下进行测试。MongoDB的读取吞吐量在8,000 QPS时达到峰值,FerretDB则为10,000 QPS。然而,由于EloqDoc使用了本地的NVMe存储而非网络连接的磁盘,因此在512个线程的情况下,其读取吞吐量高达129,000 QPS。

    上下文关联数据块(基于上下文的RAG):

    这个数据块来源于2026年1月进行的一项数据库性能测试。该测试比较了MongoDB、FerretDB和EloqDoc在包含10亿条记录的1TB数据集上的表现,重点分析了高并发环境下的读取吞吐量情况。

    当这个数据块被嵌入到检索系统中后,其向量表示形式会包含相关的上下文信息。因此,当用户搜索“2026年的数据库读取性能”时,系统就能更准确地匹配相关结果,因为这种嵌入方式既考虑了数据的内容,也考虑了它的上下文背景。

    利用大语言模型生成上下文关联的摘要

    关键在于如何高效地生成这些上下文相关的摘要。Anthropic采用的方法是使用像Claude这样的大语言模型,并向它提供如下格式的提示:

    这里是完整的文档内容:
    <document>
    {{FULL DOCUMENT}}
    </document>
    
    而我们需要确定这个数据块在文档中的位置:
    <chunk>>
    {{CHUNK_CONTENT}}
    <>/chunk>>
    
    请提供一个简短的上下文描述(2到3句话),说明这个数据块的内容以及它在文档中的位置。这样的描述会被添加到数据块的前面,从而帮助提高检索效果。

    大语言模型会先读取完整的文档内容以及具体的数据块,然后生成一个总结性文字,说明该数据块在其整体上下文中的位置。在将数据块嵌入到检索系统中之前,这个总结性文字会被添加到数据块的前面。

    混合检索:BM25与上下文嵌入技术的结合

    Anthropic的研究还发现,将上下文嵌入技术与传统的BM25搜索算法结合起来使用,其效果远优于单独使用这两种技术。原因在于,上下文嵌入技术能够捕捉数据的语义含义,而BM25则能精确匹配关键词。

    以下是一个混合检索技术能够高效发挥作用的实际场景:

    用户查询:“2026年Claude Sonnet API的定价是多少?”

    • BM25搜索结果:会找到那些包含“定价”、“Claude Sonnet”、“API”和“2026”这些关键词的数据块。

    • 语义分析结果:会找到与账单、费用、API套餐等相关的内容,即使这些内容中没有使用上述关键词。

    • 混合检索结果:会结合这两种搜索方式的结果,优先显示那些在语义上符合要求且同时包含关键术语的数据块。

    实现模式

    实际的操作流程非常简单:就是根据有意义的语义边界将文档分割成若干部分。对于每一部分,都会使用大语言模型生成简短的上下文摘要,并将其添加到该部分之前再进行嵌入处理。最终,你需要将带有上下文信息的嵌入结果以及原始文档片段一起存储在向量存储系统中。

    在进行检索时,可以采用一种混合方法:先结合BM25算法与向量相似性分析来对搜索结果进行排序,然后再使用专门的模型来评估这些结果的相关性。最后,只需将原始文档片段传递给大语言模型即可,这样就能最大限度地减少输入数据的长度。虽然上下文摘要能够提高检索的准确性,但在大语言模型生成答案的过程中其实并不需要这些摘要信息。

    更智能的分割策略

    当分割文档是基于其结构而非固定的字符数量时,基于上下文的检索方式会显得更为有效。

    三种改进文档分割的方法

    1. 语义分割:这种方法会根据句子的含义来进行分割,并在相似度下降的地方划定边界。像LangChain这样的工具包已经提供了这种功能(参考链接):

    from bs4 import Tag
    from langchain_text_splitters import HTMLSemanticPreservingSplitter
    
    headers_to_split_on = [
        ("h1", "标题 1"),
        ("h2", "标题 2"),
    ]
    
    def code_handler(element: Tag) -> str:
        data_lang = element.get("data-lang")
        code_format = f"<code:{data_lang}>{element.get_text()}</code>"
    
        return code_format
    
    splitter = HTMLSemanticPreservingSplitter(
        headers_to_split_on=headers_to_split_on,
        separators=["\n\n", "\n", ". ", "! ", "? "],
        max_chunk_size=50,
        preserve_images=True,
        preserve_videos=True,
        elements_to_preserve=["table", "ul", "ol", "code"],
        denylist_tags=["script", "style", "head"],
        customhandlers={"code": code_handler},
    )
    
    documents = splitter.split_text(html_string)
    

    2. 结构分割:这种方法会利用文档的结构(如标题、段落、代码块等)作为自然的分割边界(参考链接):

    from langchain_text_splitters import MarkdownHeaderTextSplitter
    
    markdown_document = "# Foo\n\n    ## Bar\n\nHi this is Jim\n\nHi this is Joe\n\n ### Boo\n\n Hi this is Lance\n\n ## Baz\n\n Hi this is Molly"
    
    headers_to_split_on = [
        ("#", "标题 1"),
        ("##", "标题 2"),
        ("###", "标题 3"),
    ]
    
    markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
    md_header_splits = markdown_splitter.split_text(markdown_document)
    

    3. 基于大语言模型的分割:这种方法会利用大语言模型来识别文档中的逻辑分界点。虽然这种方式计算成本较高,但能为高风险的应用场景(如医疗、法律、金融领域)生成质量最高的文档片段(参考链接)。

    重排序:一种两阶段检索方法

    即便使用了上下文嵌入技术和智能分块技术,仅依靠向量相似性也是不够的。这时,重排序就派上了用场。

    重排序是一个两阶段的检索过程:首先检索出大量的候选内容(例如前100个分块),然后使用另一个模型对这些候选内容进行重新排序,最终返回真正的排名前k的内容。

    这种方法的原理在于:第一阶段的检索器(向量搜索)速度很快,但准确性不高,它会覆盖范围很广的候选信息;而重排序器虽然速度较慢,但却非常精确,它会仔细分析每个候选内容与查询词之间的关联,并对其进行恰当的评分。

    为什么重排序如此重要

    向量嵌入技术确实能够捕捉到语义相似性,但它们无法反映相关性。某个分块可能与查询词在语义上很相似,但实际上并不能回答用户的疑问。

    假设你问“如何减少Lambda函数中的‘冷启动’现象?”使用普通的向量搜索技术可能会返回很多结果,其中一些内容会解释什么是“冷启动”,而另一些则可能提到Lambda的命名规范、无关的基准测试信息、并发配置设置,或者简要介绍SnapStart功能。

    单纯的向量相似性排序只会根据内容中出现的共同词汇来对结果进行排序,往往会导致所有相关内容被平等对待。而重排序器则会逐一评估每一对查询词与文档的内容,将真正有用的信息排在前面,将模糊或无关的内容排在后面。使用经过重排序后的结果,大型语言模型就能获得更加精确的输入信息,从而将笼统的回答转化为关于并发配置、SnapStart等功能的具体指导。

    以下是一个代码示例,展示了重排序过程的具体实现:

    from cohere import Client
    
    co = Client(api_key="...")
    query = "如何减少Lambda函数中的‘冷启动’现象?"
    
    # 第一阶段:进行广泛搜索
    candidates = vector_store.similarity_search(query, k=100)
    
    # 第二阶段:根据相关性重新排序,而不仅仅是相似性
    documents = [chunk.page_content for chunk in candidates]
    reranked = co.rerank(
        model="rerank-english-v3.0",
        query=query,
        documents=documents,
        top_n=5,
    )
    
    top_chunks = [candidates[result.index] for result in reranked.results]
    

    专门为重排序而设计的模型能够根据查询词和文档共同来预测内容的相关性。与那些在索引过程中仅将每个分块单独考虑的通用嵌入模型相比,这类模型在这项任务上表现得要好得多。

    基于图的上下文检索

    相对于基于分块的RAG技术,基于图的检索是一种新兴的替代方案。在这种方法中,知识库被建模为由实体及其相互关系构成的图结构。

    为什么图结构如此有效

    即使使用了上下文嵌入技术,分块仍然属于孤立的单元;而图结构则能够明确地表现信息之间的关联关系。

    示例:对于一家公司的内部文档而言,其中包含了与“凤凰项目”、“项目负责人莎拉·陈”以及“2025年第四季度的发展规划”等相关内容,如果这些信息之间没有明确提及彼此,那么向量数据库就无法将它们关联起来。

    通过构建图结构,你可以创建节点(实体)和边(关系):例如,“Sarah Chen” → “leads” → “Project Phoenix” → “part_of” → “Q4 2025 Roadmap”。当有人询问“Sarah正在从事哪些项目?”时,你可以通过遍历这个图结构,在一次查询中获取所有相关的信息。

    你可以将这种方法与向量搜索结合使用:图结构提供结构化的背景信息,而嵌入模型则实现语义层面的匹配。这种混合查询的方式可能如下所示:

    def retrieve_with_graph(query: str, top_k: int = 5):
    # 第一步:通过向量搜索找到起始节点
    seed_chunks = vector_store.similarity_search(query, k=20)
    seed_entities = extractentities(seed_chunks)

    # 第二步:通过图结构进行扩展查询
    related = graph.traverse(
    start_nodes=seed_entities,
    max_hops=2,
    edge_types=["leads", "part_of", "uses"],
    )

    # 第三步:将图结构中的信息与原始数据合并
    context_bundle = merge_chunks_and_relationships(seed_chunks, related)
    return contextbundle[:top_k]

    在这种机制中,向量搜索会找到“Sarah Chen”这个实体,而图结构查询则会进一步扩展到与之相关的节点,如“Project Phoenix”、“Q4 Roadmap”等。这样就能为大型语言模型提供结构化、相互关联的信息,而不是零散的、无关联的文本片段。

    常见误区及避免方法

    在构建实际的RAG系统时,以下是一些可能会出现的错误:

    • 过度优化嵌入模型,却忽视数据分块的质量:虽然人们非常关注嵌入模型的性能,但却使用了固定大小的数据分块方式。实际上,数据分块的质量比嵌入模型的质量更为重要。解决办法是首先重视语义/结构上的数据分块工作。

    • 忽略元数据的作用:即使你的向量数据库支持使用元数据进行过滤,你也应该充分利用这些功能。像{document_type: "api_docs", last_updated: "2026-03"}这样的简单元数据信息,能够显著提升搜索效果。解决办法是在添加文档时收集详细的元数据,并利用它们来过滤查询结果。

    • 采用一次性检索方式:更高效的系统会采用迭代检索的方式。也就是说,它们会先获取部分信息,生成一个初步的答案,如果需要的话,还会再次进行检索,最终才给出完整的回复。为了实现这种机制,你可以使用像AutoGPT这样的智能框架。

    • 没有备用方案

      :当检索结果中没有任何相关的信息时,大多数系统会直接向大型语言模型传递空数据,导致模型产生错误的输出。解决办法是设置一个阈值,如果查询得分低于这个阈值,就直接回复“我没有足够的信息”。

    上下文才是关键

    如果说这份指南有什么核心观点的话,那就是:在RAG系统中,上下文不仅仅是一种可选的功能,它是确保输出质量的关键因素

    在RAG技术刚出现、人们对其期望还不高的时候,简单的信息分割方法以及纯粹的向量相似性搜索技术确实能够取得不错的效果。但到了2026年,用户已经期待得到准确、完整且基于实际数据的检索结果。使用固定大小的信息片段以及简单的最近邻搜索算法是无法满足这些需求的。

    无论是通过上下文嵌入技术、基于图的结构化方法,还是混合式技术, contextual retrieval都能有效地保留并利用信息片段之间的关联关系、它们在文档结构中的位置以及它们所蕴含的语义意义。

    你可以从简单入手:为现有的信息片段添加上下文嵌入信息,再引入重新排序机制,并将信息分割的方式从固定大小改为基于语义的分析方式。仅仅这三种改变,就能显著提升检索效果。

    检索系统的作用是填补用户知识体系中的空白。但如果该系统还负责生成广告创意或社交媒体内容,那么这些输出结果仍然需要一个稳定的模板作为依据来进行呈现。基于模板的内容创作平台通过REST接口或MCP技术,提供了参数化模板来解决这一问题。

Comments are closed.