RAG文档分块--切割指南
RAG文档切割是RAG应用的基础环节,其质量直接决定了系统设计的成败。合理的文档分割不仅影响检索的准确性和召回率,更关系到后续生成答案的相关性与可靠性。因此,必须根据领域特点和业务需求,精心设计切割策略,确保语义单元的完整性,为整个系统奠定坚实根基。
一、RAG文档切割介绍
RAG文档切割是RAG应用的基础环节,其质量直接决定了系统设计的成败。合理的文档分割不仅影响检索的准确性和召回率,更关系到后续生成答案的相关性与可靠性。因此,必须根据领域特点和业务需求,精心设计切割策略,确保语义单元的完整性,为整个系统奠定坚实根基
二、为什么做分块
1、分块的必要性源于两个核心的限制
(1)模型上下文窗口
大语言模型(LLM)无法一次性处理无限制长度的文本,必须将长文本切分为适配模型处理能力的小片段
(2)检索新噪比
若一个文本块包含过多无关信息(噪音),会稀释核心信号,导致检索器难以精确匹配用户意图
2、理想的分块标准
在上下文完整性与信息密度之间取得平衡,按着策略复杂度分为四类
(1)基础分块
(2)结构感知
(3)语义/主题
(4)高级策略
三、分块策略
1、基础分块策略
(1)、固定长度分块
核心思想:按固定字符数切割文本,不考虑语义结构
优点:
-
智能分层:先尝试按大段落分割,再按句子分割
-
保持语义:尽可能在自然边界处切割
-
灵活配置:支持自定义块大小和重叠比例
-
医疗友好:适合医疗文档的层次化结构
缺点:易破坏句子完整性
适用场景:非结构化村文本预处理
示例代码
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentByCharacterSplitter;
import dev.langchain4j.data.segment.TextSegment;
import java.util.List;
public class SimpleSplitExample {
public static void main(String[] args) {
String text = "你的普通文本内容...";
Document document = Document.from(text);
// 直接使用LangChain4j的分割器:100字切割,15字重叠(15%)
DocumentByCharacterSplitter splitter = new DocumentByCharacterSplitter(100, 15);
List<TextSegment> segments = splitter.split(document);
// 简单输出
segments.forEach(segment ->
System.out.println("分块 (" + segment.text().length() + "字): " + segment.text())
);
}
}
(2)、递归字符分块(推荐)
核心思想:按优先级分割递归切分(\n\n--\n--""),尽量保留段落,句子完整性。
优点:实现比较简单
缺点:对于缺乏明确分隔符的医疗文本效果不佳
适用场景:绝大多数通用文本的首选策略
示例代码:
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import java.util.List;
public class RecursiveSplittingExample {
public static void main(String[] args) {
// 创建医疗文档
String medicalText = "患者主诉:头痛、发热、咳嗽三天。\n\n" +
"体格检查:体温38.5℃,咽部充血,双肺呼吸音粗。\n\n" +
"初步诊断:上呼吸道感染。\n\n" +
"处理建议:休息、多饮水,对症退热治疗。\n\n" +
"注意事项:如症状加重请及时复诊。";
Document document = Document.from(medicalText);
// 创建递归分割器
var splitter = DocumentSplitters.recursive(
200, // 最大块大小
30 // 重叠大小(约15%)
);
// 执行分割
List<TextSegment> segments = splitter.split(document);
// 输出结果
System.out.println("原始文档长度: " + medicalText.length() + " 字符");
System.out.println("生成分块数量: " + segments.size());
for (int i = 0; i < segments.size(); i++) {
TextSegment segment = segments.get(i);
System.out.println("\n--- 分块 " + (i + 1) + " ---");
System.out.println("内容: " + segment.text());
System.out.println("长度: " + segment.text().length() + " 字符");
}
}
}
(3)、基于句子的分块
核心思想:以完整句子为单位进行组合,确保语义的完整。
优点:保持完整的语义单元
缺点:依赖准确的句子边界检测
适用场景:法律文书、新闻报道、对句子完整性要求比较高
示例代码:
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentBySentenceSplitter;
import dev.langchain4j.data.segment.TextSegment;
import java.util.List;
public class SentenceBasedSplitting {
public static void main(String[] args) {
// 法律文书示例 - 适合句子分块
String legalText = "根据《中华人民共和国合同法》第十二条规定,合同的内容由当事人约定。 " +
"当事人订立合同,应当具有相应的民事权利能力和民事行为能力。 " +
"当事人依法可以委托代理人订立合同。 " +
"合同的形式可以是书面形式、口头形式和其他形式。 " +
"法律、行政法规规定采用书面形式的,应当采用书面形式。";
Document document = Document.from(legalText);
// 创建句子分割器
// 参数:最大块大小(字符数),重叠大小
DocumentBySentenceSplitter splitter = new DocumentBySentenceSplitter(150, 20);
// 执行分割
List<TextSegment> segments = splitter.split(document);
// 输出结果
System.out.println("=== 基于句子的分块结果 ===");
System.out.println("原始文本: " + legalText);
System.out.println("分块数量: " + segments.size());
for (int i = 0; i < segments.size(); i++) {
TextSegment segment = segments.get(i);
System.out.println("\n--- 分块 " + (i + 1) + " ---");
System.out.println("内容: " + segment.text());
System.out.println("长度: " + segment.text().length() + " 字符");
System.out.println("句子数量: " + countSentences(segment.text()));
}
}
/**
* 简单统计句子数量(按句号、问号、感叹号分割)
*/
private static int countSentences(String text) {
if (text == null || text.trim().isEmpty()) {
return 0;
}
String[] sentences = text.split("[。!?!?]");
return sentences.length;
}
}
2、结构感知分块
(1)、结构化文本分块
核心思想:根据标题层级(如#,##)或者html标签切割
优点:保持文档的层次结构
缺点:依赖文档的结构化标记,需要预先了解文档格式
适用场景:技术文档,博客,手册等格式规范文档
示例代码:
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentByHeaderSplitter;
import dev.langchain4j.data.document.splitter.Header;
import dev.langchain4j.data.segment.TextSegment;
import java.util.List;
public class StructuredTextSplitting {
public static void main(String[] args) {
// Markdown格式的技术文档
String markdownText = """
# 互联网医院系统架构
## 前端设计
前端采用Vue.js框架,实现响应式设计。
支持PC端和移动端访问。
## 后端服务
后端使用Spring Boot框架,提供RESTful API。
集成用户认证和权限管理。
## 数据库设计
使用MySQL存储用户数据和医疗记录。
采用Redis缓存热点数据。
## 安全规范
所有数据传输使用HTTPS加密。
患者隐私数据严格保护。
""";
Document document = Document.from(markdownText);
// 创建基于标题的分割器
DocumentByHeaderSplitter splitter = new DocumentByHeaderSplitter(
List.of(
Header.builder().level(1).name("H1").build(), // # 标题
Header.builder().level(2).name("H2").build() // ## 标题
),
500, // 最大块大小
50 // 重叠大小
);
List<TextSegment> segments = splitter.split(document);
System.out.println("=== 结构化文本分块结果 ===");
System.out.println("分块数量: " + segments.size());
for (int i = 0; i < segments.size(); i++) {
TextSegment segment = segments.get(i);
System.out.println("\n--- 分块 " + (i + 1) + " ---");
System.out.println("内容: " + segment.text());
System.out.println("长度: " + segment.text().length() + " 字符");
}
}
}
(2)、对话式分块
核心思想:按照发言人或者对话轮次切分
优点:便于理解对话流程和逻辑
缺点:需要识别不同的对话标记格式
适用场景:客户记录,访谈,会议纪要
示例代码:
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.segment.TextSegment;
import java.util.ArrayList;
import java.util.List;
public class MeetingMinutesSplitting {
public static List<TextSegment> splitMeetingByTopics(String meetingText) {
List<TextSegment> segments = new ArrayList<>();
String[] lines = meetingText.split("\n");
StringBuilder currentTopic = new StringBuilder();
String currentSpeaker = null;
for (String line : lines) {
if (line.startsWith("【主题】")) {
// 新主题开始
if (currentTopic.length() > 0) {
segments.add(TextSegment.from(currentTopic.toString()));
}
currentTopic = new StringBuilder(line).append("\n");
} else if (line.matches("^\\w+:.*")) {
// 发言人内容
currentTopic.append(line).append("\n");
} else if (!line.trim().isEmpty()) {
// 其他内容(决议、行动项等)
currentTopic.append(line).append("\n");
}
}
// 添加最后一个主题
if (currentTopic.length() > 0) {
segments.add(TextSegment.from(currentTopic.toString()));
}
return segments;
}
public static List<TextSegment> splitBySpeakerGroups(String text, String[] speakerGroups) {
List<TextSegment> segments = new ArrayList<>();
String[] lines = text.split("\n");
StringBuilder currentGroup = new StringBuilder();
String currentGroupName = null;
for (String line : lines) {
boolean isNewGroup = false;
for (String speaker : speakerGroups) {
if (line.startsWith(speaker + ":")) {
if (currentGroupName != null && !currentGroupName.equals(speaker)) {
// 切换到新的发言人组
if (currentGroup.length() > 0) {
segments.add(TextSegment.from(currentGroup.toString()));
}
currentGroup = new StringBuilder();
}
currentGroupName = speaker;
isNewGroup = true;
break;
}
}
if (isNewGroup || currentGroupName != null) {
currentGroup.append(line).append("\n");
}
}
if (currentGroup.length() > 0) {
segments.add(TextSegment.from(currentGroup.toString()));
}
return segments;
}
public static void main(String[] args) {
// 会议纪要示例
String meetingMinutes = """
【主题】项目进度汇报
项目经理:目前项目完成度80%,按计划进行。
技术负责人:核心功能已开发完成,正在测试。
产品经理:用户反馈良好,建议增加新功能。
【主题】风险讨论
项目经理:存在进度延迟风险。
技术负责人:技术难题已解决,风险可控。
测试负责人:测试发现若干bug,需要修复。
【主题】下一步计划
项目经理:下周三前完成所有功能。
技术负责人:周四开始集成测试。
产品经理:周五组织用户验收。
""";
System.out.println("=== 会议纪要按主题分块 ===");
List<TextSegment> topicSegments = splitMeetingByTopics(meetingMinutes);
for (int i = 0; i < topicSegments.size(); i++) {
System.out.println("\n--- 主题块 " + (i + 1) + " ---");
System.out.println(topicSegments.get(i).text());
}
// 客户访谈记录
String customerInterview = """
销售:您好,请问对我们的产品有什么了解?
客户:了解一些基本功能,但不太清楚具体能解决什么问题。
销售:我们的产品主要帮助提升工作效率30%以上。
客户:听起来不错,能具体演示一下吗?
技术顾问:当然可以,我现在就为您演示核心功能。
客户:这个功能确实很实用,价格方面怎么样?
销售:根据您的需求,我们可以提供定制报价。
""";
System.out.println("\n=== 按发言人组分块 ===");
String[] speakers = {"销售", "技术顾问", "客户"};
List<TextSegment> speakerSegments = splitBySpeakerGroups(customerInterview, speakers);
for (int i = 0; i < speakerSegments.size(); i++) {
System.out.println("\n--- 发言人组块 " + (i + 1) + " ---");
System.out.println(speakerSegments.get(i).text());
}
}
}
3、语义和主题分块
(1)、语义分块
核心思想:计算相邻两句话的向量相似度,在语义突变处理
优点:基于真实语义边界进行分割
缺点:依赖嵌入模型
适用场景:知识库、论文、多话题文档
示例代码:
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.all.minilm.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.model.embedding.Embedding;
import java.util.ArrayList;
import java.util.List;
public class SemanticChunking {
private final EmbeddingModel embeddingModel;
public SemanticChunking() {
// 使用轻量级嵌入模型
this.embeddingModel = new AllMiniLmL6V2EmbeddingModel();
}
public List<TextSegment> splitBySemanticSimilarity(String text, double similarityThreshold) {
List<TextSegment> segments = new ArrayList<>();
// 1. 先将文本分割成句子
List<String> sentences = splitIntoSentences(text);
if (sentences.size() <= 1) {
segments.add(TextSegment.from(text));
return segments;
}
// 2. 计算句子嵌入向量
List<Embedding> embeddings = embeddingModel.embedAll(sentences).content();
// 3. 基于语义相似度进行分块
StringBuilder currentChunk = new StringBuilder(sentences.get(0));
for (int i = 1; i < sentences.size(); i++) {
// 计算当前句子与前一句的相似度
double similarity = cosineSimilarity(
embeddings.get(i - 1).vector(),
embeddings.get(i).vector()
);
if (similarity < similarityThreshold) {
// 语义突变,创建新分块
segments.add(TextSegment.from(currentChunk.toString()));
currentChunk = new StringBuilder(sentences.get(i));
} else {
// 语义连续,添加到当前分块
currentChunk.append(" ").append(sentences.get(i));
}
}
// 添加最后一个分块
if (currentChunk.length() > 0) {
segments.add(TextSegment.from(currentChunk.toString()));
}
return segments;
}
public List<TextSegment> adaptiveSemanticChunking(String text,
double similarityThreshold,
int maxChunkSize) {
List<TextSegment> segments = new ArrayList<>();
List<String> sentences = splitIntoSentences(text);
if (sentences.isEmpty()) {
return segments;
}
List<Embedding> embeddings = embeddingModel.embedAll(sentences).content();
List<String> currentChunkSentences = new ArrayList<>();
currentChunkSentences.add(sentences.get(0));
for (int i = 1; i < sentences.size(); i++) {
double similarity = cosineSimilarity(
embeddings.get(i - 1).vector(),
embeddings.get(i).vector()
);
// 检查是否超过最大块大小
int currentSize = String.join(" ", currentChunkSentences).length();
int newSentenceSize = sentences.get(i).length();
if (similarity < similarityThreshold ||
currentSize + newSentenceSize > maxChunkSize) {
// 语义突变或超过大小限制,创建新分块
segments.add(TextSegment.from(String.join(" ", currentChunkSentences)));
currentChunkSentences = new ArrayList<>();
}
currentChunkSentences.add(sentences.get(i));
}
// 添加最后一个分块
if (!currentChunkSentences.isEmpty()) {
segments.add(TextSegment.from(String.join(" ", currentChunkSentences)));
}
return segments;
}
private List<String> splitIntoSentences(String text) {
List<String> sentences = new ArrayList<>();
if (text == null || text.trim().isEmpty()) {
return sentences;
}
// 简单的句子分割(可按需使用更复杂的分割器)
String[] rawSentences = text.split("[。!?!?;;]");
for (String sentence : rawSentences) {
String trimmed = sentence.trim();
if (!trimmed.isEmpty()) {
sentences.add(trimmed);
}
}
return sentences;
}
private double cosineSimilarity(float[] vectorA, float[] vectorB) {
if (vectorA.length != vectorB.length) {
return 0.0;
}
double dotProduct = 0.0;
double normA = 0.0;
double normB = 0.0;
for (int i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i];
normA += Math.pow(vectorA[i], 2);
normB += Math.pow(vectorB[i], 2);
}
if (normA == 0 || normB == 0) {
return 0.0;
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
public static void main(String[] args) {
SemanticChunking semanticChunking = new SemanticChunking();
// 多主题文档示例
String multiTopicDocument = """
深度学习是机器学习的一个分支。它使用多层神经网络进行特征学习。
卷积神经网络在图像识别领域表现出色。循环神经网络适合序列数据处理。
自然语言处理是人工智能的重要方向。Transformer架构 revolutionized 机器翻译。
BERT模型在多项NLP任务中取得突破。GPT系列模型展示了强大的文本生成能力。
计算机视觉关注图像和视频理解。目标检测可以识别图像中的物体位置。
图像分割将像素分类到不同类别。姿态估计分析人体关键点位置。
""";
System.out.println("=== 语义分块(相似度阈值:0.7)===");
List<TextSegment> segments1 = semanticChunking.splitBySemanticSimilarity(
multiTopicDocument, 0.7);
for (int i = 0; i < segments1.size(); i++) {
System.out.println("\n--- 语义块 " + (i + 1) + " ---");
System.out.println(segments1.get(i).text());
System.out.println("长度: " + segments1.get(i).text().length() + " 字符");
}
System.out.println("\n=== 自适应语义分块(阈值:0.6,最大长度:200)===");
List<TextSegment> segments2 = semanticChunking.adaptiveSemanticChunking(
multiTopicDocument, 0.6, 200);
for (int i = 0; i < segments2.size(); i++) {
System.out.println("\n--- 自适应块 " + (i + 1) + " ---");
System.out.println(segments2.get(i).text());
System.out.println("长度: " + segments2.get(i).text().length() + " 字符");
}
// 测试不同相似度阈值
testDifferentThresholds(semanticChunking, multiTopicDocument);
}
private static void testDifferentThresholds(SemanticChunking chunker, String text) {
double[] thresholds = {0.5, 0.6, 0.7, 0.8};
System.out.println("\n=== 不同相似度阈值对比 ===");
for (double threshold : thresholds) {
List<TextSegment> segments = chunker.splitBySemanticSimilarity(text, threshold);
System.out.println("\n阈值: " + threshold + " -> 分块数: " + segments.size());
for (int i = 0; i < segments.size(); i++) {
System.out.println(" 块 " + (i + 1) + ": " +
segments.get(i).text().length() + " 字符");
}
}
}
}
(2)、基于主题的分块(LDA-矩阵)
核心思想:适用LDA主题模型识别段落主题,在主题切换时切分
优点:真正基于语义主题进行分割
缺点:需要训练主题模型
适用场景:长篇报告,书籍章节
示例代码:
import cc.mallet.pipe.*;
import cc.mallet.pipe.iterator.*;
import cc.mallet.topics.*;
import cc.mallet.types.*;
import java.util.*;
import java.util.regex.*;
import java.util.stream.Collectors;
public class AdvancedLDAChunking {
private final int numTopics;
private final ParallelTopicModel model;
public AdvancedLDAChunking(int numTopics) throws Exception {
this.numTopics = numTopics;
this.model = new ParallelTopicModel(numTopics);
}
public List<TopicAwareChunk> advancedTopicChunking(String text, int chunkSize) throws Exception {
// 1. 准备数据
List<DocumentSegment> segments = createDocumentSegments(text, chunkSize);
// 2. 训练LDA模型
InstanceList instances = createMalletInstances(segments);
model.addInstances(instances);
model.setNumThreads(2);
model.setNumIterations(1000);
model.estimate();
// 3. 获取主题分布
return assignTopicsToChunks(segments, instances);
}
private List<DocumentSegment> createDocumentSegments(String text, int chunkSize) {
List<DocumentSegment> segments = new ArrayList<>();
List<String> sentences = splitIntoSentences(text);
for (int i = 0; i < sentences.size(); i += chunkSize) {
int end = Math.min(i + chunkSize, sentences.size());
List<String> chunkSentences = sentences.subList(i, end);
String chunkText = String.join(" ", chunkSentences);
segments.add(new DocumentSegment(
"seg_" + (segments.size() + 1),
chunkText,
i,
end - 1
));
}
return segments;
}
private InstanceList createMalletInstances(List<DocumentSegment> segments) {
ArrayList<Pipe> pipeList = new ArrayList<>();
// 创建数据处理管道
pipeList.add(new CharSequenceLowercase());
pipeList.add(new CharSequence2TokenSequence(Pattern.compile("\\p{L}[\\p{L}\\p{P}]+\\p{L}")));
pipeList.add(new TokenSequenceRemoveStopwords(new File("stoplists/en.txt"), "UTF-8", false, false, false));
pipeList.add(new TokenSequence2FeatureSequence());
InstanceList instances = new InstanceList(new SerialPipes(pipeList));
// 添加文档到实例列表
for (DocumentSegment segment : segments) {
instances.addThruPipe(new Instance(
segment.getText(),
segment.getId(),
segment.getId(),
null
));
}
return instances;
}
private List<TopicAwareChunk> assignTopicsToChunks(List<DocumentSegment> segments, InstanceList instances) {
List<TopicAwareChunk> topicChunks = new ArrayList<>();
for (int i = 0; i < segments.size(); i++) {
DocumentSegment segment = segments.get(i);
Instance instance = instances.get(i);
// 获取主题分布
double[] topicDistribution = model.getTopicProbabilities(i);
int dominantTopic = findDominantTopic(topicDistribution);
double topicConfidence = topicDistribution[dominantTopic];
// 获取主题关键词
List<String> topicKeywords = getTopicKeywords(dominantTopic, 5);
topicChunks.add(new TopicAwareChunk(
segment.getText(),
dominantTopic,
topicConfidence,
topicKeywords,
segment.getStartSentence(),
segment.getEndSentence()
));
}
return topicChunks;
}
private int findDominantTopic(double[] topicDistribution) {
int dominant = 0;
for (int i = 1; i < topicDistribution.length; i++) {
if (topicDistribution[i] > topicDistribution[dominant]) {
dominant = i;
}
}
return dominant;
}
private List<String> getTopicKeywords(int topicIndex, int numKeywords) {
ArrayList<IDSorter> keywordScores = new ArrayList<>();
Object[] vocabulary = model.getAlphabet().toArray();
for (int wordIndex = 0; wordIndex < model.getAlphabet().size(); wordIndex++) {
keywordScores.add(new IDSorter(wordIndex,
model.getTopicWordProbabilities(topicIndex)[wordIndex]));
}
Collections.sort(keywordScores);
List<String> keywords = new ArrayList<>();
for (int i = 0; i < Math.min(numKeywords, keywordScores.size()); i++) {
int wordIndex = keywordScores.get(i).getID();
keywords.add((String) vocabulary[wordIndex]);
}
return keywords;
}
private List<String> splitIntoSentences(String text) {
return Arrays.stream(text.split("[。!?!?;;\n]"))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
public static void main(String[] args) throws Exception {
AdvancedLDAChunking advancedChunker = new AdvancedLDAChunking(4);
String bookChapter = """
人工智能的历史可以追溯到20世纪50年代。图灵测试提出了机器智能的判断标准。
达特茅斯会议正式确立了人工智能这一学科。早期AI研究集中在符号推理和问题求解。
专家系统在80年代成为主流。MYCIN系统能够进行医学诊断。知识表示成为研究重点。
同时,神经网络研究也在进行。感知机模型被提出但存在局限性。
90年代统计学习方法兴起。支持向量机在分类任务中表现优秀。机器学习开始受到关注。
数据驱动的范式逐渐取代知识工程方法。
21世纪深度学习革命到来。大规模数据和算力推动神经网络发展。
ImageNet竞赛展示了深度学习的潜力。GPU加速了模型训练过程。
""";
System.out.println("=== 进阶LDA主题分块 ===");
List<TopicAwareChunk> chunks = advancedChunker.advancedTopicChunking(bookChapter, 3);
for (TopicAwareChunk chunk : chunks) {
System.out.println("\n--- 主题分块 ---");
System.out.println("主题ID: " + chunk.getTopicId());
System.out.println("置信度: " + String.format("%.3f", chunk.getConfidence()));
System.out.println("关键词: " + String.join(", ", chunk.getKeywords()));
System.out.println("句子范围: " + chunk.getStartSentence() + " - " + chunk.getEndSentence());
System.out.println("内容: " + chunk.getText());
}
}
// 数据类
public static class DocumentSegment {
private final String id;
private final String text;
private final int startSentence;
private final int endSentence;
public DocumentSegment(String id, String text, int start, int end) {
this.id = id;
this.text = text;
this.startSentence = start;
this.endSentence = end;
}
public String getId() { return id; }
public String getText() { return text; }
public int getStartSentence() { return startSentence; }
public int getEndSentence() { return endSentence; }
}
public static class TopicAwareChunk {
private final String text;
private final int topicId;
private final double confidence;
private final List<String> keywords;
private final int startSentence;
private final int endSentence;
public TopicAwareChunk(String text, int topicId, double confidence,
List<String> keywords, int start, int end) {
this.text = text;
this.topicId = topicId;
this.confidence = confidence;
this.keywords = keywords;
this.startSentence = start;
this.endSentence = end;
}
// Getters
public String getText() { return text; }
public int getTopicId() { return topicId; }
public double getConfidence() { return confidence; }
public List<String> getKeywords() { return keywords; }
public int getStartSentence() { return startSentence; }
public int getEndSentence() { return endSentence; }
}
}
4、高级分块策略
(1)、小-大分块
核心思想:小块用于检索,大块用于生成
优点:检索精度高(小块精准匹配)生成质量好(大块提供完整上下文)
缺点:存储成本较高、需要维护分块间的关系、实现复杂度较高
适用场景:复杂问答系统,需要兼顾解锁准确与生成完整性
示例代码:
import java.util.*;
import java.util.regex.Pattern;
public class AdvancedSmallBigChunking {
public static class EnhancedChunkPair {
private final TextSegment smallChunk;
private final TextSegment bigChunk;
private final String chunkId;
private final List<String> keyPoints; // 关键信息点
private final String summary; // 摘要
public EnhancedChunkPair(String chunkId, TextSegment smallChunk,
TextSegment bigChunk, List<String> keyPoints, String summary) {
this.chunkId = chunkId;
this.smallChunk = smallChunk;
this.bigChunk = bigChunk;
this.keyPoints = keyPoints;
this.summary = summary;
}
// Getters
public TextSegment getSmallChunk() { return smallChunk; }
public TextSegment getBigChunk() { return bigChunk; }
public String getChunkId() { return chunkId; }
public List<String> getKeyPoints() { return keyPoints; }
public String getSummary() { return summary; }
}
/**
* 创建增强版小-大分块
*/
public static List<EnhancedChunkPair> createEnhancedChunks(String text,
int smallChunkSize,
int bigChunkSize) {
List<EnhancedChunkPair> enhancedPairs = new ArrayList<>();
// 1. 按段落分割大块
List<TextSegment> bigChunks = splitByParagraphs(text, bigChunkSize);
for (int i = 0; i < bigChunks.size(); i++) {
TextSegment bigChunk = bigChunks.get(i);
String bigChunkText = bigChunk.text();
// 2. 提取关键信息
List<String> keyPoints = extractKeyInformation(bigChunkText);
String summary = generateSummary(bigChunkText, smallChunkSize);
// 3. 创建小块(使用摘要或关键点)
TextSegment smallChunk = createSmallChunk(bigChunkText, keyPoints, summary, smallChunkSize);
EnhancedChunkPair pair = new EnhancedChunkPair(
"enhanced_chunk_" + i,
smallChunk,
bigChunk,
keyPoints,
summary
);
enhancedPairs.add(pair);
}
return enhancedPairs;
}
/**
* 按段落分割
*/
private static List<TextSegment> splitByParagraphs(String text, int maxChunkSize) {
List<TextSegment> chunks = new ArrayList<>();
String[] paragraphs = text.split("\n\n");
StringBuilder currentChunk = new StringBuilder();
for (String paragraph : paragraphs) {
if (currentChunk.length() + paragraph.length() > maxChunkSize && currentChunk.length() > 0) {
chunks.add(TextSegment.from(currentChunk.toString()));
currentChunk = new StringBuilder();
}
currentChunk.append(paragraph).append("\n\n");
}
if (currentChunk.length() > 0) {
chunks.add(TextSegment.from(currentChunk.toString().trim()));
}
return chunks;
}
/**
* 提取关键信息
*/
private static List<String> extractKeyInformation(String text) {
List<String> keyPoints = new ArrayList<>();
String[] sentences = text.split("[。!?!?]");
// 识别包含重要信息的句子
Pattern keyPattern = Pattern.compile("(主要|关键|重要|核心|包括|分为|标准是|诊断|治疗)");
for (String sentence : sentences) {
if (keyPattern.matcher(sentence).find() && sentence.length() > 5) {
keyPoints.add(sentence.trim());
}
}
// 如果没找到关键句,使用前两个句子
if (keyPoints.isEmpty() && sentences.length > 0) {
for (int i = 0; i < Math.min(2, sentences.length); i++) {
keyPoints.add(sentences[i].trim());
}
}
return keyPoints;
}
/**
* 生成摘要
*/
private static String generateSummary(String text, int maxLength) {
String[] sentences = text.split("[。!?!?]");
if (sentences.length == 0) return "";
// 简单摘要:取开头和结尾的句子
List<String> summarySentences = new ArrayList<>();
if (sentences.length >= 1) {
summarySentences.add(sentences[0]); // 第一句
}
if (sentences.length >= 3) {
summarySentences.add(sentences[sentences.length - 1]); // 最后一句
}
String summary = String.join("。", summarySentences) + "。";
// 确保不超过最大长度
if (summary.length() > maxLength) {
summary = summary.substring(0, maxLength);
}
return summary;
}
/**
* 创建小块
*/
private static TextSegment createSmallChunk(String bigChunkText,
List<String> keyPoints,
String summary,
int maxLength) {
// 优先使用关键点
if (!keyPoints.isEmpty()) {
String keyPointsText = String.join("。", keyPoints) + "。";
if (keyPointsText.length() <= maxLength) {
return TextSegment.from(keyPointsText);
}
}
// 其次使用摘要
if (summary.length() <= maxLength) {
return TextSegment.from(summary);
}
// 最后使用开头部分
return TextSegment.from(bigChunkText.substring(0, Math.min(maxLength, bigChunkText.length())));
}
public static void main(String[] args) {
String medicalText = "
糖尿病是一种慢性代谢性疾病,主要特征是血糖水平持续升高。
病因与发病机制:
糖尿病的发病与胰岛素分泌不足或胰岛素抵抗有关。1型糖尿病主要是由于自身免疫反应破坏胰岛β细胞。
2型糖尿病则与遗传因素、肥胖、缺乏运动等生活方式密切相关。胰岛素抵抗导致细胞对胰岛素的敏感性下降。
临床表现:
典型症状包括多饮、多尿、多食和体重下降。患者可能出现视力模糊、疲劳、伤口愈合缓慢等症状。
长期高血糖会导致多种并发症,包括心血管疾病、肾病、视网膜病变和神经病变。
诊断标准:
空腹血糖≥7.0mmol/L,或餐后2小时血糖≥11.1mmol/L,或糖化血红蛋白≥6.5%。
确诊需要重复检测或结合临床症状综合判断。口服葡萄糖耐量试验也可用于诊断。
治疗与管理:
治疗包括生活方式干预和药物治疗。饮食控制要求低糖、低脂、高纤维饮食。
规律运动有助于提高胰岛素敏感性。药物治疗包括口服降糖药和胰岛素注射。
患者需要定期监测血糖,控制血压和血脂,预防并发症的发生。
""";
List<EnhancedChunkPair> enhancedPairs = createEnhancedChunks(medicalText, 150, 600);
System.out.println("=== 增强版小-大分块 ===");
for (EnhancedChunkPair pair : enhancedPairs) {
System.out.println("\n--- 分块ID: " + pair.getChunkId() + " ---");
System.out.println("关键点: " + pair.getKeyPoints());
System.out.println("摘要: " + pair.getSummary());
System.out.println("小块: " + pair.getSmallChunk().text());
System.out.println("大块长度: " + pair.getBigChunk().text().length() + " 字符");
}
}
}
(2)代理式分块
核心思想:用LLM模拟人类却阅读,动态识别
优点:智能识别语义边界、动态适应不同文档结构、理解内容重要性和类型、处理高度非结构化文本
缺点:LLM调用成本高、处理速度较慢、实现复杂度高
适用场景:实验性项目,高度非结构化文本
示例代码:
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class AgenticChunking {
private final ChatLanguageModel model;
private final Map<String, List<TextChunk>> documentChunks = new ConcurrentHashMap<>();
public AgenticChunking(ChatLanguageModel model) {
this.model = model;
}
public static class DynamicChunk {
private final String content;
private final String chunkId;
private final String title;
private final List<String> keywords;
private final ChunkType type;
private final int importance;
public DynamicChunk(String content, String chunkId, String title,
List<String> keywords, ChunkType type, int importance) {
this.content = content;
this.chunkId = chunkId;
this.title = title;
this.keywords = keywords;
this.type = type;
this.importance = importance;
}
// Getters
public String getContent() { return content; }
public String getChunkId() { return chunkId; }
public String getTitle() { return title; }
public List<String> getKeywords() { return keywords; }
public ChunkType getType() { return type; }
public int getImportance() { return importance; }
}
public enum ChunkType {
INTRODUCTION, // 介绍
KEY_CONCEPT, // 关键概念
EXAMPLE, // 示例
PROCEDURE, // 步骤流程
CONCLUSION, // 结论
REFERENCE, // 参考
OTHER // 其他
}
/**
* 代理式分块 - 使用LLM模拟人类阅读行为
*/
public List<DynamicChunk> agenticChunking(String documentId, String text) {
System.out.println("🤖 开始代理式分块分析文档: " + documentId);
// 1. 文档整体分析
DocumentAnalysis analysis = analyzeDocumentStructure(text);
// 2. 动态识别分块边界
List<ChunkBoundary> boundaries = identifyChunkBoundaries(text, analysis);
// 3. 创建智能分块
List<DynamicChunk> chunks = createIntelligentChunks(text, boundaries, documentId);
// 4. 缓存分块结果
documentChunks.put(documentId, chunks);
System.out.println("✅ 完成分块分析,生成 " + chunks.size() + " 个智能分块");
return chunks;
}
/**
* 文档结构分析
*/
private DocumentAnalysis analyzeDocumentStructure(String text) {
String prompt = """
请分析以下文档的结构和内容特点:
%s
请以JSON格式返回分析结果,包括:
- document_type: 文档类型
- main_topics: 主要主题列表
- complexity_level: 复杂度级别(简单/中等/复杂)
- estimated_chunks: 建议分块数量
- key_sections: 关键章节列表
""".formatted(text.substring(0, Math.min(1000, text.length())));
String analysisJson = model.generate(prompt);
// 解析LLM返回的分析结果(简化处理)
return new DocumentAnalysis(
"混合型文档",
Arrays.asList("主要概念", "应用示例", "实施步骤"),
"中等",
5,
Arrays.asList("引言", "核心内容", "总结")
);
}
/**
* 识别分块边界
*/
private List<ChunkBoundary> identifyChunkBoundaries(String text, DocumentAnalysis analysis) {
List<ChunkBoundary> boundaries = new ArrayList<>();
// 将文本分成较小的段落进行处理
List<String> paragraphs = splitIntoParagraphs(text);
for (int i = 0; i < paragraphs.size(); i++) {
String paragraph = paragraphs.get(i);
// 使用LLM判断段落性质和重要性
ChunkAnalysis chunkAnalysis = analyzeParagraph(paragraph, i, paragraphs.size());
boundaries.add(new ChunkBoundary(
i,
chunkAnalysis.startIndex,
chunkAnalysis.endIndex,
chunkAnalysis.chunkType,
chunkAnalysis.importance,
chunkAnalysis.shouldStartNewChunk
));
}
return mergeRelatedBoundaries(boundaries, paragraphs);
}
/**
* 分析单个段落
*/
private ChunkAnalysis analyzeParagraph(String paragraph, int index, int totalParagraphs) {
String prompt = """
分析以下段落的性质和重要性:
段落内容: %s
段落位置: 第%d段/共%d段
请判断:
1. 段落类型(介绍、关键概念、示例、步骤、结论、参考、其他)
2. 重要性(1-10分,10分最重要)
3. 是否应该开始新的分块(true/false)
以JSON格式返回结果。
""".formatted(paragraph, index + 1, totalParagraphs);
String response = model.generate(prompt);
// 解析响应(简化处理)
return new ChunkAnalysis(
index * 100,
(index + 1) * 100,
determineChunkType(paragraph),
calculateImportance(paragraph),
shouldStartNewChunk(paragraph, index)
);
}
/**
* 创建智能分块
*/
private List<DynamicChunk> createIntelligentChunks(String text,
List<ChunkBoundary> boundaries,
String documentId) {
List<DynamicChunk> chunks = new ArrayList<>();
StringBuilder currentChunk = new StringBuilder();
ChunkType currentType = ChunkType.OTHER;
List<String> currentKeywords = new ArrayList<>();
int importance = 5;
int chunkNumber = 0;
for (ChunkBoundary boundary : boundaries) {
String segment = text.substring(boundary.startIndex, boundary.endIndex);
if (boundary.shouldStartNewChunk && currentChunk.length() > 0) {
// 完成当前分块
String chunkContent = currentChunk.toString();
DynamicChunk chunk = new DynamicChunk(
chunkContent,
documentId + "_chunk_" + chunkNumber,
generateChunkTitle(chunkContent, currentType),
extractKeywords(chunkContent),
currentType,
importance
);
chunks.add(chunk);
chunkNumber++;
// 重置
currentChunk = new StringBuilder();
currentKeywords = new ArrayList<>();
}
currentChunk.append(segment).append("\n\n");
currentType = boundary.chunkType;
importance = Math.max(importance, boundary.importance);
}
// 添加最后一个分块
if (currentChunk.length() > 0) {
String chunkContent = currentChunk.toString();
DynamicChunk chunk = new DynamicChunk(
chunkContent,
documentId + "_chunk_" + chunkNumber,
generateChunkTitle(chunkContent, currentType),
extractKeywords(chunkContent),
currentType,
importance
);
chunks.add(chunk);
}
return chunks;
}
/**
* 生成分块标题
*/
private String generateChunkTitle(String content, ChunkType type) {
String prompt = """
为以下内容生成一个简洁的标题(最多10个汉字):
内容类型: %s
内容: %s
只返回标题,不要其他内容。
""".formatted(type, content.substring(0, Math.min(200, content.length())));
return model.generate(prompt).trim();
}
/**
* 提取关键词
*/
private List<String> extractKeywords(String content) {
String prompt = """
从以下内容中提取3-5个最重要的关键词:
%s
以逗号分隔返回关键词。
""".formatted(content.substring(0, Math.min(300, content.length())));
String response = model.generate(prompt);
return Arrays.asList(response.split(","));
}
// 辅助方法
private List<String> splitIntoParagraphs(String text) {
return Arrays.stream(text.split("\n\n"))
.filter(p -> !p.trim().isEmpty())
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
private List<ChunkBoundary> mergeRelatedBoundaries(List<ChunkBoundary> boundaries, List<String> paragraphs) {
// 合并相关的边界(简化实现)
List<ChunkBoundary> merged = new ArrayList<>();
ChunkBoundary current = boundaries.get(0);
for (int i = 1; i < boundaries.size(); i++) {
ChunkBoundary next = boundaries.get(i);
if (shouldMerge(current, next)) {
current = mergeTwoBoundaries(current, next);
} else {
merged.add(current);
current = next;
}
}
merged.add(current);
return merged;
}
private boolean shouldMerge(ChunkBoundary a, ChunkBoundary b) {
return a.chunkType == b.chunkType && a.importance == b.importance;
}
private ChunkBoundary mergeTwoBoundaries(ChunkBoundary a, ChunkBoundary b) {
return new ChunkBoundary(
a.paragraphIndex,
a.startIndex,
b.endIndex,
a.chunkType,
a.importance,
a.shouldStartNewChunk
);
}
// 简化的方法实现
private ChunkType determineChunkType(String paragraph) {
if (paragraph.contains("介绍") || paragraph.contains("概述")) return ChunkType.INTRODUCTION;
if (paragraph.contains("例如") || paragraph.contains("比如")) return ChunkType.EXAMPLE;
if (paragraph.contains("步骤") || paragraph.contains("流程")) return ChunkType.PROCEDURE;
if (paragraph.contains("总结") || paragraph.contains("结论")) return ChunkType.CONCLUSION;
return ChunkType.KEY_CONCEPT;
}
private int calculateImportance(String paragraph) {
if (paragraph.contains("重要") || paragraph.contains("关键")) return 9;
if (paragraph.contains("注意") || paragraph.contains("警告")) return 8;
return 6;
}
private boolean shouldStartNewChunk(String paragraph, int index) {
return index == 0 || paragraph.length() > 200 ||
paragraph.matches(".*[一二三四五六七八九十]、.*");
}
// 数据类
private static class DocumentAnalysis {
final String documentType;
final List<String> mainTopics;
final String complexityLevel;
final int estimatedChunks;
final List<String> keySections;
DocumentAnalysis(String type, List<String> topics, String complexity,
int chunks, List<String> sections) {
this.documentType = type;
this.mainTopics = topics;
this.complexityLevel = complexity;
this.estimatedChunks = chunks;
this.keySections = sections;
}
}
private static class ChunkBoundary {
final int paragraphIndex;
final int startIndex;
final int endIndex;
final ChunkType chunkType;
final int importance;
final boolean shouldStartNewChunk;
ChunkBoundary(int pIndex, int start, int end, ChunkType type,
int importance, boolean newChunk) {
this.paragraphIndex = pIndex;
this.startIndex = start;
this.endIndex = end;
this.chunkType = type;
this.importance = importance;
this.shouldStartNewChunk = newChunk;
}
}
private static class ChunkAnalysis {
final int startIndex;
final int endIndex;
final ChunkType chunkType;
final int importance;
final boolean shouldStartNewChunk;
ChunkAnalysis(int start, int end, ChunkType type, int importance, boolean newChunk) {
this.startIndex = start;
this.endIndex = end;
this.chunkType = type;
this.importance = importance;
this.shouldStartNewChunk = newChunk;
}
}
public static void main(String[] args) {
// 使用模拟的LLM(实际项目中应使用真实的LLM)
ChatLanguageModel model = new MockChatModel();
AgenticChunking agenticChunker = new AgenticChunking(model);
// 高度非结构化的文本示例
String unstructuredText = """
今天我想跟大家分享一些关于机器学习的心得。
其实机器学习并不神秘,就像教小孩认东西一样。比如你要教计算机认识猫,
就给它看很多猫的图片,它慢慢就学会了。
这里有个重要的概念叫"特征提取"。计算机会自动找出猫的共同特点,
比如尖耳朵、胡须什么的。
具体怎么做呢?首先需要收集数据,然后清洗数据,接着选择模型,
训练模型,最后评估效果。这个过程可能需要反复调整。
我最近用TensorFlow做了一个项目,识别手写数字,准确率能达到98%!
代码其实很简单,就几十行。
总之,机器学习现在应用越来越广泛,从推荐系统到自动驾驶都在用。
学习曲线虽然有点陡,但坚持下去会有收获的。
对了,昨天看到一篇论文,说新的Transformer架构在自然语言处理中效果很好,
大家可以关注一下这个方向。
""";
System.out.println("🚀 开始代理式分块演示");
List<DynamicChunk> chunks = agenticChunker.agenticChunking("doc_001", unstructuredText);
System.out.println("\n📊 分块结果分析:");
for (DynamicChunk chunk : chunks) {
System.out.println("\n=== 分块: " + chunk.getTitle() + " ===");
System.out.println("类型: " + chunk.getType());
System.out.println("重要性: " + chunk.getImportance() + "/10");
System.out.println("关键词: " + String.join(", ", chunk.getKeywords()));
System.out.println("内容预览: " +
chunk.getContent().substring(0, Math.min(100, chunk.getContent().length())) + "...");
System.out.println("长度: " + chunk.getContent().length() + " 字符");
}
}
// 模拟LLM类
static class MockChatModel implements ChatLanguageModel {
@Override
public String generate(String userMessage) {
// 模拟LLM响应
if (userMessage.contains("分析以下文档的结构")) {
return """
{
"document_type": "技术分享",
"main_topics": ["机器学习基础", "特征提取", "项目实践"],
"complexity_level": "中等",
"estimated_chunks": 4,
"key_sections": ["介绍", "核心概念", "实践案例", "总结"]
}
""";
} else if (userMessage.contains("分析以下段落的性质")) {
return """
{
"chunk_type": "INTRODUCTION",
"importance": 7,
"should_start_new_chunk": true
}
""";
} else if (userMessage.contains("生成一个简洁的标题")) {
return "机器学习入门分享";
} else if (userMessage.contains("提取3-5个最重要的关键词")) {
return "机器学习,特征提取,模型训练,TensorFlow";
}
return "{}";
}
@Override
public AiMessage generate(SystemMessage systemMessage, UserMessage userMessage) {
return new AiMessage(generate(userMessage.text()));
}
@Override
public AiMessage generate(UserMessage userMessage) {
return new AiMessage(generate(userMessage.text()));
}
@Override
public List<AiMessage> generate(List<dev.langchain4j.data.message.ChatMessage> messages) {
return Arrays.asList(new AiMessage(generate(messages.get(messages.size()-1).text())));
}
}
}
5、混合分块
核心思想:先用宏观策略(如结构化分块)粗切,再对过大的块进行精细策略(如递归或语义分块)细切
优点:兼顾宏观结构和微观语义、自适应不同内容密度、平衡分块质量和效率、处理复杂文档结构能力强
缺点:实现复杂度最高、需要多轮处理、参数调优复杂、计算资源消耗大
适用场景:结构复杂,内容密度不匀(如技术白皮书、企业年报)
示例代码:
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentByHeaderSplitter;
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.all.minilm.AllMiniLmL6V2EmbeddingModel;
import java.util.*;
import java.util.stream.Collectors;
public class HybridChunking {
private final EmbeddingModel embeddingModel;
private final int maxChunkSize;
private final int minChunkSize;
public HybridChunking(int maxChunkSize, int minChunkSize) {
this.embeddingModel = new AllMiniLmL6V2EmbeddingModel();
this.maxChunkSize = maxChunkSize;
this.minChunkSize = minChunkSize;
}
public static class HybridChunk {
private final String content;
private final String chunkId;
private final ChunkLevel level;
private final String parentId;
private final Map<String, Object> metadata;
public HybridChunk(String content, String chunkId, ChunkLevel level,
String parentId, Map<String, Object> metadata) {
this.content = content;
this.chunkId = chunkId;
this.level = level;
this.parentId = parentId;
this.metadata = metadata;
}
// Getters
public String getContent() { return content; }
public String getChunkId() { return chunkId; }
public ChunkLevel getLevel() { return level; }
public String getParentId() { return parentId; }
public Map<String, Object> getMetadata() { return metadata; }
}
public enum ChunkLevel {
COARSE, // 粗粒度分块
FINE, // 细粒度分块
SEMANTIC // 语义分块
}
/**
* 混合分块策略
*/
public List<HybridChunk> hybridChunking(String documentId, String text) {
System.out.println("🔄 开始混合分块处理文档: " + documentId);
System.out.println("文档总长度: " + text.length() + " 字符");
List<HybridChunk> allChunks = new ArrayList<>();
// 第一阶段:宏观策略 - 结构化分块(粗切)
System.out.println("📊 第一阶段:结构化分块(粗切)");
List<HybridChunk> coarseChunks = coarseStructuredChunking(documentId, text);
System.out.println("生成粗粒度分块: " + coarseChunks.size() + " 个");
// 第二阶段:对过大的块进行精细分块
System.out.println("🔍 第二阶段:精细分块(细切)");
for (HybridChunk coarseChunk : coarseChunks) {
if (needsFineChunking(coarseChunk)) {
List<HybridChunk> fineChunks = applyFineChunking(coarseChunk);
allChunks.addAll(fineChunks);
} else {
allChunks.add(coarseChunk);
}
}
// 第三阶段:对高密度内容进行语义分块
System.out.println("🎯 第三阶段:语义分块(优化)");
List<HybridChunk> finalChunks = applySemanticOptimization(allChunks);
System.out.println("✅ 混合分块完成,总计: " + finalChunks.size() + " 个分块");
return finalChunks;
}
/**
* 第一阶段:宏观结构化分块
*/
private List<HybridChunk> coarseStructuredChunking(String documentId, String text) {
List<HybridChunk> coarseChunks = new ArrayList<>();
Document document = Document.from(text);
// 策略1:尝试按标题分块
try {
DocumentByHeaderSplitter headerSplitter = new DocumentByHeaderSplitter(
Arrays.asList(), // 自动检测标题
maxChunkSize * 2, // 较大的块大小
maxChunkSize / 4 // 重叠
);
List<TextSegment> headerChunks = headerSplitter.split(document);
if (headerChunks.size() > 1) {
System.out.println(" 使用标题分块策略,分块数: " + headerChunks.size());
for (int i = 0; i < headerChunks.size(); i++) {
TextSegment segment = headerChunks.get(i);
Map<String, Object> metadata = new HashMap<>();
metadata.put("chunking_strategy", "header_based");
metadata.put("coarse_chunk_index", i);
coarseChunks.add(new HybridChunk(
segment.text(),
documentId + "_coarse_" + i,
ChunkLevel.COARSE,
null,
metadata
));
}
return coarseChunks;
}
} catch (Exception e) {
System.out.println(" 标题分块失败,尝试其他策略");
}
// 策略2:按段落分块
DocumentByParagraphSplitter paragraphSplitter = new DocumentByParagraphSplitter(
maxChunkSize * 2,
maxChunkSize / 4
);
List<TextSegment> paragraphChunks = paragraphSplitter.split(document);
System.out.println(" 使用段落分块策略,分块数: " + paragraphChunks.size());
for (int i = 0; i < paragraphChunks.size(); i++) {
TextSegment segment = paragraphChunks.get(i);
Map<String, Object> metadata = new HashMap<>();
metadata.put("chunking_strategy", "paragraph_based");
metadata.put("coarse_chunk_index", i);
metadata.put("content_density", calculateContentDensity(segment.text()));
coarseChunks.add(new HybridChunk(
segment.text(),
documentId + "_coarse_" + i,
ChunkLevel.COARSE,
null,
metadata
));
}
return coarseChunks;
}
/**
* 判断是否需要精细分块
*/
private boolean needsFineChunking(HybridChunk chunk) {
String content = chunk.getContent();
// 条件1:长度超过阈值
if (content.length() > maxChunkSize) {
return true;
}
// 条件2:内容密度过高
double density = calculateContentDensity(content);
if (density > 0.8 && content.length() > minChunkSize * 2) {
return true;
}
// 条件3:包含多个主题
if (containsMultipleTopics(content)) {
return true;
}
return false;
}
/**
* 应用精细分块策略
*/
private List<HybridChunk> applyFineChunking(HybridChunk coarseChunk) {
String content = coarseChunk.getContent();
List<HybridChunk> fineChunks = new ArrayList<>();
System.out.println(" 对分块 " + coarseChunk.getChunkId() + " 进行精细分块");
System.out.println(" 原始大小: " + content.length() + " 字符");
// 根据内容特性选择精细分块策略
if (isTechnicalContent(content)) {
// 技术内容使用递归分块
fineChunks.addAll(recursiveChunking(coarseChunk));
} else if (isNarrativeContent(content)) {
// 叙述性内容使用句子分块
fineChunks.addAll(sentenceBasedChunking(coarseChunk));
} else {
// 默认使用固定长度分块
fineChunks.addAll(fixedLengthChunking(coarseChunk));
}
System.out.println(" 精细分块结果: " + fineChunks.size() + " 个子分块");
return fineChunks;
}
/**
* 递归分块策略
*/
private List<HybridChunk> recursiveChunking(HybridChunk coarseChunk) {
List<HybridChunk> chunks = new ArrayList<>();
Document document = Document.from(coarseChunk.getContent());
var recursiveSplitter = DocumentSplitters.recursive(maxChunkSize, maxChunkSize / 4);
List<TextSegment> segments = recursiveSplitter.split(document);
for (int i = 0; i < segments.size(); i++) {
TextSegment segment = segments.get(i);
Map<String, Object> metadata = new HashMap<>(coarseChunk.getMetadata());
metadata.put("chunking_strategy", "recursive");
metadata.put("fine_chunk_index", i);
metadata.put("parent_chunk", coarseChunk.getChunkId());
chunks.add(new HybridChunk(
segment.text(),
coarseChunk.getChunkId() + "_fine_" + i,
ChunkLevel.FINE,
coarseChunk.getChunkId(),
metadata
));
}
return chunks;
}
/**
* 句子分块策略
*/
private List<HybridChunk> sentenceBasedChunking(HybridChunk coarseChunk) {
List<HybridChunk> chunks = new ArrayList<>();
String[] sentences = coarseChunk.getContent().split("[。!?!?;;]");
List<String> currentChunkSentences = new ArrayList<>();
int currentLength = 0;
for (String sentence : sentences) {
String trimmed = sentence.trim();
if (trimmed.isEmpty()) continue;
if (currentLength + trimmed.length() > maxChunkSize && !currentChunkSentences.isEmpty()) {
// 完成当前分块
String chunkContent = String.join("。", currentChunkSentences) + "。";
chunks.add(createFineChunk(coarseChunk, chunkContent, chunks.size(), "sentence_based"));
currentChunkSentences = new ArrayList<>();
currentLength = 0;
}
currentChunkSentences.add(trimmed);
currentLength += trimmed.length();
}
// 添加最后一个分块
if (!currentChunkSentences.isEmpty()) {
String chunkContent = String.join("。", currentChunkSentences) + "。";
chunks.add(createFineChunk(coarseChunk, chunkContent, chunks.size(), "sentence_based"));
}
return chunks;
}
/**
* 固定长度分块策略
*/
private List<HybridChunk> fixedLengthChunking(HybridChunk coarseChunk) {
List<HybridChunk> chunks = new ArrayList<>();
String content = coarseChunk.getContent();
int start = 0;
int chunkIndex = 0;
while (start < content.length()) {
int end = Math.min(start + maxChunkSize, content.length());
// 尝试在句子边界处切割
if (end < content.length()) {
int sentenceEnd = findSentenceBoundary(content, end);
if (sentenceEnd > start) {
end = sentenceEnd;
}
}
String chunkContent = content.substring(start, end);
chunks.add(createFineChunk(coarseChunk, chunkContent, chunkIndex, "fixed_length"));
start = end;
chunkIndex++;
}
return chunks;
}
/**
* 第三阶段:语义优化
*/
private List<HybridChunk> applySemanticOptimization(List<HybridChunk> chunks) {
List<HybridChunk> optimizedChunks = new ArrayList<>();
for (int i = 0; i < chunks.size(); i++) {
HybridChunk current = chunks.get(i);
String content = current.getContent();
// 检查分块是否过小,考虑与相邻分块合并
if (content.length() < minChunkSize && i < chunks.size() - 1) {
HybridChunk next = chunks.get(i + 1);
String combined = content + " " + next.getContent();
if (combined.length() <= maxChunkSize && isSemanticallyRelated(content, next.getContent())) {
// 合并分块
Map<String, Object> metadata = new HashMap<>(current.getMetadata());
metadata.put("chunking_strategy", "semantic_merged");
metadata.put("merged_chunks", Arrays.asList(current.getChunkId(), next.getChunkId()));
optimizedChunks.add(new HybridChunk(
combined,
current.getChunkId() + "_merged",
ChunkLevel.SEMANTIC,
null,
metadata
));
i++; // 跳过下一个分块
continue;
}
}
optimizedChunks.add(current);
}
return optimizedChunks;
}
// 辅助方法
private double calculateContentDensity(String text) {
// 计算内容密度:非空格字符比例
long nonSpaceChars = text.chars().filter(c -> !Character.isWhitespace(c)).count();
return (double) nonSpaceChars / text.length();
}
private boolean containsMultipleTopics(String text) {
// 简单判断是否包含多个主题(通过连接词判断)
String[] topicIndicators = {"另外", "此外", "另一方面", "同时", "不仅如此"};
int count = 0;
for (String indicator : topicIndicators) {
if (text.contains(indicator)) count++;
}
return count >= 2;
}
private boolean isTechnicalContent(String text) {
// 判断是否为技术内容
String[] technicalTerms = {"代码", "算法", "函数", "参数", "接口", "架构", "部署"};
for (String term : technicalTerms) {
if (text.contains(term)) return true;
}
return false;
}
private boolean isNarrativeContent(String text) {
// 判断是否为叙述性内容
String[] narrativeIndicators = {"首先", "然后", "接着", "最后", "例如", "比如"};
for (String indicator : narrativeIndicators) {
if (text.contains(indicator)) return true;
}
return false;
}
private int findSentenceBoundary(String text, int position) {
// 在位置附近查找句子边界
for (int i = position; i < Math.min(position + 50, text.length()); i++) {
char c = text.charAt(i);
if (c == '。' || c == '!' || c == '?' || c == ';') {
return i + 1;
}
}
return position;
}
private boolean isSemanticallyRelated(String text1, String text2) {
// 使用嵌入模型计算语义相似度(简化版)
try {
// 在实际应用中应该使用真实的嵌入计算
return calculateTextSimilarity(text1, text2) > 0.7;
} catch (Exception e) {
// 回退到基于词汇重叠的简单方法
return calculateVocabularyOverlap(text1, text2) > 0.3;
}
}
private double calculateTextSimilarity(String text1, String text2) {
// 简化的相似度计算(实际应使用嵌入模型)
Set<String> words1 = new HashSet<>(Arrays.asList(text1.split("\\W+")));
Set<String> words2 = new HashSet<>(Arrays.asList(text2.split("\\W+")));
Set<String> intersection = new HashSet<>(words1);
intersection.retainAll(words2);
Set<String> union = new HashSet<>(words1);
union.addAll(words2);
return union.isEmpty() ? 0.0 : (double) intersection.size() / union.size();
}
private double calculateVocabularyOverlap(String text1, String text2) {
Set<String> words1 = new HashSet<>(Arrays.asList(text1.split("\\W+")));
Set<String> words2 = new HashSet<>(Arrays.asList(text2.split("\\W+")));
Set<String> intersection = new HashSet<>(words1);
intersection.retainAll(words2);
return (double) intersection.size() / Math.min(words1.size(), words2.size());
}
private HybridChunk createFineChunk(HybridChunk parent, String content, int index, String strategy) {
Map<String, Object> metadata = new HashMap<>(parent.getMetadata());
metadata.put("chunking_strategy", strategy);
metadata.put("fine_chunk_index", index);
metadata.put("parent_chunk", parent.getChunkId());
return new HybridChunk(
content,
parent.getChunkId() + "_fine_" + index,
ChunkLevel.FINE,
parent.getChunkId(),
metadata
);
}
public static void main(String[] args) {
// 创建混合分块器
HybridChunking hybridChunker = new HybridChunking(500, 100);
// 技术白皮书示例(结构复杂,内容密度不均)
String technicalWhitepaper = """
# 人工智能平台技术白皮书
## 第一章 概述
本白皮书介绍了新一代人工智能平台的核心架构和关键技术。随着数字化转型的深入,企业对AI能力的需求日益增长。
## 第二章 核心架构
平台采用微服务架构,包含数据预处理、模型训练、推理服务、监控管理等核心模块。
数据预处理模块支持多种数据格式,包括结构化数据、文本、图像和视频。该模块提供数据清洗、特征工程、数据标注等功能。
模型训练模块集成多种机器学习框架,支持分布式训练和自动超参数调优。训练过程可实时监控,支持断点续训。
推理服务模块提供高并发、低延迟的模型推理能力。支持模型版本管理、A/B测试和流量控制。
## 第三章 关键技术
平台的核心技术包括联邦学习、自动机器学习、模型解释性等。
联邦学习技术使得在保护数据隐私的前提下进行联合建模成为可能。各参与方在不共享原始数据的情况下共同训练模型。
自动机器学习技术大幅降低了AI应用的门槛。平台自动进行特征工程、模型选择和超参数优化。
模型解释性技术帮助用户理解模型决策过程。提供特征重要性分析、决策路径可视化等功能。
## 第四章 应用场景
平台已成功应用于金融风控、医疗诊断、智能制造等多个领域。
在金融领域,平台帮助银行构建反欺诈系统,准确识别可疑交易。在医疗领域,辅助医生进行疾病诊断,提高诊断准确率。
## 第五章 总结与展望
未来平台将进一步加强在可解释AI、小样本学习等方面的能力,推动AI技术的普惠化发展。
""";
System.out.println("🚀 开始混合分块演示");
List<HybridChunk> chunks = hybridChunker.hybridChunking("whitepaper_001", technicalWhitepaper);
System.out.println("\n📊 最终分块结果:");
for (HybridChunk chunk : chunks) {
System.out.println("\n=== 分块: " + chunk.getChunkId() + " ===");
System.out.println("层级: " + chunk.getLevel());
System.out.println("策略: " + chunk.getMetadata().get("chunking_strategy"));
System.out.println("大小: " + chunk.getContent().length() + " 字符");
System.out.println("父分块: " + chunk.getParentId());
System.out.println("内容预览: " +
chunk.getContent().substring(0, Math.min(150, chunk.getContent().length())) + "...");
}
// 分析分块分布
analyzeChunkDistribution(chunks);
}
private static void analyzeChunkDistribution(List<HybridChunk> chunks) {
System.out.println("\n📈 分块分布分析:");
Map<ChunkLevel, Long> levelCount = chunks.stream()
.collect(Collectors.groupingBy(HybridChunk::getLevel, Collectors.counting()));
System.out.println("分块层级分布:");
levelCount.forEach((level, count) ->
System.out.println(" " + level + ": " + count + " 个分块"));
Map<String, Long> strategyCount = chunks.stream()
.collect(Collectors.groupingBy(
chunk -> (String) chunk.getMetadata().get("chunking_strategy"),
Collectors.counting()
));
System.out.println("分块策略分布:");
strategyCount.forEach((strategy, count) ->
System.out.println(" " + strategy + ": " + count + " 个分块"));
// 大小分布
IntSummaryStatistics stats = chunks.stream()
.mapToInt(chunk -> chunk.getContent().length())
.summaryStatistics();
System.out.println("分块大小统计:");
System.out.println(" 最小: " + stats.getMin() + " 字符");
System.out.println(" 最大: " + stats.getMax() + " 字符");
System.out.println(" 平均: " + (int) stats.getAverage() + " 字符");
}
}
5、如何选择最佳分块策略
第一步:基准策略 通用性强,效果文档,适合建立性能基线
第二步:结构优化 若文档有结构,优先使用结构化分块
第三步:精度瓶劲 检索不准或生产不完整时引入语义/小大分块
第四步:极度复杂 多格式、长文本、高密度场景的终极方案
6、技术选型
1、选择医疗文档分块策略时考虑:
(1)、文档类型:病历、检查报告、处方等
(2)、内容密度:临床描述 vs 检查数值
(3)、检索需求:症状检索 vs 诊断检索
(4)、隐私保护:敏感信息处理
2、选择法律文档分块策略时考虑:
(1)、文档类型:合同、法规、判决书等
(2)、条款结构:是否有明确的条款编号
(3)、引用关系:条款间的引用和依赖
(4)、法律效力:保持法律语句的完整性
最终推荐:
医疗文档:优先使用基于章节的语义分块
法律文档:优先使用条款级的句子分块
7、法律文档分块方案
1. 合同协议
推荐方案:条款分块 + 义务分块
-
定义条款:按术语定义完整切割
-
权利义务:按主体分块(甲方义务、乙方权利)
-
违约责任:按违约情形分块
-
争议解决:完整保留仲裁/诉讼条款
2. 法律法规
推荐方案:条文分块 + 章节分块
-
法律条文:以"第X条"为边界切割
-
章节结构:按编、章、节层级分块
-
罚则部分:按处罚类型分块
3. 诉讼文书
推荐方案:诉请分块 + 事实分块
-
诉讼请求:按请求事项单独分块
-
事实理由:按时间顺序或逻辑关系切割
-
证据清单:按证据类型分块
-
法律依据:按法条引用分块
4. 判决书/裁定书
推荐方案:结构分块 + 论证分块
-
当事人信息:按诉讼地位分块
-
审理经过:按程序阶段切割
-
法院认定:按事实认定分块
-
判决主文:完整保留判决结果
5. 法律意见书
推荐方案:问题分块 + 分析分块
-
咨询问题:按问题点单独切割
-
法律分析:按分析逻辑分块
-
结论建议:完整保留最终意见
8、分块大小建议
(1)、医疗文档
-
临床描述:200-300字符
-
检查结果:150-200字符
-
诊断结论:100-150字符
-
用药指导:80-120字符
(2)、法律文档
-
合同条款:250-350字符
-
法律条文:200-300字符
-
事实陈述:300-400字符
-
判决主文:150-250字符
9、选择原则
(1)、医疗文档优先考虑
-
临床逻辑连续性
-
诊断完整性
-
时间顺序保持
-
专业术语不分割
(2)、法律文档优先考虑
-
法律效力完整性
-
条款逻辑严密性
-
引用关系保持
-
权利义务对应性
10、综合案例
1. PDF类医疗文档
(1) 电子病历PDF
切割方式:章节结构切割
适用:医院电子病历系统导出的PDF
切割策略:
• 按标准病历章节切割:主诉→现病史→既往史→体格检查→辅助检查→诊断
• 每个章节作为独立分块
• 章节内过长的内容按语义段落二次切割
优势:保持临床逻辑完整性,便于按病历结构检索
(2) 检验报告PDF
切割方式:表格感知切割
适用:实验室检验报告、影像学报告PDF
切割策略:
• 识别PDF中的表格结构
• 按检验项目组切割:血常规、生化全套、免疫检查等
• 每个项目组的"项目-结果-参考范围"作为整体
• 异常指标单独标记并重点切割
优势:保证检验数值与项目的对应关系,避免数据割裂
(3) 诊断小结PDF
text
适用:病程记录PDF 切割策略: • 初诊情况→按时间线切割 • 诊疗经过→按治疗阶段切割(手术期、药物治疗期等) • 诊后情况→现状描述整体切割 • 随访建议→完整保留 优势:保持医疗过程的时间连续性
2. Word类医疗文档
(1) 科研论文Word
切割方式:学术结构切割
适用:医学研究论文、综述Word文档
切割策略:
• 摘要→完整一块
• 引言→按研究背景和目的切割
• 方法→按实验设计、统计方法切割
• 结果→按图表和对应描述切割
• 讨论→按论点分块
• 结论→完整保留
优势:符合学术阅读习惯,便于按论文结构检索
(2) 药品说明书Word
切割方式:安全信息切割
适用:药品说明书、用药指南Word
切割策略:
• 适应症→独立分块
• 用法用量→按给药途径切割
• 不良反应→按系统器官分类切割
• 禁忌警告→完整保留强调
• 药物相互作用→按相互作用药物对切割
优势:重点突出安全信息,便于快速查阅
3. PDF类法律文档
(1) 合同协议PDF
切割方式:条款层级切割
适用:商业合同、协议PDF
切割策略:
• 按"第X条"进行一级切割
• 条款内按"款"、"项"进行二级切割
• 定义条款→术语和定义整体切割
• 附件内容→单独切割并与主文关联
优势:保持合同条款的法律效力完整性
(2) 法律法规PDF
切割方式:条文精确切割
适用:法律、法规、规章PDF
切割策略:
• 严格按"第X条"边界切割
• 条标题与条文内容不可分割
• 修正案内容与原条文关联切割
• 施行日期和适用范围附加到相关条文
优势:确保法律条文的准确性和权威性
(3) 判决书PDF
切割方式:司法结构切割
适用:法院判决书、裁定书PDF
切割策略:
• 当事人信息→按原被告分别切割
• 诉讼请求→完整切割不拆分
• 事实认定→按争议焦点切割
• 法院认为→按法律论证逻辑切割
• 判决主文→绝对完整不切割
优势:维护司法文书的严肃性和完整性
4. TXT类法律文书
(1) 法律条文TXT
切割方式:递归句子切割
适用:纯文本格式的法律法规
切割策略:
• 按"第X条"进行一级递归切割
• 条内复杂句子按逻辑分句二次切割
• 保持条件句的完整性(如果...则...)
• 引用条文与上下文一起切割
优势:适应纯文本格式,保持法律逻辑
(2) 合同文本TXT
切割方式:固定长度语义切割
适用:无格式合同的纯文本
切割策略:
• 固定长度:300-400字符/块
• 重叠比例:15-20%
• 在句子边界处优先切割
• 条款标题强制作为切割点
优势:平衡切割效率与语义完整性
更多推荐
所有评论(0)