本节摘要:工具一定会失败:文件不存在、命令非零退出、网络超时、权限被拒。失败本身不可怕,可怕的是失败被翻译成"无信息"的消息——
Error: something went wrong对模型是一堵墙,它只能瞎猜重试;而一条好错误是写给模型的接口,能让它下一步就走上正确的路。本节给出错误反馈的四要素(类型、原因、线索、下一步)与结构化返回格式;一张错误分类表区分用户错、环境错、权限拒、系统错各自该反馈什么、要不要自动重试;以及重试纪律(哪些可重试、指数退避、上限),并接回 3.2 节的熔断器——错误反馈与熔断是同一套免疫系统的两半:前者治单次失败,后者治失败复利。
阅读完本节,你应当能够:
对比同一个场景(读一个不存在的文件)的两种返回:
// 坏:一堵墙——模型只能原样重试或放弃 {"error": "something went wrong"} // 好:一条路——模型知道发生了什么、为什么、下一步怎么走 {"error": { "type": "file_not_found", "message": "文件不存在:src/util.py", "hint": "相近路径:src/utils.py、lib/util.py(本仓库存在)", "next": "用 glob 工具确认路径,或改读 src/utils.py" }}
四要素拆解:类型(机器可读的稳定标识,模型能跨场景学习"这类错误意味着什么");原因(人读的一句诊断);线索(本环境里与修复相关的上下文——相近路径、退出码含义、超时阈值);下一步(一个明确建议动作)。不是每次都凑满四条,但类型 + 原因是底线,线索是拉开差距的那条。
💡 判断标准只有一句话:**模型读完你的错误消息后,能说出"我下一步该做什么"吗?**能,就是好反馈;不能,就是墙。
| 类 | 例子 | 反馈要点 | 自动重试 |
|---|---|---|---|
| 用户错(调用方错) | 参数缺字段、路径不存在、类型不对 | 指出具体字段 + 纠正线索 | 否(重试同样必败) |
| 环境错 | 网络超时、磁盘满、进程被杀 | 说明环境性质 + 是否暂时 | 视情况(暂时性可重试) |
| 权限拒 | allowlist 未命中、审批被拒、被护栏拦截 | 拒绝原因 + 合法替代路径 | 否(须改变请求或请求授权) |
| 系统错(工具自身 bug) | 注册器异常、未捕获崩溃 | 坦承工具故障 + 建议换路 | 有限次 |
权限拒尤其值得单说:第 5 章的权限门拦截一次工具调用时,拦截结果将以错误反馈的形式回填给模型——这时反馈的写法直接决定模型是"绕路"还是"绕过":
// 坏:诱导绕过——模型会尝试绕开权限(换个写法再干一次) {"error": "command blocked"} // 好:说明边界 + 给合法出路 {"error": { "type": "permission_denied", "message": "rm 被策略禁止:本 harness 不允许删除工作区外文件", "hint": "若要清理构建产物,可用受控工具 clean_build,其作用域已限定", "next": "改用 clean_build,或向用户申请一次性授权" }}
"被拦截也是一种错误反馈"——第 5.3 节的多闸门护栏会沿用这个约定,让每一次拦截都带着出路。
让成功与失败走同一种信封,模型学会一种解读规则就能处理全部工具(registry.py 的 dispatch 已按此实现):
# envelope.py —— 统一返回信封(写法示意,以实际工程为准) import json, time def ok(result) -> str: return json.dumps({"ok": True, "result": result}, ensure_ascii=False, default=str) def err(etype: str, message: str, hint: str = "", next_: str = "") -> str: return json.dumps({"error": {"type": etype, "message": message, "hint": hint, "next": next_}}, ensure_ascii=False) def retryable(call, attempts: int = 3, base: float = 0.5) -> str: """环境错的自动重试:指数退避 + 上限;耗尽后返回结构化错误。""" for i in range(attempts): try: return ok(call()) except TransientError as exc: # 只重试"暂时性"错误 time.sleep(base * 2 ** i) except Exception as exc: # 其余立即返回错误 return err(type(exc).__name__, str(exc)) return err("retries_exhausted", f"重试 {attempts} 次后仍失败(暂时性错误)", next_="可将此失败报告用户,或换一种方法")
重试的纪律四条:
Breaker;连续失败触发熔断时,回填的是元反馈("连续失败,请复述目标换方法")而不是又一条原始错误。单次失败→工具级重试;失败复利→循环级熔断——两级配合,错误风暴才真正防得住。⚠️ 最危险的错误是被吞掉的错误:
except: pass让模型以为操作成功,带着假事实继续推进——错误风暴烧的是钱,吞掉的错误烧的是正确性。宁可把失败大声说出来,也别让它沉默。
工具系统完整了:契约、选择、执行、失败反馈。但 4.3 节里反复出现的"权限拒"到底怎么判定?哪些操作能自动做,哪些必须问人?下一章进入 harness 的价值观所在——权限与审批门。
延伸阅读:同站教程库《strix》。