AG Day 2 環境與工具鏈:uv、API key 與專案骨架
執行需求:CPU 可跑。在昨天 AG Day 1(原文連結)中,我們建立了貫穿整個 45 天專欄的學習地圖與全自動研究助理(research-agent)的系統願景,並親手驗證了一個最小代理迴圈原型。在昨天的成果中,我們看到了代理如何根據目標自主決定工具呼叫並完成簡單推理。然而,在真實的軟體工程實務中,我們不能把所有的商業邏輯、工具實作與資料庫存取全部塞在單一檔案內。要打造一個能夠承受長時間維護、可觀測且具備強大擴展彈性的生產級 Agent 系統,建立現代化的環境工具鏈與模組化專案骨架是至關重要的第二步。
引言
在 2026 年的 Python 開發世界中,工具鏈生態已經發生了深刻的變革。過去開發者習慣使用 pip、virtualenv、pip-tools 或 poetry 來管理套件,但往往在專案稍微複雜時就面臨相依性解析耗時漫長、跨作業系統平台鎖定檔不一致、或是虛擬環境建立效率低落的困擾。由 Rust 語言重新打造的高效能工具鏈 uv,在 2025 至 2026 年間已經迅速成為 Python 領域的主流首選。uv 不僅將套件安裝與解析速度提升了數十倍至百倍,更提供了一體化的專案鎖定檔案(uv.lock)與工作區(Workspace)管理能力,從根本上杜絕了「在我的電腦上可以跑,在雲端伺服器上安裝失敗」的環境漂移困境。
除了套件管理,AI Agent 專案的另一大核心基石是「敏感金鑰的安全管理」與「持久化資料架構」。Agent 系統需要與各類大型語言模型 API(例如 OpenAI API、Anthropic API)以及外部網路檢索服務(例如 Tavily Search API)進行密集通訊,一旦金鑰外洩,將帶來災難性的帳單損失與機密外洩風險。此外,代理在探索研究題目的過程中,必須具備本機資料庫來記錄原始素材、分塊片段、執行軌跡與各步驟事件。今天我們將以 Python 3.13 為基礎,完整建立 research-agent 的專案架構、安全設定驗證器與 SQLite 知識庫初始化機制,並提供離線模擬模式,讓無外部金鑰的開發者也能順暢完成冒煙測試。
原理/觀念
為什麼現代 Agent 專案必須以 uv 作為工具鏈核心?
AI Agent 工程通常需要安裝大量的第三方依賴套件,包括非同步通訊框架(httpx)、資料驗證庫(Pydantic v2)、容錯重試工具(tenacity)、本地向量資料庫(chromadb)以及後續的圖編排框架(LangGraph)。傳統套件工具在處理這些複雜套件的依賴解析時,經常需要花費數分鐘甚至十幾分鐘來推算版本相容矩陣。uv 透過高效的平行演算法與全域快取機制,能夠在數秒內完成所有相依性的下載與鎖定。
更重要的是,uv.lock 檔案採用了嚴格的檔案內容雜湊(Hash)校驗機制,確保團隊中每位工程師、以及未來在 Docker 容器化打包階段所安裝的套件版本與二進位內容毫無偏差。這為我們在 Day 39 進行容器化部署提供了最堅實的重複性保證。
環境變數架構與十二要素(Twelve-Factor)原則
在軟體工程經典的十二要素原則中,第三條明確規定:「將設定(Config)儲存於環境變數中」。在 Agent 系統中,這項原則尤為關鍵。一個嚴謹的代理系統應當具備高度靈活性,能夠在開發環境、本機測試環境與正式生產環境之間自由切換,而不需要更動任何一行原始程式碼。
我們在 research-agent 專案中規範了以下三層環境變數體系:
- 模型決策變數:
RESEARCH_AGENT_MODEL。代理的所有思考與工具決策均由此變數指定模型,程式中不寫死特定廠商或版本名稱。若需要評估不同模型的呼叫費用,一律以各廠商官方文件為準,系統內不建立易過期的硬編碼計費字典。 - 運作控制變數:
RESEARCH_AGENT_MAX_STEPS。控制代理的最大執行步數,防止異常情況下的無限迴圈。 - 外部金鑰變數:
OPENAI_API_KEY、ANTHROPIC_API_KEY、TAVILY_API_KEY等。這些金鑰只透過本地.env檔案讀取,並且必須列入.gitignore,絕不提交進 Git 版本控制。我們透過.env.example提供給團隊成員作為配置模板。
SQLite 作為 Agent 本機知識庫的架構考量
許多初學者在架構 Agent 系統時,一開始就急於引進龐大的分散式資料庫,這往往徒增本機除錯與環境設定的複雜度。在研發助理的初始階段,採用 Python 內建且經過數十年考驗的 SQLite 作為本機結構化儲存是極其優雅且強大的選擇。SQLite 是一個單一檔案資料庫,免去維護外部伺服器服務的負擔,卻擁有完整的 ACID 交易保證。
在 data/knowledge.db 中,我們設計了四張核心關聯資料表:
- documents(原始研究素材表):儲存抓取到的原始網頁全文、來源 URL、標題、雜湊值與建立時間,避免重複抓取相同來源。
- chunks(文字分塊表):將 documents 中的長文切成適合模型閱讀的小段落,並記錄分塊在原素材中的偏移量,為未來的向量檢索做好準備。
- runs(代理任務執行紀錄表):記錄使用者下達的研究題目、啟動時間、完成狀態、花費的總步數與最終報告內容。
- events(步數事件軌跡表):記錄每一次模型思考(Thought)、工具呼叫(Tool Call)名稱、傳入參數、工具回傳結果與時間戳記,這是後續除錯與評估的重要依據。
完整實作
接下來我們開始動手打造完整的 research-agent 專案。請先在專案根目錄下建立標準的目錄結構。我們規劃的目錄佈局如下:
mkdir -p research-agent/src/research_agent
mkdir -p research-agent/data/chroma
mkdir -p research-agent/reports
mkdir -p research-agent/logs
mkdir -p research-agent/tests
cd research-agent
第一步:建立標準的 pyproject.toml 檔案。這裡我們明確宣告 Python 3.13 規範,並列出整個專案所需的核心相依套件:
# research-agent/pyproject_spec.py
# 此處展示 pyproject.toml 的結構定義邏輯
PYPROJECT_TOML_CONTENT = """[project]
name = "research-agent"
version = "0.1.0"
description = "生產級多代理自動研究助理系統"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"pydantic>=2.7.0",
"pydantic-settings>=2.2.0",
"python-dotenv>=1.0.1",
"httpx>=0.27.0",
"tenacity>=8.3.0",
]
[project.scripts]
research-agent = "research_agent.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"""
if __name__ == "__main__":
with open("pyproject.toml", "w", encoding="utf-8") as f:
f.write(PYPROJECT_TOML_CONTENT)
print("已成功建立 pyproject.toml 設定檔案。")
執行上述 Python 腳本後,我們便可以使用 uv 建立獨立的虛擬環境並安裝相依套件:
uv venv --python 3.13
# 在 Windows 環境中啟動:.venv\Scripts\Activate.ps1
# 在 Linux / macOS 環境中啟動:source .venv/bin/activate
uv pip install -e .
第二步:建立 .env.example 檔案,向團隊展示系統所需的環境變數規範,同時確保敏感資訊不被外洩:
# research-agent/create_env_example.py
ENV_TEMPLATE = """# ==========================================
# research-agent 環境變數範本(請複製為 .env 並填入真實金鑰)
# ==========================================
# 模型選擇(由環境變數決定,不寫死於程式碼中)
RESEARCH_AGENT_MODEL=mock-model
# 代理運作安全限制
RESEARCH_AGENT_MAX_STEPS=10
# API 金鑰(若無金鑰,系統將以 --dry-run 離線模擬模式運作)
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
TAVILY_API_KEY=
# 系統路徑設定
DATABASE_PATH=data/knowledge.db
LOG_LEVEL=INFO
"""
if __name__ == "__main__":
with open(".env.example", "w", encoding="utf-8") as f:
f.write(ENV_TEMPLATE)
print("已成功建立 .env.example 模板檔案。")
第三步:實作環境設定載入器 src/research_agent/config.py。我們採用現代 Python 的 dataclass 與型別註解,並提供金鑰檢查與離線模擬(mock/dry-run)模式的自動降級判斷:
# research-agent/src/research_agent/config.py
import os
from pathlib import Path
from dataclasses import dataclass
from dotenv import load_dotenv
# 自動尋找專案根目錄下的 .env 檔案並載入
BASE_DIR = Path(__file__).resolve().parent.parent.parent
load_dotenv(BASE_DIR / ".env")
@dataclass(frozen=True)
class Settings:
"""全域不可變設定物件"""
model_name: str
max_steps: int
openai_api_key: str | None
anthropic_api_key: str | None
tavily_api_key: str | None
database_path: Path
log_level: str
is_dry_run: bool
def load_settings(force_dry_run: bool = False) -> Settings:
"""載入並驗證系統設定"""
model = os.getenv("RESEARCH_AGENT_MODEL", "mock-model")
max_steps = int(os.getenv("RESEARCH_AGENT_MAX_STEPS", "10"))
openai_key = os.getenv("OPENAI_API_KEY")
anthropic_key = os.getenv("ANTHROPIC_API_KEY")
tavily_key = os.getenv("TAVILY_API_KEY")
db_rel = os.getenv("DATABASE_PATH", "data/knowledge.db")
db_path = BASE_DIR / db_rel
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
# 若使用者顯式要求 dry-run,或完全未提供任何 LLM 金鑰,則判定進入模擬模式
has_llm_key = bool(openai_key or anthropic_key)
dry_run = force_dry_run or (not has_llm_key)
return Settings(
model_name=model,
max_steps=max_steps,
openai_api_key=openai_key if openai_key else None,
anthropic_api_key=anthropic_key if anthropic_key else None,
tavily_api_key=tavily_key if tavily_key else None,
database_path=db_path,
log_level=log_level,
is_dry_run=dry_run,
)
這套設計實現了完全的鬆散耦合:如果開發者手邊暫時沒有商用 API 金鑰,系統會自動在設定層將 is_dry_run 標示為 True,讓後續的所有元件都能主動切換為本地模擬邏輯,避免直接爆發未授權錯誤。
第四步:實作 SQLite 知識庫儲存模組 src/research_agent/storage.py。我們在此定義四大資料表的建表 SQL,並實作安全的新增與查詢基本函式:
# research-agent/src/research_agent/storage.py
import sqlite3
from pathlib import Path
from typing import Dict, Any, List
SCHEMA_SQL = """
-- 1. 原始研究素材表
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_url TEXT UNIQUE,
title TEXT NOT NULL,
raw_content TEXT NOT NULL,
content_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. 文字分塊段落表
CREATE TABLE IF NOT EXISTS chunks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
chunk_index INTEGER NOT NULL,
chunk_text TEXT NOT NULL,
char_length INTEGER NOT NULL,
FOREIGN KEY(document_id) REFERENCES documents(id) ON DELETE CASCADE
);
-- 3. 代理任務執行紀錄表
CREATE TABLE IF NOT EXISTS runs (
run_id TEXT PRIMARY KEY,
user_query TEXT NOT NULL,
model_name TEXT NOT NULL,
status TEXT NOT NULL,
total_steps INTEGER DEFAULT 0,
final_report TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
finished_at TIMESTAMP
);
-- 4. 步驟思考與工具事件軌跡表
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
step_number INTEGER NOT NULL,
event_type TEXT NOT NULL,
thought TEXT,
tool_name TEXT,
tool_args TEXT,
tool_result TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(run_id) REFERENCES runs(run_id) ON DELETE CASCADE
);
-- 建立關鍵索引以最佳化查詢效能
CREATE INDEX IF NOT EXISTS idx_chunks_doc ON chunks(document_id);
CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
"""
def init_database(db_path: Path) -> None:
"""初始化 SQLite 資料庫與所有關聯資料表"""
db_path.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(db_path) as conn:
# 開啟 WAL(Write-Ahead Logging)模式,大幅提升讀寫並發效能
conn.execute("PRAGMA journal_mode=WAL;")
# 開啟外鍵約束檢查
conn.execute("PRAGMA foreign_keys=ON;")
conn.executescript(SCHEMA_SQL)
conn.commit()
def record_new_run(db_path: Path, run_id: str, query: str, model_name: str) -> None:
"""記錄一筆新的代理執行任務"""
with sqlite3.connect(db_path) as conn:
conn.execute(
"INSERT INTO runs (run_id, user_query, model_name, status) VALUES (?, ?, ?, ?)",
(run_id, query, model_name, "RUNNING")
)
conn.commit()
第五步:建立統一的結構化日誌記錄模組 src/research_agent/logger.py,確保代理在思考與除錯過程中的所有輸出都能同時呈現在終端機並寫入檔案:
# research-agent/src/research_agent/logger.py
import logging
from pathlib import Path
def setup_agent_logger(log_dir: Path, level: str = "INFO") -> logging.Logger:
"""配置全域代理日誌器,同時支援檔案儲存與控制台輸出"""
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "agent.log"
logger = logging.getLogger("research_agent")
numeric_level = getattr(logging, level.upper(), logging.INFO)
logger.setLevel(numeric_level)
# 若已設定過 handler 則不重複新增,避免日誌重疊輸出
if not logger.handlers:
formatter = logging.Formatter(
fmt="%(asctime)s [%(levelname)s] %(name)s (%(filename)s:%(lineno)d) - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# 檔案 Handler,以 utf-8 編碼儲存日誌
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setFormatter(formatter)
file_handler.setLevel(numeric_level)
logger.addHandler(file_handler)
# 終端機 Handler
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
stream_handler.setLevel(numeric_level)
logger.addHandler(stream_handler)
return logger
第六步:撰寫冒煙測試(Smoke Test)驗證腳本 check_setup.py。這個腳本將串起上述所有元件,驗證環境變數、初始化資料庫並寫入一筆測試任務,向開發者證明基礎建設完全到位:
# research-agent/check_setup.py
import sys
import uuid
from pathlib import Path
from research_agent.config import load_settings
from research_agent.storage import init_database, record_new_run
from research_agent.logger import setup_agent_logger
def run_smoke_test():
print("=== 開始執行 research-agent 基礎架構冒煙測試 ===")
# 1. 檢查 Python 版本
py_ver = sys.version.split()[0]
print(f"1. 檢查 Python 版本:{py_ver}(要求 >= 3.13)")
major, minor = sys.version_info.major, sys.version_info.minor
if (major, minor) < (3, 13):
print("警告:目前的 Python 版本低於 3.13,建議升級。")
# 2. 載入設定
settings = load_settings()
print(f"2. 載入系統設定:成功")
print(f" - 決策模型變數:{settings.model_name}")
print(f" - 最大安全步數:{settings.max_steps}")
print(f" - 模式狀態:{'離線模擬模式(--dry-run)' if settings.is_dry_run else '連線真實 API 模式'}")
print(f" - 資料庫路徑:{settings.database_path}")
# 3. 初始化日誌系統
log_dir = settings.database_path.parent.parent / "logs"
logger = setup_agent_logger(log_dir, settings.log_level)
logger.info("冒煙測試日誌系統連線成功。")
print("3. 日誌系統設定:成功(輸出至 logs/agent.log)")
# 4. 初始化 SQLite 資料庫
init_database(settings.database_path)
print("4. SQLite 資料庫初始化:成功(四張核心資料表與索引建立完成)")
# 5. 測試資料寫入
test_run_id = f"test_{uuid.uuid4().hex[:8]}"
record_new_run(
db_path=settings.database_path,
run_id=test_run_id,
query="驗證系統初始化管線是否運作正常",
model_name=settings.model_name
)
print(f"5. 資料庫寫入測試:成功(寫入測試任務 ID:{test_run_id})")
print("\n=== 全部冒煙測試通過!專案骨架已準備就緒 ===")
if __name__ == "__main__":
run_smoke_test()
在命令列中執行驗證指令:
python check_setup.py
此時在終端機中將會看到乾淨俐落的驗證回傳(示範輸出):
=== 開始執行 research-agent 基礎架構冒煙測試 ===
1. 檢查 Python 版本:3.13.0(要求 >= 3.13)
2. 載入系統設定:成功
- 決策模型變數:mock-model
- 最大安全步數:10
- 模式狀態:離線模擬模式(--dry-run)
- 資料庫路徑:F:\hao-code\blog-drafts\research-agent\data\knowledge.db
2026-07-16 10:00:00 [INFO] research_agent (logger.py:28) - 冒煙測試日誌系統連線成功。
3. 日誌系統設定:成功(輸出至 logs/agent.log)
4. SQLite 資料庫初始化:成功(四張核心資料表與索引建立完成)
5. 資料庫寫入測試:成功(寫入測試任務 ID:test_a1b2c3d4)
=== 全部冒煙測試通過!專案骨架已準備就緒 ===
常見錯誤與踩雷
在架構 Agent 工具鏈與基礎設施時,有幾個經典的雷區必須嚴肅防範:
- 將真實金鑰提交進版本控制系統:很多工程師在測試時為了求快,直接在程式碼中寫上
api_key = "sk-...",隨後不小心透過git push送到公開倉庫。幾分鐘內金鑰就會被自動化爬蟲掃走並濫用。必須嚴格遵循.env+.gitignore的防護機制。 - SQLite 資料庫檔案鎖定(Database Locked):在後續章節中,我們的代理會執行多執行緒或非同步並發檢索。SQLite 預設在多行程同時寫入時容易擲出
sqlite3.OperationalError: database is locked。我們在init_database中顯式啟用了PRAGMA journal_mode=WAL;(預寫式日誌),這讓多個讀取者與一個寫入者可以完全並行,大幅降低鎖定機率。 - 跨平台檔案路徑硬編碼問題:在 Windows 系統上路徑分隔符號為反斜線(
\),而在 Linux 或 macOS 上為正斜線(/)。若使用字串拼接路徑(例如data + "/" + file),在 Windows 上極易出錯。本專案一律使用 Python 標準函式庫的pathlib.Path物件處理所有檔案路徑。 - 未啟動虛擬環境直接使用全域 Python:在安裝了多個 Python 版本的開發機上,若未啟動虛擬環境,直接執行
pip install可能會將套件安裝到全域環境中,導致版本衝突或權限問題。使用 uv 時,建議善用uv run python ...指令,它會自動尋找當前工作區的虛擬環境並執行。
效能與實務提醒
在工程實務維運中,基礎設施的效能與穩定度取決於細節的把控:
第一,SQLite 索引與磁碟 I/O 規劃:我們的資料庫設計中預先替 chunks(document_id) 與 events(run_id) 建立了 B-Tree 索引。當代理研究的素材累積到數萬筆段落時,關聯查詢的速度依然能維持在毫秒級別。如果不建索引,每次比對原始素材來源都必須進行全表掃描,嚴重拖慢代理執行迴圈。
第二,uv 的全域快取最佳化:uv 預設會在使用者目錄下維護一個全域的 wheel 快取。如果在多個專案或 Docker 建置流程中頻繁安裝相同套件,uv 會直接利用硬連結(Hardlink)或快取還原,而不重新從網路下載。這在 CI/CD 自動化測試管線中能為團隊節省數倍的建置時間。
第三,日誌輪替(Log Rotation)機制:長期運行的代理系統會產出龐大的除錯日誌。如果單一 agent.log 檔案無限制膨脹,最終可能耗盡硬碟空間。在正式環境中,建議使用 logging.handlers.RotatingFileHandler 設定單一檔案大小上限(例如 50MB)並保留最多 5 個歷史備份檔案。
小結
今天我們完成了 research-agent 全專案最關鍵的「骨架搭設」工作。我們捨棄了過時繁瑣的舊式工具,引進了現代化的 uv 生態系,制定了以十二要素為標準的環境變數架構,並透過 config.py 提供了對離線開發者友善的 --dry-run 模擬降級模式。同時,我們建立了以 SQLite WAL 模式為基礎的四張核心關聯資料表,並透過嚴謹的冒煙測試驗證了整體管線的連通性。
以下為本章節涉及的關鍵技術概念與台灣用語對照:
- 專案骨架(Project Skeleton):規範良好的模組目錄配置與設定規範,作為後續所有程式碼擴展的根基。
- 相依性鎖定(Dependency Locking):透過
uv.lock鎖定套件版本與雜湊值,確保環境在不同主機間的一致性。 - 虛擬環境(Virtual Environment):隔離不同專案套件依賴的獨立 Python 執行空間。
- 環境變數(Environment Variables):儲存在作業系統執行時期、用於解耦系統設定與機密金鑰的外部變數。
- 預寫式日誌(Write-Ahead Logging, WAL):SQLite 資料庫的一種日誌記錄模式,允許讀取與寫入並發執行而不互鎖。
- 冒煙測試(Smoke Test):針對軟體最核心的關鍵路徑進行初步快速驗證,確保基本架構能順利啟動。
結語
有了標準化的專案結構與穩固的資料庫基礎後,我們終於可以開始深入大型語言模型的心臟地帶。在傳統開發中,我們可能只是呼叫一下 client.chat.completions.create() 就草草了事;但在複雜的 Agent 系統中,對話歷史的結構設計、角色分配以及系統提示詞的權威性控制,直接決定了代理的思考品質與邊界防護力。
明天,我們會進入「AG Day 3 對話 API 深入:messages、roles 與 system prompt」,深入拆解底層對話協定中的 system、user、assistant 與 tool 角色機制,設計兼具彈性與安全邊界的研究專用 System Prompt,並實作滑動視窗歷史訊息修剪器,徹底解決上下文視窗溢位難題!
延伸資源
- uv 官方文件:
https://docs.astral.sh/uv/。Astral 團隊打造的高效能 Python 套件管理工具指南。 - Python 官方文件:
sqlite3— DB-API 2.0 interface for SQLite databases。深入解析 WAL 模式與並發交易控制。 - Twelve-Factor App 官方架構準則:The Twelve-Factor App(Config 篇)。現代微服務與應用程式設定標準。
- Pydantic Settings 官方文件:
https://docs.pydantic.dev/latest/concepts/pydantic_settings/。型別安全的設定管理最佳實務。
留言
張貼留言