元数据管理:让 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")
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"] )
|
相关阅读: