Ragent项目Day-05 元数据
1. 元数据在 RAG 流程中的位置

2. 元数据的本质:给每个 chunk 贴标签
{
"content": "自签收之日起 7 天内,商品未经使用且不影响二次销售的,消费者可申请七天无理由退货。",
"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",
"updated_at": "2024-03-15T10:30:00Z",
"department": "customer_service",
"access_roles": ["employee", "customer_service", "manager"],
"start_offset": 0,
"end_offset": 58,
"chunk_index": 0
}
}
1.3 适用场景与注意事项
|
维度 |
说明 |
|---|---|
|
什么时候必须有 |
几乎所有场景都需要,这是最基础的元数据 |
|
什么时候可以省略 |
如果你的知识库只有一份文档,或者文档来源不重要(比如爬虫抓的公开网页) |
|
注意事项 |
|
元数据的三大核心应用场景
1. 回答可引用:让 AI 的回答有据可查
import org.springframework.ai.document.Document;
import java.util.*;
public class CitationExample {
/**
* 从 chunk 元数据生成引用信息
*/
public static String generateCitation(Document chunk) {
Map<String, Object> metadata = chunk.getMetadata();
String fileName = (String) metadata.get("file_name");
String title = (String) metadata.get("title");
Integer pageNumber = (Integer) metadata.get("page_number");
String sourceUrl = (String) metadata.get("source_url");
StringBuilder citation = new StringBuilder();
citation.append("**依据**: ");
if (fileName != null) {
citation.append("《").append(fileName).append("》");
}
if (title != null) {
citation.append(" ").append(title);
}
if (pageNumber != null) {
citation.append(",第 ").append(pageNumber).append(" 页");
}
if (sourceUrl != null) {
citation.append("\n\n[查看原文](").append(sourceUrl).append(")");
}
return citation.toString();
}
public static void main(String[] args) {
// 模拟一个检索到的 chunk
Map<String, Object> metadata = new HashMap<>();
metadata.put("file_name", "员工手册.pdf");
metadata.put("title", "第二章 员工入职 > 2.1 试用期规定");
metadata.put("page_number", 5);
metadata.put("source_url", "https://docs.company.com/handbook.pdf#page=5");
Document chunk = new Document(
"新员工试用期为 3 个月,试用期内工资为正式工资的 80%。",
metadata
);
String citation = generateCitation(chunk);
System.out.println("Answer: " + chunk.getContent());
System.out.println("\n" + citation);
}
}
2. 权限过滤:不同员工看不同知识
import org.springframework.ai.document.Document;
import java.util.*;
import java.util.stream.Collectors;
public class PermissionFilterExample {
/**
* 模拟向量数据库中的 chunks
*/
private static List<Document> mockChunks() {
List<Document> chunks = new ArrayList<>();
// Chunk 1: 公开信息
Map<String, Object> meta1 = new HashMap<>();
meta1.put("sensitivity_level", "public");
meta1.put("access_roles", Arrays.asList("employee"));
chunks.add(new Document("员工手册规定,所有员工享有带薪年假。", meta1));
// Chunk 2: 人事部内部信息
Map<String, Object> meta2 = new HashMap<>();
meta2.put("sensitivity_level", "confidential");
meta2.put("access_departments", Arrays.asList("hr", "executive"));
chunks.add(new Document("年终奖为月薪的 2-6 倍,根据绩效等级确定。", meta2));
// Chunk 3: 技术部可见信息
Map<String, Object> meta3 = new HashMap<>();
meta3.put("sensitivity_level", "internal");
meta3.put("access_departments", Arrays.asList("tech", "product"));
chunks.add(new Document("技术部员工可申请远程办公,每周最多 2 天。", meta3));
return chunks;
}
/**
* 根据用户权限过滤 chunks
*/
public static List<Document> filterByPermission(
List<Document> chunks,
String userRole,
String userDepartment) {
return chunks.stream()
.filter(chunk -> hasPermission(chunk, userRole, userDepartment))
.collect(Collectors.toList());
}
/**
* 判断用户是否有权限访问某个 chunk
*/
private static boolean hasPermission(
Document chunk,
String userRole,
String userDepartment) {
Map<String, Object> metadata = chunk.getMetadata();
// 公开信息,所有人都能看
String sensitivity = (String) metadata.get("sensitivity_level");
if ("public".equals(sensitivity)) {
return true;
}
// 检查角色权限
List<String> accessRoles = (List<String>) metadata.get("access_roles");
if (accessRoles != null && accessRoles.contains(userRole)) {
return true;
}
// 检查部门权限
List<String> accessDepts = (List<String>) metadata.get("access_departments");
if (accessDepts != null && accessDepts.contains(userDepartment)) {
return true;
}
return false;
}
public static void main(String[] args) {
List<Document> allChunks = mockChunks();
// 场景 1: 技术部普通员工
System.out.println("=== 技术部员工小李能看到的内容 ===");
List<Document> techResults = filterByPermission(allChunks, "employee", "tech");
techResults.forEach(chunk -> System.out.println("- " + chunk.getContent()));
System.out.println("\n=== 人事部经理能看到的内容 ===");
List<Document> hrResults = filterByPermission(allChunks, "manager", "hr");
hrResults.forEach(chunk -> System.out.println("- " + chunk.getContent()));
}
}
3. 回溯与纠错:发现错答能定位到源头
import org.springframework.ai.document.Document;
import java.util.*;
public class ErrorTrackingExample {
/**
* 根据关键词查找可能有问题的 chunks
*/
public static List<Document> findChunksByKeyword(
List<Document> allChunks,
String keyword) {
List<Document> results = new ArrayList<>();
for (Document chunk : allChunks) {
if (chunk.getContent().contains(keyword)) {
results.add(chunk);
}
}
return results;
}
/**
* 展示 chunk 的详细信息,方便人工审核
*/
public static void displayChunkDetails(Document chunk) {
Map<String, Object> metadata = chunk.getMetadata();
System.out.println("=== Chunk 详情 ===");
System.out.println("内容: " + chunk.getContent());
System.out.println("文档 ID: " + metadata.get("doc_id"));
System.out.println("文件名: " + metadata.get("file_name"));
System.out.println("Chunk 序号: " + metadata.get("chunk_index"));
System.out.println("原文位置: " + metadata.get("start_offset")
+ " - " + metadata.get("end_offset"));
System.out.println("创建时间: " + metadata.get("created_at"));
System.out.println("来源: " + metadata.get("source_url"));
System.out.println();
}
/**
* 模拟更新 chunk(实际项目中需要调用向量数据库的 API)
*/
public static void updateChunk(Document chunk, String newContent) {
System.out.println(">>> 更新 Chunk");
System.out.println("原内容: " + chunk.getContent());
System.out.println("新内容: " + newContent);
System.out.println("文档 ID: " + chunk.getMetadata().get("doc_id"));
System.out.println("Chunk 序号: " + chunk.getMetadata().get("chunk_index"));
System.out.println();
}
public static void main(String[] args) {
// 模拟知识库中的 chunks
List<Document> allChunks = new ArrayList<>();
Map<String, Object> meta1 = new HashMap<>();
meta1.put("doc_id", "doc_reimbursement_v1");
meta1.put("file_name", "报销流程.pdf");
meta1.put("chunk_index", 2);
meta1.put("start_offset", 150);
meta1.put("end_offset", 220);
meta1.put("created_at", "2023-06-01T10:00:00Z");
meta1.put("source_url", "https://docs.company.com/reimbursement_v1.pdf");
allChunks.add(new Document(
"报销时需打印发票并贴在报销单上,提交给财务部审核。",
meta1
));
Map<String, Object> meta2 = new HashMap<>();
meta2.put("doc_id", "doc_reimbursement_v2");
meta2.put("file_name", "报销流程_v2.pdf");
meta2.put("chunk_index", 1);
meta2.put("start_offset", 80);
meta2.put("end_offset", 140);
meta2.put("created_at", "2024-01-15T14:00:00Z");
meta2.put("source_url", "https://docs.company.com/reimbursement_v2.pdf");
allChunks.add(new Document(
"报销时在系统中上传电子发票,无需打印纸质版。",
meta2
));
// 场景:用户反馈"贴发票"的说法已经过时
System.out.println(">>> 用户反馈:系统说要贴发票,但新流程不需要了\n");
// 1. 查找包含"发票"的 chunks
List<Document> suspectedChunks = findChunksByKeyword(allChunks, "发票");
System.out.println("找到 " + suspectedChunks.size() + " 个相关 chunks:\n");
// 2. 展示详情,供人工审核
for (Document chunk : suspectedChunks) {
displayChunkDetails(chunk);
}
// 3. 人工确认第一个 chunk 是过时的,需要删除或更新
Document outdatedChunk = suspectedChunks.get(0);
System.out.println(">>> 确认 chunk_index=2 的内容已过时,准备删除\n");
// 4. 执行删除(实际项目中调用向量数据库的删除 API)
System.out.println(">>> 删除过时 chunk:");
System.out.println("文档 ID: " + outdatedChunk.getMetadata().get("doc_id"));
System.out.println("Chunk 序号: " + outdatedChunk.getMetadata().get("chunk_index"));
System.out.println("\n>>> 操作完成,问题已修正");
}
}
元数据设计的最佳实践
1. 元数据字段不是越多越好
新手常犯的错误:给每个 chunk 加一堆元数据字段,恨不得把能想到的信息都塞进去。
问题在于:
-
存储成本:元数据也要占存储空间,字段太多会显著增加存储成本
-
维护成本:字段越多,维护越麻烦。每次上传文档都要填一堆字段,容易出错
-
检索性能:有些向量数据库在元数据过滤时,字段越多性能越差
一个实用的原则:只加对检索、过滤、展示有实际帮助的字段。
问自己三个问题:
- 1.这个字段会用于检索过滤吗?(比如权限过滤、时间过滤)
- 2.这个字段会展示给用户吗?(比如引用信息)
- 3.这个字段会用于运维管理吗?(比如定位问题 chunk)
如果三个问题的答案都是不会,那这个字段就不要加。
2. 元数据的粒度要和业务场景匹配
场景 1:面向公众的产品帮助文档
{
"doc_id": "...",
"file_name": "...",
"title": "...",
"source_url": "..."
}
场景 2:企业内部知识库
{
"doc_id": "...",
"file_name": "...",
"title": "...",
"source_url": "...",
"access_departments": [...],
"access_roles": [...],
"created_at": "...",
"updated_at": "...",
"start_offset": ...,
"end_offset": ...,
"chunk_index": ...
}
场景 3:电商客服知识库
{
"doc_id": "...",
"file_name": "...",
"product_category": "...",
"policy_type": "...",
"priority": ...,
"effective_date": "...",
"expiration_date": "..."
}
4. 元数据参考表
|
元数据字段 |
用途 |
适用场景 |
维护成本 |
优先级 |
|---|---|---|---|---|
|
doc_id |
文档标识,用于批量管理 |
几乎所有场景 |
低(系统生成) |
必须 |
|
file_name |
展示给用户,生成引用 |
几乎所有场景 |
低(系统生成) |
必须 |
|
source_url |
提供原文链接 |
需要回溯原文的场景 |
低(系统生成) |
推荐 |
|
title / h1_title / h2_title |
生成引用,展示章节信息 |
有结构的文档 |
中(需要解析) |
推荐 |
|
page_number |
生成引用,定位原文 |
PDF 等有页码的文档 |
中(需要解析) |
推荐 |
|
created_at / updated_at |
版本管理,追踪变更 |
知识会更新的场景 |
低(系统生成) |
推荐 |
|
effective_date / expiration_date |
过滤过时内容 |
有时效性的知识(政策、活动) |
高(需要人工标注) |
可选 |
|
access_roles / access_departments |
权限过滤 |
企业内部知识库 |
高(需要人工标注) |
必须(如果有权限需求) |
|
sensitivity_level |
权限过滤 |
企业内部知识库 |
中(可以按文档标注) |
推荐(如果有权限需求) |
|
start_offset / end_offset |
定位原文,纠错 |
需要人工审核和修正的场景 |
低(系统生成) |
推荐 |
|
chunk_index |
定位 chunk,分析相邻块 |
需要管理 chunk 的场景 |
低(系统生成) |
推荐 |
|
product_category / policy_type 等 |
业务过滤和排序 |
有明确业务分类的场景 |
中到高(取决于分类复杂度) |
可选 |
更多推荐
所有评论(0)