# 本地语义分块与向量化实现文档

> 本文档记录项目中文档数据的本地语义分块、embedding 向量化、Milvus 存储的完整实现方式与完成状态。

**文档版本**：v1.0
**创建日期**：2026-06-27
**依赖状态**：已实现本地 bge-m3 dense + Milvus 2.5+ BM25 sparse，替代原智谱 embedding-2 方案。

---

## 一、总体架构

```text
┌─────────────────────────────────────────────────────────────────┐
│                         Spring Boot 后端                         │
│  ┌─────────────────┐    ┌─────────────────┐    ┌──────────────┐ │
│  │ DocumentService │───▶│ Embedding Bridge│───▶│   Milvus     │ │
│  │ (KbDocument/    │    │ (Python/FastAPI)│    │ (dense+sparse)│ │
│  │  KbChunk in H2) │    │  port 18732     │    │              │ │
│  └─────────────────┘    └─────────────────┘    └──────────────┘ │
│         │                                              ▲        │
│         │                                              │        │
│         ▼                                              │        │
│  ┌─────────────────────────────────────────────────────┐        │
│  │              HierarchicalTextSplitter                │        │
│  │  L1 (≈2400 chars, 规则) → L2 (≈1600, 语义) → L3 (≈800, 语义)│        │
│  │  仅 L3 写入 Milvus；L1/L2 保留在 H2                  │        │
│  └─────────────────────────────────────────────────────┘        │
└─────────────────────────────────────────────────────────────────┘
```

---

## 二、分块实现

### 2.1 文件位置

| 文件 | 作用 |
|------|------|
| `backend/embedding-bridge/backend/indexing/text_splitter.py` | 三级层次化分块器 `HierarchicalTextSplitter` + `SemanticTextSplitter` |
| `backend/embedding-bridge/backend/indexing/semantic_chunker.py` | 基于本地 embedding 的语义边界检测 `SemanticChunker` |

### 2.2 三级分块策略

| 层级 | 大小 | 语义分块 | 重叠 | 说明 |
|------|------|----------|------|------|
| L1 | `max(2000, chunk_size * 3)` | 否 | 0 | 对原文做粗粒度规则切分，保证所有 L1 拼接等于原文 |
| L2 | `max(1000, chunk_size * 2)` | 是 | `chunk_overlap` | 在 L1 内部按语义边界粗分，允许边界句子重叠 |
| L3 | `max(600, chunk_size)` | 是 | `chunk_overlap` | 在 L2 内部按语义边界细分，允许边界句子重叠；**仅 L3 写入 Milvus** |

> - L1 必须保证严格拼接等于原文，因此 L1 之间不允许 overlap。
> - L2/L3 允许在语义边界处共享“承上启下”的句子，相邻 chunk 之间会出现内容重叠。
> - 重叠发生在**完整语义单元**（句子/子句）边界，而不是字符级滑动窗口。

### 2.3 规则分块（`SemanticTextSplitter`）

1. **文本净化**（`sanitize_text`）：NFC 规范化、剔除零宽字符/C0/C1 控制符/BOM/PUA 区乱码、UTF-16 代理项。
2. **原子切分**：按分隔符优先级递归切分，保留分隔符本身作为独立原子，保证拼接还原。
   - 优先级：`\n\n` > `。` > `！` > `？` > `；` > `\n` > `，` > `、` > `空格` > 字符
3. **合并原子**：贪心合并到接近 `chunk_size`；超过大小时回退到最近语义分隔符。
4. **重叠区**（overlap > 0 时）：从当前 chunk 末尾回退，截取不超过 overlap 且以语义边界结尾的重叠内容。

### 2.4 语义分块（`SemanticChunker`）

在规则分块之前增加一道语义粗分：

1. **切分语义单元**：按句子/段落分隔符切分，每个单元保留末尾标点。
2. **合并短单元**：小于 `min_unit_length`（默认 12）的单元向前合并，减少 embedding 噪声。
3. **生成向量**：调用本地 `embedding_service.get_embeddings()` 批量获取单元向量。
4. **计算相邻相似度**：余弦相似度。
5. **建立边界**：
   - 强制边界：当前段落长度超过 `max_chunk_size` 必须切开。
   - 语义边界：相邻单元相似度低于**有效阈值**且出现**显著下降**时切开。

### 2.5 自适应阈值策略

中文语料相邻句子相似度普遍偏高（常见 `0.65~0.85`），固定阈值 `0.6` 几乎不触发切分。因此实现自适应阈值：

- 默认启用 `use_adaptive_threshold=True`
- 有效阈值 `effective_threshold = max(similarity_threshold, percentile(similarities, 0.25))`
  - `similarity_threshold` 默认 `0.55`，作为地板值
  - 取所有相邻相似度的 **25 分位数**作为动态阈值
- **显著下降**条件：`prev_sim - sim > significant_drop`（默认 `0.08`）
  - 避免在整体偏低但平稳的语料内部产生碎块

---

## 三、向量化实现

### 3.1 本地 dense embedding

| 项目 | 内容 |
|------|------|
| 文件 | `backend/embedding-bridge/backend/indexing/embedding.py` |
| 模型 | `BAAI/bge-m3` |
| 维度 | 1024 |
| 设备 | CPU（默认） |
| 池化方式 | mean pooling + L2 normalize |
| 最大长度 | 512 tokens |

代码核心：

```python
tokenizer = AutoTokenizer.from_pretrained(model_name, local_files_only=local_only)
model = AutoModel.from_pretrained(model_name, local_files_only=local_only)
outputs = model(**inputs)
embeddings = _mean_pooling(outputs.last_hidden_state, inputs["attention_mask"])
embeddings = F.normalize(embeddings, p=2, dim=1)
```

### 3.2 稀疏向量 / 全文检索

不再手动维护 BM25 字典，而是使用 **Milvus 2.5+ 原生 BM25**：

- Collection schema 中为 `text` 字段启用 `FunctionType.BM25`
- Milvus 在插入时自动对 `text` 字段做中文分词并生成 sparse vector
- 检索时同样使用 `FunctionType.BM25` 将查询文本转换为 sparse vector，与存储的 sparse vector 做匹配

这种方式避免了 Python 端维护词汇表与 IDF 的复杂性。

### 3.3 Embedding Bridge 服务

| 项目 | 内容 |
|------|------|
| 文件 | `backend/embedding-bridge/server.py` |
| 端口 | 18732 |
| 启动方式 | Spring Boot 自动检测并启动子进程 |
| 主要接口 | `POST /embed` 批量获取 dense embedding；`POST /chunk` 分块 |
| 退出机制 | 父进程 watcher：监控 Java 父进程 PID，父进程被杀后 Python 子进程自动退出 |

父进程 watcher 实现：

```python
def _start_parent_watcher():
    import psutil, os, time
    parent = psutil.Process(os.getppid())
    def _watch():
        while True:
            time.sleep(2)
            if not parent.is_running() or parent.status() == psutil.STATUS_ZOMBIE:
                os._exit(0)
    threading.Thread(target=_watch, daemon=True).start()
```

---

## 四、Milvus 存储

### 4.1 Collection 设计

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | VARCHAR | 主键，chunk_id，如 `docId::l3::index` |
| `document_id` | Int64 | 所属文档 ID |
| `text` | VARCHAR / TEXT | 块文本内容，用于 BM25 |
| `dense_vector` | FloatVector(1024) | bge-m3 dense 向量 |
| `sparse_vector` | SparseFloatVector | Milvus BM25 自动生成 |
| `chunk_level` | Int8 | 固定 3（仅 L3） |
| `chunk_idx` | Int64 | 全局顺序索引 |
| `file_type` | VARCHAR | 源文件类型 |
| `filename` | VARCHAR | 源文件名 |
| `file_path` | VARCHAR | 源文件路径 |
| `page_number` | Int32 | 页码（PDF 等） |

### 4.2 索引

- `dense_vector`：IVF_FLAT / COSINE，用于语义检索
- `sparse_vector`：SPARSE_INVERTED_INDEX，用于全文检索

### 4.3 检索方式

支持 hybrid search：

- dense 召回：基于 bge-m3 向量相似度
- sparse 召回：基于 BM25 全文匹配
- 可配置融合权重，综合排序返回 topK

---

## 五、完成内容

### 5.1 已实现的文件

| 文件 | 变更 |
|------|------|
| `backend/embedding-bridge/backend/indexing/semantic_chunker.py` | 新增：基于 bge-m3 的语义分块器 |
| `backend/embedding-bridge/backend/indexing/text_splitter.py` | 修改：集成语义分块到三级分块器 |
| `backend/embedding-bridge/backend/indexing/embedding.py` | 修改/新增：本地 bge-m3 dense embedding |
| `backend/embedding-bridge/server.py` | 修改：增加父进程 watcher，Java 被杀后自动退出 |
| `backend/hermes-bridge/hermes_bridge.py` | 修改：增加父进程 watcher（同机制） |
| `backend/src/main/java/com/agent/management/service/Neo4jExecutorService.java` | 修改：增加 `@PreDestroy destroy()` 关闭 Neo4j drivers |
| `backend/src/main/java/com/agent/management/config/MilvusConfig.java` | 修改：Milvus client destroyMethod="close" |

### 5.2 已验证的测试

1. **层级一致性**：短文本（674 字符）和长文本（811 字符）均验证 L1/L2/L3 各自拼接等于原文。
2. **语义边界检测**：
   - 990 字符五段跨领域文本（量子计算 / 太阳系 / 印象派 / 热带雨林 / 古罗马法）在 L3 切分为 5 块，每块对应一个主题，验证了 bge-m3 能够有效识别强语义边界。
   - 909 字符四段主题文本（AI / 烹饪 / 旅行 / 运动）中，由于中文相邻句子在 bge-m3 下相似度普遍偏高（0.70~0.85），自适应阈值（25 分位数）被抬高到约 0.74，仅在差异最显著的边界（烹饪→旅行，相似度从 0.81 骤降到 0.64）处切开，说明当前参数对高相似度中文语料偏保守，强主题边界才能稳定触发分块。
3. **重叠策略验证**：990 字符五段跨领域文本在 `chunk_overlap=0` 时 L1/L2/L3 拼接均严格等于原文；在 `chunk_overlap=100` 时 L3 在语义边界处产生预期重叠（每个 chunk 开头重复前一段末尾的完整句子），L1 仍严格无重叠。
4. **进程清理**：Java 主进程被杀后，Hermes 与 Embedding Bridge 子进程在数秒内自动退出。

---

## 六、关键设计决策

### 6.1 为什么用本地 bge-m3 替代智谱 embedding-2？

- 避免外部 API 依赖与网络延迟
- 本地模型一次加载，无限次推理
- 保护数据隐私，文档内容不出本机
- 1024 维 dense 向量质量足够支撑 RAG 检索

### 6.2 为什么 L3 才写入 Milvus？

- L3 是最小检索粒度，适合 RAG 召回
- L1/L2 仅用于层级展示与上下文聚合
- 减少 Milvus 数据量，降低存储与检索开销

### 6.3 为什么语义分块只在 L2/L3 启用？

- L1 是顶层切分，目标是控制在内存可处理的大块，不需要语义边界
- L2/L3 是内容召回粒度，语义边界能显著提升检索质量
- L1 必须保证严格拼接等于原文，因此 L1 之间不允许 overlap
- L2/L3 允许在语义边界处保留 chunk_overlap，使承上启下的句子同时出现在相邻块中，提升检索召回率；overlap=0 时 L2/L3 拼接仍严格等于父级文本

### 6.4 为什么需要自适应阈值？

- 中文句子在 bge-m3 下的余弦相似度普遍偏高
- 固定阈值 `0.6` 在实测中几乎不触发切分
- 取相似度分布的 25 分位数 + 显著下降条件，能在不同语料上保持稳定切分

---

## 七、配置说明

### 7.1 `application.yml` 相关配置

```yaml
app:
  kb:
    enabled: true
    upload-dir: ./uploads/kb
    max-file-size: 52428800
    chunk-size: 1000               # L3 目标大小
    chunk-overlap: 200             # L2/L3 语义边界处允许的重叠量；L1 始终为 0

  milvus:
    enabled: true
    host: localhost
    port: 19530
    collection: kb_documents
    vector-dimension: 1024

  embedding-bridge:
    enabled: true
    host: 127.0.0.1
    port: 18732
    chunk-size: 800                # L3 目标大小
    chunk-overlap: 100             # L2/L3 语义边界处允许的重叠量；L1 始终为 0
    embedding-model: BAAI/bge-m3
    embedding-device: cpu
```

### 7.2 Python 环境

Embedding Bridge 依赖：

```text
torch
transformers
fastapi
uvicorn
pymilvus>=2.5.0
psutil
```

---

## 八、待办 / 后续扩展

- [ ] 端到端文档上传验证（Tika 解析 → 分块 → 向量化 → Milvus upsert）
- [ ] 中文语义分块阈值调优（当前 adaptive p25 + drop=0.08 对高相似度中文语料偏保守，强主题边界才能稳定触发分块）
- [ ] hybrid search 权重调优与前端检索测试
- [ ] GPU 设备支持（当前仅 CPU）
- [ ] 多语言/英文语料自适应阈值验证
- [ ] RAG 工作流节点 `kbRetrieval` 接入

---

## 九、风险与注意事项

| 风险 | 说明 |
|------|------|
| 模型首次加载慢 | bge-m3 模型首次加载可能需要数十秒到数分钟，取决于磁盘与 CPU |
| 长文本截断 | `MAX_LENGTH=512`，超长文本会被截断；当前按句子分块后单元通常小于 512 tokens |
| Windows 子进程残留 | 已通过父进程 watcher 解决；若 psutil 未安装则降级不监控 |
| 中文分词质量 | 依赖 Milvus 2.5+ 内置中文分词，需确保 Milvus 版本 >= 2.5 |

---

**文档维护**：当 `text_splitter.py`、`semantic_chunker.py`、`embedding.py` 或 `server.py` 发生变更时，应同步更新本文档。
