Files
SuperBizAgent-java/docs/analysis/plan-chunking-step4-refactor.md
T
zhuyongxin 60be51f4a5 docs: 重构文档结构,分离学习笔记和 MVP 架构设计
**变更概述:**
- 将 MVP 架构设计文档独立到项目根目录 `mvp/`
- 整理 `docs/` 为纯学习和分析文档目录
- 按类型分类:learning(学习)、analysis(分析)、reports(报告)、guides(指南)

**目录结构:**
```
mvp/                          # MVP 架构设计(独立)
├── README.md                 # 数据库设计总览
├── architecture/             # 架构文档
│   ├── agent-architecture-mvp.md
│   ├── implementation-plan.md
│   └── ...
└── tables/                   # 数据表设计

docs/                         # 学习和分析文档
├── learning/                 # 学习笔记(00-08 编号)
├── analysis/                 # 分析笔记 + 重构计划
├── reports/                  # 临时报告
└── guides/                   # 指南文档
```

**详细变更:**
- docs/README.md → mvp/README.md(数据库设计入口)
- docs/architecture/ → mvp/architecture/(架构设计)
- docs/tables/ → mvp/tables/(数据表设计)
- docs/学习笔记-*.md → docs/learning/07-*.md, 08-*.md
- docs/项目学习路径.md → docs/learning/00-*.md
- docs/功能分析报告.md → docs/analysis/
- docs/修复报告-*.md → docs/reports/
- docs/日志配置*.md → docs/guides/ 或 docs/reports/
- docs/design/ → docs/analysis/(问题分析和重构计划)
2026-06-23 14:14:51 +08:00

201 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: 分片策略第 4 步重构
> 分支: `refactor/rag-chunking-strategy`
> 状态: 规划中
> 范围: 仅改 `DocumentChunkService.chunkSection()` 一个方法
---
## 背景
经 debug 确认,当前分片流程的 1/2/3 步逻辑正确:
```
第1步 chunkDocument() → splitByHeadings(content) ✅ 保持不变
第2步 for each Section → 循环章节 ✅ 保持不变
第3步 chunkSection() 入口 → 容量短路判断 + splitByParagraphs ✅ 保持不变
第4步 chunkSection() 累积循环 → 段落累积 + 字符触发切分 ❌ 需重构
```
**第 4 步的两个核心问题:**
| 问题 | 现象 |
|------|------|
| A. 丢失顺序 | `trim()` + 手工拼接 `\n\n` 导致 `currentStartIndex` 漂移 |
| B. 结构无感知 | 有序列表项被拆散到不同分块(排查步骤 1-4 在一块,第 5 步在另一块) |
---
## 目标
改造 `chunkSection()` 的段落累积循环,使其:
1. **不丢顺序** — 用原始文本索引替代手工拼装的 `currentStartIndex`
2. **感知列表结构** — 有序/无序列表项之间不在中间切断
3. **Token 感知** — 用启发式 token 估算替代纯字符计数(为后续 Spring AI TokenTextSplitter 做准备)
4. **软边界** — 在接近上限时查找语义安全切点,而非硬截断
---
## 不改的部分
| 组件 | 理由 |
|------|------|
| `splitByHeadings()` | 标题分割正确,正则够用 |
| `getOverlapText()` | 句子校准逻辑保留,作为安全网 |
| `DocumentChunk` 数据结构 | 字段完备,无需新增 |
| `DocumentChunkConfig` | 增加 `maxTokens` 字段,保留原字段兼容 |
| `VectorIndexService` | 消费者改动延后到下一阶段 |
---
## 改动方案
### 改动 1: `DocumentChunkConfig` — 增加 token 配置
```java
// 新增字段
private int maxTokens = 500; // token 上限(中文约500字,英文约2000字符)
private int maxTokensHard = 600; // 硬上限(maxTokens × 1.2)
// 保留原字段作为向后兼容
private int maxSize = 800; // 保留但标记 @Deprecated
```
### 改动 2: `chunkSection()` — 改造累积循环
**当前逻辑(伪代码):**
```
for each paragraph:
if length + paragraph > maxSize → 切分 → 从 overlap 开始新块
append paragraph + "\n\n"
```
**新逻辑(伪代码):**
```
for each paragraph:
currentTokens = estimateTokens(buffer)
paraTokens = estimateTokens(paragraph)
if currentTokens + paraTokens > maxTokens:
if isInUnbreakableContext(buffer, paragraph):
if currentTokens + paraTokens > maxTokensHard:
→ 必须切(硬上限保护)
else:
→ 不切,继续累积(容忍超出,保护列表完整性)
else:
→ 切分(段落边界 = 安全切点)
→ 从 overlap 开始新块
else:
→ 不切,继续累积
append paragraph + "\n\n"
```
### 改动 3: 新增 `estimateTokens()` — 启发式 token 估算
```java
/**
* 启发式 token 估算(无需外部依赖)
* 中文: ~1 字符/token
* 英文/数字: ~4 字符/token
* 标点/空白: 忽略
*/
private int estimateTokens(String text) {
int tokens = 0;
for (char c : text.toCharArray()) {
if (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A) {
tokens += 1; // 中文字符 1:1
} else if (Character.isWhitespace(c)) {
// 空白字符不计
} else {
tokens += 1; // 非中文凑 4 个算 1 token(简化)
}
}
// 非中文部分 / 4
return tokens;
}
```
### 改动 4: 新增 `isInUnbreakableContext()` — 结构感知
```java
/**
* 判断当前段落是否属于不可中断的结构
* 返回 true = 不能在当前位置切分
*/
private boolean isInUnbreakableContext(String buffer, String nextParagraph) {
// 有序列表: "1. " "2. " "3. " 格式
if (nextParagraph.matches("^\\d{1,2}\\.\\s.*")) {
// 前一个段落也是列表项 → 不切
String lastLine = getLastNonEmptyLine(buffer);
if (lastLine.matches("^\\d{1,2}\\.\\s.*|.*\\n\\d{1,2}\\.\\s.*")) {
return true;
}
}
// 无序列表: "- " 或 "* " 格式
if (nextParagraph.matches("^[-*]\\s.*")) {
String lastLine = getLastNonEmptyLine(buffer);
if (lastLine.matches("^[-*]\\s.*|.*\\n[-*]\\s.*")) {
return true;
}
}
// 代码块: ``` 内部不切
if (buffer.contains("```") && countOccurrences(buffer, "```") % 2 == 1) {
return true; // 在未闭合的代码块内 → 不切
}
return false;
}
```
### 改动 5: 修复 index 漂移
```java
// 当前问题:用手工拼装的 chunkContent.length() 推算 offset
// String chunkContent = currentChunk.toString().trim(); ← trim 丢字符
// currentStartIndex = currentStartIndex + chunkContent.length() - overlap.length(); ← 漂移
// 改为:用段落在原始文档中的实际位置
// 对每个 paragraph 记录其在 section.content 中的 offset,切分时直接使用
```
---
## 改动文件清单
| 文件 | 改动 | 行数变化 |
|------|------|----------|
| `config/DocumentChunkConfig.java` | +2 字段 | +8 |
| `service/DocumentChunkService.java` | 改造 `chunkSection()` + 3 个新方法 | ~+50 / -20 |
| `test/.../DocumentChunkServiceTest.java` | 新增列表结构感知 + token 估算用例 | +40 |
总计改动约 80 行,仅影响一个核心方法。
---
## 验收标准
| # | 用例 | 预期 |
|---|------|------|
| 1 | 有序列表(5 项,每项 50 字符,maxTokens=180) | 5 项不拆散,容忍略超上限 |
| 2 | 有序列表(20 项,超 maxTokensHard) | 在硬上限处切,但不在列表项中间切 |
| 3 | 纯段落(10 段,每段 100 字符,maxTokens=300) | 在段落边界切 |
| 4 | 中文 800 字 vs 英文 3200 字符 | 分块数接近 |
| 5 | H1→空的→H2(标题空壳) | 仍有(不在本次修复范围) |
| 6 | 原有测试:空文档、短文档、标题分割、重叠、chunkIndex | 全部通过 |
---
## 后续阶段
| 阶段 | 内容 | 依赖 |
|------|------|------|
| **Phase 1(本次)** | 改 `chunkSection()` — token + 列表感知 | 无 |
| Phase 2 | 标题空壳问题修复(`splitByHeadings` 合并相邻空 section) | Phase 1 |
| Phase 3 | 可选:切换到 Spring AI `TokenTextSplitter` | Phase 1/2 |
| Phase 4 | 语义相似度辅助切点决策 | Phase 1 |
| Phase 5 | Markdown AST 解析替代正则 | 低优先级 |