交接(handoff)与护栏(guardrail)
两个互补的机制:handoff 让 agent 之间互相转交(像分诊台把病人转给专科),guardrail 给整条流程加安全闸门(输入脏就拦、输出违规就拦、工具危险就拦)。
一、交接 Handoff
1. 它要解决的小问题
一个「总台 agent」处理不了所有事。遇到退款问题,应该交给「退款专员 agent」,由它接管整个对话(拿到完整历史,从此由它回答)。这和「调一个工具拿结果」不同——交接是换人。
2. 思路:把「换 agent」伪装成一个工具
模型怎么表达「我要交接」?SDK 的巧妙之处:把每个可交接的目标 agent 注册成一个特殊的「工具」(名字形如 transfer_to_<agent>)。模型「调用」这个工具,引擎就识别为交接,而不是普通函数。
回顾分类逻辑:resolveFunctionOrHandoff(runner/modelOutputs.ts:131)先在 handoffMap 里按工具名查——查到就是交接,返回 { type: 'handoff' }。
3. 真实实现
handoff(agent, config)(handoff.ts:346)创建一个Handoff对象。核心是onInvokeHandoff(handoff.ts:365):被「调用」时,如果配了inputType就先校验/解析模型给的参数、调你的onHandoff回调,最后return agent——即「下一个当前 agent 是它」。- 触发交接:模型输出被分类后,
resolveTurnAfterModelResponse在turnResolution.ts:995看到processedResponse.handoffs.length > 0就调executeHandoffCalls(...),产出next_step_handoff。 - 循环换 agent:回到主循环,
run.ts:1121的case 'next_step_handoff'执行state.setCurrentAgent(currentStep.newAgent),结束当前 agent 的 span,把步骤重置为run_again——下一拍就由新 agent 上场。 - 只认第一个交接:
recordHandoffRequest(runner/modelOutputs.ts:109)只持久化第一个交接请求(isPrimaryHandoff),后面的交接被忽略——注释解释这是为了不让历史里堆一串交接、误导后续判断(modelOutputs.ts:122-128)。
4. 输入过滤(input filter)
交接时新 agent 默认拿到完整历史,但你可以用 inputFilter 改写传给它的内容(比如删掉无关的工具噪音)。handoff.ts:437 把 config.inputFilter 挂上;run 级别还有个全局 handoffInputFilter(run.ts:251),优先级低于单个 handoff 的。
5. 类型安全的小心思
多个交接目标如果输出类型不一致,构造 Agent 时会打 warning(agent.ts:584-602),并建议用 Agent.create({...})(agent.ts:493)——它能自动把所有 handoff 的输出类型推断成一个联合类型,让 finalOutput 类型正确。
二、护栏 Guardrail
1. 它要解决的小问题
你需要在三个时机做安全检查:
- 输入级:用户输入要不要直接拒(如越狱 prompt)。
- 输出级:模型最终答案能不能放行(如泄露了 PII)。
- 工具级:某个工具的入参/出参要不要拦(如 SQL 注入)。
护栏就是这三个时机的「检查函数」,触发了就中止整个 run。
2. 思路:检查函数返回一个 tripwire 布尔
护栏函数返回 GuardrailFunctionOutput,关键字段 tripwireTriggered(guardrail.ts:24-34)。一旦为 true,引擎抛对应的 *TripwireTriggered 错误,run 终止。
// 示意,非源码 —— 一个输入护栏
const noJailbreak = defineInputGuardrail({
name: 'no-jailbreak',
execute: async ({ input }) => ({
tripwireTriggered: /ignore previous instructions/i.test(String(input)),
outputInfo: { checked: 'jailbreak' },
}),
});
// tripwireTriggered=true → run 立刻抛 InputGuardrailTripwireTriggered
3. 真实实现
- 定义:
defineInputGuardrail(guardrail.ts:135)和defineOutputGuardrail(guardrail.ts:271)把你的{ name, execute }包成带run()方法的 definition。 - 输入护栏可并行:
InputGuardrail有个runInParallel(默认true,guardrail.ts:78、135)——意味着护栏可以和模型调用同时跑,不阻塞首字延迟;只有命中时才回头中止。主循环里prepareTurn启动并行护栏,guardrailTracker跟踪它们,模型调用前后用throwIfError()/awaitCompletion()收口(见run.ts:1006、run.ts:1071)。 - 输出护栏在 final 时跑:主循环
next_step_final_output分支里runOutputGuardrails(state, this.outputGuardrailDefs, currentStep.output)(run.ts:1103)——拿到最终输出后才检查。 - 工具级护栏:
tool.ts:2024-2025把inputGuardrails/outputGuardrails挂到工具上;执行时由runToolInputGuardrails/runToolOutputGuardrails(utils/toolGuardrails.ts,从index.ts:270导出)跑。它们有更丰富的行为(不止 tripwire,还能 reject/替换输出),见toolGuardrail.ts的ToolGuardrailBehavior。
4. 巧妙之处:并行护栏 = 安全不付延迟税
输入护栏默认与模型调用并行(guardrail.ts:138 runInParallel = true),是个值得借鉴的设计: 安全检查通常比模型快,让它和模型赛跑,命中才中止,既安全又不拖慢正常路径。要强同步(先过护栏再调模型)就把 runInParallel 设 false。
三、横向对比
- handoff vs agent-as-tool 的取舍见
02-tools-and-agents-as-tools.md§4.2。 - 同 shelf 的兄弟项目里,「换 agent / 子 agent」这件事各家做法不同(有的用图节点、有的用消息路由);这里的取舍是「用模型原生的 tool-call 协议表达交接」,好处是模型不需要学新概念。
4. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 交接工厂 | handoff.ts | handoff、Handoff、getHandoff |
| 交接触发/执行 | runner/turnResolution.ts · runner/toolExecution.ts | executeHandoffCalls |
| 交接 vs 函数区分 | runner/modelOutputs.ts | resolveFunctionOrHandoff、recordHandoffRequest |
| 输入/输出护栏 | guardrail.ts | defineInputGuardrail、defineOutputGuardrail、GuardrailFunctionOutput |
| 护栏在循环里的收口 | run.ts · runner/guardrails.ts | runOutputGuardrails、createGuardrailTracker |
| 工具级护栏 | toolGuardrail.ts · utils/toolGuardrails.ts | defineToolInputGuardrail、runToolInputGuardrails |