企业级智能体系统 RAG的分片优化逻辑

失踪人口回归,这段时间很忙,不仅是在做项目优化还得探索所谓的口播自动化编程,后面可以跟大家一起交流。

分片优化

最简单有效非demo的手段

企业级智能体系统 RAG的分片优化逻辑

云厂商永远是你值得信赖的伙伴,花点钱可以解决的问题没必要写代码。

本期优化

使用策略模式 实现了三种方式,固定窗口,语义,命题三种策略。在实际的开发过程中可以参考并延伸。

| 文件 | 路径 | 说明 |
| — | — | — |
| ChunkingStrategyType | bootstrap/rag/chunking/ChunkingStrategyType.java | 策略类型枚举 |
| ChunkingStrategy | bootstrap/rag/chunking/ChunkingStrategy.java | 策略接口 |
| ChunkingStrategyFactory | bootstrap/rag/chunking/ChunkingStrategyFactory.java | 策略工厂 |
| ChunkingRequest | bootstrap/rag/chunking/ChunkingRequest.java | 分块请求 |
| ChunkingResult | bootstrap/rag/chunking/ChunkingResult.java | 分块结果 |
| DocumentChunk | bootstrap/rag/chunking/DocumentChunk.java | 文档分块 |
| SlidingWindowChunkingStrategy | bootstrap/rag/chunking/impl/SlidingWindowChunkingStrategy.java | 滑动窗口实现 |
| SemanticChunkingStrategy | bootstrap/rag/chunking/impl/SemanticChunkingStrategy.java | 语义切分实现 |
| PropositionChunkingStrategy | bootstrap/rag/chunking/impl/PropositionChunkingStrategy.java | 命题切分实现 |
| MultimodalChunkBuilder | bootstrap/rag/chunking/MultimodalChunkBuilder.java | 多模态分块构建 |
| ChunkingService | bootstrap/admin/service/ChunkingService.java | 分块业务服务 |
| DocumentProcessingJobService | bootstrap/admin/service/DocumentProcessingJobService.java | 文档处理管线 |
| KnowledgeController | bootstrap/admin/controller/KnowledgeController.java | API 控制器 |
* *
为什么需要分片优化

在 RAG(Retrieval-Augmented Generation)系统中,文档分片是影响检索质量的第一道关卡。分片质量直接决定了:
召回率:分片太粗,关键信息被淹没在噪音中;分片太细,上下文丢失
精确度:分片边界不当,跨边界的信息在检索时可能遗漏
语义完整性:理想情况下,每个分片应该是一个语义自洽的单元

传统固定切分的问题

最简单的做法是按固定字符数切割,但会带来严重问题:

原文:"…2024年公司营收达到50亿元。其中,|华东区域贡献了32%的份额,华|南区域贡献了28%…"                                              ↑ 粗暴切割点           ↑
切割后,"华东区域贡献了32%的份额" 被拆成两半,检索 "华东区域营收占比" 时无法精准命中。

本项目的优化思路

本项目实现了三种由粗到细的分片策略:

| 策略 | 核心思想 | 适用场景 | 依赖 |
| — | — | — | — |
| SLIDING_WINDOW | 固定窗口 + 重叠区域 + 段落感知 | 通用场景,无需额外依赖 | 无 |
| SEMANTIC | 嵌入向量相似度下降点作为切分边界 | 语义结构丰富的文档 | EmbeddingModel |
| PROPOSITION | LLM 拆解为原子级命题再重组 | 需要精细事实检索的场景 | ChatModel |
* *
整体架构设计

2.1 类图总览

┌──────────────────────┐                    │   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()   │                    └──────────────────────┘

2.2 设计模式:策略模式 + 工厂模式

本模块采用策略模式(Strategy Pattern)定义算法族,用工厂模式(Factory Pattern)管理策略注册与获取。

关键设计决策:
Spring 自动注册:所有ChunkingStrategy实现类标注@Component,Spring 自动注入List,工厂在构造时自动完成注册
配置驱动:每种策略的参数通过Map传入,支持灵活配置
策略降级:PROPOSITION 策略在 ChatModel 不可用时自动降级为 SLIDING_WINDOW
版本追踪:每个策略有版本号,生成的 chunk 携带strategyVersion,便于后续升级迁移
* *
核心数据模型

3.1 ChunkingRequest — 分块请求

文件:com.isy.rag.bootstrap.rag.chunking.ChunkingRequest

@Data@Builderpublic class ChunkingRequest {    private String content;                    // 文档文本内容    private ChunkingStrategyType strategyType; // 分块策略枚举    private Map// 策略专属配置    // 类型安全的配置读取方法    public T getConfigValue(String key, Class;}
设计亮点 —getConfigValue方法:

该方法实现了类型安全的配置读取,支持:
自动 Number 子类转换(Integer/Long/Double/Float)
String 类型自动转换
Boolean 类型智能解析
默认值兜底

这意味着前端传入的 JSON 数值(如500可能被 Jackson 解析为Integer或Long)都能被正确处理。

3.2 DocumentChunk — 文档分块

文件:com.isy.rag.bootstrap.rag.chunking.DocumentChunk

@Data@Builderpublic 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 / SEMANTICSEGMENT / IMAGE / TABLEIMAGE    private String strategyVersion;   // 策略版本    private Map// 策略专属元数据    // ===== 多模态扩展字段 =====    private List// 关联资产ID列表    private Integer blockIndex;       // 文档内顺序索引(多模态混合排序用)    private String sectionTitle;      // 所属章节标题    private Integer pageNum;          // 页码    private String pipelineStatus;    // 管线状态}
chunkType枚举值说明:

| chunkType | 来源策略 | 说明 |
| — | — | — |
| TEXT | SLIDING_WINDOW | 普通文本分块 |
| SEMANTIC_SEGMENT | SEMANTIC | 语义段落分块 |
| PROPOSITION | PROPOSITION | 命题分块 |
| IMAGE | MultimodalChunkBuilder | 图片描述分块 |
| TABLE_IMAGE | MultimodalChunkBuilder | 表格/图表描述分块 |

metadata策略专属字段:

// SEMANTIC 策略 metadata 示例{    "breakpointType": "percentile",    "boundaryScore": 0.35,        // 断点处的相似度    "mergedFromSmallChunks": true  // 是否由小chunk合并而来}// PROPOSITION 策略 metadata 示例{    "propositionRange": "3-7",    // 命题索引范围    "propositionCount": 5         // 包含的命题数量}

3.3 ChunkingResult — 分块结果

@Datapublic class ChunkingResult {    private List// 分块文档列表    private ChunkingStrategyType strategyType; // 使用的策略类型    private String strategyVersion;           // 策略版本    private long durationMs;                  // 处理耗时(毫秒)    private int totalChars;                   // 原始文档总字符数    private Map// 额外元数据}

3.4 ChunkingStrategyType — 策略类型枚举

public enum ChunkingStrategyType {    SLIDINGWINDOW("SLIDINGWINDOW", "滑动窗口切分", "固定窗口+重叠区域,确保跨边界信息不丢失"),    SEMANTIC("SEMANTIC", "语义切分", "按语义边界切分,用嵌入向量相似度下降点作为切分边界"),    PROPOSITION("PROPOSITION", "命题切分", "用LLM将文档拆解为原子级命题,每个命题是独立可验证的事实陈述");    // fromCode() 方法:code 为 null 或无效时默认返回 SLIDING_WINDOW}
* *
策略模式详解

4.1 ChunkingStrategy 接口

文件:com.isy.rag.bootstrap.rag.chunking.ChunkingStrategy

public interface ChunkingStrategy {    ChunkingStrategyType type();                           // 策略类型标识    ChunkingResult chunk(ChunkingRequest request);         // 执行分块(核心方法)    void validate(Map;             // 校验配置参数    MapgetDefaultConfig();                // 获取默认配置    default String getVersion() { return "v1"; }           // 策略版本号}
接口设计原则:
type()用于工厂注册和查找,每个实现类返回唯一标识
validate()在执行分块前校验参数,防止运行时异常
getDefaultConfig()让前端可以展示推荐配置,降低使用门槛
getVersion()默认返回"v1",策略升级时覆写

4.2 ChunkingStrategyFactory — 策略工厂

文件:com.isy.rag.bootstrap.rag.chunking.ChunkingStrategyFactory

@Slf4j@Componentpublic class ChunkingStrategyFactory {    private final Mapnew HashMap<>();    // Spring 自动注入所有 ChunkingStrategy 实现类    public ChunkingStrategyFactory(List {        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 { … }    public boolean hasStrategy(ChunkingStrategyType type) { … }}
自动注册机制:

Spring 容器启动时,SlidingWindowChunkingStrategy、SemanticChunkingStrategy、PropositionChunkingStrategy三者都标注了@Component,Spring 自动收集到List中注入给工厂。工厂在构造函数中遍历并注册到strategyMap。

好处:新增策略只需实现接口并加@Component,零修改工厂代码即可自动注册。

4.3 ChunkingService — 业务服务层

文件:com.isy.rag.bootstrap.admin.service.ChunkingService

@Slf4j@Service@RequiredArgsConstructorpublic 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        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()
* *
三种分片策略深度剖析

5.1 滑动窗口切分(SLIDING_WINDOW)

文件:com.isy.rag.bootstrap.rag.chunking.impl.SlidingWindowChunkingStrategy

5.1.1 核心思想

文档: [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]

5.1.2 两种子模式

模式一:段落感知(paragraphAware=true,默认)

private ListsplitByParagraph(String content, int chunkSize, int overlap, int minChunkSize) {    String[] paragraphs = content.split("nn+");  // 按双换行分割段落    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("nn");        // 超长段落强制分割        if (currentChunk.length() > chunkSize * 1.5) {            // 按 chunkSize – overlap 步长强制切分        }    }}
关键逻辑解析:
段落边界优先:只在段落之间切分,不把一个段落从中间切断
超长段落兜底:当单个段落超过chunkSize * 1.5时,退化为固定大小切分
最小块过滤:minChunkSize过滤过小的尾部碎片

模式二:纯固定大小(paragraphAware=false)

private ListsplitByFixedSize(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));        }    }}

5.1.3 参数校验规则

public void validate(Map {    // chunkSize 范围: 100 ~ 8000    // chunkOverlap 范围: 0 ~ chunkSize/2    // 为什么 overlap 上限是 chunkSize/2?因为超过一半会导致新窗口中大部分是重复内容}

5.1.4 Token 估算

private int estimateTokens(String text) {    return text.length() / 2;  // 粗略估算:中文约1.5字/token,英文约4字符/token}

5.1.5 适用场景
通用文档:技术文档、合同、报告等结构较规则的文档
快速处理:不依赖任何外部模型,处理速度最快
资源受限:没有 Embedding 模型或 LLM 时的唯一选择
* *

5.2 语义切分(SEMANTIC)

文件:com.isy.rag.bootstrap.rag.chunking.impl.SemanticChunkingStrategy参考:LangChain SemanticChunker

5.2.1 核心思想

核心洞察:同一主题的句子之间语义相似度高,主题切换处相似度会突然下降。找到这些"下降点"就是切分边界。

句子序列:  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]

5.2.2 算法五步骤

步骤1:句子分割

private ListsplitIntoSentences(String content) {    // 中英文标点统一处理:。!?;. ! ? ;    String[] parts = content.split("(?);    // 如果分割后句子太少(≤1),退化为按换行分割    if (sentences.size()
步骤2:逐句生成嵌入向量

List
步骤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 ListfindBreakpoints(Listint amount) {    double threshold;    switch (type) {        case "percentile":            // 将相似度排序,取第 amount 百分位的值作为阈值            // amount=95 表示:只有最低5%的相似度点才被识别为断点            List            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 ListsplitByMaxSize(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;    }}

5.2.3 前置条件

if (embeddingModel == null) {    throw new IllegalStateException(        "语义切分需要EmbeddingModel,但当前未配置。请选择SLIDING_WINDOW策略或配置Embedding模型。"    );}
语义切分必须依赖 Embedding 模型,没有配置时直接抛异常(而非降级),因为用户显式选择了语义切分,静默降级可能导致不符合预期的结果。

5.2.4 适用场景
长文档:论文、技术白皮书等主题切换频繁的文档
语义结构丰富:不同章节讨论不同话题的文档
检索精度要求高:需要按语义边界精准切分的场景
* *

5.3 命题切分(PROPOSITION)

文件:com.isy.rag.bootstrap.rag.chunking.impl.PropositionChunkingStrategy参考:NirDiamant/RAGTechniques/propositionchunking

5.3.1 核心思想

将文档拆解为原子级命题(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.8万亿" 和 "训练成本" 之间切断
命题切分确保每个事实独立完整,检索时不会遗漏半句话

5.3.2 算法三步骤

步骤1:文档分批

private ListsplitIntoBatches(String content, int maxChars) {    String[] paragraphs = content.split("nn+");    // 按段落边界分批,每批不超过 maxInputCharsPerBatch(默认3000字符)    // 超长段落强制按 maxChars 切分}
为什么要分批?因为 LLM 有输入长度限制,直接把整篇文档丢给 LLM 会导致截断或超时。

步骤2:LLM 提取命题

private ListextractPropositions(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 设计要点:
Rule 1:确保每个命题是完整独立的
Rule 2:保留数字、日期、名称等关键细节
Rule 3:禁止编造(避免幻觉)
Rule 5:无事实内容时返回空(而非强行拆解)

步骤3:命题重组

private ListreassemblePropositions(Listint 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 向量化后信息量不足。重组为包含多个相关命题的块,既有原子性又有足够上下文。

5.3.3 兜底策略

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)
LLM 提取产出为空(所有批次都失败或无事实内容)

降级时会在metadata中标记fallbackFrom和fallbackReason,便于后续排查。

5.3.4 适用场景
事实密集型文档:法律条文、规章制度、产品规格等
精细检索需求:需要精确命中某个具体事实
高质量 Embedding 可用:命题较短,需要 Embedding 模型能捕捉细粒度语义
* *
多模态分块构建

文件:com.isy.rag.bootstrap.rag.chunking.MultimodalChunkBuilder

6.1 职责

将图片分析结果(VLM 描述 + OCR 文本)生成IMAGE/TABLE_IMAGE类型的 chunk,并与文本 chunk 合并排序。

6.2 图片 Chunk 生成逻辑

对于每个 DocumentAssetDO:  if (analysisStatus == 2) {          // VLM 分析完成      → 生成 IMAGE 或 TABLEIMAGE chunk      → content 使用 embeddingText(用于向量化)      → metadata 包含: assetid, pagenum, imagetype, keywords 等  }  else if (analysisStatus == 3        // VLM 失败           && ocrText 不为空) {        // 但有 OCR 结果      → 降级生成简短 IMAGE chunk      → content = "[图片]" + ocrText      → metadata 标记 degraded=true, degradereason="VLMFAILEDWITHOCR"  }  else {      → 不生成 chunk(跳过)  }

6.3 文本与图片混合排序

public ListmergeAndSortChunks(List {    // 1. 合并    Listnew 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,确保最终顺序与原文阅读顺序一致。

6.4 Chunk-Asset 关系管理

// 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

7.1 完整处理管线

上传文件 → 解析(PARSING) → 资产提取(EXTRACTINGASSETS) → 图片分析(ANALYZINGIMAGES)         → 分块(CHUNKING) → 向量化(EMBEDDING) → 索引(INDEXING) → 完成(DONE)

7.2 分块阶段的集成代码

// —- 阶段4: 分块 (小事务) —-if (recoveryService.shouldRun(resumeStage, "CHUNKING")) {    // 1. 先清理旧分块和关系    multimodalChunkBuilder.cleanupChunksAndRelations(docId);    chunkMapper.deleteByDocId(docId);    // 2. 文本分块(调用 ChunkingService)    ChunkingResult chunkingResult = chunkingService.chunk(content, strategyCode, configJson);    List    // 3. 图片分块(调用 MultimodalChunkBuilder)    Listnew ArrayList<>();    if (multimodalEnabled) {        List        imageChunks = multimodalChunkBuilder.buildImageChunks(analyzedAssets);    }    // 4. 合并排序(按 blockIndex 混合排序)    List    // 5. 保存到数据库(小事务)    chunkDOs = saveChunksInTransaction(docId, document.getKbId(), allChunks);}

7.3 策略优先级

// 确定分块策略和配置(文档级 > 知识库级)String strategyCode = StrUtil.isNotBlank(document.getChunkStrategy())        ? document.getChunkStrategy() : kb.getChunkStrategy();String configJson = StrUtil.isNotBlank(document.getChunkConfigJson())        ? document.getChunkConfigJson() : kb.getChunkConfigJson();
优先使用文档级配置,如果文档没有指定则使用知识库级配置。这允许同一知识库下的不同文档使用不同策略。

7.4 断点恢复机制

管线通过ProcessRecoveryService实现断点恢复:

ProcessRecoveryService.ResumeStage resumeStage = recoveryService.resolveResumeStage(docId);if ("DONE".equals(resumeStage.getStartStage())) {    return;  // 已完成,跳过}// 每个阶段执行前检查是否需要运行if (recoveryService.shouldRun(resumeStage, "CHUNKING")) { … }
如果处理在分块阶段中断,重启后会从分块阶段继续,无需重新解析。
* *
API 接口使用指南

8.1 获取所有分块策略

GET /api/admin/kb/chunking-strategies
响应示例:

{  "code": "00000",  "data": [    {      "type": "SLIDINGWINDOW",      "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": "SLIDINGWINDOW",        "maxChunkSize": 500,        "minChunkSize": 50      }    }  ]}

8.2 分块预览

POST /api/admin/kb/{kbId}/chunk-previewContent-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      }    ]  }}

8.3 创建知识库时指定策略

POST /api/admin/kb/saveContent-Type: application/json{  "name": "我的知识库",  "description": "使用语义切分",  "chunkStrategy": "semantic",  "chunkConfigJson": "{"breakpointThresholdType":"percentile","breakpointThresholdAmount":90}"}

8.4 上传文档时覆盖策略

POST /api/admin/kb/{kbId}/uploadContent-Type: multipart/form-datafile: chunkStrategy: propositionchunkConfig: {"maxChunkSize": 600}

8.5 重建文档分块

POST /api/admin/kb/document/{docId}/rebuild-chunks

8.6 获取文档处理进度

GET /api/admin/kb/document/{docId}/progress
响应示例:

{  "docId": 123,  "parseStatus": 2,  "vectorStatus": 1,  "processStage": "DONE",  "processProgress": 100,  "chunkCount": 15}
* *
配置参数速查表

9.1 SLIDING_WINDOW 配置

| 参数 | 类型 | 默认值 | 范围 | 说明 |
| — | — | — | — | — |
| chunkSize | Integer | 500 | 100~8000 | 每个分块的目标字符数 |
| chunkOverlap | Integer | 50 | 0~chunkSize/2 | 重叠区字符数 |
| paragraphAware | Boolean | true | true/false | 是否感知段落边界 |
| minChunkSize | Integer | 80 | > 0 | 最小分块字符数,过小的碎片被过滤 |

9.2 SEMANTIC 配置

| 参数 | 类型 | 默认值 | 范围 | 说明 |
| — | — | — | — | — |
| breakpointThresholdType | String | "percentile" | percentile / standard_deviation / interquartile | 断点检测算法 |
| breakpointThresholdAmount | Integer | 95 | > 0 | 断点阈值参数(含义随类型变化) |
| minChunkSize | Integer | 120 | > 0 | 最小分块字符数 |
| maxChunkSize | Integer | 900 | > minChunkSize | 最大分块字符数(超长块二次切分) |

breakpointThresholdAmount含义:

| 算法 | amount=95 的含义 |
| — | — |
| percentile | 取相似度分布的第95百分位作为阈值,只有最低5%的点是断点 |
| standard_deviation | 阈值 = 均值 – 0.95 × 标准差 |
| interquartile | 阈值 = Q1 – IQR × 0.95 |

9.3 PROPOSITION 配置

| 参数 | 类型 | 默认值 | 范围 | 说明 |
| — | — | — | — | — |
| maxInputCharsPerBatch | Integer | 3000 | 500~10000 | 每批输入LLM的字符数 |
| maxPropositionsPerBatch | Integer | 50 | > 0 | 每批最大命题数 |
| fallbackStrategy | String | "SLIDING_WINDOW" | – | 兜底策略名称 |
| maxChunkSize | Integer | 500 | > 0 | 命题重组后的最大块字符数 |
| minChunkSize | Integer | 50 | > 0 | 命题重组后的最小块字符数 |
* *
如何扩展新的分片策略

只需3 步即可添加新的分片策略:

步骤1:在枚举中注册类型

// ChunkingStrategyType.javapublic enum ChunkingStrategyType {    SLIDINGWINDOW(…),    SEMANTIC(…),    PROPOSITION(…),    MYNEWSTRATEGY("MYNEW_STRATEGY", "新策略名称", "新策略描述");  // 新增    …}

步骤2:实现策略接口

@Slf4j@Component  // 关键:标注 @Component 让 Spring 自动注册public class MyNewChunkingStrategy implements ChunkingStrategy {    @Override    public ChunkingStrategyType type() {        return ChunkingStrategyType.MYNEWSTRATEGY;    }    @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        // 3. 返回结果        long durationMs = System.currentTimeMillis() – startTime;        return ChunkingResult.of(chunks, type(), getVersion(), durationMs, content.length());    }    @Override    public void validate(Map {        // 校验配置参数    }    @Override    public MapgetDefaultConfig() {        Mapnew LinkedHashMap<>();        config.put("myParam", 100);        return config;    }}

步骤3:完成!

无需修改工厂、服务层或控制器——Spring 自动注入 + 工厂自动注册,新策略立即可用。

验证方式:

调用GET /api/admin/kb/chunking-strategies,新策略应该出现在列表中。
* *
策略选型建议

11.1 决策流程图

开始 │ ├─ 是否有 Embedding 模型? │   ├─ 否 → SLIDINGWINDOW(唯一选择) │   └─ 是 ↓ │ ├─ 是否有 ChatModel (LLM)? │   ├─ 否 ↓ │   │   ├─ 文档语义结构是否丰富(多主题切换)? │   │   │   ├─ 是 → SEMANTIC │   │   │   └─ 否 → SLIDINGWINDOW(更简单快速) │   └─ 是 ↓ │       ├─ 是否需要原子级事实检索? │       │   ├─ 是 → PROPOSITION │       │   └─ 否 ↓ │       │       ├─ 文档语义结构丰富? │       │       │   ├─ 是 → SEMANTIC │       │       │   └─ 否 → SLIDING_WINDOW

11.2 性能与质量对比

| 维度 | SLIDING_WINDOW | SEMANTIC | PROPOSITION |
| — | — | — | — |
| 处理速度 | 极快(纯字符串操作) | 较慢(需调 Embedding API) | 最慢(需调 LLM API 多次) |
| 外部依赖 | 无 | EmbeddingModel | ChatModel |
| Token 消耗 | 0 | 每句一次 Embedding | 每批一次 Chat |
| 语义完整性 | 低(可能切断语义) | 高(按语义边界切分) | 最高(原子级事实) |
| 可解释性 | 高(规则明确) | 中(断点阈值可调) | 中(依赖 LLM 输出质量) |
| 推荐文档长度 | 任意 | 中长文档(>2000字) | 中短文档(LLM 输入限制) |

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片