Error Recovery

健壮的 Harness 对失败分类,决定哪种重试值得做。

错误分类 + 退避重试 + 反馈给模型。

75 行重试策略

问题

真实世界里一切都会失败:API 限流、网络抖动、工具超时、命令退出码非零、JSON 解析失败。

s01 的循环对失败毫无防御:fetch 抛异常 → 进程崩溃;工具输出乱码 → 模型得到垃圾输入;模型卡在死循环 → 烧 token。

失败是常态,不是异常。 健壮的 Harness 需要:先分类失败(可重试?可恢复?致命?),再决定动作(重试?反馈给模型让它改?终止?)。

解决方案

错误分类表 + 动作决策:

错误类型 例子 动作
rate_limit 429 退避重试(等 2s、4s、8s…)
transient 网络超时、5xx 有限重试
tool_failure 命令退出非零 不重试,把错误文本喂回模型,让它自己修正
fatal 认证失败 401 终止并报告
function classify(err) {
  if (err.status === 429) return "rate_limit";
  if (err.status === 401) return "fatal";
  if (err.status >= 500 || err.type === "network") return "transient";
  return "tool_failure";
}

async function withRetry(fn, { max = 3 } = {}) {
  let delay = 1000;
  for (let attempt = 1; ; attempt++) {
    try { return await fn(); }
    catch (err) {
      const kind = classify(err);
      if (kind === "fatal" || attempt >= max) throw err;
      if (kind === "tool_failure") throw err; // 工具失败不自动重试
      await sleep(delay * Math.pow(2, attempt - 1)); // 指数退避
    }
  }
}

工作原理

第 1 步:模型请求包上退避重试(rate_limit / transient 值得自动重试——服务器可能刚好恢复)。

const data = await withRetry(() =>
  fetch(API, { method: "POST", /* ... */ }).then((r) => { if (!r.ok) throw Object.assign(new Error(r.statusText), { status: r.status }); return r.json(); })
);

第 2 步:工具执行失败不自动重试——喂回模型。模型是唯一能"换个做法"的实体:

try {
  output = await execute(name, args);
} catch (e) {
  output = `TOOL_ERROR: ${e.message}`; // 作为 tool 结果回传,循环继续
}

第 3 步:步骤上限——模型陷入循环时,harness 必须能喊停。这属于"防失控",与重试正交但同属错误恢复:

let steps = 0;
while (true) {
  if (++steps > MAX_STEPS) return "(达到最大步数,停止)";
  // ...
}

重试哲学:自动重试只用于"环境暂时不好"(限流、网络);"模型做错了"永远不自动重试,而是把错误反馈给它——模型从错误中学习下一步。把这两类混为一谈,就会在错误的命令上重试 3 次浪费 token。

试一下

运行 s11,制造各种失败:

  • 给一个会失败的命令(cat 不存在的文件)——观察错误如何喂回模型,模型如何修正
  • 把 API 地址改成错误端口——观察重试与退避
  • 给一个永远执行不完的循环任务——观察步数上限

观察重点:模型收到 TOOL_ERROR 后是修正还是重蹈覆辙?rate_limit 的退避节奏是否合理?

以下内容基于 packages/guardpackages/llm 的核查。DSH 的错误恢复分散在几个正交的系统里:

一、守卫插件(guard)

packages/guard 是"循环卫生"插件族:

  • timeout-policy:作为部署策略给每个工具调用武装截止时间(注册 tools/execute 监听者)。工具超时不是工具自己的事——是策略在管道上施加的预算。
  • repeat-tool-reminder:监测重复工具调用,以 additionalContexts 附加提醒("你刚跑过这个命令")——不是硬拦截,是顾问式的。这对应你的"防失控",但用提醒而非硬性中止。

二、LLM 请求重试

packages/llm 提供按路由的重试策略dsh-llm-retry):不同模型/路由可以配置不同重试次数与退避——429/超时/5xx 的分类和退避是产品策略,不是写死在循环里。

三、你的分类表 vs DSH

你的最小实现 DSH 的真实实现
classify() 在循环里 分类与策略是配置/插件:超时在 timeout-policy,重试在 llm-retry,提醒在 repeat-tool-reminder
步数上限写死 turn 上限、token 预算等多条退出路径(s01 已见)
失败反馈给模型靠手写 工具结果管道保证错误文本结构化回传

一句话:错误恢复不是循环的一个分支,而是一系列环绕循环的策略插件——这正是 s04 钩子思想的延续:横切行为住在循环外面。