工具调用超时以后,运行器需要保留一种状态,结果未知。请求可能没有执行,也可能已经执行成功,只是响应没有回来。这个区别会直接影响下一次调用能不能发出去。
我在 sunday面试指南整理这道题时,把查询和写入分开讨论。AWS Builders' Library 对安全重试的说明也强调了调用方请求标识与业务意图的关系。下面用一个假设的工单系统,把设计过程走一遍。
先沿着一次超时找副作用
用户要求为设备故障创建工单。应用发出请求,下游完成写入,响应途中连接断开。调用方等到超时,只知道没有拿到确定的结果。
如果此时反馈给模型“创建失败,请重试”,模型可能再次创建,用户的一次需求就对应了两张工单。运行器要让调用结果能够表达不确定性,模型的下一步才有可靠依据。
查询工单状态的工具,如果确认没有写入副作用,可以在次数和总时长预算内重试。创建工单的工具则先检查接口有没有幂等能力,或者能否凭操作标识核对状态。工具名称含有 query,也不能替代对实现语义的检查。
幂等键跟着同一个业务动作走
应用可以在首次执行前,为这一次已经确认的创建意图分配 operationId,并保存起来。假设下游明确支持 idempotencyKey,第一次请求和后续安全重试使用同一个值。
{
"operationId": "ticket-intent-42",
"idempotencyKey": "ticket-intent-42",
"action": "create_ticket",
"arguments": {
"deviceId": "device-8",
"summary": "设备无法连接"
}
}
这是应用与工具之间的教学数据结构,需要按真实接口调整。用户明确提出第二个新的创建动作时,要生成新的标识。不能仅凭参数长得一样,就把两次合理的业务需求合并。
下游也必须真正实现去重。只在调用方生成一个随机 ID,却没有服务端处理规则,重复调用仍然可能重复写入。幂等记录与业务写入还要协调,避免工单已经生成,去重记录却没有保存。
接口约定需要说明键的作用域和有效期。同一个键带着不同参数再次提交时,应按契约拒绝或处理冲突。超过服务端的保留期限,也不能继续假设这个键仍然保证去重。
让未知状态限制下一步动作
超时后,运行器可以返回下面这样的结果。
{
"operationId": "ticket-intent-42",
"status": "unknown",
"allowedNextActions": ["query_operation_status", "request_review"],
"reason": "下游响应超时,尚未确认写入结果"
}
具体状态名称可以自行设计,关键是区分已确认成功、已确认失败和结果未知。这段数据只是协议示例,真正禁止重复创建的约束还要落实在执行器里。模型选择了未获允许的动作,工具执行层仍应拦住。
如果下游支持幂等,运行器可以按照接口约定复用原键重试。能够查状态时,先检查同一操作的结果。查不到结果也有不同含义,可能仍在处理,可能查询数据还没有同步。需要达到接口定义的确定条件,才能认定没有执行。
下游既不支持幂等,又无法核对状态,就停在待核对。继续查询也要有限制,避免把 Agent 运行变成一串没有结束条件的网络请求。
用丢失响应的测试检查一次
测试环境里可以故意让下游完成创建,再让调用方收不到响应。后续重试后,检查实际工单数量与操作记录。这能验证最容易重复写入的情况。
另外检查两个新的创建意图是否会得到不同工单,以及同一意图多次提交是否只产生一个结果。最后让状态查询一直超时,确认系统会在预算用尽后退出,并保留待核对状态。
测试要核对服务端的实际记录。客户端只拿到一次成功响应,仍不足以证明下游只写入一次。幂等保留期限之外怎样恢复,也需要单独检查,不能把有限范围的去重概括成所有情况下都能恰好执行一次。
我是程序员Sunday。完整原理和面试追问见 sunday面试指南的工具超时与幂等解析。