同样名为“工具调用”的 ResponseItem 可能由客户端执行、由服务端托管执行,或只是展示服务端已经完成的动作。ToolRouter 不是看到 *Call 就一律本地 dispatch,而是按 Item 类型与 execution 字段分流。
具体问题与启用条件
本节解释模型响应中的工具事件类型和执行归属。具体每种工具如何执行、审批和回灌留到第六章。
| 条件来源 | 决定字段或状态 | 对本机制的影响 |
|---|---|---|
| ToolSearchCall | execution=client 且有 call_id | 构造成客户端 tool_search 调用 |
| ToolSearchCall | 其他 execution | 作为非本地调用,不 dispatch |
| CustomToolCall | name/namespace/call_id/input | 构造 Custom payload 并本地路由 |
| Hosted item | WebSearchCall/ImageGenerationCall | 服务端执行结果进入事件/历史,不本地调用 |
协议、类型与状态所有权
| 状态或协议 | 所有者 | 生命周期 | 关键不变量 |
|---|---|---|---|
| ResponseItem 类型 | 模型/Responses 服务 | 完成 Item | 声明调用种类与 execution |
| ToolCall payload | ToolRouter | 本地执行期间 | 仅为可客户端执行类型创建 |
| Hosted Tool 状态 | 服务端 | 响应内 | Core 不伪造本地 ToolOutput |
| TurnItem projection | stream_events_utils | 客户端事件 | 仅部分类型可展示 |
机制调用链如下:
OutputItemDone(ResponseItem)
→ ToolRouter.build_tool_call
→ FunctionCall → Function payload
→ CustomToolCall → Custom payload
→ ToolSearchCall(execution=client) → ToolSearch payload
→ ToolSearch hosted / WebSearch / ImageGeneration → 不本地 dispatch
→ 记录完整 Item/展示相应 TurnItem
→ 本地调用才产生 Tool Result 与 follow-up
机制怎样工作
FunctionCall 和 CustomToolCall 都可形成本地 ToolCall,但 payload 不同:Function 保留 JSON 字符串,Custom 保留 freeform input。ToolSearchCall 只有同时具备 call_id 且 execution == "client" 时才解析 SearchToolCallParams 并路由到名为 tool_search 的工具;参数解析失败返回 RespondToModel。其他 ToolSearchCall 由服务端拥有执行,不创建本地 future。
WebSearchCall 是 Hosted Tool 的典型例子:它可被投影为 TurnItem、发 Started/Completed 并记录 History,但 ToolRouter 不为其创建本地执行。ImageGenerationCall 同样是服务端返回的完整项,当前通用非工具投影路径并不把所有变体都做成客户端 TurnItem;仍可作为 ResponseItem 记录。服务端意外返回 Function/Custom/ToolSearch Output 时,Core 记录诊断并不把它再当模型发起的工具。
Python 风格伪代码
这段伪代码保留生产实现中会改变结果的状态、分支和异步边界;认证 SDK、遥测字段和 Rust 所有权样板被折叠为明确的领域对象。
def classify_response_tool(item: ResponseItem) -> LocalToolCall | HostedItem | None:
match item:
case FunctionCall(name, namespace, arguments, call_id):
return LocalToolCall(qname(namespace, name), call_id,
FunctionPayload(arguments))
case CustomToolCall(name, namespace, input, call_id):
return LocalToolCall(qname(namespace, name), call_id,
CustomPayload(input))
case ToolSearchCall(call_id=str() as cid, execution="client", arguments=args):
return LocalToolCall("tool_search", cid,
ToolSearchPayload(parse_search_args(args)))
case ToolSearchCall() | WebSearchCall() | ImageGenerationCall():
return HostedItem(item)
case FunctionCallOutput() | CustomToolCallOutput() | ToolSearchOutput():
return None # response stream 中的意外 output,不再次执行
case _:
return None
失败、取消与恢复
| 故障或边界 | 已发生的状态 | 对上层的结果 | 能否直接重试 | 恢复动作 |
|---|---|---|---|---|
| client ToolSearch 缺 call_id | 无法配对结果 | 不创建本地 ToolCall | 不适用 | 作为非执行项处理 |
| ToolSearch 参数解析失败 | 尚未执行 | RespondToModel | 模型可修正 | 记录调用与错误 output |
| 未知本地 Custom Tool | 完整调用已记录 | Handler/Router 错误 | 模型可见失败 | 下一采样修正 |
| Hosted Tool 误当本地 | 会重复外部副作用 | 当前类型分支阻止 | 否 | 由服务端状态为准 |
设计取舍与验证
显式类型与 execution 分支防止重复执行 Hosted Tool,也允许同一种 ToolSearch 协议选择客户端或服务端执行。代价是新增工具 Item 必须同时决定 Router、TurnItem 投影、History 和事件行为,不能只加一个 enum variant 就算完成。
| 可验证契约 | 证据方式 | 预期结果 |
|---|---|---|
| execution=client 的 ToolSearch 进入本地 Registry | Search Tool 集成测试 | 产生结果并触发下一采样 |
| Hosted WebSearch 只展示/记录 | web_search fixture | 无本地 ToolOutput 调用 |
| Custom Tool 使用 freeform input | tool harness 请求捕获 | Handler 收到原始字符串 |
Mini Codex 对照
Mini Codex 应在工具调用领域类型中加入 execution_owner: client|provider,并让 Router 只接受 client。若暂不支持 Hosted Tool,应保留其历史项但不假装本地执行。
本节边界
本节只决定“谁执行”。第六章将对每个 client-owned 工具追到验证、审批、副作用和结果编码。5.19 先把所有完成 Item 的历史提交边界闭合。
评论
登录后即可评论