進入第二週的 RAG 知識庫實戰,首要任務是處理「資料品質(Data Quality)」。在真實企業環境中,開發規範通常分散在不同格式的文件中:
CONTRIBUTING.md。如果未經清洗直接將這些文件丟進 Embedding 模型,無關的格式噪音(如大量連續換行、無意義的 HTML 標籤、版本日誌)將大幅稀釋核心語意。今天我們將實作一個前處理管道(Pipeline),負責解析這三類文件並建立結構化的 Metadata。
#、##、###)以維持文脈階層。pip install markdown pyyaml pydantic
建立 document_preprocessor.py:
import re
import json
import yaml
from typing import List, Dict, Any
from dataclasses import dataclass, asdict
@dataclass
class ProcessedDocument:
doc_id: str
title: str
category: str
language: str
content: str
metadata: Dict[str, Any]
class DocumentPreprocessor:
def __init__(self):
pass
def clean_markdown_noise(self, text: str) -> str:
"""清理 Markdown 中的圖片、HTML 標籤與多餘空白符號"""
# 移除 Markdown 圖片語法 
text = re.sub(r'!\[.*?\]\(.*?\)', '', text)
# 移除 HTML 標籤 (例如 <br>, <div> 等)
text = re.sub(r'<[^>]+>', '', text)
# 移除 Markdown 連結但保留文字描述 [text](url) -> text
text = re.sub(r'\[(.*?)\]\(.*?\)', r'\1', text)
# 壓縮多個連續空白行為單一空行
text = re.sub(r'\n{3,}', '\n\n', text)
return text.strip()
def parse_markdown_with_frontmatter(
self,
raw_text: str,
fallback_doc_id: str,
category: str = "coding_style",
language: str = "all"
) -> List[ProcessedDocument]:
"""
解析 Markdown 文本,若包含 YAML Frontmatter 則優先提取屬性,
並依據二級標題 (##) 拆分為語義段落。
"""
# 1. 檢查並解析 Frontmatter
frontmatter = {}
content_body = raw_text
fm_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', raw_text, re.DOTALL)
if fm_match:
try:
frontmatter = yaml.safe_load(fm_match.group(1)) or {}
content_body = raw_text[fm_match.end():]
except yaml.YAMLError:
pass
# 2. 清理正文噪音
clean_body = self.clean_markdown_noise(content_body)
# 3. 依據 Markdown 標題提取章節
# 匹配 ## 標題開頭的段落
sections = re.split(r'\n(?=##\s+)', clean_body)
documents: List[ProcessedDocument] = []
doc_title = frontmatter.get("title", "通用開發規範")
target_lang = frontmatter.get("language", language)
target_cat = frontmatter.get("category", category)
for idx, sec in enumerate(sections):
sec = sec.strip()
if not sec:
continue
# 抓取第一行標題
first_line = sec.split('\n')[0]
section_title = first_line.replace('#', '').strip() if first_line.startswith('#') else f"{doc_title}-段落{idx}"
documents.append(ProcessedDocument(
doc_id=f"{fallback_doc_id}-SEC-{idx:02d}",
title=f"{doc_title} - {section_title}",
category=target_cat,
language=target_lang,
content=sec,
metadata={
**frontmatter,
"section_title": section_title,
"source_type": "markdown"
}
))
return documents
def parse_openapi_spec(
self,
raw_spec_text: str,
spec_id: str
) -> List[ProcessedDocument]:
"""
解析 OpenAPI JSON/YAML 規範,將每個 Endpoint 轉換為標準檢索文本。
"""
try:
# 優先嘗試 JSON,失敗則嘗試 YAML
spec_data = json.loads(raw_spec_text)
except json.JSONDecodeError:
spec_data = yaml.safe_load(raw_spec_text)
documents: List[ProcessedDocument] = []
paths = spec_data.get("paths", {})
for path, methods in paths.items():
for method, details in methods.items():
if method.lower() not in ["get", "post", "put", "delete", "patch"]:
continue
operation_id = details.get("operationId", f"{method}_{path}")
summary = details.get("summary", "無描述說明")
description = details.get("description", "")
# 抽取請求參數摘要
parameters = details.get("parameters", [])
param_summary = []
for p in parameters:
param_summary.append(f"- {p.get('name')} ({p.get('in')}): {p.get('description', '')}")
param_text = "\n".join(param_summary) if param_summary else "無特殊參數"
# 組合成具備高語義關聯的標準文字
endpoint_content = (
f"### API 端點:{method.upper()} {path}\n"
f"**摘要說明**:{summary}\n"
f"**詳細描述**:{description}\n"
f"**參數規格**:\n{param_text}\n"
)
documents.append(ProcessedDocument(
doc_id=f"{spec_id}-{operation_id}",
title=f"API 規格: {method.upper()} {path}",
category="api_schema",
language="all",
content=endpoint_content,
metadata={
"path": path,
"method": method.upper(),
"operation_id": operation_id,
"source_type": "openapi"
}
))
return documents
撰寫verify_parsing.py,測試 Markdown 規範與 OpenAPI Schema 的處理結果:
import json
from document_preprocessor import DocumentPreprocessor
preprocessor = DocumentPreprocessor()
# 1. 測試 Markdown 規範清洗與切分
markdown_sample = """---
title: Python 後端開發準則
category: coding_style
language: python
version: v2.1.0
---
# Python 開發規範
以下為企業內部核心規範。詳細請參考 [內部連結](https://wiki.corp.internal/dummy)。
## 變數命名與型別標註
所有公開函式必須強制使用 Type Hints。
變數命名遵循 snake_case 規則,類別命名遵循 PascalCase。
## 例外處理規範
嚴禁使用空的 except 區塊捕捉所有錯誤。
必須明確指定 Exception 型別,並透過 logger.error 記錄完整 Traceback。
<br><div class="footer">Confidential - Internal Only</div>
"""
parsed_md_docs = preprocessor.parse_markdown_with_frontmatter(
markdown_sample,
fallback_doc_id="PY-STD"
)
print(f"--- Markdown 解析結果 (共 {len(parsed_md_docs)} 篇) ---")
for doc in parsed_md_docs:
print(f"ID: {doc.doc_id}")
print(f"標題: {doc.title}")
print(f"語言: {doc.language} | 分類: {doc.category}")
print(f"內容預覽:\n{doc.content}\n" + "-"*40)
# 2. 測試 OpenAPI 轉換為自然語言檢索塊
openapi_sample = """
paths:
/api/v1/orders:
post:
summary: 建立新訂單
operationId: createOrder
description: 接收購物車商品清單並建立待付款訂單
parameters:
- name: idempotency_key
in: header
description: 防止重複下單之冪等性金鑰
"""
parsed_api_docs = preprocessor.parse_openapi_spec(openapi_sample, spec_id="ORDER-API")
print(f"\n--- OpenAPI 解析結果 (共 {len(parsed_api_docs)} 篇) ---")
for doc in parsed_api_docs:
print(f"ID: {doc.doc_id}")
print(f"標題: {doc.title}")
print(f"內容:\n{doc.content}")
標籤層級保持完整性:直接粗暴按固定字數切斷 Markdown,會導致開頭失去 # 標籤,讓模型無法理解段落所屬的大章節。透過 ## 正規切分,能確保每個子規範獨立成完整單位。
OpenAPI 語義轉換:原始 YAML/JSON 中的層級巢狀對 Embedding 模型而言非常難以比對語意。透過前處理將 path、method、summary 與 parameters 轉化為易於檢索的 Markdown 結構,檢索比對命中率會顯著提高。
完成基礎資料清洗後,明天 Day 09 我們將進入 RAG 管線中最關鍵的環節:針對程式架構文件的 Chunking 策略,探討長代碼塊(Code Block)與長篇規範在切分時如何避免上下文斷裂,並實作保留代碼邊界的語義切割器。