Day 05:工程基礎最小集合——環境、Git、容器、金鑰
「在我電腦上明明跑得起來啊。」
這句熟悉的對話日常,在 ML 系統會是場災難,因為 ML 多了兩個變數:資料與模型權重。第一部曲我們將 AI 職缺目前在市場的需求進行說明,第二部曲聚焦在地基,今天要做的,就是降低只能回答出「在我電腦上明明可以」的風險。
今天要解決的問題
以下三個問題都很常見:環境、版控與金鑰,不處 理會卡住:
問題一:環境不可重現。 去年執行 pip install pandas 裝到的是 2.x,今天重新建立環境可能已經裝到 3.x。同一份程式碼、一樣呼叫 groupby(),光是套件預設行為改變,輸出就可能不同。程式碼能重跑,不代表結果能重現。 [註1]
問題二:Git 用得太感人。 把 500MB 的模型檔 commit 進去、把 data/ 整包推上 GitHub、或更糟——把 .env 推上去了。
問題三:金鑰外洩。 這件事沒有「小心一點就好」,因為 GitHub 是被機器人 24 小時掃描的。推上去的那一刻就要當作已經外洩,改密碼是唯一解,刪 commit 沒有用。
📊 職缺訊號
這篇對應的不是單一任務訊號,而是所有任務的前提。來自 4,343 職缺之中梳理了實際的數字:MLOps 職缺中 58.5% 提到平臺與基礎設施、42.6% 提到訓練管線與 CI/CD,這兩件事都建立在「環境可重現」之上。
一、現象:三個你一定遇過的坑
坑一:requirements.txt 沒有鎖版本。
pandas
scikit-learn
正確做法是鎖住版本,並區分「直接依賴」與「完整鎖定」,專案實際主動使用的套件、間接相依套件版本鎖住,確保不同時間、不同機器都能建立出一致的執行環境。
坑二:Dockerfile 分層順序寫反。 這個坑不會讓你的程式出錯,只會默默偷走你的時間:

圖 5-1:Dockerfile 分層順序對重建時間的影響(依 Docker 快取失效規則推算的示範值)。左圖是單次重建,右圖是一天 30 次建置的累積差距,約 48 分鐘。
原理是 Docker 的逐層快取:任何一層變動,其後所有層的快取全部失效。如果你寫 COPY . /app 再 RUN pip install,那麼改一行程式碼就會讓依賴安裝層失效,每次都重裝一次。
坑三:金鑰進了版本控制。 反覆提醒自己:程式碼裡永遠不出現金鑰字串。
二、原理:可重現=程式碼+依賴+系統+資料+種子
「環境」不只是套件。一個 ML 工作可重現,需要五件事同時被固定:
| 要素 | 用什麼固定 | 沒固 定會怎樣 |
|---|---|---|
| 程式碼 | Git commit | 不知道跑的是哪一版 |
| 依賴套件 | uv.lock / requirements.lock | 套件改版導致行為改變 |
| 系統層(CUDA、glibc) | Docker 映像 | 「我這台有 GPU 你那台沒有」 |
| 資料 | DVC 或資料版本標記(Day 09) | 同一份程式碼跑出不同結果 |
| 隨機性 | 固定 seed | 每次訓練結果都不同,無法比較 |
前三項是今天說明的範圍,後兩項在 Day 09 與 Day 10。 這五項缺一就難以達到「可重現」,而可重現是 Day 04 「不能省的三件事」之首。
三、動手:四十行搞定的最小工作環境
3.1 Python 環境(用 uv,比 pip 快一個量級)
# 安裝 uv(Rust 寫的 Python 套件管理器)
curl -LsSf https://astral.sh/uv/install.sh | sh
uv init my-ai-service && cd my-ai-service
uv add fastapi uvicorn scikit-learn pandas # 會自動產生並更新 uv.lock
uv add --dev pytest ruff # 開發依賴分開管理
uv run pytest # 在鎖定環境中執行
uv.lock 必須進版控——它記錄了完整依賴樹的精確版本,是「可重現」的第二根柱子。
3.2 ML 專案的 .gitignore
一般範本不夠用,ML 專案要多擋三類東西:
# ── 一般 Python ──
__pycache__/
.venv/
*.egg-info/
# ── ML 專案特有:大檔絕不進 Git ──
data/ # 資料改用 DVC 管理(Day 09)
*.pkl # 模型檔改用 MLflow / 物件儲存(Day 10)
*.pt
*.safetensors
mlruns/ # MLflow 本地紀錄
.ipynb_checkpoints/
# ── 金鑰:這一段最重要 ──
.env
.env.*
!.env.example # 只有「範本」可以進版控
*.pem
*credentials*.json
3.3 金鑰管理:.env.example 模式
# .env.example —— 進版控,只有鍵沒有值,讓別人知道要準備什麼
OPENAI_API_KEY=
DATABASE_URL=
MODEL_REGISTRY_URI=
# app/config.py —— 程式碼裡只讀環境變數,且缺少時「大聲失敗」
import os
def require(key: str) -> str:
"""取得必要的環境變數,缺少時立刻失敗——不要讓服務帶著空金鑰啟動。"""
val = os.environ.get(key)
if not val:
raise RuntimeError(f"缺少必要環境變數 {key};請參考 .env.example 設定")
return val
OPENAI_API_KEY = require("OPENAI_API_KEY")
⚠️ 如果你已經把金鑰推上去了
步驟只建議一個順序:1. 立刻到服務商後台撤銷該金鑰 → 2. 產生新金鑰 → 3. 才處理 Git 歷史。 很多人順序做反先清 Git 歷史,但就算只晚 10 分鐘撤銷,那把金鑰也早就被掃走了。
3.4 第一個正確的 Dockerfile
# ── 建置階段:裝依賴,這一層要能被快取 ──
FROM python:3.11-slim AS builder
WORKDIR /app
COPY pyproject.toml uv.lock ./ # ← 只複製依賴描述,程式碼還沒進來
RUN pip install --no-cache-dir uv && uv sync --frozen --no-dev
# ── 執行階段:只帶走執行期需要的東西 ──
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
COPY app/ ./app/ # ← 程式碼最後才複製,改它不影響上面的快取
EXPOSE 8000
# 健康檢查:讓編排器知道服務活著(Day 12 會用到)
HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request;urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
三個重點:依賴描述先複製(快取)、多階段建置(最終映像不含建置工具,體積與攻擊面都小)、永遠不用 ENV 塞金鑰(執行時用 -e 或 K8s Secret 注入)。
docker build -t my-ai-service:0.1.0 .
docker run --rm -p 8000:8000 --env-file .env my-ai-service:0.1.0
把這四步串起來,就是你之後 25 天每天都會用到的開發迴圈:

圖 5-2:本機開發迴圈的示意架構——鎖定依賴、程式碼進版控、金鑰走環境變數、服務跑在容器裡並掛載 volume 供熱重載。這個迴圈在 Day 12 會延伸到 Kubernetes,在 Day 17 會延伸到 LLM 應用。
四、取捨:什麼時候不需要這麼講究
工程紀律有成本,不是每個情境都值得:
| 情境 | 建議 | 理由 |
|---|---|---|
| 探索性分析、一次性報表 | Notebook + venv 就好 | 不會有人重跑,投資報酬率低 |
| 要交給別人跑的分析 | 加上鎖定的依賴清單 | 「別人跑不起來」的成本開始出現 |
| 會上線的服務 | 完整容器化 + 健康檢查 | 沒有例外 |
| 需要 GPU 的訓練 | 容器化,但基底映像換成 CUDA 版 | 系統層依賴(驅動、CUDA)是最難重現的部分 |
| 團隊共用的 Notebook | 考慮改寫成腳本+管線(Day 11) | Notebook 無法 code review、無法自動測試 |
💡 一個實用的判準
問自己:「這份程式碼,三個月後有沒有可能被我或別人再跑一次?」如果答案是可能,現在多花的 30 分鐘會回本。如果不會,就別過度工程。
