iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
AI Engineering

30天從零打造 AI 中台自學之路系列 第 8

Day 08 - 內部規範文件解析與前處理:Markdown、Git 規範與 OpenAPI Schema 階層解析

  • 分享至 

  • xImage
  •  

進入第二週的 RAG 知識庫實戰,首要任務是處理「資料品質(Data Quality)」。在真實企業環境中,開發規範通常分散在不同格式的文件中:

  • 團隊 Wiki / 知識庫:常見為 Markdown 或 HTML 格式,包含大量的目錄標籤、版本修訂紀錄與註記。
  • 版本控制流程(Git Flow):通常記錄在專案根目錄的 CONTRIBUTING.md
  • API 設計規範與規格書:以 OpenAPI Spec(Swagger JSON/YAML)為代表,屬於高結構化的定義檔。

如果未經清洗直接將這些文件丟進 Embedding 模型,無關的格式噪音(如大量連續換行、無意義的 HTML 標籤、版本日誌)將大幅稀釋核心語意。今天我們將實作一個前處理管道(Pipeline),負責解析這三類文件並建立結構化的 Metadata。


一、文件預處理架構目標

  1. 噪音清理:移除 Markdown 內部連結、多餘空行、HTML 註解與 Base64 內嵌圖片。
  2. 語意保留:保留 Markdown 標題層級(######)以維持文脈階層。
  3. OpenAPI Schema 語義轉換:將機器讀取的 JSON/YAML 轉換為適合向量檢索的文字摘要區塊。
  4. 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 圖片語法 ![alt](url)
        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)與長篇規範在切分時如何避免上下文斷裂,並實作保留代碼邊界的語義切割器。


上一篇
Day 07 - Embedding 與 Reranker 模型落地:開源 Embedding 評測與本地 Cross-Encoder 部署
系列文
30天從零打造 AI 中台自學之路8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言