优雅的工具调用失败:你的 Agent UI 缺失的错误契约
你见过的每一个 Agent 演示都以干净的结果收尾。工具调用返回了模型预期的数据,响应在两秒内到达,最终答案清晰准确。那是演示。生产环境则是另一回事。
在生产环境中,工具会超时。API 会返回 403,因为某个服务账户上周二被轮换了。第三方数据丰富端点返回 200,但响应体写着 {"status": "degraded", "data": null}。OAuth 令牌在周六凌晨 3 点过期。这些不是边缘案例——这是任何与真实世界交互的 Agent 的正常运行状态。失败模式是可预见的。问题在于,大多数 Agent 架构将它们视为事后补救,而大多数 Agent UI 根本没有向用户传达这些失败的词汇。
缺失的错误契约
当你构建 REST API 时,你会考虑完整的响应范围:200、400、401、403、404、429、500、503。每个状态码都承载着语义契约。调用方知道如何处理每一种情况。整个 Web 都建立在这套共享词汇之上。
Agent 工具调用没有等效的约定。大多数框架中的工具要么返回成功值,要么抛出异常。如果框架将异常转换为观察字符串,LLM 可以看到该异常消息——但前提是有人记得配置了 handle_tool_error。在 Agent 要么恢复要么放弃之前,用户什么也看不到。没有状态分类,没有重试语义,没有对用户可见的进度状态。
这种缺失的契约正是工具调用失败感觉比 API 失败糟糕得多的原因。当网络请求失败时,浏览器会显示合理的错误页面。当 Agent 工具调用在推理过程中失败时,Agent 要么无声地停滞,要么幻觉出一个恢复方案,要么给出一条令人困惑的最终消息,没有任何关于出了什么问题或用户下一步可以做什么的上下文。
解决方案不纯粹是技术问题——它是一个横跨工具实现、Agent 运行时和用户界面的设计问题。
处理前先分类
第一个架构决策是大多数团队跳过的:在决定如何响应之前对错误进行分类。
并非所有工具失败都是平等的。两大类别至关重要:
瞬时失败是那些可能通过重试解决的:速率限制(429)、网络超时、临时服务降级、上游 API 过载。这些值得用指数退避重试——通常在放弃前延迟 1 秒、2 秒和 4 秒进行三次尝试。对瞬时失败重试超过三次通常是浪费;如果三次尝试后仍未解决,接下来三十秒内可能也不会解决。
永久性失败是那些重试无法修复的:无效凭证(401)、权限拒绝(403)、格式错误输入导致的验证错误(400)以及超出上下文限制。重试这些会消耗令牌和时间,同时让用户等待一个无法改变的结果。正确的响应是快速失败,给出清晰的解释,然后要么回退到替代方案,要么升级给用户。
工具应在返回值中明确编码这种分类。携带 error_type 字段的错误响应——例如 RateLimitError、PermissionDeniedError、ValidationError、ServiceUnavailableError——为 Agent 运行时提供了智能路由失败所需的信息,而不是应用一刀切的重试策略。
LLM 也能从这种结构中受益。像"日历 API 返回 403:Agent 的服务账户没有对此日历的写入权限"这样的消息给了模型足够的上下文来决定是向用户寻求澄清、尝试不同方法,还是诚实地报告问题。原始的 Python 回溯什么可操作的信息都没有传达。
系统性失败的熔断器
单独的重试逻辑是不够的。当上游提供商经历中断时,每个请求都会失败。没有熔断器,每个触及该提供商的 Agent 会话都会排队并依次超时,消耗延迟预算,让用户等待不会到来的结果。
熔断器模式——直接借鉴自分布式系统——在滚动窗口内跟踪失败率。当失败超过阈值时,熔断器打开并立即开始阻止新请求,而不是让它们尝试并失败。在配置的冷却期后,熔断器进入半开状态,用单个探测请求进行测试。如果成功则关闭;否则保持打开。
对于 Agent 工具,这意味着下游服务的失败应在毫秒内呈现给用户("搜索工具当前不可用"),而不是在 30 秒的级联超时之后。用户接受"这个工具现在宕机 了"比看着加载转圈半分钟后得到一个令人困惑的错误要容易得多。
每个工具需要跟踪的关键数据:失败计数、最后失败时间戳、熔断器状态和恢复探测结果。大多数 Agent 框架不提供内置的熔断器支持——这是你在工具包装层添加的插桩。
部分结果优于没有结果
Agent 设计中最有价值的 UX 决策之一是选择在无法返回所有内容时返回什么。
考虑一个调用四个工具的研究 Agent:网络搜索、文档检索系统、结构化数据库查询和引用格式化器。如果引用格式化器失败,Agent 拥有它所需信息的四分之三。正确的响应不是丢弃部分结果并报告完全失败——而是返回可用的内容并明确说明缺少什么。
这需要将工具设计为考虑部分成功。一个可以检索当前状况但无法检索预报数据的天气工具应返回 { "current": {...}, "forecast": null, "forecast_error": "upstream timeout" },而不是抛出异常。一个找到五个请求文档中三个的文档检索工具应返回找到的内容并注明缺少的两个。
然后 Agent 可以明确地推理部分结果:"我找到了三个相关文档,但由于权限错误无法检索两个。这是我从可用来源了解到的,以及哪些内容是不确定的。"这比无声失败要好得多——更糟糕的是 Agent 捏造缺失的部分。
这里的设计原则是,工具应该像精心设计成功路径响应一样精心设计降级响应。
