OpenAI Agents SDK:交接、护栏与追踪 本节摘要:无法干净委托的 Agent,最后都会把所有东西塞进一个提示里——上下文炸裂、角色串味、无法审计。没有护栏的 Agent,会把 PII(个人身份信息)、违规内容直接发出去,或者无限循环烧钱。OpenAI Agents SDK 是构建在 Responses API 之上的轻量多智能体框架,把这件事产品化成了五个原语:Agent(LLM + 指令 + 工具 + 交接)、Handoff(交接,被建模成名为 的工具)、Guardrail(护栏,在输入/输出/工具调用上触发)、Session(跨轮会话历史)、Tracing(追踪,默认开启的 span)。
本节摘要:无法干净委托的 Agent,最后都会把所有东西塞进一个提示里——上下文炸裂、角色串味、无法审计。没有护栏的 Agent,会把 PII(个人身份信息)、违规内容直接发出去,或者无限循环烧钱。OpenAI Agents SDK 是构建在 Responses API 之上的轻量多智能体框架,把这件事产品化成了五个原语:Agent(LLM + 指令 + 工具 + 交接)、Handoff(交接,被建模成名为
transfer_to_<agent>的工具)、Guardrail(护栏,在输入/输出/工具调用上触发)、Session(跨轮会话历史)、Tracing(追踪,默认开启的 span)。核心洞见是把交接做成工具——模型在工具列表里看到transfer_to_billing_agent,调用它即触发运行时:拷贝上下文、初始化目标 Agent、用目标 Agent 续跑。这其实就是第 13/28 节的监督者(supervisor)模式,只是被厂商打包成了开箱即用的一等公民。本节吃透五个原语、三种护栏(输入/输出/工具)、并行 vs 阻塞两种模式、默认开启的 span 追踪,并用标准库从零实现一个带交接 + 护栏 + span 追踪的运行时,跑一个分诊 Agent 把请求交接给计费或支持团队。读完本节,你应能识别 SDK 的三种失败模式:交接漂移、护栏绕过、过度追踪。
对应原课程:Phase 14 · Lesson 16 ·
openai-agents-sdk(原英文phases/14-agent-engineering/16-openai-agents-sdk/docs/en.md)。前置:第 01 节(Agent 循环)、第 06 节(工具使用)。
阅读完本节,你应当能够:
run_in_parallel(并行)与阻塞两种模式。两类失败把生产 Agent 拖垮。
无法干净委托的 Agent——它把计费问题、技术支持、退款流程、FAQ 全塞进同一个提示。结果是:上下文窗口被无关指令塞满(第 07 节说的「注意力稀释」),角色定义互相打架,模型在「该用哪个角色的语气」上反复横跳,审计时根本说不清某段输出是谁产出的。
没有护栏的 Agent——它会把用户输入里的 PII 原样回流进日志,会生成违反政策的内容并直接对外发送,会在工具失败后无限重试直到账单爆炸。
OpenAI Agents SDK 把这两件事都固化进原语:Handoff 解决干净委托,Guardrail 解决边界守护,Session 解决跨轮记忆,Tracing 解决可观测性,Agent 把它们组合在一起。
LLM + 指令 + 工具 + 交接。SDK 里的 Agent 类型,拥有自己的工具列表和可交接的目标列表。transfer_to_<agent_name> 的工具。模型在工具列表里看到 transfer_to_billing_agent。调用它会触发运行时做三件事:
nest_handoff_history beta 选项把它折叠成摘要)。模型不直接「跳转」,它只是像调用任何工具一样调一个特殊名字的工具——运行时识别这个名字并执行交接语义。这让路由决策留给了模型的判断力,而执行语义留给了确定的代码。这正是第 13 节(状态图)与第 28 节(编排)里的监督者模式,只是被厂商产品化了。
运行模式有两种:
run_in_parallel=False) —— 护栏 LLM 先跑。若绊倒,主调用不花 token。绊倒时抛 InputGuardrailTripwireTriggered / OutputGuardrailTripwireTriggered 异常,由运行时转成结构化的拒绝响应。
Tracing 默认开启。每一次 LLM 生成、工具调用、交接、护栏都发一个 span,构成一棵 span 树。OPENAI_AGENTS_DISABLE_TRACING=1 可关闭;add_trace_processor(processor) 可把 span 扇出到你自己的后端(与 OpenAI 的并行)。
Session 把对话历史存进后端(SQLite、Redis、自定义)。Runner.run(agent, input, session=session) 自动加载历史并在本轮追加——这是第 07 节「短期记忆」在 SDK 层的落点。
⚠️ 三种失败模式:① 交接漂移(Handoff drift)——Agent A 交给 B,B 又交回 A,无限乒乓;解法是加一个跳数计数器(hop counter),超过 N 次交接即拒绝。② 护栏绕过(Guardrail bypass)——工具护栏只对函数工具触发;内置工具(文件读取、网页抓取)需要单独的策略,别假设它们被覆盖。③ 过度追踪(Over-tracing)——span 里可能含敏感内容;配合 OTel GenAI 内容捕获规则(第 23 节),把敏感内容外部存储、按 ID 引用,而非塞进 span。
原课程 code/main.py 用标准库实现了 SDK 的形状:
Agent、FunctionTool、Handoff(表现为带交接语义的函数工具)。Runner,带输入/输出/工具护栏、交接派发、跳数计数器。@dataclass class Agent: name: str instructions: str tools: list = field(default_factory=list) handoffs: list = field(default_factory=list) # 目标 Agent 名 def tool_list(self): # 交接被表示成名为 transfer_to_<name> 的工具 tools = list(self.tools) for target in self.handoffs: tools.append({"name": f"transfer_to_{target}", "description": f"把对话交给 {target} Agent"}) return tools
class Runner: def __init__(self, max_hops=5): self.max_hops = max_hops def run(self, agent, user_input, ctx): self.trace.open("agent", agent.name) for g in agent.input_guardrails: # 输入护栏(仅首个) if g(user_input): raise Tripwire(g.name) hops = 0 while True: decision = llm_decide(agent, user_input, ctx) # 选工具或收尾 if decision.is_handoff: # 交接 hops += 1 if hops > self.max_hops: raise HandoffDrift(hops) agent = REGISTRY[decision.target] # 拷贝上下文 + 切 Agent continue if decision.is_finish: for g in agent.output_guardrails: # 输出护栏(仅末个) if g(decision.output): raise Tripwire(g.name) self.trace.close(); return decision.output self.run_tool(agent, decision.tool_call, ctx) # 含工具护栏
class SpanEmitter: def open(self, kind, name): print(f"[span start] {kind}/{name}") def event(self, kind, **kv): print(f"[span event] {kind} {kv}") def close(self): print(f"[span end]")
运行 python3 code/main.py 会展示:两次成功的交接、一次输入护栏绊倒、一棵映射真实 SDK 发射形状的 span 树。
💡 设计要点:护栏的「绊倒」与执行的「成功」是同等一等公民——绊倒不是一个被吞掉的异常,它产出一条结构化的拒绝响应,让上层能据以行动。这呼应第 06 节:校验失败要变成观察字符串,而不是崩溃。
| 框架 | 何时选 | 关键取舍 |
|---|---|---|
| OpenAI Agents SDK | OpenAI 优先的产品 | handoff/guardrail/tracing 原生化,但绑 Responses API |
| Claude Agent SDK(第 17 节) | Claude 优先的产品 | subagent + session store,绑 Anthropic |
| LangGraph(第 13 节) | 想要显式状态与持久续跑 | 图形状更可控,但交接要自己写 |
| 自建 | 需要精确控制(语音、多厂商、联邦部署) | 自己实现这五个原语 |
跨厂商不变量仍是第 06 节那四样:name + description + JSON Schema 参数 + 关联 ID。SDK 只是把交接也变成了一个符合这套形状的工具。
原课程 outputs/skill-agents-sdk-scaffold.md:脚手架一个 Agents SDK 应用——分诊 Agent、交接、输入/输出/工具护栏、会话存储、一个追踪处理器。把 Mock LLM 换成真实 Responses API 调用即可投入生产,五个原语的组合逻辑无需改动。
nest_handoff_history 选项——交接前把历史消息折叠成一段摘要。add_trace_processor 接到一个 JSON logger。每个 span 发射出什么形状?openai-agents-python。你把哪些地方建模错了?transfer_to_<agent>,调用它触发运行时拷贝上下文、切 Agent、续跑;路由留模型,语义留代码。Input/OutputGuardrailTripwireTriggered,被转成拒绝响应而非崩溃。OPENAI_AGENTS_DISABLE_TRACING=1 关闭,add_trace_processor 扇出到自有后端。Runner.run 自动加载与追加。name+description+JSON Schema+关联 ID;交接只是又一个符合这套形状的工具。下一节,我们转向 Claude Agent SDK——把 subagent(子智能体)、session store(会话存储)与 Claude 原生的工具调用作为一等公民,看 Anthropic 这边如何回答同样的五个工程问题。