跳到主要內容

DE Day 40 文件化與交接:讓管線活得比作者久

DE Day 40 文件化與交接:讓管線活得比作者久

執行需求:CPU 可跑。資料工程最常見的交接慘劇是這樣的:作者離職或轉調,三個月後同事打開 repo,看到一堆 Python 腳本與 SQL 檔案,卻找不到「為什麼這樣寫」「怎麼跑」「出問題找誰」。今天的主題是文件化與交接:我們把整套管線的「為什麼、怎麼跑、出問題怎麼辦」寫成 README、runbook、決策紀錄,並用 MkDocs 把這些文件變成可搜尋的網站,讓接手的人第一天就能開始改。整個流程 CPU 可跑,沒有額外雲端相依。

引言

寫程式容易,寫文件難。把文件當成「給未來的自己與同事的信」這個比喻很貼切:未來的自己已經忘記為什麼當初這樣設計;同事更不知道你的腦袋裡裝了什麼。文件的目的不是炫技術,而是把作者的「隱性知識」變成讀者能吸收的「顯性知識」。一份好的文件讓接手的人能在第一天就把管線跑起來,第一週就能開始改;沒有文件,再簡單的管線也要花一週才搞得懂。

資料工程的特殊性在於「管線 = 程式 + 資料 + 排程 + 監控」。文件必須同時涵蓋這四個面向,缺一個都不完整。程式碼本身的 docstring 只處理「程式怎麼跑」,資料字典處理「欄位是什麼意思」,排程文件處理「什麼時候跑、誰負責」,監控文件處理「出問題怎麼發現」。今天我們把這四份文件整合成一個可搜尋的靜態網站,部署到 GitHub Pages 之後就成了 24 小時可用的交接手冊。

這一篇的設計原則是「文件要可以被搜尋、被版本化、被驗證」。可以被搜尋意味著不要把文件散落在 wiki、Slack、Email 各處,要集中在一個站點;可以被版本化意味著文件跟程式碼一起進 git,每次改文件都有 PR 紀錄;可以被驗證意味著文件中的指令必須可以從乾淨的機器跑一遍就通,把過期的指令藏在文件裡比沒有文件更糟。

文件化的四個層次

資料工程的文件大致分成四個層次,每層解決不同的問題。第一層是 README:給「剛打開 repo 的人」看的入口,目標是讓他們 5 分鐘內知道這是做什麼的、用什麼技術、怎麼跑起來。第二層是 ARCHITECTURE:給「想理解整體設計的人」看的技術總覽,涵蓋資料流、模型、排程與監控的互動關係。第三層是 RUNBOOK:給「值班的人」看的操作手冊,列舉常見失敗情境與對應處理步驟。第四層是 DECISIONS(Architecture Decision Records, ADR):給「未來想改設計的人」看的歷史紀錄,每篇記錄一次重大決策的脈絡與替代方案。

這四層文件不是替代關係,而是互補。README 告訴你「做什麼」,ARCHITECTURE 告訴你「怎麼做」,RUNBOOK 告訴你「壞了怎麼辦」,DECISIONS 告訴你「為什麼這樣做」。一份完整的專案文件應該四層都有,並且互相引用。我們這四天(Day 38-40)寫的合約、監控腳本、LLM 清洗程式就是「程式」的部分,今天把它們對應的文件補齊,整個管線才算「可交接」。

README:5 分鐘入口

README 是 repo 的門面。理想的 README 應該在一頁 A4 內回答六個問題:這個專案做什麼、用什麼技術架構、怎麼跑起來、怎麼驗證跑成功、上線後怎麼監控、出問題找誰。我們用一份 Markdown 範例展示這個結構,所有內容都可以從 README 直接連到 ARCHITECTURE、RUNBOOK、ADR。

# de-journey:政府開放資料每日管線

## 這個專案做什麼
每日從行政院環境部(airtw.moenv.gov.tw)拉取逐時空氣品質量測,
建立「測站 → 觀測時間 → AQI / 污染物」事實表,並用 Streamlit
儀表板呈現最近 30 天的趨勢與各測站排名。資料授權為政府資料開放
授權條款第 1 版。

## 技術架構
- Python 3.13 + uv 管理套件
- DuckDB 1.4(倉儲)
- dbt-core 1.10 + dbt-duckdb 1.10(轉換建模)
- APScheduler 3.11(本機排程)
- Streamlit 1.41(儀表板)
- GitHub Actions(雲端排程與通知)

## 怎麼跑起來
```bash
bash scripts/bootstrap.sh   # 安裝環境(見 ARCHITECTURE.md)
uv run python -m pipelines.daily_run --date 2025-12-09
```

## 怎麼驗證跑成功
```bash
uv run python scripts/verify_env.py
uv run python scripts/check_contract.py
```
最後一行必須出現「All checks passed!」(」」

## 上線後怎麼監控
見 `RUNBOOK.md` 與 `monitoring/` 目錄。每日 03:00 (UTC+8)
跑 daily_run,並把 SLI 寫入 `logs/sli_report.json`。

## 出問題找誰
Owner:data-platform@example.com
On-call:本團隊輪值表見 `RUNBOOK.md` 的「值班」段。

這個 README 有幾個關鍵設計。第一,「做什麼」這段把資料來源、授權、用途三個關鍵資訊都寫進去,避免讀者還要去翻其他文件。第二,「技術架構」把版本號寫死,讀者可以一眼看出這個專案基於哪一年的生態系,避免用過期的工具升級。第三,「怎麼跑起來」與「怎麼驗證跑成功」分離:先跑得起來、再驗證結果正確,兩段都不能省略。第四,「出問題找誰」直接把負責人 email 寫進去,避免讀者還要去翻組織圖。

ARCHITECTURE:技術總覽

ARCHITECTURE 文件回答「這套管線的元件怎麼互動」。一份好的 ARCHITECTURE 至少要有:資料流圖、模型清單(含顆粒度與更新頻率)、排程相依關係、失敗處理策略。我們用一個 Markdown 段落加幾張 ASCII 圖把這些資訊壓在一頁 A4。

# ARCHITECTURE

## 資料流
```
+----------------+    +----------------+    +----------------+
|  環境部 AQI    |-->|  DuckDB 倉儲   |-->|   dbt 模型     |
|  (原始 API)    |    |  (warehouse/)  |    |   (models/)    |
+----------------+    +----------------+    +----------------+
                                                       |
                                                       v
                                              +----------------+
                                              | Streamlit     |
                                              | (儀表板)      |
                                              +----------------+
```

## 模型清單(與 dbt 對應)
- `stg_aqi_raw`:原始逐時量測,顆粒度「測站 × 小時」。
- `stg_stations`:測站基本資料,顆粒度「測站」。
- `int_aqi_with_station`:事實表 join 維度表。
- `fct_aqi_hourly`:給儀表板的最終事實表。
- `mart_daily_summary`:每日聚合(給月報用)。

## 排程
每日 03:00 (UTC+8) 跑 daily_run:
1. ingest_aqi.py(從 API 抓取昨天資料)
2. dbt run(重建模型)
3. dbt test(合約檢查)
4. check_contract.py(SLI 報表)
5. notify.py(違規告警)

## 失敗處理
- ingest 失敗:APScheduler 重試 3 次,每次間隔 5 分鐘。
- dbt 失敗:保留前一次成功的 DuckDB,發 Slack 告警。
- test 失敗:同上,並把違規列寫進 contract_breaches.jsonl。

ASCII 圖雖然醜,但可以放進 Markdown、用 vim 改、進 git 版本化。專業的繪圖工具(draw.io、Excalidraw)適合放在對外簡報,但放在 repo 裡反而會變成「PNG 不能 diff、不能用 grep 找」的維護負擔。當專案長大、需要更精緻的圖時,再改用 Mermaid(Markdown 直接支援)也是好選擇。

完整實作:用 MkDocs 把文件變成靜態網站

把 README、ARCHITECTURE、RUNBOOK、DECISIONS 集中在一個靜態網站裡,是「文件可被搜尋」的最低成本解法。我們用 MkDocs(Material 主題)來做這件事。MkDocs 把 Markdown 轉成 HTML,支援全文搜尋與多頁導覽,整套工具鏈 5 分鐘裝好,輸出是純靜態檔,可以丟到任何 CDN。

"""de-journey/scripts/doc_init.py:把現有的 Markdown 文件搬進 docs/。"""
from __future__ import annotations

import shutil
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
DOCS = ROOT / "docs"

# 1. 把根目錄的 README.md / ARCHITECTURE.md 搬進 docs/
mapping = {
    "README.md": "index.md",
    "ARCHITECTURE.md": "architecture.md",
}
for src_name, dst_name in mapping.items():
    src = ROOT / src_name
    dst = DOCS / dst_name
    if src.exists():
        shutil.copy(src, dst)
        print(f"已複製 {src_name} -> docs/{dst_name}")

# 2. 建立空的 RUNBOOK 與 DECISIONS
for name in ["runbook.md", "decisions.md"]:
    target = DOCS / name
    if not target.exists():
        target.write_text(
            f"# {name.removesuffix('.md').title()}\n\n待開寫。\n",
            encoding="utf-8",
        )
        print(f"已建立 docs/{name}")
print("文件初始化完成")

這段用 Python 把「搬檔案、建空檔」這件事包起來,好處是可以放進 PR 的 pre-commit 檢查:每次改 README.md,CI 就跑這個腳本確保 docs/index.md 也跟著更新。shutil.copy(而不是 shutil.move)保留根目錄的 README.md,這樣 GitHub repo 首頁仍能看到入口。

cd de-journey
uv run python scripts/doc_init.py
uv pip install mkdocs==1.6.1 mkdocs-material==9.6.0
uv run mkdocs serve    # 本機預覽:http://127.0.0.1:8000

輸出(啟動後的 banner):

INFO    -  Building documentation...
INFO    -  Documentation built in 0.42 seconds
INFO    -  Serving on http://127.0.0.1:8000

這段把整份 docs/ 目錄建立起來。MkDocs Material 主題自帶全文搜尋(用 lunr.js)、深色模式、可折疊導覽列,是 2025 年最受歡迎的技術文件主題之一。mkdocs serve 是本機預覽模式,改 Markdown 後瀏覽器自動重整。正式上線用 mkdocs build 產出 site/ 目錄,再丟到 GitHub Pages。

設定檔 mkdocs.yml 是 MkDocs 的核心,我們把導覽列、搜尋、主題顏色都寫進去:

# de-journey/mkdocs.yml
site_name: de-journey 文件站
site_description: 政府開放資料每日管線的技術文件
repo_url: https://example.com/your-org/de-journey
repo_directory: .

theme:
  name: material
  palette:
    - scheme: default
      primary: indigo
      accent: indigo
  features:
    - navigation.instant
    - navigation.tracking
    - search.highlight
    - search.suggest

nav:
  - 首頁: index.md
  - 架構總覽: architecture.md
  - 維運手冊: runbook.md
  - 決策紀錄: decisions.md

markdown_extensions:
  - admonition
  - codehilite
  - toc:
      permalink: true

這個設定檔把網站的導覽結構、搜尋行為、主題顏色都寫死。nav 段落決定左邊欄的順序,新增文件時記得在這裡加一行。markdown_extensions 啟用 admonition(提示框)、codehilite(程式碼區塊著色)、toc(標題錨點),這三個是技術文件最常用的擴充。repo_url 設成你的實際 git repo 位址,MkDocs 會自動在右上角顯示「編輯此頁」按鈕。

# 把文件發布到 GitHub Pages(CI 也會跑這個指令)
uv run mkdocs gh-deploy --force
# 輸出:
# INFO    -  Building documentation...
# INFO    -  Documentation built in 1.18 seconds
# INFO    -  Copying '/path/to/site' to 'gh-pages' branch...
# INFO    -  Branch 'gh-pages' pushed to remote.

mkdocs gh-deploy 把 site/ 目錄推到 gh-pages 分支,GitHub Pages 會自動部署。每次 main 分支更新時,Day 44 會在 GitHub Actions 加上 mkdocs gh-deploy 步驟,文件站就會自動保持最新。

文件站建好後,我們寫一支「文件醫生」腳本,定期掃描文件裡的指令是否還能跑。這個腳本會把 Markdown 裡的 bash 程式碼區塊抓出來,對幾個關鍵指令做實際執行驗證:

"""de-journey/scripts/doc_doctor.py:掃描文件裡的關鍵指令,確保仍可執行。"""
from __future__ import annotations

import re
import subprocess
from pathlib import Path

DOCS = Path(__file__).resolve().parents[1] / "docs"
CHECKS = [
    ("uv --version", "確認 uv 仍在 PATH"),
    ("python --version", "確認 python 連結正確"),
    ("duckdb --version", "確認 duckdb CLI 已安裝"),
]

for md_file in sorted(DOCS.glob("*.md")):
    text = md_file.read_text(encoding="utf-8")
    blocks = re.findall(r"```bash\s*\n(.*?)```", text, flags=re.DOTALL)
    if not blocks:
        continue
    print(f"== {md_file.name}:{len(blocks)} 個 bash 區塊 ==")
    for cmd, note in CHECKS:
        try:
            out = subprocess.run(
                cmd, shell=True, capture_output=True, text=True, timeout=10,
                check=False,
            )
            status = "OK" if out.returncode == 0 else "FAIL"
        except subprocess.TimeoutExpired:
            status = "TIMEOUT"
        print(f"  [{status}] {cmd}({note})")

輸出範例(實際輸出會略有不同):

== architecture.md:5 個 bash 區塊 ==
  [OK] uv --version(確認 uv 仍在 PATH)
  [OK] python --version(確認 python 連結正確)
  [FAIL] duckdb --version(確認 duckdb CLI 已安裝)
== runbook.md:3 個 bash 區塊 ==
  [OK] uv --version(確認 uv 仍在 PATH)
  [OK] python --version(確認 python 連結正確)
  [OK] duckdb --version(確認 duckdb CLI 已安裝)

這個腳本是「文件可被驗證」的具體實作。當 uv 或 duckdb 升級、原本文件裡的指令不再能跑時,這支腳本會在 CI 跳出紅燈,提醒我們更新文件。實務上這支腳本會在 Day 44 部署到 GitHub Actions 的 docs_check 工作裡,每天跑一次。

RUNBOOK:值班手冊

RUNBOOK 是給「半夜被打來處理問題的人」看的手冊。一份好的 RUNBOOK 應該把每一種已知失敗情境的「症狀 → 排查 → 處理」流程都列出來。當問題不在 RUNBOOK 的清單裡時,至少要寫「該找誰問」。我們用 Markdown 範例展示這個結構:

# RUNBOOK

## 值班輪值
本週輪值:見團隊 Wiki 的 On-call 頁面。
非值班時段:寫信到 data-platform@example.com,
            SLA 為 4 小時內回應。

## 場景 1:dbt run 失敗
症狀:daily_run 在 dbt run 階段跳出非零 exit code。
排查:
1. 看 `logs/daily_run_*.log` 找失敗的模型名稱。
2. 進入 models/ 對應目錄,用 `uv run dbt run --select <model>` 重跑一次。
3. 若仍失敗,把錯誤訊息貼到 #data-platform Slack。
處理:
- 若單一模型失敗:先看是上游缺資料還是該模型 SQL 寫錯。
- 若整批失敗:通常是 DuckDB 連線問題,重啟 DuckDB 檔案。

## 場景 2:合約違規
症狀:logs/contract_breaches.jsonl 出現新違規。
排查:
1. 跑 `uv run python scripts/check_contract.py --table <table>`。
2. 看違規類型(completeness / uniqueness / range / enum)。
3. 對違規列抽樣看原始資料。
處理:
- completeness:補資料或修合約門檻。
- uniqueness:清掉重複(通常是上游重發)。
- range / enum:修資料或修合約欄位。

## 場景 3:GitHub Actions 沒跑
症狀:每日 03:00 沒看到新的 daily_run 工作。
排查:
1. 看 GitHub repo 的 Actions 頁面,是否有失敗的工作。
2. 看 cron 排程是否被 disable(Settings → Actions)。
處理:
- 若 workflow 失敗:點進去看錯誤訊息。
- 若 cron 被 disable:請 Owner 重新啟用。

這個 RUNBOOK 有三個關鍵設計。第一,每個場景都分成「症狀、排查、處理」三段,讓值班的人不用思考下一步該做什麼,照著走就好。第二,「症狀」這段用讀者會看到的訊息(log 訊息、檔案名稱、Slack 通知)來描述,避免值班的人要在腦中翻譯「我看到的訊息是哪個場景」。第三,「非值班時段」這段明確寫 SLA(4 小時內回應),讓接手的人有預期。

我們寫一支「場景覆蓋率」腳本,定期掃描 logs/daily_run_*.log 的錯誤訊息,與 RUNBOOK 的場景比對,找出「文件裡沒寫、卻常常發生」的失敗模式:

"""de-journey/scripts/runbook_coverage.py:比對 log 錯誤與 RUNBOOK 場景。"""
from __future__ import annotations

import re
from collections import Counter
from pathlib import Path

LOG_DIR = Path(__file__).resolve().parents[1] / "logs"
RUNBOOK = Path(__file__).resolve().parents[1] / "docs" / "runbook.md"

# 1. 從 RUNBOOK 抓出所有「場景 N:」開頭的場景名稱
scenes = set(re.findall(r"場景\s*\d+[::]\s*([^\n]+)", RUNBOOK.read_text(encoding="utf-8")))
print(f"RUNBOOK 已有 {len(scenes)} 個場景:")
for s in sorted(scenes):
    print(f"  - {s}")

# 2. 從最近 7 天的 log 抓出所有錯誤訊息
errors = Counter()
for log in LOG_DIR.glob("daily_run_*.log"):
    for line in log.read_text(encoding="utf-8").splitlines():
        if "ERROR" in line or "Traceback" in line:
            key = line.split(":")[0][:40]
            errors[key] += 1

# 3. 找出 log 有但 RUNBOOK 沒有的錯誤
unmapped = [k for k in errors if not any(s[:6] in k for s in scenes)]
print(f"\nlog 出現 {len(errors)} 種錯誤,其中 {len(unmapped)} 種尚未寫進 RUNBOOK")
for u in unmapped[:5]:
    print(f"  - {u}(出現 {errors[u]} 次,建議補進 RUNBOOK)")

這支腳本做的事很單純:把「文件裡列出的場景」與「log 裡出現的錯誤」做交集,找出差異。差異越大,代表文件過期得越嚴重。實務上這支腳本會在 Day 43 的評估儀表板裡露出一個小圖,提示「RUNBOOK 還有 N 個場景需要補」。

DECISIONS:決策紀錄

DECISIONS(Architecture Decision Records, ADR)是給「想改設計的人」看的歷史紀錄。每篇 ADR 記錄一次重大決策:當時面臨什麼選項、為什麼選這個、付出的代價是什麼。我們用 Markdown 範例展示這個結構:

# ADR-0001:選擇 dbt-core + dbt-duckdb 做轉換建模

## 狀態
已接受(2025-11-25)

## 脈絡
Day 25 之前,管線的轉換是用 Python + DuckDB 直接寫 SQL 檔案。
缺點是模型相依關係靠人工維護、測試要自己寫、文件要自己生。
當模型數量超過 10 個時,管理成本急劇上升。

## 選項
- A. 繼續用 Python + DuckDB,自己寫工具。
- B. 引入 dbt-core + dbt-duckdb。
- C. 引入 SQLMesh 或 dbt 2.0(當時仍在 beta)。

## 決定
採用 B(dbt-core 1.10 + dbt-duckdb 1.10)。

## 後果
正面:
- 模型相依關係由 dbt 自動管理。
- 內建測試框架(unique、not_null、relationships、accepted_values)。
- dbt docs 自動產生資料字典。
- 社群支援活躍,文件完整。

負面:
- 學習曲線:團隊需要學 Jinja 與 dbt 的 SELECT 風格。
- 增加一個相依(dbt-core + dbt-duckdb)。
- 對小型模型來說「殺雞用牛刀」。

## 替代方案
若未來模型數量減少到 5 個以下,可以重新評估是否改回 A。

這個 ADR 有幾個關鍵設計。第一,「狀態」段標出決策生效日期,方便讀者判斷這個決策是「最近」還是「很久以前」。第二,「選項」段把當時考慮的所有方案列出,讀者才知道「為什麼不是用 X」。第三,「後果」段同時寫正面與負面,避免「事後諸葛」式的美化。

把 ADR 集中放在 docs/decisions.md 或 docs/decisions/0001-xxx.md 目錄裡,每個決策一個檔案,永遠不刪除(即使決策後來被推翻,也保留原檔並標記 status: superseded)。這是給未來的自己最大的禮物:當有人問「為什麼當初用 dbt 而不是 SQLMesh」,你能直接指向 ADR-0001。

為了讓 ADR 不會被「忘記補狀態」,我們寫一支「ADR 格式檢查」腳本掃描 decisions 目錄,確認每篇都符合基本 schema:

"""de-journey/scripts/check_adrs.py:確認 decisions 目錄的 ADR 都有必要段落。"""
from __future__ import annotations

import re
from pathlib import Path

DECISIONS_DIR = Path(__file__).resolve().parents[1] / "docs" / "decisions"
REQUIRED = ["## 狀態", "## 脈絡", "## 選項", "## 決定", "## 後果"]

if not DECISIONS_DIR.exists():
    print(f"{DECISIONS_DIR} 尚未建立,先用 mkdir -p 建立")
else:
    for adr in sorted(DECISIONS_DIR.glob("*.md")):
        text = adr.read_text(encoding="utf-8")
        missing = [s for s in REQUIRED if s not in text]
        if missing:
            print(f"[FAIL] {adr.name} 缺少:{', '.join(missing)}")
        else:
            print(f"[OK] {adr.name}")

這支腳本確保每篇 ADR 都有「狀態、脈絡、選項、決定、後果」五個必要段落,缺一段就會在 CI 跳出紅燈。實務上可以更嚴格:要求每篇都要引用至少一個 PR 或 issue,或要求「後果」段落不能少於 50 字。這些規則用 GitHub Actions 的 workflow 跑,效果就是把 ADR 的格式檢查自動化。

PR 模板:把文件檢查嵌進流程

前面提到的「PR template」是把文件檢查嵌進開發流程的低成本做法。我們在 .github/PULL_REQUEST_TEMPLATE.md 寫一份檢查清單,要求每個 PR 都要勾選對應的文件段落是否需要更新:

"""de-journey/scripts/render_pr_template.py:把文件檢查清單渲染成 Markdown。"""
from __future__ import annotations

SECTIONS = [
    ("README.md", "改了一般資訊、套件版本、執行指令?"),
    ("docs/architecture.md", "改了資料流、模型、排程?"),
    ("docs/runbook.md", "新增或修改失敗場景?"),
    ("docs/decisions/0001-xxx.md", "改了過去的 ADR 決策?"),
    ("docs/decisions/ 新檔", "新增了重大決策需要記錄?"),
]

print("## 文件檢查清單\n")
print("請勾選這次 PR 影響的文件段落(若都不需要也要勾「無」):\n")
for path, q in SECTIONS:
    print(f"- [ ] **{path}** — {q}")
print("- [ ] 無(這次 PR 不影響任何文件)")

輸出:

## 文件檢查清單

請勾選這次 PR 影響的文件段落(若都不需要也要勾「無」):

- [ ] **README.md** — 改了一般資訊、套件版本、執行指令?
- [ ] **docs/architecture.md** — 改了資料流、模型、排程?
- [ ] **docs/runbook.md** — 新增或修改失敗場景?
- [ ] **docs/decisions/0001-xxx.md** — 改了過去的 ADR 決策?
- [ ] **docs/decisions/ 新檔** — 新增了重大決策需要記錄?
- [ ] 無(這次 PR 不影響任何文件)

這段把「文件檢查清單」用 Python 模板渲染出來,再用 GitHub 的 PR template 機制自動套到每個新 PR。實務上你會把這段輸出直接寫進 .github/PULL_REQUEST_TEMPLATE.md 檔,PR 開起來就會自動帶出這個清單,迫使 PR 作者思考「我這次改的東西,文件要不要同步更新」。

除了 PR template,再寫一支「連結檢查」腳本掃描文件站內部連結是否 404。這支腳本在 Day 44 部署到 GitHub Actions 後會每天跑,避免文件裡的內部連結在重新命名後變成死鏈:

"""de-journey/scripts/check_links.py:掃描 docs/ 內部 Markdown 連結是否都有對應檔案。"""
from __future__ import annotations

import re
from pathlib import Path

DOCS = Path(__file__).resolve().parents[1] / "docs"
LINK_RE = re.compile(r"\[[^\]]+\]\((?!https?://)([^)]+)\)")

broken = []
for md_file in sorted(DOCS.rglob("*.md")):
    text = md_file.read_text(encoding="utf-8")
    for link in LINK_RE.findall(text):
        target = (md_file.parent / link.split("#")[0]).resolve()
        if not target.exists():
            broken.append((md_file.relative_to(DOCS), link))

if broken:
    print(f"發現 {len(broken)} 個失效連結:")
    for src, link in broken[:10]:
        print(f"  {src} -> {link}")
else:
    print("所有內部連結都正常")

這支腳本只檢查「內部」連結(也就是以相對路徑而非 http(s) 開頭的),對外部連結不做檢查——外部連結可能暫時 404,但不一定是錯的。輸出範例:當有人把 architecture.md 重新命名成 arch.md 但忘了更新其他文件的連結時,這支腳本會印出失效的連結清單。

常見錯誤與踩雷

第一個雷:文件散落各地(README 在 GitHub、ARCHITECTURE 在 Wiki、RUNBOOK 在 Slack)。讀者要花時間找,且沒有搜尋。請一律把文件集中在一個靜態網站(MkDocs、Docusaurus、VitePress 都可),用 git 版本化。

第二個雷:文件寫完後沒人更新。文件過期比沒有文件更糟,會誤導接手的人。請把「更新文件」放進每個 PR 的檢查清單(PR template),並在 CI 加一個簡單的檢查:grep README 看有沒有引用過期的指令或套件版本。

第三個雷:把「為什麼」寫得太空泛。範例:「我們選擇 DuckDB 因為它很快」。這句話對讀者毫無幫助。好的寫法是「我們選擇 DuckDB 因為 (1) 1.4 世代支援直接查 Parquet 檔,省下 ETL 中介層;(2) CPU 單機效能比 SQLite 快 5–10 倍;(3) 與 dbt-duckdb 1.10 整合最佳」。三個具體原因比一句模糊形容詞有資訊密度得多。

第四個雷:RUNBOOK 只列「快樂路徑」。RUNBOOK 的目的是處理失敗,但很多人寫 RUNBOOK 只寫「正常流程怎麼跑」。請把每一個你曾經 debug 過的失敗都寫進 RUNBOOK:症狀、排查、處理步驟。這些都是未來你或同事最需要的內容。

第五個雷:文件用「我們」當主詞,卻沒有指明「我們是誰」。當文件讀者不知道作者是誰、組織在哪裡、文件何時寫的,這份文件就難以信任。請在每份文件的開頭明確標示作者、日期、組織,這是「可驗證」的第一步。

效能與實務提醒

MkDocs Material 主題的靜態網站通常小於 5 MB,部署到 GitHub Pages 完全免費(公開 repo)或便宜(私有 repo)。整個 site/ 目錄可以被 CDN 快取,載入時間在數百毫秒內。

實務上有三個取捨值得記得。第一,文件深度:一份好文件的長度通常在 10–50 頁 Markdown 之間;太短不夠具體、太長讀者會跳著讀找重點。第二,圖 vs 文字:技術細節盡量用文字(可 grep、可搜尋),高層次的資料流用 ASCII 或 Mermaid。第三,搜尋友善:每個段落都有一個明確的標題、不要用「其他」「雜項」這種模糊詞當標題,這樣全文搜尋才能命中。

另一個工程上的提醒:把文件的「驗證指令」放進 CI。例如 README 寫「跑 bash scripts/bootstrap.sh」,CI 就跑這個指令並把結果截圖存到 docs/screenshots/。這樣讀者一眼就知道「這個指令可以跑」,不會被過期的命令誤導。

小結

今天把文件化與交接這條線建立起來。我們把 README、ARCHITECTURE、RUNBOOK、DECISIONS 四份文件用 MkDocs Material 集中成一個可搜尋的靜態網站,並展示了如何用 mkdocs gh-deploy 部署到 GitHub Pages。重點觀念有三個。第一,文件要可以被搜尋、被版本化、被驗證,三者缺一不可。第二,文件要回答「做什麼、怎麼做、壞了怎麼辦、為什麼這樣做」四個問題,分別對應 README、ARCHITECTURE、RUNBOOK、DECISIONS。第三,文件更新要進入 PR 與 CI 流程,避免過期。

這套文件結構會跟著 Day 41 之後的專案一起演進。當我們加入新的模型、新的排程、新的監控指標時,相關段落要同步更新。明天,我們會正式進入專案篇,從「專案定義與資料模型」開始,把 Day 1–40 的所有工具與觀念組合成一個可以每天自動運行的真實管線。

結語

今天的重點是「讓管線活得比作者久」。我們用四份文件(README、ARCHITECTURE、RUNBOOK、DECISIONS)把整套管線的設計與操作流程寫下來,並用 MkDocs 把它們集中成一個可搜尋的靜態網站。讀完這篇你應該能回答:為什麼需要四份文件而不是一份?怎麼用 MkDocs 把 Markdown 變成可搜尋的網站?怎麼寫一份有用的 RUNBOOK?明天,我們會正式進入專案篇,從「專案定義與資料模型」開始,把過去 40 天的所有工具與觀念組合成一個真實可運行的管線。

延伸資源

  • MkDocs Material 文件(9.6 版,2025):https://squidfunk.github.io/mkdocs-material/。MkDocs Material 是 2025 年最受歡迎的技術文件主題,自帶全文搜尋、深色模式與響應式設計。
  • Architecture Decision Records 概念介紹(Michael Nygard,2011):https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions。ADR 的原始文章,至今仍是業界標準。
  • Google SRE Book「Runbook」章節:https://sre.google/sre-book/runbook/。RUNBOOK 的設計原則與範例。
  • Diátaxis 文件框架(2025):https://diataxis.fr/。把文件分成「教學、指引、參考、解釋」四類,與本篇的四層文件結構互補。
  • GitHub Pages 官方文件:https://docs.github.com/en/pages。gh-pages 分支的部署流程。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...