微信扫码
添加专属顾问
我要投稿
探索企业级RAG系统分片优化的实战方案,分享三种高效策略的实现逻辑与代码结构。核心内容:1. RAG系统中分片优化的必要性与价值2. 固定窗口、语义、命题三种分片策略的详细实现3. 完整的项目代码结构与服务设计解析
失踪人口回归,这段时间很忙,不仅是在做项目优化还得探索所谓的口播自动化编程,后面可以跟大家一起交流。
最简单有效非demo的手段
云厂商永远是你值得信赖的伙伴,花点钱可以解决的问题没必要写代码。
使用策略模式 实现了三种方式,固定窗口,语义,命题三种策略。在实际的开发过程中可以参考并延伸。
bootstrap/rag/chunking/ChunkingStrategyType.java | ||
bootstrap/rag/chunking/ChunkingStrategy.java | ||
bootstrap/rag/chunking/ChunkingStrategyFactory.java | ||
bootstrap/rag/chunking/ChunkingRequest.java | ||
bootstrap/rag/chunking/ChunkingResult.java | ||
bootstrap/rag/chunking/DocumentChunk.java | ||
bootstrap/rag/chunking/impl/SlidingWindowChunkingStrategy.java | ||
bootstrap/rag/chunking/impl/SemanticChunkingStrategy.java | ||
bootstrap/rag/chunking/impl/PropositionChunkingStrategy.java | ||
bootstrap/rag/chunking/MultimodalChunkBuilder.java | ||
bootstrap/admin/service/ChunkingService.java | ||
bootstrap/admin/service/DocumentProcessingJobService.java | ||
bootstrap/admin/controller/KnowledgeController.java |
在 RAG(Retrieval-Augmented Generation)系统中,文档分片是影响检索质量的第一道关卡。分片质量直接决定了:
最简单的做法是按固定字符数切割,但会带来严重问题:
原文:"...2024年公司营收达到50亿元。其中,|华东区域贡献了32%的份额,华|南区域贡献了28%..."
↑ 粗暴切割点 ↑
切割后,"华东区域贡献了32%的份额" 被拆成两半,检索 "华东区域营收占比" 时无法精准命中。
本项目实现了三种由粗到细的分片策略:
| SLIDING_WINDOW | |||
| SEMANTIC | |||
| PROPOSITION |
┌──────────────────────┐
│ ChunkingStrategy │ ◄── 策略接口
│──────────────────────│
│ + type() │
│ + chunk(request) │
│ + validate(config) │
│ + getDefaultConfig() │
│ + getVersion() │
└──────────┬───────────┘
│ implements
┌────────────────┼────────────────┐
│ │ │
┌──────────▼─────┐ ┌───────▼──────┐ ┌──────▼────────┐
│ SlidingWindow │ │ Semantic │ │ Proposition │
│ ChunkingStrat. │ │ ChunkingStr.│ │ ChunkingStr. │
└────────────────┘ └──────────────┘ └───────────────┘
┌──────────────────────┐
│ ChunkingStrategyFactory │ ◄── 策略工厂
│──────────────────────│
│ - strategyMap │
│ + getStrategy(type) │
│ + listStrategies() │
│ + validateConfig() │
└──────────┬───────────┘
│ used by
┌──────────▼───────────┐
│ ChunkingService │ ◄── 业务服务层
│──────────────────────│
│ + chunk() │
│ + preview() │
│ + validateConfig() │
│ + listStrategies() │
└──────────────────────┘
本模块采用 策略模式(Strategy Pattern) 定义算法族,用 工厂模式(Factory Pattern) 管理策略注册与获取。
关键设计决策:
ChunkingStrategy 实现类标注 @Component,Spring 自动注入 List,工厂在构造时自动完成注册Map config 传入,支持灵活配置strategyVersion,便于后续升级迁移文件:
com.isy.rag.bootstrap.rag.chunking.ChunkingRequest
@Data
@Builder
public class ChunkingRequest {
private String content; // 文档文本内容
private ChunkingStrategyType strategyType; // 分块策略枚举
private Map config; // 策略专属配置
// 类型安全的配置读取方法
public T getConfigValue(String key, Class type, T defaultValue) ;
}
设计亮点 — getConfigValue 方法:
该方法实现了类型安全的配置读取,支持:
这意味着前端传入的 JSON 数值(如 500 可能被 Jackson 解析为 Integer 或 Long)都能被正确处理。
文件:
com.isy.rag.bootstrap.rag.chunking.DocumentChunk
@Data
@Builder
public class DocumentChunk {
// ===== 基础字段 =====
private String content; // 分块内容
private int index; // 分块索引
private int charCount; // 字符数
private Integer tokenCount; // token数(估算)
private Integer startOffset; // 原文起始偏移
private Integer endOffset; // 原文结束偏移
private String chunkType; // TEXT / PROPOSITION / SEMANTIC_SEGMENT / IMAGE / TABLE_IMAGE
private String strategyVersion; // 策略版本
private Map metadata; // 策略专属元数据
// ===== 多模态扩展字段 =====
private List assetIds; // 关联资产ID列表
private Integer blockIndex; // 文档内顺序索引(多模态混合排序用)
private String sectionTitle; // 所属章节标题
private Integer pageNum; // 页码
private String pipelineStatus; // 管线状态
}
chunkType 枚举值说明:
TEXT | ||
SEMANTIC_SEGMENT | ||
PROPOSITION | ||
IMAGE | ||
TABLE_IMAGE |
metadata 策略专属字段:
// SEMANTIC 策略 metadata 示例
{
"breakpointType": "percentile",
"boundaryScore": 0.35, // 断点处的相似度
"mergedFromSmallChunks": true // 是否由小chunk合并而来
}
// PROPOSITION 策略 metadata 示例
{
"propositionRange": "3-7", // 命题索引范围
"propositionCount": 5 // 包含的命题数量
}
@Data
public class ChunkingResult {
private List chunks; // 分块文档列表
private ChunkingStrategyType strategyType; // 使用的策略类型
private String strategyVersion; // 策略版本
private long durationMs; // 处理耗时(毫秒)
private int totalChars; // 原始文档总字符数
private Map metadata; // 额外元数据
}
public enum ChunkingStrategyType {
SLIDING_WINDOW("SLIDING_WINDOW", "滑动窗口切分", "固定窗口+重叠区域,确保跨边界信息不丢失"),
SEMANTIC("SEMANTIC", "语义切分", "按语义边界切分,用嵌入向量相似度下降点作为切分边界"),
PROPOSITION("PROPOSITION", "命题切分", "用LLM将文档拆解为原子级命题,每个命题是独立可验证的事实陈述");
// fromCode() 方法:code 为 null 或无效时默认返回 SLIDING_WINDOW
}
文件:
com.isy.rag.bootstrap.rag.chunking.ChunkingStrategy
public interface ChunkingStrategy {
ChunkingStrategyType type(); // 策略类型标识
ChunkingResult chunk(ChunkingRequest request); // 执行分块(核心方法)
void validate(Map config) ; // 校验配置参数
Map getDefaultConfig(); // 获取默认配置
default String getVersion() { return "v1"; } // 策略版本号
}
接口设计原则:
type() 用于工厂注册和查找,每个实现类返回唯一标识validate() 在执行分块前校验参数,防止运行时异常getDefaultConfig() 让前端可以展示推荐配置,降低使用门槛getVersion() 默认返回 "v1",策略升级时覆写文件:
com.isy.rag.bootstrap.rag.chunking.ChunkingStrategyFactory
@Slf4j
@Component
public class ChunkingStrategyFactory {
private final Map strategyMap = new HashMap<>();
// Spring 自动注入所有 ChunkingStrategy 实现类
public ChunkingStrategyFactory(List strategies) {
for (ChunkingStrategy strategy : strategies) {
strategyMap.put(strategy.type(), strategy);
log.info("注册分块策略: {} - {}", strategy.type().getCode(), strategy.type().getName());
}
}
public ChunkingStrategy getStrategy(ChunkingStrategyType type) { ... }
public ChunkingStrategy getStrategy(String strategyCode) { ... }
public List
public void validateConfig(String strategyCode, Map config) { ... }
public boolean hasStrategy(ChunkingStrategyType type) { ... }
}
自动注册机制:
Spring 容器启动时,SlidingWindowChunkingStrategy、SemanticChunkingStrategy、PropositionChunkingStrategy 三者都标注了 @Component,Spring 自动收集到 List 中注入给工厂。工厂在构造函数中遍历并注册到 strategyMap。
好处:新增策略只需实现接口并加 @Component,零修改工厂代码即可自动注册。
文件:
com.isy.rag.bootstrap.admin.service.ChunkingService
@Slf4j
@Service
@RequiredArgsConstructor
public class ChunkingService {
private final ChunkingStrategyFactory chunkingStrategyFactory;
public ChunkingResult chunk(String content, String strategyCode, String configJson) {
ChunkingStrategyType strategyType = ChunkingStrategyType.fromCode(strategyCode);
ChunkingStrategy strategy = chunkingStrategyFactory.getStrategy(strategyType);
Map config = parseConfig(configJson, strategy);
strategy.validate(config); // 先校验
ChunkingRequest request = ChunkingRequest.builder()
.content(content).strategyType(strategyType).config(config).build();
return strategy.chunk(request); // 再执行
}
public ChunkingResult preview(String content, String strategyCode, String configJson) {
return chunk(content, strategyCode, configJson); // 预览与正式分块逻辑相同
}
}
调用链路:
Controller → ChunkingService.chunk() → Factory.getStrategy() → Strategy.chunk()
↓
parseConfig() (JSON → Map)
↓
Strategy.validate()
文件:
com.isy.rag.bootstrap.rag.chunking.impl.SlidingWindowChunkingStrategy
文档: [AAAAAA|BBBBBB|CCCCCC|DDDDDD|EEEEEE]
固定窗口无重叠:
Chunk 0: [AAAAAA]
Chunk 1: [BBBBBB] ← 边界处信息可能丢失
Chunk 2: [CCCCCC]
滑动窗口有重叠(overlap=2):
Chunk 0: [AAAAAA]
Chunk 1: [AABBBBBB] ← 重叠区保留跨边界信息
Chunk 2: [BBCCCCCC]
Chunk 3: [CCDDDDDD]
模式一:段落感知(paragraphAware=true,默认)
private List splitByParagraph(String content, int chunkSize, int overlap, int minChunkSize) {
String[] paragraphs = content.split("\n\n+"); // 按双换行分割段落
StringBuilder currentChunk = new StringBuilder();
for (String paragraph : paragraphs) {
// 如果加入当前段落会超过 chunkSize → 先保存当前块
if (currentChunk.length() + paragraph.length() > chunkSize && currentChunk.length() > 0) {
// 保存 & 处理重叠部分
String overlapText = currentChunk.substring(currentChunk.length() - overlap);
currentChunk = new StringBuilder(overlapText);
}
currentChunk.append(paragraph).append("\n\n");
// 超长段落强制分割
if (currentChunk.length() > chunkSize * 1.5) {
// 按 chunkSize - overlap 步长强制切分
}
}
}
关键逻辑解析:
chunkSize * 1.5 时,退化为固定大小切分minChunkSize 过滤过小的尾部碎片模式二:纯固定大小(paragraphAware=false)
private List splitByFixedSize(String content, int chunkSize, int overlap, int minChunkSize) {
int step = chunkSize - overlap; // 步长 = 窗口大小 - 重叠区
for (int i = 0; i < content.length(); i += step) {
int end = Math.min(i + chunkSize, content.length());
String chunkText = content.substring(i, end).trim();
if (chunkText.length() >= minChunkSize) {
chunks.add(buildChunk(chunkText, chunkIndex++, i, end));
}
}
}
public void validate(Map config) {
// chunkSize 范围: 100 ~ 8000
// chunkOverlap 范围: 0 ~ chunkSize/2
// 为什么 overlap 上限是 chunkSize/2?因为超过一半会导致新窗口中大部分是重复内容
}
private int estimateTokens(String text) {
return text.length() / 2; // 粗略估算:中文约1.5字/token,英文约4字符/token
}
文件:
com.isy.rag.bootstrap.rag.chunking.impl.SemanticChunkingStrategy参考:LangChain SemanticChunker
核心洞察:同一主题的句子之间语义相似度高,主题切换处相似度会突然下降。找到这些"下降点"就是切分边界。
句子序列: S1 ──S2 ──S3 ──S4 ──S5 ──S6 ──S7 ──S8
相似度: 0.92 0.88 0.85 0.31 0.90 0.87 0.28
↑ ↑
主题切换点 主题切换点
切分结果: [S1 S2 S3 S4] | [S5 S6 S7] | [S8]
步骤1:句子分割
private List splitIntoSentences(String content) {
// 中英文标点统一处理:。!?;. ! ? ;
String[] parts = content.split("(?<=[。!?;.!?;])\\s*");
// 如果分割后句子太少(≤1),退化为按换行分割
if (sentences.size() <= 1) {
sentences.clear();
String[] lines = content.split("\\n+");
}
}
步骤2:逐句生成嵌入向量
List<double[]> embeddings = new ArrayList<>();
for (String sentence : sentences) {
TextSegment segment = TextSegment.from(sentence);
Embedding embedding = embeddingModel.embed(segment).content();
float[] vector = embedding.vector();
// float[] → double[] 转换
double[] doubleVector = new double[vector.length];
for (int i = 0; i < vector.length; i++) {
doubleVector[i] = vector[i];
}
embeddings.add(doubleVector);
}
步骤3:计算相邻句子的余弦相似度
private double cosineSimilarity(double[] a, double[] b) {
double dotProduct = 0.0, normA = 0.0, normB = 0.0;
for (int i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
步骤4:根据断点阈值确定切分边界
三种断点检测算法:
private List findBreakpoints(List similarities, String type, int amount) {
double threshold;
switch (type) {
case "percentile":
// 将相似度排序,取第 amount 百分位的值作为阈值
// amount=95 表示:只有最低5%的相似度点才被识别为断点
List sorted = similarities.stream().sorted().collect(Collectors.toList());
int idx = (int) Math.ceil(amount / 100.0 * sorted.size()) - 1;
threshold = sorted.get(idx);
break;
case "standard_deviation":
// 均值 - 标准差 × 倍数
// amount=95 → 倍数=0.95,相似度显著低于均值的点为断点
double mean = similarities.stream().mapToDouble(Double::doubleValue).average().orElse(0);
double stdDev = Math.sqrt(similarities.stream()
.mapToDouble(s -> Math.pow(s - mean, 2)).average().orElse(0));
threshold = mean - stdDev * (amount / 100.0);
break;
case "interquartile":
// Q1 - IQR × 倍数
// 基于四分位距,对异常值更鲁棒
double q1 = sorted.get(sorted.size() / 4);
double q3 = sorted.get(sorted.size() * 3 / 4);
double iqr = q3 - q1;
threshold = q1 - iqr * (amount / 100.0);
break;
}
// 相似度低于阈值的点即为断点
for (int i = 0; i < similarities.size(); i++) {
if (similarities.get(i) < threshold) breakpoints.add(i);
}
}
步骤5:合并句子为块
// 先按断点合并 → 过滤小于 minChunkSize 的 → 超过 maxChunkSize 的二次切分
// 合并小于 minChunkSize 的候选块与相邻块
for (int i = 0; i < candidateTexts.size(); i++) {
String text = candidateTexts.get(i);
mergeBuffer.append(text);
if (mergeBuffer.length() >= minChunkSize || i == candidateTexts.size() - 1) {
mergedTexts.add(mergeBuffer.toString().trim());
mergeBuffer = new StringBuilder();
}
}
// 超长块二次切分(优先在句号处断开)
private List splitByMaxSize(String text, int maxSize, int minSize) {
// 尝试在句号、分号等标点处断开
int bestPunc = Math.max(
Math.max(text.lastIndexOf("。", end), text.lastIndexOf(".", end)),
text.lastIndexOf(";", end)
);
if (bestPunc > start + minSize) {
end = bestPunc + 1;
}
}
if (embeddingModel == null) {
throw new IllegalStateException(
"语义切分需要EmbeddingModel,但当前未配置。请选择SLIDING_WINDOW策略或配置Embedding模型。"
);
}
语义切分必须依赖 Embedding 模型,没有配置时直接抛异常(而非降级),因为用户显式选择了语义切分,静默降级可能导致不符合预期的结果。
文件:
com.isy.rag.bootstrap.rag.chunking.impl.PropositionChunkingStrategy参考:NirDiamant/RAG_Techniques/proposition_chunking
将文档拆解为原子级命题(Atomic Proposition)——每个命题是一个独立可验证的事实陈述,然后将命题重新组装为适合检索的块。
原文: "GPT-4于2023年3月发布,参数量约为1.8万亿,训练成本超过1亿美元。"
提取命题:
- "GPT-4于2023年3月发布"
- "GPT-4的参数量约为1.8万亿"
- "GPT-4的训练成本超过1亿美元"
重组为块:
Chunk 0: "GPT-4于2023年3月发布\nGPT-4的参数量约为1.8万亿\nGPT-4的训练成本超过1亿美元"
为什么比直接切分更好?
步骤1:文档分批
private List splitIntoBatches(String content, int maxChars) {
String[] paragraphs = content.split("\n\n+");
// 按段落边界分批,每批不超过 maxInputCharsPerBatch(默认3000字符)
// 超长段落强制按 maxChars 切分
}
为什么要分批?因为 LLM 有输入长度限制,直接把整篇文档丢给 LLM 会导致截断或超时。
步骤2:LLM 提取命题
private List extractPropositions(String text, int maxProps) {
String systemPrompt = "You are an expert at decomposing text into atomic propositions.\n"
+ "Rules:\n"
+ "1. Each proposition must be a complete, standalone fact.\n"
+ "2. Preserve specific details: numbers, dates, names, quantities.\n"
+ "3. Do not add information not present in the source text.\n"
+ "4. Output one proposition per line, prefixed with \"- \".\n"
+ "5. If the text contains no verifiable facts, return an empty response.";
// 调用 ChatModel
ChatResponse chatResponse = chatModel.chat(chatRequest);
String responseText = chatResponse.aiMessage().text();
// 解析 LLM 输出
for (String line : lines) {
// 去除 "- " 或 "1. " 前缀
if (line.startsWith("- ")) {
line = line.substring(2).trim();
} else if (line.matches("^\\d+\\.\\s+.*")) {
line = line.replaceFirst("^\\d+\\.\\s+", "").trim();
}
if (!line.isEmpty() && line.length() > 5) {
propositions.add(line);
}
}
}
Prompt 设计要点:
步骤3:命题重组
private List reassemblePropositions(List propositions, int maxChunkSize, int minChunkSize) {
// 按顺序合并命题直到达到 maxChunkSize
for (int i = 0; i < propositions.size(); i++) {
if (currentChunk.length() + prop.length() + 2 > maxChunkSize && currentChunk.length() > 0) {
// 保存当前块,记录 propositionRange 和 propositionCount
meta.put("propositionRange", propStartIndex + "-" + (i - 1));
meta.put("propositionCount", i - propStartIndex);
}
currentChunk.append(prop);
}
}
为什么需要重组? 单个命题可能只有十几个字,直接作为 chunk 向量化后信息量不足。重组为包含多个相关命题的块,既有原子性又有足够上下文。
private ChunkingResult fallbackChunk(ChunkingRequest request) {
log.warn("[KB检索] 命题切分使用兜底策略: SLIDING_WINDOW");
SlidingWindowChunkingStrategy fallback = new SlidingWindowChunkingStrategy();
// 使用同样的 maxChunkSize 作为 chunkSize
ChunkingResult result = fallback.chunk(fallbackRequest);
result.setMetadata(Map.of(
"fallbackFrom", "PROPOSITION",
"fallbackReason", "ChatModel不可用或产出为空"
));
return result;
}
降级触发条件:
chatModel == null(未配置 LLM)降级时会在 metadata 中标记 fallbackFrom 和 fallbackReason,便于后续排查。
文件:
com.isy.rag.bootstrap.rag.chunking.MultimodalChunkBuilder
将图片分析结果(VLM 描述 + OCR 文本)生成 IMAGE / TABLE_IMAGE 类型的 chunk,并与文本 chunk 合并排序。
对于每个 DocumentAssetDO:
if (analysisStatus == 2) { // VLM 分析完成
→ 生成 IMAGE 或 TABLE_IMAGE chunk
→ content 使用 embeddingText(用于向量化)
→ metadata 包含: asset_id, page_num, image_type, keywords 等
}
else if (analysisStatus == 3 // VLM 失败
&& ocrText 不为空) { // 但有 OCR 结果
→ 降级生成简短 IMAGE chunk
→ content = "[图片]" + ocrText
→ metadata 标记 degraded=true, degrade_reason="VLM_FAILED_WITH_OCR"
}
else {
→ 不生成 chunk(跳过)
}
public List mergeAndSortChunks(List textChunks, List imageChunks) {
// 1. 合并
List allChunks = new ArrayList<>();
allChunks.addAll(textChunks);
allChunks.addAll(imageChunks);
// 2. 为没有 blockIndex 的文本 chunk 分配估算的 blockIndex
int maxBlockIndex = allChunks.stream()
.filter(c -> c.getBlockIndex() != null)
.mapToInt(DocumentChunk::getBlockIndex)
.max().orElse(-1);
for (DocumentChunk chunk : textChunks) {
if (chunk.getBlockIndex() == null) {
maxBlockIndex++;
chunk.setBlockIndex(maxBlockIndex);
}
}
// 3. 按 blockIndex 排序
allChunks.sort(Comparator.comparingInt(c -> c.getBlockIndex() != null ? c.getBlockIndex() : Integer.MAX_VALUE));
// 4. 重新分配全局递增的 chunk index
for (int i = 0; i < allChunks.size(); i++) {
allChunks.get(i).setIndex(i);
}
return allChunks;
}
blockIndex 的作用:图片 chunk 来自文档解析阶段,携带了解析时的 blockIndex(文档中的物理顺序),文本 chunk 没有这个信息。混合排序时,图片按原始位置插入,文本按顺序分配 blockIndex,确保最终顺序与原文阅读顺序一致。
// PRIMARY 关系:一个 IMAGE chunk 与其关联的资产
public void savePrimaryRelation(Long chunkId, Long assetId) {
ChunkAssetRelDO rel = new ChunkAssetRelDO();
rel.setChunkId(chunkId);
rel.setAssetId(assetId);
rel.setRelationType("PRIMARY");
chunkAssetRelMapper.insert(rel);
}
// 重建时清理
public void cleanupChunksAndRelations(Long docId) {
chunkAssetRelMapper.softDeleteByDocId(docId, System.currentTimeMillis());
}
文件:
com.isy.rag.bootstrap.admin.service.DocumentProcessingJobService
上传文件 → 解析(PARSING) → 资产提取(EXTRACTING_ASSETS) → 图片分析(ANALYZING_IMAGES)
→ 分块(CHUNKING) → 向量化(EMBEDDING) → 索引(INDEXING) → 完成(DONE)
// ---- 阶段4: 分块 (小事务) ----
if (recoveryService.shouldRun(resumeStage, "CHUNKING")) {
// 1. 先清理旧分块和关系
multimodalChunkBuilder.cleanupChunksAndRelations(docId);
chunkMapper.deleteByDocId(docId);
// 2. 文本分块(调用 ChunkingService)
ChunkingResult chunkingResult = chunkingService.chunk(content, strategyCode, configJson);
List textChunks = chunkingResult.getChunks();
// 3. 图片分块(调用 MultimodalChunkBuilder)
List imageChunks = new ArrayList<>();
if (multimodalEnabled) {
List analyzedAssets = assetMapper.selectByDocId(docId);
imageChunks = multimodalChunkBuilder.buildImageChunks(analyzedAssets);
}
// 4. 合并排序(按 blockIndex 混合排序)
List allChunks = multimodalChunkBuilder.mergeAndSortChunks(textChunks, imageChunks);
// 5. 保存到数据库(小事务)
chunkDOs = saveChunksInTransaction(docId, document.getKbId(), allChunks);
}
// 确定分块策略和配置(文档级 > 知识库级)
String strategyCode = StrUtil.isNotBlank(document.getChunkStrategy())
? document.getChunkStrategy() : kb.getChunkStrategy();
String configJson = StrUtil.isNotBlank(document.getChunkConfigJson())
? document.getChunkConfigJson() : kb.getChunkConfigJson();
优先使用文档级配置,如果文档没有指定则使用知识库级配置。这允许同一知识库下的不同文档使用不同策略。
管线通过 ProcessRecoveryService 实现断点恢复:
ProcessRecoveryService.ResumeStage resumeStage = recoveryService.resolveResumeStage(docId);
if ("DONE".equals(resumeStage.getStartStage())) {
return; // 已完成,跳过
}
// 每个阶段执行前检查是否需要运行
if (recoveryService.shouldRun(resumeStage, "CHUNKING")) { ... }
如果处理在分块阶段中断,重启后会从分块阶段继续,无需重新解析。
GET /api/admin/kb/chunking-strategies
响应示例:
{
"code": "00000",
"data": [
{
"type": "SLIDING_WINDOW",
"name": "滑动窗口切分",
"description": "固定窗口+重叠区域,确保跨边界信息不丢失",
"version": "v1",
"defaultConfig": {
"chunkSize": 500,
"chunkOverlap": 50,
"paragraphAware": true,
"minChunkSize": 80
}
},
{
"type": "SEMANTIC",
"name": "语义切分",
"description": "按语义边界切分,用嵌入向量相似度下降点作为切分边界",
"version": "v1",
"defaultConfig": {
"breakpointThresholdType": "percentile",
"breakpointThresholdAmount": 95,
"minChunkSize": 120,
"maxChunkSize": 900
}
},
{
"type": "PROPOSITION",
"name": "命题切分",
"description": "用LLM将文档拆解为原子级命题,每个命题是独立可验证的事实陈述",
"version": "v1",
"defaultConfig": {
"maxInputCharsPerBatch": 3000,
"maxPropositionsPerBatch": 50,
"fallbackStrategy": "SLIDING_WINDOW",
"maxChunkSize": 500,
"minChunkSize": 50
}
}
]
}
POST /api/admin/kb/{kbId}/chunk-preview
Content-Type: application/json
{
"text": "这是需要预览分块的文本内容...",
"chunkStrategy": "SLIDING_WINDOW",
"chunkConfig": {
"chunkSize": 200,
"chunkOverlap": 50
}
}
响应示例:
{
"code": "00000",
"data": {
"strategyType": "SLIDING_WINDOW",
"strategyVersion": "v1",
"totalChunks": 5,
"totalChars": 1000,
"durationMs": 12,
"chunks": [
{
"index": 0,
"content": "这是需要预览分块的文本内容...",
"charCount": 200,
"tokenCount": 100,
"chunkType": "TEXT",
"strategyVersion": "v1",
"metadata": null
}
]
}
}
POST /api/admin/kb/save
Content-Type: application/json
{
"name": "我的知识库",
"description": "使用语义切分",
"chunkStrategy": "semantic",
"chunkConfigJson": "{\"breakpointThresholdType\":\"percentile\",\"breakpointThresholdAmount\":90}"
}
POST /api/admin/kb/{kbId}/upload
Content-Type: multipart/form-data
file: <文件>
chunkStrategy: proposition
chunkConfig: {"maxChunkSize": 600}
POST /api/admin/kb/document/{docId}/rebuild-chunks
GET /api/admin/kb/document/{docId}/progress
响应示例:
{
"docId": 123,
"parseStatus": 2,
"vectorStatus": 1,
"processStage": "DONE",
"processProgress": 100,
"chunkCount": 15
}
chunkSize | ||||
chunkOverlap | ||||
paragraphAware | ||||
minChunkSize |
breakpointThresholdType | ||||
breakpointThresholdAmount | ||||
minChunkSize | ||||
maxChunkSize |
breakpointThresholdAmount 含义:
maxInputCharsPerBatch | ||||
maxPropositionsPerBatch | ||||
fallbackStrategy | ||||
maxChunkSize | ||||
minChunkSize |
只需 3 步 即可添加新的分片策略:
// ChunkingStrategyType.java
public enum ChunkingStrategyType {
SLIDING_WINDOW(...),
SEMANTIC(...),
PROPOSITION(...),
MY_NEW_STRATEGY("MY_NEW_STRATEGY", "新策略名称", "新策略描述"); // 新增
...
}
@Slf4j
@Component // 关键:标注 @Component 让 Spring 自动注册
public class MyNewChunkingStrategy implements ChunkingStrategy {
@Override
public ChunkingStrategyType type() {
return ChunkingStrategyType.MY_NEW_STRATEGY;
}
@Override
public ChunkingResult chunk(ChunkingRequest request) {
long startTime = System.currentTimeMillis();
String content = request.getContent();
// 1. 读取配置(使用 getConfigValue 安全获取)
int myParam = request.getConfigValue("myParam", Integer.class, 100);
// 2. 实现你的分块算法
List chunks = myChunkingAlgorithm(content, myParam);
// 3. 返回结果
long durationMs = System.currentTimeMillis() - startTime;
return ChunkingResult.of(chunks, type(), getVersion(), durationMs, content.length());
}
@Override
public void validate(Map config) {
// 校验配置参数
}
@Override
public Map getDefaultConfig() {
Map config = new LinkedHashMap<>();
config.put("myParam", 100);
return config;
}
}
无需修改工厂、服务层或控制器——Spring 自动注入 + 工厂自动注册,新策略立即可用。
验证方式:
调用 GET /api/admin/kb/chunking-strategies,新策略应该出现在列表中。
开始
│
├─ 是否有 Embedding 模型?
│ ├─ 否 → SLIDING_WINDOW(唯一选择)
│ └─ 是 ↓
│
├─ 是否有 ChatModel (LLM)?
│ ├─ 否 ↓
│ │ ├─ 文档语义结构是否丰富(多主题切换)?
│ │ │ ├─ 是 → SEMANTIC
│ │ │ └─ 否 → SLIDING_WINDOW(更简单快速)
│ └─ 是 ↓
│ ├─ 是否需要原子级事实检索?
│ │ ├─ 是 → PROPOSITION
│ │ └─ 否 ↓
│ │ ├─ 文档语义结构丰富?
│ │ │ ├─ 是 → SEMANTIC
│ │ │ └─ 否 → SLIDING_WINDOW
| 处理速度 | |||
| 外部依赖 | |||
| Token 消耗 | |||
| 语义完整性 | |||
| 可解释性 | |||
| 推荐文档长度 |
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2026-06-10
Vector Graph RAG 开源!一套向量数据库同时搞定语义检索+RAG多跳
2026-06-10
企业 RAG 知识库落地,应如何设计实现?
2026-06-10
知识库分层编排:从 RAG 到 Agent-native Knowledge Context Layer
2026-06-10
RAG 优化 20 法:从"搜得到"到"答得好"
2026-06-10
企业 RAG 知识库落地,真正难的不是调用大模型
2026-06-10
RAG 2.0 落地实战:从「检索增强」到「知识推理」的工程跃迁
2026-06-10
别再傻傻分块了:这个开源引擎让RAG准确率飙升260%
2026-06-09
为什么你的知识库回答不了"张三和B公司什么关系"
2026-03-23
2026-04-06
2026-03-18
2026-03-20
2026-04-27
2026-03-31
2026-04-02
2026-03-21
2026-03-17
2026-04-23
2026-06-10
2026-05-20
2026-05-18
2026-05-11
2026-05-07
2026-05-06
2026-04-27
2026-04-21