第二周:OpenAI Agents SDK 的 Agent、Trace 与 Runner
代码编排、agent 当工具、handoff 交接——三种方式的差别不在写法,在谁掌握控制权,以及出了问题你能不能查出是哪一步歪的。
第一周从零写了循环。这一周换成用 SDK,而 SDK 真正教给你的是多个 agent 怎么协作。
三个核心概念
Agent、Runner、Trace —— 三个词撑起整个 SDK。
Agent 是一份配置:指令、工具、模型、护栏。它本身不动,是个描述。
Runner 是那个跑循环的东西。Runner.run(agent, input) 里面就是第一周那个 while,只是加了重试、异常、并发。
Trace 是我认为最容易被跳过、也最该早点重视的一个。它把一次运行里的每次模型调用、每次工具调用串成一条可回放的链。
为什么 trace 值得单独说:agent 出问题的时候,症状往往在第五步,原因在第二步。没有 trace,你只能重跑一遍猜;有 trace,你能看到第二步的模型输出到底是什么。这和 execution lineage 是同一件事 —— 能不能事后重建现场,决定了你能不能排查。
三种编排方式
这是这一周的核心。同样是「让多个 agent 协作」,有三种写法,控制权在不同的地方。
| 方式 | 谁决定下一步 | 什么时候用 |
|---|---|---|
| 代码编排 | 你的 Python | 步骤确定、要并行、要可复现 |
| Agent as tool | 主 agent 选调用哪个 | 子任务边界清楚,但顺序不确定 |
| Handoff 交接 | 当前 agent 决定交给谁 | 领域切换,比如售前转售后 |
代码编排用 asyncio.gather 把几个 agent 并发跑起来,然后自己合并结果。它最土,也最可控 —— 同一份输入两次跑,路径完全一样。
Agent as tool 是把一个 agent 包装成另一个 agent 的工具。主 agent 看到的是一个叫 sales_agent 的函数,调不调、调几次由它决定。
Handoff 是控制权的真正转移:A 决定这事该 B 管,把整个对话交给 B,A 退出。
这三种的排序不是能力从低到高,是自主性从低到高,而自主性的代价是可复现性。 选哪个的判据很实际:你需不需要比较两次运行的结果? 需要就往上面选。
会话记忆
SQLiteSession 这类东西解决的是:多次 Runner.run 之间,agent 记不记得上次说了什么。
这里有个概念区分值得钉死:
- 上下文(context) 是这一轮请求里模型看见的东西,请求结束就没了
- 记忆(memory) 是跨轮、跨天存在的东西,落在磁盘或数据库里
agent 会忘,仓库不会。
追踪这一格我付过一次很具体的学费。我那套东西把每次判定都写进 events.jsonl,看起来很完备 —— 直到我要做一次消融对比,才发现拒收事件里少记了一个 storyId。少这一个字段的后果是:我知道被拒了多少条,但没法把「这条被拒的」和「同一个输入在另一组里的结果」对上,配对分析直接做不了。更糟的是我第一次跑消融拿到的是个假的零 —— 因为我读的是只存活下来的那份结果文件,被拒的根本不在里面。我的经验是:日志要能回答「这一条后来怎么了」,而不只是「这一步发生了什么」。 一个循环要跨天接着干,状态必须落盘 —— 而 session 就是 SDK 给的最小落盘方案。
护栏与结构化输出
结构化输出用 Pydantic 定义返回的形状,让模型的输出能被程序读。这里有一条实战经验:schema 迁就模型,比让模型迁就 schema 划算。
同一个字段,模型这一次写 on,下一次写 action,再下一次写 trigger。每一种都可以再写一版提示词去纠正,但那是把成本压在每一次调用上;写在解析层只付一次。
护栏在 SDK 里分输入和输出两侧。输入护栏拦住不该处理的请求,输出护栏拦住不该返回的内容。
值得补一句 SDK 文档不会强调的:护栏拦下来之后不能静默跳过。「处理了」「处理失败」「被拦下没处理」是三个状态,混成两个就会出现假绿灯。
边界与代价
SDK 省掉的是管道,不是判断。 重试、追踪、并发这些它替你做了;但「这一步该不该让模型决定」「停止条件是什么」它不替你想。
Handoff 用起来最爽,也最难调试。 控制权转移之后,出了问题你得先搞清楚是在哪个 agent 手里出的。trace 在这种模式下不是可选项。
多 agent 不总是更好。 有实验专门量过让 meta agent 去设计 agent 结构,结论是经济回本点在上万个样例,很多场景下永远回不了本。多 agent 编排比那个便宜,但同样的问题存在:先确认瓶颈真的在「一个 agent 不够」,再上第二个。