Spring ai 中的DocumentTransformer学习笔记
DocumentTransformer的主要作用是对文档进行转换,包括数据结构化、清理数据、数据分块等。这些转换操作旨在确保文档以最适合AI模型检索的格式存储,从而提升检索增强生成模型的能力,提高生成输出的质量和相关性。
在Spring AI中有以下几个实现方法:
分别为:TextSplittter、TokenTextSplitter、SummaryMetadataEnricher、KeyWordMetadataEnricher、ContentFormatTransformer;
TextSplittter
TextSplittter:textSplitter是一个文档切割器,它可以将长文档切割成更小的文本块。这样做的好处是,在RAG应用场景中,可以更方便地从数据集中检索相关信息,同时节省token的使用。
TokenTextSplitter:是TextSplitter的一个具体实现,它允许用户自定义文本块的目标大小、最小大小、丢弃短文本块的长度以及从文本中生成的最大文本块数量等参数。
TokenTextSplitter成员变量分析:
public class TokenTextSplitter extends TextSplitter {
private final EncodingRegistry registry = Encodings.newLazyEncodingRegistry();
private final Encoding encoding;
private int defaultChunkSize;
private int minChunkSizeChars;
private int minChunkLengthToEmbed;
private int maxNumChunks;
private boolean keepSeparator;
....
}
registry:一个EncodingRegistry类型的常量,使用Encodings.newLazyEncodingRegistry()方法进行初始化。EncodingRegistry用于管理不同的编码方式,newLazyEncodingRegistry()会创建一个延迟加载的编码注册表。encoding:一个Encoding类型的常量,用于表示具体的编码方式。在后续的构造函数中会对其进行初始化。defaultChunkSize:一个整数类型的变量,用于指定默认的文本块大小。minChunkSizeChars:一个整数类型的变量,用于指定文本块的最小字符数。minChunkLengthToEmbed:一个整数类型的变量,用于指定可嵌入的最小文本块长度。maxNumChunks:一个整数类型的变量,用于指定最大的文本块数量。keepSeparator:一个布尔类型的变量,用于控制是否保留分隔符。
使用案例代码:
@Bean
public TokenTextSplitter tokenTextSplitter(int defaultChunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks, boolean keepSeparator) {
return new TokenTextSplitter(defaultChunkSize,minChunkSizeChars,minChunkLengthToEmbed, maxNumChunks, keepSeparator);
}
TikaDocumentReader reader = new TikaDocumentReader("./data/test.md");
List<Document> documents = reader.get();
List<Document> documentSplitterList = tokenTextSplitter.apply(documents);
ContentFormatTransformer
ContentFormatTransformer的主要作用是对文档中的元数据进行格式化处理。在处理文档时,我们经常会遇到各种格式的元数据,如标题、作者、日期、关键词等。这些元数据可能以不同的方式嵌入在文档中,如纯文本、HTML标签、XML元素等。ContentFormatTransformer能够将这些不同格式的元数据统一转换成键值对字符串格式,从而方便后续的数据处理和检索操作
public class ContentFormatTransformer implements DocumentTransformer {
private boolean disableTemplateRewrite;
private ContentFormatter contentFormatter;
public ContentFormatTransformer(ContentFormatter contentFormatter) {
this(contentFormatter, false);
}
public ContentFormatTransformer(ContentFormatter contentFormatter, boolean disableTemplateRewrite) {
this.disableTemplateRewrite = false;
this.contentFormatter = contentFormatter;
this.disableTemplateRewrite = disableTemplateRewrite;
}
public List<Document> apply(List<Document> documents) {
if (this.contentFormatter != null) {
documents.forEach((document) -> {
if (document.getContentFormatter() instanceof DefaultContentFormatter && this.contentFormatter instanceof DefaultContentFormatter) {
DefaultContentFormatter docFormatter = (DefaultContentFormatter)document.getContentFormatter();
DefaultContentFormatter toUpdateFormatter = (DefaultContentFormatter)this.contentFormatter;
ArrayList<String> updatedEmbedExcludeKeys = new ArrayList(docFormatter.getExcludedEmbedMetadataKeys());
updatedEmbedExcludeKeys.addAll(toUpdateFormatter.getExcludedEmbedMetadataKeys());
ArrayList<String> updatedInterfaceExcludeKeys = new ArrayList(docFormatter.getExcludedInferenceMetadataKeys());
updatedInterfaceExcludeKeys.addAll(toUpdateFormatter.getExcludedInferenceMetadataKeys());
DefaultContentFormatter.Builder builder = DefaultContentFormatter.builder().withExcludedEmbedMetadataKeys(updatedEmbedExcludeKeys).withExcludedInferenceMetadataKeys(updatedInterfaceExcludeKeys).withMetadataTemplate(docFormatter.getMetadataTemplate()).withMetadataSeparator(docFormatter.getMetadataSeparator());
if (!this.disableTemplateRewrite) {
builder.withTextTemplate(docFormatter.getTextTemplate());
}
document.setContentFormatter(builder.build());
} else {
document.setContentFormatter(this.contentFormatter);
}
});
}
return documents;
}
}
学习到这里,就出现一个疑问,这个元数据和普通数据是怎么区分开的
所以插入个小插曲,看一下spring ai中Document类是由什么构成的
可以看到,这个类是由元数据和普通数据组成,元数据是以Map这种键值对的形式存在的:
默认的内容格式化器代码如下:
public class DefaultContentFormatter implements ContentFormatter {
private static final String TEMPLATE_CONTENT_PLACEHOLDER = "{content}";
private static final String TEMPLATE_METADATA_STRING_PLACEHOLDER = "{metadata_string}";
private static final String TEMPLATE_VALUE_PLACEHOLDER = "{value}";
private static final String TEMPLATE_KEY_PLACEHOLDER = "{key}";
private static final String DEFAULT_METADATA_TEMPLATE = String.format("%s: %s", "{key}", "{value}");
private static final String DEFAULT_METADATA_SEPARATOR = System.lineSeparator();
private static final String DEFAULT_TEXT_TEMPLATE = String.format("%s\n\n%s", "{metadata_string}", "{content}");
private final String metadataTemplate;
private final String metadataSeparator;
private final String textTemplate;
private final List<String> excludedInferenceMetadataKeys;
private final List<String> excludedEmbedMetadataKeys;
public static Builder builder() {
return new Builder();
}
public static DefaultContentFormatter defaultConfig() {
return builder().build();
}
private DefaultContentFormatter(Builder builder) {
this.metadataTemplate = builder.metadataTemplate;
this.metadataSeparator = builder.metadataSeparator;
this.textTemplate = builder.textTemplate;
this.excludedInferenceMetadataKeys = builder.excludedInferenceMetadataKeys;
this.excludedEmbedMetadataKeys = builder.excludedEmbedMetadataKeys;
}
public String format(Document document, MetadataMode metadataMode) {
Map<String, Object> metadata = this.metadataFilter(document.getMetadata(), metadataMode);
String metadataText = (String)metadata.entrySet().stream().map((metadataEntry) -> this.metadataTemplate.replace("{key}", (CharSequence)metadataEntry.getKey()).replace("{value}", metadataEntry.getValue().toString())).collect(Collectors.joining(this.metadataSeparator));
return this.textTemplate.replace("{metadata_string}", metadataText).replace("{content}", document.getContent());
}
protected Map<String, Object> metadataFilter(Map<String, Object> metadata, MetadataMode metadataMode) {
if (metadataMode == MetadataMode.ALL) {
return new HashMap(metadata);
} else if (metadataMode == MetadataMode.NONE) {
return new HashMap(Collections.emptyMap());
} else {
Set<String> usableMetadataKeys = new HashSet(metadata.keySet());
if (metadataMode == MetadataMode.INFERENCE) {
usableMetadataKeys.removeAll(this.excludedInferenceMetadataKeys);
} else if (metadataMode == MetadataMode.EMBED) {
usableMetadataKeys.removeAll(this.excludedEmbedMetadataKeys);
}
return new HashMap((Map)metadata.entrySet().stream().filter((e) -> usableMetadataKeys.contains(e.getKey())).collect(Collectors.toMap((e) -> (String)e.getKey(), (e) -> e.getValue())));
}
}
}
1. 关键字段
- metadataTemplate(默认
"{key}: {value}") 单个元数据键值对的格式化模板。例如author: Alice。 - metadataSeparator(默认换行符) 多个元数据之间的分隔符。
- textTemplate(默认
"{metadata_string}\n\n{content}") 最终文本模板,将元数据字符串和内容合并。 - excludedInferenceMetadataKeys 在
INFERENCE模式下需要排除的元数据键。 - excludedEmbedMetadataKeys 在
EMBED模式下需要排除的元数据键。
关键函数:
在format函数中定义了如何提取元数据和如何提取元数据和拼接出最终的结果
Document doc = new Document("Spring AI 是 Java 的 AI 框架。");
doc.getMetadata().put("author", "Alice");
doc.getMetadata().put("version", "1.0");
DefaultContentFormatter formatter = DefaultContentFormatter.defaultConfig();
String formattedText = formatter.format(doc, MetadataMode.ALL);
// 输出结果:
// author: Alice
// version: 1.0
//
// Spring AI 是 Java 的 AI 框架。
默认的文本格式化如上代码所示
然后也可以自定义:
DefaultContentFormatter formatter = DefaultContentFormatter.builder()
.withTextTemplate("{content} [METADATA: {metadata_string}]")
.withMetadataTemplate("{key}={value}")
.withMetadataSeparator(", ")
.build();
String formattedText = formatter.format(doc, MetadataMode.EMBED);
// 输出结果:
// Spring AI 是 Java 的 AI 框架。 [METADATA: author=Alice, version=1.0]
也可以利用过滤器,过滤掉敏感的元数据
DefaultContentFormatter formatter = DefaultContentFormatter.builder()
.withExcludedInferenceMetadataKeys("password") // 推理时不带密码
.build();
doc.getMetadata().put("password", "123456");
String result = formatter.format(doc, MetadataMode.INFERENCE); // password 会被过滤
扩展:
- 自定义模板:通过
Builder修改metadataTemplate或textTemplate。 - 动态过滤:根据业务需求配置不同的排除键列表。
- 多语言支持:可修改模板中的文本为其他语言(例如
"{key}的值为{value}")。
如果需要对不同文档类型应用不同格式化规则,可以创建多个 DefaultContentFormatter 实例。
SummaryMetadataEnricher
SummaryMetadataEnricher 是一个文档元数据增强器,专为长文本分块场景设计。它利用 AI 模型为每个文档分块生成摘要,并将三类摘要信息添加到元数据中:
- 当前分块摘要(
section_summary) - 前一分块摘要(
prev_section_summary) - 后一分块摘要(
next_section_summary)
这些摘要信息可显著提升 RAG(检索增强生成)的效果,帮助模型理解分块间的上下文关系。
核心字段:
chatClient:用于调用 AI 模型生成摘要。summaryTypes:需生成的摘要类型(CURRENT/PREVIOUS/NEXT)。summaryTemplate:生成摘要的提示词模板(默认模板见下文)。metadataMode:文档格式化时的元数据模式(如MetadataMode.ALL包含全部元数据)。
核心方法:
public List<Document> apply(List<Document> documents) {
// 第一步:为每个文档生成摘要
List<String> summaries = new ArrayList();
for (Document doc : documents) {
String content = doc.getFormattedContent(metadataMode); // 格式化内容
Prompt prompt = new PromptTemplate(summaryTemplate).create(Map.of("context_str", content));
String summary = chatClient.call(prompt).getOutput().getContent();
summaries.add(summary);
}
// 第二步:将摘要添加到元数据
for (int i = 0; i < documents.size(); i++) {
Map<String, Object> metadata = new HashMap();
Document currentDoc = documents.get(i);
// 添加当前分块摘要
if (summaryTypes.contains(SummaryType.CURRENT)) {
metadata.put("section_summary", summaries.get(i));
}
// 添加上一分块摘要
if (i > 0 && summaryTypes.contains(SummaryType.PREVIOUS)) {
metadata.put("prev_section_summary", summaries.get(i - 1));
}
// 添加后一分块摘要
if (i < documents.size() - 1 && summaryTypes.contains(SummaryType.NEXT)) {
metadata.put("next_section_summary", summaries.get(i + 1));
}
currentDoc.getMetadata().putAll(metadata);
}
return documents;
}
使用案例:
// 初始化组件
ChatClient chatClient = new OpenAiChatClient(apiKey);
List<SummaryType> summaryTypes = List.of(
SummaryType.CURRENT,
SummaryType.PREVIOUS,
SummaryType.NEXT
);
SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(
chatClient,
summaryTypes,
"请总结以下法律条款的核心内容:\n{context_str}\n\n摘要:",
MetadataMode.ALL
);
// 原始文档分块(假设已分割)
List<Document> documents = List.of(
new Document("《民法典》第1024条:自然人享有名誉权..."),
new Document("《民法典》第1025条:行为人为公共利益实施新闻报道..."),
new Document("《民法典》第1026条:行为人是否尽到合理核实义务的认定...")
);
// 增强元数据
List<Document> enrichedDocs = enricher.apply(documents);
// 查看结果
enrichedDocs.forEach(doc -> {
System.out.println("内容: " + doc.getContent());
System.out.println("元数据: " + doc.getMetadata());
System.out.println("------");
});
///输出结果
内容: 《民法典》第1024条:自然人享有名誉权...
元数据: {
"section_summary": "本条明确了自然人名誉权的法律保护。",
"next_section_summary": "第1025条规定了新闻报道中的公共利益免责情形。"
}
------
内容: 《民法典》第1025条:行为人为公共利益实施新闻报道...
元数据: {
"section_summary": "规定了新闻报道在公共利益下的免责条款。",
"prev_section_summary": "第1024条明确了名誉权的保护。",
"next_section_summary": "第1026条提出了核实义务的认定标准。"
}
------
内容: 《民法典》第1026条:行为人是否尽到合理核实义务的认定...
元数据: {
"section_summary": "明确了核实义务的六项认定标准。",
"prev_section_summary": "第1025条规定了新闻报道的免责情形。"
}
在RAG中的应用:
1.在向量化时,将元数据摘要与内容合并,增强语义表示,这里就是先用SummaryMetadataEnricher生成摘要并添加到元数据中,然后再使用文本格式化器来进行格式化。
// 示例:格式化文档内容(包含元数据摘要)
ContentFormatter formatter = DefaultContentFormatter.builder()
.withTextTemplate("""
当前摘要: {section_summary}
前文摘要: {prev_section_summary}
后文摘要: {next_section_summary}
内容: {content}
""")
.build();
String formatted = formatter.format(enrichedDoc, MetadataMode.ALL);
2.生成阶段:提示词中利用相邻摘要提升连贯性。
String promptTemplate = """
请根据以下上下文回答问题:
[前文摘要] {prev_section_summary}
[当前内容] {content}
[后文摘要] {next_section_summary}
问题:{question}
""";
// 检索到相关文档后,填充模板
Prompt prompt = new PromptTemplate(promptTemplate).create(contextMap);
KeyWordMetadataEnricher
KeywordMetadataEnricher 是一个文档关键词元数据增强器,专为自动提取文档关键词设计。它通过 AI 模型分析文档内容,生成指定数量的关键词,并将结果存入文档元数据。主要作用:
- 自动化关键词抽取:替代人工标注,快速生成内容关键词。
- 增强检索能力:关键词可用于向量搜索的补充字段。
- 支持下游任务:为分类、聚类、标签化提供结构化数据。
核心字段:
EXCERPT_KEYWORDS_METADATA_KEY(固定为"excerpt_keywords") 存储关键词的元数据键。KEYWORDS_TEMPLATE(动态模板) 生成关键词的提示词模板,其中%s会被替换为keywordCount值。keywordCount要求生成的关键词数量(通过构造函数指定)。
核心方法源码:
public List<Document> apply(List<Document> documents) {
for (Document doc : documents) {
// 1. 构建动态提示(如生成3个关键词)
String promptText = String.format(
"%s. Give %d unique keywords for this document. Format as comma separated. Keywords:",
doc.getContent(),
keywordCount
);
// 2. 调用 AI 模型获取关键词
Prompt prompt = new PromptTemplate(promptText).create(Map.of("context_str", doc.getContent()));
String response = chatClient.call(prompt).getOutput().getContent();
// 3. 将关键词存入元数据
doc.getMetadata().put("excerpt_keywords", response);
}
return documents;
}
应用案例:
// 继承并重写提示模板(更符合电商场景)
public class EcommerceKeywordEnricher extends KeywordMetadataEnricher {
private static final String CUSTOM_PROMPT =
"{context_str}\n请提取%d个适合电商搜索的关键词,用逗号分隔。要求包含品牌、核心属性和使用场景。关键词:";
public EcommerceKeywordEnricher(ChatClient client, int keywordCount) {
super(client, keywordCount);
}
@Override
public List<Document> apply(List<Document> docs) {
// 使用定制化模板
String template = String.format(CUSTOM_PROMPT, this.keywordCount);
// ... 类似原逻辑
}
}
// 使用示例
Document productDoc = new Document("""
Apple iPhone 15 Pro 5G智能手机,6.1英寸超视网膜XDR显示屏,
A16仿生芯片,4800万像素主摄,支持卫星通讯功能。
""");
List<Document> enriched = new EcommerceKeywordEnricher(chatClient, 5).apply(List.of(productDoc));
#########结果
关键词: Apple, iPhone 15 Pro, 5G手机, A16芯片, 卫星通讯
总结
TextSplittter、TokenTextSplitter、SummaryMetadataEnricher、KeyWordMetadataEnricher、ContentFormatTransformer作为对输入文档的预处理工具,各有各的作用:
TokenTextSpliter直接对文档进行划分,得到分块
ContentFormatTransformer:对文档进行重新格式化,自定义元数据与普通数据的组合方式
SummaryMetadataEnricher:为文档生成前后以及本文摘要,并注入到元数据中
KeyWordMetadataEnricher:为文档生成关键词,并注入到元数据中