那天做视频画面文字验收时,我遇到的不是识别结果有误,而是负责识别的本地工具不见了。它原本被放在系统临时目录里,平时运行得很安静,直到临时区清理周期到来,文件被清掉。等真正需要验收时,工具入口还在,依赖的可执行文件却已经不存在,只能现场用本机编译工具重新构建。
现场重建当然不是完全做不到。重新准备一段源码,调用系统编译器,等它结束,再拿图片跑一遍,事情还是可以继续。但那种“可以继续”很容易掩盖问题:验收流程本来应该检查视频,最后却先变成了修复环境;时间被消耗在找回工具,而不是判断画面是否合格。
我一开始以为,这只是一个文件保存位置选错了。后来发现,真正要处理的是一条依赖链:源码在哪里,产物放在哪里,缺失时谁负责恢复,恢复以后怎样证明它真的能工作。临时目录里的文件不是依赖本身,只是依赖在某一刻留下的一个结果。
一次清理,为什么会变成现场救火
这个问题的危险之处,在于它平时几乎没有症状。工具已经编译完成,验收脚本也能正常调用,下一次运行甚至可能隔了几天。只要系统清理临时文件的时间还没到,所有人都会误以为这条链路很稳定。
但临时目录的承诺本来就不是持久保存。它适合放缓存、中间文件和可以随时重建的东西,不适合放一个验收流程必须依赖的执行入口。文件消失并不算系统异常,真正的错误是我们把“不保证留下来”的位置,当成了“以后一定找得到”的位置。
如果不处理,后果不只是下次多花几分钟。自动验收可能在最不方便的时候才发现工具缺失,任务会被迫中断;现场重编译还会把编译环境、源码版本和当前机器状态一起带进结果。最后即使验收通过,也很难判断通过的是视频,还是某次临时修复恰好成功。
这件事让我重新意识到,可靠性不等于今天运行正常。一个依赖本地工具的流程,至少要回答一个更难的问题:当这台机器忘记了上一次准备好的状态,它能不能按原来的规则恢复到可验证状态?
表面缺的是二进制,实际缺的是来源
最容易误判的是把问题描述成“编译产物丢了”。如果只盯着产物,就会自然想到再复制一份,或者把这次编译出的文件换个地方放好。这能解决眼前一次消失,却没有解决下一次如何重建。
当时的现场经验已经说明,工具是可以用系统自带的编译器从源码重建的。既然源码才是恢复的起点,就不能让它只藏在某段提示文字、聊天记录或某个人的记忆里。文字里“可以这样编译”和一个随流程保存的源文件,看起来只差一步,实际上代表了完全不同的恢复能力。
提示文字会随着上下文被改写,也可能因为人没有看到那一段而失效;纳入工作技能的源码则能和调用它的逻辑一起版本化、检查和维护。后者不保证永远不出错,却让错误有了可追溯的材料。一个本地编译依赖要具备可恢复性,首先要把源码当成资产,而不是把它当成临场说明。
这里还有一个容易被忽略的边界:保存源码并不等于恢复完成。源码在,但编译器不可用、输出位置不对,或者编译出的程序和调用方约定不一致,流程仍然会停住。所以来源只是第 1 层,后面还需要稳定产物和明确的启动动作。
我没有继续依赖临时目录
错误捷径其实很诱人:把当前已经编译好的可执行文件复制到临时目录,出了问题再手工重建。这样改动最少,也能让今天的检查先跑完。可我没有选择它,是因为它把恢复责任继续留给了下一个执行者,而下一个执行者通常只会在任务已经开始时才知道文件没了。
我选择把编译源码纳入工具自身的目录,把产物放到同一套工作内容可以稳定访问的持久位置,并在启动时检查产物是否存在。如果缺失,就自动编译一次,再继续后续工作。这样做的理由不是追求完全无人干预,而是把必然会发生的恢复动作提前设计好,让它发生在一个可预期的入口,而不是发生在验收中途。
持久路径也不是“随便找个不容易被清理的地方”。它应该和工具的版本、权限与更新方式相容,调用方能通过固定规则找到它,编译失败时还能留下清楚的错误。否则只是从一种隐含依赖换成另一种隐含依赖。
我更愿意把这看成一份启动契约:启动时先确认产物是否存在,再确认它是否能被当前流程使用;缺失时执行唯一的恢复路径;恢复后再进入正式验收。自修复不是把所有错误都吞掉,而是把已知的缺失状态变成可重复、可观察的启动步骤。
存在不等于可用,真实图片才是验收边界
如果启动检查只写成“文件存在”,看起来已经比临时目录好很多,实际仍然可能漏掉关键问题。文件可能是空的,可能没有执行权限,可能编译后输出格式变了,也可能程序能够启动却没有返回调用方需要的内容。
所以这次修复里,最后一步不能停在检查产物路径。要用真实图片跑一次完整调用,确认程序能够读取输入、完成画面文字识别,并以调用方约定的格式输出结果。这里的真实图片不需要复杂,它只需要真的经过完整链路,而不是用一个假的成功返回替代。
这一步尤其重要,因为本地工具的契约经常分散在多个地方。调用方可能依赖一行一条的输出,可能依赖输出中包含输入路径,也可能依赖空结果与错误结果的区别。只看“编译命令返回成功”,验证不到这些行为;只看文件属性,也验证不到它是否与上游真正接得起来。
当时的需求已经给出了足够具体的验收边界:重新编译后要拿真实图片运行,并检查输出格式。这个边界让我避免了另一个捷径——把“编译器没有报错”当成“工具已经恢复”。编译通过证明的是源码能生成某个程序,真实输入通过才证明这个程序仍然履行原来的工作契约。
当然,真实样本也有局限。一次图片验证不能证明所有视频帧都能识别,也不能替代更大范围的回归测试。但它至少把最危险的假成功挡住了:文件在、命令能启动,却没有按约定给出可消费的结果。
把恢复拆成3层,流程才不靠记忆
回头看,这件事可以整理成一个很小的流程。第 1 层是来源层:源码和必要的编译说明随工具保存,不能只依赖提示文本。第 2 层是产物层:编译结果进入稳定路径,启动时有明确的存在性与版本检查。第 3 层是行为层:缺失时自动编译,编译后用最小真实样本验证输入、运行和输出。
这3层不是为了把简单工具包装得复杂,而是分别回答3个不同问题:没有产物时能不能重建?产物重建后能不能找到?找到了以后是不是还能完成原来的工作?把它们混成一个“检查一下工具”的动作,往往会漏掉最关键的一层。
我会把启动顺序固定为:先检查版本和存在性,再决定是否编译;编译成功后检查产物是否真的生成;随后运行最小真实样本,核对输出格式;任何一步失败,都停在对应阶段并报告原因。这样做的价值在于,失败不会再被推迟到视频验收的中间。
这个方法也适用于其他本地编译依赖。比如一个格式转换器、图像处理小程序或音频分析辅助工具,只要它不是系统保证始终存在的标准命令,就应该问同样的问题:源码是否随项目保存,产物是否放在持久位置,缺失时是否能自修复,恢复后是否有真实输入验证。流程成熟的标志,不是它能保住某个二进制文件,而是环境清理以后仍然知道从哪里恢复、恢复到什么状态,以及怎样证明恢复有效。
下次我会怎么做
下次再接入本地编译工具,我会在第 1 次让它跑通时就完成这4件事:把源码放进可维护的工作内容里;确定一个稳定的产物位置;把缺失时的单次编译放进启动入口;准备一个最小但真实的样本做行为验证。不会等到文件消失后,才开始补这些基础设施。
我还会把“启动检查”和“正式任务”分开看。启动检查负责恢复和证明工具可用,正式任务负责处理视频或图片。两者边界清楚以后,验收失败时更容易判断是输入问题、工具恢复问题,还是业务结果问题,不会把所有异常都归因于同一条命令。
这件事让我重新意识到,临时目录之所以危险,不是因为它一定会马上清空,而是因为它让依赖的真实来源变得不可见。只要一次清理就能让流程回到手工救火,说明我们保存的只是结果,没有保存恢复能力。真正值得留下的,也不是某个今天还能运行的文件,而是一条下一次仍然走得通的路。