每個軟體甚至於系統,都會有「設定」這個功能。
設定內的每個項目,一開始會有「預設」的設定值,如果沒有額外改動,就依照這個預設值去運作,而當然,也會有接口可以去更改設定,今天這篇,我們來實作設定檔這個功能。
設定檔案的部分,這裡選擇以 JSON 的格式儲存,好處是它可以跟 Pydantic 做很好的配合。
檔案架構的部分,分成了負責讀寫 JSON 設定檔的 config_manager.py 以及管理設定項目的 config_schema.py 和恢復檔案的 config_repair.py(下一篇會提到,用來自主修復設定檔的錯誤):
src/meowgent/
├── ...
└── config/
├── __init__.py
├── config_schema.py # 定義設定項目
├── config_manager.py # 讀寫設定檔
└── config_repair.py # 恢復設定檔
__init__.py的部分:from .config_schema import MeowgentConfig from .config_manager import ConfigManager, CONFIG_FILE, CONFIG_DIR from .config_repair import repair
接下來的四篇,我們會專注於處理「設定檔」這件事上,分別是:
這裡使用的是 Pydantic 的「資料模型 - BaseModel」來定義設定項目,
我們先簡單給定兩個類別的設定項目,分別是與模型相關的及與 Agent 行為相關的兩種,先簡單做這幾個選項就好:
在這裡 ModelsConfig、AgentConfig 被稱為「模型」,需要繼承 BaseModel,而底下的項目被稱為「欄位」。
看到下面程式碼的部分:
每個欄位上我們都明確的「標註上型別」(就算之後傳入時誤把 "12" 這樣的字串傳入 int 時也可以自動轉換,要是沒標註型別,就會繼續錯下去),
定義好欄位的型別後要對欄位進行「約束」(也就是 Field()),每個欄位都會要有一個「預設值 default」以及「描述 description」,在數值的部分還可以設定數值範圍。
gt(greater than -> 大於);ge(greater equal -> 大於等於)。lt(less than -> 小於);le(less equal -> 小於等於)。
# src/meowgent/config/config_schema.py
from pydantic import BaseModel, Field
from typing import Literal
class ModelsConfig(BaseModel):
""" 模型相關 """
default_model: str = Field(
default="qwen3.8",
description="模型"
)
temperature: float = Field(
default=0.1,
ge=0, le=1.5,
description="溫度係數(0~1.5)"
)
class AgentConfig(BaseModel):
""" Agent 行為 """
max_turns: int = Field(
default=20,
ge=1,
description="每輪的最大工具調用次數"
)
tool_approval_mode: Literal[
"always ask", "default", "approval_all"
] = Field(
default="default",
description="工具審核模式"
)
而兩個模型建立好後,最外層再用一個模型打包成一個整體:
在 Field() 的部分,這裡是用 default_factory 傳入前面定義的模型。
default_factory的本質就是「傳入一個 callable(函式或類別),且每次建立時動態呼叫它生出全新物件」:
- 避免共用可變記憶體:包含
list、dict,以及繼承BaseModel的子模型,也就是這裡用default_factory的原因。- 獲取即時動態資料:例如當前時間(
datetime.now)。
# src/meowgent/config/config_schema.py
...
class MeowgentConfig(BaseModel):
models: ModelsConfig = Field(default_factory=ModelsConfig)
agent: AgentConfig = Field(default_factory=AgentConfig)
而我們來看一下,現在這個 MeowgentConfig 轉為 JSON 會長什麼樣:
Python 程式碼 產出的 JSON 檔案
─────────────────────────────────────────────────────────────────────────────
class MeowgentConfig(BaseModel): ───> { (最外層大括號)
models: ModelsConfig ───> "models": {
default_model = "..." ───> "default_model": "qwen3.8",
temperature = 0.1 ───> "temperature": 0.1
},
agent: AgentConfig ───> "agent": {
max_turns = 20 ───> "max_turns": 20,
tool_approval_mode = "..." ───> "tool_approval_mode": "default"
}
}
而接下來,我們馬上來把 MeowgentConfig 模型轉為 JSON 儲存吧!
剛剛,我們定義好了設定檔上有哪些項目可以被設定、預設值是怎麼樣的,現在,首先我們先來做儲存的邏輯。
第一步,我們先把這個設定檔要存在哪給定好,路徑為 ~/.meowgent/config.json。
資料夾的部分,有注意到名字前面有一個點嗎?
這代表它是「隱藏資料夾」,正常打開 Finder 或檔案總管不會被顯示出來(用cmd + shift + .或 win 上的 檢視 - 顯示 - 勾選 隱藏的項目 來顯示)。
用隱藏資料夾的原因是為了保持使用者家目錄的簡潔、防止誤刪。而為什麼是存在使用者家目錄裡,而不是像專案資料夾內呢?
最主要的原因是,當我們未來把專案打包後,在一般使用者電腦裡將不再有「專案資料夾」這個概念,而且通常軟體的安裝目錄是唯讀的,無法隨意寫入。
所以放在使用者家目錄是業界 CLI 軟體普遍的資料存放點。
# src/meowgent/config/config_manager.py
from pathlib import Path
from .config_schema import MeowgentConfig
from typing import Tuple
# 定義存放位置
CONFIG_DIR = Path.home() / ".meowgent"
CONFIG_FILE = CONFIG_DIR / "config.json"
接著我們進入到儲存函數的部分,這裡把它設為「靜態方法」:
靜態方法是一個不依賴實體物件(
self)或類別屬性(如類別方法@classmethod接收的cls))的方法(類別中的函數)。
簡單來說,它和類別外的函數沒什麼區別,只是為了邏輯分類或可讀性,把它「歸類」在一個類別之中。而如果是「類別方法」,它專門處理不需實體物件的事情,用來從「整個類別的視角」對整個類別做統籌管理。
進到函數裡,首先先建立好隱藏資料夾,要加上 exist_ok=True 來避免已存在時出錯。
接著,把打包好的 MeowgentConfig 模型序列化(轉換)成 JSON 格式字串,這裡設定縮排為 2,不然會是沒有縮排的單行字串。
這裡
MeowgentConfig裡設定內容就是我們目前要存檔的內容了,後續會做邏輯來處理更改MeowgentConfig。
轉換成字串後,就可以將它寫入 ~/.meowgent/config.json 檔案中,編碼要設定好,防止出現亂碼。
# src/meowgent/config/config_manager.py
...
class ConfigManager():
@staticmethod # 靜態方法
def save_config(config: MeowgentConfig):
CONFIG_DIR.mkdir(parents=True, exist_ok=True) # 建立目錄
json_data = config.model_dump_json(indent=2)
# 將模型物件導出(序列化)為格式化的 JSON 字串
CONFIG_FILE.write_text(json_data, encoding="utf-8") # 寫入設定檔
看完代碼,你可能會覺得,這看起來只能存預設樣式的設定,其實不是這樣的!
舉個例子,如果我們對模型進行了更改,會在外部做以下的邏輯(下一篇來做)進行以下動作:..., config = ConfigManager.load_config() # 等等要做的讀檔邏輯 config.models.default_model = "qwen-2.5" # 在這裡修改 ConfigManager.save_config(config) # 這裡把修改的儲存可以理解為:
- 讀取 JSON 格式,轉為
MeowgentConfig模型物件。- 對物件做修改。
- 將修改後的物件儲存為 JSON。
也就是說,修改其實「主要是對資料模型物件」,對物件改好後再利用save_config()存為 JSON 檔案。
存檔搞定了,那我們要怎麼把存好的設定拿出來使用呢?
這時我們就要來做負責讀檔的「靜態方法」:
回傳的部分用元組,布林值的部分是用來表示是否有正確讀取到已儲存的設定檔。
# src/meowgent/config/config_manager.py
...
class ConfigManager():
...
@staticmethod
def load_config() -> Tuple[bool, MeowgentConfig]:
這邊下面會有三種情況出現:
True 及預設情況的 MeowgentConfig 模型。
# src/meowgent/config/config_manager.py
...
class ConfigManager():
...
@staticmethod
def load_config() -> ...:
if not CONFIG_FILE.exists(): # 設定檔不存在 -> 建立設定檔
config = MeowgentConfig() # 獲取預設設定
ConfigManager.save_config(config)
return (True, config)
True 及讀取到的 MeowgentConfig 模型。
# src/meowgent/config/config_manager.py
...
class ConfigManager():
...
@staticmethod
def load_config() -> ...:
if ...:
...
else: # 設定檔存在
try:
content = CONFIG_FILE.read_text(encoding="utf-8")
config = MeowgentConfig.model_validate_json(content)
# 讀取到的 JSON(字串形式)載入模型(MeowgentConfig)
ConfigManager.save_config(config) # 若有新設定更新進 JSON 檔
return (True, config)
False 及預設情況的 MeowgentConfig 模型。main.py 收到 False 會去調用模型,讓模型自主檢查設定檔的部分是出了什麼問題(未來會實作的部分)。
# src/meowgent/config/config_manager.py
...
class ConfigManager():
...
@staticmethod
def load_config() -> ...:
...
else:
try:
...
except Exception: # 讀取錯誤時
return (False, MeowgentConfig())
今天這篇,我們處理好了設定檔的格式、讀取,下一篇,我們要來把設定項目給套用!