<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Handoffs &#8211; 小人物看世界</title>
	<atom:link href="https://blog.che-ya.com/tag/handoffs/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.che-ya.com</link>
	<description>軟體工程師的技術筆記</description>
	<lastBuildDate>Wed, 08 Apr 2026 08:07:38 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://blog.che-ya.com/wp-content/uploads/2021/08/cropped-APP_icon-32x32.png</url>
	<title>Handoffs &#8211; 小人物看世界</title>
	<link>https://blog.che-ya.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Agent SDK 進階：Guardrails 與 Handoffs</title>
		<link>https://blog.che-ya.com/claude-agent-sdk-guardrails-handoffs/</link>
		
		<dc:creator><![CDATA[ㄚ槌]]></dc:creator>
		<pubDate>Mon, 04 May 2026 02:17:00 +0000</pubDate>
				<category><![CDATA[Claude Agent SDK]]></category>
		<category><![CDATA[Claude Code]]></category>
		<category><![CDATA[Agent SDK]]></category>
		<category><![CDATA[AI 編程工具]]></category>
		<category><![CDATA[Anthropic]]></category>
		<category><![CDATA[Guardrails]]></category>
		<category><![CDATA[Handoffs]]></category>
		<category><![CDATA[Multi-agent]]></category>
		<guid isPermaLink="false">https://blog.che-ya.com/?p=873</guid>

					<description><![CDATA[在上一篇 Claude Agent SDK 入門教學中，我們學會了如何建構基本的 AI Agent。本篇將深入 ... <a title="Agent SDK 進階：Guardrails 與 Handoffs" class="read-more" href="https://blog.che-ya.com/claude-agent-sdk-guardrails-handoffs/" aria-label="Read more about Agent SDK 進階：Guardrails 與 Handoffs">閱讀全文</a>]]></description>
										<content:encoded><![CDATA[
<p>在<a href="https://blog.che-ya.com/claude-agent-sdk-getting-started/">上一篇 Claude Agent SDK 入門教學</a>中，我們學會了如何建構基本的 AI Agent。本篇將深入探討兩個進階主題：<strong>Guardrails（護欄）</strong>與 <strong>Handoffs（任務移交）</strong>。這兩者是打造生產級 Agent 應用的關鍵機制，讓你的 Agent 既安全可靠，又能有效處理複雜任務。</p>



<h2 class="wp-block-heading">什麼是 Guardrails？</h2>



<p>Guardrails（護欄）是在 Agent 執行前後加入的驗證機制，用來確保輸入合法、輸出符合預期。簡單來說，就是在 Agent 的「進出口」設立檢查站。</p>



<p>與 OpenAI Agents SDK 使用裝飾器（<code>@input_guardrail</code>、<code>@output_guardrail</code>）的方式不同，Claude Agent SDK 採用更靈活的設計：<strong>Guardrails 就是你自己撰寫的普通函式</strong>，在程式碼中直接呼叫，搭配 Hooks 機制實現攔截與驗證。</p>



<h3 class="wp-block-heading">Guardrails 的兩種類型</h3>



<figure class="wp-block-table"><table><thead><tr><th>類型</th><th>時機</th><th>用途</th><th>實作方式</th></tr></thead><tbody><tr><td>Input Guardrail</td><td>Agent 執行前</td><td>驗證使用者輸入是否合法</td><td>普通函式 + 提前返回，或 UserPromptSubmit Hook</td></tr><tr><td>Output Guardrail</td><td>Agent 執行後</td><td>驗證輸出結果是否符合規範</td><td>對 result.result 執行驗證函式</td></tr><tr><td>Tool-level Guardrail</td><td>工具呼叫前後</td><td>控制特定工具的存取與行為</td><td>PreToolUse / PostToolUse Hook</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Input Guardrail：過濾不合法輸入</h3>



<p>最簡單的輸入護欄是在呼叫 Agent 前用普通函式驗證輸入。以費用申報 Agent 為例，我們要求使用者必須提供金額：</p>



<pre class="wp-block-code"><code lang="python" class="language-python">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)</code></pre>



<h3 class="wp-block-heading">使用 UserPromptSubmit Hook 實作 Input Guardrail</h3>



<p>若想讓 Guardrail 與 Agent 框架深度整合，可以使用 <code>UserPromptSubmit</code> Hook，這是最接近 OpenAI <code>@input_guardrail</code> 裝飾器的做法：</p>



<pre class="wp-block-code"><code lang="python" class="language-python">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)</code></pre>



<h3 class="wp-block-heading">Output Guardrail：驗證輸出結果</h3>



<p>Output Guardrail 在 Agent 完成後對輸出進行檢查。以費用申報為例，我們要確認輸出包含明確的核准或拒絕決策：</p>



<pre class="wp-block-code"><code lang="python" class="language-python">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)  # 正常輸出</code></pre>



<h3 class="wp-block-heading">Tool-level Guardrail：保護敏感操作</h3>



<p>透過 <code>PreToolUse</code> Hook，你可以在工具執行前攔截危險操作。例如禁止修改 <code>.env</code> 檔案：</p>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">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&lt;string, unknown&gt;;
  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);
}</code></pre>



<h2 class="wp-block-heading">什麼是 Handoffs？</h2>



<p>Handoffs（任務移交）是 Multi-agent 架構中的核心概念：主 Agent 將特定任務委派給專門的子 Agent 處理。在 OpenAI Agents SDK 中，Handoffs 會讓第一個 Agent「完全退場」，由第二個 Agent 接手對話；而在 Claude Agent SDK 中，設計哲學略有不同。</p>



<h3 class="wp-block-heading">OpenAI vs Claude 的 Handoffs 差異</h3>



<figure class="wp-block-table"><table><thead><tr><th>面向</th><th>OpenAI Agents SDK</th><th>Claude Agent SDK</th></tr></thead><tbody><tr><td>機制</td><td>Handoff 轉移控制權</td><td>Delegation（委派），主 Agent 保持控制</td></tr><tr><td>主 Agent 狀態</td><td>Handoff 後停止運行</td><td>持續活躍，接收子 Agent 結果</td></tr><tr><td>定義方式</td><td><code>handoffs=[specialist]</code></td><td><code>AgentDefinition</code> + <code>agents={}</code> 參數</td></tr><tr><td>結果回傳</td><td>第二個 Agent 直接輸出</td><td>子 Agent 結果回傳給主 Agent</td></tr><tr><td>適用場景</td><td>純路由（分診後交棒）</td><td>需要主 Agent 綜合子結果的複雜任務</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">用 AgentDefinition 實作 Handoffs</h3>



<p>Claude Agent SDK 透過 <code>AgentDefinition</code> 定義子 Agent，再由主 Agent 透過 Agent 工具進行委派。以費用申報為例：</p>



<pre class="wp-block-code"><code lang="python" class="language-python">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)</code></pre>



<h3 class="wp-block-heading">TypeScript 版本的 Handoffs 實作</h3>



<pre class="wp-block-code"><code lang="typescript" class="language-typescript">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);
}</code></pre>



<h3 class="wp-block-heading">用 SubagentStart / SubagentStop Hook 監控委派</h3>



<p>你可以使用 SubagentStart 和 SubagentStop Hook 追蹤子 Agent 的執行狀況，這對於除錯與效能分析非常有用：</p>



<pre class="wp-block-code"><code lang="python" class="language-python">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)</code></pre>



<h2 class="wp-block-heading">Guardrails 與 Handoffs 的組合應用</h2>



<p>在實際的生產環境中，Guardrails 與 Handoffs 通常會搭配使用。以下是一個完整的費用申報系統範例，結合了輸入驗證、多 Agent 路由與輸出檢查：</p>



<pre class="wp-block-code"><code lang="python" class="language-python">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，類別：交通"))</code></pre>



<h2 class="wp-block-heading">Hook 的執行優先順序</h2>



<p>當多個 Hooks 同時存在時，了解執行順序非常重要：</p>



<figure class="wp-block-table"><table><thead><tr><th>優先順序</th><th>步驟</th><th>說明</th></tr></thead><tbody><tr><td>1</td><td>Hooks 執行</td><td>PreToolUse 等 Hook 先行判斷，可允許、拒絕或繼續</td></tr><tr><td>2</td><td>Deny 規則</td><td>disallowed_tools 設定，即使 bypassPermissions 也有效</td></tr><tr><td>3</td><td>Permission Mode</td><td>bypassPermissions / acceptEdits / dontAsk / default</td></tr><tr><td>4</td><td>Allow 規則</td><td>allowed_tools 設定的白名單</td></tr><tr><td>5</td><td>canUseTool Callback</td><td>最後的互動確認（dontAsk 模式跳過此步）</td></tr></tbody></table></figure>



<p>重要原則：當多個 Hook 對同一操作返回不同決策時，<strong>deny 優先於 ask，ask 優先於 allow</strong>。只要有一個 Hook 返回 deny，操作就會被阻止。</p>



<h2 class="wp-block-heading">Guardrails 最佳實踐</h2>



<ul class="wp-block-list">
<li><strong>職責單一</strong>：每個 Guardrail 只做一件事，透過 Hook 鏈組合多個驗證邏輯，保持程式碼清晰</li>



<li><strong>錯誤訊息清楚</strong>：拒絕請求時，提供具體的拒絕原因，讓使用者知道該如何修正</li>



<li><strong>避免無限迴圈</strong>：<code>UserPromptSubmit</code> Hook 若觸發子 Agent，需防止遞迴呼叫；在 Hook 內部加入子 Agent 標記來避免</li>



<li><strong>非同步 Hook 用於記錄</strong>：純記錄用途的 Hook 使用非同步模式（<code>async: true</code>），避免阻塞 Agent 執行</li>
</ul>



<h2 class="wp-block-heading">Handoffs 最佳實踐</h2>



<ul class="wp-block-list">
<li><strong>AgentDefinition 描述要精確</strong>：子 Agent 的 <code>description</code> 欄位是主 Agent 決定路由的依據，描述要清楚說明「何時」應該使用這個子 Agent</li>



<li><strong>子 Agent 工具最小化</strong>：每個子 Agent 只授予完成任務所需的工具，避免過度授權</li>



<li><strong>若需要「真正交棒」</strong>：如果你的場景需要子 Agent 完全接管（不回傳給主 Agent），考慮用 Python 薄層 Dispatcher 直接呼叫不同的 Agent，而非依賴 LLM 決策路由</li>



<li><strong>監控子 Agent 執行</strong>：使用 SubagentStart / SubagentStop Hook 記錄每個子 Agent 的執行時間與結果，便於效能分析與除錯</li>
</ul>



<h2 class="wp-block-heading">總結</h2>



<p>Guardrails 與 Handoffs 是 Claude Agent SDK 中讓 Agent 達到生產級可靠性的兩大支柱。Guardrails 透過 Hook 機制在 Agent 的「進出口」設立檢查站，確保輸入合法、輸出符合預期、工具使用安全；Handoffs 透過 AgentDefinition 實現多 Agent 協作，讓複雜任務被分解到專門的子 Agent 處理，主 Agent 保持對全局的控制。</p>



<p>掌握這兩個機制後，你就具備了打造複雜 Agent 應用的核心能力。在接下來的系列文章中，我們將進一步探討如何將這些技術應用在實戰專案中，打造真正解決問題的自動化工具。</p>
<div class="saboxplugin-wrap" itemtype="http://schema.org/Person" itemscope itemprop="author"><div class="saboxplugin-tab"><div class="saboxplugin-gravatar"><img alt='ㄚ槌' src='https://secure.gravatar.com/avatar/9914399915f96350f302945e8ddddee1a9b1995350182f513fd2e1fa816c100a?s=100&#038;d=mm&#038;r=g' srcset='https://secure.gravatar.com/avatar/9914399915f96350f302945e8ddddee1a9b1995350182f513fd2e1fa816c100a?s=200&#038;d=mm&#038;r=g 2x' class='avatar avatar-100 photo' height='100' width='100' itemprop="image"/></div><div class="saboxplugin-authorname"><a href="https://blog.che-ya.com/author/a3230230/" class="vcard author" rel="author"><span class="fn">ㄚ槌</span></a></div><div class="saboxplugin-desc"><div itemprop="description"></div></div><div class="saboxplugin-web "><a href="https://blog.che-ya.com" target="_self" >blog.che-ya.com</a></div><div class="clearfix"></div></div></div><p><a class="a2a_button_facebook" href="https://www.addtoany.com/add_to/facebook?linkurl=https%3A%2F%2Fblog.che-ya.com%2Fclaude-agent-sdk-guardrails-handoffs%2F&amp;linkname=Agent%20SDK%20%E9%80%B2%E9%9A%8E%EF%BC%9AGuardrails%20%E8%88%87%20Handoffs" title="Facebook" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_line" href="https://www.addtoany.com/add_to/line?linkurl=https%3A%2F%2Fblog.che-ya.com%2Fclaude-agent-sdk-guardrails-handoffs%2F&amp;linkname=Agent%20SDK%20%E9%80%B2%E9%9A%8E%EF%BC%9AGuardrails%20%E8%88%87%20Handoffs" title="Line" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Fblog.che-ya.com%2Fclaude-agent-sdk-guardrails-handoffs%2F&amp;linkname=Agent%20SDK%20%E9%80%B2%E9%9A%8E%EF%BC%9AGuardrails%20%E8%88%87%20Handoffs" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Fblog.che-ya.com%2Fclaude-agent-sdk-guardrails-handoffs%2F&#038;title=Agent%20SDK%20%E9%80%B2%E9%9A%8E%EF%BC%9AGuardrails%20%E8%88%87%20Handoffs" data-a2a-url="https://blog.che-ya.com/claude-agent-sdk-guardrails-handoffs/" data-a2a-title="Agent SDK 進階：Guardrails 與 Handoffs"></a></p>]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
