Code Agent Harness容错设计:从超时控制到断点恢复
2026/9/5 10:06:46 网站建设 项目流程

写在前面:为什么“能跑”和“扛得住”是两码事

聊到 Code Agent 的 Harness 设计,前面几篇我们分别拆过上下文管理、工具调用编排和状态机流转。今天这篇是系列的第四篇,主题是容错与恢复。为什么要把这个单独拎出来写?因为我见过太多 demo 阶段惊艳全场、一上生产就原形毕露的 Agent 项目。问题几乎都出在同一个地方:只考虑了“正常路径”,没考虑“世界会以各种姿势出bug”。

在聊容错之前,先明确一个概念:这里的 Harness,指的是包在 Agent 核心推理循环外面的那一层“脚手架”。它不是模型本身,而是负责把模型接进真实环境的那套基础设施。比如你用什么方式管理上下文、怎么调用工具、怎么解析输出、怎么把一个任务的中间状态保存下来。容错与恢复,就是这套基础设施里负责“事情搞砸了怎么办”的机制。

真实的业务场景里,大模型 API 会超时、第三方工具会有随机限流、用户会在一句话里塞五个互相矛盾的需求、上下文中途会被截断。任何一个问题处理不好,整个 Agent 就白干了。这篇文章我会结合自己做过的几个项目,把容错与恢复这件事掰开揉碎,讲清楚每一层该做什么、不该做什么,以及踩过哪些坑。

1. 容错设计的基本盘:先搞清楚“错”从哪里来

1.1 故障源全景:不止“模型抽风”这一种

很多人一想到 Agent 出问题,第一反应就是“模型答错了”。其实模型答错只是故障冰山的一角。我在实际项目里总结过,故障来源大致分四类:

第一类是基础设施故障。网络闪断、API 限流、服务端 5xx、本地磁盘满了导致缓存写不进去。这类故障的特点是:你完全控制不了,只能通过超时、重试、退避策略去缓解。

第二类是工具链故障。比如 Agent 调了个内部 API,结果参数格式不对、鉴权过期、下游服务返回了非 JSON 的结构。更常见的是某些外部服务只管限流不管体验,一分钟内调用超过 10 次就直接把你封了。

第三类是解析故障。这一步特别容易被新手忽略——模型输出内容本身可能完全正确,但格式不对。你要求它输出 JSON,它偏偏前后都包了 markdown 代码块;你要求它调用某个工具,它把参数名写成了近似值。所有依赖模型输出严格格式的 Harness,都可能在这一步挂掉。

第四类是逻辑故障。模型“自信地”选错了工具、算错了参数、对一个明显超出自身能力的问题强行给出了结果。这种最棘手,因为 API 没报错、格式也合规,但结果就是不对。

四类故障的处理策略完全不同。基础设施故障靠退避重试;工具链故障靠参数校验和降级方案;解析故障靠容错解析器和规则约束;逻辑故障靠的是后面要讲的“置信度校验 + 人工介入兜底”。如果不对故障进行分类,就容易用一种方案去处理所有问题,结果什么都没处理好。

1.2 容错的三个层次与其对应的设计目标

根据故障发生在哪个环节,容错设计可以分三个层次,每一层目标不一样:

  • 交互层容错:Agent 和外部世界打交道时发生的错误。目标是把“临时性故障”和“永久性故障”尽可能区分开,临时故障就重试,永久故障就放弃并向上反馈。
  • 推理层容错:模型在生成过程/输出格式中出问题。目标是让 Harness 能“矫正”模型输出,而不是直接让整个任务失败。
  • 任务层容错:整个多步任务执行到一半失败了。目标是能不能从断点恢复,而不是从头再来。

这个分层视角特别重要。如果你把“模型 API 超时”和“模型生成了非法 JSON”都当成“异常”,用同一套 try-catch 处理,那你的 Harness 天花板就很低。实际设计时,这三层应该分别维护各自的错误类型、处理策略和恢复机制。

2. Harness 容错的核心机制设计

2.1 超时控制:所有容错的“第一道闸门”

绝大多数线上故障的起点,都是“某个环节卡住了”。模型 API 没有返回、外部工具 socket 挂起、本地代码执行死循环。如果没有超时控制,整个 Agent 会无限期阻塞,不仅拖垮当前任务,还会积压后续请求,最后把整个系统搞崩。

不要只给 API 调用设超时,要给“每一个可能阻塞的环节”都设超时。我在一个体验过线上事故的项目里吃过这个亏:只给模型 API 设了 60 秒超时,没给工具调用设,结果工具请求第三方接口时因为网络问题一直挂着,一个任务 20 分钟没结束,连取消都做不到。

我后来定的这套超时体系基本没再出大问题:

环节超时值超时后的处理
模型调用首 token 延迟30s重试1次,换备用模型
模型完整响应5-10分钟(视任务复杂度)记录超时点,任务转入断点恢复
工具调用20s重试2次(指数退避)后标记失败
本地代码执行依据用户提示判断,默认60s强制终止进程并返回 stderr
循环总时长30分钟强制终止,保存上下文供复盘

超时值最忌讳拍脑袋。建议你把这四个数据都记下来:API 响应时间 P95、工具调用耗时分布、历史最大 token 生成耗时、排队等待时间。然后按“P95 的两倍再加一点缓冲”来设置。宁可超时后重试几次,也比卡住不恢复好——用户能接受慢,但接受不了没反应。

重试也不是无脑重试。指数退避是默认规则:first_retry 是 1s,后面翻倍:1s、2s、4s、8s……上限控制在 10 次以内。同时要加 jitter(抖动),把实际等待时间在理论值上随机偏移 ±20%。因为如果同一时刻大批请求都在等同一个故障服务,大家退避节奏一模一样,恢复后又会同时飞出去,把服务再打挂一次。这就叫重试风暴。所有做分布式系统的人都知道这个坑,做 Agent 的 Harness 也要懂。

2.2 错误分类与重试策略:别把“救不活的”反复救

我见过有的 Harness 把所有异常一视同仁,一律重试三次。这带来的问题是:一个因为“参数类型错误”导致必定失败的调用,被反复重试了三次,白白等了好几分钟,才进入失败分支。而一个因为“API 限流”的调用,可能在第三次重试时恰好限流解除,能成功。

所以错误分类要非常细致。我的做法是给每个错误打上标签:

  • retryable=true:链路超时、5xx、限流(429)、网络抖动。这些重试能救活。
  • retryable=false:参数校验失败、鉴权 401/403、资源不存在 404、JSON 解析失败。这些重试多少次都一样。
  • degradable=true:说明有替代方案可以绕过。比如主模型挂了,可以用备用模型顶上;主工具挂了,可以走 fallback 接口。

这里有一个特别容易忽略的教训:429 限流也分“可恢复”和“不可恢复”。很多 API 返回 429 时会带一个Retry-After头,告诉你要等多久。如果你不管这个字段,直接套用默认的指数退避去重试,很可能在服务器要求的时间之前就冲过去了,结果第二次 429,白等。正确做法是:优先读取Retry-After,如果大于你的最大容忍时间,就不要重试了,直接降级或放弃。

有人可能在纠结 429 到底是 HTTP 层的错误还是工具层的错误。这不是重点,重点是你的设计里必须有这么一层“错误分类器”,并且能被调用链里任何一个环节复用。

2.3 状态快照与原子化恢复:让任务“从断点继续”

多步 Agent 任务最怕的是:做到第 5 步时挂掉,结果第 1 步到第 4 步的结果全丢了,必须从头来。这不仅浪费钱(每次调用都烧 token),还拖垮了用户体验。

我的做法是把“用户会话状态”和“任务执行中间态”分开保存。用户会话状态包括用户原始输入、历史上下文、偏好设置;任务执行中间态包括已经完成的工具调用及其结果、当前所处步骤、已生成的中间产物。每一轮 Agent 执行完毕后,就把这些内容序列化成一个 JSON 快照,存入 Redis(或任意 KV 存储),key 是agent_session:{session_id},TTL 设成 24 小时。快照里还要记录一个heartbeat字段用于后续展示进度。

恢复流程是这样的:

  1. Agent 进程收到新的用户输入。
  2. 先查 Redis 中是否有未完成的任务快照。
  3. 如果有,把快照反序列化,重建上下文管理器。
  4. 判断快照中记录的最后状态(例如“正在执行工具A”还是“等待模型决定下一步”)。
  5. 根据状态决定是恢复执行工具 A 还是跳过直接进入模型推理。

这里要特别留心一个“副作用一致性”问题。如果第 5 步是“向用户邮箱发送了一封邮件”,结果发送成功了,但还没来得及保存快照进程就崩了,恢复时如果重发一遍,用户就会收到两封邮件。解决办法是:给每个工具调用生成一个全局唯一的tool_call_id,在工具调用前先写入“调用记录”,工具执行成功后再更新“调用结果”。恢复时看到调用记录里没有结果,就先查一下这是不是个幂等操作(可以安全重放),或者直接标记为“结果未知”,让人工介入确认。

这就是为什么说要给工具定义“副作用等级”:只读操作可以放心重放;写操作最好做成幂等的;不可逆操作(比如“删除服务器”)绝对不能自动重放。

2.4 上下文窗口溢出的“软恢复”:比直接截断优雅得多

上下文溢出是 Agent 特有的“内存不够”问题,也可以看作一种特殊故障。很多人的第一反应是“直接丢掉最早的对话”,但代价常常是模型失去了前置信息,逻辑开始错乱。更糟的是,早期对话里可能还藏着用户明确表达过的工作偏好,丢了就再也回不来了。

我建议用“分层摘要”机制去缓解。当上下文占用接近阈值(比如达到上限的 75%)时,Harness 触发后台任务:把最早的一部分消息用模型压缩成几百字的摘要,保留关键信息(用户的原始目标、已经确认的约束、涉及的关键实体),丢弃细节对话。摘要生成后替换掉原始消息,把上下文腾出来。

要注意的是,摘要过程本身也会消耗 token,而且如果设计不好,摘要生成的耗时会让用户等待很久。我的经验是:把摘要任务设计成异步的,不阻塞主流程;同时处理完之后要人工做一次“摘要质量抽检”,否则模型可能会在摘要里丢掉关键信息,后面任务就越跑越偏。顺带一提,摘要对比较早的环节尤其重要,越早越容易丢细节。

如果上下文已经满了怎么办?这时 Harness 要发出一个状态通知给用户,而不是静默截断。比如“检测到上下文已满,系统自动精简了之前的对话,某些细节可能丢失。你可以补充这些关键信息:……”。这样既避免了硬截断,也让用户有了重新明确需求的入口。

3. 实操:把容错逻辑落地到 Harness 代码

3.1 一个轻量级的错误分类器和重试器

这一节我直接给一套可用的设计思路,基于 TypeScript 风格的伪代码,重点看结构,不要纠结具体实现。

错误分类器的核心是维护一个“错误 → 策略”的映射表。我用的是装饰器模式,给每个工具函数标上元数据:这个工具可不可以重试、有没有降级方案、副作用等级是什么。然后在一个统一的execute_with_resilience函数里包一层。

type ErrorPolicy = { retryable: boolean; maxRetries: number; baseDelayMs: number; degradable: boolean; fallback?: () => Promise<any>; }; const policyRegistry = new Map<string, ErrorPolicy>(); function registerToolPolicy(toolName: string, policy: ErrorPolicy) { policyRegistry.set(toolName, policy); } // 统一执行入口:所有的工具调用都走这里 async function executeWithResilience(toolName: string, args: any) { const policy = policyRegistry.get(toolName) ?? { retryable: true, maxRetries: 2, baseDelayMs: 1000, degradable: false }; let attempts = 0; let lastError: any; while (attempts <= policy.maxRetries) { try { // 真正调用工具 return await toolExecutor.execute(toolName, args); } catch (e) { lastError = e; const retryable = isRetryableError(e) && policy.retryable; if (!retryable || attempts === policy.maxRetries) { // 不可重试或已重试耗尽,判断能否降级 if (policy.degradable && policy.fallback) { return await policy.fallback(); } throw lastError; } // 指数退避 + jitter const delayMs = policy.baseDelayMs * Math.pow(2, attempts) + randomJitter(); await sleep(delayMs); attempts++; } } } function isRetryableError(e: any): boolean { // 5xx / 429 / 网络层 ECONNRESET / ETIMEDOUT 都返回 true // 4xx(非429)、解析类错误返回 false const retryableStatus = [408, 425, 429, 500, 502, 503, 504]; if (e?.status && retryableStatus.includes(e.status)) return true; if (e?.code === 'ECONNRESET' || e?.code === 'ETIMEDOUT') return true; return false; }

写这段代码的核心意图是:把“能不能重试”这个决策,从业务逻辑里剥离出来。业务代码只需要关心“这一步该调什么工具”,容错策略统一由 Harness 层接管。这样新增工具时,只需要注册策略,不需要改调用链。

3.2 断点续跑机制的实现要点:用事件溯源保存“执行到哪一步”

快照恢复听上去简单,但真正落地时最麻烦的是“记录哪些数据”。只存一个“进度百分比”没有任何价值;只存对话历史也没法恢复工具执行状态。我采用的是“事件溯源”的思路:把整个 Agent 执行过程看成一系列不可变的事件流,Harness 只需要 append,不需要修改历史。

事件流里的事件类型包括:

  • user_message_received: 记录用户 ID、输入内容、时间戳。
  • context_snapshot_taken: 记录当前 token 数、消息摘要、关键状态。
  • tool_call_started: 记录tool_call_id、工具名、参数、开始时间。
  • tool_call_succeeded: 记录工具名、输出摘要(或全文)、耗时。
  • tool_call_failed: 记录失败原因、错误类型。
  • model_inference: 记录模型请求参数、输出内容、token 使用情况。
  • task_finished: 记录最终输出结果。

所有事件追加写入一个事件表(Postgres 或 MongoDB 都能胜任)。快照只是“某个时间点所有事件的累计视图”。恢复时,只需要扫描事件表,找到最后一个成功的事件,从那个事件的下一个动作开始尝试续跑。

这个设计最爽的地方是:天然具备审计能力。即使不需要容错,这套事件流日志也可以用来调试 prompt、分析模型行为、统计工具成功率。

有一点要提醒:事件流日志在数据量大了以后会非常占空间。我的策略是:保留最近 2 天全量事件,超过 2 天的聚合为“任务级摘要”,只保存最终结果和头部几个关键事件。这样不至于事后复盘时发现数据被清得干干净净。

3.3 从零搭建 Harness 恢复流程的四个步骤

第一步:定义状态结构。确定存储模型——用 JSON blob 还是事件表,状态里放哪些字段。建议至少包含:session_id、user_id、task_context、tool_logs、last_event_type、created_at、updated_at。

第二步:实现快照的原子写入。每次状态变更,必须在一个事务里完成“写事件流 + 更新状态视图”。要么都成功,要么都失败。避免出现事件记录和状态不一致的情况。

第三步:实现恢复入口。在 Agent 启动和用户回复触发时,检查有没有进行中的任务。有就提示用户“检测到一个未完成的任务,是否继续”,由用户确认。不要自作主张地恢复——有些场景用户就是不想继续了。

第四步:定期做“故障演练”。具体做法是:用一个随机数生成器,在任务执行过程中随机中断进程,然后检查恢复流程能不能接上。这个测试要作为 CI 的一部分,每个新版本发布前必须跑一遍。不演练的容错机制,等于没有容错机制。

4. 上下文损坏、并发冲突与恢复后的“认知断层”

4.1 上下文不一致:比崩溃更隐蔽的故障

快照恢复有一个很隐蔽的坑:上下文不一致。假设用户在执行第 3 步时临时改了主意说“算了,不查数据库了,换个 API 吧”,但第 3 步的工具结果已经写进上下文了。恢复时如果你直接拿旧快照重建上下文,模型会发现上下文里有个工具结果,但用户的最新指令已经否定了它。这时候模型的行为不可预测——它可能忽略用户最新指令,继续顺着旧上下文走。

我的解决办法是:恢复时额外注入一条系统消息,明确指出“以下是历史上下文快照,其中有部分内容可能已被最新指令覆盖,请以用户最新指令为准。”这条消息虽小,但能显著降低模型跑偏的概率。

与之类似的问题还有“时间混乱”。如果快照是昨天的,今天恢复时模型不知道“现在”是几点,可能导致它对时效性任务的判断出错。2024 年后的大模型普遍支持在系统提示里注入当前时间,恢复时务必把“当前时间 + 快照生成时间”同时注入,让模型自己判断哪些信息过期了。

4.2 并发控制:同一会话的“单飞模式”

用户不会因为你实现了断点续跑,就只用一个设备。他可能手机开一个会话,电脑又开一个会话。如果两个会话同时触发同一个 session_id 的恢复,事件流就乱了:一个进程恢复了状态开始跑,另一个进程也恢复了同一个状态开始跑,后面的写入互相覆盖。这是分布式系统里经典的“共享状态竞争”问题。

我的方案很朴素:对同一个 session_id 加分布式锁(Redis SETNX + 过期时间)。恢复任务开始前先抢锁,抢不到就返回“该任务正在另一个会话中执行”。锁的过期时间要设成比任务最大耗时长一点点,以防进程崩溃后锁不释放,其他会话永远进不来。这个“一点点”通常设成任务超时值的 1.5 倍。记得在 finally 块里释放锁。

还可以考虑版本号机制:每次快照更新时自增version,恢复时带上 version 参数,写入时校验当前 version 是否变化。如果变了说明有并发写入冲突,直接中止并通知用户。但实际工作中,加锁已经够用了,版本号机制适合更复杂的协作场景。

4.3 静默降级 vs 显式降级:如何向用户交代

容错机制经常导致一个“看不见的副作用”:Harness 为了能完成任务,悄悄用了一个替代方案,但用户并不知道。比如主向量数据库挂了,Harness 自动切到了备用库,结果搜索结果质量下降,模型给出的答案开始变得离谱。用户不知道原因,只感觉“怎么突然智商变低了”。

这是特别常见的“静默降级”陷阱。我后来定了一条规则:凡是发生了“非用户指令触发的方案切换”,都必须生成一条显式提示。可选的范围很小,但至少要让用户感知到异常。比如在回复里加一句:“由于主数据源暂时不可用,当前回答基于备用数据源,部分内容可能不完整。”

显式降级还有一层好处:用户可能会因为反馈“那就算了,不查了”,你就能提前终止任务,省下时间。如果一直闷头降级到完成,用户看到的可能是一个很不可靠的结果,信任感会大打折扣。

5. 纵深防御:把“最后一次重试”也不能出的问题也兜住

5.1 全局熔断器:防止“重试”把系统打成雪崩

前面讲了单次工具调用的重试策略,但如果整个下游服务都在挂,重试并不能解决问题。你需要一个“熔断器”,在连续失败达到阈值时,快速放行到降级路径,而不是继续重试。

我用的是标准熔断器三态模型:

  • 关闭态(正常):所有请求正常放行,统计失败率。
  • 打开态(故障短路):连续失败次数 > 阈值(比如 5 次),或失败率 > 50%(近 10 次窗口),后续请求直接走降级逻辑,不真正发起调用。
  • 半开态(试探恢复):熔断打开后等待冷却时间(比如 30s),允许 1-3 个请求通过;成功率达到阈值,熔断关闭;否则继续保持打开。

这个模式在微服务架构里已经非常成熟,但在 Agent 场景里容易被忽略。原因很简单:Agent 的一次调用会触发很多工具,开发者只盯着单个工具的重试,没有全局视角。结果是每个工具都重试了 3 次,下游服务被打了 10 倍的流量,最终把自己打挂。

熔断器还要结合 HTTP 的Retry-After做“节流”处理。如果熔断器打开时,降级方案也不可用,那 Harness 应该停止自动重试,直接进入人工上报流程。这个阈值点要小,不要贪。

5.2 “逃生舱”设计:人工接管应成为第一优先

无论 Harness 做得再好,总会有机器判断不了的情况。这时候人工接管不是“兜底”,而应该是一种被积极设计的“逃生舱”。具体来说有三种做法:

  • 阈值触发人工接管:当同一任务连续失败超过 3 次,或执行时间超过预算的 150%,Harness 自动暂停并发送通知给用户,附带当前执行摘要、已尝试的 方案列表、可能的原因判断。用户可以选择“调整后继续”“停在这里”“换一种方式重新开始”。
  • 置信度过低触发人工确认:当 Harness 对模型输出结果信心不足时(比如解析置信度 < 0.6),在交付最终答案前请求用户确认。这对“删除操作”“发送邮件”“下单”这类不可逆动作尤其重要。
  • 完全失败时的人工工单:如果任务彻底失败,Harness 要生成一个“失败报告”,包含事件时间线、关键错误栈、重试历史、上下文快照位置。用户或开发者可以直接拿着这个报告去调试,而不是对着一个泛泛的“出错了”干瞪眼。

人工接管机制听起来简单,但它改变的是 Agent 的整体设计理念:不是“尽最大可能做一个全自动的 Agent”,而是“让 Agent 知道自己的能力边界,在边界处优雅地放手”。这个思维转变,是我做容错设计收获最大的一点。

5.3 可观测性:没有日志的容错就是盲人摸象

在所有容错机制之上,还必须有一层可观测性。没有完整的日志和监控,就算实现了重试、快照、熔断,出了问题你也不知道具体哪一环出错了。

我建议的监控指标分三层:

第一层是“接口层指标”,包括重试次数、重试成功率、熔断器状态变化次数、平均恢复时间。这些指标能在长时间维度上告诉你容错机制本身是否健康。

第二层是“任务层指标”,包括任务总成功率、平均任务时长、上下文溢出触发率、人工接管触发率。任务成功率下降时,第一反应去看上下文溢出触发率是否升高——两者常常强相关。

第三层是“执行层追踪”,给每个任务分配一个trace_id,贯穿所有事件流。无论出现什么故障,都能按trace_id查询到完整的事件链。我见过太多项目,日志里每条消息孤零零的,没法串联,排查问题要开十来个窗口手动对时间戳。

这事最好从第一天就做起。后补日志和追踪的成本是前期的十倍不止,而且往往效果还是打折的。

6. 我踩过的坑和值得借鉴的经验

6.1 经验一:不要把“重试成本”和“恢复成本”混为一谈

有一个项目,我给所有工具都配了 maxRetries=5。看起来是增强了容错性,但实际效果是:一个参数写错的调用,白白重试了 5 次,每次间隔指数增长,加起来拖了整整 3 分多钟,用户早就关了页面。

后来我做了两个改动:第一,非重试类错误(4xx、参数问题、解析失败)直接零重试;第二,为重试增加“预算”——单个任务的所有重试时间加起来不能超过 30 秒。超过预算,直接进入降级或人工接管流程。

这个成本预算的思路,比单纯限制重试次数更实用。因为不同任务需要的重试次数不一样,限死次数会导致一部分任务过度重试、另一部分任务不够重试。

6.2 经验二:使用“任务编排层”而不是“单一循环”

最早我做 Agent 时,就是一个 while 循环里反复调用模型。一旦某个环节出错,整个循环就断掉,很难做恢复。

后来改用“任务编排”结构:把一次 Agent 执行拆成节点组成的图,每个节点执行完把状态交给编排器,编排器决定下一个要执行的节点。这样“断点续跑”就变成了“从图的某个节点重新开始跑”。结构上它很像工作流引擎,只不过这个工作流的每一步都可能由模型决策。

这个调整顺带解决了一个问题:工具调用之间可以有并行和分支,而不再是一条线性链。容错时可以只重放失败的子树,不用重放整个图。

6.3 经验三:用“checkpoint 频率”平衡成本与安全

快照不是越频繁越好。如果每执行一步就存一次,Redis 的写入压力会很大,而且大部分快照根本用不上(任务就正常跑完了)。如果存得太少,恢复时长会变长,丢失最近几步的状态。

我试验下来比较合适的节奏是:在三个节点强制存快照——每个“模型决策完成、即将执行工具”前、每个“工具执行成功”后、以及“一轮完整循环结束”时。这种频率下,最多丢失一次工具调用的状态,恢复成本可以接受。

另外,快照本身也要设体积上限。如果一个会话的上下文膨胀到几万 token,快照 JSON 会很大。不要全量存,可以把上下文做摘要后再存。恢复时能力会有所损失,但比什么都存不下来好。

6.4 经验四:故障演练要“随机化”

一到发布节点才想着验证容错机制,是来不及的。我在团队里做了一个“混沌小工具”,每隔一段时间随机杀掉一个 Agent 工作进程,或随机让一个下游 API 返回 500。刚开始跑的时候,每周都能抓出几个恢复流程的 bug。比如 Redis 锁没释放、事件流写入时序错了、快照里的某个字段没反序列化出来。

故障演练的一个关键是:随机性必须足够“真”。不能每次都在同一个步数杀掉进程,否则你会无意中只对某一段逻辑健壮,另一段还是脆的。让它随机分布到任务执行的不同阶段,才能暴露更多问题。

6.5 经验五:用户沟通文案也是容错的一部分

最后说一个经常被忽略的点:容错机制的用户体验,最后都落在“文案”上。错误提示写得烂,会让用户觉得系统很不可靠;写得好,反而能提升信任。

我比较推崇的文案风格是:

  • 不要只说“出错了”,要说“哪一步出了什么错”。
  • 给出下一步建议,让用户知道接下来会发生什么。
  • 不要让用户干等,提供“停止重试”“跳过此步骤”“重新生成”这类按钮。
  • 如果原因是模型服务限流或超时,可以直说“当前模型服务负载较高”,这远比“系统错误”有说服力。

把容错当成产品体验的一环,而不是纯粹的后台技术,这是我做这个系列最大的体验升级。很多用户其实不介意 Agent 出现异常,他们介意的是“出异常后毫无反馈地卡在原地”。

7. 一个可参考的 Harness 容错配置模板

为方便直接套用,我把前面提到的设计汇总成一份基础配置模板。不同项目需要微调,但这个模板可以作为起点。

配置项推荐值说明
模型调用超时首 token 30s / 完整响应 600s依据模型和任务复杂度调整
工具调用超时20s高耗时工具单独配置
重试最大次数3(可重试错误)非重试错误设为 0
重试退避策略指数退避 1s/2s/4s + jitter 20%避免重试风暴
熔断器阈值5 次失败 或 50% 失败率按服务重要程度调整
熔断器冷却时间30s半开态试探恢复
快照频率工具调用前后 + 每轮循环结束平衡成本与安全
快照 TTL24h任务不能隔天恢复就清理
上下文溢出阈值75% 触发摘要90% 强制截断并通知
人工接管条件连续失败≥3次 或 超时>150%触发后立刻通知用户
事件流保留时间全量 2 天,之后聚合摘要兼顾审计和存储成本

这个模板背后的核心思想就一句话:容错设计不是一个独立的“错误处理模块”,而是贯穿整个 Harness 所有模块的横切关注点。从上下文管理到工具调用,从状态存储到用户交互,每一层都需要考虑“如果这里出错了,该怎么恢复”。

8. 离线断网、幂等控制等高级场景实践

如果上面的内容已经能满足大多场景,下面两个更极端的情况也可以提前考虑。

8.1 离线断网环境下的任务降级

有些 Agent 会跑在用户的本地机器上,网络随时可能断开。比如笔记本电脑合盖、地铁隧道里没信号。断网会导致什么?模型 API 不可用、外部工具不可用,但本地缓存和本地代码执行仍然可用。

我的经验是做一个“离线模式”开关。检测到连续几次 API 调用失败后,自动切换为离线模式:不再发起新的模型请求,而是直接读取最近一次快照,提示用户“当前处于离线状态,我可以继续处理本地任务,但无法调用外部模型和在线工具。”如果本地有模型可以离线推理,那最好;没有的话,就把任务队列挂起,等网络恢复后由用户一键续跑。

这个设计的关键在于:不要在断网时反复重试 API(网络恢复前没意义),而是快速报出“当前不可行”,并给出可用的替代范围。处理得好,用户会觉得这个 Agent 很聪明;处理得不好,用户只会觉得它卡死了。

8.2 幂等控制的更多细节:让重放安全

前面提到过幂等,这里再往深说一点。很多工具调用天然不是幂等的,比如“发邮件”“下单”“转账”。要让重放安全,我更推荐“预检 + 提交”两步模式。

比如一个“发送验证邮件”工具。调用前先创建一个delivery_id,写入事件流,状态为 pending。真正发送时带上这个delivery_id。如果 Harness 在发送过程中崩溃,恢复后发现有一个 pending 的 delivery,不要直接重发,而是先调一个查询接口确认这个delivery_id是否已经真实发出(很多邮件服务商支持按 custom_id 查询)。如果已发出,就标记为 succeeded,不再重发;如果没发出,才真正发送。

这个模式本质上是在工具层和 Harness 层之间建立一种“两阶段提交”的语义。不是所有工具都能支持,但对那些“只执行一次”的高风险操作,非常值得。

9. 补充:关于 DeepSeek Harness 的一些使用观察

顺手回应一下评论区常问的 “DeepSeek Harness” 相关搜索。很多人可能是从 DeepSeek 相关的 Agent 项目接触到 Harness 这个词的。需要澄清的是,这里讨论的“Harness”是一个通用的 Agent 外围框架概念,并不是 DeepSeek 专属的产品。你的项目用 DeepSeek、Claude、GPT 或者本地开源模型,这一套容错设计同样成立。

从我实测的使用体验看,当前很多模型服务的超时重试策略都还比较“糙”。模型厂商不会替你保证链路的可靠性,用户侧的 Harness 必须自己扛。所以不管模型用的是哪家,围绕容错与恢复的这层设计,建议在项目初期就着手,等出了问题再补,惨痛的线上事故会让你后悔。

从安装、配置的角度来说,不同 Harness 开源实现的侧重点差异很大。有的主打本地优先,有的主打云端任务编排。评估一个 Harness 方案的容错能力时,重点看三件事:是否支持断点快照与恢复、是否内置了可配置的重试与熔断策略、是否提供了完整的事件追踪机制。三者缺一,就要慎重。

10. 最后说几点心得体会

做 Code Agent 的 Harness 设计,做到“容错与恢复”这一层时,我最大的感受是:这个领域不像写业务代码,有一套明确的正解。大部分设计决策都是在“成本”“复杂度”和“体验”之间做权衡。没有完美的容错方案,只有在你当前的约束条件下最适合的方案。

如果你只能从这篇文章里带走三个点,我的建议是:

第一,先把超时控制做对。超时是容错的基石,围绕超时的重试、退避、熔断,能根治大部分线上问题。

第二,从第一天就做事件溯源。把 Agent 的每一步执行都记录成事件,它是断点续跑的前提,也是事后复盘的一手资料。后补永远比从零做更难。

第三,不要追求“永远不挂”,要追求“挂了之后恢复得快 + 用户感知良好”。容错设计的最终目标不是消灭异常,而是管理异常。

最后再分享一个我个人的工作习惯:每次给 Harness 加一种新的容错机制时,我都会顺手写一个“模拟故障”的测试用例,放进 CI 里。哪怕当前用不上,以后有新改动时它也能自动验证恢复路径是通的。这些东西在平时看起来很“浪费”,真正出事故时,你就会发现它是你最大的底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询