小叶用 AI 帮自己做了一个资料整理工具:选中一批资料,程序逐份读取,交给模型生成摘要草稿,再把草稿和对应出处保存下来。第一次演示很顺利,于是他准备让程序在自己上课时继续工作。

这次共有八份资料,编号为D01至D08,批次记为B16。前三份的草稿已经保存,小叶也检查过它们与原资料的对应关系。程序处理D04时却一直等不到生成结果,最后显示超时;D05至D08还没有开始。页面上只剩下一句“任务失败,请重试”。

此时,重试整个批次可能把前三份再做一遍,直接从第五份开始又可能漏掉第四份。若把错误提示藏起来,页面上的报错消失了,D04的结果却仍未确认,后续任务也还没有安排。本章要解决的就是这个问题:一项任务没有按预期走完,怎样根据已经发生的事情,决定下一步怎样做。

以下工具、编号、故障和时间都是虚构的教学情景。八份资料可以分别整理,彼此没有生成顺序上的依赖;原始资料保留。生成的内容始终先称为摘要草稿,保存成功和格式合格都不代表内容已经核对无误。我们先把软件任务的恢复过程说明白,再据实际完成标准判断成果是否可用。

一、出现异常,与任务已经失败,不完全是同一件事

程序执行到某一步,可能遇到当前步骤无法正常继续的情况。例如,准备读文件时发现文件不在,准备解析结果时发现格式不符合要求,准备等待远端回答时超过了设定时限。在很多编程语言里,这类执行中的特殊情况可以通过异常报告,让正常路径中断,交由相应位置处理。

异常提供了一种控制执行过程的办法,却没有替我们决定任务最后算不算失败。读取D04时发现路径已经改变,如果程序能找到用户明确选择的新路径,重新读取并完成后续工作,这次异常就可能得到处理。反过来,程序没有抛出任何异常,却把D03的摘要存到了D04名下,任务仍然没有正确完成。

因此,小叶需要同时看两层结果。一层是某个操作是否正常结束,例如这次读取有没有成功;另一层是任务要求是否满足,例如对应资料有没有得到可以继续核对的草稿。后者需要检查具体输出,不能用“运行时没有红字”代替。

业务上的拒绝也未必采用异常表达。工具可以正常返回“资料超过本次允许长度,请拆分后提交”。这是一项清楚的处理结果,程序甚至完全没有出错。要不要用异常、返回状态或其他方式表示它,是接口的安排;使用者关心的是原因是否明确,程序会据此采取什么行动。

回到B16,“任务失败”把这些不同情况都合到了一起。单凭这句提示,看不出具体哪个阶段需要处理,也看不出本次中断与前三份已有草稿的关系。小叶还得另找记录,才能作出恢复判断。这个提示之所以不够用,是它没有交代恢复所需的信息。

一个更有用的现场说明可以是:“前三份草稿已保存;D04的生成请求尚未确认结果;后四份未处理。”它还没有替小叶解决超时,却已经让后续判断有了对象。接下来应围绕D04的实际状态调查,并保护已经确认的成果。

二、先看失败原因和已经发生的作用,再选择动作

假设现在分别遇到三种情况。第一种,D04的源文件已经被移走,原路径不存在;第二种,源文件能读,远端服务暂时没有响应;第三种,程序把一个本来应当是文本的对象传成了列表,在内部处理中出错。它们都可能出现在“生成第四份草稿”这一步,合适的处理却不同。

文件缺失时,不改变路径或补回文件,反复执行原来的读取通常仍会失败。应先找到正确资料,或者把该项记为等待补充。暂时无响应则可能在稍后恢复,但能否重发还要看第一次调用是否已经产生作用。程序把对象传错时,需要检查调用关系和代码;让模型反复生成摘要并不会修好这个传参问题。

这里可以认识暂时性故障与持续性问题的区别:前者可能在原任务条件不变时随环境恢复而消失,后者通常需要改变输入、权限、配置或实现等条件才能解决。这是选择行动时有用的区分,不是见到某个错误名称就永远归入一个固定类别。

例如,提示“无权限”,可能是账号确实没有访问资格,也可能是登录凭据已经失效。前一种需要由有权处理的人调整访问安排,后一种可能需要重新登录。程序若只看见“失败”两个字,持续快速重试,既没有改变这两种条件,也没有帮助使用者知道该做什么。

还要问清:失败之前,有没有已经完成的作用?假如草稿文本已经生成,只在保存时遇到磁盘空间不足,恢复重点就是保留现有文本、腾出合适空间并完成保存。重新发起远端生成既增加工作,也可能得到另一版草稿,而原来已经取得的文本其实仍有机会继续使用。

相反,如果只是请求尚未送出,当前没有远端任务,那么网络恢复后可以从发送步骤继续。两种现场都可能显示“生成失败”,但一个已有结果、一个尚未开始,恢复位置自然不同。恢复应从有证据的状态出发,不能只从错误提示的标题出发。

小叶不必一开始就猜中全部原因。证据不足时,可以先记下失败位置、已确认的输出和还不知道的部分,再安排核对。保留未知,比把未知写成某个熟悉的错误更有用;错误分类只有能帮助下一步行动时,才值得细分。

三、捕获异常,是把特定情况交给合适的位置处理

在Python里,try与except可以让程序尝试一段操作,并在遇到指定异常时转入处理分支。异常发生以后,当前try中尚未执行的语句会被跳过;匹配的处理分支运行完,程序才能按后面的安排继续。没有被这一层接住的异常,还会向外层传递。Python异常教程说明了这种执行方式。

小叶的工具要求模型先交回一段JSON文字,其中summary字段保存摘要草稿。下面只展示读取这段文字并检查一个字段的小函数,暂不包括网络请求、资料身份核对和文件保存。

可复制模板
import json

def parse_summary(raw):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return {"status": "invalid_json"}

    if not isinstance(data, dict):
        return {"status": "invalid_summary"}
    summary = data.get("summary")
    if not isinstance(summary, str) or not summary.strip():
        return {"status": "invalid_summary"}
    return {"status": "received", "summary": summary}

print(parse_summary('{"summary": "资料讨论了活动报名安排。"}'))
print(parse_summary('{"summary":'))
print(parse_summary('{"summary": 12}'))

三个调用依次得到:

可复制模板
{'status': 'received', 'summary': '资料讨论了活动报名安排。'}
{'status': 'invalid_json'}
{'status': 'invalid_summary'}

第一份输入能够解析,摘要字段也是非空文字,因此交给后续环节。第二份文字没有写完,解析时产生JSONDecodeError,函数在对应分支返回格式问题,没有继续检查不存在的解析结果。第三份JSON本身能够解析,但摘要竟是数字,函数通过普通条件判断返回字段不合要求。Python的JSON文档说明了怎样把文字解析成程序对象;摘要字段是否适用,则由本例按自己的要求另行检查。

这个小例子也显示,异常处理与条件判断可以配合。小叶无需把“字段缺失”“空白摘要”都强行改成异常;他需要保证这些情况不能进入后续的正常草稿路径。函数返回received只说明已经取到符合这里基本结构要求的文字,并没有证明摘要说得对,也没有证明它属于D04。

如果其他程序错误地把一个列表直接传给parse_summary,json.loads会报告输入类型问题。这里没有用宽泛的捕获把它改写成invalid_json,因为这与“远端交来一段没有写完的JSON文字”不是同一类输入。调用方应当按约定交来文本;程序对象传错,值得继续向外报告,帮助定位实际错误。

有人会建议,把所有异常都接住,然后返回空摘要。这样确实可能让批处理继续往下跑,但后面再也分不清“资料确实没有内容”和“程序没有取得内容”。缺失来源、编程错误、连接失败会被悄悄换成同一种正常值,最后整批看起来完成了,恢复依据却被抹掉。

**捕获异常应当伴随一个有意义的处置。**能够处理的格式问题,可以保留原回复并转入修复;需要上游补输入的,返回明确原因;这一层不知道怎样处理的,让更合适的位置接手。接住异常以后什么也不做,并不是默认的恢复策略。

捕获范围也宜贴近需要处理的操作。如果把读取资料、发请求、解析、保存全部塞进同一个大范围,再把所有错误都叫作“生成格式不对”,保存空间不足也可能被误引到重新生成。把出错环节保留下来,才能让下一步真正作用于失败原因。

四、一份资料出问题,怎样判断要不要停下整批

在B16中,八份资料可以分别生成草稿。现在另设一种情况:D04的文件格式不符合要求,但其余资料仍可正常读取和生成。因此,小叶可以把D04记为待处理,继续检查其余资料,最后交出一份清楚列明缺项的批次结果。这叫作对部分失败作出明确安排:承认一部分工作没有完成,同时保留其他已经完成的作用。

不过,能继续的依据是资料之间确实独立。若任务改成“先从D04提取统一词汇表,后四份摘要必须使用这份词汇表”,D04就成了后续工作的前提。此时跳过它继续生成,会改变成果要求;除非先调整任务范围,否则不能再沿用前一个案例的继续策略。

失败原因也可能影响更多资料。只有D04是一份无法解析的文件,适合暂时隔开它;如果发现访问模型的凭据失效,后四份都要使用同一凭据,逐份再撞一次相同错误就缺少意义。应暂停依赖这一条件的生成,先恢复访问,再接着做。

这种区分也能帮助安排 AI 修改程序。小叶可以要求它说明:“这项错误只影响哪份资料,哪些后续步骤确实不依赖它,整批结果怎样保留缺项。”如果 AI 仅回答“加一个异常捕获即可继续”,仍然没有回答继续以后能否满足原来的任务要求。

批次状态因此要有自己的含义。假设继续处理后,七份草稿已保存,一份待处理,就可以显示“七份已有草稿,一份待处理”;不能只因为主循环走完,就显示“八份全部完成”。如果还有内容核对要求,则应另外说明哪些草稿已经核对,不能把保存计数又当成核对计数。

暂时继续与遗忘失败是两种动作。继续意味着失败项有身份、有原因、有接续入口;遗忘失败则是把它从结果中删掉,让人误以为这批从来只有七份。后者会把局部失败藏到最终交付里,通常比明确停下更难发现。

同理,整批暂停也不等于先前成果都要作废。共享服务故障可以阻止新生成,却未必影响已经保存的前三份草稿。把“接下来不能做”与“过去做出的内容是否仍可用”分开,能够避免为了处理一次中断而无依据地重做全部工作。

五、超时限制的是等待,不能替我们确认远端结果

现在回到开篇D04的实际设定:请求已经发出,小叶在规定时间里没有收到生成结果。这时的超时首先说明,调用方不再继续按原方式等待这次回答;它不自动说明远端没有收到请求、没有工作,也不自动撤销远端已经产生的作用。

例如,远端可能还没开始,也可能正在生成,还可能已经保存结果,只是回复没有到达。对小叶来说,这几种状态都可能表现为等待超时。仅凭这个现象,不能挑一种最方便恢复的状态当成事实。

如果小叶已经取得并保存了原任务编号,而且服务支持按号查询,就可以据服务实际承诺查找处理状态和结果。若查询明确找到已完成的D04任务,就继续取得对应草稿;若明确报告尚在处理,就保留等待;若仍无法确认,则继续把结果记为未知。一次查询没有找到,也要看该查询保证了什么,不能自动认定原任务永远不存在。

上一章的幂等约定在这里就有了用途。服务若明确支持同一请求标识在指定范围内重发而不重复创建任务,调用方可以按那个约定继续确认或重发。反过来,服务没有提供这样的保证,页面自己加一个编号也不能让远端自动认得它。

对纯粹生成一段文本的操作,小叶有时可以接受重新生成一份候选草稿。但这仍可能增加调用成本、得到不同内容,或者遗留第一次尚未结束的任务。是否允许这种重做,要按当前工具的作用和任务要求决定;不能把“反正只是文字”推广成任何 AI 调用都可以无限重发。

如果调用还会发送通知、提交表单或创建其他业务记录,重复作用就更加明显。一封通知已经发出去,回复超时后再次发送,收件人可能收到两封。恢复首先需要弄清发送状态或使用已有的去重保证,捕获超时本身没有提供这种保证。

合理的超时值也取决于任务。等待太短,会把仍有希望正常完成的调用频繁打断;等待太长,又可能长期占着连接和处理名额,让其他任务也无法推进。应根据正常耗时、使用者可以等待多久、失败后准备怎样处理来决定,而不是只把数字调大直到暂时看不见报错。

还要知道计时覆盖哪段过程:只算等待回复,还是包括连接建立及其他准备?如果小叶给整个任务安排了十秒,但某个“十秒超时”只覆盖其中一段,实际任务仍可能更久。读取所用工具的具体约定,才能将等待上限真正放入整体安排。

六、能重试、值得重试,还有时间重试,要分别判断

重试是再次尝试尚未取得所需结果的操作。它最有价值的情形,是故障可能已经恢复,而且再次执行的作用可以接受。已知文件路径错误却仍读原路径,缺少资格却继续发相同请求,都没有因为多试几次而改变成功条件。

判断可以沿三个问题推进。第一,再次执行的作用是否可以接受?第二,失败原因是否可能在当前条件下改善?第三,剩余时间和资源是否容许继续?这三项都是允许继续的条件。若有一项不满足,或还无法确认,就需要先核对、改变条件或停止自动尝试。

为了看清第三个问题,另设一个简化的等待方案:一次操作首次尝试以后,最多再重试两次,共三次尝试。每次请求最多等四秒;第一次失败后等一秒再试,第二次失败后等两秒再试。先忽略本地处理和调度开销,只画每次都等到超时的情况。

三次尝试的请求等待与两段重试间隔
图 S06-01|三次尝试的请求等待与两段重试间隔

图中,第一段请求等待结束以后才进入一秒间隔,第二段结束以后才进入两秒间隔;这些时间顺次占用同一个任务的等待预算。三次请求等待合计十二秒,两段间隔合计三秒,因此图示情形总共等待十五秒;请求提前结束时,实际等待可以更短。这一上限约束调用方的等待,不保证远端结果届时已经查清。

若小叶只允许整个操作等十秒,固定方案就不能完整执行。第二次已经等到第九秒时,只剩一秒,已经放不下原定的两秒间隔,更放不下后面又一次四秒请求。此时应停止这套固定重试安排,或者使用支持剩余预算的办法,重新限制后续等待;不能让每一层只看自己的四秒,最后合起来超过任务上限。

有些工具允许按剩余时间缩短下一次调用的等待,这是一种具体实现选择。但剩余一秒不代表远端一秒就能完成,也不能因为客户端停止等待就把正在处理的任务当作已经取消。等待预算决定何时把控制权交回来,最终状态仍需按真实协议处理。

因此,“最多重试两次”和“最多尝试两次”必须分清。按前一种限制,整个过程先有一次首次尝试,之后最多再试两次,总共最多三次;按后一种限制,整个过程总共最多两次。给 AI 安排实现时,要把首次是否计入、间隔从何时开始、总时限如何约束写清,避免一个字的省略改变整体等待。

也要查清重试由哪一层负责。假如外层工具每次最多尝试三次,而内部使用的客户端每次也会尝试三次,一次始终失败的操作最多可能触发九次远端尝试。这个例子另设两层都用尽次数、每个外层尝试都会调用内层的条件,不是所有系统的固定倍率。它说明,只在外层看到“重试两次”还不足以知道实际负载。

小叶可以把恢复责任放在能看到足够上下文的一层,并弄清所用客户端已经做了什么。现成工具若已有合适的重试功能,先理解和配置它,再补缺失部分,比在每一层都加同样的循环更容易控制总耗时与重复作用。

七、退避和抖动,改变的是失败后怎样分配尝试

立即重试有时能成功,但在服务已经繁忙时,大量客户端同时再次请求,会给它追加工作。原来的问题可能只是处理变慢,密集重试却让仍在处理的请求和新请求一起堆积,使服务更难恢复。

退避是失败后先等一段时间,再作下一次尝试;连续失败时可以逐步拉长间隔。前面的等待一秒、再等两秒,就是一种简单安排。它给系统和网络一些恢复时间,也降低同一调用方短时间内不断重复的密度。间隔还可以设置上限,并结合最多尝试次数和整体时限使用。

不过,如果许多客户端都在同一个时刻失败,又都精确等一秒,它们仍可能在下一时刻一起回来。抖动是在约定的等待范围内引入一定随机差异,让不同调用方的下一次尝试错开。比如这一轮有人等得稍短、有人稍长,原先集中到一点的请求就可能分散到一段时间里。

抖动主要改变尝试在时间上的集中程度,并不自动减少每项任务总共会试几次,也不保证任何一次一定成功。若文件内容本身不符合格式,随机等多久都没有修复它;若服务持续不可用,还需要达到条件后停止、延后或交给其他处理方式。

AWS关于超时、重试、退避和抖动的工程文章讨论了这些相互影响。值得带回小叶工具中的,是同时考虑再次尝试的作用、时间和额外负载;具体延迟数值要由当前任务决定,不照搬某个云产品的默认设置。

回到八份资料,如果发现生成服务持续无响应,先停下新的生成、保留已有草稿和待办,比让八项任务各自无限循环更容易恢复。已经取得结果的保存步骤若仍能正常完成,可以继续做;依赖远端的生成则等待共同条件改善。这样的分流是依据依赖关系作出的,不是简单规定“出错后全部停止”或“出错后一律继续”。

当服务恢复时,也不必让所有积压任务在同一瞬间全部重发。按允许的处理能力逐步恢复,并继续观察结果,可以避免一恢复就再次压满。对大学生的小工具来说,先控制并发数量和重试次数,往往就能把这一关系落实成可检查的行为。

八、检查点记录的是可据以接续的状态

前面讨论怎样再次尝试,还没有解决一个问题:程序被关闭、电脑重启或运行进程退出以后,怎么知道该从哪里继续?如果“前三份已完成”只保存在内存变量里,程序结束后,这句话也随之消失。

检查点是在执行过程中保存的一份接续依据。它记录哪些阶段已经达到、对应哪些输入和输出,使后续运行可以从已确认的位置继续,而不必把全部工作从头再来。能否据此恢复,取决于记录与实际成果是否一致,以及当前任务是否仍适用这些成果。

小叶可以为B16逐份保存任务信息:资料编号、输入版本、这次使用的整理要求、当前阶段和输出位置。D03若有草稿,应能从记录找到那份草稿,再核对它确实来自本次D03资料。只记“完成数量:3”,不足以区分究竟完成哪三份;资料顺序改变后,也无法据此认出哪些应当跳过、哪些还要继续处理。

输入版本可以是由工具管理的版本编号,也可以采用文件内容的指纹。这里的指纹,是按文件内容计算出的识别值,用于发现内容是否变化;它帮助判断是不是同一份输入,不判断内容是否正确。文件名仍叫D03,而内容已经换了,旧草稿就不能仅因名称相同而自动沿用。

整理要求也会影响可复用性。例如,旧任务要求提炼活动报名流程,新任务改成比较隐私条款,即使原资料没有变化,已有草稿也未必满足新要求。检查点除了认输入,还要认任务条件;否则“继续执行”可能只是快速跳过了真正需要重新做的工作。

阶段名称同样需要实际含义。“已发送”说明发出请求,不表示已取得结果;“草稿已保存”说明对应文件完成保存,不表示内容已经审核。B16开篇只确认前三份草稿已保存并核对资料对应,恢复后也只能按这一实际阶段登记,不能把它们记为内容已经全部核对通过。

重启后将任务记录与实际草稿对应核对
图 S06-02|重启后将任务记录与实际草稿对应核对

图以D03的输入版本v1、整理要求r1为例,两个编号只用于识别当时的条件。图中的两个来源分别提供不同证据:任务记录告诉程序此前记下了哪个阶段,实际输出告诉程序当前究竟有什么。恢复时把两者对应起来,再判断输入和要求是否仍相同。不能让任意一侧单独决定“跳过这份资料”。

例如,记录写着D03草稿已保存,文件却不在预定位置,恢复程序应报告不一致,查找或重新安排这一项。文件在,但只写了一半,也不能按已完成跳过。另一种情况是文件已经完整保存,程序恰在更新任务记录前退出;恢复时若能核实其内容、输入对应和阶段要求,就可以补正记录,避免无必要地再次生成。

后一种核实不能只看文件是否存在。工具需要有足够信息把它认作这项任务的成果,并完成当前阶段约定的检查;证据不足,就先作为待核对文件保留。这样既保护可能已经完成的工作,又不会把不知道来源的文件直接算成成果。

小叶还应保留一个明确的待处理清单。D04结果未知,D05至D08未开始,二者不能都写成“失败”。恢复程序对前者先确认已有作用,对后者则可以安排首次执行。两种状态分开,才有机会同时避免重复和遗漏。

九、保存与清理要围绕具体作用安排

如果程序把草稿直接写入正式文件,写到一半退出,文件名已经出现,内容却还不完整。更稳妥的安排是先写到临时位置,完成写入和必要检查,再把它放到约定的正式位置。读者据正式位置取得成果时,就更容易避开正在生成的半成品。

这会用到原子操作的一个用途:将一次对外可见的改变作为整体完成。例如,在支持相应保证的文件系统上,可以把已经准备好的临时文件一次替换到目标路径。Python的os.replace说明给出了成功替换的相关约定,也说明跨文件系统等条件可能导致操作失败。实际实现应遵守所用环境的保证。

这项做法保护的是那一次文件替换,不会顺带把其他记录也一起更新。如果草稿文件已经替换成功,随后任务记录更新失败,仍会出现上一节所说的两边不一致。因此,恢复设计要检查整个交接:先完成了什么,后续哪一步没有完成,重启后怎样判断现有文件。

也不能把“保存调用返回成功”无限扩大成任何断电情况下都绝不会丢失。操作系统、存储设备和所用保存方式决定实际持久化保证。小叶在一般资料工具里至少要明确写入完成后再登记相应阶段,并通过所需的重启检查确认能够接续;有更高可靠性要求时,再依据具体存储能力补足措施。

程序运行中还会占用文件、网络连接等资源。资源清理是结束使用以后把这些资源适当释放,避免一次失败让后面的操作也受阻。Python的with可以帮助一些支持这种用法的对象在离开代码块时完成清理,例如正常退出或发生普通异常时关闭打开的文件;finally也常用于安排必须执行的收尾步骤。

清理的作用要说准确。关闭文件可以释放句柄,却不会自动把写到一半的内容变完整;释放连接不会撤回已经送达远端的通知。进程被强制终止或机器突然断电时,也不能期待普通语言层收尾代码仍然得到运行机会。检查点与临时文件之所以需要能被下一次运行识别,正是因为这次收尾可能来不及完成。

如果一项业务需要几处状态共同改变,还可以考虑事务:在其管理范围内,把一组改变作为全部提交或全部放弃的整体处理。PostgreSQL的事务教程用这种整体性说明事务的一项基本作用。比如同一个数据库里更新任务阶段和对应成果记录,适合根据数据库能力共同安排。事务保护到哪里,取决于哪些操作真正纳入其中;不能只给函数起名“事务保存”,就让文件、远端服务和数据库自动共同成功。

对已经发生、无法直接撤销的作用,可能需要补偿操作,也就是再做一项明确动作来处理前面留下的后果。例如通知已经发出但附件地址有误,可以发送订正信息;这不会让原通知从所有人的记忆或设备里消失。微软关于补偿处理的说明也区分了补偿与完整恢复原状态。补偿本身可能失败,仍需要记录状态并继续处理,不能把它当成时间倒流。

这些办法针对不同问题:临时写入和替换减少半成品暴露,资源清理释放占用,事务协调其范围内的改变,补偿处理已经留下的后果。小叶应先指出哪种作用需要保护,再选择办法;把这些名词都写进需求,并不会自动形成可以恢复的程序。

十、降级与人工接续,要保留真实的完成状态

有时候,故障不能立刻修复,但用户仍有一部分事情可以做。生成服务暂时不可用时,小叶的工具也许还可以展示原资料、已有草稿和待处理清单,让他先阅读和手工整理。这是降级:在部分能力受限时,保留任务仍可接受的一部分功能。

降级需要说明保留了什么、暂时缺了什么。如果页面用原资料替代摘要,应让小叶知道当前显示的是原文;如果只能展示之前保存的草稿,应说明它对应哪份输入。不能为了让页面看起来完整,把“尚未生成”替换成一段没有依据的摘要,再称其为可用的降级结果。

是否可以降级,还由当前用途决定。小叶若只是自己预览资料,原文入口通常有用;若接下来的工作必须比较八份资料在同一标准下的摘要,七份摘要加一份原文就还不是已经完成的比较材料。可以先做部分工作,但最终完成标准不能悄悄降低。

当工具不知道怎样处理时,人工接续也应有明确交接。至少让人知道正在处理哪份资料、失败在哪一步、已有成果在哪里、哪些事实尚未确认,以及可以采取什么动作。只给一张错误截图或一行“联系管理员”,通常没有把接续所需的信息交出去。

如果工具已保留原任务编号、服务也支持查询,面向小叶的提示可以是:“D04的生成结果尚未确认,已保留任务编号;前三份草稿在原位置,后四份尚未处理。可以先查看现有草稿,或在连接可用时查询D04。”如果没有可用于查询的原任务信息,就应将已有线索交给维护者核对,不显示一个实际无法使用的查询动作。这样的提示把已知状态与可做的事连在一起。

给维护者的记录可以更具体,包括批次、资料编号、失败阶段、发生时间、错误类型及相关请求标识。这样的日志用于追踪发生过什么。日志和检查点可以相互配合,但不完全相同:日志可能记下“开始保存”,检查点却应在达到约定阶段后才允许恢复程序据此跳过相应工作。

也不是记录越多越有用。若每一步只写“成功”,没有对象和阶段,留下再多行也无法追踪D04;若把不需要的完整私人资料都写入错误记录,又会让接续材料难以管理。应保存能够定位和核对的问题信息,按任务实际需要保留相应内容。

人工处理以后还要把新结果交回工具。小叶补回D04资料、确认某个已有草稿可用,或者决定暂不处理,都应形成清楚状态。否则下一次自动运行仍只看到旧的“待处理”,可能重复发起已经完成的工作。恢复是人与程序之间的交接,不仅是出现错误时弹出一个按钮。

十一、怎样检查恢复能力真的接上了

程序正常跑通一次,只说明那条正常路径可以完成。要知道异常处理是否有效,需要有意安排几种会改变后续动作的现场,并观察结果。这里可以使用故障注入的思路:在可控制的检查环境中,让某个步骤按指定方式出问题,检验程序是否保持了应该保持的状态。

例如,用测试替身让D04的生成调用等待超时,观察程序会不会把它误写成已生成;准备一份没有写完的JSON,观察是否进入格式处理;让保存位置不可写,观察是否仍然登记“草稿已保存”。这些情况各检查一条不同关系,不能用十次同样的成功调用代替。

还要检查失败发生在动作的不同位置。保存前中断,可能没有正式草稿;草稿保存后、检查点更新前中断,则可能已有可核实的文件。恢复程序若把两者都当作首次执行,就可能重复;若把两者都当作已完成,又可能遗漏。按实际作用的先后选择中断位置,才能检验接续依据是否可靠。

测试替身能安排这些情景,却有相应范围。替身说“已经保存”,不等于真实存储真的保存了;在内存里模拟重启,也不证明退出整个进程后仍能读取状态。因此,需要的实际恢复检查应当包括结束本次运行、重新启动,再从保存下来的记录和文件继续。

用一个小批次就可以先检查关键路径。运行到前三份已有草稿、第四份尚未确认时停止,再启动工具,核对前三份没有被无理由重做、第四份仍被正确标记和确认、后四份没有从清单里消失。这个检查先验证已设计的关系,不需要一开始就搭建大型测试平台。

对于重试,还要看实际调用次数、等待间隔和停止条件,而不是只看代码里写了一个次数变量。若内层客户端本来就会重试,观察可能暴露出比外层设置更多的调用。对于有外部作用的操作,额外核对有没有重复创建、重复发送或错误覆盖,次数符合预期也不能替代作用检查。

小叶可以把失败结果直接交给 AI 定位:给出输入、已确认状态、预期恢复行为和实际观察,要求它解释哪一段处理造成了差异。修改以后再检查对应情形及可能受影响的正常路径。这比反复要求“全面增强健壮性”更容易得到可以验证的改动。

十二、回到B16,让中断后的每一项都有去处

现在重新处理开篇现场。D01至D03已有保存的草稿并核对过资料对应;D04的远端生成结果未知;D05至D08未开始。当前任务仍是八份独立摘要草稿,输入资料和整理要求没有变。

第一步是保留并核实已有成果与任务记录的对应。前三份不因为D04超时就自动作废,也不因为已保存就自动升级成内容全部核对通过。如果重启后发现文件缺失或记录不一致,先处理那一项实际缺口。

第二步围绕D04确认已经发生的作用。工具若保留了可用于服务查询的原任务信息,就据此查状态;若支持符合当前条件的幂等重发,就按真实约定处理。没有足够依据时保持未知,不能随意新建一项请求,再把它叫作确认旧任务。是否接受重新生成候选草稿,也应作为明确选择处理。

第三步判断能否继续D05至D08。若只有D04的源资料或返回格式需要处理,而生成服务可以正常使用,后四份又与它独立,可以继续;若共同的访问条件仍未恢复,就暂停这部分生成,同时让已有资料和草稿仍可查看。下一动作由当前原因决定,不能仅从“D04发生过超时”推断整批永远停或永远继续。

假设后续核对发现D04已经生成了对应草稿,并且成功取得和保存它,工具就更新这一项的实际阶段,再完成后四份。若D04仍无法处理而后四份顺利生成,则批次应交代七份已有草稿、一份待处理,以及那一份的接续原因。这两种是后续可能出现的不同结果,不能提前把其中一种登记为已经发生。

最后,用真实文件、对应任务记录和需要的内容检查决定是否达到交付要求。这样,中断以前的工作得到保留,尚未确定的作用得到核对,未开始的任务仍有安排,必须由人处理的部分也有入口。

异常处理与恢复带来的能力,是让任务在遇到问题时仍然保留清楚的方向。小叶可以更放心地把较长过程交给 AI 辅助完成,因为“接下来做什么”有具体依据,而不只是依赖每一步恰好都成功。

练习与参考分析

练习一:空摘要让程序走完了,问题是否已经处理

工具无法读取D04的源资料,开发者把所有读取异常都改为返回空字符串。后续程序接受空字符串并保存一份空摘要,批次显示八份完成。开发者说,程序现在不会中断,异常处理已经成功。这个结论哪里有问题?

**参考分析:**无法读取资料与资料本来没有可提取内容是不同事实。用空字符串代替读取失败,抹掉了原因和状态;后续保存空摘要也没有完成对D04的整理。主循环走完,只证明程序走到了结尾,不能证明八份资料都得到符合要求的成果。

应保留D04读取失败及具体原因,不让它进入普通已生成路径。是否继续其他资料,要看它们是否独立、是否也受同一原因影响。修复需要重新检查读取环节到批次计数的关系,而不仅是把页面“完成”改成更保守的一句话。

练习二:断网以后重新点提交,为什么出现了两份任务

某服务收到资料后会先建立一个生成任务,再返回编号;该服务没有说明重复提交会被合并。小叶第一次提交后断网,没收到编号,恢复连接后再次提交相同资料,最后看到两个生成任务。有人认为资料和参数完全一样,所以服务器一定违反了幂等性。如何判断?

**参考分析:**不能因为输入看起来一样,就认定接口承诺将两次提交识别为同一意图。题面没有提供去重保证,第一次未收到编号也不能证明任务未建立。两个任务提示可能发生了重复作用,却不足以据不存在的保证判断服务违约。

需要核对服务实际支持的请求识别、查询和重发机制,决定怎样恢复未知状态。若服务无法识别旧请求,小叶应把重新提交可能另建任务纳入处理安排,而不是在页面加一个未被服务器使用的标识就宣布问题解决。今后选择或设计接口时,再明确需要怎样的重试保证。

练习三:只剩三秒,还能照原方案再试一次吗

一次操作总共允许等待十秒。已经花去七秒,原计划再等两秒,然后发起一次最多等待四秒的请求。开发者认为,请求自己的超时设置仍是四秒,所以代码没有问题。应怎样分析?

**参考分析:**剩余的是整个操作的三秒。两秒间隔以后只剩一秒,容不下原定的四秒等待;若完整执行,简化计算会把总等待推到十三秒。每段设置单独成立,不能证明合起来满足十秒要求。

若仍准备再试,需要先确认重复作用可接受、失败原因可能改善,再由实际支持的计时方式限制剩余等待;也可以停止自动尝试并保留未确定结果。即使把最后请求的等待缩为一秒,也不代表远端一定一秒完成或自动取消。题目只给了时间条件,不能直接许诺再次尝试必有结果。

练习四:有草稿文件,为什么恢复程序仍然不能直接跳过

重启后,目录中存在D03对应的草稿文件,但任务记录只写“准备保存”。同时,原资料D03已被替换为另一版内容。有人建议,只要文件名相同就把它记为完成,免得浪费此前工作。需要核对什么?

**参考分析:**文件存在不说明保存完整,也不说明它基于当前版本的D03。记录“准备保存”没有保证达到完成阶段,而输入变化又使旧草稿的适用性成为新问题。直接按文件名跳过,会把可能的旧成果当成本次所需结果。

应核对文件内容是否完整、它对应哪个输入版本和整理要求、当前任务需要哪一版。若旧草稿对旧资料仍有价值,可以保留;本次要求若必须使用新资料,则还要重新处理相应部分。恢复的目标是减少无必要重复,不是无条件沿用一切旧文件。

练习五:通知已发出,回滚记录能不能撤销通知

工具先发出一封通知,再把“已通知”写入数据库。通知已经送达,但写数据库时失败。开发者回滚了数据库事务,认为整个操作已经回到发送之前,可以放心重新发送。这个判断成立吗?

**参考分析:**数据库回滚只能作用于实际纳入该事务的数据库改变。题面中的通知已经送达,回滚那条记录没有撤回收件人得到的通知。此时数据库可能没有完成记录,外部作用却已经发生,正是需要恢复时核对的不一致。

应通过实际支持的发送记录、任务标识或查询能力确认状态,避免把缺少本地记录直接解释为从未发送。若确需订正或其他补偿,要将它作为新的明确动作安排;不能用重新发送代替对旧作用的确认,也不能声称补偿一定使外界恢复原状。

练习六:一份失败以后继续,什么时候反而不正确

最初的工具逐份独立整理八份资料,单份格式错误时可以记下缺项并继续。后来任务改变,要求先从D01生成统一术语表,后七份摘要都必须遵循它。D01生成失败后,程序仍沿用原来的“记下错误,继续后面资料”策略。为什么旧策略不再足够?

**参考分析:**旧策略依赖资料可独立处理。现在后七份需要先取得D01产生的统一术语表,这个条件已经改变;在缺少术语表时继续生成,未必满足新的摘要要求。只记录D01失败,并没有补足后续工作的前提。

应先恢复术语表生成,或者明确调整允许完成的范围。例如暂时只读取后七份原资料并保留待生成状态,而不宣称摘要已经按统一术语完成。若决定接受另一种标准,也要先形成新的任务要求。异常处理策略必须随着实际依赖与完成标准调整,不能把曾经有效的继续方式当成永久通则。