The OpenAI Agents SDK: Agents, Handoffs, Guardrails, Sessions, Tracing
Fold yesterday's hand-written loop into a few lines of API with the official Agents SDK: how agents hand off to each other, how to put guardrails at the entry and exit, how to remember multiple turns, and how to see every step clearly.
今日目标
- 能用 Agent 与 run 或 Runner 写出一个带函数工具的最小 Agent,并说清 maxTurns 的作用
- 能用 handoffs 让分诊 Agent 把对话交给专家 Agent,并解释它与「一个大 Agent 加很多工具」的取舍
- 能写一个输入侧 guardrail 并捕获 tripwire 异常,说清输入侧与输出侧护栏的分工
昨天你手写了「请求工具 → 执行 → 回填」的一次来回。今天要解决的是:这个来回要转很多圈怎么办?活太杂一个搭档接不下来怎么办?怎么防止他接了不该接的活、说了不该说的话?读完做完,回到页面顶部把三条目标勾掉。
小白版讲解
Agent 与 Runner:把「模型 + 循环 + 工具」收进一个对象和一个函数
昨天的脚本里,「模型请求工具、你执行、回填、再问」这个循环只转了一圈。真实任务往往要转好几圈:先查清单,再查详情,再决定改什么。手写这个 while 循环不难,但每个项目都写一遍就烦了,而且要处理循环不停、工具报错、参数解析这些边角。OpenAI 的 Agents SDK 做的第一件事就是把这个循环收起来:Agent 对象装下指令和工具,run 函数替你转圈。
回到搭档的类比:Agent 就是一位有明确岗位说明(instructions)和一套工具箱(tools)的搭档;run 就是你把活交给他、等他做完回来。他中间调了几次工具、想了几轮,你不用管,只要结果——除非圈数超了。
import { Agent, run, tool } from '@openai/agents'
import { z } from 'zod'
const listRoutes = tool({
name: 'list_routes',
description: '列出 TODO API 的全部路由',
parameters: z.object({}),
execute: async () => ['POST /todos', 'GET /todos', 'DELETE /todos/:id'],
})
const helper = new Agent({
name: 'TODO API 助手',
instructions: '你只回答与这个 TODO API 项目有关的问题,需要事实时先调工具。',
model: process.env.OPENAI_MODEL,
tools: [listRoutes],
})
const result = await run(helper, '项目里有哪些接口还没有测试?', { maxTurns: 5 })
console.log(result.finalOutput)import os
from agents import Agent, Runner, function_tool
@function_tool
def list_routes() -> list[str]:
"""列出 TODO API 的全部路由"""
return ["POST /todos", "GET /todos", "DELETE /todos/:id"]
helper = Agent(
name="TODO API 助手",
instructions="你只回答与这个 TODO API 项目有关的问题,需要事实时先调工具。",
model=os.environ.get("OPENAI_MODEL"),
tools=[list_routes],
)
result = Runner.run_sync(helper, "项目里有哪些接口还没有测试?", max_turns=5)
print(result.final_output)对照昨天:tool 助手(Python 是 function_tool 装饰器)替你把 zod schema 或类型注解变成昨天手写的那段 JSON Schema;execute 就是昨天的 runTool;run 里面藏着昨天的「筛 function_call、回填 function_call_output、再调一次」。它藏起来的正是你昨天亲手写过的东西,所以出问题时你知道该往哪层找。
maxTurns 是必须理解的一个参数。一「turn」是模型的一次调用,循环每转一圈算一次;超过上限 SDK 抛 MaxTurnsExceededError(TypeScript 默认 10)。它不是性能参数,是安全阀:一个把工具结果误读、反复调同一个工具的 Agent,没有这个阀会一直烧钱到你发现为止。生产里要按任务类型设:一问一答的助手 3 到 5 圈够了,要改多个文件的任务可能要 20 圈以上。TypeScript 里还有 Runner 类,new Runner({ ... }) 后调 runner.run(),用来给一批运行共用同一套配置(模型、追踪、默认的 maxTurns);单次跑用 run 就够。
函数工具:声明参数,SDK 替你生成 schema
工具是 Agent 的手。上一节的 list_routes 没有参数,真实工具几乎都有。声明方式两边都是「用你熟悉的类型系统写参数,SDK 转成 JSON Schema」:TypeScript 用 zod(注意 SDK 用的是 zod v4),Python 用函数签名的类型注解加 docstring。
import { tool } from '@openai/agents'
import { z } from 'zod'
const todos = [{ id: 1, title: '给 POST /todos 补校验', done: false }]
export const markDone = tool({
name: 'mark_done',
description: '把某条 TODO 标记为完成',
parameters: z.object({
id: z.number().int().describe('TODO 的 id'),
}),
execute: async ({ id }) => {
const todo = todos.find((t) => t.id === id)
if (!todo) return `没有 id 为 ${id} 的 TODO`
todo.done = true
return `已完成:${todo.title}`
},
})from agents import function_tool
todos = [{"id": 1, "title": "给 POST /todos 补校验", "done": False}]
@function_tool
def mark_done(id: int) -> str:
"""把某条 TODO 标记为完成
Args:
id: TODO 的 id
"""
todo = next((t for t in todos if t["id"] == id), None)
if todo is None:
return f"没有 id 为 {id} 的 TODO"
todo["done"] = True
return f"已完成:{todo['title']}"两个工程习惯值得养成。第一,description 和参数描述是给模型看的接口文档,写得越像人话,模型选错工具、填错参数的概率越低——「把某条 TODO 标记为完成」比「更新 todo」好得多。第二,工具的返回值也是给模型看的,返回一句「没有 id 为 7 的 TODO」比抛异常好:抛异常会让 SDK 把错误文本回给模型,模型可能理解也可能不理解;一句清楚的话它一定能理解,并据此决定下一步。
工具有副作用时(上面的 mark_done 就改了数据),还要想清楚 D1 讲过的那件事:模型判断要调它,你的代码就真的执行了。SDK 层面可以用工具级的 guardrail(下面讲)或 needsApproval 这类机制加一道人工确认,原则和 Codex 的审批一样——不可逆的动作前要有人点头。
handoffs:分诊 Agent 把对话整个交出去
活杂了怎么办?一个搭档什么都懂是不现实的。公司里的做法是前台分诊:你来问,前台判断这事归谁,然后把你整个人领到那位同事面前,之后你直接跟那位同事说,前台不再插手。Agents SDK 里这叫 handoff(交接)——注意和「工具」的区别:调工具是搭档替你去问了一句再回来告诉你;handoff 是搭档把对话交给另一位,之后由那位负责回答。
实现上,handoff 就是一个特殊的工具:给分诊 Agent 配一组 handoffs,SDK 会为每个目标 Agent 生成一个名为 transfer_to_<agent_name> 的工具;分诊 Agent 一调它,运行时就把对话(可以经过过滤)交给目标 Agent 继续。
import { Agent, run } from '@openai/agents'
import { RECOMMENDED_PROMPT_PREFIX } from '@openai/agents-core/extensions'
const validationExpert = new Agent({
name: 'validation_expert',
instructions: '你负责 TODO API 的输入校验问题:schema 设计、错误响应形状。',
model: process.env.OPENAI_MODEL,
})
const testingExpert = new Agent({
name: 'testing_expert',
instructions: '你负责 TODO API 的单元测试问题:用例设计、测试隔离。',
model: process.env.OPENAI_MODEL,
})
const triage = Agent.create({
name: 'triage',
instructions: `${RECOMMENDED_PROMPT_PREFIX}
你是分诊台。校验相关交给 validation_expert,测试相关交给 testing_expert,其它自己简短回答。`,
model: process.env.OPENAI_MODEL,
handoffs: [validationExpert, testingExpert],
})
const result = await run(triage, 'POST /todos 的 title 为空时应该返回什么?')
console.log(result.lastAgent?.name) // validation_expert
console.log(result.finalOutput)import os
from agents import Agent, Runner
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
validation_expert = Agent(
name="validation_expert",
instructions="你负责 TODO API 的输入校验问题:schema 设计、错误响应形状。",
model=os.environ.get("OPENAI_MODEL"),
)
testing_expert = Agent(
name="testing_expert",
instructions="你负责 TODO API 的单元测试问题:用例设计、测试隔离。",
model=os.environ.get("OPENAI_MODEL"),
)
triage = Agent(
name="triage",
instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
你是分诊台。校验相关交给 validation_expert,测试相关交给 testing_expert,其它自己简短回答。""",
model=os.environ.get("OPENAI_MODEL"),
handoffs=[validation_expert, testing_expert],
)
result = Runner.run_sync(triage, "POST /todos 的 title 为空时应该返回什么?")
print(result.last_agent.name) # validation_expert
print(result.final_output)RECOMMENDED_PROMPT_PREFIX 是官方给的一段前缀,告诉模型「你身处一个多 Agent 系统,交接是正常操作」,实测能明显提高交接的准确率。result.lastAgent(Python 是 last_agent)告诉你最后是谁在回答——这是验证路由是否正确的最直接方法,实验里你会拿它做断言。需要更细的控制时用 handoff() 助手包一层:toolNameOverride 改工具名,onHandoff 在交接时回调(记日志、发通知),inputFilter 过滤交给下一位的历史(比如把之前的工具调用记录删掉,官方 extensions 里有 removeAllTools 这类现成过滤器),inputType 让模型在交接时附带结构化的元信息(比如「原因」「优先级」)。
那什么时候该拆多个 Agent 用 handoff,什么时候一个大 Agent 挂一堆工具更好?判据不是「工具多不多」,而是指令会不会互相打架。校验专家的指令里全是 zod 与错误形状,测试专家的指令里全是用例隔离与 mock——把这两套写进同一份 instructions,模型每次都要在两套上下文里切换,出错率上升,提示也越长越贵。指令彼此独立、上下文能明显分开的,拆;工具虽多但共享同一套背景知识的,不拆。还有一个硬约束:handoff 发生在同一次 run 里,输入侧 guardrail 只对链条上的第一个 Agent 生效,这引出下一节。
guardrails:入口查用户,出口查模型
搭档再能干,也有两种事不该发生:接了不该接的活(用户让 TODO API 助手写情书),说了不该说的话(回答里带出了数据库连接串)。前者要在入口拦,后者要在出口拦。Agents SDK 把这两道门叫 input guardrail 与 output guardrail,统称 guardrails(护栏)。
一个 guardrail 就是一个函数:拿到输入(或输出)和上下文,返回一个判断,其中 tripwireTriggered(Python 是 tripwire_triggered)为真表示「拉响警报」。警报一响,run 立刻抛出 InputGuardrailTripwireTriggered 或 OutputGuardrailTripwireTriggered 异常,你在外面捕获、决定怎么回应用户。
import { Agent, run, InputGuardrailTripwireTriggered } from '@openai/agents'
import type { InputGuardrail } from '@openai/agents'
// 一个不花钱的护栏:纯规则判断,不调模型
const onTopicOnly: InputGuardrail = {
name: 'on_topic_only',
execute: async ({ input }) => {
const text = typeof input === 'string' ? input : JSON.stringify(input)
const offTopic = !/todo|接口|校验|测试|路由|api/i.test(text)
return { outputInfo: { offTopic }, tripwireTriggered: offTopic }
},
}
const triage = new Agent({
name: 'triage',
instructions: '你只处理 TODO API 项目的问题。',
model: process.env.OPENAI_MODEL,
inputGuardrails: [onTopicOnly],
})
try {
const result = await run(triage, '帮我写一首情诗')
console.log(result.finalOutput)
} catch (err) {
if (err instanceof InputGuardrailTripwireTriggered) {
console.log('这个问题与项目无关,我只回答 TODO API 相关的问题。')
} else {
throw err
}
}import os
import re
from agents import (
Agent,
GuardrailFunctionOutput,
InputGuardrailTripwireTriggered,
RunContextWrapper,
Runner,
TResponseInputItem,
input_guardrail,
)
# 一个不花钱的护栏:纯规则判断,不调模型
@input_guardrail
async def on_topic_only(
ctx: RunContextWrapper[None], agent: Agent, input: str | list[TResponseInputItem]
) -> GuardrailFunctionOutput:
text = input if isinstance(input, str) else str(input)
off_topic = re.search(r"todo|接口|校验|测试|路由|api", text, re.I) is None
return GuardrailFunctionOutput(output_info={"off_topic": off_topic}, tripwire_triggered=off_topic)
triage = Agent(
name="triage",
instructions="你只处理 TODO API 项目的问题。",
model=os.environ.get("OPENAI_MODEL"),
input_guardrails=[on_topic_only],
)
try:
result = Runner.run_sync(triage, "帮我写一首情诗")
print(result.final_output)
except InputGuardrailTripwireTriggered:
print("这个问题与项目无关,我只回答 TODO API 相关的问题。")上面的护栏是纯规则,零成本、零延迟;更常见的是用一个便宜的小模型当护栏——在 execute 里再跑一个只做分类的小 Agent,判断「这是不是项目相关问题」。输入侧护栏和主 Agent 是并行跑的,所以它不会拖慢正常请求;一旦警报响起,主 Agent 那边昂贵的运行会被取消,这正是护栏省钱的方式:用一个便宜模型挡住那些本来要让贵模型白跑一趟的请求。
输入侧还是输出侧?分工很清楚。输入侧管「该不该做」:话题越界、明显的注入攻击、超出服务范围的请求,越早拦越省。输出侧管「能不能说」:泄露敏感信息、格式不合规、违反业务规则的建议,只有等模型说完才能查。两边的漏网情况也不同:输入侧拦不住「问题正常但回答跑偏」,输出侧拦不住「模型已经调了有副作用的工具」——所以有副作用的工具还得有第三种护栏,工具级 guardrail,围着每一次函数调用跑。三层加起来才是完整的防线。
记住那条硬约束:输入侧 guardrail 只在链条的第一个 Agent 上生效,输出侧只在产出最终回答的那个 Agent 上生效。所以护栏要挂在分诊 Agent 上,挂在专家 Agent 上的输入护栏永远不会跑。
sessions:多轮记忆交给 SDK 存
昨天讲了多轮历史的两条路:服务端存(previous_response_id)或自己带。Agents SDK 把这个选择包成了 session:你给 run 传一个 session 对象,SDK 在每次运行前从里面取历史、运行后把新条目写回去,你的代码不再碰 input 数组。
import { Agent, run, MemorySession } from '@openai/agents'
const helper = new Agent({
name: 'helper',
instructions: '你是 TODO API 项目的助手。',
model: process.env.OPENAI_MODEL,
})
const session = new MemorySession() // 进程内存,重启即失
await run(helper, '我们的校验库是 zod。', { session })
const result = await run(helper, '刚才说的校验库是什么?', { session })
console.log(result.finalOutput) // 应该提到 zodimport os
from agents import Agent, Runner, SQLiteSession
helper = Agent(
name="helper",
instructions="你是 TODO API 项目的助手。",
model=os.environ.get("OPENAI_MODEL"),
)
session = SQLiteSession("conversation_todo") # 文件或内存 SQLite,重启后还在
Runner.run_sync(helper, "我们的校验库是 zod。", session=session)
result = Runner.run_sync(helper, "刚才说的校验库是什么?", session=session)
print(result.final_output) # 应该提到 zod两边可选的存储不同:TypeScript 有进程内的 MemorySession 和用 OpenAI Conversations 接口托管的 OpenAIConversationsSession;Python 更丰富,SQLiteSession、RedisSession、SQLAlchemySession、MongoDBSession,还有一个 EncryptedSession 包装器给任何 session 加透明加密。Session 的接口只有四个动作:取历史、追加条目、弹出最后一条、清空。pop_item 这个看起来奇怪的方法有一个实际用途:用户说「刚才那句撤回」时,把最后一轮从历史里拿掉再重跑。
选 session 还是 previous_response_id?session 的历史在你手里(内存、SQLite、Redis),可以审计、可以裁剪、可以换模型重放;previous_response_id 的历史在 OpenAI 那边,请求最小但你看不到全貌。两者能同时用——SDK 也支持 previousResponseId 与 conversationId 这两种服务端方案。原则和昨天一样:面向用户的生产系统,历史自己至少落一份。
tracing:默认开着的追踪
最后一件事:这一切跑起来之后,你怎么知道分诊有没有交接对、护栏有没有误拦、哪一步花了最多 token?Agents SDK 的答案是 tracing,而且默认就是开的——每次 run 都会生成一条 trace,里面按层级记录 Agent 的每一轮、每次模型调用、每次工具调用、每次交接和护栏判断,传到 OpenAI 平台的 Traces 面板里可视化。
你只需要知道三个动作。看:跑完一次真实运行,去平台的 Traces 页面找到它,展开看分诊 Agent 是在第几轮调了 transfer_to_validation_expert。关:环境变量 OPENAI_AGENTS_DISABLE_TRACING=1 全局关掉;TypeScript 里也可以在 run 配置里设 tracingDisabled。处理敏感数据或合规要求数据不出境时要关,或者用 setTraceProcessors 换成你自己的导出器,比如接到 OpenTelemetry。分组:默认每次 run 一条 trace;一段多轮对话想归到一起,用 withTrace(Python 是 with trace(...))把多次 run 包起来,或者设 groupId 用会话 id 关联。
tracing 是这门课里最容易被跳过、又最值钱的一节。没有它,多 Agent 系统出问题时你只能猜;有了它,「为什么这个问题被交给了测试专家」是一个三十秒能看清的事实。30 天课 D21 讲的评估与可观测,在这里已经有了一个开箱即用的起点。
源码导读
动手实验
代码在 labs/codex-mastery/day-04-handoff-guardrail,starter/ 挖了四个练习点,solution/ 是完整答案。MOCK 模式下不调模型,但护栏、路由、session 这些业务逻辑都真实执行——这正是 MOCK 的意义:验证你的编排逻辑,而不是验证模型。
- 跑
MOCK=1 pnpm start "有哪些接口",看到一个带函数工具的最小 Agent 给出回答,并打印lastAgent。 - 实现练习 1:把 helper 拆成 triage 加两个专家,用
handoffs交接;跑校验类与测试类两个问题,确认lastAgent分别是两位专家。 - 实现练习 2 与 3:写
onTopicOnly输入护栏挂到 triage 上,在main里捕获InputGuardrailTripwireTriggered打印友好提示;跑「帮我写一首情诗」确认被拦。 - 实现练习 4:
--session模式下用同一个MemorySession连跑两次,确认第二次记得第一次。 - 配好 key 与模型去掉 MOCK 跑一次,到 Traces 面板里找到这次运行,看交接发生在第几轮。
面试题
今天 3 道题在下方题库区,侧重多 Agent 拆分的判据、guardrail 的放置位置、会话记忆与可观测的工程取舍。展开后先看「分析过程」再看要点——照着推导练,比背要点管用。
检查清单与明日预告
- 能用 Agent 与 run 或 Runner 写出一个带函数工具的最小 Agent,并说清 maxTurns 的作用
- 能用 handoffs 让分诊 Agent 把对话交给专家 Agent,并解释它与「一个大 Agent 加很多工具」的取舍
- 能写一个输入侧 guardrail 并捕获 tripwire 异常,说清输入侧与输出侧护栏的分工
- 能说出「输入护栏只在第一个 Agent 上跑」这条约束对护栏放置位置的影响
- 实验的 4 条验收标准全部通过
- 3 道面试题不看要点也能答出至少 2 道
明天(D5)是收官:用同一个任务——给 TODO API 补输入校验和单元测试——分别走一遍 Codex 与 Claude Code,从交代方式、审批、验证、成本四个维度做对比,再把两家工具组合成「一个写、一个审」的日常工作流。把对比放在最后是有意的:前四天你已经知道 Codex 每一层是怎么工作的,明天看到两家的差异时,才能判断那是工作方式的差异还是能力的差异——这两种差异,应对方法完全不同。
Interview questions
When should you split one agent into several connected by handoffs, and when is a single agent with many tools the better design?什么时候该把一个 Agent 拆成多个、用 handoff 交接?什么时候「一个大 Agent 加很多工具」反而更好?
Common in ChinaCommon overseasIntermediate#agents-sdk#handoffs#architectureHow to reason about it · think before answering
- This tests your splitting criterion, not API fluency; 'split when there are many tools' is the common wrong answer.
- First separate handoffs from tools: a tool call fetches an answer and returns; a handoff transfers the whole conversation so the receiving agent owns it, even though it is implemented as a transfer_to_xxx tool.
- The criterion is whether instructions conflict: when two task groups need independent, clashing background, constraints and tone, one instruction block forces constant context switching, longer prompts and more errors, so split; many tools sharing one background do not justify a split.
- Name the costs: an extra model call for triage, possible misrouting, input guardrails only on the first agent, and history trimming across agents via inputFilter.
- Expect the follow-up: what if triage misroutes? Use RECOMMENDED_PROMPT_PREFIX, assert on lastAgent in regression tests, inspect the handoff turn in tracing, and allow experts to hand back.
分析过程 · 先想清楚再作答
- 这题考的是拆分判据,不是会不会用 API。答「工具多了就拆」是最常见的错误,工具数量不是判据。
- 先说清 handoff 与工具的区别:调工具是替你去问一句再回来,handoff 是把对话整个交给另一个 Agent,之后由它负责;实现上 handoff 也是一个名为 transfer_to_xxx 的工具,但语义是转移控制权。
- 判据是「指令会不会互相打架」:两组任务需要的背景知识、约束、语气彼此独立且冲突时,塞进一份 instructions 会让模型反复切换上下文、提示越长越贵、出错率上升,这时拆;工具虽多但共享同一套背景的,不拆。
- 补拆分的代价:多一次模型调用(分诊那一跳)、路由可能错、输入护栏只在第一个 Agent 上跑、跨 Agent 的历史要靠 inputFilter 裁剪。
- 可预期的追问:分诊错了怎么办?用 RECOMMENDED_PROMPT_PREFIX 提高交接准确率,用 lastAgent 做回归断言,用 tracing 看交接发生在哪一轮,必要时让专家 Agent 也能交接回分诊台。
Key points
- A handoff transfers conversational control; a tool call only fetches a result
- Split on conflicting instructions, not on tool count
- Costs: an extra hop, possible misrouting, input guardrails only on the first agent
- Control routing quality with the recommended prefix, lastAgent assertions and tracing
答题要点
- handoff 转移的是对话控制权,工具调用只是取一次结果
- 拆分判据是指令是否互相打架,不是工具数量
- 拆的代价:多一跳、可能路由错、输入护栏只在第一个 Agent 生效
- 用前缀提示、lastAgent 断言与 tracing 控制路由质量
Should guardrails sit on the input side or the output side? What does each cost, what does it catch, and what slips through?guardrail 应该放在输入侧还是输出侧?各自的成本、能拦住什么、拦不住什么?
Common in ChinaCommon overseasDeep dive#agents-sdk#guardrails#safetyHow to reason about it · think before answering
- The crux is 'what slips through'; saying 'use both' without naming each side's blind spot signals no production incidents survived.
- Division of labor: input guardrails decide whether to act at all (off-topic, obvious injection, out of scope) and are cheapest early; output guardrails decide whether the answer may be said (leaks, format, policy) and can only run after generation.
- Cost: input guardrails run in parallel with the main agent and cancel its expensive run on a tripwire, so a cheap classifier there saves money; output guardrails wait for the full run and only prevent incidents.
- Blind spots: input cannot catch a normal question with a drifting answer; output cannot undo a side-effecting tool already called, hence a third layer of tool-level guardrails around each function call.
- Add the SDK constraint: input guardrails run only on the first agent, output guardrails only on the agent producing the final answer; misplaced guardrails never execute.
- Expect the follow-up: the common failure mode? Too strict, not too loose; regex blocklists over-block real users, so keep a regression set of legitimate requests and watch the false-block rate.
分析过程 · 先想清楚再作答
- 题眼在「拦不住什么」。只说两边都要放而不说各自的漏网情况,就是没在生产里被漏网案例打过脸。
- 先给分工:输入侧管「该不该做」——话题越界、明显注入、超出服务范围,越早拦越省;输出侧管「能不能说」——泄露敏感信息、格式不合规、违反业务规则,只有模型说完才能查。
- 再说成本:输入护栏与主 Agent 并行跑,警报一响就取消主 Agent 的昂贵运行,所以用便宜小模型做输入护栏是省钱手段;输出护栏必须等主 Agent 跑完,省不了钱,只能防事故。
- 漏网情况:输入侧拦不住「问题正常但回答跑偏」;输出侧拦不住「模型已经调了有副作用的工具」——所以有副作用的工具需要第三层,围着每次函数调用跑的工具级护栏。
- 补一条 SDK 约束:输入护栏只在链条第一个 Agent 上跑,输出护栏只在产出最终回答的 Agent 上跑,挂错位置等于没挂。
- 可预期的追问:护栏最常见的失败模式是什么?太严而不是太松——正则黑名单误拦正常用户;上线前要有正常请求的回归集,误拦率是必看指标。
Key points
- Input side decides whether to act and is cheapest early; output side decides what may be said and only runs afterwards
- Input guardrails run in parallel and cancel the main run, so cheap models save money there; output guardrails only prevent incidents
- Input misses drifting answers, output misses side effects already taken; tool-level guardrails add the third layer
- Input guardrails run only on the first agent; the common failure is over-blocking, so keep a regression set
答题要点
- 输入侧管该不该做,越早拦越省;输出侧管能不能说,只能事后查
- 输入护栏与主 Agent 并行、触发即取消,便宜模型在此省钱;输出护栏省不了钱只防事故
- 输入侧漏「回答跑偏」,输出侧漏「已调有副作用的工具」,需工具级护栏补第三层
- 输入护栏只在第一个 Agent 生效;常见失败是太严,需正常请求回归集
Both Agents SDK sessions and the Responses API's previous_response_id remember multi-turn state. How do you choose, and what role does tracing play?Agents SDK 的 session 和 Responses API 的 previous_response_id 都能记住多轮,怎么选?tracing 在这里起什么作用?
Common in ChinaCommon overseasIntermediate#agents-sdk#sessions#tracingHow to reason about it · think before answering
- This probes your sensitivity to who holds the state, the SDK-level echo of 'you carry the history yourself'.
- Ask three questions: can the history be audited, trimmed or replayed, and kept within data-residency rules? previous_response_id keeps history server-side with minimal requests but answers all three poorly; sessions keep it in your store and answer all three, at the cost of managing storage.
- Conclude: prototypes and internal tools take previous_response_id; user-facing production keeps its own copy, for which sessions are the ready-made path; both can coexist.
- Of the four session operations, pop_item deserves mention: removing the last turn to honor a user's undo is only possible when you own the history.
- Tracing makes multi-agent behavior explainable: on by default, one trace per run recording turns, tool calls, handoffs and guardrail results; group a conversation with withTrace or group_id; disable via env var or swap in your own exporter for sensitive data.
- Expect the follow-up: does tracing ship user data out? By default it goes to the platform dashboard, so regulated settings must disable it or replace the processors.
分析过程 · 先想清楚再作答
- 这题考的是对「状态放在谁手里」的敏感度,是 30 天课 D1「历史靠你自己搬」在 SDK 层的翻版。
- 拆法是问三件事:历史能不能审计、能不能裁剪或重放、能不能满足数据驻留要求。previous_response_id 的历史在服务端,请求最小、代码最简,但三个问题都答不好;session 的历史在你手里(内存、SQLite、Redis),三个都能做,代价是自己管存储。
- 结论:原型与内部工具用 previous_response_id 省事;面向用户的生产系统至少自己落一份历史,session 是现成的落法;两者可以同时用。
- session 的四个接口(取、追加、弹出最后一条、清空)里 pop_item 值得点出:用户撤回上一句时把最后一轮拿掉再重跑,这是自己持有历史才能做的事。
- tracing 的作用是让多 Agent 系统的行为可解释:默认开启,每次 run 一条,记录每轮、每次工具调用、交接与护栏判断;用 withTrace 或 group_id 把一段对话归到一起;敏感数据场景用环境变量关掉或换成自己的导出器。
- 可预期的追问:tracing 会不会把用户数据传出去?默认会传到平台面板,所以合规场景要么关、要么 setTraceProcessors 换成自己的后端。
Key points
- previous_response_id keeps history server-side, small and simple, but weak on audit, trimming and residency
- Sessions keep history in your store, auditable and replayable, with pop_item for undo; production keeps its own copy
- Tracing is on by default, one trace per run, capturing turns, tools, handoffs and guardrails, grouped via group_id
- For sensitive data disable it with OPENAI_AGENTS_DISABLE_TRACING or swap in your own exporter
答题要点
- previous_response_id 历史在服务端,请求小代码简,但难审计、难裁剪、难满足数据驻留
- session 历史在自己手里,可审计可重放,pop_item 支持撤回;生产至少自己落一份
- tracing 默认开、每次 run 一条,记录每轮工具、交接与护栏,用 group_id 归组
- 敏感数据场景用 OPENAI_AGENTS_DISABLE_TRACING 关掉或换成自己的导出器
Comments
Sign in to join the discussion
No comments yet — be the first.