元数据管理:让 RAG 答案可引用、可追溯

元数据管理:让 RAG 答案可引用、可追溯

RAG 系统能回答问题,这只是第一步。更重要的是:这个答案是从哪个文档来的?哪一页?什么时候更新的?

这些信息就靠元数据管理。

为什么只有文本内容还不够

痛点一:无法回答”依据是什么”

系统能回答问题,但说不出答案的出处。用户追问”你凭什么这么说”,系统哑口无言。

痛点二:权限控制缺失

不同部门员工可能看到不该看的敏感信息——财务不该看到 HR 的薪酬文档,销售不该看到竞品分析。

痛点三:问题定位困难

发现答案有误时,无法快速定位是哪个 chunk 出了问题,排查如大海捞针。

元数据的本质

给每个 chunk 贴标签

完整 chunk 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"content": "退货政策文本...",
"metadata": {
"doc_id": "doc_20240315_001",
"source_url": "https://docs.company.com/policy/return.pdf",
"file_name": "退货政策.pdf",
"title": "一、退货政策",
"page_number": 3,
"created_at": "2024-03-15T10:30:00Z",
"department": "customer_service",
"access_roles": ["employee", "customer_service", "manager"]
}
}

常见元数据字段

1. 文档标识类

字段 说明
doc_id 文档唯一标识符
source_url 文档访问地址
file_name 原始文件名

2. 结构信息类

字段 说明
h1_title 一级标题
h2_title 二级标题
page_number 页码(PDF)

3. 时间版本类

字段 说明
created_at 创建时间
updated_at 更新时间
effective_date 生效时间
expiration_date 失效时间

4. 权限控制类

字段 说明
access_roles 可访问角色列表
access_departments 可访问部门列表
sensitivity_level 敏感级别(public/internal/confidential/secret)

5. 位置追溯类

字段 说明
start_offset 在原文的起始位置
end_offset 在原文的结束位置
chunk_index chunk 序号

6. 业务自定义类

字段 说明
product_category 产品类目
policy_type 政策类型
tech_stack 技术栈

三大核心应用

1. 回答可引用

让 AI 的回答有据可查,用户可点击查看原文:

1
2
3
4
AI回答:
根据《退货政策》第三条,拆封商品不影响退货...
[来源:退货政策.pdf 第5页]
[查看原文]

2. 权限过滤

不同员工看不同知识,保护敏感信息:

1
2
3
4
5
6
7
# 检索时过滤权限
results = client.search(
collection_name="rag_docs",
data=[query_vector],
filter='access_roles contains "customer_service"', # 只返回客服能看的
limit=5,
)

3. 知识版本管理

追踪文档变更,支持版本回溯:

1
2
3
4
5
6
7
8
9
10
# 新版本入库
new_chunk = {
"content": "更新后的退货政策...",
"metadata": {
"doc_id": "doc_20240315_001",
"updated_at": "2026-04-26T10:00:00Z",
"version": "2.0"
}
}
# 旧版本保留在历史分区,检索默认不召回

在 RAG 流程中的位置

1
原始文档 → Tika 解析 → 分块 → [添加元数据] → 向量化 → 向量数据库

元数据不参与语义检索(不被向量化),但在检索过滤引用生成等环节发挥作用。

实战:Python 添加元数据到 Milvus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pymilvus import MilvusClient

client = MilvusClient(uri="http://localhost:19530")

# 创建带 schema 的 collection
client.create_collection(
collection_name="rag_docs",
dimension=1024,
schema=[
{"name": "id", "type": "int64", "is_primary": True},
{"name": "vector", "type": "float32[1024]"},
{"name": "text", "type": "varchar", "max_length": 65535},
{"name": "doc_name", "type": "varchar", "max_length": 512},
{"name": "page_number", "type": "int64"},
{"name": "access_roles", "type": "varchar", "max_length": 256},
]
)

# 插入带元数据的数据
client.insert(
collection_name="rag_docs",
data=[
{
"id": 1,
"vector": [0.1] * 1024,
"text": "退货政策内容...",
"doc_name": "退货政策.pdf",
"page_number": 3,
"access_roles": "employee,manager"
}
]
)

# 带权限过滤的检索
results = client.search(
collection_name="rag_docs",
data=[query_vector],
filter='access_roles like "%manager%"',
limit=5,
output_fields=["text", "doc_name", "page_number"]
)

相关阅读: