243. 技术:HarnessX 第 11 课:让 Agent 识别三类模型故障,用重试、压缩和续写恢复执行

2026.06.29

·技术harnessx

总结

第 11 课讲的是一个很工程的问题:

  • 模型调用不稳定时,Agent 要从“失败就退出”,升级成“ 识别信号、有限恢复、明确停止”。

这节课没有新增工具,也没有改变 Agent Loop 的大方向。它只是在模型请求外面包了一层恢复控制

243. 技术:HarnessX 第 11 课:让 Agent 识别三类模型故障,用重试、压缩和续写恢复执行 图表 1

这张图就是这一课的核心:错误是一条运行信号,Harness 读懂信号后做有限动作。能恢复就回到正常 Agent Loop,恢复不了就明确停下

用调用下游 API 理解错误恢复 ---> 容错机制

把 HarnessX 调 DeepSeek 想成一个 Node.js 程序调用下游 API。

程序员都熟悉这种场景:本机代码没崩,业务逻辑也没走错,但下游 API 可能返回异常、限流、超时、结果不完整。

调用方不能只写 try/catch 后直接退出,它要根据返回信号决定下一步。

  • 响应成功,但业务结果被截断:
    • 这对应模型返回 finish_reason=lengthmax_tokens
    • 类似调用一个导出接口,HTTP 200 回来了,但服务端告诉你“这次只生成到一半”。
    • 第一次先把本次响应上限调大,按原请求再调一次。
    • 或者如果 仍然不够,就保存已经拿到的文本,再发一条“从中断处继续”的请求。
  • 请求体太大,下游装不下:
    • 这对应上下文超限。
    • 类似把一个过大的 JSON payload 发给接口,服务端返回 413 或明确说请求太长。
    • 这时候继续重试没有意义,要先把请求体变小。
    • HarnessX 的做法是把旧消息压成摘要,只保留摘要和最近消息,再重新请求。
  • 下游服务限流或过载:
    • 这对应 HTTP 429 或 529。
    • 类似第三方接口告诉你“请求太频繁”或“服务暂时忙”。
    • 调用方应该退避等待,逐步拉开重试间隔。
    • 连续过载时,如果配置了备用模型,就切到备用下游继续同一个任务。

这个例子要表达的重点很简单:错误恢复要比“失败后再试一次”更具体。调用方要先读下游返回的信号,再选择对应动作。

243. 技术:HarnessX 第 11 课:让 Agent 识别三类模型故障,用重试、压缩和续写恢复执行 图表 2

这一层放在系统哪里

第 11 课的改动位置很关键。错误恢复不能散落在工具里,也不应该让每个业务分支自己判断。

243. 技术:HarnessX 第 11 课:让 Agent 识别三类模型故障,用重试、压缩和续写恢复执行 图表 3

这张图里,恢复控制层只包住模型请求。

  • Agent Loop 仍然负责主流程:
    • 准备 system prompt。
    • 维护 messages。
    • 执行工具。
    • 打印最终答案。
  • 恢复控制层只负责模型请求:
    • 这次失败能不能恢复。
    • 需要等多久。
    • 要不要压缩上下文。
    • 要不要让模型接着写。
    • 什么时候停止。

这样做的好处是边界清楚。模型请求可以多试几次,但工具执行仍然只发生在完整模型消息之后。

三类信号和三种动作

这一课真正要记的是这张表。函数细节可以回源码再看。

DeepSeek 返回的信号工程含义HarnessX 的动作停止条件
finish_reason=lengthmax_tokens输出空间不够,回答被截断第一次扩大输出上限;仍然截断就保存片段并要求续写续写超过 3 次,或出现半截工具调用
413 或明确上下文超限输入太长,模型装不下做一次 reactive compact,把早期材料压成摘要压缩后仍然超限
HTTP 429 / 529服务端限流或过载等一会儿再发同一个请求;连续 529 可切备用模型重试超过 10 次

这张表比代码细节更重要。它定义了错误恢复的工程合同:

  • 哪些错误可以恢复。
  • 每种错误怎么恢复。
  • 恢复最多做几次。
  • 哪些情况必须停。

再换成图,就是三层关系:信号、动作、边界。

243. 技术:HarnessX 第 11 课:让 Agent 识别三类模型故障,用重试、压缩和续写恢复执行 图表 4

三条恢复路径

如果只看恢复决策,第 11 课可以压成这张图。这里不画回到请求模型的回边,把“重新请求”写进动作节点里,避免线条交叉。

243. 技术:HarnessX 第 11 课:让 Agent 识别三类模型故障,用重试、压缩和续写恢复执行 图表 5

输出被截断:先换大纸,再接着写

模型正常返回,也可能只返回了一半。判断点是 finish_reason

程序发请求时会给一个输出上限。回答撞到上限时,DeepSeek 会把 finish_reason 标成 length 或类似含义。

text
第一次截断:
保持原任务不变
把输出上限从默认值提高到更大值
重新请求

再次截断:
保存已经拿到的文本
追加“从中断处继续”的提示
最多续写三次

这里有一个硬边界:如果截断的是工具调用,不能执行。

原因很现实。工具调用是给本机执行的指令,参数少一个括号、少一段路径,都可能造成错误行为。文本回答可以续写,半截工具调用只能停下。

上下文超限:把旧材料压成摘要

上下文超限表示输入太长。它和输出截断是两类问题。

第 8 课已经做过上下文整理。第 11 课只是补了一层兜底:如果请求发出去后,服务端仍然明确说“装不下”,就做一次 reactive compact。

text
旧消息太多
-> 写入完整 transcript
-> 生成连续性摘要
-> 保留摘要和最近消息
-> 重新请求 DeepSeek

这条路径只处理明确的上下文超限。

text
普通 400 直接报错。

这个判断很重要。400 可能是模型名错了、参数错了、请求体错了。把所有 400 都当成上下文太长,会把真正的工程 bug 藏起来。

HTTP 429 / 529:等待后重发同一个请求

429 和 529 属于临时故障。

  • 429:
    • 请求太频繁。
  • 529:
    • 服务端过载。

这类问题一般不需要改任务,也不需要改 messages。更合理的做法是等一会儿,把同一个请求再发一次。

等待策略也不复杂:

text
先看服务端有没有给 Retry-After
有,就按服务端建议等
没有,就按 0.5 秒、1 秒、2 秒、4 秒逐步等待
每次再加一点随机错峰

连续三次遇到 529 时,如果配置了 DEEPSEEK_FALLBACK_MODEL,HarnessX 会切到备用模型继续当前请求。

这里要守住的是同一张任务状态。切模型只是一个可选动作,用户任务、messages、恢复次数都没有丢。

一次 HTTP 请求如何进入恢复流程

243. 技术:HarnessX 第 11 课:让 Agent 识别三类模型故障,用重试、压缩和续写恢复执行 图表 6

这张图要说明两个工程事实:

  • 恢复发生在模型请求层。
  • 工具调用发生在完整模型消息返回之后。

所以 429 / 529 的重试不会重复执行工具。输出截断也不会把半截工具调用交给本地系统。

怎么跑

沿用第 0 课的 npm link 和 DeepSeek 配置。

先跑正常工具循环:

bash
hx agent "读取 README.md 前 40 行,只回答当前课程列表最后一项的编号和名称"

关键输出:

bash
# 第一轮让 DeepSeek 决定读取文件
> read_file path=README.md limit=40

# 第二轮根据工具结果回答
当前课程列表最后一项是 `11`:Error Recovery 错误恢复。

再把本次进程的输出上限临时调小,真实触发输出截断:

bash
DEEPSEEK_MAX_TOKENS=64 \
DEEPSEEK_ESCALATED_MAX_TOKENS=512 \
hx agent "用至少 500 字解释当前 HarnessX 的三条恢复路径:finish_reason=length、上下文超限、HTTP 429/529。只输出正文,不调用工具"

这条命令仍然调用真实 DeepSeek。两个环境变量只缩小本次请求的输出空间。

本次实际观察到:

bash
# 64 token 不够,保持原 messages,把上限扩大到 512
[RECOVERY] output truncated max_tokens=64 -> 512

# 512 token 仍然不够,保存已有片段,再请求模型接着写
[RECOVERY] continuation=1/3

# 最终正常结束;恢复请求没有占用新的 Agent 轮次
[HOOK] Stop: turns=1, tool_calls=0, blocked=0

判断跑通的标准:

  • 能看到 output truncated
  • 能看到一次 continuation
  • 最终能得到合并后的完整文本。
  • Stop 仍然显示 turns=1

429 / 529 依赖真实服务状态,本课不造假错误。实际遇到时看恢复日志:

bash
[RECOVERY] HTTP 429 retry=1/10 wait=0.6s model=deepseek-chat
[RECOVERY] HTTP 529 retry=3/10 wait=2.4s model=deepseek-chat
[RECOVERY] fallback model=备用模型名

这一课真正要记住

这节课的重点是工程思维

  • 错误要分类。
    • 输出不够、输入太长、服务临时繁忙,是三件不同的事。
  • 恢复要有限。
    • 没有上限的重试会把故障扩大。
  • 状态要属于当前任务。
    • 当前模型、输出上限、续写次数、连续 529 次数,都只服务这一次 hx agent
  • 工具执行要保守。
    • 只有完整模型消息才能进入工具分发。
  • 普通错误要快速暴露。
    • 参数错、模型名错、请求体错,不应该被压缩或重试掩盖。

一句话收束:

text
Error Recovery 是一层工程保险:读懂模型故障信号,做有限恢复,成功就回到 Agent Loop,失败就明确停下。

源码

源码只看主链路。这里不追每个分支的具体写法,只看错误恢复怎样接入 Agent Loop。

源码大流程图

243. 技术:HarnessX 第 11 课:让 Agent 识别三类模型故障,用重试、压缩和续写恢复执行 图表 7

代码概览

src/deepseek.js 只做一件关键事:把 DeepSeek 的返回信号保留下来。

js
// HTTP 失败时,不只抛一句字符串。
// 上层要拿 status、body、Retry-After 判断能不能恢复。
if (!response.ok) {
  throw new DeepSeekApiError(message, { status, body, retryAfterMs });
}

// HTTP 成功时,也要保留 finish_reason。
// 这决定回答是正常结束,还是因为输出上限被截断。
return { ...choice.message, finish_reason };

src/agent-loop.js 把一次模型调用交给恢复控制层。

js
// 每个 hx agent 任务都有自己的恢复状态。
const recovery = createRecoveryState();

const message = await callMainModelWithRecovery(
  messages,
  stats,
  recovery,
  {
    tools: TOOL_DEFINITIONS,
    tool_choice: "auto",
  },
);

// 恢复控制层返回后,才按原来的 Agent Loop 继续。
messages.push(normalizeAssistantMessage(message));

代码细分

模型请求的外层逻辑可以缩成这段伪代码:

js
async function requestModelWithRecovery(messages, recovery) {
  while (true) {
    try {
      // 这里面处理 429 / 529:
      // 等待、错峰、必要时切换备用模型。
      const message = await callWithTransientRetry(messages, recovery);

      if (输出被截断(message)) {
        // 第一次扩大输出上限。
        // 后续保存文本片段并让模型接着写。
        // 半截工具调用直接报错。
        处理输出恢复();
        continue;
      }

      // 到这里说明拿到了完整模型消息。
      return 合并续写片段(message);
    } catch (error) {
      if (明确是上下文超限(error)) {
        // 沿用第 8 课的 reactive compact,只做一次。
        压缩上下文();
        continue;
      }

      // 其他错误快速失败,避免掩盖真实 bug。
      throw error;
    }
  }
}

HTTP 请求和返回在这一课只需要看这几个字段:

text
// 发出去:任务、工具、当前输出上限、当前模型。
POST /chat/completions
{
  model,
  messages,
  tools,
  max_tokens
}

// 成功回来:看 message,也看 finish_reason。
choices[0].message
choices[0].finish_reason

// 失败回来:看 HTTP status,也看服务端有没有建议等待多久。
status
body
Retry-After

这就是第 11 课的源码主线:传输层保留真实信号,Agent 层按有限状态恢复,恢复成功后再回到原来的工具循环。