昨天手刻了 ReAct 迴圈,也給了兩個護欄(迭代上限、成本上限)。今天處理讓它能真正上線的三件事:工具怎麼接得可維護、多代理什麼時候值得、怎麼確保它不會闖禍。
問題一:整合成本的乘法。 你有 3 個 AI 應用(客服機器人、內部助理、程式助理),要接 8 個內部系統(訂單、庫存、工單、CRM……)。傳統做法是每個應用各自實作一次整合——3 × 8 = 24 份要維護的整合程式碼,而且每份的參數格式、錯誤處理都不一樣。
問題二:代理闖禍。 一個能寄信的代理,在某次執行中把內部成本表寄給了外部廠商。事後查日誌,只看到「呼叫了 send_email」,不知道為什麼它決定這樣做。
這兩個問題,一個關於架構,一個關於責任。
📊 職缺訊號
Agent 相關任務出現在 47.6% 的生成式 AI 職缺中。但 JD 的層次差異很大:初階寫「串接 LLM 與內部 API」,資深寫「設計 agent 的權限邊界與可觀測性」。今天的內容就是那條分界線。
Model Context Protocol(MCP)是 2024 年底提出的開放協定,解決的正是問題一:

圖 22-1:MCP 如何降低整合成本(示意架構)。每個系統只需實作一次 MCP Server,任何支援 MCP 的應用都能使用。
MCP 定義了三種可以被暴露的東西:
| 概念 | 是什麼 | 例子 |
|---|---|---|
| Tools | 模型可以呼叫的動作 | lookup_order、create_ticket |
| Resources | 模型可以讀取的資料 | 檔案內容、資料庫查詢結果 |
| Prompts | 預先寫好的提示範本 | 「分析這張工單的緊急程度」 |
對台灣企業的實際意義:如果你們有一套用了十幾年的 ERP,實作一次 MCP Server 之後,未來所有 AI 應用都能用同一個介面接它——而不是每個新專案都重寫一次整合。
"""一個最小的 MCP Server(訂單查詢)。"""
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("order-system")
@mcp.tool()
def lookup_order(order_id: str) -> dict:
"""查詢訂單的出貨狀態與物流編號。
當使用者詢問訂單進度、何時送達、物流狀態時使用。
"""
order = erp.get_order(order_id) # 接你既有的系統
if not order:
return {"error": "查無此訂單"}
return {"status": order.status, "tracking": order.tracking_no}
if __name__ == "__main__":
mcp.run()
⚠️ MCP 不會自動解決權限問題
MCP Server 的工具依然要自己做權限檢查。實務上要在協定之外,建立呼叫者身分傳遞的機制(例如以 token 帶入使用者身分),否則你只是把一個沒有權限控制的 API 換了個介面暴露出去——而且暴露給更多應用。
多代理系統(一個代理拆任務、多個代理各司其職)聽起來很美,但實務上有四種常見的失敗:
| 失敗模式 | 症狀 | 根因 |
|---|---|---|
| 傳話遊戲 | 資訊經過三個代理後失真,最終答案與原始需求無關 | 每次傳遞都是一次有損壓縮 |
| 成本爆炸 | 一次請求花了 40 次 LLM 呼叫 | 代理之間反覆確認、互相委派 |
| 責任真空 | 出錯了不知道是哪個代理的問題 | 沒有端到端的 trace |
| 無限委派 | A 叫 B 做、B 覺得該 A 做,來回踢皮球 | 沒有明確的終止條件 |
實務建議:多代理的門檻應該設得比你想的更高。
只有當「不同子任務需要明顯不同的工具集或系統提示,且放在同一個代理裡會互相干擾」時,才值得拆成多代理。
「因為架構圖比較好看」不是理由。

圖 22-2:Agent 可靠性工程的五道護欄(示意架構)——迭代上限、成本上限、沙箱、審批關卡與 trace 在系統中的位置。
昨天實作了前兩道,今天補完後三道。
"""guard.py —— 依「可逆性」與「影響範圍」把動作分級。"""
from enum import Enum
class RiskLevel(Enum):
READ = 1 # 唯讀:查詢、搜尋 → 直接執行
WRITE_OWN = 2 # 寫入自己的資料:建立草稿、加註記 → 直接執行但記錄
WRITE_SHARED = 3 # 寫入共享資料:更新工單、改狀態 → 需確認
EXTERNAL = 4 # 對外:寄信、呼叫外部 API → 必須人工審批
DESTRUCTIVE = 5 # 不可逆:刪除、退款、下單 → 必須人工審批 + 二次確認
TOOL_RISK = {
"search_kb": RiskLevel.READ,
"lookup_order": RiskLevel.READ,
"add_note": RiskLevel.WRITE_OWN,
"update_ticket": RiskLevel.WRITE_SHARED,
"send_email": RiskLevel.EXTERNAL, # ← 開頭那個事故的元凶
"issue_refund": RiskLevel.DESTRUCTIVE,
}
async def execute_with_guard(tool_name, args, ctx):
risk = TOOL_RISK.get(tool_name, RiskLevel.DESTRUCTIVE) # 未知工具視為最高風險
if risk.value >= RiskLevel.EXTERNAL.value:
# 不直接執行,而是回傳「待審批」,讓 UI 呈現給人確認
approval_id = await create_approval_request(
user=ctx["user"], tool=tool_name, args=args,
reason=ctx.get("current_reasoning", "")) # 附上模型的理由讓人判斷
return {"status": "pending_approval", "approval_id": approval_id,
"message": "此動作需要您確認後才會執行"}
result = await execute_tool(tool_name, args, ctx)
await audit_log(user=ctx["user"], tool=tool_name, args=args,
risk=risk.name, result_ok="error" not in result)
return result
TOOL_RISK.get(tool_name, RiskLevel.DESTRUCTIVE) 這一行是刻意的:未知的工具預設為最高風險。 安全設計的通則是「預設拒絕」,不是「預設允許」。
如果你的代理會執行程式碼或處理不可信的檔案,執行環境必須隔離:容器化執行、禁止對外網路、限制檔案系統存取、設定 CPU 與記憶體上限、執行逾時。這是 Day 12 容器知識的直接應用——又一個雙主軸交會的地方。
"""observability.py —— 沒有 trace,代理就是黑盒子。"""
async def run_agent_traced(question, ctx):
trace_id = str(uuid.uuid4())
span = {"trace_id": trace_id, "user": ctx["user"].id,
"question": question, "steps": [], "started_at": now()}
try:
for i in range(MAX_ITER):
resp = await call_llm(...)
span["steps"].append({
"i": i,
"reasoning": resp.choices[0].message.content, # ← 為什麼這樣決定
"tool_calls": [{"name": c.function.name,
"args": json.loads(c.function.arguments)}
for c in (resp.choices[0].message.tool_calls or [])],
"tokens": resp.usage.total_tokens,
"latency_ms": elapsed(),
})
...
finally:
span["ended_at"] = now()
span["total_tokens"] = sum(s["tokens"] for s in span["steps"])
await save_trace(span) # 送到 Langfuse / OpenTelemetry / 自建資料表
return result
reasoning 這個欄位是關鍵。 開頭那個「寄錯信」的事故,如果有記錄模型的推理過程,你就會看到它讀到了 PDF 裡的某段文字然後「決定」要照做——這正是 Day 25 要談的間接提示注入。沒有這個欄位,你只會看到一行「呼叫了 send_email」,永遠查不出原因。
| 框架 | 適合 | 注意 |
|---|---|---|
| 原生 SDK 手刻 | 單一代理、流程可控 | 昨天那 30 行就是全部;除錯最容易 |
| LangGraph | 需要明確的狀態機與分支 | 學習曲線;但狀態管理確實比自己寫穩 |
| AutoGen/CrewAI | 多代理協作的快速原型 | 成本與行為較難預測,上生產前務必壓成本上限 |
| 雲端 Agent 服務 | 想少維運 | 可觀測性與權限模型要先確認是否夠用 |
上線前的檢核清單(缺任何一項都不該上生產):
□ 迭代次數上限(建議 5–10)
□ 單次任務成本上限(超過就中止並降級)
□ 每個工具都有明確的風險分級
□ 所有 EXTERNAL 與 DESTRUCTIVE 動作有人工審批關卡
□ 工具權限以「呼叫者身分」限制,不依賴提示
□ 完整 trace(含 reasoning、工具參數、token、延遲)
□ 失敗時有降級路徑(轉人工,而不是回一個錯誤)
□ 執行不可信內容時有沙箱隔離
□ 評測集包含「代理該拒絕的請求」(Day 24–25)
如果你走 MLOps 路線,代理系統會給你三個傳統 ML 服務沒有的維運挑戰:
其一:成本不可預測。 傳統推論服務每次請求的成本幾乎固定,代理不是——同一個請求可能花 3 次或 15 次 LLM 呼叫。這代表容量規劃與成本預算的方法要改:不能用「平均成本 × 請求數」,要看成本的分布與 P99。
其二:延遲極長且變異大。 代理跑完可能要 30 秒到 3 分鐘。這讓 Day 13 的同步 API 模式不再適用,要改成非同步任務 + 狀態查詢(提交任務拿到 ID、輪詢或推播結果),並且逾時設定要重新設計。
其三:trace 的資料量很大。 每次執行要記錄多輪的推理、工具參數與結果。這對日誌儲存、查詢效能與保存政策都是新的壓力——而且其中可能含個資(Day 27)。
這三點都是雙主軸交會的具體例子:它們是生成式 AI 帶來的問題,但要用 MLOps 的方法解決。
💡 一個關於「自主性」的觀念
很多人把「需要人工審批」看成 agent 不夠聰明的證據。我的看法相反:願意在正確的地方停下來等人確認的系統,才是成熟的系統。
想想真實世界的組織——新進員工也不會有無限授權。權限是隨著信任逐步擴大的,AI 系統也該如此。 先從唯讀開始,累積足夠的正確率數據後,再逐步放寬。