新專案的第一個 commit 通常是一個 main.py 加一句 hello world,反正「架構之後再說」。但 Day 2 的三個問題(不確定性隔離在哪、什麼能進 prompt、什麼必須被觀測)有一個共同點:它們都只能在結構上回答,而結構是第一個 commit 定下來的。今天把 Day 2 那張圖變成能跑的專案骨架。跑完這篇,你會有一個測試全綠、基礎 API 慣例(版本路徑、錯誤格式、correlation ID)已定、而且開發起來一毛錢都不用花的起點。
以下指令在 macOS、Python 3.13、uv 0.11 驗證過(2026-07);Linux 相同,Windows 建議 WSL。只需要先裝 uv:Python 本身不用預先裝,uv 找不到符合 pyproject.toml 裡 requires-python = ">=3.13" 的 Python 時會自動下載(預設行為;離線或停用 managed Python download 的環境,就先自備 Python 3.13)。
git clone --branch day-03 https://github.com/kehao-playground/azure-genai-backend-lab.git
cd azure-genai-backend-lab
uv sync --locked
--locked 是重點:所有環境都使用 repo 裡同一份 locked resolution,uv 不會自行重算或改寫 lock;實際安裝時仍會依 OS、CPU 與 Python marker 選擇目前平台適用的套件。CI 跑的也是這份 lock,「照著做能重現依賴決策」靠的是這個,不是運氣。
uv run pytest
uv run behave
預期輸出(節錄):
4 passed, 1 xfailed, 1 warning in 0.10s
5 features passed, 0 failed, 0 skipped
5 scenarios passed, 0 failed, 0 skipped
然後把服務跑起來,開另一個終端機戳兩個 endpoint:
uv run uvicorn azgenai_lab.main:app --port 8000
curl localhost:8000/health
# {"status":"ok","service":"azure-genai-backend-lab"}
curl -X POST localhost:8000/api/v1/chat -H 'content-type: application/json' -d '{"message":"hello"}'
# {"error":{"code":"not_implemented","message":"Chat API will be implemented in Day 5."},
# "correlation_id":"fbce30e6-..."}
注意第二個回應。Chat 還沒實作,但它已經用正式的錯誤信封回你了:{"error": {"code", "message"}, "correlation_id"}。這是刻意的:骨架階段先鎖住跨 endpoint 共用的慣例(錯誤信封、correlation ID、/api/v1/ 版本路徑)。之後 27 天會新增成功回應的 schema 與 streaming 的事件格式,但不會再推翻這些基礎。
每個回應都帶 X-Correlation-Id header,錯誤信封還把它放進 body,client 回報問題時可以直接引用。Day 2 說的觀測面那條虛線,從第一個 commit 就存在。
src/azgenai_lab/
├── api/ # API 層:routers、HTTP schemas(health、chat、streaming、rag)
├── core/ # 跨層地基:config、logging、correlation、錯誤信封
├── services/ # Adapter 層:LLM 與檢索的 adapter(現在是 fake 佔位)
├── models/ # 跨層共用的 DTO
└── prompts/ # Prompt 資產(Day 8 才會認真用)
tests/
├── unit/ # 單元測試
└── bdd/ # behave features:API 行為用自然語言釘住
兩個對照說明。api/ 只放 HTTP 形狀與路由,services/ 只放外部互動。Day 2 的「不確定性的籠子」就是這個目錄。core/ 目前裝的是設定與跨層基礎設施(correlation、錯誤處理),是編排層的地基;真正的對話編排邏輯會在 Day 5–7 隨功能長出來,先不要在這裡預蓋空抽屜。
core/config.pyclass Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
# ...(app_name 等欄位略,完整版見 tag)
azure_openai_endpoint: str | None = None
azure_openai_deployment_name: str | None = None
use_fake_llm: bool = Field(default=True)
use_fake_search: bool = Field(default=True)
use_fake_llm 預設 True——這是整個骨架最重要的一個決策,而且它不是測試技巧,是成本與速度的架構決策。Day 3 尚未接上真模型;這個預設先把後續的 composition policy 定下來,clone 後不需要 Azure 資源或 API key 就能開發與測試。Day 4 加入 real adapter 後,只有明確關閉 fake 並提供完整 Azure 設定時才會呼叫模型、開始產生 token 費用。
忍喵:「預設不花錢是設計出來的,不是省出來的。這個default=True是開發環境的成本護欄,不是所有環境都該照抄。」
注意 Azure 相關欄位全部是 None 預設、從 .env 讀。repo 只提供 .env.example,.env 不進版本控制,把真的 key 被 commit 的風險壓到最低。
uv:因為 lockfile 是「可重現」的基礎建設,而 uv 把「裝 Python、建 venv、鎖依賴、跑指令」收斂成一個工具,uv sync --locked 一條指令等於過去 pyenv + venv + pip-tools 三件事。
FastAPI:因為它的 request/response model 就是 Pydantic,OpenAPI 規格是從程式碼長出來的(這個 repo 的 CI 會檢查 export 出的 OpenAPI 有沒有跟程式碼漂移),而之後 Day 6 要做的 streaming 它原生支援。這兩個選擇都有替代品(Poetry、Litestar、Django Ninja),我不主張它們是唯一解,但整個系列會證明這組工具鏈撐得起 30 天的演進。
pytest 直接跑,出現 ModuleNotFoundError: azgenai_lab:要用 uv run pytest(或先 source .venv/bin/activate),套件裝在專案的 venv 裡,不在系統 Python。uv run behave 找不到 feature:確認在 repo 根目錄執行。Behave 的路徑設定在根目錄的 behave.ini(paths = tests/bdd/features),從子目錄跑會找不到。uv sync 抱怨 Python 版本:別自己降版本需求,跑 uv python install 3.13 讓 uv 抓正確版本;系列全程 pin 3.13。今天的成果:一個測試全綠的骨架,基礎 API 合約(版本路徑、error envelope、correlation ID)與成本預設(USE_FAKE_LLM=true)在還沒有任何真功能之前就鎖住了。完整程式碼在 day-03 tag,該 tag 上 CI 是綠的。
明天(Day 4)開始碰 Azure:Azure OpenAI 的 resource、deployment、model 之間是什麼關係、quota 怎麼看,以及把第一個真的 endpoint 設定接進這個骨架——錢包從那天開始才有感覺。
(本篇無新增雲端資源。)
本文由作者規劃與撰寫,AI(Claude)協助草稿整理與程式碼驗證;技術內容與觀點由作者確認並負責。