跳至內容

從零開始學 AI 工程(六):
Phase 0-6 Python Environments

搞懂 venv 隔離、pyproject.toml、lockfile、uv init 跟 uv add 的差別,還有那些跑完才理解的事。

本文作者是 AI 工程的初學者,正透過《AI Engineering from Scratch》課程自學,並全程搭配 AI 輔助:一步步照著課程開發者的進度動手執行,卡關時就向 AI 提問、請它引導。這篇文章,是把作者與 AI 互動的完整過程與學習歷程,先交由 AI 記錄、統整成初稿,再由作者親自逐段審視、修改、優化而成。AI 負責整理,最終判斷與文字由作者把關。

寫下它有兩個用意:替後來的讀者鋪一條能照著走的路徑,也幫作者自己複習、把學過的東西沉澱下來。

這一課跟 Lesson 1 有重疊

第一課 Dev Environment 的時候,我已經建過 .venv、用 uv pip install 裝過套件。這一課又帶我做了一次類似的事情。

差別在於:第一課是「先讓你能跑起來」,這一課才是「回頭教你為什麼要這樣做」。概念的部分之前學過一輪,但動手的部分只做了一半。這次決定全部重來細跑一遍,把每個指令的意思搞清楚。

這一課導覽框

  • 課名:Phase 0 - Lesson 6 — Python Environments
  • 目標:理解 venv 隔離的原理、學會 uv inituv add、搞懂 pyproject.toml 和 lockfile 的分工
  • 時間:課程標約 30 分鐘,我邊理解邊動手大概花了 50 分鐘
  • Mac 使用者:CUDA 相關的內容跳過,Mac 走 MPS 不走 CUDA
  • 完課驗證:跑 env_setup.sh 全部 PASS、理解 pyproject.toml 和 uv.lock 的角色

這一課要解決的問題

課程開頭就說:

Dependency hell is real. Virtual environments are the cure.

依賴地獄是真的。虛擬環境是解藥。

什麼是依賴地獄?你的 A 專案需要 PyTorch 2.4,B 專案需要 PyTorch 2.1。兩個版本不能同時存在於同一個 Python 環境裡,你裝了 2.4,B 就壞了;裝了 2.1,A 就壞了。在 AI/ML 的世界裡,這種事會不斷發生,因為 PyTorch、JAX、TensorFlow 各自綁定不同版本的 CUDA。

解法就是:每個專案給它自己獨立的環境,各裝各的套件,互不干擾。


課程給的三種工具

課程列了三種建 venv 的方式:

工具特色什麼時候用
uv速度最快(比 pip 快 10-100 倍),課程推薦大部分情況
python3 -m venvPython 內建,不用額外裝沒有 uv 的環境
conda能管 Python 以外的東西(CUDA toolkit、C 函式庫)需要特定 CUDA 版本,或在共用伺服器上

我用的是 uv,之前 Lesson 1 就裝好了。

conda 有一條規則:如果你用 conda 建了環境,裡面的套件就全部用 conda 裝,不要混 pip 進去。混了會讓 conda 的依賴追蹤亂掉。


動手:用 uv init 從零建一個專案

課程有兩種動手方式。第一種是 uv venv + uv pip install,在既有的資料夾裡建環境裝套件,我在 Lesson 1 已經做過。第二種是 uv init,從頭建一個全新的專案。

我在課程的 06-python-environments/ 底下建了一個練習用的資料夾:

cd ~/Desktop/Learning_AI/ai-engineering-from-scratch/phases/00-setup-and-tooling/06-python-environments
uv init my-ai-project
cd my-ai-project

uv init 幫我生了三個檔案:

檔案是什麼
pyproject.toml專案的設定檔,記錄名稱、Python 版本需求、需要哪些套件
.python-version記一行 Python 版本號,讓工具知道該用哪版
main.py一個 hello world 範例

注意:uv init 這時候**還沒有建 .venv/**。它只是產生設定檔,還沒有實際的環境。

uv init 不能用在已經有環境的專案

我一開始搞混了,跑到課程資料夾裡(已經有 .venv 的地方)直接打 uv init,結果它在課程目錄裡生了一堆不該出現的檔案。清理了一輪才搞定。

我問 AI:「如果我的既有專案需要 pyproject.toml 呢?」

答案是:uv init 是從零蓋新房子,不是在既有的房子裡加東西。如果你的專案已經有 .venv,只是缺一份 pyproject.toml,直接手動建一個文字檔就好,不需要什麼特別的指令。

整理一下分工:

情境做法
全新專案,什麼都沒有uv init
已有專案,要建或重建環境uv venv + uv pip install
已有專案 + 已有 venv,想補一份 pyproject.toml手動建 pyproject.toml 這個檔案

uv adduv pip install 的差別

建好專案後,課程要我用 uv add 裝套件:

uv add numpy matplotlib

跑完之後輸出了三件事:

Using CPython 3.12.11
Creating virtual environment at: .venv
Installed 11 packages

第二行很關鍵:.venv 是在這一步才建的,不是 uv init 的時候。uv add 發現還沒有環境,就自動幫我建了一個。

我本來以為 uv adduv pip install 一樣,問了 AI 才知道差別:

指令裝進 .venv寫進 pyproject.toml
uv pip install numpy沒有
uv add numpy

uv add 做兩件事:裝套件,同時把套件名稱寫進 pyproject.toml 的 dependencies 清單。uv pip install 只裝,不記。

我的理解是:uv pip install 是買了東西放進冰箱但沒寫購物清單,uv add 是買了東西、同時寫到清單上。我在 Lesson 1 用 uv pip install numpy matplotlib jupyter 裝的那些套件,其實就是「有冰箱但沒清單」的狀態。


pyproject.toml 裡面長什麼樣

uv add 跑完之後,cat pyproject.toml 看到的內容:

[project]
name = "my-ai-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "matplotlib>=3.11.1",
    "numpy>=2.5.1",
]

每個欄位的意思:name 是專案名稱,version 是版本號,requires-python 是最低 Python 版本需求,dependencies 是這個專案需要的套件清單。

注意 dependencies 裡寫的是 >=(大於等於),不是 ==(等於)。代表「至少要這個版本,更新的也行」。

optional-dependencies:看需求才裝的套件

課程介紹了一個進階功能。如果有些套件不是每個人都需要,可以分組放在 optional-dependencies 裡:

[project.optional-dependencies]
torch = ["torch>=2.3", "torchvision>=0.18"]
llm = ["anthropic>=0.39", "openai>=1.50"]

安裝的時候用中括號指定要哪一組:

uv pip install -e ".[torch]"       # 必裝的 + torch 那組
uv pip install -e ".[llm]"        # 必裝的 + llm 那組
uv pip install -e ".[torch,llm]"  # 全部

-e 是 editable 的意思,「可編輯模式」:裝這個專案本身,但改了程式碼不用重裝就生效。. 代表「當前資料夾的這個專案」。

這個設計在 AI 專案裡很常見。PyTorch 本身就要 2GB 以上,如果只是要呼叫 API,根本不需要裝它。分組讓每個人各取所需,寫在 pyproject.toml 裡也讓接手的人看到就知道有哪些選項。

套件名稱不等於專案名稱

寫 pyproject.toml 的時候我問了一個問題:「我能寫 pytorch>= 嗎?」

不行。PyTorch 這個專案的套件名稱是 torch,不是 pytorch。同樣的,Anthropic SDK 的套件名稱是 anthropic,不能寫成 anthropic SDK(套件名不能有空格)。判斷的方法是:你 uv pip install 的時候打什麼名字,pyproject.toml 裡就寫什麼。


Lockfile:為什麼 pyproject.toml 不夠

uv add 跑完之後,資料夾裡除了 pyproject.toml 還多了一個 uv.lock。我 cat uv.lock 看了一下,跑出來一大串東西,記了每一個套件的精確版本號、它依賴誰、從哪裡下載。

pyproject.toml 寫的是 numpy>=2.5.1,代表「2.5.1 以上都行」。但三個月後 numpy 出了新版,別人照你的清單裝就會裝到新版。如果新版有什麼改動讓你的程式壞掉,你就有問題了。lockfile 把每個套件釘死在精確版本,保證任何人任何時候照它裝,都跟你一模一樣。

我問 AI:「如果 lockfile 就能精確重建環境,那為什麼還需要 pyproject.toml?」

答案是:它們是給不同對象看的。

pyproject.toml 是給人看的。你打開就知道「這個專案用了 numpy 和 matplotlib」,要加套件、改版本範圍,改的是這個檔案。uv.lock 是給機器看的,你跑 uv add 它自動更新,人類不會去手動編輯它。

更根本的原因是:lockfile 是算出來的結果,pyproject.toml 是算的輸入。沒有輸入就沒辦法重算。如果你想放寬 numpy 的版本範圍,或加一個新套件,你改的是 pyproject.toml,然後 uv 根據它重新算出新的 lockfile。光有 lockfile,你連哪些是你主動裝的、哪些是被拖進來的依賴都分不出來。

我想了一下,覺得食譜跟收據的類比很好懂:pyproject.toml 是食譜,lockfile 是這次買菜的收據。你不能只留收據不留食譜,下次想換個牌子的材料,得改食譜重算。

三個檔案各自的角色

檔案給誰看要不要 commit 進 git
pyproject.toml
uv.lock機器(uv 自動生成)
.venv/你自己的電腦不要(太大、路徑寫死、每個人自己建)

常見錯誤:課程列了五個

裝到全域

沒有 activate venv 就 pip install,套件會裝到系統 Python 裡,所有專案共用,就回到了打架的狀態。

檢查方式是 which python。如果顯示 .venv/bin/python,你在 venv 裡;如果顯示 /usr/bin/python,你在系統全域。

pip 和 conda 混用

如果用 conda 建的環境,裡面的套件就全部用 conda 裝。混 pip 進去會讓 conda 的依賴追蹤亂掉。我目前沒有用 conda,這條先記著。

忘記 activate

我跑 which python3 的時候看到 /usr/bin/python3(系統的 Python 3.9.6),才發現自己忘了 activate。啟動之後路徑變成 .venv/bin/python,提示列前面也多了 (my-ai-project)

source .venv/bin/activate 逐字拆開來看:source 是把檔案的內容讀進當前 shell 執行,.venv/bin/ 是 venv 裡放可執行檔的地方(bin 是 binary 的縮寫),activate 是一個 shell 腳本,它做的事就是改你的 PATH 環境變數,讓 python 這個字優先指向 .venv/bin/python

source 而不是 bash 來跑 activate,是因為 bash 會開一個新的子 shell,跑完就關掉,你現在的 shell 不會改變。source 是直接改你正在用的 shell。

把 .venv commit 進 git

.venv/ 通常 200MB 到 2GB,而且裡面的路徑是寫死的(之前 Lesson 1 學過 venv 不能搬家),不應該進 git。在 .gitignore 裡加一行 .venv/ 就好。

加的指令是 echo ".venv/" >> .gitignore>> 是附加到檔案尾端,不會蓋掉原本的內容。如果用 > 只有一個箭頭,會清空整個檔案再寫入。

CUDA 版本不匹配

PyTorch 編譯時會綁定特定的 CUDA 版本,如果跟你電腦 GPU driver 的版本對不上,就會裝了 PyTorch 卻用不了 GPU。我的 Mac 沒有 NVIDIA 顯卡,走 MPS,不會遇到這個問題。


跑 env_setup.sh

課程的 Use It 段落要你跑一個腳本:

bash phases/00-setup-and-tooling/06-python-environments/code/env_setup.sh

這個腳本做的事情是:檢查有沒有 uv → 檢查 Python 版本 → 建 .venv(已有就沿用) → 啟動 venv → 裝 core 套件(numpy、matplotlib、jupyter、scikit-learn、pandas)→ 逐一驗證 → 跑一個 NumPy 矩陣乘法確認正常 → 順便看有沒有 PyTorch。

我第一次跑的時候失敗了。腳本說 Python 3.11+ not found,因為它先檢查 Python 版本再建 venv,而我的系統全域 Python 是 Mac 內建的 3.9.6。解法是先 source .venv/bin/activate,讓 shell 指向 venv 裡的 Python 3.12,再跑腳本。

第二次就全部 PASS 了:

[PASS] uv found: uv 0.10.4
[PASS] Python: Python 3.12.11
[PASS] All checks passed

腳本也幫我補裝了之前沒有的 pandasscikit-learnscipy 等套件。


四個關鍵詞

術語一般怎麼講白話意思
Virtual environment「a venv」一個獨立的資料夾,裡面有自己的 Python 和套件,跟系統 Python 分開
Lockfile「pinned dependencies」(釘死的依賴)記錄每個套件精確版本的檔案,讓任何人都能裝到一模一樣的環境
pyproject.toml「the new setup.py」Python 專案的標準設定檔,取代了以前的 setup.py、setup.cfg、requirements.txt
Transitive dependency「a dependency of a dependency」(依賴的依賴)你裝 matplotlib,它背後需要 pillow、fonttools 等,這些你沒親手裝的就是 transitive dependency

走到這裡才會知道的事

uv init 不帶名字會在當前目錄直接生檔案。 我一開始在課程資料夾裡直接打 uv init,結果它在那裡生了 pyproject.tomlmain.py.python-version,跟課程原本的檔案混在一起。清理了一輪才搞定。帶上名字 uv init my-ai-project 就會建一個子資料夾,不會污染外面。

env_setup.sh 在 activate venv 之前跑會失敗。 腳本的邏輯是先檢查 Python 版本,但 Mac 系統的 Python 是 3.9.6,不到它要求的 3.11+。先手動 source .venv/bin/activate 切到 venv 裡的 3.12,再跑腳本就行了。

這一課跟 Lesson 1 內容有重疊。 建 venv、裝套件、activate,兩課都做了。作者的設計是 Lesson 1 先讓你跑起來,Lesson 6 再教背後的原理和進階用法(pyproject.toml、lockfile、optional-dependencies)。如果你已經照 Lesson 1 建好了 .venv,這課的練習可以建在別的地方(像我建在 06-python-environments/my-ai-project/ 裡),練完刪掉就好。

uv add 時如果沒有 .venv/ 會自動建一個。 不需要先跑 uv venvuv add 發現沒有環境,就自己處理了。但心裡要清楚哪一步做了什麼,不要把 uv inituv add 的功能混在一起。