Agent SDK 進階:Guardrails 與 Handoffs

上一篇 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 GuardrailAgent 執行前驗證使用者輸入是否合法普通函式 + 提前返回,或 UserPromptSubmit Hook
Output GuardrailAgent 執行後驗證輸出結果是否符合規範對 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 SDKClaude 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 同時存在時,了解執行順序非常重要:

優先順序步驟說明
1Hooks 執行PreToolUse 等 Hook 先行判斷,可允許、拒絕或繼續
2Deny 規則disallowed_tools 設定,即使 bypassPermissions 也有效
3Permission ModebypassPermissions / acceptEdits / dontAsk / default
4Allow 規則allowed_tools 設定的白名單
5canUseTool Callback最後的互動確認(dontAsk 模式跳過此步)

重要原則:當多個 Hook 對同一操作返回不同決策時,deny 優先於 ask,ask 優先於 allow。只要有一個 Hook 返回 deny,操作就會被阻止。

Guardrails 最佳實踐

  • 職責單一:每個 Guardrail 只做一件事,透過 Hook 鏈組合多個驗證邏輯,保持程式碼清晰
  • 錯誤訊息清楚:拒絕請求時,提供具體的拒絕原因,讓使用者知道該如何修正
  • 避免無限迴圈UserPromptSubmit Hook 若觸發子 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 應用的核心能力。在接下來的系列文章中,我們將進一步探討如何將這些技術應用在實戰專案中,打造真正解決問題的自動化工具。