在上一篇 Claude Agent SDK 入門教學中,我們學會了如何建構基本的 AI Agent。本篇將深入探討兩個進階主題:Guardrails(護欄)與 Handoffs(任務移交)。這兩者是打造生產級 Agent 應用的關鍵機制,讓你的 Agent 既安全可靠,又能有效處理複雜任務。
什麼是 Guardrails?
Guardrails(護欄)是在 Agent 執行前後加入的驗證機制,用來確保輸入合法、輸出符合預期。簡單來說,就是在 Agent 的「進出口」設立檢查站。
與 OpenAI Agents SDK 使用裝飾器(@input_guardrail、@output_guardrail)的方式不同,Claude Agent SDK 採用更靈活的設計:Guardrails 就是你自己撰寫的普通函式,在程式碼中直接呼叫,搭配 Hooks 機制實現攔截與驗證。
Guardrails 的兩種類型
| 類型 | 時機 | 用途 | 實作方式 |
|---|---|---|---|
| Input Guardrail | Agent 執行前 | 驗證使用者輸入是否合法 | 普通函式 + 提前返回,或 UserPromptSubmit Hook |
| Output Guardrail | Agent 執行後 | 驗證輸出結果是否符合規範 | 對 result.result 執行驗證函式 |
| Tool-level Guardrail | 工具呼叫前後 | 控制特定工具的存取與行為 | PreToolUse / PostToolUse Hook |
Input Guardrail:過濾不合法輸入
最簡單的輸入護欄是在呼叫 Agent 前用普通函式驗證輸入。以費用申報 Agent 為例,我們要求使用者必須提供金額:
import re
from claude_agent_sdk import query, ClaudeAgentOptions
def check_has_dollar_amount(user_input: str) -> tuple[bool, str | None]:
"""Input Guardrail:檢查是否包含金額"""
if re.search(r"$d+", user_input):
return True, None
return False, "請提供金額(例如:$100)才能處理這個請求。"
async def run_expense_agent(msg: str) -> None:
# Input Guardrail:在 Agent 執行前驗證
allowed, rejection = check_has_dollar_amount(msg)
if not allowed:
print(rejection) # 直接返回,不執行 Agent
return
async for message in query(
prompt=msg,
options=ClaudeAgentOptions(allowed_tools=["Read", "Bash"]),
):
if hasattr(message, "result"):
print(message.result)
使用 UserPromptSubmit Hook 實作 Input Guardrail
若想讓 Guardrail 與 Agent 框架深度整合,可以使用 UserPromptSubmit Hook,這是最接近 OpenAI @input_guardrail 裝飾器的做法:
import re
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher
async def has_dollar_amount_hook(input_data, tool_use_id, context):
"""透過 Hook 實作 Input Guardrail"""
if re.search(r"$d+", input_data["prompt"]):
return {} # 允許繼續執行
# 返回 block 決策,Agent 不會執行
return {
"decision": "block",
"reason": "請提供金額(例如:$100)才能處理這個請求。"
}
async for message in query(
prompt="幫我申報這個費用", # 沒有金額,會被攔截
options=ClaudeAgentOptions(
allowed_tools=["Read", "Bash"],
hooks={
"UserPromptSubmit": [
HookMatcher(hooks=[has_dollar_amount_hook])
]
},
),
):
print(message)
Output Guardrail:驗證輸出結果
Output Guardrail 在 Agent 完成後對輸出進行檢查。以費用申報為例,我們要確認輸出包含明確的核准或拒絕決策:
import re
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
def check_has_decision(result: str) -> tuple[bool, str | None]:
"""Output Guardrail:檢查是否包含明確決策"""
if re.search(r"(核准|拒絕|需要審查|approv|reject|review)", result, re.IGNORECASE):
return True, None
return False, "無法得出明確決策,請重新提交。"
async def run_with_output_guardrail(msg: str) -> None:
messages = []
async for message in query(
prompt=msg,
options=ClaudeAgentOptions(allowed_tools=["Read", "Bash"]),
):
if isinstance(message, ResultMessage):
messages.append(message)
# Output Guardrail:驗證最終輸出
if messages:
final_result = messages[-1].result or ""
ok, override = check_has_decision(final_result)
if not ok:
print(override) # 輸出不符合要求
else:
print(final_result) # 正常輸出
Tool-level Guardrail:保護敏感操作
透過 PreToolUse Hook,你可以在工具執行前攔截危險操作。例如禁止修改 .env 檔案:
import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
const preInput = input as PreToolUseHookInput;
const toolInput = preInput.tool_input as Record<string, unknown>;
const filePath = toolInput?.file_path as string;
const fileName = filePath?.split("/").pop();
if (fileName === ".env") {
return {
hookSpecificOutput: {
hookEventName: preInput.hook_event_name,
permissionDecision: "deny",
permissionDecisionReason: "禁止修改 .env 檔案以保護安全性"
}
};
}
return {}; // 允許其他檔案操作
};
for await (const message of query({
prompt: "更新資料庫設定",
options: {
hooks: {
PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
}
}
})) {
console.log(message);
}
什麼是 Handoffs?
Handoffs(任務移交)是 Multi-agent 架構中的核心概念:主 Agent 將特定任務委派給專門的子 Agent 處理。在 OpenAI Agents SDK 中,Handoffs 會讓第一個 Agent「完全退場」,由第二個 Agent 接手對話;而在 Claude Agent SDK 中,設計哲學略有不同。
OpenAI vs Claude 的 Handoffs 差異
| 面向 | OpenAI Agents SDK | Claude Agent SDK |
|---|---|---|
| 機制 | Handoff 轉移控制權 | Delegation(委派),主 Agent 保持控制 |
| 主 Agent 狀態 | Handoff 後停止運行 | 持續活躍,接收子 Agent 結果 |
| 定義方式 | handoffs=[specialist] | AgentDefinition + agents={} 參數 |
| 結果回傳 | 第二個 Agent 直接輸出 | 子 Agent 結果回傳給主 Agent |
| 適用場景 | 純路由(分診後交棒) | 需要主 Agent 綜合子結果的複雜任務 |
用 AgentDefinition 實作 Handoffs
Claude Agent SDK 透過 AgentDefinition 定義子 Agent,再由主 Agent 透過 Agent 工具進行委派。以費用申報為例:
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
# 定義核准子 Agent:處理符合規定的費用
approver = AgentDefinition(
description="核准符合政策的費用申請。當金額在政策限額內時使用。",
prompt="你是費用核准專員。確認金額與類別,若符合規定即核准,並提醒是否需要收據。",
tools=["mcp__expense__check_policy"],
)
# 定義升級子 Agent:處理超出限額的費用
escalator = AgentDefinition(
description="將超出限額的費用升級給主管。當金額超出政策限額時使用。",
prompt="你是費用升級專員。草擬一行通知給主管,包含金額、類別及超出限額的幅度。",
tools=["mcp__expense__check_policy"],
)
# 主 Agent:根據政策決定路由方向
async for message in query(
prompt="申請出差費用 $350,類別:交通",
options=ClaudeAgentOptions(
system_prompt="根據費用政策,將每筆申請路由到適當的子 Agent 處理。金額在限額內交給 approver,超出則交給 escalator。",
mcp_servers={"expense": {"command": "npx", "args": ["@company/expense-mcp"]}},
allowed_tools=["Agent", "mcp__expense__check_policy"],
agents={"approver": approver, "escalator": escalator},
),
):
if hasattr(message, "result"):
print(message.result)
TypeScript 版本的 Handoffs 實作
import { query } from "@anthropic-ai/claude-agent-sdk";
const approver = {
description: "核准符合政策的費用申請。當金額在政策限額內時使用。",
prompt: "你是費用核准專員。確認金額與類別,若符合規定即核准,並提醒是否需要收據。",
tools: ["mcp__expense__check_policy"] as string[]
};
const escalator = {
description: "將超出限額的費用升級給主管。當金額超出政策限額時使用。",
prompt: "你是費用升級專員。草擬一行通知給主管,包含金額、類別及超出限額的幅度。",
tools: ["mcp__expense__check_policy"] as string[]
};
for await (const message of query({
prompt: "申請出差費用 $350,類別:交通",
options: {
systemPrompt: "根據費用政策,將每筆申請路由到適當的子 Agent 處理。",
mcpServers: {
expense: { command: "npx", args: ["@company/expense-mcp"] }
},
allowedTools: ["Agent", "mcp__expense__check_policy"],
agents: { approver, escalator }
}
})) {
if ("result" in message) console.log(message.result);
}
用 SubagentStart / SubagentStop Hook 監控委派
你可以使用 SubagentStart 和 SubagentStop Hook 追蹤子 Agent 的執行狀況,這對於除錯與效能分析非常有用:
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher
import time
start_times = {}
async def track_subagent_start(input_data, tool_use_id, context):
"""記錄子 Agent 啟動時間"""
agent_id = input_data.get("agent_id", "unknown")
start_times[agent_id] = time.time()
print(f"[子 Agent 啟動] ID: {agent_id}")
return {}
async def track_subagent_stop(input_data, tool_use_id, context):
"""計算子 Agent 執行時間"""
agent_id = input_data.get("agent_id", "unknown")
if agent_id in start_times:
duration = time.time() - start_times[agent_id]
print(f"[子 Agent 完成] ID: {agent_id},耗時: {duration:.2f}秒")
return {}
async for message in query(
prompt="分析這份程式碼並找出所有安全漏洞",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep", "Agent"],
hooks={
"SubagentStart": [HookMatcher(hooks=[track_subagent_start])],
"SubagentStop": [HookMatcher(hooks=[track_subagent_stop])],
},
agents={
"security-scanner": {
"description": "專門掃描安全漏洞的子 Agent",
"prompt": "你是資安專家,專注於找出 SQL Injection、XSS、CSRF 等常見漏洞。",
"tools": ["Read", "Glob", "Grep"]
}
}
),
):
if hasattr(message, "result"):
print(message.result)
Guardrails 與 Handoffs 的組合應用
在實際的生產環境中,Guardrails 與 Handoffs 通常會搭配使用。以下是一個完整的費用申報系統範例,結合了輸入驗證、多 Agent 路由與輸出檢查:
import re
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, HookMatcher, ResultMessage
# ---- Guardrails ----
async def input_guardrail_hook(input_data, tool_use_id, context):
"""Input Guardrail:要求必須包含金額"""
if not re.search(r"$d+", input_data["prompt"]):
return {
"decision": "block",
"reason": "請提供金額(例如:$100)才能處理費用申請。"
}
return {}
# ---- Sub-agents(Handoffs 的目標)----
approver = AgentDefinition(
description="核准符合政策限額的費用申請",
prompt="你是費用核准專員。確認金額符合政策,給出核准通知。",
tools=["mcp__expense__check_policy"],
)
escalator = AgentDefinition(
description="升級超出政策限額的費用申請給主管",
prompt="你是費用升級專員。說明超出限額的情況,草擬主管通知。",
tools=["mcp__expense__check_policy"],
)
# ---- 主 Agent:結合 Guardrails + Handoffs ----
async def process_expense(expense_request: str):
messages = []
async for message in query(
prompt=expense_request,
options=ClaudeAgentOptions(
system_prompt="你是費用申報路由系統。先用 check_policy 確認限額,再根據結果委派給 approver 或 escalator。",
mcp_servers={"expense": {"command": "npx", "args": ["@company/expense-mcp"]}},
allowed_tools=["Agent", "mcp__expense__check_policy"],
agents={"approver": approver, "escalator": escalator},
hooks={
"UserPromptSubmit": [HookMatcher(hooks=[input_guardrail_hook])]
},
),
):
if isinstance(message, ResultMessage):
messages.append(message)
# Output Guardrail:確認輸出包含明確決策
if messages:
result = messages[-1].result or ""
if not re.search(r"(核准|升級|拒絕)", result):
print("⚠️ 無法得出明確決策,請重新提交")
else:
print(result)
# 執行
import asyncio
asyncio.run(process_expense("申請出差費用 $350,類別:交通"))
Hook 的執行優先順序
當多個 Hooks 同時存在時,了解執行順序非常重要:
| 優先順序 | 步驟 | 說明 |
|---|---|---|
| 1 | Hooks 執行 | PreToolUse 等 Hook 先行判斷,可允許、拒絕或繼續 |
| 2 | Deny 規則 | disallowed_tools 設定,即使 bypassPermissions 也有效 |
| 3 | Permission Mode | bypassPermissions / acceptEdits / dontAsk / default |
| 4 | Allow 規則 | allowed_tools 設定的白名單 |
| 5 | canUseTool Callback | 最後的互動確認(dontAsk 模式跳過此步) |
重要原則:當多個 Hook 對同一操作返回不同決策時,deny 優先於 ask,ask 優先於 allow。只要有一個 Hook 返回 deny,操作就會被阻止。
Guardrails 最佳實踐
- 職責單一:每個 Guardrail 只做一件事,透過 Hook 鏈組合多個驗證邏輯,保持程式碼清晰
- 錯誤訊息清楚:拒絕請求時,提供具體的拒絕原因,讓使用者知道該如何修正
- 避免無限迴圈:
UserPromptSubmitHook 若觸發子 Agent,需防止遞迴呼叫;在 Hook 內部加入子 Agent 標記來避免 - 非同步 Hook 用於記錄:純記錄用途的 Hook 使用非同步模式(
async: true),避免阻塞 Agent 執行
Handoffs 最佳實踐
- AgentDefinition 描述要精確:子 Agent 的
description欄位是主 Agent 決定路由的依據,描述要清楚說明「何時」應該使用這個子 Agent - 子 Agent 工具最小化:每個子 Agent 只授予完成任務所需的工具,避免過度授權
- 若需要「真正交棒」:如果你的場景需要子 Agent 完全接管(不回傳給主 Agent),考慮用 Python 薄層 Dispatcher 直接呼叫不同的 Agent,而非依賴 LLM 決策路由
- 監控子 Agent 執行:使用 SubagentStart / SubagentStop Hook 記錄每個子 Agent 的執行時間與結果,便於效能分析與除錯
總結
Guardrails 與 Handoffs 是 Claude Agent SDK 中讓 Agent 達到生產級可靠性的兩大支柱。Guardrails 透過 Hook 機制在 Agent 的「進出口」設立檢查站,確保輸入合法、輸出符合預期、工具使用安全;Handoffs 透過 AgentDefinition 實現多 Agent 協作,讓複雜任務被分解到專門的子 Agent 處理,主 Agent 保持對全局的控制。
掌握這兩個機制後,你就具備了打造複雜 Agent 應用的核心能力。在接下來的系列文章中,我們將進一步探討如何將這些技術應用在實戰專案中,打造真正解決問題的自動化工具。