执行循环与调用关联

本节目标

  • 建立供应商无关的工具调用状态机;
  • 正确关联每个 call 与 result;
  • 区分完成、需工具、需审批、拒绝、截断和故障;
  • 用预算、重复检测和进度条件阻止无限循环。

最小五步循环

OpenAI 当前官方文档把客户端 function calling 概括为五步:

  1. 请求模型并提供工具;
  2. 接收模型的 tool call;
  3. 应用侧执行代码;
  4. 携带对应 tool output 再请求模型;
  5. 接收最终回答或更多 tool calls。

Anthropic 客户端工具同样需要应用驱动循环,只是消息块和 stop reason 不同。Gemini 也明确模型不会替应用执行函数。

用内部状态机隔离供应商差异

不要在业务代码到处判断 function_call 或 tool_use。Adapter 把它们归一为有限状态:

MODEL_REQUESTED
  ├─ FINAL_TEXT ─────────────→ COMPLETED
  ├─ TOOL_PROPOSALS ─────────→ VALIDATING
  │     ├─ approval needed ──→ WAITING_APPROVAL
  │     ├─ executable ───────→ EXECUTING → RESULTS_READY
  │     └─ invalid/denied ───→ RESULTS_READY(error envelope)
  ├─ REFUSAL ────────────────→ REFUSED
  ├─ TRUNCATED ──────────────→ INCOMPLETE
  └─ API_FAILURE ────────────→ FAILED/RETRY

RESULTS_READY 再送回模型,可能回到 TOOL_PROPOSALS。只有明确完成原因才是成功;“没有 tool call”可能是拒绝、max tokens 或服务错误。

三类 ID

ID范围作用
Provider response/item ID供应商响应排障与继续 API 状态
call ID一次模型提出的调用将 tool result 对回正确 call
operation ID整个业务任务跨模型回合、服务和审批追踪
idempotency key业务意图防止重试产生重复副作用
approval ID人类确认记录审计谁批准了哪项动作

Call ID 不能代替幂等键:模型重试时可能产生新 call ID,却仍是同一次退款意图。

关联 ID 仍不等于结果绑定

只把 call_id 复制到结果上,无法阻止合法形状的 A 结果被换到 B。项目在受保护审计中同时绑定:

  • provider + API family + response ID + call ID;
  • operation ID、idempotency key 与 adapter revision;
  • 当前 tenant/subject 的引用;
  • 输入、输出、效果、handler、producer 与 policy revision;
  • 完整 request_sha256、result_sha256 与 call_binding_sha256。

回传前用当前受信任上下文重算,而不是只检查字段存在或摘要长度。多调用集合按 (response_id, call_id) 建表,拒绝缺失、重复和未知结果;数组顺序可以改变,但关联身份不能交换。详见结果、错误与不可信数据。

Provider Adapter 的职责

Adapter 应:

  • 解析零、一个或多个 call;
  • 保留 call ID、name 与原始完成原因;
  • 严格解析 arguments;
  • 把内部 result 变成供应商要求的回传形状;
  • 处理流式参数的完整性与 done 事件;
  • 携带供应商要求延续的对话/推理项;
  • 把 API 错误映射到内部有限状态;
  • 记录 provider、API、SDK、model 与 adapter revision。

本库离线 adapter 当前覆盖 OpenAI Responses function_call_output、Anthropic Messages tool_result 与 Gemini Interactions function_result。它们先验证内部双投影,只把 model_result 放入供应商 payload;protected_audit 留在模型外。Anthropic 的 result blocks 需位于普通文本之前,Gemini Interactions 当前使用 result 字段而不是把三家统一伪造成同一外部消息结构。

Adapter 不应:

  • 决定用户是否有权退款;
  • 自动修正金额/路径并执行;
  • 直接调用任意模型给出的函数名;
  • 把供应商错误文本作为业务状态。

Registry 与 Dispatcher

registry = { # host 维护固定工具注册表,而不是按模型输出动态导入函数
    "get_order": ToolSpec( # 为唯一允许的读取工具建立显式合同
        schema_version="get-order-v1", # 参数/结果解析必须匹配这一版本
        risk="read", # 只读风险分类,仍可受 tenant 与数据策略限制
        timeout_ms=500, # 为外部调用设置有限超时,避免 loop 无限等待
        handler=get_order, # 绑定经过审阅的本地 handler 函数对象
    ) # 结束 get_order 工具规格
} # 结束 allowlist 注册表

Dispatcher 从 registry 获取 schema、风险、超时、授权策略和 handler。未知名称拒绝;禁止:

# 危险示意:不要这样做 # 模型文本绝不能决定要从全局命名空间取哪个可执行对象
handler = globals()[model_name] # 不安全:任意全局名称都可能被模型选择,绕过 allowlist 与风险分类
handler(**model_arguments) # 不安全:未做 schema、授权、预算、审批或目标范围校验就产生副作用

显式 registry 使能力暴露面可审计、可测试、可按用户/任务裁剪。

每轮算法

deadline / budgets 初始化
while 未终止:
    调模型
    解析完成原因与 calls
    若 final → 校验并结束
    若 refusal/truncated/error → 对应状态结束或受控恢复
    对 calls:
        去重 call ID
        schema + auth + policy
        若需要审批 → 持久化 pause state
        否则执行并产生严格 result
    关联所有结果并回传
    更新进度、成本与重复轨迹

需要审批时不要占用进程等待。保存 operation、conversation continuation、call、参数摘要、策略版本与 expiry;用户批准后恢复并重新校验。

循环保护

至少设置:

  • 最大模型回合;
  • 最大工具调用数;
  • 每工具与总 deadline;
  • token/费用预算;
  • 最大并行度;
  • 重复 tool + canonical arguments 检测;
  • 无进度计数;
  • 结果大小上限;
  • 人工升级/安全停止路径。

若模型连续调用相同工具与参数却没有新信息,返回结构化 NO_PROGRESS 或转人工,而不是不断把相同结果喂回去。

流式调用

流式 API 可能分片发送工具参数。只能在供应商明确的完成事件后解析并执行:

  • 片段按 item/output index 关联;
  • 限制累计字节;
  • JSON 完整前不运行 handler;
  • 流中断视为 incomplete,不“补一个括号”后执行;
  • 最终解析仍走 schema、授权和审批。

进度 UI 可以展示“模型正在准备调用 X”,但不能显示未完成参数为已执行。

动手实践

画出“查订单 → 创建退款草稿 → 等待批准 → 提交退款 → 最终说明”的状态机,标出:

  1. call ID、operation ID、approval ID、idempotency key;
  2. API max tokens;
  3. 未知工具;
  4. 审批过期;
  5. 提交超时但下游可能已成功;
  6. 模型再次提出完全相同调用。

为每个分支写终止/恢复状态,不能只写“报错”。

常见错误

  • stop_reason != tool_use 就当成功;
  • 多个 call 共用一个无标识结果字符串;
  • 只保存 call ID,不保存业务 operation;
  • 流式 arguments 未完成就执行;
  • 审批等待只存在进程内存;
  • 无限循环没有 deadline/进度条件;
  • Provider message 形状直接渗入 handler。

自测

  1. 为什么没有 tool call 不一定是完成?
  2. Call ID 与 idempotency key 各解决什么?
  3. Adapter 与 dispatcher 的边界在哪里?
  4. 审批暂停后恢复需要保存哪些状态?
  5. 流式参数为什么必须等 done 后再执行?

下一步:多调用、并行与依赖。

参考资料

来源获取日期:2026-07-19。Provider 消息字段与 continuation 规则属于动态适配内容。