iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
AI Engineering

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

自主決策的分界線:什麼時候該讓 AI 自己決定

  • 分享至 

  • xImage
  •  

這個系列的標題是「從 Prompt 到自主決策」。走到第 25 天,該來正面回答這個問題了:

什麼是「自主」?什麼時候該放手?


一、自主的三個層次

「自主」不是一個開關,是一個光譜。我把它拆成三個可以分開決定的面向:

1. 決定「怎麼做」(How)

最低的自主層次。目標和步驟都由我們定,AI 只決定執行細節。

# 我們決定:要產生一段天氣建議
# AI 決定:怎麼寫這段話
advice = llm.summarize(weather_data)

**這個層次幾乎沒有風險。**最壞的情況是文字寫得不好。

2. 決定「做什麼」(What)

AI 決定要呼叫哪些工具、什麼順序、幾次。這就是 Day 21–22 的 Tool Use。

# 我們決定:目標是「回答使用者的問題」
# AI 決定:要查天氣?查待辦?都查?先查哪個?
agent.run("我明天要不要帶傘?")

**風險中等。**AI 可能選錯工具、漏掉步驟、或陷入迴圈。但因為工具是我們定義的,最壞的結果被工具的能力範圍限制住。

3. 決定「要不要做」(Whether)

AI 判斷「該不該執行某個動作」——特別是有副作用、不可逆的動作。

# AI 決定:現在該不該幫使用者刪掉這筆資料 / 發出這封信 / 下這個單

風險最高。而這正是我認為現階段不應該完全放手的部分。


二、一個判斷框架

決定要不要放手,我會問四個問題:

問題一:錯了會怎樣?(Cost of Error)

後果 例子 建議
沒什麼 回答的文字不夠好 放手
浪費一點資源 多查了一次天氣 放手
使用者要花時間修正 記錯了待辦事項 放手,但要能撤銷
無法復原 刪除資料、發送訊息、付款 一定要確認

分界線就在「能不能復原」。

可復原的錯誤,讓 AI 自由嘗試沒關係。不可復原的,一定要有人(或明確的規則)把關。

問題二:我能不能事先列出所有情況?

如果能,就用 Workflow:

if intent == "query_weather": ...
elif intent == "add_todo": ...

如果不能(使用者可能問任何事),才需要 Agent 的彈性。

實務上大部分系統是 80/20:80% 的請求落在你能列舉的情況裡,20% 是長尾。用 Routing 處理前者,Agent 兜底後者(Day 23 的做法)。

問題三:錯了能不能被發現?

# ✅ 錯了看得出來
"明天降雨機率 60%" → 使用者看天氣預報就知道對不對

# ⚠️ 錯了看不出來
"根據你過去三個月的習慣,建議調整作息" → 使用者無從驗證

如果錯誤不容易被發現,就不該讓 AI 自主決定。

這也是為什麼我在 system prompt 裡一直強調「引用具體數字」——數字可以被檢查,模糊的建議不行。

問題四:值得嗎?

Agent 比 Workflow 貴 3~10 倍,慢 2~5 倍。

如果一個任務用固定流程就能做到 95% 的效果,那 Agent 多出來的 5% 值那個價錢嗎?

有時候值得(開放式的探索任務),有時候不值得(每天固定的早安簡報)。


三、實作:確認機制

對於不可逆的操作,加上確認層:

"""agent/confirm.py"""

import logging

logger = logging.getLogger(__name__)


class ConfirmationRequired(Exception):
    """這個操作需要使用者確認。"""

    def __init__(self, tool_name, arguments, description):
        self.tool_name = tool_name
        self.arguments = arguments
        self.description = description
        super().__init__(description)


class ConfirmationPolicy:
    """決定哪些操作需要確認。"""

    def __init__(self, mode="ask"):
        """
        Args:
            mode: "ask"(危險操作要問)、"auto"(全部放行)、
                  "strict"(所有有副作用的都要問)
        """
        self.mode = mode
        self.approved_this_session = set()

    def needs_confirmation(self, tool, arguments) -> bool:
        if self.mode == "auto":
            return False

        if self.mode == "strict":
            return "write" in tool.tags or tool.dangerous

        # 預設模式:只有明確標記 dangerous 的要問
        return tool.dangerous

    def describe(self, tool, arguments) -> str:
        """產生給使用者看的確認訊息。"""
        args_text = "、".join(f"{k}={v}" for k, v in (arguments or {}).items())
        return f"即將執行「{tool.name}」({args_text})\n{tool.description.split('。')[0]}。"

接進 ToolBox:

def execute(self, name, arguments, tracer=None, policy=None, confirm_fn=None):
    tool = self.get(name)

    if tool is None:
        available = "、".join(self._tools)
        return {"ok": False,
                "content": f"沒有名為「{name}」的工具。可用的有:{available}",
                "duration": 0.0}

    # 確認關卡
    if policy and policy.needs_confirmation(tool, arguments):
        description = policy.describe(tool, arguments)
        logger.info("需要確認:%s", description)

        approved = confirm_fn(description) if confirm_fn else False

        if not approved:
            return {
                "ok": False,
                "content": (
                    "使用者拒絕執行這個操作。"
                    "請不要再嘗試,改為詢問使用者想怎麼做。"
                ),
                "duration": 0.0,
            }

    if tracer:
        tracer.tool_call(name, arguments)
    result = tool.safe_run(arguments)
    if tracer:
        tracer.tool_result(name, result["ok"], result["content"], result["duration"])
    return result

CLI 的確認函式:

def cli_confirm(description):
    print(f"\n⚠️  {description}")
    answer = input("   確定要執行嗎?(y/N) ").strip().lower()
    return answer in ("y", "yes", "是")

實際體驗:

你:幫我把第 3 筆待辦刪掉

⚠️  即將執行「delete_todo」(todo_id=3)
   永久刪除一筆待辦事項,這個操作無法復原。
   確定要執行嗎?(y/N) n

小幫:好的,我沒有刪除。你是想把它標記成完成,還是改成別的內容?

注意拒絕時回傳的訊息:

"使用者拒絕執行這個操作。請不要再嘗試,改為詢問使用者想怎麼做。"

**它不只說「被拒絕」,還告訴模型下一步該做什麼。**沒有這句的話,模型可能會換個參數再試一次。


四、實作:預算控制

自主的另一個代價是成本不可控。加上預算上限:

"""agent/budget.py"""

import logging

logger = logging.getLogger(__name__)

# 每百萬 token 的價格(美元),以實際公告為準
PRICING = {
    "claude-opus-5": {"input": 5.00, "output": 25.00},
    "claude-sonnet-5": {"input": 2.00, "output": 10.00},
    "claude-haiku-4-5": {"input": 1.00, "output": 5.00},
}


class BudgetExceeded(Exception):
    """超出預算上限。"""


class Budget:
    """追蹤並限制一次任務的資源消耗。"""

    def __init__(self, max_turns=10, max_tool_calls=20,
                 max_cost_usd=0.50, model="claude-opus-5"):
        self.max_turns = max_turns
        self.max_tool_calls = max_tool_calls
        self.max_cost_usd = max_cost_usd
        self.model = model

        self.turns = 0
        self.tool_calls = 0
        self.input_tokens = 0
        self.output_tokens = 0

    @property
    def cost_usd(self):
        price = PRICING.get(self.model, PRICING["claude-opus-5"])
        return (
            self.input_tokens / 1_000_000 * price["input"]
            + self.output_tokens / 1_000_000 * price["output"]
        )

    def record_turn(self, response):
        self.turns += 1
        self.input_tokens += response.usage.input_tokens
        self.output_tokens += response.usage.output_tokens
        self._check()

    def record_tool_call(self):
        self.tool_calls += 1
        self._check()

    def _check(self):
        if self.turns > self.max_turns:
            raise BudgetExceeded(f"超過最大輪數 {self.max_turns}")
        if self.tool_calls > self.max_tool_calls:
            raise BudgetExceeded(f"超過最大工具呼叫次數 {self.max_tool_calls}")
        if self.cost_usd > self.max_cost_usd:
            raise BudgetExceeded(f"超過成本上限 ${self.max_cost_usd:.2f}")

    @property
    def remaining_turns(self):
        return max(0, self.max_turns - self.turns)

    def report(self):
        return (
            f"{self.turns} 輪、{self.tool_calls} 次工具呼叫、"
            f"{self.input_tokens:,}+{self.output_tokens:,} tokens、"
            f"約 ${self.cost_usd:.4f}"
        )

用在 Agent 迴圈裡:

def run(self, user_input, budget=None, ...):
    budget = budget or Budget()

    while True:
        try:
            response = self.client.messages.create(...)
            budget.record_turn(response)
        except BudgetExceeded as e:
            logger.warning("預算用盡:%s", e)
            return f"這個任務比預期複雜({e}),我先停下來。要我繼續嘗試嗎?"
        ...

還有一個進階技巧——讓模型知道自己的預算:

if budget.remaining_turns <= 2:
    messages.append({
        "role": "user",
        "content": [{
            "type": "text",
            "text": (
                f"(系統提示:你還剩 {budget.remaining_turns} 次機會。"
                f"請用目前已有的資訊直接給出答案,不要再呼叫工具。)"
            ),
        }],
    })

這樣模型會「收尾」,而不是被硬生生切斷。優雅降級比直接失敗好。


五、自主 Agent 的 System Prompt

放手的同時,要把邊界寫清楚。這是我實際在用的版本:

AUTONOMOUS_SYSTEM = """你是「小幫」,一個台灣使用者的生活助理。

## 你的目標
協助使用者管理日常生活:行程、待辦事項、天氣判斷。
以「幫使用者省下思考成本」為原則,主動整合資訊、給出可行動的建議。

## 工作方式
- 需要資訊時,直接使用工具取得,不要反問使用者你查得到的事
- 處理任何與「今天」「明天」相關的問題前,先確認目前時間
- 一次可以呼叫多個工具,不要一個一個慢慢來
- 資訊足夠時就給答案,不要為了保險而重複查詢

## 你必須遵守的限制

**絕對不要編造事實。**
天氣、日期、待辦內容一律以工具回傳的結果為準。
工具沒給的資訊,就說「我查不到」。

**不要假裝完成了你做不到的事。**
如果沒有對應的工具,直接說明你目前沒有這個能力,
不要用文字描述一個沒有真正發生的動作。

**不可逆的操作要先問。**
刪除、發送、付款這類動作,執行前先向使用者確認。

**工具失敗時不要盲目重試。**
同一個工具用相同參數失敗兩次,就改用其他方式或告訴使用者遇到的問題。

**不確定就問。**
使用者的要求有多種解讀時,直接反問,不要自己選一個然後往下做。

## 回應風格
繁體中文,台灣用語。三到五句話,具體、有數字、可以行動。
不要用條列符號,用自然的口語。
"""

我覺得這份 prompt 有幾個地方值得說明:

「不要反問使用者你查得到的事」
沒有這句的話,模型常常會問「請問您想查詢哪個城市的天氣?」——但我們明明有預設值。

「不要假裝完成了你做不到的事」
這是我遇過最頭痛的問題。模型會說「已經幫你設定明天早上七點的鬧鐘」,但它根本沒有鬧鐘工具。這句明確禁止這種行為。

「同一個工具用相同參數失敗兩次」
給了一個具體的數字,而不是模糊的「不要一直重試」。模型對具體規則的遵守度比較高。

「不確定就問」
這其實是「主動放棄自主權」的指令。讓模型知道:問清楚不是無能,是負責。


六、我對「自主」的看法

寫到這裡,想講一些比較主觀的東西。

這三十天做下來,我對「AI 自主決策」的想法有一些改變。

改變一:自主不是目標,是手段

一開始我以為「越自主越厲害」。但實際做下來,大部分情況下,可預測比聰明重要。

早安簡報這種每天跑的東西,我要的是「每天都給我一樣品質的結果」,不是「有時候驚喜、有時候失望」。

所以現在我的預設是:能寫死的就寫死,只在真的需要彈性的地方放手。

改變二:自主的品質取決於「環境設計」

模型的決策品質,跟你給它的東西直接相關:

你給的 影響
Tool description 它知不知道有這個能力、什麼時候該用
Input schema 它填的參數對不對
錯誤訊息 失敗後它知不知道怎麼辦
System prompt 它的行為邊界和優先順序
預算提示 它會不會適時收尾

**這些全部都是我們寫的。**所以「AI 自主決定」的說法其實有點誤導——它是在我們設計的空間裡做選擇。

寫 Agent 的工作,很大一部分是設計那個空間。

改變三:最難的不是讓它做事,是讓它承認做不到

模型很願意幫忙,願意到有時候會「腦補」。

讓它說「我查不到」「我做不到」「我不確定」,比讓它做事難得多。而這恰恰是一個可靠助理最重要的特質。

所以我的 system prompt 裡,關於「不要編造」「不要假裝」的規則,比關於「要做什麼」的規則還多。

改變四:人要留在迴圈裡

不是因為 AI 不夠好,是因為責任不能外包。

如果 Agent 幫我刪掉了一份重要檔案,我不能說「這是 AI 決定的」。那是我的檔案,我的責任。

所以確認機制不是「因為 AI 不可靠」,而是「因為那是我的事」。這個區分我覺得很重要。


七、光譜總表

把整個系列的架構整理成一張表:

層級 誰決定 How 誰決定 What 誰決定 Whether 適用 系列對應
純程式 人 人 人 確定的邏輯 Day 2–17
單次呼叫 AI 人 人 文字處理 Day 18–20
Workflow AI 人 人 固定流程 Day 23–24
Agent(受限) AI AI 人 開放任務 + 確認機制 Day 21–22, 25
Agent(全自主) AI AI AI 低風險、可復原的環境 ⚠️ 謹慎

**我建議停在第四層。**第五層在目前的技術和責任框架下,只適合完全可復原、低風險的場景。


小結

  • 自主有三個層次:決定怎麼做、決定做什麼、決定要不要做
  • 判斷是否放手的四個問題:錯了會怎樣、能不能列舉情況、錯誤看不看得出來、值不值得
  • 分界線在「能不能復原」——不可逆的操作一定要確認
  • 確認被拒絕時,要告訴模型下一步該做什麼,不然它會換個方式再試
  • 用 Budget 限制輪數、工具呼叫次數、成本;接近上限時提醒模型收尾
  • System prompt 裡「不要做什麼」的規則,往往比「要做什麼」更重要
  • 自主的品質取決於我們設計的環境:工具說明、錯誤訊息、行為邊界
  • 人留在迴圈裡,不是因為 AI 不可靠,是因為責任不能外包

明天講最後一塊拼圖:記憶。讓 Agent 記得你是誰、你喜歡什麼。


上一篇
讓流程能轉彎:Workflow 的控制流與狀態管理
系列文
從 Prompt 到自主決策:用 Python × Agentic Workflow 實作生活助理 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言