CAI Handoff Filters 实战指南:用输入过滤器与提示词前缀精确控制多智能体交接
【免费下载链接】caiCybersecurity AI (CAI), the framework for AI Security项目地址: https://gitcode.com/GitHub_Trending/cai3/cai
CAI(Cybersecurity AI)在cai.sdk.agents.extensions模块中提供了开箱即用的 handoff 输入过滤器(handoff_filters)与推荐提示词前缀(handoff_prompt),用于在多智能体交接时裁剪对话历史、隔离工具调用噪声。本文以 handoff_filters.py 与 handoff_prompt.py 为主线,结合 handoffs.py、单元测试 test_extension_filters.py 与完整示例 message_filter.py 展开,读完你将掌握过滤器的工作原理、自定义过滤器的写法以及推荐的交接提示词规范。
一、为什么需要 Handoff Filters
在基于 Agent + Handoff 的多智能体系统中,交接(handoff)意味着当前 Agent 把对话控制权转交给另一个 Agent。默认情况下,下一个 Agent 会看到完整的对话历史——包括:
- 触发交接的输入项(触发 handoff 的那条 tool call);
- 交接工具的输出项(表示 handoff 结果的 tool output 消息);
- 此前 Agent 执行过的所有工具调用、工具输出、计算机操作等中间过程。
这在很多场景下是多余的,甚至有害:
- 上下文污染:下一 Agent 看到大量工具调用细节,与它的任务无关,浪费 token 且干扰判断;
- 信息泄漏:交接给下游 Agent 时,不需要把上游所有内部工具行为带过去;
- 历史截断:多轮长会话中,可能需要丢弃较早的消息,只保留近期上下文。
HandoffInputFilter正是为此设计的机制:它是一个接收HandoffInputData、返回HandoffInputData的函数,在交接发生时对传给下一 Agent 的输入做任意裁剪或改写。从 handoffs.py 的类型定义可以看到其签名:
HandoffInputFilter: TypeAlias = Callable[[HandoffInputData], HandoffInputData]二、HandoffInputData:过滤器收到的数据结构
要写过滤器,先要理解它操作的数据。HandoffInputData是定义在 handoffs.py 中的冻结数据类(frozen dataclass),包含三个字段:
| 字段 | 类型 | 含义 |
|---|---|---|
input_history | str \| tuple[TResponseInputItem, ...] | Runner.run()被调用之前的输入历史 |
pre_handoff_items | tuple[RunItem, ...] | 在触发交接的那一轮 Agent 运行之前生成的所有运行项(RunItem) |
new_items | tuple[RunItem, ...] | 当前这一轮 Agent 运行期间新生成的运行项,包括触发交接的输入项,以及代表交接输出响应的 tool output 消息 |
关键点:
input_history可能是字符串(当直接以字符串作为初始输入时),也可能是TResponseInputItem(即 OpenAI Responses API 的输入项字典)元组,写过滤器时要做类型判断;RunItem是运行期产物对象,包括MessageOutputItem、HandoffCallItem、HandoffOutputItem、ToolCallItem、ToolCallOutputItem等,它们定义在 items.py;- 过滤器返回的新
HandoffInputData将直接决定下一 Agent 看到的输入——注释中明确说明:The next agent that runs will receive handoff_input_data.all_items(由三个字段组合而成)。
三、内置过滤器 remove_all_tools 源码解析
handoff_filters模块目前提供一个开箱即用的过滤器remove_all_tools(handoff_filters.py),作用是一键移除所有工具相关的条目:file search、web search 以及函数调用与函数输出。
def remove_all_tools(handoff_input_data: HandoffInputData) -> HandoffInputData: """Filters out all tool items: file search, web search and function calls+output.""" history = handoff_input_data.input_history new_items = handoff_input_data.new_items filtered_history = ( _remove_tool_types_from_input(history) if isinstance(history, tuple) else history ) filtered_pre_handoff_items = _remove_tools_from_items(handoff_input_data.pre_handoff_items) filtered_new_items = _remove_tools_from_items(new_items) return HandoffInputData( input_history=filtered_history, pre_handoff_items=filtered_pre_handoff_items, new_items=filtered_new_items, )其内部由两个私有辅助函数分工:
1._remove_tools_from_items(针对RunItem元组)(handoff_filters.py):遍历pre_handoff_items与new_items,凡是以下四种运行项一律跳过:
HandoffCallItem(交接调用)HandoffOutputItem(交接输出)ToolCallItem(工具调用)ToolCallOutputItem(工具输出)
只保留MessageOutputItem等纯消息项。
2._remove_tool_types_from_input(针对TResponseInputItem元组)(handoff_filters.py):检查每条输入项的type字段,命中以下工具类型即剔除:
function_call function_call_output computer_call computer_call_output file_search_call web_search_call由此可见,remove_all_tools从历史输入、交接前运行项、当轮新运行项三个层面同时做了清理,保证交接后到达下一 Agent 的只剩纯对话消息(user/assistant 文本),工具痕迹被彻底剥离。
行为边界:从测试看过滤语义
remove_all_tools的行为在 test_extension_filters.py 中有详尽验证,几个关键用例说明了它的边界:
test_empty_data/test_str_historyonly:输入为空或input_history为字符串时,原样返回(字符串历史不会被过滤);test_removes_tools_from_history:input_history中混入function_call_output类型条目时,仅该条目被移除,普通消息保留(断言历史从 3 条减为 2 条);test_removes_tools_from_new_items:new_items中ToolCallOutputItem被移除,MessageOutputItem保留;test_removes_handoffs_from_history:HandoffCallItem、HandoffOutputItem、ToolCallOutputItem在历史与运行项中都会被清除。
这些测试同时证明了过滤是非破坏性的:没有工具项的数据会保持原样,纯消息流不受影响。
四、编写自定义过滤器:以 message_filter 为例
内置的remove_all_tools只解决"去工具化"这一种需求。实际场景往往还需要截断历史、改写消息、注入上下文等,此时应编写自己的HandoffInputFilter。仓库在 message_filter.py 给出了标准范式:
def spanish_handoff_message_filter(handoff_message_data: HandoffInputData) -> HandoffInputData: # 第一步:复用内置过滤器,先移除所有工具相关消息 handoff_message_data = handoff_filters.remove_all_tools(handoff_message_data) # 第二步:从历史中再删掉前两条(仅作演示) history = ( tuple(handoff_message_data.input_history[2:]) if isinstance(handoff_message_data.input_history, tuple) else handoff_message_data.input_history ) return HandoffInputData( input_history=history, pre_handoff_items=tuple(handoff_message_data.pre_handoff_items), new_items=tuple(handoff_message_data.new_items), )自定义过滤器的要点:
- 先复用内置过滤器:组合式写法(先
remove_all_tools,再做自定义裁剪)是官方推荐模式; - 注意
input_history可能是字符串:做切片前必须isinstance(..., tuple)判断,否则对字符串切片会得到错误的字符级结果; - 返回新的
HandoffInputData:过滤器是纯函数式的,必须显式构造并返回新对象,而不是修改原对象(HandoffInputData本身也是 frozen dataclass)。
将该过滤器挂接到交接上,只需在构造handoff时传入input_filter参数(message_filter.py):
second_agent = Agent( name="Assistant", instructions=( "Be a helpful assistant. If the user speaks Spanish, handoff to the Spanish assistant." ), handoffs=[handoff(spanish_agent, input_filter=spanish_handoff_message_filter)], )运行python examples/handoffs/message_filter.py后可以看到:交接给西班牙语助手时,最终消息列表里不再包含任何 tool call,且前两条历史消息被裁掉,只有用户问题与纯文本回复。流式版本见 message_filter_streaming.py,二者逻辑一致。
关于流式模式的一个重要约束
在Handoff.input_filter的文档注释中(handoffs.py)明确说明:流式模式下,过滤器执行期间不会产生任何新的流式输出——过滤器之前已经生成并流式传输给用户的内容不会回退或重放。这意味着过滤器只影响"传给下一 Agent 的上下文",不影响用户已经看到的内容,设计时无需担心过滤导致输出重复。
五、handoff_prompt:推荐交接提示词前缀
除了输入过滤,handoff_prompt模块提供两个与交接配套的提示词工具(handoff_prompt.py):
1.RECOMMENDED_PROMPT_PREFIX(推荐提示词前缀):一段固定的系统上下文文本,向模型说明它处于一个基于 Agents SDK 的多智能体系统中,包含两种核心抽象:Agents与Handoffs。其关键指令是:
- Agent 封装了指令与工具,在合适时可以把对话交接给另一个 Agent;
- 交接通过调用一个通常命名为
transfer_to_<agent_name>的交接函数实现; - 后台自动完成的 Agent 间转移是无缝的,不要在对话中向用户提及或强调这些转移。
这一点对用户体验至关重要——模型若在回答里说出"我现在把你转给另一个助手"之类的元话术,会破坏对话的自然感。
2.prompt_with_handoff_instructions(prompt):一个便捷封装函数,把推荐前缀追加到任意自定义提示词前面:
def prompt_with_handoff_instructions(prompt: str) -> str: """ Add recommended instructions to the prompt for agents that use handoffs. """ return f"{RECOMMENDED_PROMPT_PREFIX}\n\n{prompt}"因此,任何使用了 handoffs 的 Agent,建议要么直接在自己的instructions中内嵌RECOMMENDED_PROMPT_PREFIX,要么用prompt_with_handoff_instructions(...)包裹,以保证模型理解交接语义并避免向用户暴露内部转移行为。
六、接入流程总结:过滤器的完整生命周期
把上述内容串起来,一个带输入过滤的交接完整工作流是:
- 构造目标 Agent(如
spanish_agent),定义其instructions与handoff_description; - 编写过滤函数:接收
HandoffInputData,内部复用handoff_filters.remove_all_tools并叠加自定义裁剪,返回新的HandoffInputData; - 通过
handoff(spanish_agent, input_filter=my_filter)创建带过滤器的交接对象; - 把该
Handoff放入源 Agent 的handoffs列表; - 运行时,当模型调用交接工具(
transfer_to_<agent_name>),SDK 在_invoke_handoff内部完成交接(handoffs.py),随后过滤器对历史、交接前项与当轮新项执行裁剪,再启动下一 Agent。
从 handoffs.py 的Handoff数据类定义看,除了input_filter,交接还支持tool_name_override(工具名覆盖,默认由default_tool_name按transfer_to_{agent.name}生成)、tool_description_override、on_handoff(交接触发时的回调)、input_type(交接输入的结构化校验,默认开启 strict JSON schema,见 strict_schema.py)等能力,过滤器只是整个交接控制面中的一个维度。
七、最佳实践与注意事项
- 优先组合而非重写:多数过滤需求 =
remove_all_tools+ 少量定制。先调用内置过滤器,再叠加自己的逻辑,既省代码又降低遗漏工具类型的风险。 - 字符串历史防御:
input_history可能是str,所有切片、索引操作前先做isinstance判断。 - 流式场景预期管理:流式模式下过滤器不会产生新的流式事件,已输出内容不受影响,不要依赖过滤器去"修正"已经流式输出的文本。
- 提示词配套:只要 Agent 使用了 handoffs,就应把
RECOMMENDED_PROMPT_PREFIX(或prompt_with_handoff_instructions的产物)放入instructions,否则模型可能不了解交接语义、或在回复中暴露内部转移过程。 - 用测试验证过滤语义:参考 test_extension_filters.py 的用例设计,为自定义过滤器补充"历史含工具、新项含工具、纯消息流"三类用例,确保边界行为可预期。
八、延伸阅读
- Handoff 核心数据结构与 API:
HandoffInputData、Handoff、handoff()工厂函数 - 运行项类型定义:
HandoffCallItem、HandoffOutputItem、ToolCallItem等被过滤的目标类型 - 过滤器单元测试:
remove_all_tools全部行为用例 - 非流式完整示例 与 流式完整示例
- Handoff 相关文档 与 CAI 多智能体文档
【免费下载链接】caiCybersecurity AI (CAI), the framework for AI Security项目地址: https://gitcode.com/GitHub_Trending/cai3/cai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考