iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0

上一篇,我們把設定檔的格式以及存取部分完成了,
而今天,我們要正式地來使用這些設定項目,也就是把它給接入我們的 Agent 系統裡,最後,我們還要做一個有趣的應用,可以期待一下~


如何使用設定檔

先簡單來看一下 MeowgentConfig 要如何使用,
首先,要先獲取經過讀取的 MeowgentConfig 資料模型(也就是 config 而不是單純的 MeowgentConfig 資料模型,它只是預設的「設計圖」),用它來做處理:

..., config = ConfigManager.load_config()

如果是要「讀取」(取)這個項目則是:

當前模型 = config.models.default_model

而如果要「修改」(存)一個項目,例如 default_model:

config.models.default_model = "新模型名稱"

可以看出,讀取完的 MeowgentConfig 這個模型物件是我們要「存」「取」設定的媒介,而不是存在資料夾裡的 config.json 檔案。

所以,先來在 main.py 裡調用 load_config() 吧!
接著,就可以用 config 來取用設定項目了:

# src/meowgent/cli/main.py
...
if __name__ == "__main__":
	...
	
	config_success, config = ConfigManager.load_config()
	
	...

模型選擇

這裡,我們先把預設的模型給綁定上去:

# src/meowgent/cli/main.py
...
if __name__ == "__main__":
	config_success, config = ...
	
	model_name = config.models.default_model
	
	model = ...
	...

然後除了這裡,還記得 Day 15 做的 _change_model() 嗎?
用指令改完模型後,也別忘了要傳入設定檔以及改設定檔,但在這之前,別忘了 handle_cmd() 還沒有接收 config 這個參數,在 main.py 也還沒有傳入它喔!

# src/meowgent/cli/main.py
...
if __name__ == "__main__":
	...
	try:
		while ...:
			...
			if user_input ...:
				handle_cmd(..., config=config)
				...
# src/meowgent/cli/commands.py
...
def handle_cmd(..., config: MeowgentConfig):
	...
# src/meowgent/cli/commands.py
from ...
from config import MeowgentConfig, ConfigManager
...
@cmd_registry(...)
def _change_model(..., config: MeowgentConfig, **kwargs):
	...
    model_object.renew_system_prompt(...)
    
    config.models.default_model = new_model
	ConfigManager.save_config(config)
	
	cli.console.print(...)

溫度係數

我們在建立 OllamaProvider 時加上一個參數 temperature,而別忘了 ollama.chat() 的參數部分也要跟進:

# src/meowgent/providers/ollama_provider.py
...
class OllamaProvider(LLMProvider):
    def __init__(..., temperature: float = 0.1):
        super()...
        self.temperature = temperature
	
	def stream_generate(...) -> ...:
		...
		
		response = ollama.chat(
			...,
			options={
				...,
				"temperature": self.temperature
			}
		)
		
		...

然後接入 main.py:

# src/meowgent/cli/main.py
...
if __name__ == "__main__":
	...
	
	model = Agent(OllamaProvider(..., temperature=config.models.temperature), ...)

工具調用次數

這裡非常簡單,只需要傳入 max_turns 參數就行了:

# src/meowgent/cli/main.py
...
if __name__ == "__main__":
	...
	
	model = Agent(OllamaProvider(...), max_turns=config.agent.max_turns)
	
	...

工具審核模式

這裡我們採取的方案是直接在 Agent 的 chat() 中依據審核模式判斷是否需要審核(need_approval),藉此決定是否需要呼叫 ask_tool_approval() 向使用者詢問:

# src/meowgent/agent.py
...
class Agent():
	def __init__(..., tool_approval_mode: str = "default"):
		...
		self.tool_approval_mode = tool_approval_mode
# src/meowgent/cli/main.py
...
if __name__ == "__main__":
	...
	
	model = Agent(..., tool_approval_mode=config.agent.tool_approval_mode)

接著,分別做各個模式的邏輯處理:

# src/meowgent/agent.py
...
class Agent():
	...
	def chat(...) -> ...:
		while ...:
			...
			if tools_to_execute:
				for ...:
					...
					t = ToolCall(...)
					
					if self.tool_approval_mode == "approval_all":
                        need_approval = False

					elif self.tool_approval_mode == "always ask":
						need_approval = True

					else: # "default" 時
						need_approval = getattr(
							TOOL_REGISTRY.get(t.tool_name, None),
							"need_approval",
							True
						)
	                if not need_approval ...:
		                ...

設定檔壞了?!

雖然可能不太會發生設定檔損毀的問題,但我們想像一下,某天使用者發現設定檔只是非常簡單的 JSON 格式,心血來潮想要自己來更改看看,但一不小心改壞了,該怎麼辦呢?

這裡就做一個非常有趣的應用了,這軟體本身不就是一個 Agent 嗎?我們讓程式「自動呼叫模型」來對設定檔進行修復。

如何被呼叫

建立 repair(),傳入的參數為設定檔(預設設定檔)以及負責輸出的 CLIRenderer 物件(要用裡面的 Console 物件),回傳為 MeowgentConfig,也就是資料模型物件。

# src/meowgent/config/config_repair.py

from .config_schema import MeowgentConfig
from pathlib import Path
import shutil
from .config_manager import CONFIG_FILE, ConfigManager
from cli import CLIRenderer
import textwrap
import questionary
from prompt_toolkit.styles import Style
from rich.padding import Padding

def repair(config: MeowgentConfig, cli: CLIRenderer) -> MeowgentConfig:
    from agent import Agent
    from providers import OllamaProvider
    from cli import get_tool_approval

有些模組在函式內部才進行匯入,是為了避免「循環匯入」。

這時,先來處理一下在什麼情況下會去呼叫這個函數:
當 config_success 為 False 時需要呼叫此函數,而在 False 的狀況下,回傳的 config 只會是預設情況下的設定檔。

# src/meowgent/cli/main.py
...
if __name__ == "__main__":
	...
	cli = ...
	...
	
	config_success, config = ConfigManager.load_config()

    if not config_success: # 未成功
        config = repair(config=config, cli=cli)

要特別注意的是:CLIRenderer 物件要在 repair() 前被建立(因為 repair() 會用到)。


repair() 要做到的事

  1. 備份已損壞的設定檔。
  2. 呼叫模型做處理。
  3. 驗證是否修復成功。
  4. 存檔。

備份

為什麼需要備份呢?那是因為,我們無法確定模型做的修復是否意外竄改了設定項目,最好還是保留原先錯誤的檔案以防萬一:
用的是 shutil.copy2(),參數的部分前者是原始檔路徑,後者是新路徑。

shutil 裡有很多類型的檔案複製函數,其中 shutil.copy2() 是「最完整」的複製函數,它完整保留原始檔案的建立時間、修改時間(時間戳記)與讀寫執行權限,也就是會保留完整的「元數據」。

copyfileobj() --> 僅複製 with open() 物件位元組串流
copyfile() -----> 僅複製硬碟檔案內容 (不帶權限、不帶時間)
copy() ---------> 複製檔案內容 + 檔案權限 (chmod)
copy2() --------> 複製檔案內容 + 完整元數據 (包含時間戳記 mtime/atime) 【最佳】

新路徑的部分,使用 Pathlib 的 with_suffix() 來替換副檔名。

.bak 是專門用於做「備份」的,加在任何格式的副檔名後面。

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
	
    backup_path = shutil.copy2(
	    CONFIG_FILE,
	    CONFIG_FILE.with_suffix(".json.bak")
    ) # 備份

呼叫模型

接著要先建立模型物件(Agent 類別物件):

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
	
	model = Agent(
        OllamaProvider(model_name=config.models.default_model, temperature=config.models.temperature),
        max_turns=config.agent.max_turns,
        tool_approval_mode=config.agent.tool_approval_mode
    )

有發現嗎?跟 main.py 裡完全一模一樣呢!

然後,先把要給模型做修復的 prompt 組裝好:

while 是為了要在沒有成功修復的情況下可以繼續做修復,等等會詳細說到。

model_json_schema() 可以取得資料模型的「JSON Schema 規範定義」(定義了每個欄位的名稱、型別與範圍約束),讓模型知道正確的格式結構應該長什麼樣子。
CONFIG_FILE 是上一篇在 config_manager.py 裡定義的:

# src/meowgent/config/config_manager.py
...
CONFIG_DIR = Path.home() / ".meowgent"
CONFIG_FILE =  CONFIG_DIR / "config.json"
...
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
	
	while True:
        prompt = textwrap.dedent(f"""
            你是一個專門修復損壞 JSON 設定檔的助理,且回答時力求精簡。
            以下是這份設定檔應當符合的正確 JSON Schema 規範:
            {MeowgentConfig.model_json_schema()}

            對無法正常解析的 JSON 內容做修復:
            ```json
            {CONFIG_FILE.read_text(encoding="utf-8")}
            ```

            在不更動原始數據的前提下,用 write_file 工具,
            直接將 {CONFIG_FILE} 修改成正確樣式,
            盡量在只使用一次工具的情況下準確完成。
            
            調用工具前不說客套話、開場白,例如「好的,我調用工具來做修復:」,直接調用工具。
        """)

跟之前在 Day 9 做 system prompt 時一樣,用 textwrap.dedent() 來優化縮排。

有注意到上面的提示詞裡的最後一句嗎?為什麼要要求模型不要廢話直接調用工具呢?
這邊先解釋一下底下的邏輯,我們的目的是讓模型「盡可能的減少不必要的 token 輸出」,也就是回答部分盡量避免,以節省時間。

所以理想情況上,狀態為 response 時(視為工具調用結束)就不讓模型繼續輸出了,
而提示詞的最後一行就是為了避免出現「好,這就來調用 write_file <tool_call>...」這樣導致前面文字輸出就誤判,直接結束迴圈(工具根本還沒執行)。

但還有一個問題,如果模型不聽話,還是說了廢話怎麼辦?
這邊判斷的部分,會先檢查是否執行過工具(用 executed_tool 記錄),執行過才放行到 response 狀態下的 break。

但如果模型真的很皮,第二次調用工具還是先廢話,那就沒救了哈哈。
所以在提示詞的部分我還特別提醒了使用 write_file 以及盡量在使用一次的情況下完成。

但也是有其他的方法可以試試,像是不使用工具,而是叫模型回答只說 JSON 格式的內容,然後再寫把格式放入檔案的邏輯,
但這樣做同樣怕模型不聽話,輸出了除了格式以外的其他廢話。

模型輸出的部分跟在 main.py 一樣是用 for 來獲取出 chat 裡 yield 回傳的內容,然後根據上面所說的邏輯做出停止的邏輯:

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
	while True:
		...
		
		executed_tool = False
        for chunk in model.chat(
            user_input=prompt,
            tool_approval=get_tool_approval
        ):
            if chunk.status == "tool_executed":
                executed_tool = True
            
            elif chunk.status == "response" and executed_tool:
                break

驗證結果

模型執行完後,不能盲目的認定已經修復好了,還需要對其做驗證:

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
	while True:
		...
		
		success, config = ConfigManager.load_config()

這邊就是前面說過的,如果失敗的話,讓使用者選擇是否繼續修復,
所以在成功以及選擇不繼續時「跳出迴圈」:

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
	while True:
		...
		
		if success:
            break
        else:
            if questionary.confirm(
                "格式驗證失敗,是否交由模型繼續修復",
                default=False,
                style=Style([
                    ("question", "dim"),         
                    ("instruction", "dim")
                ])
            ).ask() is False:
                break

存檔

直到迴圈結束,就可以開始存檔了,分成幾種狀況做處理:

  1. 格式驗證成功。
    1. 覆蓋原先設定檔。
    2. 不使用修復樣式做覆蓋。
      1. 使用預設樣式覆蓋。
      2. 不覆蓋。
  2. 格式驗證失敗。
    1. 使用預設樣式覆蓋。
    2. 不覆蓋。
      可以看到下圖,就是等等要做的邏輯部分:
      ![[meowgent_repair_flowchart.png|417]]

先準備好 cover_confirm(是否覆蓋)、use_default_confirm(使用預設覆蓋) 兩個布林值,接著先處理情況 1 分割成 1-1 及 1-2:

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
	while True:
		...
	
	# 狀態初始化	
	cover_confirm = False
    use_default_confirm = False 
    
    if success: # 如果驗證格式成功
        cli.console.print(Padding("[green]模型成功修復設定檔[/green]", (0, 0, 0, 2)))
        cover_confirm = questionary.confirm(
            "已成功修復設定檔,是否覆蓋套用?",
            default=True,
            style=Style([
                ("question", "dim"),         
                ("instruction", "dim")
            ])
        ).ask()

情況 1-1,模型的修復被批准了,調用 save_config() 做存檔,回傳資料模型給 main.py:

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
	if cover_confirm: # 使用者同意套用模型的修復
    
		ConfigManager.save_config(config)

		cli.console.print(Padding("[green]修復檔成功覆蓋[/green]", (0, 0, 0, 2)))
		return config

仔細看會發現,1-2 和 2 底下的情況是一樣的,這裡直接共用判斷:

細節處我們用 remind 做出區別。

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
	
	if not success or not cover_confirm:
        # 如果模型修復的還是沒辦法讀 or 使用者拒絕用模型修復的

        remind = "已取消使用模型修復的檔案" if success else "模型修復失敗"

        use_default_confirm = questionary.confirm(
            f"{remind},是否將預設設定覆蓋到設定檔(已將損壞檔案備份於 {backup_path})",
            default=True,
            style=Style([
                ("question", "dim"),         
                ("instruction", "dim")
            ])
        ).ask()

接下來是用預設來做覆蓋,也就是 1-2-1 跟 2-1,也是要回傳資料模型(在這裡回傳的是預設的設定):

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
		
	if use_default_confirm: # 使用者同意套用預設值 -> 重置為全新乾淨的預設物件!

        config = MeowgentConfig()
        ConfigManager.save_config(config)

        cli.console.print(Padding("[green]已套用預設設定[/green]", (0, 0, 0, 2)))

        return config

最後剩下的就是 1-2-2 和 2-2 的不做任何覆蓋了,這邊需要把原始的檔案做恢復(把備份檔複製回原始的檔案做覆蓋),同樣是用到 shutil.copy2(),最後回傳資料模型:

# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
	...
		
	if use_default_confirm:
		...
		
	else: # 使用者不同意套用預設 -> 還原原始備份,當次以預設物件開機

        shutil.copy2(backup_path, CONFIG_FILE)

        cli.console.print(Padding("[green]備份檔已恢復[/green]", (0, 0, 0, 2)))

        return MeowgentConfig()

哇~今天這篇好長啊,應該是至今為止最長的一篇了!
我們把昨天做好的設定項目都給套用了上去,還做了讓模型修復 JSON 設定檔的應用。
但你有沒有發現,這些設定都還沒有可以做更改的管道呢,下一篇,我們來處理這件事!


上一篇
Day 17 - 設定檔 - 上
系列文
手刻 AI Agent!大一新生的 Python 實戰筆記 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言