宇树科技一面问“Agent 调用工具失败如何处理”,表面看是一个异常处理问题,实际上考察的是你对 Agent 工程化全链路的理解:工具调用链路怎么设计、失败信息如何回传、模型如何自纠错、系统如何兜底、物理世界场景下如何保证安全。这篇文章把这个问题拆成一个完整的系统设计题来讲,从失败分类、根因分析、代码实现到面试回答框架一次说清。
1. 先搞清楚:什么是 Agent 调用工具失败
要处理一个故障,先定义故障。Agent 调用工具失败并不是某一种特定报错,而是从“模型生成工具调用意图”到“工具执行完成并返回结果”这个完整链路里任意一环出错的总称。
一条典型的工具调用链路是:
- 用户输入任务,Agent 将任务拆解成步骤。
- 模型根据工具描述(Tool Schema)决定调用哪个工具,并生成 JSON 格式的参数。
- Agent 运行时解析 JSON,校验参数。
- 代码执行工具函数,可能访问外部 API、数据库、文件系统或硬件设备。
- 工具返回结果,Agent 将结果作为上下文继续推理。
- 如果链路中某一步失败,Agent 需要拿到失败信息并决定下一步动作。
面试官问“失败如何处理”,真正的潜台词是:你是否能设计一套让 Agent 在失败后仍然可以继续前进的机制,而不是直接崩溃或无限重试。
从材料来看,这个题目放在宇树科技的面试里,还有一层隐含背景:机器人场景下的工具调用往往连接物理设备,工具失败不只是“接口报 500”,还可能是“机械臂没抓稳”“传感器读数异常”“执行器超时”,这类失败在软件系统的基础上叠了一层物理世界的不确定性。
2. Agent 工具调用失败的常见类型与根因
设计处理策略之前,先对失败分类。我把常见失败分成六类,覆盖绝大多数实际场景:
| 失败类型 | 典型表现 | 典型根因 |
|---|---|---|
| 工具不存在或名称不匹配 | 模型生成了get_weather,但系统里注册的是get_weather_info | 工具描述更新后提示词未同步;模型幻觉 |
| 参数校验失败 | JSON 缺少必填字段、字段类型错误、枚举值非法 | 工具 Schema 描述不清晰;模型对参数约束理解不到位 |
| 外部服务异常 | 第三方 API 超时、返回 5xx、网络抖动 | 被调服务不稳定;超时设置不合理;依赖的中间件故障 |
| 鉴权与权限不足 | 401、403、API Key 无效 | 凭据过期;模型越权尝试访问敏感工具 |
| 执行环境异常 | 内存溢出、进程被杀、GPU 显存不足、运行时崩溃 | 资源配额不足;代码 bug;镜像依赖缺失 |
| 结果解析失败 | 工具返回了非预期格式、空结果、超大响应 | 上游返回结构变化;对结果没有做 schema 校验 |
注意,这里还有一种很容易被忽略的失败:模型根本没有生成工具调用,而是试图直接编造结果。这种“工具没被调用”的失败要靠提示词约束和工具调用率监控来发现,不是简单 try-catch 能处理的。
3. 处理失败的第一步:错误捕获与结构化诊断
很多初期的 Agent 项目,失败处理只写了一个 try-except,然后打印日志、返回“工具调用失败”。这在演示环境够用,但进入生产环境后,模型拿到一个笼统的 “Tool error” 无法做任何有效决策。
正确的做法是把错误信息结构化,然后把关键信息回传给模型。模型需要知道三件事:哪个工具失败了、为什么失败、当前是否允许重试。
下面是一个将错误转换为模型可读反馈的 Python 示例:
import traceback from dataclasses import dataclass from typing import Any @dataclass class ToolResult: success: bool result: Any = None error_code: str = "" error_message: str = "" retryable: bool = False observation: str = "" def execute_tool(tool_name: str, tool_args: dict) -> ToolResult: """执行工具函数,并返回结构化结果。""" try: tool_func = get_tool(tool_name) except KeyError: return ToolResult( success=False, error_code="TOOL_NOT_FOUND", error_message=f"Tool '{tool_name}' is not registered.", retryable=False, ) try: result = tool_func(**tool_args) return ToolResult(success=True, result=result) except TypeError as e: # 参数结构问题,一般不可重试,需要模型修正参数 return ToolResult( success=False, error_code="INVALID_ARGUMENT", error_message=str(e), retryable=False, ) except TimeoutError as e: # 外部服务超时,通常可重试 return ToolResult( success=False, error_code="TOOL_TIMEOUT", error_message=str(e), retryable=True, ) except Exception as e: return ToolResult( success=False, error_code="INTERNAL_ERROR", error_message=f"{str(e)}\n{traceback.format_exc()}", retryable=False, )这段代码的核心是retryable字段。它告诉决策层:这个失败是否值得重试。超时可以重试,参数错误重试一百遍也没用,必须让模型重新生成参数。
诊断阶段的另一个关键动作是:不要只在代码层面捕获异常,还要把失败类型、失败上下文、模型当时的决策日志一并记录到可观测系统。这样后续做回归测试和 prompt 优化才有依据。
4. 处理失败的第二步:重试、降级与模型自纠错
拿到结构化错误之后,Agent 需要做出决策。整体策略分三层:重试、降级、自纠错。
4.1 有限次重试
重试不是无脑重试。每类工具定义不同的重试策略:
- 幂等工具(查询类):可以重试 2-3 次,间隔递增。
- 非幂等工具(下单、删除、写入、操作硬件):默认不重试,除非接口明确支持幂等键。
- 外部 API 超时:根据 HTTP 状态码决定,5xx 重试,4xx 不重试。
import time MAX_RETRIES = 3 def run_with_retry(tool_name: str, tool_args: dict) -> ToolResult: last_result = None for attempt in range(MAX_RETRIES): result = execute_tool(tool_name, tool_args) if result.success: return result if not result.retryable: return result wait_time = 2 ** attempt time.sleep(wait_time) last_result = result return last_result4.2 降级到备用工具
有些工具不是唯一实现。比如查询天气可以调 A 服务,也可以调 B 服务;翻译可以调云端 LLM,也可以调本地小模型。设计工具注册表时预留fallback_tools字段,主工具失败后自动降级。
TOOL_REGISTRY = { "get_weather": { "func": get_weather_a, "fallback": ["get_weather_b"], }, "get_weather_b": { "func": get_weather_b, "fallback": [], } }4.3 把错误反馈给模型,让模型自纠错
这是 Agent 与普通程序最大的区别:Agent 可以把失败信息作为新的上下文交给模型,让模型重新决策。很多面试候选人都漏掉这一步。
一个典型的重规划循环:
messages = [ {"role": "user", "content": user_task}, ] for step in range(MAX_STEPS): response = llm.chat(messages, tools=tool_schemas) # 模型决定调用工具 tool_call = response.tool_call if tool_call is None: return response.content tool_result = run_with_retry(tool_call.name, tool_call.arguments) # 把工具结果作为 observation 加入对话 messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": format_observation(tool_result) }) if not tool_result.success: # 关键:让模型基于错误信息重新规划 messages.append({ "role": "user", "content": ( f"工具 {tool_call.name} 执行失败,错误码:{tool_result.error_code}。" f"失败原因:{tool_result.error_message}。" f"请重新选择工具或修正参数,如果无法解决,请告知用户。" ) })这里有一个设计要点:format_observation必须把错误信息转换成人话。不要把堆栈直接喂给模型,而是提炼成“错误码 + 简要原因 + 建议动作”。模型不是调试器,给太多原始堆栈反而会干扰推理。
4.4 兜底:无法恢复时的用户反馈
当重试、降级、自纠错都用完之后,Agent 要能体面地停下来。好的 Agent 不会说“系统错误,请稍后再试”,而会说“我尝试了 3 次查询订单状态,但订单服务当前不可用,请你稍后重试,或者联系客服人工处理”。
这段兜底回复也应结构化,方便上层 UI 展示错误码和恢复建议。
5. 处理失败的第三步:架构层兜底与人工审批
工具调用失败处理不能只靠 Agent 自己,还需要系统层面的三道防线。
5.1 全局超时与熔断
单个工具调用必须有超时上限。一个工具卡住会阻塞整个 Agent 循环,甚至拖垮进程。建议流程:
- 网络请求设置 10-30 秒超时。
- 工具执行设置 60 秒超时。
- 整个 Agent 循环设置最大执行时长(比如 5 分钟)。
- 对核心外部服务做熔断:连续失败 N 次后快速失败,不再发起请求。
{ "timeout": { "tool_call_seconds": 30, "agent_loop_seconds": 300 }, "circuit_breaker": { "failure_threshold": 5, "open_state_seconds": 60 } }5.2 人工审批与安全闸门
最新的 agent 框架里,高风险工具(支付、删除、发送邮件、控制硬件)往往要过审批。如果审批超时,服务端会返回类似 “tool call requires approval” 的响应,Agent 不能视为普通失败,更不能反复重发。
这种情况下正确的处理是:
- 暂停执行,将审批请求挂起。
- 通知用户或审批人。
- 审批通过后恢复执行;审批拒绝则终止该步骤,并向 Agent 传递“用户拒绝了该操作”的语义。
class ApprovalRequiredError(Exception): """工具需要人工审批,且当前审批未通过。""" def __init__(self, tool_call_id, message="Approval required"): self.tool_call_id = tool_call_id super().__init__(message)在设计审批流程时要注意:审批拒绝也是一种信息,Agent 应该能理解“用户拒绝”和“执行失败”的区别,避免在用户拒绝后反复尝试同一个操作。
5.3 会话内的恢复与持久化
Agent 执行到一半失败,用户不会满足于“下次重新开始”。更合理的做法是把执行状态持久化,支持断点恢复。简单场景可以在 Session 中保存pending_tools,复杂场景需要把整个 Agent 状态机写入数据库。
6. 代码示例:一个最小可运行的 Agent 工具调用失败处理框架
这一节把上面的思路拼成一个完整的最小示例。假设使用 OpenAI 兼容的 function calling 接口,核心逻辑包含:工具注册、结构化执行、有限重试、错误回传、模型重规划。
import json import time from typing import Callable, Dict, List import openai client = openai.OpenAI() SYSTEM_PROMPT = "你是可调用工具完成任务的助手。工具调用失败时,请根据错误信息修正参数或改用其他工具。" class Agent: def __init__(self, tools: Dict[str, Callable], tool_schemas: List[dict]): self.tools = tools self.tool_schemas = tool_schemas self.max_steps = 8 self.max_retries = 3 def execute_tool(self, name: str, args: dict) -> dict: """执行工具并返回结构化结果。""" if name not in self.tools: return { "success": False, "error_code": "TOOL_NOT_FOUND", "error_message": f"Tool '{name}' 未注册", "retryable": False, } try: result = self.tools[name](**args) return { "success": True, "result": result, "retryable": False, } except TypeError as e: return { "success": False, "error_code": "INVALID_ARGUMENT", "error_message": str(e), "retryable": False, } except TimeoutError: return { "success": False, "error_code": "TIMEOUT", "error_message": "工具执行超时", "retryable": True, } except Exception as e: return { "success": False, "error_code": "INTERNAL_ERROR", "error_message": str(e), "retryable": False, } def run_with_retry(self, name: str, args: dict) -> dict: """带重试地执行工具。""" last = None for attempt in range(self.max_retries): result = self.execute_tool(name, args) if result["success"] or not result["retryable"]: return result history_msg = ( f"尝试 {attempt + 1} 失败:{result['error_message']}," "该错误可重试,正在自动重试..." ) print(history_msg) time.sleep(2 ** attempt) last = result return last def run(self, user_task: str) -> str: messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_task}, ] for step in range(self.max_steps): response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=self.tool_schemas, tool_choice="auto", ) message = response.choices[0].message messages.append(message) if not message.tool_calls: return message.content for tool_call in message.tool_calls: name = tool_call.function.name try: args = json.loads(tool_call.function.arguments or "{}") except json.JSONDecodeError: args = {} result = self.run_with_retry(name, args) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False), }) if not result["success"]: # 错误信息回传模型,让它重新决策 messages.append({ "role": "user", "content": ( f"工具 {name} 执行失败。错误码:{result['error_code']}," f"原因:{result['error_message']}。" "请重新选择工具或修正参数。如果已经无法完成,请明确告知用户。" ) }) return "已达到最大执行步骤,任务未能完成,请稍后重试。"这个框架可以直接跑通一个简单的“调用失败 -> 修正参数 -> 重新调用”的循环。实际项目里只需要替换tools和tool_schemas,并接入自己的日志、监控和审批系统。
7. 机器人场景下的特殊处理:从软件调用到物理世界
如果是普通后端 Agent 岗位,上面这套基本够用。但题目挂在宇树科技一面,需要额外思考机器人/具身智能场景的差异。物理世界的工具调用和纯软件工具调用有一个本质区别:调用动作可能已经对现实产生了影响,无法简单地“重试”。
机器人场景中必须考虑这三个问题:
7.1 非确定性执行结果
软件工具调用是确定性的:同样的参数,同样的服务,结果基本一致。但机器人调用一个“抓取物体”的工具,每次执行受摩擦力、光照、物体形态影响,结果可能完全不同。失败处理必须基于传感器反馈(视觉、力矩、位置)判断工具是否真的成功,而不是只看进程退出码。
7.2 动作的副作用无法撤销
调用“发送一封邮件”失败可以重发,但调用“移动机械臂到指定位置”如果执行了一半,物理状态已经改变,不能简单重新执行。这类场景需要设计前置的状态检查、中途的暂停机制、以及失败后的安全复位动作。
7.3 实时性要求更高
Agent 的 LLM 推理速度通常是秒级到十秒级,这在对话场景可以接受。但机器人执行任务时,如果工具失败后要等模型重新规划,可能已经错过了动作窗口。因此机器人场景通常要设计本地快速兜底逻辑:紧急情况下由确定性控制模块接管,而不是等模型返回。
面试时如果能主动提到这几个点,说明你不只是背过 Agent 框架,而是真的思考过物理世界约束下的系统设计,这会是不错的加分项。
8. 面试回答思路:从“怎么处理报错”到“怎么设计系统”
一个完整的面试回答,建议按这个框架组织:
- 先定义失败范围:工具调用失败不是一个点,而是一条链路,先讲清楚哪个环节可能出错。
- 再给失败分类:至少区分不可恢复错误(参数错、工具不存在)和可恢复错误(超时、网络抖动、服务 5xx),因为处理策略完全不同。
- 然后讲处理动作:结构化诊断、有限重试、降级到备用工具、错误信息回传模型自纠错。
- 接着讲架构兜底:全局超时、熔断、人工审批、状态持久化。
- 最后补充场景特殊性:如果涉及机器人或物理硬件,要强调非确定性和动作副作用。
参考回答话术:
我一般把工具调用失败分成几类:参数问题、外部服务问题、权限问题、环境问题。参数问题不会重试,直接让模型重新生成参数;外部服务问题会做有限次退避重试,再加上熔断;权限问题会转人工审批。整个执行过程中,错误信息会被结构化地回传给模型,让模型基于失败原因重新规划,而不是直接中断。在更底层,我会给每个工具设置超时上限,给整个 Agent 循环设置最大步数,并做好状态持久化,保证系统异常退出后可以恢复。另外,如果是在机器人场景里,调用动作本身可能有物理副作用,不能随便重试,还需要结合传感器反馈判断是否真正执行成功,并设计安全复位逻辑。
这样回答,既展示了编码能力,又展示了系统设计视野,还体现出了场景差异意识。
9. 工程化落地最佳实践与检查清单
最后给一组可以直接落到项目里的检查项,面试聊到工程化时也可以主动抛出。
| 检查项 | 建议 |
|---|---|
| 工具描述是否足够清晰 | 每个工具写明用途、参数约束、返回格式,从源头减少参数错误 |
| 失败信息是否结构化 | 统一使用成功状态、错误码、错误信息、是否可重试四个字段 |
| 重试是否有上限 | 所有重试必须设置最大次数和退避策略,禁止无限重试 |
| 是否有熔断机制 | 外部服务连续失败后进入熔断,避免雪崩 |
| 错误信息是否回传模型 | 模型必须能看到结构化失败原因,才能自纠错 |
| 高风险操作是否有人工审批 | 支付、删除、发送、硬件控制类操作必须有独立审批 |
| 是否记录完整轨迹 | 每一步推理决策、工具调用、错误信息、耗时都要可回放 |
| 是否有故障注入测试 | 主动模拟超时、5xx、参数错误,验证 Agent 是否按预期降级 |
| 模型幻觉是否被监控 | 统计工具调用失败率,发现持续走高的工具要检查描述或模型行为 |
| 是否有用户可读的兜底话术 | 最终失败时不抛堆栈,而是给出恢复建议 |
10. 总结与下一步
这个面试题的核心考察点从浅到深是三层:第一层会不会 try-except,第二层能不能设计重试和降级,第三层能不能理解 Agent 自纠错、人工审批、物理世界的额外约束。建议按这个顺序准备,不要一上来就讲大架构。
如果手里有可用的 LLM API,建议直接跑一遍第 6 节的示例代码:先注册一个故意会抛错的工具,观察 Agent 在错误信息回传后能否修正参数;再把超时时间调短,观察重试和最终兜底话术。这套实验能帮你把知识变成真实经验,面试时也能讲得更具体。