iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
AI Engineering

從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰系列 第 5

Day 05:工程基礎最小集合——環境、Git、容器、金鑰

  • 分享至 

  • xImage
  •  

「在我電腦上明明跑得起來啊。」

這句熟悉的對話日常,在 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 分層順序寫反。 這個坑不會讓你的程式出錯,只會默默偷走你的時間

Dockerfile 分層順序對開發迭代速度的影響

圖 5-1:Dockerfile 分層順序對重建時間的影響(依 Docker 快取失效規則推算的示範值)。左圖是單次重建,右圖是一天 30 次建置的累積差距,約 48 分鐘。

原理是 Docker 的逐層快取:任何一層變動,其後所有層的快取全部失效。如果你寫 COPY . /appRUN 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 分鐘會回本。如果不會,就別過度工程。

4.1 關於 Notebook

我知道有人看到「別用 Notebook」會不服氣,所以講清楚:Notebook 沒有錯,錯的是把 Notebook 當成交付物。

Notebook 在探索階段是無可取代的:即時看到中間結果、圖表與資料並排、想到什麼就試什麼。這種互動性在寫死的腳本裡做不到。問題出在它同時具備三個特性——執行順序可以亂跳、狀態藏在記憶體裡、diff 出來是一團 JSON——這三件事讓它無法被 code review、無法自動測試、也無法保證重跑結果一致。

實務上的分界線很清楚:

  • 探索用 Notebook:資料長什麼樣、特徵有沒有訊號、模型大概能到哪。這階段追求速度,不追求嚴謹。
  • 交付用腳本:只要這段程式碼會被排程執行、被別人接手、或被寫進 CI,就必須是腳本(明天的 train.py 就是範例)。
  • 中間地帶:用 jupytext 之類的工具把 Notebook 與 .py 雙向同步,探索與版控兩者兼得。

一個判斷 Notebook 該不該畢業的訊號:當你開始在 Notebook 裡複製貼上同一段程式碼到第三次時,它就該變成模組了。

4.2 這一天的投資報酬

今天的內容看起來離 AI 很遠,但它決定了你後面 25 天的體驗。一個具體的對照:同樣是 Day 12 部署到 Kubernetes 時遇到 Pod 一直重啟,環境有紀律的人 10 分鐘查出是探針逾時設太短;環境沒紀律的人會花兩小時,因為他無法確定「本機能跑、叢集不能跑」的差異究竟來自程式碼、依賴、還是系統層。


今日小結

  • 可重現=程式碼+依賴+系統+資料+隨機種子,五者缺一不可。今天處理前三項。
  • uv 管理環境並把 uv.lock 進版控;ML 專案的 .gitignore 要額外擋資料、模型檔與金鑰。
  • Dockerfile 的分層順序決定快取能不能用:依賴描述先複製、程式碼最後複製,一天可省下近一小時。
  • 金鑰只走環境變數,.env.example 進版控、.env 不進;外洩處理的第一步永遠是撤銷金鑰,不是清 Git 歷史
  • 不是每個情境都要完整工程化——判準是「三個月後會不會有人再跑一次」。

附註

  1. pandas 2.1 已經宣布 DataFrame.groupby() / Series.groupby()observed=False 將在未來改成 True,但到了 pandas 2.3,預設仍然是 False。到了pandas 3.0 改成 True 為預設 。

    import pandas as pd
    
    df = pd.DataFrame({
        "type": pd.Categorical(
            ["A", "A"],
            categories=["A", "B"]
        ),
        "value": [10, 20]
    })
    
    df.groupby("type")["value"].sum()
    

    在舊版預設 observed=False 時,未出現過的 B 也可能出現在結果中:

    type
    A    30
    B     0
    

    而 pandas 3.0 預設改成 observed=True 後,只會留下實際觀察到的群組:

    type
    A    30
    

明天預告

環境有了,明天談 ML 專案本身的形狀:為什麼一個 ML 專案裡,模型程式碼只佔 5%?以及那個吃掉最多專案的斷崖——從「跑出模型」到「上線」。我會用一張漏斗圖說明為什麼多數 AI 專案死在最後一哩。

延伸閱讀


上一篇
Day 04:從職缺任務到學習地圖——您走哪條路?
下一篇
Day 06:ML 生命週期與「筆記本到產品的鴻溝」
系列文
從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言