最近在准备 AI Agent 相关岗位的面试时,很多同学都会遇到一类看似基础、实际非常考验工程能力的问题:“Agent 调用工具失败,你会怎么处理?”尤其是一些做机器人、具身智能的公司,比如宇树科技的一面中,这个问题背后问的绝不仅仅是“加个 try-catch”,而是你对 Agent 运行链路的理解深度。
这篇文章我结合自己实际做 Agent 项目的经验,把工具调用失败这件事从现象、原因、分类、处理策略到代码实现完整梳理一遍。文章不假定你已经看过多少 Agent 框架源码,只要对 Function Calling 有基本概念,就能跟着一步步看懂。
1. 这道面试题,到底在问什么
1.1 从一道面试题说起
先说结论:面试官问“Agent 调用工具失败如何处理”,真正想听的不是某个固定的 API 怎么用,而是你在面对一个真实工程问题时,能不能快速定位故障层次,并且给出系统性的解决方案。
在传统的后端开发里,调用一个接口失败,我们通常会说“看日志、查状态码、看超时、做重试”。但在 Agent 场景里,问题复杂得多,因为中间多了一层大模型。
Agent 的典型工作方式是:
用户输入 -> LLM 决策 -> 生成工具调用参数 -> 执行工具 -> 返回结果 -> LLM 继续推理这个链路里,工具调用失败可能发生在多个环节:
- LLM 生成了不合法 JSON,导致参数解析失败。
- LLM 编造了不存在的工具名称。
- 工具参数类型正确,但业务上不合法,比如查询一个不存在的城市。
- 工具在执行过程中抛异常。
- 工具调用外部服务超时。
- 工具返回了结果,但结果格式不符合 LLM 后续推理的预期。
所以,处理工具调用失败,本质上是在处理一个“大模型 + 确定性系统”混合架构下的稳定性问题。这也是为什么它频繁出现在 Agent 岗位的面试中。
1.2 为什么 Agent 需要调用工具
在解释失败处理之前,先明确一个概念:Agent 为什么需要调用工具?
大语言模型本身是一个“文本生成模型”,它虽然具备一定的推理能力,但有几个天然短板:
- 知识有截止日期,无法获取实时信息。
- 不具备访问外部系统的能力,比如查数据库、调用 API、操作系统。
- 计算能力有限,数学计算容易出错。
工具(Tool / Function)就是为了弥补这些短板。Agent 通过大模型理解用户意图,然后以结构化的方式调用外部工具,把结果带回给模型继续推理。
常见的 Agent 工具包括:
| 工具类型 | 示例 |
|---|---|
| 信息检索 | 搜索引擎、知识库查询、日志查询 |
| 数据操作 | 数据库 SQL 查询、文件读写 |
| 系统操作 | 执行 Shell 命令、调用内部服务 API |
| 机器人控制 | 运动控制、导航、机械臂操作 |
| 第三方服务 | 天气查询、地图导航、订单查询 |
工具调用一旦失败,Agent 的任务链路就会中断,轻则回答错误,重则产生错误动作。举个机器人场景的例子:Agent 想控制机器人前进,但命令执行返回异常,如果不加处理,Agent 可能会在接下来继续执行一个基于错误状态的决策,这是很危险的。所以,失败处理不是“锦上添花”,而是 Agent 系统上线的必要条件。
1.3 工具调用失败问题的高频性
在我们实际开发中,工具调用失败几乎是每天都会遇到的事情。业内讨论比较多的几种现象包括:
- 工具调用审批失败:某些 Agent 平台为了防止 Agent 执行危险操作,会加入审批环节,审批被拒绝后工具无法执行。
- 执行超时:比如
the agent execution provider did not respond in time这类报错,本质上是 Agent 执行环境在指定时间内没有响应。 - 参数幻觉:模型生成了不存在的参数名,或者把字符串拼进了数字字段。
- 上下文不匹配:多 Agent 场景下,子 Agent 返回的结果父 Agent 无法理解。
这些问题的共性是:出错位置不确定、错误形式多样、单靠固定代码难以覆盖。因此我们需要一套分层、可观测、可恢复的处理机制。
2. 工具调用失败的类别与根因
在代码层面动手之前,先把失败分类。我是按照“故障发生的位置”来分的,这样在排查时能快速缩小范围。
2.1 LLM 侧失败:解析失败与参数幻觉
这一层的问题发生在“模型生成工具调用指令”之后,还没真正执行工具之前。
常见表现:
- 模型返回的
tool_calls中arguments不是合法 JSON。 - 模型调用的工具名不在注册表中。
- 参数缺少必填项。
- 参数类型错误,例如要求
integer,模型给了字符串。 - 参数的值本身是模型编造的,比如虚构了一个订单号。
这类失败的根因比较复杂。可能是模型能力不足、工具描述不清晰、Few-shot 示例不够,也可能是温度参数设置过高导致输出不稳定。
处理思路:先做 schema 校验,再做参数修正,必要时重试。重点是不要让不合法参数进入执行阶段,否则容易把错误传导到下游系统。
2.2 执行侧失败:超时、异常与依赖服务不可用
这一层的问题是真正执行工具函数时发生的,跟 LLM 关系不大。
常见表现:
- 工具抛出了自定义异常。
- 工具依赖的外部 HTTP 接口返回 500。
- 工具调用数据库超时。
- 工具执行时间过长,超过了 Agent 循环的等待时间。
- 工具所需的资源不足,比如磁盘满、网络连接被拒绝。
这类失败相对“正常”,因为任何一个真实系统都会出现依赖服务抖动。处理思路:异常捕获、超时控制、重试、降级、返回友好错误信息。
2.3 状态侧失败:幂等性、重复调用与上下文损坏
这一层的问题更隐蔽,也是很多有经验的工程师会重点考察的点。
常见表现:
- 工具第一次调用超时,但实际上后端已经执行成功了。重试时重复执行,产生了重复扣款、重复下单等问题。
- 工具执行成功,但返回结果太大,导致模型上下文过长,后续推理异常。
- 工具执行成功,但返回结果与预期格式不符,导致模型后续生成了错误回答。
- 多工具调用时,工具 A 修改了状态,工具 B 失败后没有回滚,系统处于中间状态。
处理思路:幂等设计、事务补偿、结果截断、状态持久化。
我把三类失败整理成一张表,方便你对照:
| 失败层次 | 典型表现 | 核心原因 | 处理重点 |
|---|---|---|---|
| LLM 侧 | JSON 解析失败、参数幻觉 | 模型输出不稳定、工具描述不清晰 | Schema 校验、重试、描述优化 |
| 执行侧 | 工具抛异常、外部服务超时 | 依赖服务不稳定、代码缺陷 | 捕获异常、超时控制、重试降级 |
| 状态侧 | 重复执行、结果不兼容 | 缺少幂等、结果格式不规范 | 幂等设计、结果规范化、补偿 |
3. 一个可复现的 Agent 工具调用失败处理示例
下面我用代码演示一个完整的工具调用循环,重点展示“失败后如何反馈给模型,并让模型自纠正”。这个示例基于 Python,核心逻辑不依赖特定框架,你可以直接复制运行,也可以迁移到 LangChain、Semantic Kernel 等框架中。
3.1 环境准备与项目结构
本示例环境:
- Python 3.10+
- 仅使用标准库
json、time、logging,不依赖第三方包。
为了便于演示,示例项目结构如下:
agent_tool_demo/ ├── main.py # 主程序,包含完整调用循环 └── tools.py # 工具定义与注册表我们的目标很简单:
- 定义两个工具:查天气、计算器。
- 实现一个函数模拟 LLM 决策,随机生成合法或非法的工具调用。
- 实现工具执行器,对失败进行分类处理。
- 将失败信息反馈给模型,让模型重新生成。
3.2 定义工具注册表与执行器
先来看tools.py。
# 文件路径:agent_tool_demo/tools.py import json import time import logging from typing import Any, Callable, Dict, Optional logger = logging.getLogger(__name__) class ToolError(Exception): """工具执行时的自定义异常。""" def __init__(self, message: str, retryable: bool = False): super().__init__(message) # retryable 表示该错误是否值得重试 self.retryable = retryable class ToolRegistry: """工具注册表,集中管理工具元数据与执行函数。""" def __init__(self): self._tools: Dict[str, Dict[str, Any]] = {} def register( self, name: str, description: str, parameters_schema: dict, func: Callable[..., str], ): self._tools[name] = { "name": name, "description": description, "parameters": parameters_schema, "func": func, } def get(self, name: str) -> Optional[Dict[str, Any]]: return self._tools.get(name) def list_tools(self) -> list[dict]: """返回 OpenAI 风格的 tools 描述,供模型使用。""" tools = [] for tool in self._tools.values(): tools.append( { "type": "function", "function": { "name": tool["name"], "description": tool["description"], "parameters": tool["parameters"], }, } ) return tools def execute(self, name: str, arguments: dict) -> str: """执行工具,返回字符串形式的结果。""" tool = self.get(name) if tool is None: raise ToolError(f"工具 {name} 不存在", retryable=False) try: result = tool["func"](**arguments) if not isinstance(result, str): result = json.dumps(result, ensure_ascii=False) return result except ToolError: raise except Exception as exc: logger.warning("工具 %s 执行失败: %s", name, exc) raise ToolError(f"工具 {name} 执行异常: {exc}", retryable=True) from exc这里的ToolRegistry有四个核心能力:
- 注册工具时保存完整的 JSON Schema。
- 提供 OpenAI 风格的工具描述列表。
- 按名称查找工具。
- 统一执行入口,方便在入口处做拦截、日志、错误包装。
3.3 注册具体工具
再来看如何注册一个查询天气的工具和一个计算器工具。
# 继续在 tools.py 中追加 registry = ToolRegistry() def get_weather(city: str) -> str: """模拟查询天气,city 为 '未知城市' 时抛错。""" if city == "未知城市": raise ToolError("未找到该城市信息", retryable=False) # 模拟网络延迟 time.sleep(0.2) return f"{city} 当前温度 26°C,多云" def calculator(expression: str) -> str: """模拟计算器,只支持简单的加减乘除表达式。""" # 这里用 eval 仅用于演示,生产环境不要直接 eval 用户输入 allowed = set("0123456789+-*/(). ") if not set(expression).issubset(allowed): raise ToolError("表达式包含非法字符", retryable=False) try: result = eval(expression) # noqa: S307 return f"{expression} = {result}" except Exception as exc: raise ToolError(f"表达式计算失败: {exc}", retryable=False) from exc registry.register( name="get_weather", description="查询指定城市的实时天气", parameters_schema={ "type": "object", "properties": { "city": { "type": "string", "description": "城市名,例如 上海", } }, "required": ["city"], }, func=get_weather, ) registry.register( name="calculator", description="计算简单的数学表达式", parameters_schema={ "type": "object", "properties": { "expression": { "type": "string", "description": "数学表达式,例如 1+2*3", } }, "required": ["expression"], }, func=calculator, )这里有一个细节:get_weather里我故意让未知城市抛ToolError,并且retryable=False。这模拟了“参数合法但业务上无数据”的场景,这种失败重试多少次都没用。而calculator里把eval限制为纯数学字符集,避免执行任意 Python 代码,这也是工具设计的一个重要原则:工具入口要做输入校验,不要把底层漏洞暴露给模型。
3.4 实现带自纠正能力的调用循环
下面进入核心部分:main.py里实现 Agent 的执行循环。
# 文件路径:agent_tool_demo/main.py import json import logging import time from typing import Any, Dict, List, Optional from tools import ToolError, ToolRegistry, registry logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", ) logger = logging.getLogger(__name__) class BaseAgentLoop: """一个极简 Agent 循环,包含工具失败处理与自纠正。""" def __init__(self, registry: ToolRegistry, max_retries: int = 3): self.registry = registry self.max_retries = max_retries self.messages: List[Dict[str, Any]] = [] def llm_think(self, messages: list, tools: list) -> Dict[str, Any]: """ 模拟 LLM 决策。 真实项目中,这里会调用 OpenAI / 通义千问 / 本地模型, 输入 messages + tools,输出 content 或 tool_calls。 这里为了演示失败处理,返回一个硬编码的工具调用。 """ # 模拟模型偶尔产生非法 JSON,概率 30% if time.time() % 10 < 3: return { "content": "", "tool_calls": [ { "id": "call_demo_1", "function": { "name": "get_weather", "arguments": "{'city': '上海'}", # 单引号,非法 JSON }, } ], } return { "content": "", "tool_calls": [ { "id": "call_demo_2", "function": { "name": "get_weather", "arguments": json.dumps({"city": "上海"}, ensure_ascii=False), }, } ], } def parse_arguments(self, arguments: str) -> dict: """解析模型生成的参数,捕获 JSON 解析错误。""" try: args = json.loads(arguments) except json.JSONDecodeError as exc: raise ToolError( f"工具参数不是合法 JSON: {arguments},错误: {exc}", retryable=True, ) from exc if not isinstance(args, dict): raise ToolError("工具参数必须是 JSON 对象", retryable=True) return args def validate_arguments(self, name: str, args: dict) -> None: """ 简易参数校验:检查必填字段是否存在。 真实项目建议使用 jsonschema 库做完整校验。 """ tool = self.registry.get(name) if tool is None: raise ToolError(f"工具 {name} 不存在", retryable=False) schema = tool["parameters"] required = schema.get("required", []) properties = schema.get("properties", {}) for field in required: if field not in args or args[field] is None: raise ToolError( f"缺少必填参数: {field}", retryable=True ) for field, value in args.items(): if field not in properties: # 多传了未知参数,保守起见返回错误 raise ToolError( f"未知参数: {field}", retryable=True ) expected_type = properties[field].get("type") if expected_type == "string" and not isinstance(value, str): raise ToolError( f"参数 {field} 期望类型 string,实际 {type(value).__name__}", retryable=True, ) if expected_type == "integer" and not isinstance(value, int): raise ToolError( f"参数 {field} 期望类型 integer,实际 {type(value).__name__}", retryable=True, ) def execute_tool(self, call: dict) -> str: """执行单个工具调用。""" function = call.get("function", {}) name = function.get("name", "") arguments_raw = function.get("arguments", "{}") logger.info("开始执行工具: %s, 参数: %s", name, arguments_raw) arguments = self.parse_arguments(arguments_raw) self.validate_arguments(name, arguments) result = self.registry.execute(name, arguments) logger.info("工具 %s 执行成功: %s", name, result) return result def run(self, user_input: str) -> str: """主循环:LLM 决策 -> 执行工具 -> 失败反馈 -> 重新决策。""" self.messages = [ {"role": "user", "content": user_input}, ] tools = self.registry.list_tools() for attempt in range(1, self.max_retries + 1): logger.info("===== 第 %d 轮 =====", attempt) llm_response = self.llm_think(self.messages, tools) tool_calls = llm_response.get("tool_calls", []) if not tool_calls: # 模型认为不需要调用工具,直接返回内容 return llm_response.get("content", "(无内容)") # 逐条执行工具调用 tool_results = [] has_error = False for call in tool_calls: try: result = self.execute_tool(call) tool_results.append( { "role": "tool", "tool_call_id": call.get("id", ""), "content": result, } ) except ToolError as exc: has_error = True logger.warning("工具调用失败: %s, retryable=%s", exc, exc.retryable) if not exc.retryable: # 非可重试错误,直接终止 return f"执行失败,无法继续: {exc}" # 把错误信息反馈给模型,让模型重新生成 self.messages.append( { "role": "assistant", "content": None, "tool_calls": tool_calls, } ) self.messages.append( { "role": "tool", "tool_call_id": call.get("id", ""), "content": f"工具执行失败: {exc},请检查参数后重新调用。", } ) # 重新开始下一轮 break if has_error: continue # 所有工具执行成功,将结果追加到消息,让模型继续生成 self.messages.append( { "role": "assistant", "content": None, "tool_calls": tool_calls, } ) self.messages.extend(tool_results) # 模型继续生成最终回答 final_response = self.llm_think(self.messages, tools) if final_response.get("content"): return final_response["content"] # 如果没有 content,说明模型还要继续调工具,继续循环 return "达到最大重试次数,任务处理失败。" if __name__ == "__main__": agent = BaseAgentLoop(registry=registry, max_retries=3) answer = agent.run("帮我查一下上海的天气") print("最终回答:", answer)这个示例虽然结构简单,但已经把“工具调用失败处理”的完整脉络串起来了。你可以看到几个关键设计点:
parse_arguments负责拦截非法 JSON。validate_arguments负责参数 schema 校验。execute_tool统一收口工具执行。- 异常处理上区分了
retryable,避免对“参数永远错误”的请求做无效重试。 - 错误信息通过
messages重新交给模型,让模型基于错误调整下次调用。
3.5 运行与预期结果
直接在项目目录下运行:
cd agent_tool_demo python main.py由于llm_think中我加了一个基于时间的随机逻辑,你运行多次可能会看到两种结果:
- 参数是非法 JSON,触发重试,但在重试中模型生成了合法参数,最终查询成功。
- 参数直接合法,一次调用成功。
正常情况下,只要不是必现错误,最后的输出都应该是:
最终回答: 上海 当前温度 26°C,多云如果你把max_retries改成 1,会发现即使模型第二次能生成正确参数,也会因为重试次数耗尽而失败。这说明重试次数不能设得太小,但也不能无限大,否则模型会在一个错误上反复打转,浪费 token。
4. 失败处理策略的工程化落地
上面示例演示了“反馈给模型让它自纠正”这个核心思路。但在真实项目中,还需要补充更多工程化策略。
4.1 重试策略:区分可重试与不可重试
在所有失败处理逻辑里,第一件事就是判断这个错误是否值得重试。
推荐的做法是像示例中一样,为异常增加retryable属性。分类原则如下:
| 错误类型 | 是否可重试 | 原因 |
|---|---|---|
| JSON 解析失败 | 可重试 | 重新生成参数大概率会变合法 |
| 缺少必填参数 | 可重试 | 模型补齐参数即可 |
| 工具不存在 | 不可重试 | 模型选错工具,重试也可能选错,需要改提示词 |
| 业务数据不存在 | 不可重试 | 换个参数结果也一样,可能是用户问题本身无解 |
| 外部服务超时 | 可重试 | 服务抖动,重试可能成功 |
| 权限不足 | 不可重试 | 重试不会改变权限 |
重试策略上,还要考虑两个参数:
max_retries:建议 2 到 4 次,太少容易失败,太多浪费时间和 token。- 退避策略:如果外部服务超时,建议采用指数退避,比如
0.5s → 1s → 2s,避免重试风暴。
4.2 超时控制:避免 Agent 卡死
工具调用最怕的就是“永久等待”。外部服务的 P99 可能是 1 秒,但偶尔会卡住几分钟。如果 Agent 没有超时控制,整个任务就被拖住。
推荐使用 Python 的concurrent.futures来做超时控制。
import concurrent.futures def execute_with_timeout(func, args_dict: dict, timeout: int = 10) -> str: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(func, **args_dict) try: return future.result(timeout=timeout) except concurrent.futures.TimeoutError: future.cancel() raise TimeoutError(f"工具执行超过 {timeout} 秒")这里要说明一点:future.cancel()只能取消尚未开始执行的任务。如果任务已经在线程中运行,线程没办法被强制终止。所以在生产环境中,超时控制更多是“放弃等待”,而不是“终止执行”。对于已经开始修改外部状态的工具,超时后还需要额外的补偿机制。
4.3 错误反馈:让模型基于失败信息自纠正
这是 Agent 失败处理里最有“智能感”的一环。
当工具执行失败时,不要只是简单地返回一个空字符串或者把异常吞掉,而是要把结构化的错误信息回传给模型。模型读到错误信息后,才会知道自己刚才的调用出了什么问题。
错误信息里建议包含:
- 工具名称。
- 失败原因。
- 如果是参数问题,尽量告诉模型“期望什么格式”。
- 如果是业务错误,说明“哪些值是无效的”。
示例:
工具 get_weather 执行失败: 未找到该城市信息,请检查城市名是否拼写正确。当前输入: 未知城市不要返回英文长堆栈给模型。模型虽然能读,但会浪费上下文,而且对普通用户不友好。
更重要的一个原则:错误信息不要让模型猜测意图。比如工具返回{"code": 500},模型不一定能理解它是什么意思。如果能在工具侧把错误翻译成人话,模型后续修复成功的概率会大幅提高。
4.4 降级与人工介入
有一些场景不适合无限自纠正,需要及时止损。
- 非可重试错误:比如“权限不足”“参数永远非法”,建议直接终止本轮任务,并向用户说明失败原因。
- 多次重试仍失败:达到
max_retries后,不要继续循环,而是进入人工降级流程。 - 高风险操作:在机器人控制、支付、删除数据等场景,工具执行失败后不应自动重试,而应该暂停并请求确认。
降级路径可以设计为:
自动重试 -> 自动修复参数 -> 切换备用工具 -> 人工介入 -> 友好提示用户例如:主工具是调用 A 服务获取天气,A 服务挂了,可以降级到 B 服务。如果 B 也没有数据,则提示用户“当前天气服务暂时不可用”。
4.5 幂等设计:避免重复执行副作用
我在前面状态侧失败里提到过幂等。这里单独说一下,因为这是最容易掉坑的地方。
场景:Agent 调用“创建订单”工具,因为网络问题,Agent 以为调用失败,于是重试了一次。如果工具没有幂等控制,就会创建两笔订单。
解决方案:
- 在工具调用参数中增加
request_id或trace_id。 - 工具内部根据
request_id判断是否已处理过,如果已处理,直接返回上一次的结果。 - 对于更新类操作,使用版本号或乐观锁。
这个设计并不复杂,但它决定了 Agent 系统能不能安全地执行有副作用的工具。
4.6 工具协议设计:从源头降低失败率
很多工具调用失败,其实是工具描述写得不够好。模型不是人,它只能通过工具描述来理解“这个工具是干什么的、参数怎么传”。
优化建议:
- 工具描述要写“什么时候用、什么时候不用”。
- 参数描述要写清楚格式、取值枚举、示例。
- 必填项与选填项要明确,不要依赖模型猜。
- 工具数量不宜过多,避免模型误选。
下面是一个对比示例:
{ "type": "function", "function": { "name": "get_weather", "description": "查询天气", "parameters": { "type": "object", "properties": { "city": { "type": "string" } } } } }这段描述很简洁,但不够好。模型可能不知道city应该填什么、是拼音还是中文。
更好的写法:
{ "type": "function", "function": { "name": "get_weather", "description": "根据城市名查询实时天气。当用户询问某个城市的温度、天气、是否下雨时使用。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "标准城市中文名,例如:北京、上海、广州。不支持省份和区县。" } }, "required": ["city"] } } }工具描述越清晰,LLM 侧“参数幻觉”和“误选工具”的概率就越低,这套功夫是失败处理体系里性价比最高的一环。
5. 面试回答框架:从问题到方案的表达逻辑
如果你正在准备面试,这一节可能比前面的代码更值得反复看。因为面试官不会真的让你在半小时内写一套完整的容错框架,他看的更多是你的思维模型。
5.1 先分类,再给策略
我的建议是,遇到“如何处理失败”这类问题时,不要直接跳进“重试 N 次”这种细节。先给一个分类框架,让面试官知道你有全局视角。
可以这样回答:
我会先判断工具调用失败发生在哪一层。工具调用链路大致是模型生成参数、参数解析、参数校验、执行工具、返回结果。失败可能出现在任意一环。
第一类是模型侧问题,比如生成非法 JSON、参数幻觉、工具名不存在。这种我要做参数解析与 schema 校验,然后把结构化错误反馈给模型,让模型自纠正,同时修正提示词和工具描述来降低概率。
第二类是执行侧问题,比如工具内部异常、外部服务超时。这种我要做异常捕获、超时控制、重试和降级,同时区分可重试与不可重试错误。
第三类是状态侧问题,比如重试导致重复下单、结果太大撑爆上下文。这种我要在工具设计上保证幂等、对返回结果做截断和格式化。
这段话的价值在于:你没有一上来就背 API,而是先把问题域切开,然后逐层给方案。面试官很容易从中看出你的工程经验。
5.2 结构化答案示例
接下来可以对每一类展开,给出具体的处理手段。按照我前面设计的层次,可以按下面顺序谈:
第一层:防御性校验
- 对模型返回的
arguments做严格 JSON 解析。 - 用 JSON Schema 对参数做完整校验,不满足则直接让模型重写。
- 不合法参数不进入执行阶段。
第二层:执行阶段容错
- 每个工具执行入口做
try-except。 - 设置默认超时时间,避免长时间阻塞。
- 根据错误类型决定是否重试。
- 把错误信息包装成可读形式返还给模型。
第三层:错误反馈与自纠正
- 将工具返回的错误追加到上下文,让模型看过后重新生成调用。
- 控制重试次数,比如 2 到 3 次。
- 非可重试错误直接结束,避免无效循环。
第四层:观测与预警
- 为每次工具调用记录日志、耗时、入参、出参、错误类型。
- 对失败率做监控,超过阈值告警。
- 累积失败样本,定期分析是模型问题还是工具问题。
这四层是层层递进的关系,从“避免失败”到“处理失败”再到“修复失败”和“发现隐患”。
5.3 加分项:结合业务场景谈
如果你面试的是机器人、具身智能相关岗位,可以补充一句:
在机器人场景里,工具调用失败不只是“任务失败”,还可能涉及安全问题。比如控制类工具调用失败后不能盲目重试,应该先回到安全状态,再根据状态机的设计决定是否重新执行。
这种回答的深度明显高于“报错就重试”的初级思路。如果你真的做过类似项目,还能举个例子说明你如何处理过“Agent 执行中途失败”的情况。
面试官问这个问题的核心目的,其实是考察你有没有“工程化上线一个 Agent”的经验。所以回答时尽量多用自己的项目经历去支撑,少讲空理论。
6. 高频问题与排查清单
6.1 常见错误速查表
下面是我在实际项目里经常遇到的一些错误现象和处理思路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
arguments解析失败,报 JSONDecodeError | 模型输出单引号、多余逗号或截断 | 对参数做 JSON 修复,或让模型重新生成 |
| 工具返回“参数不能为空” | 模型没生成必填字段 | 工具描述中明确必填项,代码中做 schema 校验 |
| 工具一直超时 | 外部服务慢或线程池阻塞 | 设置超时时间,使用异步调用,分析依赖服务 P99 |
| 模型多次调用同一个工具但参数相同 | 模型陷入循环,上下文丢失了之前的结果 | 检查是否正确把 tool_message 追加到了上下文 |
| 重试导致重复下单 | 工具缺少幂等 | 增加 request_id,工具内部做幂等判断 |
| 错误信息返回给模型后仍不修正 | 模型能力不足或提示词不足以表达约束 | 在工具描述中增加示例,或更换更强模型 |
| 工具返回 JSON 太大,把上下文撑爆 | 查询结果量太大 | 对结果做限制条数、截断、摘要后返回 |
6.2 排查步骤建议
遇到“Agent 工具调用失败”时,我建议按下面顺序排查,不要上来就改代码:
- 看原始日志:确认失败发生在哪一层。是模型返回异常,还是工具执行异常。
- 看入参出参:把模型的
arguments原文打出来,很多时候一眼就能看出是参数格式问题。 - 模拟工具调用:脱离 Agent 环境,单独用测试脚本验证工具本身是否正常。
- 复现问题:如果问题能稳定复现,尝试修改工具描述,观察模型输出变化。
- 检查上下文:确认错误反馈是否正确传递给了模型,以及上下文有没有丢失之前的信息。
6.3 工具开发规范
最后分享一套我在团队内部推行的工具开发规范,照着做可以显著降低失败率:
- 每个工具必须有清晰的描述和 JSON Schema。
- 每个工具必须支持幂等,尤其是写操作。
- 每个工具必须有超时控制。
- 工具错误必须包装为可读文本并区分“可重试/不可重试”。
- 工具的副作用要尽量小,能查询就不要改成写操作。
- 所有工具调用必须记录日志,便于事后分析。
- 对工具返回结果设置大小上限,避免上下文膨胀。
7. 总结与下一步学习建议
如果想在 Agent 开发这条路上走得更远,工具调用失败处理只是第一道门槛。这道题训练的核心能力是“拆解复杂问题 + 分层构建容错体系”的能力。建议你按照本文的思路,实现一个自己的 Agent 工具调用框架,把重试、超时、参数校验、错误反馈、幂等设计都加进去,再考虑引第三方框架如 LangChain、LlamaIndex 来做对比。
更进阶的方向是研究 Agent 的可观测性,比如用 LangSmith、Langfuse 记录工具调用的 trace,分析哪些工具的失败率最高,哪些描述导致了模型误用。这些数据,才是优化 Agent 系统最有价值的素材。
如果你正在准备面试,可以找一个自己实际遇到过的工具失败案例,把背景、定位过程、解决手段、最终效果完整写下来,比背十道八股文更有说服力。