SDK:从 OpenAI Agents 迁移

OpenAI Agents SDK 到 Claude SDK 的迁移指南

🌐 查看英文原文 | 源码 Notebook


从 OpenAI Agents SDK 迁移

如果你有一个基于 OpenAI Agents SDK 的应用并想将其移植到 Claude Agent SDK,本 notebook 使用一个单一示例映射每个原语:一个费用审批 Agent。

迁移后的收益。 Claude Agent SDK 运行在与 Claude Code 相同的运行时上——你继承了其内置的 Read、Edit、Bash 和 Grep 工具,一个用于控制 Agent 可访问内容的分层权限系统,自动 prompt 缓存,以及直接访问事件流以进行进度流式传输或运行中拦截。工具定义是显式的(你声明 schema 而不是依赖类型提示内省),循环由你来驱动。大多数移植在每个工具上需要更多样板代码,但在其他地方需要更少的样板代码。

通过本 notebook,你将能够:

  • 用 Claude 的等价物替换 @function_tool、guardrails 和 Runner.run,而无需重写你的业务逻辑
  • 移植一个单 Agent 应用:自定义工具、输入/输出 guardrails、多轮会话和持久化恢复
  • 知道何时使用 ClaudeSDKClient 与无状态的 query() 函数
  • 将 SDK 的 OpenTelemetry 导出连接到你现有的可观测性栈

两个 SDK 都会实际运行。你需要在 .env 中设置 OPENAI_API_KEY 和 ANTHROPIC_API_KEY。

OpenAI Agents SDK Claude Agent SDK
Agent(name, instructions, tools) ClaudeAgentOptions + 系统提示
@function_tool @tool + create_sdk_mcp_server
@input_guardrail 循环前的普通函数(或 UserPromptSubmit hook)
@output_guardrail ResultMessage.result 上的普通函数
Runner.run(agent, msg) ClaudeSDKClient 上下文管理器
Sessions(客户端管理) 复用同一个 ClaudeSDKClient
conversation_id(服务端管理) resume=session_id(磁盘支持)
内置追踪仪表板 OTel 原生——插入你现有的 Grafana/Datadog/Honeycomb
handoffs=[...] AgentDefinition + Agent 工具——见附录

示例:费用审批

一个审批或标记费用提交的单 Agent。一个工具(check_policy),一个输入 guardrail(如果没有金额则拒绝),以及循环。

我们先用 OpenAI Agents SDK 构建它,然后移植每个原语。

前提条件

所需知识

  • 熟悉 Python 中的 async/await
  • 熟悉你正在迁移的 OpenAI Agents SDK 原语

所需工具

  • Python 3.11 或 3.12
  • ANTHROPIC_API_KEY(获取)和 OPENAI_API_KEY(获取)——两个 SDK 都会实际运行

推荐:

  • 浏览 notebook 00 了解 query() 和 ClaudeSDKClient 基础知识

openai-agents 固定在 0.9.3——它处于 1.0 之前的阶段,API 变化频繁。如果你升级版本,请重新验证 Agent/Runner/@function_tool 的签名。

%%capture
# openai-agents pinned to 0.9.3 — see prereqs note before bumping
%pip install "openai-agents==0.9.3" claude-agent-sdk python-dotenv
import os
import re
from dataclasses import replace

from dotenv import load_dotenv

load_dotenv()

assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY missing from .env"
assert os.getenv("ANTHROPIC_API_KEY"), "ANTHROPIC_API_KEY missing from .env"

OAI_MODEL = "gpt-4.1"
CLAUDE_MODEL = "claude-sonnet-4-6"
# ===== OpenAI Agents SDK — full implementation =====
from agents import Agent, GuardrailFunctionOutput, Runner, function_tool, input_guardrail


@function_tool
def check_policy(category: str, amount: float) -> dict:
    """Look up the expense policy for a category. Returns the approval limit. Valid categories: meals, travel, software, other."""
    limits = {"meals": 75.0, "travel": 500.0, "software": 200.0, "other": 50.0}
    return {
        "category": category,
        "limit": limits.get(category.lower(), 50.0),
        "requires_receipt": amount > 25.0,
    }


@input_guardrail
def has_dollar_amount(ctx, agent, user_input: str) -> GuardrailFunctionOutput:
    has_amount = bool(re.search(r"\$\d+", user_input))
    return GuardrailFunctionOutput(
        output_info={"has_amount": has_amount},
        tripwire_triggered=not has_amount,
    )


expense_agent = Agent(
    name="Expense Approver",
    instructions=(
        "You approve or flag expense submissions. "
        "Always call check_policy first to get the limit for the expense category. "
        "Approve if the amount is under the limit; otherwise flag for manager review."
    ),
    tools=[check_policy],
    input_guardrails=[has_dollar_amount],
    model=OAI_MODEL,
)


async def run_oai(msg: str) -> str:
    result = await Runner.run(expense_agent, msg)
    return result.final_output


print(await run_oai("Lunch with Acme, $47"))
This lunch expense of $47 falls under the "meals" category, which has an approval limit of $75. Since the amount is below the limit, I approve the expense (assuming a receipt is provided, as required by policy).

逐个原语移植

@function_tool → @tool + create_sdk_mcp_server

OpenAI: @function_tool 从类型提示和文档字符串推导工具 schema。被装饰的函数直接放入 tools=[...]。

Claude: @tool 将 name、description 和 schema 作为显式参数——你写什么,模型就看到什么。处理函数是 async 的,接收一个 args 字典,返回 {"content": [{"type": "text", "text": ...}]}。工具被打包到一个进程内 MCP 服务器中(尽管名字如此,但没有子进程或网络传输),这是你传递给 Agent 的单元。

函数内部的逻辑不变,只有包装器变了。

import json

from claude_agent_sdk import create_sdk_mcp_server, tool


@tool(
    "check_policy",
    "Look up the expense policy for a category. Returns the approval limit. Valid categories: meals, travel, software, other.",
    {"category": str, "amount": float},
)
async def check_policy_claude(args):
    limits = {"meals": 75.0, "travel": 500.0, "software": 200.0, "other": 50.0}
    category, amount = args["category"], args["amount"]
    result = {
        "category": category,
        "limit": limits.get(category.lower(), 50.0),
        "requires_receipt": amount > 25.0,
    }
    return {"content": [{"type": "text", "text": json.dumps(result)}]}


policy_server = create_sdk_mcp_server(name="expense", tools=[check_policy_claude])

Agent(...) → ClaudeAgentOptions

ClaudeAgentOptions 承载模型、工具和系统提示——实际发送给 Claude 的配置。你的 guardrail 逻辑保留在应用代码中,而不是注册在框架对象上。OpenAI 的 Agent 将所有这些捆绑在一起。

Agent(...) 字段 ClaudeAgentOptions 等价物
instructions=... system_prompt=...
tools=[fn] mcp_servers={...} + allowed_tools=[...]
model=... model=...
name=... 不需要——options 是配置值,不是命名实体。子 Agent 名称放在 AgentDefinition 上(见附录)。
input_guardrails=[...] 在你的运行函数中调用——见下一节

allowed_tools 中的工具名称遵循 mcp__{server_name}__{tool_name} 模式。

关于权限的说明: allowed_tools 是一个允许规则——它使工具对 Agent 可用。Agent 是否可以在没有用户批准的情况下调用它取决于 permission_mode。像 check_policy 这样的只读自定义工具默认自由运行;写入文件或运行 shell 命令的工具会提示,除非你设置 permission_mode="acceptEdits" 或 "bypassPermissions"。如果你的工具不仅仅是读取,请参阅权限指南。

from claude_agent_sdk import ClaudeAgentOptions

expense_system_prompt = (
    "You approve or flag expense submissions. "
    "Always call check_policy first to get the limit for the expense category. "
    "Approve if the amount is under the limit; otherwise flag for manager review."
)

expense_options = ClaudeAgentOptions(
    model=CLAUDE_MODEL,
    system_prompt=expense_system_prompt,
    mcp_servers={"expense": policy_server},
    allowed_tools=["mcp__expense__check_policy"],
)

内置工具。 费用 Agent 只使用自定义工具,但 SDK 附带了 Read、Edit、Bash、Grep 等——通过名称将它们添加到 allowed_tools(例如 allowed_tools=["Read", "Grep", "mcp__expense__check_policy"])。有关文件系统模式,请参阅notebook 00。

@input_guardrail / @output_guardrail → 调用前/后检查

OpenAI: @input_guardrail 验证用户的消息;@output_guardrail 验证 Agent 的最终答案。两者都返回 GuardrailFunctionOutput(tripwire_triggered=True) 来阻止,这会引发 InputGuardrailTripwireTriggered(或输出变体)。输入 guardrails 与 Agent 并发运行。

Claude: 你的检查是普通函数。在启动客户端之前调用输入检查,在循环结束后对 ResultMessage.result 调用输出检查。

OAI Claude
@input_guardrail 装饰器 循环前调用的普通函数
@output_guardrail 装饰器 循环后对 result.result 调用的普通函数
tripwire_triggered=True 引发异常 提前返回(输入)或覆盖结果(输出)
注册在 Agent(input_guardrails=[...]) 上 在你的运行函数中调用

UserPromptSubmit hook 替代方案

SDK 的 UserPromptSubmit hook 在 prompt 到达 Claude 之前触发并可以阻止它——最接近 @input_guardrail 的结构匹配。我们在下面展示了两种方式,但使用普通函数进行演示,原因有两个:拒绝消息保持在你的控制之下(当 hook 阻止时,ResultMessage.result 返回空——原因不会暴露给调用者),guardrail 逻辑在你的运行函数中保持可见,而不是注册在 options 对象上。@output_guardrail 没有干净的 hook 等价物——Stop 在响应完成后触发但不重写输出。有关 PreToolUse hooks 控制工具调用的内容(这是一个不同的用例),请参阅notebook 03。

def has_dollar_amount_check(user_input: str) -> tuple[bool, str | None]:
    """Input check — returns (allowed, rejection_message)."""
    if re.search(r"\$\d+", user_input):
        return True, None
    return False, "I need a dollar amount to process this. Please include one (e.g., '$47')."


def has_decision_check(result: str) -> tuple[bool, str | None]:
    """Output check — returns (allowed, override_message)."""
    # Stem match is intentionally permissive for a demo; tighten for production.
    if re.search(r"\b(approv|flag|review)", result, re.IGNORECASE):
        return True, None
    return False, "I couldn't reach a clear approve/flag decision. Please resubmit."

为了对比,以下是将相同检查作为 UserPromptSubmit hook 连接的方式。回调接收 input_data["prompt"],返回 {} 允许或 {"decision": "block", "reason": "..."} 阻止。这里的 HookMatcher 不接受 matcher 参数,因为 UserPromptSubmit 在每个 prompt 上触发——没有工具名称可以过滤。

要尝试它,在下一节的 ClaudeSDKClient(...) 调用中将 options=expense_options 替换为 options=hooked_options。一个注意事项:当 UserPromptSubmit hook 阻止时,阻止原因目前不会出现在 ResultMessage.result 中——你会得到一个空字符串而不是拒绝文本。这就是为什么演示循环使用普通函数来控制自己的返回。

对于工具级别的 guardrails——保护 Agent 做什么 而不是用户 发送什么——请参阅notebook 03 中的 PreToolUse hook 模式。

from claude_agent_sdk import HookMatcher


async def has_dollar_amount_hook(input_data, tool_use_id, context):
    # tool_use_id is None for UserPromptSubmit — it's a uniform signature across all hook types.
    if re.search(r"\$\d+", input_data["prompt"]):
        return {}
    return {"decision": "block", "reason": "I need a dollar amount to process this."}


hooked_options = ClaudeAgentOptions(
    model=CLAUDE_MODEL,
    system_prompt=expense_system_prompt,
    mcp_servers={"expense": policy_server},
    allowed_tools=["mcp__expense__check_policy"],
    hooks={"UserPromptSubmit": [HookMatcher(hooks=[has_dollar_amount_hook])]},
)

Runner.run() → ClaudeSDKClient

OpenAI: result = await Runner.run(agent, msg) 运行循环并返回一个结果对象。读取 result.final_output 获取文本。

Claude: ClaudeSDKClient 是一个异步上下文管理器。调用 .query(msg) 发送,然后迭代 .receive_response() 获取到达的事件——你可以按顺序看到每个工具调用、每个文本块和最终结果。事件类型:

事件 包含
SystemMessage 会话初始化元数据
AssistantMessage 文本块或工具使用块
UserMessage 工具结果块
ResultMessage 始终最后:.result、.usage、.total_cost_usd

最终答案是 messages[-1].result。

SDK 还暴露了一个无状态的 query() 函数(参见 notebook 00),用于使用内置工具进行一次性调用。当你通过 create_sdk_mcp_server 引入自定义工具时,ClaudeSDKClient 是正确的选择,因为它的持久传输处理进程内 MCP 握手。

from utils.agent_visualizer import display_agent_response, print_activity, reset_activity_context

from claude_agent_sdk import ClaudeSDKClient


async def run_claude(msg: str) -> None:
    # Input guardrail — short-circuit before the agent runs
    allowed, rejection = has_dollar_amount_check(msg)
    if not allowed:
        print(rejection)
        return

    reset_activity_context()
    messages = []
    async with ClaudeSDKClient(options=expense_options) as client:
        await client.query(msg)
        async for event in client.receive_response():
            print_activity(event)
            messages.append(event)

    # receive_response() guarantees ResultMessage is the last event yielded
    final = messages[-1].result
    ok, override = has_decision_check(final or "")
    if not ok:
        print(override)
        return

    display_agent_response(messages)


await run_claude("Lunch with Acme, $47")
🤖 Thinking...
🤖 Thinking...
🤖 Using: mcp__expense__check_policy()
✓ Tool completed
🤖 Thinking...
<IPython.core.display.HTML object>

Sessions → ClaudeSDKClient(内存中)或 resume=(磁盘支持)

OpenAI 提供两种会话模式:

OAI 模式 持久性 Claude 等价物
result.to_input_list() + 重新发送 客户端在内存中保存历史 复用一个 ClaudeSDKClient——在同一开放上下文中再次调用 .query()
conversation_id 服务端,在进程重启后存活 resume=session_id——对话记录写入本地磁盘,在重启后存活

内存中(如下): 保持 ClaudeSDKClient 上下文打开并再次调用 .query()。无需重新发送历史。当你的进程终止时也会终止。

磁盘支持: 每次运行产生一个 session_id(在 ResultMessage.session_id 上)。存储它,然后在下次运行时传递 ClaudeAgentOptions(resume=session_id, ...)。记录保存在本地文件系统上,而不是 Anthropic 的服务器上——它在重启后持久化,但不能跨机器。

对于在任一模式下超出上下文窗口的对话,请参阅 ../misc/session_memory_compaction.ipynb。

# --- In-memory: reuse the same client ---
async def run_claude_multiturn():
    async with ClaudeSDKClient(options=expense_options) as client:
        await client.query("Lunch with Acme, $47")
        async for _ in client.receive_response():
            pass  # consume the stream

        # Same client — remembers turn 1
        await client.query("What about $90?")
        turn2 = [m async for m in client.receive_response()]
        # receive_response() guarantees ResultMessage is the last event yielded
        return turn2[-1].result


print(await run_claude_multiturn())
🚩 **Flagged for Manager Review**

| Detail | Info |
|---|---|
| **Description** | Lunch with Acme |
| **Category** | Meals |
| **Amount** | $90.00 |
| **Policy Limit** | $75.00 |
| **Over Limit By** | $15.00 |
| **Status** | **Flagged** |

The $90 lunch exceeds the $75 meals limit by **$15.00**, so it requires manager approval before it can be reimbursed.
# --- Disk-backed: capture session_id, resume later ---

# Turn 1: capture the session_id from the ResultMessage (always the last event)
async with ClaudeSDKClient(options=expense_options) as client:
    await client.query("Lunch with Acme, $47")
    turn1 = [m async for m in client.receive_response()]
session_id = turn1[-1].session_id

# Turn 2: new client, new process — resume from disk
resume_opts = replace(expense_options, resume=session_id)
async with ClaudeSDKClient(options=resume_opts) as client:
    await client.query("What about $90?")
    turn2 = [m async for m in client.receive_response()]
print(f"[resumed {session_id[:8]}...] {turn2[-1].result}")
[resumed 4fc6dc1e...] Here's the rundown for both expenses:

| Expense | Amount | Limit | Status |
|---|---|---|---|
| Lunch with Acme | $47 | $75 | ✅ **Approved** |
| Lunch with Acme | $90 | $75 | 🚩 **Flagged for Manager Review** |

- **$47** – Under the $75 meals limit, so it's **approved**. (Receipt required.)
- **$90** – Exceeds the $75 meals limit by $15, so it's **flagged for manager review**.

并排对比

通过两个 SDK 运行所有三个测试输入。我们期望两者都批准 47 美元的午餐,标记 650 美元的航班,并拒绝没有金额的输入。

from agents.exceptions import InputGuardrailTripwireTriggered


async def run_claude_quiet(msg: str) -> str:
    # Same guardrail as run_claude — duplicated here for a minimal comparison helper
    allowed, rejection = has_dollar_amount_check(msg)
    if not allowed:
        return rejection
    async with ClaudeSDKClient(options=expense_options) as client:
        await client.query(msg)
        messages = [m async for m in client.receive_response()]
        # receive_response() guarantees ResultMessage is the last event yielded
        return messages[-1].result or ""


test_inputs = [
    ("Lunch with Acme, $47", "approve"),
    ("Flight to NYC, $650", "flag"),
    ("Need approval for the thing", "guardrail"),
]

for msg, expected in test_inputs:
    print(f"\n{'=' * 60}\nINPUT: {msg}  (expect: {expected})\n{'=' * 60}")

    try:
        oai_out = await run_oai(msg)
    except InputGuardrailTripwireTriggered:
        oai_out = "[guardrail: InputGuardrailTripwireTriggered]"

    claude_out = await run_claude_quiet(msg)

    print(f"OAI:    {str(oai_out)[:200]}")
    print(f"Claude: {str(claude_out)[:200]}")

============================================================
INPUT: Lunch with Acme, $47  (expect: approve)
============================================================
OAI:    The expense for lunch ($47) is under the meals category limit of $75. This expense is approved.
Claude: ✅ **Expense Approved**

| Detail | Info |
|---|---|
| **Description** | Lunch with Acme |
| **Category** | Meals |
| **Amount** | $47.00 |
| **Meal Limit** | $75.00 |
| **Status** | **Approved** |

Yo

============================================================
INPUT: Flight to NYC, $650  (expect: flag)
============================================================
OAI:    The approval limit for travel expenses is $500. Since your flight to NYC costs $650, this expense exceeds the limit and will be flagged for manager review.
Claude: 🚩 **Expense Flagged for Manager Review**

| Detail | Info |
|---|---|
| **Description** | Flight to NYC |
| **Category** | Travel |
| **Amount** | $650.00 |
| **Policy Limit** | $500.00 |
| **Over Lim

============================================================
INPUT: Need approval for the thing  (expect: guardrail)
============================================================
OAI:    [guardrail: InputGuardrailTripwireTriggered]
Claude: I need a dollar amount to process this. Please include one (e.g., '$47').

追踪

Claude 发射到你的团队已经运行的标准。

每次运行的指标已经在你的手中。 每个 ResultMessage 都带有 .total_cost_usd 和 .usage——输入/输出 token、缓存读取、缓存写入、轮次计数。你将在下面的缓存单元格中看到这一点。有关程序化成本跟踪,请参阅 ../observability/usage_cost_api.ipynb。

OpenTelemetry 是原生的。 SDK 共享 Claude Code 运行时,在启用时导出 OTel 指标和事件:

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://your-collector:4317

这些都不是移植所必需的——无论遥测是否开启,你的 Agent 都会以相同的方式运行。

每次工具调用和 API 请求都会发射一个标记有 prompt.id 的事件,因此你可以将所有活动关联回原始 prompt。将它指向你现有的 Grafana / Datadog / Honeycomb / Langfuse 栈。有关完整的事件 schema,请参阅监控文档。

Claude 专有:prompt 缓存

系统提示和工具 schema 在第一次调用后自动缓存——无需配置。具有相同前缀的后续调用只需为该前缀支付大约 10% 的输入 token 成本。

由于之前的单元格已经使用 expense_options 运行过,缓存在我们到达这里时已经是热的——所以即使是运行 1 也显示出大量的 cache_read_input_tokens。在一个冷启动的 notebook 中,运行 1 主要是 cache_creation,运行 2 主要是 cache_read。工具调用循环每次查询发起多个 API 请求,每个请求都扩展缓存的前缀——所以即使在重复运行中 cache_creation 也不会降到零,但它应该会缩小,而 cache_read 会增长。

for i in range(2):
    async with ClaudeSDKClient(options=expense_options) as client:
        await client.query("Coffee, $6")
        messages = [m async for m in client.receive_response()]
    # receive_response() guarantees ResultMessage is the last event yielded
    usage = messages[-1].usage or {}
    print(
        f"Run {i + 1}: cache_creation={usage.get('cache_creation_input_tokens', 0):>5}  "
        f"cache_read={usage.get('cache_read_input_tokens', 0):>5}  "
        f"input={usage.get('input_tokens', 0):>5}"
    )
Run 1: cache_creation=38728  cache_read=66272  input=    4
Run 2: cache_creation=  659  cache_read=95025  input=    4

附录:迁移 handoffs

如果你的 OpenAI Agents SDK 应用使用 handoffs=[...],这就是心智模型转变的地方。

OpenAI: handoffs=[specialist] 暴露一个 transfer_to_specialist 工具。调用时,专家接管对话。第一个 Agent 就完成了。

Claude: 子 Agent 通过 AgentDefinition 以编程方式定义,并在 ClaudeAgentOptions(agents={...}) 中传递。编排者通过 Agent 工具委托,获取结果,并保持控制。

对于纯路由器(分诊→专家),差异很小。对于”Agent B 接管后 A 不再运行”是承重逻辑的应用,考虑使用轻量级 Python 分发器而不是 LLM 决定的路由。

你也可能在 Claude Code CLI 工作流中看到子 Agent 定义为 .claude/agents/*.md 文件(例如 notebook 01)。那是交互式使用的基于文件系统的路径。对于作为代码交付的 SDK 应用,下面的编程式 agents= 参数是自然的选择——没有文件系统依赖。

from claude_agent_sdk import AgentDefinition

approver = AgentDefinition(
    description="Approves expenses under policy limits. Use for any submission where the amount is at or under the limit returned by check_policy.",
    prompt="You approve expense submissions that are within policy. Confirm the amount and category, and remind the submitter if a receipt is required.",
    tools=["mcp__expense__check_policy"],
)

escalator = AgentDefinition(
    description="Escalates over-limit expenses to a manager. Use when the submitted amount exceeds the policy limit for its category.",
    prompt="You escalate over-limit expenses. Draft a one-line note to the manager: include the amount, the category, and how far over the limit it is.",
    tools=["mcp__expense__check_policy"],
)

triage_options = ClaudeAgentOptions(
    model=CLAUDE_MODEL,
    system_prompt="Route each expense to the appropriate subagent. Delegate to approver if under limit, escalator if over.",
    mcp_servers={"expense": policy_server},
    allowed_tools=["Agent", "mcp__expense__check_policy"],
    agents={"approver": approver, "escalator": escalator},
)

print(f"Orchestrator configured with subagents: {list(triage_options.agents)}")

# Config only — not executed here. For a full end-to-end multi-agent run,
# see ./01_The_chief_of_staff_agent.ipynb
Orchestrator configured with subagents: ['approver', 'escalator']

总结

你构建了什么

一个费用审批 Agent,逐个原语移植:@function_tool → @tool,Agent → ClaudeAgentOptions,@input_guardrail/@output_guardrail → 普通的调用前/后检查,Runner.run → ClaudeSDKClient,内存中和磁盘支持的会话。

关键要点

  • 工具: 显式 schema 意味着你写什么,模型就看到什么——没有内省意外。打包为进程内 MCP 服务器并通过 ClaudeSDKClient 传递。
  • Guardrails: 普通函数将拒绝流程放在你的代码中,而不是框架的。当你想在 options 上注册时,UserPromptSubmit hook 就在那里。
  • 会话: 一个 ClaudeSDKClient 用于内存中多轮对话。resume=session_id 用于在重启后存活的磁盘支持对话。
  • 追踪: OpenTelemetry 原生,因此它插入你已经在运行的栈。每轮成本和 token 使用量都在每个 ResultMessage 上——无需额外连线。

后续步骤


Tip

中文读者提示:本章节的代码和输出为英文原文。如需理解具体实现细节,可参考上方中文导读和代码注释。如有疑问,欢迎在 GitHub Issues 讨论。