開發日誌 #1:專案初始化與智能分片引擎

專案背景

公司需要建立內部招標文件知識庫,但市面上的向量資料庫分片效果並不理想。特別是對於中文招標文件這種高度結構化的文檔,傳統的固定大小分片或語義分片都會破壞文檔結構,導致:

  • 目錄被拆散
  • 章節標題與內容分離
  • 階層關係遺失

因此,我決定從零開始打造一套完整的 RAG 系統,核心重點是開發專門針對中文招標文件的智能分片引擎。

系統架構設計

在動手寫程式之前,我先規劃了整個系統的架構:

rag-chunking-engine/
├── src/
│   ├── parsers/          # 文檔解析器
│   │   ├── pdf_parser.py
│   │   ├── word_parser.py
│   │   └── text_parser.py
│   ├── chunkers/         # 分片策略
│   │   ├── fixed_size_chunker.py
│   │   ├── semantic_chunker.py
│   │   ├── structure_aware_chunker.py
│   │   ├── hybrid_chunker.py
│   │   └── proposal_chunker.py  ⭐ 核心
│   └── utils/            # 工具模組
│       ├── models.py
│       ├── evaluator.py
│       └── config.py
├── config/
│   └── chunking_config.yaml
└── test_document.py      # 文檔測試腳本

設計理念

模組化設計:將解析、分片、評估分離,每個模組都可以獨立測試和替換。

可擴展性:預留多種分片策略接口,雖然目前主要使用 ProposalChunker,但未來可以輕鬆加入其他策略。

統一數據模型:定義 ChunkDocumentMetadata 等標準資料結構,確保各模組間的數據流轉順暢。

ProposalChunker 開發

核心需求分析

中文招標文件(服務建議書)有以下特點:

  1. 特殊區域:封面、評選表、目次等需要完整保留
  2. 階層式標題:使用「壹貳參」→「一二三」→「(一)(二)」→「1.2.3.」的多層標題
  3. 語義完整性:每個章節應該保持完整,不能隨意切斷
  4. 長度控制:有些章節過長,需要智能分割但不能破壞上下文

實作策略

1. 特殊區域識別

使用正則表達式識別並完整保留特殊區域:

def _is_special_section(self, text: str) -> tuple[bool, str]:
    """識別特殊區域"""
    special_patterns = {
        '封面': r'服務建議書',
        '評選表': r'評選項目對照表',
        '目次': r'\s*|\s*',
        '圖目錄': r'\s*\s*',
        '表目錄': r'\s*\s*'
    }
 
    for section_type, pattern in special_patterns.items():
        if re.search(pattern, text[:200]):
            return True, section_type
 
    return False, ''

2. 階層式標題解析

支援四層中文標題格式:

# 層級 1: 壹、貳、參、肆、伍、陸、柒、捌、玖、拾
level1_pattern = r'^([壹貳參肆伍陸柒捌玖拾]+)\s*(.+)$'
 
# 層級 2: 一、二、三、四、五、六、七、八、九、十
level2_pattern = r'^([一二三四五六七八九十百]+)[\.\、]\s*(.+)$'
 
# 層級 3: (一)、(二)、(三)
level3_pattern = r'^\(([一二三四五六七八九十]+)\)[\.\、]?\s*(.+)$'
 
# 層級 4: 1.、2.、3.
level4_pattern = r'^(\d+)[\.\、]\s*(.+)$'

3. 章節路徑追蹤

維護完整的章節階層路徑:

def _update_section_path(self, level: int, title: str):
    """更新章節路徑"""
    # 截斷當前層級之後的路徑
    self.current_section_path = self.current_section_path[:level]
 
    # 添加新的層級
    self.current_section_path.append(title)
 
    return ' > '.join(self.current_section_path)

這樣就能生成像這樣的完整路徑:

參、專案需求規劃 > 二. 數位雙生智能火場鑑識輔助系統 > 1. 使用者介面層

4. 智能長度控制

當章節內容超過設定的最大長度(預設 3000 字符)時,自動分割但保留上下文:

def _split_large_section(self, content: str, metadata: dict) -> list:
    """分割過長的章節"""
    chunks = []
    max_chunk = self.config.get('max_chunk_size', 3000)
 
    # 以段落為單位分割
    paragraphs = content.split('\n\n')
    current_chunk = ""
 
    for para in paragraphs:
        if len(current_chunk) + len(para) <= max_chunk:
            current_chunk += para + "\n\n"
        else:
            if current_chunk:
                chunks.append(self._create_chunk(current_chunk, metadata))
            current_chunk = para + "\n\n"
 
    if current_chunk:
        chunks.append(self._create_chunk(current_chunk, metadata))
 
    return chunks

實測效果

測試文檔:「華電聯網服務建議書.pdf」(130 頁,約 65,000 字符)

分片結果

✅ 文檔解析成功
📄 總頁數: 130
📝 總字符數: 65,127

✅ 智能分片完成
📦 總分片數: 360
├─ 特殊區域: 5 個
│  ├─ 封面: 1
│  ├─ 評選表: 1
│  ├─ 目次: 1
│  ├─ 圖目錄: 1
│  └─ 表目錄: 1
└─ 正文分片: 355 個

⏱️  處理時間: 4 秒

分片品質檢查

隨機抽查幾個分片查看效果:

分片 #15(章節開頭):

章節路徑: 壹、專案說明 > 一. 本專案規劃重點摘要
標題層級: 2
內容預覽: (一) 運用數位雙生(Digital Twin)技術建置火場智能鑑識輔助系統...

分片 #87(多層路徑):

章節路徑: 參、專案需求規劃 > 二. 數位雙生智能火場鑑識輔助系統 > 1. 使用者介面層
標題層級: 4
內容: 本層提供友善的 Web-based 操作介面...

可以看到,ProposalChunker 完美地保留了文檔結構和階層關係!

技術難題與解決

難題 1:Windows 編碼問題

問題描述

UnicodeEncodeError: 'cp950' codec can't encode character '\u2460'

Windows 命令列預設使用 CP950 編碼,無法顯示部分中文字符和符號。

解決方案

在程式開頭強制使用 UTF-8:

import sys
import io
 
if sys.platform == 'win32':
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

難題 2:返回類型不一致

問題描述

AttributeError: 'list' object has no attribute 'text'

_split_large_section() 有時返回 list[Chunk],有時返回單個 Chunk,導致後續處理出錯。

解決方案

統一處理返回值,使用 isinstance() 判斷類型:

result = self._create_section_chunk(...)
if result:
    if isinstance(result, list):
        chunks.extend(result)  # 是 list,用 extend
    else:
        chunks.append(result)  # 是單個對象,用 append

數據輸出設計

為了方便後續使用和除錯,設計了完整的輸出結構:

rag/01 華電聯網服務建議書_20251012_124248/
├── chunks.json          # 所有分片的 JSON 數據
├── report.txt           # 統計報告
├── preview.txt          # 前 5 個分片預覽
└── chunks/              # 每個分片的獨立文字檔
    ├── chunk_0001.txt
    ├── chunk_0002.txt
    └── ...

chunks.json 結構

{
  "chunk_id": "chunk_0087",
  "text": "本層提供友善的 Web-based 操作介面...",
  "metadata": {
    "source_file": "doc/01 華電聯網服務建議書.pdf",
    "section": "參、專案需求規劃 > 二. 數位雙生智能火場鑑識輔助系統 > 1. 使用者介面層",
    "heading": "1. 使用者介面層",
    "heading_level": 4,
    "page_number": 45,
    "start_char": 15234,
    "end_char": 17891,
    "is_special": false,
    "chunk_type": "正文"
  }
}

完整的元數據讓每個分片都可追溯,為後續的檢索和問答提供了堅實基礎。

階段總結

第一階段完成了專案的核心基礎:

完成項目

  • 模組化專案結構
  • 多格式文檔解析器(PDF/Word/Text)
  • ProposalChunker 智能分片引擎
  • 完整的數據輸出與報告

技術突破

  • 四層中文標題識別
  • 章節路徑追蹤
  • 智能長度控制
  • Windows UTF-8 編碼處理

🎯 下一步: 將分片數據向量化並存入 ChromaDB,實現語義檢索功能。


← 返回專案主頁 | 下一篇:向量化與檢索系統建置 →