首页 / 文章

RAG中的元数据增强:过滤器、引用、新鲜度与访问控制

为何RAG系统会返回错误地区的政策,以及如何在数据采集时附带来源、机构、时间和安全元数据来修正检索结果。

发布者:REACTAPP.TOP阅读约 14 分钟3163 词

来源: Metadata Enrichment in RAG: The Secret Ingredient for Better Retrieval

假设某个团队为公司开发了一个基于检索增强的生成式聊天机器人。他们花了数天时间选择嵌入模型,配置向量数据库,并在其上部署强大的大型语言模型。对成果颇为满意后,有人输入了一个关于欧洲办事处员工产假政策的问题。

答案很快便出现了:员工有权享受20周的带薪产假。这看起来似乎正确。但随后通过源文件核对后发现,检索到的文本实际上是美国员工的政策,而非欧洲的。检索系统虽然找到了有关产假的内容,却因分块和索引过程中丢失了上下文,而无法判断其所属地区。

这是RAG系统中最为常见的故障之一。大多数关于RAG的讨论都集中在嵌入模型、分块策略和向量数据库上,而影响检索质量的关键因素之一却被忽视了:元数据增强。元数据能让检索系统不仅了解某段文本的内容,还能知晓其来源、适用对象、生成时间以及在其他资料中的位置。在许多企业应用中,它能够区分仅与主题相关的内容与真正准确的内容。

仅基于内容的检索会出现哪些问题

看看初学者使用的RAG系统通常是如何运作的。某机构拥有三份政策文件:一份针对美国员工的,一份针对欧洲员工的,还有一份针对亚太地区员工的。这三份文件都有关于产假的内容。经过分块处理后,向量数据库中存储的句子几乎完全相同,仅数值有所差异:一份规定为20周,另一份为16周,第三份则为26周。

对嵌入模型而言,这些分块内容极为相似,因为它们都在讨论同一个主题。如果询问产假相关内容,检索系统可能会返回其中任何一份。该模型能够理解语义含义,但无法自动把握业务背景。正因如此,元数据就显得至关重要。

什么是元数据

元数据是关于数据的数据。在RAG系统中,它用于描述分块内容,但并不属于该分块的主要内容。它涵盖了与文本相关的所有事实,但并非文本本身的表述内容。

取开头示例中的那段内容:

Employees are entitled to 20 weeks of paid maternity leave.

单独来看,那句话清晰完整,内容也给出了答案。但它没有说明该内容出自哪份文件、描述的是哪个地区的政策、由谁负责保持其准确性,以及上次审核的时间。这段文字只能回答政策的内容是什么,却无法判断该政策是否适合特定人的情况,而这正是当有人用美国的相关内容来询问欧洲问题时出错的原因。

现在是在相同内容中添加了元数据后的版本:

{
  "source": "EU_HR_Policy.pdf",
  "region": "Europe",
  "department": "Human Resources",
  "last_updated": "2025-03-15"
}

这四个字段均不属于句子的一部分。“Europe”这个词以及文件名在实际文本中根本不存在。然而,检索系统现在有了更多可利用的信息。这些片段包含了可用于检索、过滤、排序和生成响应的上下文,而且这些上下文与内容分开存在,不会混入其中。

正是这种分离赋予了元数据强大的功能。按照设计,嵌入模型仅会对片段的page_content进行编码,而永远不会看到元数据。元数据并非用于影响片段的嵌入方式,它的存在是为了让系统的其他部分——包括过滤器、访问权限检查、引用机制以及排序逻辑——无论嵌入模型对语义相似度的判断如何,都能有具体的依据来执行操作。

一个简单的思维模型:元数据就是附加在每条知识上的标签集合。没有标签的话,每段内容就只是脱离上下文的孤立意义,检索系统只能依靠相似度来猜测,就像当三个区域政策在嵌入模型中看起来几乎相同时那样。有了标签之后,猜测就被系统可以直接验证的内容所取代。问题也从“某段内容是否与查询语句相似”转变为“它是否真的属于所询问的场景”。

元数据如何提升检索效果

再回到产假案例,分别查看有元数据和无元数据时的检索过程。

在没有元数据的情况下,关于欧洲员工的查询会针对整个向量数据库进行。此时仅考虑语义相似度,由于三个区域数据块与嵌入向量的相似度极高,检索系统可能会错误地返回来自其他区域的政策文本。相似度得分中没有任何迹象表明存在问题。

而拥有元数据的系统则可以在相似度搜索之前或同时应用过滤机制:

filter = {
    "region": "Europe"
}

现在的搜索仅限于标记有“欧洲”标签的数据块。检索系统无需扫描所有政策文件,只需查看相关子集,这样在相似度判断可能出错之前,候选范围就已缩小。

过滤的实现方式很重要。大多数生产级向量数据库都支持预过滤,即先应用元数据过滤,仅对匹配的数据块进行相似度搜索。这种方式比后过滤更快、更可靠,因为后过滤会遍历所有数据后再丢弃不匹配的结果。后过滤可能会在无关数据上浪费计算资源,而在极端情况下,如果太多高匹配度结果在后期被过滤掉,返回的结果数量可能会少于预期。Qdrant和Weaviate都是为支持高效的预过滤而设计的。

以下是从头到尾具体呈现的产假流程:

from sentence_transformers import SentenceTransformer
import numpy as np

embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
chunks = [
    {"text": "Employees are entitled to 20 weeks of paid maternity leave.", "region": "United States"},
    {"text": "Employees are entitled to 16 weeks of paid maternity leave.", "region": "Europe"},
    {"text": "Employees are entitled to 26 weeks of paid maternity leave.", "region": "Asia-Pacific"},
]

for c in chunks:
    c["embedding"] = embedding_model.encode(c["text"])

query = "What is the maternity leave policy for employees in the European office?"
query_embedding = embedding_model.encode(query)

def search(chunks, query_embedding, region_filter=None):
    candidates = chunks if region_filter is None else [c for c in chunks if c["region"] == region_filter]
    scored = [(np.dot(query_embedding, c["embedding"]), c) for c in candidates]
    scored.sort(key=lambda x: x[0], reverse=True)
    return scored[0][1]

print("Without metadata filter:")
result = search(chunks, query_embedding)
print(f"  Returned: '{result['text']}' (region: {result['region']})")
print("\nWith metadata filter (region='Europe'):")
result = search(chunks, query_embedding, region_filter="Europe")
print(f"  Returned: '{result['text']}' (region: {result['region']})")

这直接导致了初始阶段的失败。在没有过滤机制的情况下,仅凭相似度就会选择美国的相关内容,只是因为其得分稍高,而非因为它确实适合该问题。一旦应用了地区过滤,搜索范围就会缩小到唯一的欧洲相关内容,这样就能得到正确答案,即便其他嵌入向量的相似度很高也是如此。

精确度会有显著提升,而且随着语料库规模的扩大,这种提升幅度还会进一步增大。在三份文档中,嵌入模型可能仅凭运气就将正确答案排在首位。但在涵盖多个地区、部门和年份的数百份文档中,仅依靠相似度已不可靠,而对元数据进行过滤才是剔除所有无关内容的唯一可靠方法。

值得保留的四种元数据

并非所有元数据都具有同等的作用。在真实的企业系统中,有四类元数据出现频率极高,且每类都用于解决不同的问题。

来源元数据

这类元数据用于说明信息的来源。常见的字段包括文件名、页码、URL以及文档编号。当用户需要引用来源或验证答案时,这类元数据就显得非常重要。以下是引用信息的生成方式:

def format_citation(metadata):
    return f"Source: {metadata['source']}, Page {metadata['page']}"

chunk_metadata = {"source": "Employee_Handbook_2025.pdf", "page": 42}
print(format_citation(chunk_metadata))

将打印:

Source: Employee_Handbook_2025.pdf, Page 42

那个引用字符串并非由大型语言模型猜测答案来源而生成,而是对文档加载时捕获的字段进行确定性查询的结果。当用户能够追溯信息的来源时,信任度就会提升,而这取决于从信息采集到最终响应的整个过程中元数据都能得到保留。

组织元数据

企业知识通常分散在人力资源、财务、法律和工程等不同的部门及业务单元中。通过这种方式对内容进行标记,可以在运行相似度匹配之前,利用上述相同的过滤机制将搜索范围缩小到企业中相关的部分:

filter = {"department": "Finance"}

为金融相关问题搜索工程文档会浪费时间,但还有另一个问题:用金融查询在工程文本中检索时,可能会得到一些明显错误且与主题无关的内容,尽管其在原始相似度评分上却显得很高。

时间元数据

随着政策更新、流程演变以及法规变动,信息也会随之改变。如果没有时间戳,检索系统就无法区分现行政策与过时政策。它只能判断哪些内容在语义上与查询相近,而无法判断哪些内容仍然有效。

试想两个关于同一政策但更新日期不同的内容片段:

chunks = [
    {"text": "Employees receive 15 vacation days per year.", "last_updated": "2023-01-10"},
    {"text": "Employees receive 20 vacation days per year.", "last_updated": "2025-03-15"},
]

在关于休假天数的问题上,两者的得分很可能非常接近,因为主题和表述几乎完全一致。通过存储时间戳,系统可以刻意优先考虑较新的数据,而非依赖随机性:

most_recent = max(chunks, key=lambda c: c["last_updated"])
print(most_recent["text"])

结果:

Employees receive 20 vacation days per year.

在信息频繁变化的行业中,这一点至关重要。如果没有明确的最新性检查,对于仅通过相似度来判断的检索系统而言,旧政策与其替代版本在功能上毫无区别。

安全元数据

许多组织需要基于角色的访问控制:人力资源文件仅对人力资源人员可见,财务报告仅对财务团队可见。元数据使得这种控制得以实现。一个数据块可能包含如下字段:

chunk_metadata = {
    "access_level": "confidential",
    "allowed_roles": ["HR_Manager", "HR_Admin"]
}

在检索之前,系统会将请求用户的角色与有权查看该数据块的角色进行比对:

def can_access(user_role, chunk_metadata):
    return user_role in chunk_metadata["allowed_roles"]

print(can_access("HR_Manager", chunk_metadata))   # True
print(can_access("Engineering", chunk_metadata))  # False

输出:

True
False

在真实的处理流程中,这项检查会在内容检索时作为过滤器执行,而非在大型语言模型已经获取到内容后才进行。用户无权查看的片段绝不应进入提示词阶段。如果没有元数据,构建安全的RAG系统将极其困难,因为每个片段都没有可用的可靠标识,系统无法在决定是否检索之前将其与用户的权限进行比对。

在数据导入时添加元数据

元数据不会自动出现,必须在文档导入时添加;如果未能在此阶段捕获,就无法再找回,因为当片段被用于嵌入或存储时,其来源链接早已丢失。

以正在被加载的HR政策PDF为例,常规的导入流程会按顺序执行以下步骤:

  1. 加载文档。
  2. 提取文本。
  3. 将内容分割成片段。
  4. 附加元数据。
  5. 生成嵌入向量。
  6. 将分块内容、嵌入向量及元数据写入向量数据库。

元数据应在分块之后添加,而非之前,顺序很重要。元数据必须被添加到文档被分割成的每个单独分块中,而不能仅附加在整个文档上。

from langchain_core.documents import Document

doc = Document(
    page_content="Employees receive 20 weeks of maternity leave.",
    metadata={
        "department": "HR",
        "region": "Europe",
        "source": "EU_HR_Policy.pdf"
    }
)

print(doc.page_content)
print(doc.metadata)

输出:

Employees receive 20 weeks of maternity leave.
{'department': 'HR', 'region': 'Europe', 'source': 'EU_HR_Policy.pdf'}

当文档大小足够小、可以保持为一个整体时,这很简单。然而,真实的文档会被分割成许多部分,因此元数据必须复制到每一个部分中:

def chunk_with_metadata(document, chunk_size=60):
    text = document.page_content
    chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
    return [
        Document(page_content=chunk_text, metadata=document.metadata)
        for chunk_text in chunks
    ]

source_doc = Document(
    page_content="Employees receive 20 weeks of maternity leave. Eligibility begins after six months of employment.",
    metadata={"department": "HR", "region": "Europe", "source": "EU_HR_Policy.pdf"}
)
chunked_docs = chunk_with_metadata(source_doc)
for i, chunk in enumerate(chunked_docs, 1):
    print(f"Chunk {i}: {chunk.page_content!r}")
    print(f"  Metadata: {chunk.metadata}\n")

输出:

Chunk 1: 'Employees receive 20 weeks of maternity leave. Eligibility b'
  Metadata: {'department': 'HR', 'region': 'Europe', 'source': 'EU_HR_Policy.pdf'}
Chunk 2: 'egins after six months of employment.'
  Metadata: {'department': 'HR', 'region': 'Europe', 'source': 'EU_HR_Policy.pdf'}

尽管这两个片段属于同一文档的不同部分,但它们继承了完全相同的区域、部门及来源值。这正是上述过滤示例能够生效的原因:无论文本如何被分割,每个片段都能表明自己属于欧洲人力资源政策的一部分。通过检索、过滤和引用,每个片段除了嵌入向量外还包含了有价值的上下文信息。

让大语言模型生成元数据

现代RAG系统越来越多地利用大语言模型来生成元数据,而非完全依赖人工标注。无需人工逐份阅读文档并手动分配标签,模型即可分析文档并确定主题、部门、类别、关键词及摘要等字段。下面的代码展示了用于生成元数据的提示词,而不仅仅是输出结果:

def generate_metadata(text, llm):
    prompt = f"""Analyze the following document and return metadata as JSON with these fields:
topic, category, and keywords (a list of 3-5 relevant terms).

Document:
{text}
Respond with only the JSON, no other text."""
    response = llm.generate(prompt)
    return response

sample_text = "Employees are entitled to 20 weeks of paid maternity leave, with eligibility beginning after six months of continuous employment. Additional unpaid leave may be requested with manager approval."
# In practice, llm.generate() would call an actual model (Claude, GPT, etc.)
# Below is the kind of output this prompt is designed to produce:

example_output = {
    "topic": "Employee Benefits",
    "category": "HR Policy",
    "keywords": ["maternity leave", "eligibility", "employee benefits"]
}

print(example_output)

输出:

{'topic': 'Employee Benefits', 'category': 'HR Policy', 'keywords': ['maternity leave', 'eligibility', 'employee benefits']}

这些元数据会像手动设置的元数据一样被附加到数据块上。唯一的不同在于决定这些值的人不同。这样一来,无需过多人工操作即可获得更优质的检索体验,尤其是在处理大规模数据时:手动为少量文档添加标签是可行的,但要为不断增长的知识库中的数万个数据块添加标签通常不可行。越来越多的高级处理流程将此类操作视为常规预处理步骤,在数据导入时与分块和嵌入操作一同执行。

一个实际需要注意的问题是:要对大语言模型生成的输出进行抽查,尤其是在初期阶段。模型有时会为内容分配与实际不符的主题或类别,而且与人类误标单个文档不同,如果提示词存在系统性缺陷,自动化错误可能会同时影响大量文档而无人察觉。

元数据过滤与混合检索

当元数据与语义搜索结合时,其作用最为强大,不过明确这种组合的方式有助于更好地理解其效果,因为在RAG技术中,“混合”一词可指多种不同的技术。

严格来说,混合检索意味着将两种搜索方法——密集向量搜索和稀疏关键词搜索(BM25)——结合使用,同时运行这两种方法并合并结果。而元数据过滤则是另一回事,它并非另一种搜索方法,而是一种在已使用的任何搜索方法或方法组合之前或同时,用来缩小候选集的方法。

向量搜索用于判断哪些片段在语义上与查询相似。元数据过滤则用于筛选出属于正确类别、区域、部门或时间段的片段。这两个过程相互独立,因此无论是在单纯的向量搜索、单独的BM25算法,还是完整的混合检索系统中,过滤机制都能发挥同等作用。它是一种可以叠加在任意上述检索方式之上的额外层。

正如产假示例所示,二者结合能显著提升检索质量。系统不再盲目地在整个知识库中搜索,而是智能地在最相关的子集中进行查找,仅通过相似度来决定该已缩小范围且正确的集合内的排序顺序。正因如此,现代企业RAG系统很少仅依赖嵌入向量:嵌入向量负责判断内容之间的相似性,而元数据则决定哪些内容具备被考虑的资格。

被忽视的关键因素

许多机构在挑选嵌入模型和向量数据库时投入了大量精力。这些选择固然重要,但实际上,检索失败往往源于一个更简单的原因:无论模型或数据库多么优秀,关键的语境信息始终没有被捕捉到。

想象一下,某个图书馆里的每本书都丢失了书名、作者、出版日期和分类信息,只剩下原始文本。即便拥有出色的书架系统和检索目录,要找到任何内容也会极为困难。不带元数据存储数据块也是如此:即便检索基础设施再完美,如果没有可供其参考的语境信息,依然无法发挥作用。

元数据提供了组织结构,使得检索系统能够在庞大的知识库中高效导航。随着数据集从数百份文档增长到数百万个数据块,元数据的重要性愈发凸显。在规模较小的情况下,嵌入模型可能仅凭运气就将正确的数据块排在首位,就像它对三份政策文件所做的那样。但在真实的企业知识库中,这种运气很快就会用尽。许多企业团队表示,提升元数据质量带来的收益远高于更换更先进的嵌入模型,鉴于目前人们对模型选择的过度关注以及对元数据策略的忽视,这一点值得牢记。

要点总结

为文本块添加元数据是提升RAG系统性能的最佳方法之一,但相比嵌入模型、向量数据库和大型语言模型,它受到的关注要少得多。嵌入模型能够捕捉内容的含义,而元数据则能记录其周围的上下文,二者结合能让系统不仅找到相似的信息,还能找到最准确的信息。无论是追踪信息来源、按地区筛选、保障安全、记录时间戳,还是添加人工智能生成的标签,元数据都能帮助将简单的检索系统转变为可投入实际使用的知识平台。

因此,在评估RAG流程时,不仅要考虑使用哪种嵌入模型,还要考虑应为文本块添加哪些元数据,因为在许多实际系统中,更优质的元数据意味着更出色的检索效果,更好的检索效果则能带来更准确的答案,而准确的答案自然能为用户带来更佳的体验。

相关阅读

  • 用于提升RAG检索效果的父子分块与层次化索引——为何小分块虽能提高匹配率却无法获得准确答案,以及父子分块、去重扩展和层次化索引如何为RAG检索恢复上下文。
  • Spring AI方案:通过元数据按游戏名称筛选RAG结果——该方案演示了如何为文档添加标题标签,利用大语言模型从问题中筛选出正确值,进而对向量搜索结果进行过滤。
  • 基于工程维基的检索技术:BM25、令牌安全型密集窗口与RRF —— 面向技术维基的以评估为先的检索方法:精确证据目标、BM25基准模型、MiniLM截断错误、令牌窗口以及互斥排名融合结果。