跳到主要内容

交接(handoff)与护栏(guardrail)

两个互补的机制:handoff 让 agent 之间互相转交(像分诊台把病人转给专科),guardrail 给整条流程加安全闸门(输入脏就拦、输出违规就拦、工具危险就拦)。

一、交接 Handoff

1. 它要解决的小问题

一个「总台 agent」处理不了所有事。遇到退款问题,应该交给「退款专员 agent」,由它接管整个对话(拿到完整历史,从此由它回答)。这和「调一个工具拿结果」不同——交接是换人。

2. 思路:把「换 agent」伪装成一个工具

模型怎么表达「我要交接」?SDK 的巧妙之处:把每个可交接的目标 agent 注册成一个特殊的「工具」(名字形如 transfer_to_<agent>)。模型「调用」这个工具,引擎就识别为交接,而不是普通函数。

回顾分类逻辑:resolveFunctionOrHandoffrunner/modelOutputs.ts:131)先在 handoffMap 里按工具名查——查到就是交接,返回 { type: 'handoff' }

3. 真实实现

  • handoff(agent, config)handoff.ts:346)创建一个 Handoff 对象。核心是 onInvokeHandoffhandoff.ts:365):被「调用」时,如果配了 inputType 就先校验/解析模型给的参数、调你的 onHandoff 回调,最后 return agent——即「下一个当前 agent 是它」。
  • 触发交接:模型输出被分类后,resolveTurnAfterModelResponseturnResolution.ts:995 看到 processedResponse.handoffs.length > 0 就调 executeHandoffCalls(...),产出 next_step_handoff
  • 循环换 agent:回到主循环,run.ts:1121case 'next_step_handoff' 执行 state.setCurrentAgent(currentStep.newAgent),结束当前 agent 的 span,把步骤重置为 run_again——下一拍就由新 agent 上场。
  • 只认第一个交接recordHandoffRequestrunner/modelOutputs.ts:109)只持久化第一个交接请求(isPrimaryHandoff),后面的交接被忽略——注释解释这是为了不让历史里堆一串交接、误导后续判断(modelOutputs.ts:122-128)。

4. 输入过滤(input filter)

交接时新 agent 默认拿到完整历史,但你可以用 inputFilter 改写传给它的内容(比如删掉无关的工具噪音)。handoff.ts:437config.inputFilter 挂上;run 级别还有个全局 handoffInputFilterrun.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,关键字段 tripwireTriggeredguardrail.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. 真实实现

  • 定义defineInputGuardrailguardrail.ts:135)和 defineOutputGuardrailguardrail.ts:271)把你的 { name, execute } 包成带 run() 方法的 definition。
  • 输入护栏可并行InputGuardrail 有个 runInParallel(默认 trueguardrail.ts:78135)——意味着护栏可以和模型调用同时跑,不阻塞首字延迟;只有命中时才回头中止。主循环里 prepareTurn 启动并行护栏,guardrailTracker 跟踪它们,模型调用前后用 throwIfError() / awaitCompletion() 收口(见 run.ts:1006run.ts:1071)。
  • 输出护栏在 final 时跑:主循环 next_step_final_output 分支里 runOutputGuardrails(state, this.outputGuardrailDefs, currentStep.output)run.ts:1103)——拿到最终输出后才检查。
  • 工具级护栏tool.ts:2024-2025inputGuardrails/outputGuardrails 挂到工具上;执行时由 runToolInputGuardrails / runToolOutputGuardrailsutils/toolGuardrails.ts,从 index.ts:270 导出)跑。它们有更丰富的行为(不止 tripwire,还能 reject/替换输出),见 toolGuardrail.tsToolGuardrailBehavior

4. 巧妙之处:并行护栏 = 安全不付延迟税

输入护栏默认与模型调用并行(guardrail.ts:138 runInParallel = true),是个值得借鉴的设计:安全检查通常比模型快,让它和模型赛跑,命中才中止,既安全又不拖慢正常路径。要强同步(先过护栏再调模型)就把 runInParallelfalse

三、横向对比

  • handoff vs agent-as-tool 的取舍见 02-tools-and-agents-as-tools.md §4.2。
  • 同 shelf 的兄弟项目里,「换 agent / 子 agent」这件事各家做法不同(有的用图节点、有的用消息路由);这里的取舍是「用模型原生的 tool-call 协议表达交接」,好处是模型不需要学新概念。

4. 代码地图

主题文件符号
交接工厂handoff.tshandoffHandoffgetHandoff
交接触发/执行runner/turnResolution.ts · runner/toolExecution.tsexecuteHandoffCalls
交接 vs 函数区分runner/modelOutputs.tsresolveFunctionOrHandoffrecordHandoffRequest
输入/输出护栏guardrail.tsdefineInputGuardraildefineOutputGuardrailGuardrailFunctionOutput
护栏在循环里的收口run.ts · runner/guardrails.tsrunOutputGuardrailscreateGuardrailTracker
工具级护栏toolGuardrail.ts · utils/toolGuardrails.tsdefineToolInputGuardrailrunToolInputGuardrails