Appearance
Q39 · 工具调用失败时,Agent 应该怎么处理?
用户在客服中明确说:“订单 A123 的耳机有杂音,请帮我提交售后检测申请。”Agent 调用提交工具后,屏幕上出现“请求超时”。这时直接对用户说“提交失败”可能不对:服务端也许已创建申请,只是回包丢了。再发送一份新申请也可能不对:用户会收到两张售后工单。
处理工具失败,第一步是弄清失败发生在哪里、是否已经产生副作用、接下来怎样得到可信状态。对只读查询,有限重试通常较容易;对创建、退款、发消息等会改变外部世界的写操作,“超时”只能说明本端没有收到确定结果,不能等同于“服务端没有执行”。AWS 的幂等 API 设计文章专门用创建资源后响应丢失的情形解释这个难题。Q4:工具调用错误与重试已有基础总览;这里进一步把错误类型和写操作的结果核对讲完整。
先认清术语与例子中的名字
| 词或名字 | 直白解释 | A123 例子中的对应物 |
|---|---|---|
| Agent | 根据当前结果提出下一动作的应用;模型负责提议,程序负责执行和约束 | 提议提交检测申请、查看回执或停止 |
| 工具调用 | 应用把模型提出的工具名和参数交给真实服务执行,再把结果反馈 | 调用售后系统创建工单 |
| 只读操作 / 写操作 | 只读获取信息;写操作改变外部状态,叫作产生“副作用” | 查订单是只读;创建售后工单是写操作 |
| 超时 | 本端在设定时间内没有拿到完整答复 | 发送申请后 3 秒没有收到工单号 |
| 结果未知 | 本端不能确认服务端已完成、未完成,还是仍在处理 | A123 可能已生成 T900,也可能没有 |
| 幂等 | 在服务端承诺的范围和有效期内,同一操作重试不会重复产生副作用 | 同一个请求号 R7 最多对应一张工单 |
请求号 / request_id | 标识“这一次提交意图”的稳定编号;重试同一意图时保持不变 | R7 |
工单号 / ticket_id | 售后系统创建成功后给出的结果编号 | T900;它与请求号 R7 不是一回事 |
限流 / Retry-After | 服务要求调用方暂缓请求;后者是可能返回的建议等待时间 | 服务回复稍后再试,例如 60 秒 |
| 退避与抖动 | 逐渐拉长重试间隔,并加少量随机错开时间 | 不让许多 Agent 同时反复打售后服务 |
| 业务拒绝 | 服务可用,但根据业务规则拒绝这次动作 | A123 已有一张待处理检测单 |
A123 / u7 | 虚构订单号 / 已认证的登录用户 | 程序检查 u7 是否有权操作 A123 |
“停止”在本文指本次 Agent 执行不再发起危险调用,并给出明确状态或转人工,不是让整个客服系统停机。若依赖服务持续故障,平台还可以暂时关闭这一工具的自动提交入口,避免大量请求叠加;这是运维层面的额外保护。
固定一笔可核对的售后申请
以下订单、政策和服务协议都是虚构教学数据。假设今天是 2026 年 9 月 20 日,已认证用户 u7 有权查看并操作订单 A123;耳机于 2026 年 9 月 10 日签收,所以已签收 10 天;适用的 V2 政策允许“签收 30 天内出现非人为故障时提交检测申请”,最终是否退货或维修待检测。用户已明确要求提交检测申请,尚未授权直接退款。申请中把用户描述的故障写成“左耳有杂音”;它只是待核实描述,不等于系统已经认定非人为故障。
应用给本次提交意图生成请求号 R7。售后服务假设支持按 (已认证用户、R7) 去重,并提供“按请求号查工单”接口;同一个 R7 携带的订单号和故障描述必须保持相同,服务端记录请求号与创建结果的关系。下面是接口草图,不是可以直接运行的代码:
text
create_service_ticket(order_id, issue, request_id)
-> {status, ticket_id, error_code, retry_after_seconds}
get_ticket_by_request(request_id)
-> {status, ticket_id}create_service_ticket 是“创建售后检测工单”的业务动作;order_id=A123 指要处理的订单,issue=左耳有杂音 是用户描述,request_id=R7 是这次意图的去重标识。返回的 status 说明执行状态;ticket_id 只有核实创建后才可当作工单号;error_code 是机器可识别的失败类别;retry_after_seconds 是服务建议等待的秒数,可能为空。get_ticket_by_request 以 R7 查这次请求的实际处理结果。登录身份来自已认证会话,不由模型自由填写。实际服务是否提供这些能力必须核实;仅在提示词里写“不要重复创建”不能实现幂等。
程序要在调用前检查用户授权、工具参数和超时预算,在调用后把结构化结果反馈给 Agent。以 Claude 的客户端工具协议为例,官方文档允许把失败作为对应的 tool_result 返回,并用 is_error: true 标记;它建议给出可操作的错误信息。本文的接口草图不绑定 Claude,核心是:模型应收到“超时、结果未知、允许查 R7”,而不是只有一个模糊的“失败”;同时程序必须拦住不安全的动作,不能把重试权限完全交给模型。
正常成功与超时后的两条路线
正常成功。 Agent 提出创建 A123 的检测申请;程序复核 u7 的权限,带着 R7 调用 create_service_ticket。工具返回 status=created, ticket_id=T900。程序记录 R7 与 T900,Agent 可以告诉用户“检测申请已提交,工单 T900;处理结果待检测”,但不能说“已经退款”。这个输出有真实回执支持。
响应超时,但服务端其实已创建。 同样的创建请求已经到达售后服务并生成 T900,返回途中连接断开,本端只收到超时。工具适配层把它标为 status=unknown,不编造 created 或 failed。程序或 Agent 随后调用 get_ticket_by_request(R7),查到 T900,再按正常成功路线告知用户。此时无需第二次创建。AWS 的文章指出网络超时后调用方不知道操作是否成功,贸然重试可能创建重复资源,因此要依靠结果核对或服务端的幂等契约。

图只画写操作超时后结果未知这一分支。下方“仍未确认”并不等于已经证实“未创建”:查询也可能因延迟、数据尚未同步或服务故障而暂时查不到。若售后服务明确保证 R7 在有效期内幂等,且本次操作还在重试预算内,才可以带相同参数、相同 R7做有限重试;如果没有这种保证,就停下自动创建,记录待核对状态,交人工或后台对账。不能随手换一个新请求号 R8 重发同一意图,否则去重保护失效。
幂等还取决于服务端实现:它必须可靠地把请求号与写入结果绑定,规定键的有效期与同键不同参数的处理方式;客户端只生成 R7 并不足够。比如 Stripe 的官方幂等请求说明要求重试时复用同一个键,并会比较参数;它也说明键清理后复用可能被视为新请求。这是 Stripe 的具体契约,本文售后服务是否有同样保证,必须由自己的 API 文档和测试确认。AWS 的幂等设计文章进一步强调服务端记录去重标识与实际写入需要一致,避免“记了请求号却没建工单”或“建了工单却没记请求号”。
不同失败要给不同动作
下表中的 HTTP 状态码只是常见传输表现,真实工具也可能在成功的 HTTP 响应体里返回业务错误。先按业务含义分类,再决定 Agent 能做什么。
| 工具结果 | 本例可能发生什么 | 可以做的下一步 | 不能做什么 |
|---|---|---|---|
| 超时、连接中断、结果未知 | 创建 A123 申请的请求或回包在网络中断掉 | 先按 R7 核对;有服务端幂等保证时同号有限重试;仍未知则停止自动提交并转人工对账 | 直接宣称“失败”;改用新号盲重发 |
| 限流(常见为 429) | 售后服务暂时拒绝过密请求,可能附 Retry-After | 若明确未执行、可重试且等待时间在本次预算内,按建议时间等待;否则告知暂不能完成 | 立刻并发重试、无视服务等待时间 |
| 暂时不可用(常见为 503) | 售后服务故障或过载 | 对只读调用做有限退避重试;对写调用先确定结果与幂等条件,再按预算重试或暂停 | 把所有 503 都解释为“写入一定没发生” |
| 认证/权限失败(常见为 401/403) | 会话失效,或 u7 无权操作 A123 | 停止;让用户走正常登录或授权流程,必要时转人工 | 让模型换账号、换工具或绕过权限 |
| 参数缺失或格式不对 | 工具缺少订单号或故障描述格式不合约定 | 若能从可信上下文修正则校验后再调用;缺用户信息就询问 | 原样重复同一错误参数 |
| 业务拒绝 | 服务返回 OPEN_TICKET_EXISTS,已存在待处理工单 T850 | 核实 T850 属于 A123 和 u7,说明已有申请,按流程继续处理它 | 反复创建新工单,或把拒绝描述成网络故障 |
HTTP RFC 6585 的 429 定义允许服务在限流响应中给出 Retry-After;RFC 9110规定了 403、503 等状态的语义。状态码只提供线索:一个写请求在客户端看来超时,服务端是否已做完,仍需按服务契约核对。Retry-After 若建议等待 60 秒,而本次客服交互只允许再等 8 秒,就结束本轮并明确告知“暂未确认提交”,不能让 Agent 持续等待或假装成功。
退避也应有边界。假设本例最多允许一次原始创建、一次安全重试,总耗时不超过 8 秒;对可重试错误,程序选一个受上限约束的等待时间并加入抖动,避免许多会话同一秒再次冲击故障服务。若 Retry-After 已超过总预算,直接停止本轮或交后台任务处理;后台任务也必须保留相同 R7 和审计记录。AWS Well-Architected 的重试建议强调限制重试次数、使用退避与抖动,并理解 SDK 自带重试,避免 Agent、工具层、SDK 各自重试导致次数相乘。具体“8 秒、一次重试”只是教学配置,应由用户体验、服务能力与风险来定。
Agent、程序和人工各负责什么
Agent 负责提出下一步,但不批准危险动作。 它读到 status=unknown 后可以提出“按 R7 查询”;读到 OPEN_TICKET_EXISTS 后可以解释已有工单;读到缺少用户信息后可以提问。工具说明与系统规则要让它知道哪些错误允许改参数、哪些必须退出,但最终权限、重试预算、幂等键和写入确认由程序校验。模型若说“我再试十次”,程序仍应拒绝超出上限的调用。Anthropic 的工具结果文档说明了把执行错误作为工具结果回送给模型的机制;回送错误不等于把控制边界交给模型。
程序负责可验证状态。 对同一用户请求保存 R7、A123、参数摘要、已尝试次数、最近工具状态和查到的 T900/T850,并记录能关联售后服务的调用 ID。若调用超时且核对也失败,状态应写成“待核对”,而非“失败”;恢复后继续对账。用户再次问“刚才提交了吗”,应查这条记录,不重新生成一个提交意图。日志要避免无关个人信息扩散,同时保留排障必需的编号和时间。
人工处理无法安全自动判断的情形。 例如售后服务没有按请求号查询能力,也没有幂等保证;或查询与写入持续矛盾;或权限、政策资格有争议。此时停止写工具,向用户说明“申请状态暂时未确认,会人工核查”,并把请求号、订单号及工具轨迹交给授权工作人员。若售后服务整体持续 503,可暂时关闭自动提交入口或降级为人工受理,防止每个 Agent 都继续压服务。
怎样测试这套处理
用可控的假工具模拟六种结果:正常返回 T900、创建成功但回包丢失、未创建且超时、429 且等待超预算、403、已有工单 T850。尤其要让“创建成功但回包丢失”的用例验证最终只有一张工单,并验证用户看到的是查实的 T900;“查询暂时未命中”的用例要验证不会立刻换新请求号。再测同 R7 改了故障描述、幂等键过期、SDK 已自动重试等边界,确保本层不会误把相同意图执行多次。
除了最终答复,记录每轮工具名、参数摘要、请求号、错误类别、重试次数、等待时间、是否转人工和服务端最终工单数。只看“Agent 最后说了抱歉”无法发现它是否曾经悄悄建了两张单。测试应把用户看到的话和外部系统真实状态一起核对。
面试时怎样回答
可以这样说:“Agent 调工具失败后,我先把错误分成结果未知、可等待的瞬态错误、参数或权限错误、业务拒绝,再区分只读还是会产生副作用的写操作。只读超时可以有限重试;写操作超时不能直接判失败,更不能换请求号重发,要先用同一请求号查服务端状态。只有服务端有明确的幂等保证,才用相同参数和相同键做有限重试。429 看等待时间与总预算,403 停止,参数错误修正后再调用,业务拒绝按业务状态处理。错误信息结构化回送给模型,让它解释或提出补救,但权限、重试上限和写入确认由程序控制。状态仍未知时停下并转人工,绝不向用户谎称已提交或已失败。”
如果追问“有了幂等键就万无一失吗”,可以答:不行,还要看服务端是否原子地保存键与结果、键有效多久、同键不同参数怎样处理,以及多系统副作用是否都受保护。如果追问“模型能否自己决定重试”,可以答:它可以提议,但程序应依据错误类别、操作副作用、服务契约和预算做最终许可,避免模型进入失败重试循环。
资料
- Anthropic:Handle tool calls:工具请求、结果配对和错误回传。
- AWS Builders' Library:Making retries safe with idempotent APIs;AWS Well-Architected:Control and limit retry calls:超时后的不确定性、幂等与有限退避重试。
- Stripe:Idempotent requests:同键重试、参数一致性和键有效期的真实 API 契约示例。
- RFC 6585、RFC 9110:HTTP 限流、拒绝与暂时不可用状态的标准语义。