我的 Agent 调用工具失败后,要么把报错信息当成正确结果继续往下编,要么对着同一个错误反复重试到耗尽,我对着工具错误处理排查了大半天的复盘
那是我做的一个能调用多个工具(查数据库、调 API、读文件)的 Agent。顺利路径下它表现得很聪明。可一旦某个工具调用失败了——数据库超时、API 返回 500、文件不存在——它的表现就变得极其离谱:有时它把工具返回的那段报错信息,当成了"查询结果",一本正经地基于这段错误继续往下推理、给用户编了个荒唐的答案;有时它对着同一个失败的工具调用,用一模一样的参数反复重试几十次,直到把步数/token 耗尽才崩。我盯着这些日志哭笑不得:它怎么连"工具失败了"这件事都意识不到?排查了大半天,我才真正理解了 Agent 工程里一个被严重低估的环节:工具调用的错误处理。这篇就把这场"Agent 不会处理工具失败"的事故,从头复盘一遍。
故障现场:把报错当数据,或对着错误死磕
先看现场。问题就藏在我那个"只考虑了工具成功、没考虑工具失败"的实现里:
# 我的 Agent 工具执行: 只想着"成功返回结果", 没处理"失败"
def run_tool(call):
if call.name == "query_db":
return db.execute(call.args["sql"]) # ✗ 如果抛异常/超时呢?
if call.name == "call_api":
return requests.get(call.args["url"]).text # ✗ 如果返回500/超时呢?
# Agent 主循环(简化):
def agent_loop(question):
messages = [...]
while True:
resp = llm.chat(messages, tools=TOOLS)
if resp.tool_calls:
for call in resp.tool_calls:
try:
result = run_tool(call)
except Exception as e:
result = str(e) # ✗✗ 把异常信息当结果塞回去!
messages.append({"role": "tool", "content": str(result)})
continue
return resp.content
# 两种离谱的失败表现:
# 表现一: "把报错当数据" —— 基于错误继续编
# 工具返回: "ERROR: Connection timeout after 30s"
# → 这段错误被当成 tool 结果塞回上下文。
# → 模型【分不清】这是"错误"还是"数据", 它看到上下文里有内容,
# 就基于这段 "Connection timeout..." 继续推理, 给用户编了个答案。
# → 用户得到一个"基于错误信息脑补出来的、完全错误的"回答。
# 表现二: "对着错误死磕" —— 无限重试
# 工具调用失败 → 模型看到失败, 决定"再试一次"。
# → 但它用【完全一样的参数】重试 → 当然又失败 → 再试 → 又失败...
# → 它没有意识到"这样重试是没用的", 一直磕到步数/token 耗尽。
# 现象拼图:
# - 我只设计了"工具成功"的路径, 完全没设计"工具失败"该怎么办。
# - 大模型【不会天然地正确处理失败】: 它分不清"错误信息"和"正常数据"
# (对它都是文本), 也不会"聪明地"判断"这个错误该不该重试、怎么换个法子"。
# - ★ 根因: 我把 Agent 的"工具调用"当成了"一定会成功"的理想操作,
# 而没有把它当成"在真实世界里可能以各种方式失败"的操作来设计。
看清真相后,我意识到自己漏掉了 Agent 设计里至关重要的一半。我只设计了"工具成功"的路径,完全没设计"工具失败"该怎么办,而我那句把异常 str(e) 直接当结果塞回上下文的代码,正是灾难的源头。大模型不会天然地正确处理失败:它分不清"错误信息"和"正常数据"(对它都只是文本),于是把 "Connection timeout" 当成查询结果、基于它给用户编了个荒唐答案;它也不会"聪明地"判断"这个错误该不该重试、怎么换个法子",于是用完全一样的参数反复重试到耗尽。根本原因是:我把 Agent 的"工具调用"当成了"一定会成功"的理想操作,而没有把它当成"在真实世界里可能以各种方式失败"的操作来设计。
第一件事:搞懂 Agent 为什么不会天然处理失败
要解决它,得先理解为什么大模型"看到错误"却"意识不到那是错误",以及失败处理为何如此关键。
为什么 Agent 不会天然处理工具失败
# 一、模型分不清"错误"和"数据" —— 对它都是文本
# - 工具返回的一切, 进到上下文里, 对模型来说都是"一段文字"。
# - "查询返回了3条订单" 和 "ERROR: connection timeout", 在模型眼里
# 都只是"工具这一步产生的文本", 它没有内在机制去区分"成功 vs 失败"。
# - 除非你【明确地告诉它】"这是一个错误", 否则它可能把错误当数据用。
# 二、模型不会天然"聪明地"应对失败
# - 人遇到"超时", 会想"是不是网络问题? 等一下再试? 换个接口?"。
# - 模型不会自动有这套"故障应对策略", 除非你引导/约束它。
# - 它可能: 把错误忽略、把错误当数据、用相同参数傻重试、或直接放弃。
# 三、失败是"常态"不是"例外"(真实世界)
# - Agent 调用的工具, 都是真实世界的操作: 网络会抖、API会限流/报错、
# 数据库会超时、文件会不存在、参数可能不对... 失败【经常发生】。
# - 一个只在"工具都成功"时才工作的 Agent, 在真实环境里几乎必然出问题。
# 四、失败处理不好的连锁危害:
# - 把错误当数据 → 给用户错误答案(还很自信, 见幻觉篇)。
# - 无限重试 → 浪费 token/步数、拖慢响应、甚至加剧下游故障(见重试风暴)。
# - 一错就崩 → Agent 太脆弱, 一个小故障就让整个任务失败。
# 核心: 模型分不清错误与数据(都是文本)、不会天然聪明应对失败; 而工具调用在真实
# 世界里失败是常态; 不设计失败处理, Agent 会把错误当数据/傻重试/一错就崩。
想透这几点,我才明白 Agent 的健壮性差在哪。一、模型分不清"错误"和"数据"——工具返回的一切进到上下文里,对模型都只是"一段文字";"查询返回 3 条订单"和"ERROR: timeout"在它眼里都是"工具产生的文本",它没有内在机制区分成功 vs 失败,除非你明确告诉它"这是错误"。二、模型不会天然"聪明地"应对失败——人遇到超时会想"等一下再试?换个接口?",但模型不会自动有这套故障应对策略,可能忽略错误、当数据用、用相同参数傻重试、或直接放弃。三、失败是"常态"不是"例外"——Agent 调用的都是真实世界操作(网络抖、API 限流、DB 超时、文件不存在),失败经常发生;只在"工具都成功"时工作的 Agent 几乎必然出问题。四、连锁危害:把错误当数据→给用户错误答案、无限重试→浪费 token 拖慢响应、一错就崩→Agent 太脆弱。归根结底:模型分不清错误与数据、不会天然聪明应对失败,而工具调用失败是常态;不设计失败处理,Agent 就会把错误当数据/傻重试/一错就崩。
第二件事:正解——把失败"结构化"地告诉模型,并给出应对策略
搞懂了原理,正解就清晰了:明确标注错误(让模型知道这是失败)、给出可操作的提示、限制重试次数、提供降级路径。
# ====== 正解一: 把工具结果"结构化", 明确区分成功/失败 ======
def run_tool(call) -> dict:
try:
result = _execute(call)
return {"status": "success", "data": result} # ✓ 明确标"成功"
except TimeoutError:
return {"status": "error", "error_type": "timeout",
"message": "工具调用超时", "retryable": True,
"hint": "下游可能繁忙, 可稍后重试一次, 或换用其他工具"}
except NotFoundError:
return {"status": "error", "error_type": "not_found",
"message": "未找到对应数据", "retryable": False, # ✗ 别重试
"hint": "该数据不存在, 请告知用户未查到, 不要重试相同查询"}
except Exception as e:
return {"status": "error", "error_type": "unknown",
"message": str(e), "retryable": False,
"hint": "工具执行出错, 请如实告知用户遇到问题, 不要编造结果"}
# → 关键: 让模型一眼看出"这是错误"(status=error), 而不是把错误当数据。
# 还告诉它"能不能重试(retryable)"和"该怎么办(hint)", 引导它正确应对。
# ====== 正解二: 在系统提示里, 教模型如何处理工具失败 ======
SYSTEM = """你是助手。调用工具时, 注意工具返回的 status 字段:
- status=success: 用 data 字段的数据继续。
- status=error: 这是一次【失败】, 不要把 error/message 当成数据!
* 若 retryable=true: 最多重试1次(可调整参数), 仍失败就如实告知用户。
* 若 retryable=false: 不要重试相同调用; 按 hint 处理或告知用户。
* 绝不要基于错误信息编造答案。无法完成就诚实说"暂时无法完成"。"""
# ====== 正解三: 在"代码层"硬性限制重试(别只靠模型自觉)======
class ToolRunner:
def __init__(self): self.call_history = {} # 记录每个调用的失败次数
def run(self, call):
key = (call.name, json.dumps(call.args, sort_keys=True)) # 调用指纹
if self.call_history.get(key, 0) >= 2:
# 同一个调用(相同工具+相同参数)已失败2次 -> 硬性熔断, 不再执行
return {"status": "error", "message": "该调用已多次失败, 已停止重试",
"retryable": False, "hint": "请换一种方式或告知用户"}
result = run_tool(call)
if result["status"] == "error":
self.call_history[key] = self.call_history.get(key, 0) + 1
return result
# → 在代码层兜底: 即使模型"想"无限重试相同调用, 代码也会拦住它。
# ====== 正解四: 整体步数/预算上限 + 降级 ======
# - Agent 主循环设最大步数(如20步), 超了强制停止, 走降级(转人工/告知失败)。
# - 别让 Agent 无限循环下去(配合"工具失败处理", 双保险)。
# 核心: 把工具结果结构化(status明确成功/失败 + retryable + hint)让模型识别错误、
# 系统提示教它如何应对失败、代码层硬性限制相同调用的重试、整体设步数上限+降级。
修复的核心,是"把'失败'清清楚楚地、结构化地告诉模型,并从代码层兜住它的错误应对"。正解一:把工具结果结构化,明确区分成功/失败——成功返回 {status: success, data},失败返回 {status: error, error_type, message, retryable, hint};关键是让模型一眼看出"这是错误"(而非当数据),还告诉它"能不能重试(retryable)"和"该怎么办(hint)"。正解二:在系统提示里教模型如何处理工具失败——看 status 字段、error 时别当数据、按 retryable 决定是否重试、绝不基于错误编造答案、无法完成就诚实说。正解三:在"代码层"硬性限制重试——记录每个调用的指纹(工具+参数),同一调用失败 2 次就硬性熔断,即使模型"想"无限重试相同调用,代码也拦住它(别只靠模型自觉)。正解四:整体步数/预算上限 + 降级——主循环设最大步数,超了强制停止走降级(转人工/告知失败)。归根结底:把工具结果结构化(status + retryable + hint)让模型识别错误、系统提示教它应对、代码层硬限相同调用的重试、整体设步数上限 + 降级。
第三件事:Agent 健壮性的全景
修这个问题时我意识到,工具错误处理只是 Agent 健壮性的一环。我把 Agent 健壮性梳理了一遍。
Agent 健壮性全景(让 Agent 在真实世界里活下来)
# Agent 在真实运行中会遇到的各种"不理想":
# 1. 工具调用失败(本文): 超时/报错/不存在 → 结构化错误 + 应对策略。
# 2. 工具调用参数错误: 模型给的参数不对/缺失 → 校验参数, 报错引导它改。
# 3. 死循环/不收敛(见死循环篇): 反复调用、不结束 → 步数上限 + 进度检测。
# 4. 上下文膨胀(见上下文篇): 工具返回太大 → 裁剪/摘要。
# 5. 幻觉(见幻觉篇): 编造事实/编造工具/编造参数 → 接地 + 校验。
# 6. 危险操作: 删数据/发邮件/花钱等 → 高危操作要确认/限权/sandbox。
# 7. 模型输出格式错误: 该给JSON却给了别的 → 校验 + 重试 + 兜底解析。
# 一个核心思想: 把 Agent 当成"一个不可靠的执行者"来设计外围保障
# - 模型本身是"概率的、可能犯各种错的", 你不能假设它"总会做对"。
# - 所以要在它【外面】, 用确定性的代码, 包一层"防护网":
# 校验它的输出、结构化地喂给它信息、限制它的行为、兜底它的失败。
# - 这层"防护网"(也叫 Agent 的"脚手架/harness"), 才是把"一个会犯错的模型"
# 变成"一个可靠的 Agent"的关键工程。
# 一句话: Agent = 模型(智能但不可靠) + 脚手架(确定性的保障和约束)。
# 工程的重点, 往往不在模型本身, 而在那层"约束和兜底模型"的脚手架上。
# 核心: 工具错误只是Agent健壮性一环, 还有参数校验/死循环/上下文/幻觉/危险操作/
# 格式错误; 核心思想是把模型当不可靠执行者, 在外面用确定性代码包防护网(脚手架)。
修这个问题让我看到了更完整的图景:工具错误处理只是 Agent 健壮性的一环。Agent 在真实运行中会遇到各种"不理想":工具调用失败(本文)、工具参数错误(校验参数引导它改)、死循环不收敛(步数上限+进度检测)、上下文膨胀(裁剪/摘要)、幻觉(接地+校验)、危险操作(确认/限权/sandbox)、输出格式错误(校验+重试+兜底解析)。它们指向一个核心思想:把 Agent 当成"一个不可靠的执行者"来设计外围保障——模型本身是概率的、可能犯各种错的,你不能假设它"总会做对";所以要在它外面,用确定性的代码,包一层"防护网":校验它的输出、结构化地喂信息、限制它的行为、兜底它的失败。这层防护网(也叫 Agent 的"脚手架/harness"),才是把"一个会犯错的模型"变成"一个可靠的 Agent"的关键工程。一句话:Agent = 模型(智能但不可靠) + 脚手架(确定性的保障和约束);工程的重点往往不在模型本身,而在那层"约束和兜底模型"的脚手架上。下面这张图,是这次 Agent 不处理工具失败的成因与解法:
第四件事:工具错误的分类与应对速查
这次踩坑后,我把工具调用可能遇到的错误类型和对应的处理策略整理成一张表。
| 错误类型 | 该不该重试 | 给模型的应对提示 |
|---|---|---|
| 超时/网络抖动 | 可重试(限次+退避) | 稍后重试一次,仍失败则告知 |
| 限流 429 | 可重试(退避后) | 等待后重试,别立即重试 |
| 数据不存在 404 | 不重试 | 告知用户未查到,别重试相同查询 |
| 参数错误 400 | 改参数后可重试 | 检查并修正参数再调,别原样重试 |
| 权限/鉴权错 | 不重试 | 无权限,告知用户或转人工 |
| 服务端 500 | 谨慎重试1次 | 下游故障,重试无果则降级 |
| 已知不可恢复 | 不重试 | 如实告知,不要编造结果 |
这张表,把"不同的错误该怎么应对"讲清了。核心区分是:"暂时性的错误"(超时、限流、500)可以谨慎重试(限次+退避),"确定性的错误"(404 不存在、400 参数错、权限错)重试相同调用毫无意义、要么改参数要么告知用户。它给我的启发是:"失败"不是一个笼统的概念,而是有不同类型、不同性质的;而正确的应对,取决于"这是哪一类失败"——是"再试一次可能就好的偶然失败",还是"再试一万次也没用的确定性失败"。我之前的 Agent 之所以"对着错误死磕",正是因为它把所有失败都当成了"再试一次可能就好"的同一类,而没有区分"这个 404 不存在的错误,重试一百次还是 404"。这让我领悟到一个处理错误的通用智慧:处理失败的第一步,是"识别失败的类型";只有先判断出"这是一个什么样的失败"(暂时的还是永久的、我方的还是对方的、可恢复的还是不可恢复的),才能选择正确的应对(重试、改参数、降级、还是放弃)。不分青红皂白地"统一处理所有错误"(比如统一重试、或统一忽略),往往就是错误处理本身做得不好的根源。
第五件事:可靠 Agent 的"脚手架"清单
这次事故让我系统梳理了一个生产级 Agent 该有的"脚手架"。我把它整理成清单。
| 脚手架 | 防的问题 | 做法 |
|---|---|---|
| 工具错误结构化 | 把错误当数据(本文) | status/retryable/hint 标注 |
| 重试熔断 | 对着错误死磕(本文) | 相同调用失败N次硬性停止 |
| 步数/预算上限 | 死循环/无限消耗 | 最大步数+token预算 |
| 参数校验 | 工具参数幻觉/缺失 | 调用前校验schema |
| 工具返回裁剪 | 上下文膨胀 | 聚合/摘要/截断 |
| 危险操作确认 | 误删/误发/乱花钱 | 高危操作要确认/限权/sandbox |
| 输出格式校验 | 格式错乱无法解析 | schema校验+重试+兜底 |
| 降级与人工兜底 | 实在完不成的任务 | 诚实告知/转人工 |
这张清单,是我用一连串 Agent 事故换来的"生产级 Agent 必备脚手架"。它把一个可靠 Agent 需要的、围绕在模型外围的确定性保障都列了出来:错误结构化、重试熔断、步数上限、参数校验、返回裁剪、危险操作确认、输出校验、降级兜底。它给我的最大启发,彻底改变了我对"做 Agent"这件事的认识:我一开始以为,"做一个强大的 Agent" = "用一个强大的模型 + 给它接上一堆强大的工具";可这一连串事故告诉我:"模型 + 工具"只是 Agent 的"能力",而决定 Agent 能否在真实世界里"可靠运行"的,是那层包裹在它们外面的、不起眼但至关重要的"脚手架"。这就像:一个再强的引擎(模型),如果没有刹车、没有安全带、没有仪表盘、没有故障保护(脚手架),也造不出一辆能安全上路的车。这让我领悟到 Agent 工程的真谛:构建可靠 Agent 的核心工作量和技术含量,很大一部分不在"选模型、写 prompt",而在"设计那层把不可靠的模型,约束、保护、兜底成可靠系统的脚手架"上;这层脚手架,是 AI 应用从"炫酷的 demo"走向"扛得住生产的产品"之间,最关键、也最考验工程功力的部分。
第六件事:给 Agent 加一个工具时,我现在的决策习惯
现在每当我给 Agent 加一个工具,我都会按这张图把"它失败了怎么办"一并设计好:
这张图的精髓,是"加工具时,把'它成功怎么用'和'它失败怎么办'一起设计"。第一步就问 "它可能怎么失败"(超时/报错/不存在/参数错),然后把结果结构化(成功带 data、失败带 status/retryable/hint)。接着按错误性质设计应对:暂时性错误标 retryable、限次重试+退避;确定性错误标不可重试、给改参数或告知的 hint。再加两层兜底:代码层对相同调用失败 N 次硬熔断、系统提示教模型"error 别当数据按 status 应对"、整体设步数上限+降级。最后一步是我现在的硬习惯:测试时故意让工具失败,看 Agent 会不会崩/编/死磕(这次的坑正是因为测试时所有工具都成功、从没测过失败路径)。这套习惯,让我加工具时,从"只想它成功"变成了"成功失败一起设计"——核心始终是:工具失败是常态,加工具必须同时设计好它失败时 Agent 该如何识别和应对。
我立下的几条规矩
这场"Agent 不会处理工具失败"的事故,换来了我做 Agent 时,刻进骨子里的几条铁律:
- 工具调用失败是常态,不是例外。真实世界里超时/报错/不存在经常发生,必须设计应对。
- 别把异常信息直接当结果塞回上下文。模型分不清错误和数据,会把报错当数据用。
- 工具结果要结构化。用 status/retryable/hint 让模型明确识别成功还是失败、该怎么办。
- 在代码层硬性限制重试。相同调用失败 N 次就熔断,别只靠模型自觉。
- 区分错误类型。暂时性错误可重试,确定性错误(404/400/权限)重试无意义。
- 设步数/预算上限 + 降级。别让 Agent 无限消耗,完不成就诚实告知/转人工。
- 测试要覆盖失败路径。故意让工具失败,验证 Agent 不崩、不编、不死磕。
附:一个带错误处理脚手架的工具执行器
口说无凭。下面把"结构化错误 + 重试熔断 + 参数校验 + 步数兜底"合到一个可复用的工具执行器里:
import json, time
class ResilientToolRunner:
def __init__(self, max_same_call_failures=2):
self.fail_count = {} # 调用指纹 -> 失败次数
self.max_fail = max_same_call_failures
def _fingerprint(self, call):
return f"{call.name}:{json.dumps(call.args, sort_keys=True)}"
def run(self, call) -> dict:
fp = self._fingerprint(call)
# 1. 熔断: 相同调用已失败太多次, 直接拦截(防死磕)
if self.fail_count.get(fp, 0) >= self.max_fail:
return {"status": "error", "error_type": "circuit_open",
"message": "该调用已多次失败, 已停止重试",
"retryable": False, "hint": "请换一种方式或如实告知用户无法完成"}
# 2. 参数校验: 调用前先校验参数(防参数幻觉)
err = self._validate_args(call)
if err:
return {"status": "error", "error_type": "invalid_args",
"message": err, "retryable": True,
"hint": "参数有误, 请修正参数后重试, 不要原样重试"}
# 3. 执行 + 把各类异常映射成结构化错误
try:
data = self._execute(call)
self.fail_count.pop(fp, None) # 成功了, 清除失败计数
return {"status": "success", "data": self._truncate(data)} # 顺便裁剪返回
except TimeoutError:
return self._record_fail(fp, "timeout", "调用超时", True,
"下游繁忙, 可稍后重试一次或换工具")
except FileNotFoundError:
return self._record_fail(fp, "not_found", "资源不存在", False,
"目标不存在, 请告知用户未找到, 勿重试")
except PermissionError:
return self._record_fail(fp, "forbidden", "无权限", False,
"无权访问, 请转人工或告知用户")
except Exception as e:
return self._record_fail(fp, "unknown", str(e), False,
"执行出错, 请如实告知用户, 不要编造结果")
def _record_fail(self, fp, etype, msg, retryable, hint):
self.fail_count[fp] = self.fail_count.get(fp, 0) + 1
return {"status": "error", "error_type": etype, "message": msg,
"retryable": retryable, "hint": hint}
def _truncate(self, data, limit=2000): # 防上下文膨胀
text = str(data)
return text if len(text) <= limit else text[:limit] + "...[已截断]"
def _validate_args(self, call): ... # 按工具 schema 校验, 返回错误信息或None
def _execute(self, call): ... # 真正执行工具
# → 一个执行器, 兜住了: 结构化错误 + 重试熔断 + 参数校验 + 返回裁剪。
# Agent 主循环只管调 runner.run(call), 所有"工具可能出的岔子"都被它接住了。
# 核心: 把"结构化错误+相同调用熔断+参数校验+返回裁剪"封装进ResilientToolRunner,
# 让Agent的每次工具调用都自动获得健壮性保障; 这就是"脚手架"包裹"不可靠模型"的实践。
这个 ResilientToolRunner,把这篇文章的核心解法,落成了一个可以包住所有工具调用的"脚手架"。它在一个 run 方法里,层层兜住了工具调用可能出的各种岔子:熔断(相同调用失败太多次就拦截,防死磕)、参数校验(调用前校验,防参数幻觉)、结构化错误映射(把各类异常翻译成带 status/retryable/hint 的结构化错误,让模型识别)、返回裁剪(防上下文膨胀)、成功后清除失败计数。这样,Agent 的主循环只管调 runner.run(call),所有"工具可能出的岔子"都被这一层接住了,业务代码不用在每个工具里重复处理这些。这,正是我想用这个执行器,留给每个做 Agent 的人的最后一课,也是对前面那个核心思想最具体的诠释:所谓"脚手架(harness)",不是什么玄乎的概念,它就是这样一层用确定性代码写成的、把"模型/工具可能犯的各种错"统一接住并妥善处理的"防护层"。它的价值在于:把"健壮性"从"依赖每个工具、每次调用都正确处理错误"(不可靠),变成了"一处封装、处处自动获得"(可靠)。当你的 Agent 框架里有了这样一层扎实的脚手架,你就可以放心地给它接入各种工具、放心地让模型去自由发挥——因为你知道,无论模型怎么犯错、工具怎么失败,都有这层防护网在下面稳稳地接住。给聪明但不可靠的模型,配一套可靠的脚手架——这,是我这一系列 Agent 事故复盘里,最想传递的、关于"如何把 AI 真正用好、用稳"的核心心法。
写在最后
回头看,这场由"Agent 不会处理工具失败"引发的、又编又磕的事故,真正教给我的,远不止"工具错误要结构化"这一套技巧。它彻底刷新了我对"构建 Agent"这件事的认知。我最初构建 Agent 的思路,几乎全部聚焦在"让它在理想情况下,把事情做对"——我精心设计 prompt、挑选工具、调试它"顺利完成任务"的样子,并为它的聪明而欣喜。可这次事故狠狠地告诉我:一个 Agent 在"顺利路径"上表现得多漂亮,和它能不能在"真实世界"里可靠地工作,是两件几乎无关的事。因为真实世界,充满了"不顺利":工具会失败、网络会抖、数据会缺、模型会犯错;而一个 Agent 的可靠性,恰恰不取决于它"顺利时有多聪明",而取决于它"不顺利时有多稳健"。这让我领悟到一个适用于一切系统(尤其是 AI 系统)的深刻道理:衡量一个系统的成熟度,关键看它的"异常路径"设计得有多周全,而不是它的"正常路径"跑得有多漂亮;"正常路径"决定了它"能不能用",而"异常路径"决定了它"敢不敢用、能不能扛住真实世界"。尤其对 Agent 这种"核心是一个会犯错的概率模型"的系统来说,"处理各种不顺利"(失败、错误、异常、边界)的工程量,往往远大于"实现核心功能"本身;而这部分工作,正是把"一个能演示的 Agent"和"一个能交付的 Agent"区分开来的、真正的分水岭。从"关注它顺利时多聪明"到"关注它不顺利时多稳健"——这,是我用一次"Agent 又编又磕"的事故,换来的、关于 AI Agent、也关于"系统成熟度看异常路径"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次给 Agent 加工具时,顺手想一想"它失败了,Agent 会怎样?",那我对着那些"基于报错编答案、对着错误死磕"的日志熬的这大半天,就值了。
—— 别看了 · 2026