Agents SDK 2.0:sama 说它"被严重低估",我用代码验证了三个原因

Sam Altman 在一次公开场合说过一句话,大意是:Agents SDK 是 OpenAI 目前"被严重低估"的产品之一。

这句话当时没引起太大水花——毕竟 OpenAI 自己夸自己的产品,不是什么新鲜事。

但如果你在 1.0 时代真的用它搭过多 Agent 系统,你大概会有另一种反应:"低估?我当时是直接骂娘的。"

handoff 逻辑写起来像在手动搬运行李;guardrails 就是往 system prompt 里堆规则,堆完自己都不确定有没有用;Agent 出了问题,排查方式基本靠 print 和玄学。

2.0 出来之后,我花了几天时间把原来的一个客服分流项目迁移过去,踩了一些坑,也真的感受到了几处具体的改善。

不是发布会通稿那种"全面升级",是那种让你少骂几句的改善。下面用代码说话。

---

一、背景:1.0 时代开发者的三大抱怨

在说 2.0 之前,先还原一下 1.0 的现实处境。

多 Agent 协作的典型场景:用户进来先接触客服 Agent,如果涉及退款,转给退款 Agent,退款 Agent 搞不定的,升级到人工坐席 Agent(或者直接触发人工介入)。

这条链路在 1.0 里要跑通,开发者要自己解决三件事:

1. handoff 时的上下文传递:Agent A 知道的东西,Agent B 不自动继承,要手动序列化、手动塞进新的 context

2. guardrails 的实现:没有结构化的拦截层,只能在 system prompt 里写"你不能做 XXX",可测试性为零

3. 调试:Agent 为什么没有按预期 handoff?静默失败,没有任何日志,只能加 print 猜

这三个问题不是小毛病,是在生产环境里真实会让项目延期的问题。2.0 对这三个点都有针对性的改动,我们一个个看。

---

二、省事之处 #1 —— Handoff 不再是"甩锅",是真正的上下文接力

1.0 的做法:手动搬运行李

在 1.0 里,handoff 的本质是截断重来。你需要自己把当前对话的关键信息提取出来,序列化成字符串或字典,然后作为新 Agent 的初始 context 塞进去。

# ── Agents SDK 1.0 风格:手动 handoff(约 35 行)──

from openai import OpenAI

client = OpenAI()

def run_customer_service_agent(user_message: str) -> dict:

"""客服 Agent,返回结果和是否需要转接"""

response = client.chat.completions.create(

model="gpt-4o",

messages=[

{"role": "system", "content": "你是客服,判断是否需要退款处理。如需退款,回复 TRANSFER_REFUND。"},

{"role": "user", "content": user_message}

]

)

content = response.choices[0].message.content

needs_transfer = "TRANSFER_REFUND" in content

return {"response": content, "needs_transfer": needs_transfer}

def run_refund_agent(user_message: str, prior_context: str) -> str:

"""退款 Agent,需要手动接收上下文"""

# ⚠️ 开发者必须手动构造 prior_context,漏掉任何字段就会出问题

system_prompt = f"""你是退款专员。

上下文信息(来自客服Agent):

{prior_context}

处理以下用户请求:"""

response = client.chat.completions.create(

model="gpt-4o",

messages=[

{"role": "system", "content": system_prompt},

{"role": "user", "content": user_message}

]

)

return response.choices[0].message.content

调用方:需要手动编排整条链路

def handle_user_request(user_message: str):

result = run_customer_service_agent(user_message)

if result["needs_transfer"]:

# ⚠️ 手动序列化上下文,字段多了就容易漏

prior_context = f"用户原始消息:{user_message}\n客服初步判断:需要退款处理"

return run_refund_agent(user_message, prior_context)

return result["response"]

这段代码的问题不是"写不出来",而是每次 handoff 都要手动决定传什么、怎么传。项目一复杂,这里就成了 bug 温床。

---

2.0 的做法:声明关系,SDK 负责接力

# ── Agents SDK 2.0 风格:声明式 handoff(约 20 行)──

from agents import Agent, handoff, Runner

声明退款 Agent(先声明,因为客服 Agent 要引用它)

refund_agent = Agent(

name="退款专员",

instructions="你是退款专员,负责处理所有退款申请。你可以访问完整的对话历史。",

model="gpt-4o",

)

声明人工坐席 Agent

human_agent = Agent(

name="人工坐席",

instructions="你代表人工客服,处理退款专员无法解决的复杂问题。",

model="gpt-4o",

)

客服 Agent:声明可以 handoff 给哪些 Agent,SDK 自动处理上下文传递

customer_service_agent = Agent(

name="客服",

instructions="你是第一线客服。如果用户需要退款,转交给退款专员。",

model="gpt-4o",

handoffs=[refund_agent, human_agent], # ✅ 声明关系,不需要手动序列化

)

运行:SDK 自动管理整条链路的上下文

result = Runner.run_sync(customer_service_agent, "我的订单 #12345 有问题,我要退款")

print(result.final_output)

对比结论: | 维度 | 1.0 | 2.0 | | 代码行数(同等功能) | ~35 行 | ~20 行 | | 上下文传递 | 手动序列化,易漏字段 | SDK 自动接力 | | 新增一个 Agent | 需修改编排逻辑 | 加一行 handoffs 声明 | | 出错定位 | 靠猜 | trace 可见(第三章讲) |

大白话总结:1.0 的 handoff 你要自己搬行李,2.0 是行李直接跟着人走。

---

💡 想直接跑这段代码?
文中所有示例都基于 OpenAI API 调用。如果你还没有稳定的 API 访问渠道,可以试试 [api.884819.xyz](https://api.884819.xyz)——支持 Agents SDK 直接对接,国内环境可用,按量计费,新用户注册即送体验 token。
配置方式只需把 base_url 替换一行,后面代码原样跑:
> client = OpenAI(base_url="https://api.884819.xyz/v1", api_key="your-key")

---

三、省事之处 #2 —— Guardrails 从"自己写 if-else"到声明式拦截

1.0 的痛:规则堆在 prompt 里,不可测试

1.0 时代,你想让退款 Agent 不能批准超过 500 元的退款,怎么做?

# ── 1.0 风格:把规则塞进 system prompt(约 20 行判断逻辑)──

system_prompt = """

你是退款专员。

重要限制(必须严格遵守):

  • 单笔退款金额不得超过 500 元
  • 如果用户要求超过 500 元的退款,告知需要上级审批
  • 不得透露公司内部退款审批流程
  • 不得承诺具体的退款到账时间

(后面还可能跟着十几条规则...)

"""

问题:

1. 规则写在 prompt 里,LLM 不一定每次都遵守

2. 无法单元测试这些规则

3. 规则之间可能互相矛盾,自己都不知道

4. 想复用到别的 Agent?复制粘贴,然后祈祷

这种做法有个根本性问题:你没有办法确定规则生效了。LLM 可能在某些措辞下绕过去,而你只有在生产事故之后才知道。

---

2.0 的做法:结构化 guardrails,输入输出双向拦截

# ── Agents SDK 2.0 风格:声明式 guardrails(核心逻辑约 3 行声明)──

from agents import Agent, GuardrailFunctionOutput, RunContextWrapper, input_guardrail

from pydantic import BaseModel

定义拦截结果的数据结构

class RefundAmountCheck(BaseModel):

amount_exceeds_limit: bool

detected_amount: float | None

reason: str

✅ 用装饰器声明一个输入 guardrail(可独立测试!)

@input_guardrail

async def check_refund_amount(

ctx: RunContextWrapper, agent: Agent, input: str

) -> GuardrailFunctionOutput:

"""检查用户要求的退款金额是否超出授权范围"""

# 用一个轻量模型做意图提取(不消耗主 Agent 的 token)

from agents import Runner

extraction_agent = Agent(

name="金额提取器",

instructions="从用户消息中提取退款金额。如果没有明确金额,detected_amount 返回 null。",

model="gpt-4o-mini", # 用小模型降低成本

output_type=RefundAmountCheck,

)

result = await Runner.run(extraction_agent, input, context=ctx.context)

check = result.final_output_as(RefundAmountCheck)

# ✅ 核心判断逻辑:3 行,清晰,可测试

exceeds = check.detected_amount is not None and check.detected_amount > 500

return GuardrailFunctionOutput(

output_info=check,

tripwire_triggered=exceeds, # True = 拦截,不进入主 Agent

)

把 guardrail 绑定到 Agent

refund_agent = Agent(

name="退款专员",

instructions="你是退款专员,处理 500 元以内的退款申请。",

model="gpt-4o",

input_guardrails=[check_refund_amount], # ✅ 声明绑定,可复用到任何 Agent

)

这个写法的核心价值,不只是"少写代码":

1. 可独立测试check_refund_amount 是一个普通的 Python 函数,你可以直接写单元测试,传入各种 edge case,验证拦截逻辑是否正确

2. 可复用:同一个 guardrail 函数可以绑定到多个 Agent,不需要复制粘贴规则

3. 输出双向拦截:除了 input_guardrail,还有 output_guardrail,可以在 Agent 回复之后、发给用户之前再做一次检查

大白话总结:1.0 的 guardrails 是贴在门上的纸条,2.0 是真正的门禁系统——可以刷卡验证,可以记录日志,可以单独测试。

---

四、省事之处 #3 —— 调试从"猜"到"看"

1.0 的调试体验:开盲盒

1.0 时代,Agent 没有按预期 handoff 怎么办?

# 1.0 时代的调试方式,很多人都经历过

print("=== 进入客服 Agent ===")

result = run_customer_service_agent(user_message)

print(f"客服输出: {result}")

print("=== 判断是否需要转接 ===")

if result["needs_transfer"]:

print("=== 进入退款 Agent ===")

# ...

满屏 print,还不一定能找到问题所在。更糟的是 1.0 的静默失败问题:Agent 有时候不报错,也不 handoff,就直接给了一个看起来合理但其实走错路径的回答,你根本不知道发生了什么。

---

2.0 的 trace:调用链路可视化

2.0 内置了 trace 机制,默认在终端输出结构化的调用链路:

# 2.0 终端 trace 输出示意(实际运行效果)

[TRACE] Run started: agent=客服, input="我的订单 #12345 有问题,我要退款"

[AGENT] 客服 → thinking...

[LLM] model=gpt-4o, tokens=342

[AGENT] 客服 → handoff triggered: target=退款专员

[HANDOFF] context transferred: messages=3, metadata={order_id: "12345"}

[AGENT] 退款专员 → guardrail check: check_refund_amount

[GUARDRAIL] input="...退款..." → amount=null → tripwire=False → PASS

[AGENT] 退款专员 → thinking...

[LLM] model=gpt-4o, tokens=518

[AGENT] 退款专员 → final output

[TRACE] Run completed: steps=2, total_tokens=860, duration=3.2s

这个输出告诉你:

  • 每一步 Agent 做了什么决策
  • handoff 在哪里触发,传了什么 context
  • guardrail 是否生效,为什么通过或拦截
  • 每个步骤的 token 消耗和耗时

原来需要几个小时排查的"Agent 为什么没有 handoff",现在看一眼 trace 就知道了。

⚠️ 国内网络注意:如果你在国内环境运行,trace 数据默认会尝试上报到 OpenAI 的 tracing 服务,可能因网络问题超时。建议在开发阶段加上 OPENAI_AGENTS_DISABLE_TRACING=1 环境变量,只看本地终端输出,避免影响主流程。

大白话总结:1.0 调试是在黑屋子里找东西,2.0 给你开了灯。

---

五、综合评价:值得升级吗?

2.0 真实存在的局限

说完三个改善,也要说说 2.0 目前还没解决的问题,不然这篇文章就成了软文:

  • 嵌套 Agent 的 token 成本:多层 handoff 意味着完整对话历史被反复传递,token 消耗会比预期高,复杂场景下要做好成本预算
  • 国内接入摩擦:直连 OpenAI API 在国内需要解决网络问题,api.884819.xyz 这类中转方案可以缓解,但引入了额外的依赖
  • 复杂嵌套 Agent 的调试:trace 已经好很多了,但超过 4 层的嵌套 Agent 调用链,trace 输出依然会让人眼花

升级判断标准

我给一个清晰的建议:

  • 有 2 个以上 Agent 协作 → 立刻升,handoff 和 guardrails 的改善会直接减少你的维护成本
  • 单 Agent 项目 → 暂时没必要动,1.0 够用,等你真的需要多 Agent 时再迁移
  • 生产环境已经跑着 1.0 → 先在新功能上用 2.0,不要一次性全量迁移

今晚就能做的下一步

如果你想今晚就开始:

# 安装最新版 Agents SDK

pip install openai-agents --upgrade

验证版本

python -c "import agents; print(agents.__version__)"

然后把本文第二章的 handoff 示例代码复制下来,替换成你自己项目里的两个 Agent,跑通一次。不需要迁移整个项目,先感受一下 handoff 的新写法,剩下的改动自然会有动力继续。

---

下一篇我想聊的是:当 Agents SDK 2.0 遇上本地模型(比如 Qwen3 或 DeepSeek R1),handoff 机制还能正常工作吗?

我已经在跑测试了,结果有点出乎意料——本地模型的 tool calling 格式和 OpenAI 的规范有几处微妙的差异,会在 handoff 链路上触发一些很难复现的 bug。有兴趣的先点个关注,下周见。

---

本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。

#AgentsSDK #OpenAI #多Agent开发 #AI开发教程 #8848AI #LLM应用开发 #Guardrails #AI工程实践