iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
佛心分享-IT 人技術創業

Berry AI:從零開始打造全美第一的得來速 Vision AI系列 第 28 篇

人類和 AI Agent 都用得順手的 Python 開發環境:uv、ruff、ty 與 prek

  • 分享至 

  • xImage
  •  

系列的前 27 天談的是系統本身,本篇談開發這些系統的環境,也就是開發者體驗 (DevEx, developer experience)。Berry AI 的主力語言是 Python:從 ML 的推理管線 (inference pipeline)、資料工程的 ETL、後端服務,到上千台機器的部署自動化。GitHub organization 裡的 repo 超過一百個,其中過半是 Python 專案,分屬 Day 03 介紹過的四個團隊。工程師跨團隊支援是常態,若每個 repo 的環境建置方式、lint 規則、檢查指令各自為政,每次支援都要重新熟悉一套流程。因此我們將工具鏈收斂為少數幾個工具,本篇逐一介紹,文末以這些工具從零建立一個專案範本 (project template)。

同一套檢查會在三個時機執行,越早執行回饋越快。三個時機用的都是 uv 依鎖檔 (lockfile) 還原的同一個環境:

時機 執行方式 檢查內容 回饋速度
編輯器存檔 編輯器整合 ruff format、ruff check --fix 毫秒
git commit prek hooks ruff、ty、uv lock --check、雜項 hooks 秒
push 之後 GitHub Actions uv sync --locked、prek run --all-files、pytest 分鐘

uv:直譯器、虛擬環境與相依套件的單一入口

過去建置 Python 開發環境需要組合多個工具:pyenv 管理直譯器 (interpreter) 版本、venv 建立虛擬環境 (virtual environment)、pip 安裝套件、pip-tools 鎖定版本。這個組合有幾個長期痛點。pyenv 安裝直譯器要在本機編譯,耗時,而且要先裝好系統套件;pip 解析相依套件慢,手寫的 requirements.txt 又不記錄間接相依套件 (transitive dependency) 的版本,環境會隨時間漂移 (drift)。改用 pip-tools 編譯鎖檔可以解決漂移,但它的輸出只對編譯當下的環境有效,官方文件要求在每個目標環境 (作業系統、Python 版本與實作的組合) 各編一份。流程還分散在多個工具與設定檔上,新成員照 README 逐步操作,漏一步就會得到不一致的環境。

uv 是 Astral 以 Rust 開發的套件與專案管理器,以單一工具涵蓋上述全部功能,並逐一處理這些痛點:直譯器改用預先編譯的 CPython,下載即可使用;解析與安裝相依套件比 pip 快一到兩個數量級,全域快取 (global cache) 讓重建 .venv 不必重新下載與建置套件;uv.lock 是跨平台鎖檔,間接相依套件與 hash 一併鎖定。在任一 repo 執行 uv sync,它會讀取 .python-version 決定直譯器版本 (本機沒有時自動下載)、建立 .venv,再依 uv.lock 安裝出與所有同事一致的環境,包含 ruff、ty 這些開發工具本身的版本。建置環境從一頁 README 收斂成一個指令。

相依套件以 PEP 735 的 dependency groups 分組,以下取自後端服務的設定:

[dependency-groups]
dev   = [{ include-group = "check" }, { include-group = "test" }, "ipython>=9.15"]
check = ["prek>=0.5", "ruff>=0.16", "ty>=0.0.80"]
test  = ["pytest>=9.0", "pytest-xdist>=3.8.0"]
ci    = [{ include-group = "check" }, { include-group = "test" }]

開發者安裝預設的 dev,CI 用 uv sync --no-dev --group ci 安裝檢查與測試所需的最小集合,部署映像則一個 group 都不裝。--group 是疊加的,少了 --no-dev,預設的 dev 會被一起裝進來。另一個常用的進階功能是 [tool.uv.sources],可將個別套件指向 git repo 或指定的套件索引 (index)。ML 團隊的專案以它將 PyTorch 指向 CUDA 專用的 index:

[[tool.uv.index]]
name = "pytorch-cu128"
url = "https://download.pytorch.org/whl/cu128"
explicit = true # 只用於明確指定的套件

[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

[tool.uv.sources]
torch = [
    { index = "pytorch-cu128", marker = "sys_platform == 'linux'" },
    { index = "pytorch-cpu", marker = "sys_platform == 'darwin'" },
]

同一份 pyproject.toml,GPU 伺服器解析到 CUDA 版的 wheel,macOS 取得內建 MPS (Metal Performance Shaders,Apple 的 GPU 運算介面) 支援的 wheel。這個寫法出自 uv 官方的 PyTorch 指南。

ruff:lint 跟 format 都交給它

ruff 之前的標準配置同樣是一組工具:flake8 負責 lint、isort 排序 import、black 負責 formatting、pyupgrade 更新舊語法,再加上十幾個 flake8 plugin。每個工具有自己的設定檔與版本週期,規則之間還會互相衝突:isort 與 black 對 import 格式各有主張,要靠 profile = "black" 這類設定協調。全部執行一輪需要數十秒,大型 repo 上只能放進 CI,本機得不到即時回饋。

ruff 同時是 linter 與 formatter,重新實作了上述工具與大部分常用 plugin 的規則,規則編號直接沿用原工具 (F 來自 pyflakes、S 來自 flake8-bandit),既有專案的豁免設定可以直接對照遷移。同樣以 Rust 開發,速度比被取代的組合快一到兩個數量級,足以掛在編輯器的存檔事件上即時執行;設定集中在 pyproject.toml 一處,linter 與 formatter 出自同一個專案,不再需要協調規則衝突。

Berry AI 的慣例是 select = ["ALL"] 先啟用全部規則,再逐條豁免,且每條豁免都要註記理由:

[tool.ruff.lint]
select = ["ALL"]
ignore = [
    "D",      # pydocstyle:不強制 docstring
    "COM812", # 與 formatter 衝突
]

[tool.ruff.lint.per-file-ignores]
"tests/*" = [
    "S101",    # 測試裡允許 assert
    "PLR2004", # 測試裡允許 magic value
]

這個設計有兩個效果:ruff 發布新規則時預設即為啟用,規則集只會隨時間趨嚴;ignore 清單本身即是文件,記錄這個 repo 豁免了哪些規則與理由。

型別檢查:pyright 與 ty

型別檢查 (type checking) 目前是兩套工具並行。pyright 是 Microsoft 開發的 type checker,也是 VS Code Pylance 的核心,成熟穩定,是既有 repo 的標準配備。ty 是 Astral 開發中的 type checker,同樣以 Rust 實作,速度比 pyright 快一個數量級,但仍在 beta 階段 (版本停留在 0.0.x)。

遷移的動機是速度。幾個大型 repo 的 pyright 全量檢查已經慢到無法掛進 commit 流程,而檢查頻率一降低,型別錯誤就會累積到 CI 才暴露,這是不得不換的原因。換用 ty 之後,每次 commit 都完整執行型別檢查成為可行的預設。代價則是:pyright 的型別檢查仍比 ty 嚴謹且完整,ty 未達 1.0,規則覆蓋與生態也還在補齊。這是以嚴謹度換回饋速度的取捨,速度尚未成為問題的 repo 就維持 pyright。

兩者的設定都寫在 pyproject.toml。以下的 pyright 設定取自既有 repo:venvPath 與 venv 讓命令列執行的 pyright 找到 uv 建立的 .venv (使用 Pylance 時這兩項會被忽略,改用 VS Code 選定的直譯器)。另一項值得打開的是預設關閉的 reportUnnecessaryTypeIgnoreComment,過時的 # type: ignore 會被指出,豁免不會隨時間累積:

[tool.pyright]
venvPath = "."
venv = ".venv"
pythonVersion = "3.12"
typeCheckingMode = "basic"
reportUnnecessaryTypeIgnoreComment = "warning"

已遷移的 repo 上,ty 的設定明顯少,指定檢查範圍與 Python 版本即可,extra-paths 放的是自行補寫的 stubs (只含型別宣告的 .pyi 檔):

[tool.ty.src]
include = ["src", "tests"]

[tool.ty.environment]
python-version = "3.14"
extra-paths = ["stubs"] # 自行維護的 stubs

prek:讓 pre-commit hook 擋下不合格的 commit

檢查工具備齊後,下一個問題是執行時機。我們將檢查掛在 git hook 上,原本用 pre-commit 跑,後來整批轉換到 prek。prek 是 pre-commit 的 Rust 重寫,完全相容:讀取同一份 .pre-commit-config.yaml、支援同一個 hook 生態,遷移時只要替換執行檔。新專案也可以改用 prek 自己的 prek.toml,兩個都有時 prek.toml 優先。換來兩項改善:pre-commit 是 Python 套件,安裝 hook 前必須先有可用的 Python 環境,prek 是單一執行檔 (single binary),沒有這個前置條件;hook 環境的建置與執行也快數倍。這是整套工具鏈裡遷移成本最低的一步。

我們的 hook 清單除了 ruff 與 ty,還有私鑰偵測、合併衝突標記檢查、拼字檢查,以及規範 commit message 格式的 commitizen。清單裡另有 uv lock --check,它會攔截 pyproject.toml 已修改但鎖檔未更新的 commit。核心原則是:「什麼叫檢查通過」在一個 repo 裡只有一份定義,就是這份 hook 清單。它就是事實來源 (source of truth),編輯器、commit 與 CI 執行的都是它。

同一套指令,從本機到 CI

每個 repo 的根目錄都有一份 Makefile 或 justfile (just 的指令檔),目標名稱完全一致:install-dev、format、lint、check、test,跨團隊支援時不需重新熟悉指令。CI 不另外定義檢查邏輯,GitHub Actions 的步驟即是以 astral-sh/setup-uv 裝好 uv、uv sync --locked 還原環境、prek run --all-files 執行那份 hook 清單,最後執行 pytest。本機 make check 與 make test 通過,CI 就會通過,兩者執行的是同一份規則定義;--locked 則確保鎖檔與 pyproject.toml 不一致時 CI 立即失敗。

Makefile 裡另有一段設定:

.EXPORT_ALL_VARIABLES: # 變數傳進子行程

ifdef CLAUDECODE
NO_COLOR = 1
RUFF_OUTPUT_FORMAT = concise
TY_OUTPUT_FORMAT = concise
PYTEST_ADDOPTS = -q --maxfail=1 --tb=short --show-capture=no --disable-warnings
endif

AI coding agent 也是這套 DevEx 的使用者:偵測到指令由 Claude Code 執行時,工具切換為安靜模式、輸出改為精簡格式、測試在第一個失敗即中止,減少 agent 消耗的 token 與往返輪次。

動手玩:從 uv init 到通過 make check

以下是我們新專案的初始設定,可直接當成專案範本。共四步:以 uv init 建立骨架、把工具設定寫進 pyproject.toml、列出 hook 清單、再以 Makefile 收斂指令入口。先建立骨架:

uv init --package berry-demo
cd berry-demo

這會產生 .python-version、pyproject.toml、src/berry_demo/__init__.py、.gitignore 與 README.md。將工具設定追加到 pyproject.toml 的結尾:

[dependency-groups]
dev = [
    "prek>=0.5",
    "pytest>=9.0",
    "ruff>=0.16",
    "ty>=0.0.80",
]

[tool.ruff]
target-version = "py313"

[tool.ruff.lint]
select = ["ALL"]
ignore = [
    "D",      # pydocstyle:不強制 docstring
    "COM812", # 與 formatter 衝突
    "CPY001", # 不強制 license header
]

[tool.ruff.lint.per-file-ignores]
"tests/*" = [
    "S101",    # 測試裡允許 assert
    "INP001",  # tests 不需要是 package
    "PLR2004", # 測試裡允許 magic value
]

[tool.ty.src]
include = ["src", "tests"]

[tool.pytest.ini_options]
testpaths = ["tests"]

.pre-commit-config.yaml 定義 hook 清單:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-merge-conflict
      - id: detect-private-key
      - id: end-of-file-fixer
      - id: trailing-whitespace
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.16.7
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format
  - repo: https://github.com/astral-sh/uv-pre-commit
    rev: 0.11.26
    hooks:
      - id: uv-lock
        args: [--check]
  - repo: https://github.com/astral-sh/ty-pre-commit
    rev: v0.0.80
    hooks:
      - id: ty

Makefile 收斂指令入口:

install-dev: ## 建立環境並安裝 git hooks
	uv sync
	uv run prek install

format: ## 跑 formatter
	uv run --no-sync ruff format

lint: format ## lint 並自動修正
	uv run --no-sync ruff check --fix

check: ## 完整靜態檢查,等同 CI
	uv run --no-sync prek run --all-files

test: ## 跑測試
	uv run --no-sync pytest
.PHONY: install-dev format lint check test

除了 install-dev,其餘目標都經由 uv run --no-sync 執行,跳過預設的環境自動同步,環境只在安裝那一步變動。再加入一個最小的測試 tests/test_demo.py,接著建置環境:

make install-dev
Downloading cpython-3.13.15-macos-aarch64-none (download) (24.0MiB)
 Downloaded cpython-3.13.15-macos-aarch64-none (download)
 + prek==0.5.2
 + pytest==9.1.1
 + ruff==0.16.7
 + ty==0.0.80
Installed Git hook at `.git/hooks/pre-commit`

這台機器原本沒有 Python 3.13,由 uv 自動下載安裝。prek 只檢查 git 追蹤的檔案,因此先 git add -A,再執行完整檢查:

make check
check for merge conflicts................................................Passed
detect private key.......................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
ruff check...............................................................Failed
- hook id: ruff-check
- exit code: 1

  T201 `print` found
   --> src/berry_demo/__init__.py:2:5
    |
  1 | def main() -> None:
  2 |     print("Hello from berry-demo!")
    |     ^^^^^
  help: Remove `print`

  Found 1 error.
ruff format..............................................................Passed
uv-lock..................................................................Passed
ty.......................................................................Passed
make: *** [check] Error 1

第一次執行就攔下一個問題:uv init 生成的 main() 範例以 print 輸出,未通過全部啟用的 ruff 規則 (T201 出自 flake8-print,正式程式碼應改用 logging)。將 __init__.py 改寫為一般函式並補上測試,再執行一次:

check for merge conflicts................................................Passed
detect private key.......................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
ruff check...............................................................Passed
ruff format..............................................................Passed
uv-lock..................................................................Passed
ty.......................................................................Passed

make test 同樣通過。從 uv init 到這裡約五分鐘,此後這個專案的每一個 commit 都會經過同一套檢查。

小結

這就是目前 Berry AI 的 Python 工具鏈:新進工程師以 git clone 與 make install-dev 兩個指令即可開始工作;檢查規則與設定都存放在 repo 內,隨版本控制演進;uv、ruff、ty 與 prek 皆以 Rust 實作,速度極快執行成本低,讓人沒有跳過檢查的理由。編輯器、commit 與 CI 三個時機共用同一份定義,整套組合簡單好用,搭配 AI agent 也順手。

參考資料


本系列由 Berry AI 工程團隊出品。更多工程實戰紀錄都在 Berry AI 技術部落格。


上一篇
Superset 壓力測試:合成監控與事件日誌,Dashboard 載入時間的兩種量測路徑
下一篇
Berry AI 使用了哪些開源軟體?我們又回饋了什麼?
系列文
Berry AI:從零開始打造全美第一的得來速 Vision AI 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言