← 全部工程笔记Cognitive OS 系列 / 06
可靠性与实测第 06 / 08 篇

从“回答完成”到“任务完成”:Agent 的状态、证据与故障恢复

建立完成证据契约,拆开取消、超时、中断恢复与幂等,说明状态误判的原因和验证方法。

文章目录

一个 Agent 产品最影响信任的地方,往往不是答得不够流畅,而是状态与事实不一致:工作没做却显示完成,或者答案已经完整却显示失败。

这是 Cognitive OS 技术系列第 6 篇。可靠性没有一个开关,需要在多个边界持续校验。

这篇文章要解决什么问题?

本文围绕“从完成契约理解可靠执行”展开。阅读顺序是:成功如何定义 → 状态如何记录 → 中断如何解释 → 重试如何约束 → 如何用故障验证。先建立概念与执行顺序,再进入具体函数、异常分支和测试,避免把模块名当成理解系统的起点。

阅读阶段 要回答的问题
任务完成需要满足哪些证据契约? 先定义成功,再定义验证器失效时怎么办;运行结束与业务交付需要分别表达。
状态与证据怎样贯穿一次执行? 把规划、执行、持久化和界面放在同一时间线上,避免总结文本覆盖真实错误。
取消、超时和重启应如何恢复? 先识别中断类型与结果是否已知,再决定继续查询、人工确认或恢复执行。
何时可以重试,如何处理副作用? 重试需要可识别的输入和副作用边界;事务、幂等和审计分别解决不同问题。
怎样设计故障演练与最小回归? 从停止点重建事故时间线,使用相邻反例检验修复是否伤及正常任务。
如何衡量可靠性并沉淀改进? 用误报成功、漏报成功、未知结果与重复副作用评价系统,再将事故转成可维护的契约。

文中的源码行为以原文标注的 cca98b6 为阅读基线;涉及“应该”“可以”或后续演进的内容属于设计分析与验证建议。历史基准和故障记录不代表当前版本每次运行都会产生相同结果。

1. 任务完成需要满足哪些证据契约?

先定义成功,再定义验证器失效时怎么办;运行结束与业务交付需要分别表达。

贯穿案例:超时发生后,为什么不能马上重试?

设工具向外部系统提交了一个带副作用的请求,客户端却在收到响应前超时。此时至少存在两种可能:请求未到达;请求已生效,只是响应丢失。客户端观察到的“超时”无法独自区分它们。

已掌握的事实 合理的下一步 需要避免的误判
明确没有进入执行 修复输入或按策略重试 把接纳失败当成业务成功
外部状态可按业务标识查询 先查询并核对目标结果 因为没收到响应就重复创建
支持稳定的幂等标识 使用同一标识重试并检查结果 每次重试换标识,失去去重依据
已执行,但后续步骤失败 记录部分结果并决定补偿 将整项任务简单重跑
无法确认外部结果 保留未知状态,提供进一步确认路径 直接写成成功或失败,掩盖不确定性

这里讨论的是恢复决策,不表示项目已经为任意外部工具提供通用去重保证。是否可查询、能否幂等以及补偿范围,都需要针对工具协议确认。

可靠性设计首先要保留这份区别:请求目标是什么,执行到了哪里,哪些副作用已证实,哪些仍未知。然后才是重试次数、退避时间或重启策略。后文先定义完成契约和状态记录,再讨论中断恢复与幂等,原因就在于缺少证据时,自动化恢复可能放大原始错误。

需要防止两种相反的错误

第一种错误是缺少执行证据。用户要求创建文件,模型只回答“我会创建”;或者要求完整阅读代码,模型只浏览了目录。这时不能把文本当成完成凭证。

第二种错误是过度纠偏。用户问“你的操作安全吗”,模型完整解释权限和风险,其中一句“如果你让我执行危险命令”包含“让我”。旧的意向检测在整段文本中匹配短语,误把条件说明当成未执行计划,重试后反而标记失败。

修复采用更明确的任务范围:独立的安全、权限、能力边界说明问答可以直接完成;匹配整个请求,追加“创建文件”“连接服务器”等操作后,就不再获得该豁免。原有文件读写与完成检查继续生效。

这种基于规则的判断仍有语言边界,不能被宣传为通用自然语言意图证明。它的价值是把已知误判变成清晰、可回归的正反例。

完成判定应看作一组契约

“任务完成”不是单个统一条件。说明性问答、源码阅读、文件生成和发布操作,需要不同证据。把它们都简化为“模型返回了一段文字”,会误报成功;把它们都简化为“必须写过文件”,又会误报正常问答失败。

请求 合理的完成证据 单独不足以证明完成的信号
解释权限机制 回答覆盖问题且没有待执行操作 出现过一次工具调用
完整阅读指定源码 明确范围与连续读取证据 枚举目录、看过 EOF 标记
生成 PPT 本次产物、可打开的结构、内容核对 导入 pptx 库成功
修改并验证代码 相关修改、所要求的验证结果 只写了修改建议
执行远程动作 对应远程结果及其状态 SSH 登录成功

当前实现利用用户任务、工具结果、待续读文件和意向检测等规则组合判断。它能纠正一些已知失败,但还不是通用的任务契约引擎。例如,成功运行一个只读 shell 探针,可能提供“运行过命令”的证据,却没有证明所请求产物已经生成。

因此我会把更严格的产物声明、版本关联和验证步骤视为后续可靠性方向。文章应保留这些不足,开发者才能判断系统在自己的任务里还需要补哪一层。

完成证据应该与任务类型绑定

一个稳定的完成判定可以拆成三部分:任务类型、可观察证据和缺失条件。比如“介绍文件读取 API”属于说明任务,证据是一段准确回答;“读取整个文件并找出末尾函数”需要连续读取覆盖目标文件;“创建报告”需要本次任务生成并验证报告;“部署”则要检查部署平台的实际状态。完成契约越接近用户想要的结果,状态越不容易被普通自然语言误导。

可以把每个动作视为一条证据记录:来源工具、参数摘要、开始和结束时间、返回码、产物 hash、关联任务 ID。完成条件再把这些记录组合起来。文件生成可能要求写工具成功、目标路径在预期 workspace 内、格式解析成功、内容包含必需章节。这样可以区分“生成了一个 PDF”与“生成的 PDF 有 12 页且包含用户指定的图表”。

契约设计还要支持否定证据和部分完成。例如文档中只有 6/8 个章节,任务状态可以提示部分结果,而不是丢弃已生成内容;若写入过旧文件或发现路径越界,应标为验证失败。对远程调用,响应超时是“结果未知”,不等于外部服务未执行;状态系统需要能表达这种不确定性,避免将它强行映射成失败后自动重试。

并非每种任务都适合由 Runtime 做深度内容检查。代码正确性可能需要测试,摘要质量需要人工或外部评估,部署成功需要目标系统回读状态。完成判定应让适当的 verifier 提供结构化结果,而不是让同一个模型用一段总结判断自己的成果是否合格。

运行状态与业务状态应该分开

Scheduler 的 DONE 通常表示它的 runner 返回,任务完成处理结束;这并不自动意味着业务验收已经通过。任务可以拥有明确终态 DONE,但报告内容仍然指出某个业务项未验证。如果需要将“代码执行完成”“质量门通过”“发布成功”统一展示,建议建立多个维度,而不是让一个任务枚举承担所有含义。

例如一个建置任务可以记录 execution_status=done、artifact_status=validated、policy_status=approved、publication_status=not_started。这几个字段分别由 Runtime、验证器、策略审批和发布系统负责。用户可以清楚看到文件已生成但尚未发布,而不是看到一个 DONE 后误以为网站已经上线。

事件顺序也应与最终聚合状态分开保存。可以先记录工具成功,再记录验证失败,最终任务为 FAILED 并保留已经产出的文件。任务失败不表示没有任何可交付内容;状态详情需要告诉用户哪些步骤成功、哪些不满足条件。否则用户可能重复执行已经完成的非幂等步骤。

对于异步验证,可以先返回“生成中/待校验”,后续验证器发回结果,再转为 validated 或 rejected。整个流程需要 correlation ID 和超时策略,以免文件生成后验证器离线导致状态永久停在中间。如果验证任务可重新排队,重复执行时要保证只提交一次最终状态。

验证器自身也需要故障路径

如果主任务执行成功但验证工具崩溃,最终状态应为“未验证”或验证失败,而不能默认通过。验证器的退出码、输出格式、超时和版本需要与产物一起记录。若验证器是模型,仍应区分它产生的判断与可复现的静态检查。

PPT 验收可以分结构、文本、版面和内容四层:ZIP/XML 包结构可读;预期 slide 数与备注存在;页面渲染没有截断重叠;内容覆盖用户请求。PDF 可做结构完整性、页面数和文字检索;视频可做容器 metadata、实际时长、音轨检查和片段播放;代码交付则跑构建、测试与目标路径检查。每一层的结果都应作为证据单独保存。

验证器误报也会损害信任。过严的标题边界检测可能拒绝可读的页面;过宽的“文件有内容”则放过坏 PPT。对于人工设计评估的质量标准,可保留审核状态与审核者,而不要伪装成自动客观分数。

2. 状态与证据怎样贯穿一次执行?

把规划、执行、持久化和界面放在同一时间线上,避免总结文本覆盖真实错误。

状态要能解释,也要能留下来

Scheduler 区分 QUEUED、RUNNING、DONE、FAILED、CANCELLED、TIMEOUT。前端展示的运行状态应来自任务实际生命周期,执行过程中的模型阶段与工具阶段属于更细的进度信息。

Runtime 还会把任务转移写入 journal。用户关心的不只是当前进程中的对象,而是重启之后能否知道:哪项任务正在执行、哪项已经终止、之前做到了哪一步。

在多 Agent 场景中,这个要求更明显。一个角色成功、另一个失败时,不能只因为“有人说话了”就把群讨论标成成功;依赖失败阻断的节点也需要与实际执行失败分开表示。

为什么最终兜底总结不能把失败变成成功

长任务可能达到规划轮数上限、重复计划而停滞,或连续工具失败。此时 Runtime 仍可用已收集观察生成一段工具不可用的总结,让用户了解进展。这个总结属于结果说明,不能反过来成为任务已完成的证据。

reasoning.c 因此区分正常最终回答与在预算耗尽等条件下触发的 synthesis。即使合成出了流畅文本,若原任务仍缺阅读、操作或验证,就保留未完成状态,并说明具体原因。

同理,“已经保存执行记录”是系统的恢复能力,不是用户任务的交付物。一次失败任务也可以有完整日志;日志存在,不应让前端写成工作已经完成。

Planner 失败后的恢复与完成检查

工具失败发生后,最短恢复循环包含几个必须保存的输入:失败工具名称、退出码、标准错误、命令或参数的安全摘要、脚本路径与当前任务要求。如果只把“执行失败”传回模型,模型只能猜测;如果错误输出被工具层截断又不标明截断,也可能错过真正根因。

接下来的恢复分支应由失败类型驱动。Python SyntaxError 需要定位行列并检查同类语法;ModuleNotFoundError 需要检查项目依赖和所用解释器;文件不存在需要核对当前工作目录与相对路径;权限拒绝需要选择已授权目录或停止请求授权;网络超时需要检查远端操作是否已经生效再决定是否重试。对于持续失败,应该改变方法、缩小范围或如实保留失败,而不是无限重复相同命令。

一次修改之后必须重跑产生失败的原命令,至少确认该错误已消失。若原命令本身仅做语法检查,还要继续运行实际生成步骤;若构建通过,还应运行测试;若测试通过但输出目录没有产物,就不能完成交付。验证层次应一路对应用户的交付要求。

有一个特别容易漏掉的假完成:旧产物已经存在。可靠验证把输出放进唯一目录或先记录既有 hash,再确认本次运行产生新的文件,并重新读取其内容。PPT 还需要检查 slide 数、notes 与版面;视频需要时长、编码和播放抽查;PDF 应解析页数与文字。产物创建后无法读取或打不开,也不能只靠存在性检查。

可观测性要能够串回一次执行

排查时至少要能把用户请求、任务 ID、会话、模型轮次、工具步骤和最终状态联系起来。同一个任务中,模型等待慢与工具运行慢需要分别计时;工具名相同但参数不同,也不能简单统计成同一个失败。

一个有用的事故记录可以采用下面的结构。这是整理日志的示意,不是当前 API 响应的字段承诺:

{
  "build": "实际运行二进制的来源",
  "task": "任务 ID 与会话",
  "expectation": "用户要求的交付结果",
  "last_model_response": "最后一轮的格式与含义",
  "last_tool_result": "成功、失败、超时或根本未调用",
  "artifact_check": "产物路径、内容与本次执行的关联",
  "terminal_state": "Runtime 最终写入的状态"
}

这种记录能直接识别几类不同问题:计划没有被解析、工具返回失败、成功结果在后续处理中丢失、旧版程序未更新、完成规则没有覆盖该请求。只有一句“执行失败”通常无法区分它们。

Journal 的写入顺序和损坏边界

任务 journal 采用追加式事件的好处是可以重建最后状态,并保留前序转移。若每行独立 JSON,读取器能逐条解析;文件末尾遭遇半行写入时,可以保留此前有效前缀并报告截断。若损坏发生在中间,忽略后续所有事件可能更安全,因为失去一条状态转移后,不知道后续记录是否仍属于同一任务序列。

持久化顺序是关键:先把任务变更写入内存再异步 flush,和每个状态立即 fsync,耐久性不同。前者通常吞吐较高,但断电可能丢失末尾记录;后者写放大和延迟更高。文章应只引用代码和配置实际保证的耐久性。文件追加成功也不等于存储设备已经将数据持久化到稳定介质,OS 缓存和硬件行为仍需纳入定义。

重启时通过最大历史 task ID 推进下一个 ID,避免简单从零开始与旧记录冲突。如果 journal 存在重复 ID、乱序记录或非法时间戳,应提供明确的修复策略和诊断。静默跳过损坏行可能让用户以为历史完整;过度失败退出又可能阻止系统读取无关任务。可以将原文件备份、输出恢复报告,并由用户决定是否继续。

恢复回归要使用真实的子进程关闭和重新启动。测试可在 RUNNING 状态与快照可观察后结束子进程,再用相同 state 根启动第二进程。测试环境和线上 data directory 应完全隔离。若只构造一段模拟 journal 直接调用恢复函数,就无法覆盖进程退出、句柄关闭、日志 flush 和服务初始化的组合问题。

Snapshot 能保证什么,不能保证什么

快照通过保留某些文件的旧版本,支持限定范围内的恢复。要说明可靠性,需要明确纳入范围的根目录、文件类型、符号链接策略、最大文件大小、权限元数据和多文件原子性。如果快照只覆盖工作目录中的文本文件,就不能把它描述为能够回滚数据库、系统注册表、容器镜像和 SSH 远端资源。

单文件原子替换可用临时文件加 rename 降低目标半写的窗口,但跨文件事务需要额外协议。比如任务先写 config,再写 checksum;如果进程在两者之间终止,文件状态可能不匹配。恢复时要以 manifest 记录一个事务组、完整性 hash 和提交点,而不是根据文件名猜测所有文件属于同一次操作。

快照文件本身也可能损坏、空间不足或被意外删除。出现这种问题时,恢复能力应退化成“报告可用快照与缺失文件”,而不是继续声称回滚成功。定期做 restore test 比只确认快照目录在增长更有价值。备份未经恢复验证时,只证明写入流程运行过,不证明数据可用。

保留策略同样影响安全与磁盘使用。过短的保留期降低恢复窗口;无限保留则会让用户数据、敏感文件和磁盘占用长期增长。对于项目缓存与用户数据,应使用不同的保留策略,并提供清理方式。清理过程要避开活动事务,且保留正在被任务引用的版本。

UI 状态要映射到可解释的后端状态

用户看到“执行失败”时,应能找到具体失败步骤;看到“任务未完成”时,应知道缺少哪项操作或产物;看到“已取消”时,应知道取消请求和真正停止之间可能存在进度。前端如果只显示一个 spinner,长时间等待会让人不知道卡在 Provider、文件解析还是队列。

状态展示可以将总体阶段和最近事件分开:总体是 QUEUED/RUNNING/DONE/FAILED;阶段是解析附件、等待模型、执行工具或验证产物;细节日志列出时间与输出摘要。阶段事件不应伪装成终态,模型的“正在整理结果”也不能覆盖工具未完成这一事实。

UI 轮询与 WebSocket 事件可能乱序。每个更新应带 task ID 和顺序信息,前端忽略小于当前 revision 的迟到事件。刷新页面后从任务查询接口重建状态,而不是假定连接期间没有遗漏。用户点取消后,如果后端返回仍在清理,应继续显示取消处理中,而不是立即删除任务卡片。

左侧任务列表等紧凑位置适合放状态 badge,但状态文字需要在窄窗下仍可见且不被标题遮挡。详细错误可以在任务详情展开。颜色不应是唯一信号,图标和文字都要可读;失败原因过长时提供展开查看。

3. 取消、超时和重启应如何恢复?

先识别中断类型与结果是否已知,再决定继续查询、人工确认或恢复执行。

取消、超时和进程崩溃是三类事件

取消来自用户或接管逻辑,超时来自时间预算,崩溃则可能让当前代码根本来不及返回。它们可以都导致任务不再继续,却具有不同的排查信息和恢复策略。

调度器的 task_should_abort() 检查取消标记及执行时间;工具和推理在相应位置响应。真正终止一个在途系统调用,还依赖底层进程与 HTTP 超时实现。UI 先展示取消意图,与后端已经完成清理之间可能存在时间差,应通过真实状态继续观察。

重启恢复则读取持久 journal。tasklog_recover() 按任务 ID 与记录顺序整理最后一次转移,把最后状态仍是 RUNNING/QUEUED 的任务追加为 INTERRUPTED,同时根据历史最大 ID 推进下一任务 ID,避免重启后 ID 被重新复用。

id=41 QUEUED
id=41 RUNNING
    ← 进程在此被终止,没有最终记录
重启扫描:41 的最后状态仍在执行
id=41 INTERRUPTED
下一次新任务 ID > 历史已使用 ID

这里的目标先是恢复事实:上次执行中断了。系统没有资格在缺少外部证据时猜测“也许已经完成”,也不能直接宣布所有副作用都已撤销。

取消是协议,不是一面 UI 按钮

用户点取消后,前端可以先进入“正在取消”;后端收到请求、设置取消标记、runner 到达检查点、工具收到中断、任务写终态是不同事件。若底层子进程无法被终止,取消按钮不会立刻释放 worker。超时也不一定能撤销已经发出的 HTTP 请求。

一个可靠的取消流程需要定义截止时间和强制策略:先协作式取消,等待工具清理;超过宽限期,终止本地子进程;仍不能确认外部副作用时,将它们标为结果未知。锁释放与文件临时目录清理也应在 finally/析构路径完成。取消后再提交新任务时,旧任务的迟到通知要依据 task ID 与会话代次丢弃,不能覆盖新进度。

测试应覆盖取消发生在 queued、模型请求前、模型请求中、工具执行中、事务提交后、completion callback 中等多个点。每个点的预期可能不同,但都要保证不泄漏 worker,不重复完成回调,且终态唯一。固定睡眠并检查取消通常不稳定,应等到可观察的 hook 或阶段状态再发取消。

恢复首先要承认执行被中断

进程被强制终止,与一个工具正常返回错误不同。前者可能根本没有机会写最终状态。

项目的进程恢复测试会启动独立 Runtime,等待 RUNNING 任务和可用快照,强制终止该测试进程,再用同一状态目录启动。系统识别残留任务为 INTERRUPTED,之后显式恢复快照并重放适合的文件工作。

RUNNING + 快照
 → 进程被终止
 → 重启识别 INTERRUPTED
 → 核对并恢复快照
 → 显式重放
 → DONE
 → 验证后续任务仍可运行

测试选择幂等文件替换,是为了让恢复后的结果可验证。任意 shell、SSH 或外部服务请求可能已经产生不可逆副作用,因此不能由这个实验推导所有外部操作都能自动重试、恰好执行一次,或保证断电耐久性。

重启恢复的状态矩阵

重启后的处理取决于最后持久化的状态与副作用类型。将所有残留任务都重放,是最容易造成重复影响的策略;将所有残留任务标为完成,则可能丢失未完成工作。系统至少应能区分确定失败、确定成功、进程中断和外部结果未知。

重启前观测 恢复动作 用户看到的状态 主要风险
最后事件为 DONE 读取结果和历史 DONE journal 尚未持久化到磁盘
最后事件为 RUNNING 标记 INTERRUPTED 并检查快照 中断待处理 外部动作可能已发生
文件事务未提交 按事务协议回滚或恢复 FAILED/INTERRUPTED 快照范围之外的副作用
远程请求超时 查询外部状态或用幂等键重试 结果未知/待核对 重复提交
journal 行损坏 保留有效前缀并显式报错 恢复受限 ID 或状态误读
没有可用快照 不虚构恢复成功 INTERRUPTED 文件处于部分写入状态

矩阵是恢复设计的讨论工具,具体系统行为仍应以源码和回归测试为准。它强调一个事实:重新启动进程只恢复运行能力,不自动恢复所有任务语义。用户界面需要展示“已识别中断”“可恢复操作”和“外部状态待确认”,不能用一个绿灯隐藏风险。

处理“结果未知”的重启与网络故障

进程崩溃时任务可能最后记录为 RUNNING,重启后将其标记 INTERRUPTED 是诚实的状态恢复。接下来需要分析每个动作有没有可能在崩溃前已完成。文件写入可以读取目标内容 hash,若与期望一致,可能安全地进入后续验证;若目标内容不同,应该记录冲突而不是覆盖。

外部请求更难判断。Runtime 发送了创建资源请求,服务端完成但确认包丢失,然后本地进程退出。重启后 journal 只看到最后状态仍是 RUNNING。此时不能凭本地记录判断远端有没有资源。若 API 支持查询业务操作 ID,就先查询;若请求支持幂等键,重放同一 key;否则需要人工核对。

恢复队列可以包含 INTERRUPTED、READY_TO_RETRY、UNKNOWN_EXTERNAL_RESULT 与 MANUAL_REVIEW 等语义。当前项目使用的状态集合是否有这些枚举,应看真实任务模型;即使 UI 只显示 INTERRUPTED,日志也应至少记录恢复决策细节。状态不要暗示系统已完成远端查询,除非实际有证据。

崩溃注入位置可以放在工具调用前、调用后但 journal 更新前、journal 更新后、验证后但任务终态前。对每个位置重启,观察重复执行与结果识别。这样的测试能发现双写窗口,而“正常运行成功后重新启动服务”只验证了读取持久数据的路径。

重启后的状态要能被用户继续操作

识别 INTERRUPTED 只是第一步。用户需要知道是否可以重新运行、是否应先恢复文件、哪些外部操作需手工核对、快照位于何处、原任务输入是否可查看。若界面仅写“任务未能完成,记录已保留”,却不显示执行阶段和下一步建议,日志虽然存在,仍不能帮助恢复工作。

恢复入口应提供 dry-run 或预览,让用户在重放前查看会执行哪些工具。对于纯文件任务,可比较目标文件当前 hash 和快照 hash;对于 SSH 或外部 API,则提供只读查询或要求用户确认。恢复动作本身产生新的 task/attempt ID,并引用原 interrupted task,以保留审计链。

用户可能选择“放弃中断任务”而不是重试。放弃应停止尚未运行的后续步骤,保留原记录,并按允许范围清理临时数据;它不能默默删除已写入的目标文件。任务历史 UI 应允许从原记录查看错误与快照证据。

对于被新消息接管的讨论任务,恢复语义可能是直接终止,而不是重新运行旧话题。不同任务类别应注册自己的恢复策略:可重放、需验证后重放、只能人工处理和不可恢复。统一的“Resume”按钮容易让危险操作重复。

4. 何时可以重试,如何处理副作用?

重试需要可识别的输入和副作用边界;事务、幂等和审计分别解决不同问题。

事务和审计各自提供什么

文件事务与快照帮助恢复覆盖前的内容,审计和执行记录帮助追踪发生过什么。它们服务于不同问题,不能因为存在日志就认为可以撤销,也不能因为支持文件回滚就认为可以撤销一条远程命令。

SSH 还涉及密码处理。项目对 SSH 密码做专门保护,避免将其暴露为普通模型参数或命令行文本;这不意味着可以对任意字符串进行宽泛脱敏。普通 URL、文件路径和用户需要阅读的内容被误改,同样会破坏执行与排错。

回滚、重试与幂等为什么不能混用

文件快照可以保存写入前的内容,事务失败时恢复本地受管理文件。外部 shell 或 SSH 可能已经发送请求、创建远程资源或修改另一套系统,这些副作用不一定处在本地快照覆盖范围内。

假设远程服务已经完成“扣款”,但响应在回程丢失。Runtime 只看到超时;盲目重试可能再次扣款。这是结果未知,而不是已知失败。要可靠重试,通常需要对方支持幂等键、可查询的业务操作 ID,或在重试前核对外部状态。

对于文件生成,也存在不同语义:把 note.txt 覆盖为固定内容通常可重复;向日志末尾追加一行、递增计数器或发一封邮件则可能重复产生效果。恢复测试选择固定文件替换,是为了把重放结果限制在可验证范围。

机制 核心问题 能直接推出的保证
journal 上次执行处于何种状态 可追踪已记录的转移
快照 某些受管理文件原来是什么 可以按支持范围恢复
重试 是否再次执行 不能单独推出无重复副作用
幂等设计 重复请求怎样合并 取决于具体操作与外部系统
审计 谁在何时调用了什么 可排查,不等于可撤销

要讨论 exactly-once,必须明确业务边界、持久化原子性和外部协议。单机进程恢复实验不足以建立这个结论。

重试决策表与幂等边界

重试之前先问:动作是否已开始?结果是否确定?操作是否幂等?是否能用唯一键去重?是否能查询外部状态?文件快照是否覆盖此路径?如果这些问题不清楚,暂停并标注“结果未知”往往比自动执行第二次安全。

操作 重试前的核对 典型策略
纯读取 是否为同一文件版本 可重试,但保留版本或 hash
固定内容写入 目标路径与已有内容 通常可覆盖重跑,先检查作用范围
日志追加 上次是否已追加 使用记录 ID 去重或检查尾部标记
SSH 配置变更 远端状态和凭据是否有效 先读取状态,变更命令设计成幂等
创建远程资源 请求是否已成功但响应丢失 通过业务 ID 查询,再决定重试
发送通知/付款 接收方是否已处理 使用远端幂等键,不靠本地快照

exactly-once 往往要求操作接收方参与协议。Runtime 单方面将状态写成 DONE 或用锁阻止本地并发,只能约束自己能控制的部分。分布式场景中,请求可能已经被远端提交而确认尚未返回。将这个窗口明确写成未知状态,比夸大为 exactly-once 更容易建立可信恢复方案。

对于代码生成与部署,安全重跑也要看副作用。构建到隔离的临时目录容易重复;覆盖用户的正式配置、推送远端仓库或发布网站则需要显式确认发布范围,并在发布前保存可核对的差异。Runtime 的事务操作与用户授权应在动作执行边界上对应起来。

任务的“重放”需要版本化输入

重放历史任务用于复现 bug 很有价值,但原始 prompt、工具结果和 provider 版本变化后,重跑结果不必相同。为了可审计,需要保存模型 ID、temperature/采样设置、系统提示版本、tool schema、附件 hash 和 Runtime commit。密钥仍由当前安全配置提供,不应写入历史任务文件。

历史回放可以分成只执行本地工具 fixture、用缓存模型响应重建控制流,以及重新访问真实 Provider 三种模式。第一种最稳定,第二种适合验证 parser 和 planner,第三种受模型漂移与外部系统变化影响。用户界面应标记 replay mode,避免新生成回答被误认为原任务的实际记录。

重放具备副作用时,要默认走沙箱或 dry run。发布、SSH、文件删除和外部 webhook 不能仅凭历史日志自动再执行。即使工具请求看起来幂等,服务端配置与目标对象可能已经变化;重放前再次校验权限和当前状态。

任务数据 schema 演进也属于可靠性

持久 journal 会跨越多个软件版本。新版本需要读取旧事件结构,未知字段应被忽略还是拒绝,缺少字段用什么默认值,都必须有兼容策略。新增 enum 值后,旧 UI 可能不认识;数据库迁移中途失败也可能让新旧程序都不能启动。

一个可靠迁移通常先读版本号,再逐版本转换到当前结构,并在写回前保留原件或用原子替换。若转换可能丢信息,应保留兼容字段或明确提示不支持。不要在首次读取时静默覆盖用户的旧 state,除非迁移本身已做备份并可回滚。

测试需要检查三种 fixture:历史最老版本、当前生产版本、缺字段/损坏版本。测试升级后能读取任务状态、保留 ID 单调性,并确认输出不会被重新解释。持续部署时还要考虑旧旧进程与新二进制同时访问 state 的可能性;若不支持多版本并发,启动时应进行进程锁或清楚报错。

与代码格式变化一样,state schema 不能只通过测试新建空目录来验证。要将若干匿名化历史样本纳入回归,覆盖 journal 追加、重启扫描、任务 ID 恢复和错误行处理。对用户来说,旧任务历史有时也是审计依据,升级不能悄悄将它们丢掉。

身份、权限与秘密管理中的可靠性

工具执行通常继承运行进程权限。Runtime 自己添加的 allowlist 或 workspace 限制可能降低风险,但必须确认它们覆盖了所有路径和间接入口。Shell 通过脚本调用文件系统,SSH 在远端执行命令,MCP 插件可能拥有独立授权;单一工具策略若只检查工具名,没有检查目标资源,就可能存在越权路径。

密码脱敏应该按字段做。在 SSH tool 参数中,password 和私钥内容属于敏感字段;用户名、主机名和端口通常是连接诊断所需信息,不应该因为周围文本看起来像凭据就全部脱敏。测试可使用专门的 dummy secret,确认它不出现在 prompt 回传、stdout 日志、audit record 和错误信息中,再确认非敏感字段仍能帮助定位连接失败。

重试也会重复使用凭据,因此日志不应为了让模型“修复”连接错误而把密码反复注入普通 shell 命令。更稳健的方式是凭据句柄由密钥存储提供,工具仅拿短期引用,模型看到可识别的 alias 和字段是否配置。若当前接口只能通过配置传秘密,应至少确保文件权限、日志脱敏和退出清理有测试。

权限变更与任务恢复需要联动。某任务开始时拥有权限,用户之后撤销授权并重启,系统应在恢复前重新评估当前权限;不能只依赖 journal 中历史上曾经批准过。对于不可逆的发布、删除和远端变更,可在执行边界要求确认,记录批准人、范围与时间。

5. 怎样设计故障演练与最小回归?

从停止点重建事故时间线,使用相邻反例检验修复是否伤及正常任务。

故障演练:从日志找到停止点

可以用一个只在测试 workspace 执行的故障注入流程:写入固定文件 A;在第二步工具调用前让 fixture 返回非零;确认任务被标为 FAILED 并保留失败记录;重启 Runtime;确认任务标记 INTERRUPTED 或根据持久记录恢复为最终状态;查看快照;显式恢复文件;最后再次读取 A 的内容并发起一个无关任务,确认服务可继续接收工作。

每个阶段都记录 task ID、最后一次 journal 状态、事务 ID、文件 hash 与预期 hash。这样可以把“失败后保留了记录”和“失败后没有副作用”分开。保留日志并不会删除已经完成的外部动作,回滚成功也不证明所有资源都回到了原状。

演练还应加入人为破坏:删除快照文件、截断 journal 最后一行、在提交边界终止进程、制造 workspace 权限错误。系统应给出不同诊断。若所有情况都显示同一个 interrupted 且没有下一步建议,虽然没有崩溃,却没有实现有用的可恢复性。

分层失败注入矩阵

测试可靠性可以按故障位置与期望结果建立矩阵:

故障注入点 例子 需要看到的结果
规划 Provider 返回空或格式错误 明确解析错误,未调用工具
执行前 策略拒绝路径 保留策略理由,不尝试绕过
工具内部 文件权限或子进程非零 输出错误与 exit code,任务继续评估
副作用之后 文件写成但确认失败 校验文件并谨慎决定是否重复
事务提交 快照记录失败或磁盘满 状态不宣称可回滚成功
任务终态 process kill before final journal 重启标记中断并保留可恢复证据
结果传播 callback 写 history 失败 任务和历史状态差异可见
发布验证 上传成功但页面检查失败 发布状态未通过并给出 rollback 选项

每个 case 都应有一个恢复/清理步骤,确认之后独立任务还能运行。如果失败注入改变了全局单例、留下锁或占住端口,下一条测试失败就无法归因。测试环境必须与用户 state 隔离。

统计恢复指标时,可以报告中断识别成功率、无需重复副作用恢复率、人工介入比例、恢复耗时、验证器误报和日志关联完整度。每个数字需要分母与样本范围。极少几次 fixture 测试只能证明控制流,不该被表述成生产级灾难恢复 SLA。

设计一份能复盘根因的事故时间线

一份有用的时间线需要统一关联 task ID、会话、build ID 和工具调用 ID。按单调时钟记录接收、入队、规划开始、Provider 请求、解析结束、工具开始/结束、验证与终态;同时保留面向人的 wall clock。墙钟便于与外部事件对齐,单调时钟适合计算持续时间,系统时间校准不应导致耗时为负。

每个阶段要区分“没有发生”和“发生但日志缺失”。例如 planner 没有 tool plan,和 tool call 被 parser 丢弃,是不同原因;HTTP 请求没有开始与已开始后 timeout 也不同。事件结构可以带 event type、sequence number、attempt number 和 reason code。重试时新增 attempt,而不是覆盖上一轮错误。

事故记录不需要无限保存整个 prompt。可以保留模型名、输入 token estimate、response format、动作数量、工具名、参数脱敏摘要、错误类型、输出长度和 hash;用户授权调试时再写入本地受保护 trace。要排查 DSML 乱码,通常需要原始格式样本;要排查 SSH 密码泄漏,则应该验证秘密字符串从未进入持久 trace。

一条时间线的关键不是字段越多越好,而是能回答:用户希望什么、系统执行了什么、最后一次确定成功的副作用是什么、剩下哪些结果未知。对无法避免的未知窗口,记录清楚比编造更可靠的状态。

用相邻反例防止修复范围过大

修正“安全问答被误判为操作意向”后,应测试纯解释请求、纯执行请求与两者混合请求。纯解释应正常完成;创建文件的请求应要求文件证据;“解释安全边界后创建文件”需要同时满足文字回答和实际创建。只测纯问答容易造成一个新漏洞:任何包含安全关键词的操作请求都被豁免。

修改读取完成检测时,同样需要正反例:目录枚举不能证明文件已读;最后一页到 EOF 应通过;从 offset 100 跳到 500 的不连续读取不应通过;用户只要求看函数签名时无需把整份仓库全部读完。范围要由明确任务决定,不能将“完整阅读所有文件”误当成默认要求。

对产物检查也要有邻近样例:旧文件预先存在但本次脚本没有运行;脚本执行成功却写入空文件;PPT 可打开但少一个必需章节;有效的纯问答任务没有文件也应 DONE。这样能够验证完成规则没有把所有任务统一压成同一种证据。

规则型 verifier 每新增一个例外,就要同步新增一个反例。对于难以靠关键词判断的请求,宁可询问验收目标或将完成状态细分,也不要悄悄扩大豁免条件。

如何把事故变成回归

我会先固定最小触发输入,再给外部依赖一个可控制的 fixture。对于提前结束,fixture 先输出读取计划、再输出错误的完成声明;测试必须确认 Runtime 要求继续或保留未完成。对于误报失败,则用完整说明性回答确认它不再被当作操作意向。

崩溃测试需要更高一层:启动自己拥有的独立进程,等待 RUNNING 与快照可用,强制终止,再用相同 state 重启。若仅调用某个“恢复函数”并传入静态数据,就测不到进程退出、日志落盘和重启初始化的组合边界。

测试也应避免用固定睡眠猜测时机。等待可观察状态后再终止,比“睡一秒然后 kill”更稳定;错误路径应保留日志,并让测试后续提交正常任务,确认恢复没有留下损坏的运行状态。

回归应该检查事实

cd backend
python tests/test_information_answers_e2e.py
python tests/test_completion_recovery_e2e.py
python tests/test_crash_recovery_e2e.py

第一组核对完整说明的一轮完成、混合操作请求的未完成状态,以及历史和重启一致性。后两组核对阅读恢复、真实进程终止和显式重放。它们各自覆盖有限但明确的故障面。

源码:任务日志、事务实现、完成状态回归。

6. 如何衡量可靠性并沉淀改进?

用误报成功、漏报成功、未知结果与重复副作用评价系统,再将事故转成可维护的契约。

可靠性目标要和错误预算对应

单次任务通过率不足以描述可靠性。需要分别定义:错误输入被接受的比例、任务实际成功但显示失败的比例、未完成却显示 DONE 的比例、重复副作用发生率、恢复时间分布和人工介入比例。不同错误的用户影响不同,可以设置不同门槛。

样本量也影响结论。如果测试 20 次没有出现误报,仍不能证明误报率为零;真实故障很少时,更需要故障注入、历史事故复现和静态不变量。生产监控可以报告 rate 与置信区间,但任务复杂度差异很大,分母应按任务类别拆分。

错误预算可以用于发布门槛:若最近一批 artifact verification 有较多 false pass,就应暂停自动发布或提高人工抽查;若 Provider 暂时失败率升高,可限制重试以避免雪崩。规则要关联用户实际结果,而不只是工具调用成功率。

用误判与副作用指标评价可靠性

可靠性检查的目标是使状态与事实一致,让失败可解释、进展可恢复。它不能消除模型能力、工具权限和外部系统的不确定性。

完成规则修正后,还需要相邻反例。例如“解释如何创建文件”和“实际创建文件”应有不同完成条件,混合请求也必须单独验证。关键词规则存在语言边界,新增规则可能修正一种误判,同时引入另一种。

评估时应同时统计误报成功、误报失败、重复副作用、取消完成时间以及恢复后的可用性。仅提高 DONE 的比例,可能掩盖实际未完成的任务。

源码阅读基线:完成与兜底总结、中断识别和 ID 恢复、真实进程恢复测试。

可靠性复盘的实际产物

一次修复完成后,可以留下四样东西:匿名化的最小事故输入、按时间排序的事件 trace、明确的状态与副作用不变量,以及自动化回归。若只提交代码而没有保留触发条件,后续版本可能再次引入同样误判;若只留 issue 描述而无测试,CI 也无法帮助维护者发现退化。

复盘文档还应列出未覆盖事项,例如“已验证本地文件替换可恢复,未验证 SSH 远端命令”。这一句能阻止后来读者把有限实验扩大成系统级保证。限制应和成功结果一起发布,形成可持续更新的技术记录。

严重故障可进一步安排演练:让团队在不知道预设原因的情况下从 task ID 开始定位,记录花了多久找到错误层、是否选对恢复操作。演练发现的日志缺口、UI 含糊状态和 runbook 过期,都可以转成新的独立改进任务。

延伸阅读与实践顺序

完成本文后,建议选取“取消、超时和重启应如何恢复?”中的一个分支,用固定输入记录预期行为、实际结果和差异。先验证一个具体不变量,再扩大任务规模;若结果不同,优先核对源码版本、配置与输入证据。

上一篇:协议与执行证据;下一篇:百万任务实测。欢迎用真实的中断、误判和恢复案例检验 Runtime,可靠性通常就是从这些边界一点点长出来的。

在 GitHub 阅读 Markdown 源文反馈与交流 ↗