iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
AI Engineering

從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理系列 第 8

讓程式在出錯時不會停下來:例外處理與 Agent 的穩定性

  • 分享至 

  • xImage
  •  

前七天我們把 Python 的基本骨架都走過一遍了:變數、判斷、迴圈、List、Dictionary、Function。到這裡為止,我們寫的程式有一個共同特徵——只要有一個地方出錯,整支程式就直接停掉

這在練習的時候沒什麼感覺,反正改一改再跑一次就好。但如果把場景換成後面要做的「生活助理」,情況就完全不一樣了:

使用者說「幫我安排明天的行程」,助理去抓天氣 API,結果剛好網路斷線。

如果程式直接崩潰,使用者看到的就是一大串紅字,而不是「抱歉,我現在拿不到天氣資料,先幫你排其他的」。

一個會自主做事的 Agent,最重要的能力之一就是在事情出錯時還能繼續往下走。今天就來處理這件事。


一、錯誤是什麼?

Python 的錯誤大致可以分成兩種:

1. 語法錯誤(SyntaxError):你程式碼寫錯了,Python 根本讀不懂,連跑都跑不起來。

if x = 5:      # 應該是 ==
    print("hi")

這種錯誤只能自己改,沒得救。

2. 執行時錯誤(Exception):程式碼本身是合法的,但執行到某一行的時候出事了。

numbers = [1, 2, 3]
print(numbers[10])        # IndexError:索引超出範圍

data = {"name": "Alice"}
print(data["age"])        # KeyError:字典裡沒有這個 key

print(int("abc"))         # ValueError:字串轉不成數字

print(10 / 0)             # ZeroDivisionError:除以零

今天要處理的是第二種。這類錯誤的共同點是:它跟「當下的資料」有關,不是跟「程式碼」有關。而 Agent 面對的資料永遠來自外部(使用者輸入、API 回應、檔案內容),所以這種錯誤是必然會發生的。


二、try / except:先試試看,出事再說

基本語法長這樣:

try:
    # 有可能出錯的程式碼
    result = 10 / 0
except ZeroDivisionError:
    # 出錯時要做的事
    print("不能除以零喔")

執行流程是:

  1. Python 先執行 try 區塊裡的程式碼。
  2. 如果順利跑完,就跳過 except,繼續往下。
  3. 如果中途出錯,而且錯誤型態符合 except 後面寫的,就跳到 except 區塊執行。
  4. 整支程式不會停下來。

我們把 Day 2 練習過的「讓使用者輸入數字」改寫看看:

def ask_number(prompt):
    while True:
        try:
            return int(input(prompt))
        except ValueError:
            print("請輸入數字,例如 25")

age = ask_number("請輸入年齡:")
print(f"你今年 {age} 歲")

以前使用者只要不小心打了「二十五」,程式就爆掉。現在它會禮貌地再問一次。

這個「出錯就重問」的模式,其實就是 Agent 最原始的**重試(retry)**邏輯。


三、多種錯誤分開處理

一段程式碼可能有很多種出錯方式,我們可以寫多個 except

def divide(a, b):
    try:
        return int(a) / int(b)
    except ValueError:
        print("輸入的不是數字")
        return None
    except ZeroDivisionError:
        print("除數不能是 0")
        return None

print(divide("10", "2"))    # 5.0
print(divide("10", "abc"))  # 輸入的不是數字 → None
print(divide("10", "0"))    # 除數不能是 0 → None

Python 會由上往下比對,第一個符合的 except 會被執行,其他就跳過。

如果你想一次抓多種:

except (ValueError, ZeroDivisionError):
    print("輸入有問題")

四、else 與 finally

完整的結構其實有四個部分:

def read_config(path):
    try:
        f = open(path, "r", encoding="utf-8")
        content = f.read()
    except FileNotFoundError:
        print(f"找不到檔案:{path}")
        return None
    else:
        # 沒有出錯才會執行
        print("讀取成功")
        return content
    finally:
        # 不管有沒有出錯,都一定會執行
        print("這行一定會跑")
  • else:try 區塊沒有出錯時才執行。
  • finally不管如何都會執行,通常用來做「收尾」,例如關閉檔案、關閉連線。

對 Agent 來說,finally 特別有用——不管這次工具呼叫成功或失敗,我們都想把「這次做了什麼」記錄下來。


五、取得錯誤的詳細內容

只印「出錯了」對除錯沒什麼幫助。我們可以把錯誤物件接起來:

try:
    value = int("abc")
except ValueError as e:
    print(f"發生錯誤:{e}")
    # 發生錯誤:invalid literal for int() with base 10: 'abc'

as e 把錯誤物件綁到變數 e,印出來就是 Python 給的錯誤訊息。

如果你想抓「任何錯誤」,可以用 Exception

try:
    do_something()
except Exception as e:
    print(f"未預期的錯誤:{type(e).__name__}: {e}")

⚠️ 但要小心:except Exception 會把所有錯誤都吞掉,包括你自己寫錯的 bug。平常盡量寫明確的錯誤型態,只有在「最外層」(例如整個 Agent 的主迴圈)才用它當最後一道防線。


六、主動丟出錯誤:raise

有時候「合法但不合理」的情況,Python 不會報錯,但我們希望它報錯:

def set_temperature(celsius):
    if celsius < -273.15:
        raise ValueError("溫度不能低於絕對零度")
    return celsius

raise 會主動製造一個錯誤。這在寫 Function 的時候很重要:與其讓錯誤的資料默默往下流,不如在源頭就擋下來

搭配 Day 7 的 Function 概念,一個「好工具」應該要有明確的邊界檢查:

def get_weather(city):
    if not isinstance(city, str) or not city.strip():
        raise ValueError("city 必須是非空字串")
    # ...實際查詢邏輯

七、把這一切連到 Agent

現在把今天的東西放進 Agent 的脈絡裡。

一個 Agent 在執行任務時,大致是這樣的流程:

使用者輸入 → 決定要用哪個工具 → 執行工具 → 拿到結果 → 回覆

每一個箭頭都可能出錯

階段 可能的錯誤 沒處理會怎樣
使用者輸入 空字串、亂打 後面全部爛掉
決定工具 模型給了不存在的工具名 KeyError,程式停掉
執行工具 網路斷線、API 額度用完 整個對話中斷
解析結果 回傳的不是預期格式 JSONDecodeError

所以我們希望的不是「不會出錯」,而是「出錯時 Agent 知道自己出錯了,並且能把這件事變成下一步的判斷依據」。

用今天學到的東西,可以先寫出一個雛形:

def run_tool(tool_name, tools, arguments):
    """執行工具,並把錯誤轉換成可以回報給模型的文字。"""
    if tool_name not in tools:
        return {"ok": False, "error": f"沒有名為 {tool_name} 的工具"}

    try:
        result = tools[tool_name](**arguments)
    except ValueError as e:
        return {"ok": False, "error": f"參數有問題:{e}"}
    except Exception as e:
        return {"ok": False, "error": f"工具執行失敗:{type(e).__name__}: {e}"}
    else:
        return {"ok": True, "result": result}


# 用法
def get_weather(city):
    if city == "火星":
        raise ValueError("這個城市不在地球上")
    return f"{city} 今天晴天 26 度"

tools = {"get_weather": get_weather}

print(run_tool("get_weather", tools, {"city": "台北"}))
# {'ok': True, 'result': '台北 今天晴天 26 度'}

print(run_tool("get_weather", tools, {"city": "火星"}))
# {'ok': False, 'error': '參數有問題:這個城市不在地球上'}

print(run_tool("查星座", tools, {}))
# {'ok': False, 'error': '沒有名為 查星座 的工具'}

注意這裡的關鍵設計:錯誤沒有讓程式停下來,而是變成一段可以被閱讀的文字

後面(Day 21、22)我們會看到,這段文字可以原封不動地丟回給 AI,讓它知道「剛剛那個工具失敗了,原因是 XXX」,然後它可以自己決定要換一個工具、換一組參數,還是直接告訴使用者做不到。

這就是「自主決策」的起點——Agent 要能看見失敗,才有辦法繞過失敗


八、一個小練習

試著把 Day 6 寫過的「待辦清單」加上例外處理:

todos = []

def add_todo(title, priority):
    if not title.strip():
        raise ValueError("標題不能是空的")
    if priority not in ("高", "中", "低"):
        raise ValueError(f"優先度只能是 高/中/低,你給的是 {priority}")
    todos.append({"title": title, "priority": priority, "done": False})
    return todos[-1]


for title, priority in [("買牛奶", "高"), ("", "中"), ("運動", "超高")]:
    try:
        item = add_todo(title, priority)
        print(f"已新增:{item['title']}")
    except ValueError as e:
        print(f"新增失敗:{e}")

print(f"\n目前有 {len(todos)} 筆待辦")

輸出:

已新增:買牛奶
新增失敗:標題不能是空的
新增失敗:優先度只能是 高/中/低,你給的是 超高
目前有 1 筆待辦

三筆資料裡有兩筆有問題,但程式完整跑完了。這就是我們要的。


小結

今天講的是一個很基礎、但常常被略過的東西:例外處理

  • try / except 讓程式在出錯時能繼續往下走
  • else / finally 幫你把「成功才做」和「一定要做」分開
  • raise 讓你在源頭主動擋下不合理的資料
  • 把錯誤轉成文字訊息而不是讓它中斷程式,是 Agent 能「知道自己失敗了」的前提

明天要講檔案讀寫——讓程式的記憶不再只存在於記憶體裡,關掉之後還記得住。這也是 Agent 長期記憶的第一步。

我們明天見。


上一篇
將程式碼打包成可重用的模組:Function 與 Agent 的 Tool 第一步
下一篇
讓資料離開記憶體:檔案讀寫與 Agent 的長期記憶
系列文
從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言