這個系列的標題是「從 Prompt 到自主決策」。走到第 25 天,該來正面回答這個問題了:
什麼是「自主」?什麼時候該放手?
「自主」不是一個開關,是一個光譜。我把它拆成三個可以分開決定的面向:
最低的自主層次。目標和步驟都由我們定,AI 只決定執行細節。
# 我們決定:要產生一段天氣建議
# AI 決定:怎麼寫這段話
advice = llm.summarize(weather_data)
**這個層次幾乎沒有風險。**最壞的情況是文字寫得不好。
AI 決定要呼叫哪些工具、什麼順序、幾次。這就是 Day 21–22 的 Tool Use。
# 我們決定:目標是「回答使用者的問題」
# AI 決定:要查天氣?查待辦?都查?先查哪個?
agent.run("我明天要不要帶傘?")
**風險中等。**AI 可能選錯工具、漏掉步驟、或陷入迴圈。但因為工具是我們定義的,最壞的結果被工具的能力範圍限制住。
AI 判斷「該不該執行某個動作」——特別是有副作用、不可逆的動作。
# AI 決定:現在該不該幫使用者刪掉這筆資料 / 發出這封信 / 下這個單
風險最高。而這正是我認為現階段不應該完全放手的部分。
決定要不要放手,我會問四個問題:
| 後果 | 例子 | 建議 |
|---|---|---|
| 沒什麼 | 回答的文字不夠好 | 放手 |
| 浪費一點資源 | 多查了一次天氣 | 放手 |
| 使用者要花時間修正 | 記錯了待辦事項 | 放手,但要能撤銷 |
| 無法復原 | 刪除資料、發送訊息、付款 | 一定要確認 |
分界線就在「能不能復原」。
可復原的錯誤,讓 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"請用目前已有的資訊直接給出答案,不要再呼叫工具。)"
),
}],
})
這樣模型會「收尾」,而不是被硬生生切斷。優雅降級比直接失敗好。
放手的同時,要把邊界寫清楚。這是我實際在用的版本:
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 限制輪數、工具呼叫次數、成本;接近上限時提醒模型收尾明天講最後一塊拼圖:記憶。讓 Agent 記得你是誰、你喜歡什麼。