写这段内容之前,我先说一句大实话:如果你已经写过超过 10 个业务脚本,你八成遇到过这种事——某个同步任务跑到第 3000 条数据时,对方接口毫无预兆地返回一次超时;某个批量清洗脚本刚处理了两个文件,第三份文档的分隔符格式完全不符合预期,程序直接崩溃退出。那一次,你可能靠手动修补撑了过去。但这类问题累积到一定规模,你就会意识到:真正值得花时间设计的,不是那些一次就能写对的逻辑,而是那些“第一次没做对之后,系统怎么办”的路径。
Loop Engineering 说的就是这件事。它不是把while True挂在代码里无限空转,也不是简单地在异常处理里加一层try-except-retry。它是一套把“单次执行”升级为“闭环运行”的工程方法:每件事先做一次,然后校验结果,把偏差反馈回系统,再决定是重试、修正、还是放弃。这套思路尤其适合当前大量模型调用和不确定环境下运行的业务流。本教程会从最基础的概念讲起,给出一套可以直接落到工程里用的最小框架,再通过一个完整的文档清洗项目,让你看到闭环设计在真实场景中是怎么运转的。
1. 从一次执行到闭环运行:我理解的 Loop Engineering 到底是什么
过去我们写脚本,默认的思考模型是线性的:准备好输入,执行一次,拿到结果,结束。如果结果不符合预期,那是 bug,继续调试到它“应该正确”为止。这个模型在处理确定性的小任务时完全成立,但在真实系统里,不确定性才是常态。
1.1 先抛一个问题:单次执行凭什么不可靠
我做内部数据同步时,发现一个很有意思的规律:同一个任务,上午跑和下午跑,成功概率完全不同。上午接口负载低,几十毫秒就返回;下午高峰时段,同样的请求可能要超时。如果你只调用一次,超时就是失败,失败就要人工介入。而人工介入的成本,往往比任务本身还高。
更麻烦的是“静默失败”和“语义偏差”。比如清洗一份 Markdown 文档,程序可能顺利跑完,没有抛任何异常,但输出的日期格式依旧是2024/03/10,而不是标准要求里的2024-03-10。再比如让模型生成一段摘要,它“能跑通”,但摘要内容里缺了三分之一的关键信息。这类问题的共性是:异常处理捕获不到它们,因为程序根本没有报错,只是结果不达标。
Loop Engineering 要解决的就是这一类问题。它的核心假设很简单:执行结果是否合格,不能由“是否抛异常”来判定,必须由一个独立的校验环节来判定。校验不通过,就进入下一轮尝试,并且在下一轮尝试时带上上一轮的偏差信息。
1.2 闭环和 for/while 循环不是一回事
很多人一听“Loop Engineering”,觉得这有什么好讲的,不就是写循环吗?这里的区别在于:普通循环是“按次数把同一件事重复做”,闭环是“每次重复都根据上次结果决定下一步怎么做”。
我画过一张对比表,用来解释两种模式的差别,现在直接贴出来:
| 维度 | 一次性脚本 | Loop Engineering 闭环 |
|---|---|---|
| 结果的判定依据 | 无异常即成功 | 独立校验器打分,达标才成功 |
| 失败后的动作 | 进程退出或跳到下一个 | 进入重试队列,带上失败反馈 |
| 迭代是否携带状态 | 每次执行彼此独立 | 前一轮的偏差会写入本轮上下文 |
| 退出条件 | 执行完就结束 | 成功收敛,或达到资源上限 |
| 可观测性 | 关键,出事再看日志 | 每一步都被记录,可回放完整轨迹 |
| 复杂度代价 | 低 | 高,需要额外设计 |
从表格里能看出来,Loop Engineering 本质上是一种“把失败当成正常路径”的设计哲学。它不是在追求永不失败,而是在接受失败必然发生的前提下,让系统具备自我修正的能力。这套思路在支付回调、数据搬运、批量处理、内容生成这些场景里特别适用,因为这些任务的失败点太分散,你不可能在写代码时把所有情况全部列出来。
1.3 什么样的任务适合引入闭环
也不是所有任务都值得做成闭环。如果一个任务的结果确定、执行时间稳定、外部依赖没有波动,那么加循环纯粹是过度设计。我判断一个任务要不要上闭环,主要看三个信号:
第一,执行结果是否存在“评分空间”。也就是说,结果能不能用一个连续或分级的指标去评估,而不是简单的对/错。能做评分,反馈才有意义。第二,失败能不能被分类。至少你要能区分“这次失败是临时的,还是永久性的”。第三,重试是否有概率改善结果。如果外部接口稳定三周没抖动,某次超时大概率是偶发;如果同一个文件连续处理三次结果都一样差,那重试就是在浪费时间。
把这三点想清楚,再开始设计循环。否则你只是在写一个带while的脚本,不是在做一个闭环系统。
2. 闭环设计的四个基础件:任务实例、执行器、校验器、收敛条件
一个可运行的闭环,不管业务多复杂,骨架都可以抽成四个部分:任务实例、执行器、校验器、收敛条件。很多人写循环失败,就是因为只实现了前两个,把后两个扔掉了。
2.1 任务实例:让每次循环都有一张“病历卡”
任务实例是整个循环里最重要的数据结构。它不只是一个入参数组,而是一张不断累积信息的“病历卡”。每个任务在运行过程中做过几次尝试、每次尝试拿到什么分数、最后一次失败的原因是什么,都应该记录在这个实例上。
我用 Python 里的 dataclass 给过一个比较顺手的定义:
from dataclasses import dataclass, field from typing import Any, Optional @dataclass class LoopTask: task_id: str payload: dict max_attempts: int = 3 attempts: int = 0 status: str = "pending" # pending / running / success / dead score: float = 0.0 # 最近一次尝试的校验得分 best_score: float = 0.0 # 历史最高得分,最后兜底用 last_error: str = "" # 最近一次错误信息 history: list = field(default_factory=list) # 完整的尝试轨迹 def record_attempt(self, attempt_no: int, score: float, reason: str = ""): self.history.append({ "attempt": attempt_no, "score": score, "reason": reason }) if score > self.best_score: self.best_score = score这里的history字段是容易被人忽略但价值最高的部分。它让任务具备了“记忆”。当你要做带反馈的重试时,执行器可以直接读取历史记录,判断上一次为什么挂掉。这是闭环从“盲目重试”进化为“聪明重试”的关键一步。
2.2 执行器和校验器:干活的与验收的必须分离
执行器比较好理解,它负责把一个任务真正跑一遍。可以读一个文件,可以请求一个接口,也可以调用某个模型生成一段内容。问题在于,执行器往往只负责“做事”,不负责“判断做得对不对”。
所以必须单独立一个校验器。校验器负责回答四个问题:结果出来了吗、结果合法吗、结果达标吗、如果不达标,差在哪。前三个问题很容易想到,第四个问题才是闭环的引擎。因为只有把“差在哪”翻译成机器可读的原因,反馈机制才成立。
我举一个家政服务的类比。你去干洗店取衣服,如果把衣服塞给店员就走,等到家才发现领口污渍还在,这就是“执行了但没有校验”。而一个合格的闭环,应该当场把衣服翻开检查,发现问题就立刻返工,并且明确告诉店员:“领口第三颗纽扣下方还有一块黄色污渍,这次请重点处理。”第二次处理时,执行器带着这个明确的反馈去做,成功率会高很多。
校验器返回的不应该只是True/False,而应该是一个包含 score 和 reason 的对象:
@dataclass class ValidateResult: passed: bool score: float reason: str = "" @classmethod def ok(cls, score: float = 1.0): return cls(passed=True, score=score, reason="") @classmethod def fail(cls, reason: str, score: float = 0.0): return cls(passed=False, score=score, reason=reason)2.3 收敛条件:循环不能成为永动机
收敛条件是整个闭环的“闸门”,它告诉我们什么时候可以停。最常见的收敛条件有三类:分数达标、尝试次数用尽、总时间预算耗尽。
分数达标是最理想的情况,相当于任务成功完成。尝试次数用尽则意味着系统已经尽力但结果还是不合格,这时候任务应该进入“死信队列”,等待人工或者另一套规则去处理。总时间预算从全局视角防止任务拖垮整个集群,比如一个任务最多执行 10 分钟,超时就中断并标记为超时。
这三类条件要同时生效,缺一个都会出事。只设分数,不设次数,遇到不可修复的任务就会无限循环;只设次数,不设时间,某个执行器调用外部慢接口时,可能磨磨蹭蹭跑两个小时都退不出来。正确的做法是三层都设:单次执行有超时,单任务重试有次数上限,整批任务有 deadline。
3. 保姆级实战:搭建一个不把脏活干坏的文件清洗流水线
理论讲完,下面进入实战。我用自己处理批量 Markdown 文档的场景来演示:一批历史笔记需要清洗格式,标准是日期全部转成YYYY-MM-DD,标题编号必须连续,段落间不允许出现连续空行,同时要求把全角逗号替换成半角逗号。项目就叫“loop_cleaning”,麻雀虽小五脏俱全。
3.1 项目结构和业务约束
项目结构是这样:
loop_cleaning/ ├── engine.py # 通用循环引擎,不关心具体业务 ├── tasks.py # 文件清洗执行器和校验器 ├── main.py # 组装入口,读取目录并跑循环 └── inputs/ # 放待处理的文档之所以要把循环引擎和具体业务拆开,是为了让engine.py可以复用到后续其他项目里。清洗文档是一套闭环,未来做数据搬运是另一套闭环,但循环的控制逻辑完全一样。这就像把发动机做成独立部件,换了车身也能用。
业务规则也很简单:清洗脚本每个文件最多重试 3 次;单次处理超时 5 秒;校验不通过时,把校验器给的原因附加到任务的feedback字段,下次执行时优先处理上次的问题。
3.2 通用循环引擎的实现
先看engine.py。这里我刻意把循环引擎写得很薄,方便你理解骨架:
import time from typing import Callable, Iterable from tasks import LoopTask def run_loop( tasks: Iterable[LoopTask], executor: Callable[[LoopTask], None], validator: Callable[[LoopTask], None], max_attempts: int = 3, backoff_base: float = 1.2, per_task_timeout: float = 5.0, ) -> list: results = [] for task in tasks: while task.attempts < max_attempts and task.status != "success": task.attempts += 1 task.status = "running" start = time.time() try: # 1. 执行 executor(task) # 2. 校验 validator(task) # 3. 根据校验结果决定状态 if task.score >= 1.0: task.status = "success" else: task.status = "running" # 还会进入下一轮 while reason = f"score={task.score:.2f}, {task.last_error}" task.record_attempt(task.attempts, task.score, reason) except Exception as err: task.last_error = str(err) task.record_attempt(task.attempts, 0.0, str(err)) task.status = "running" # 4. 检查单次执行是否超时 elapsed = time.time() - start if elapsed > per_task_timeout: task.last_error = f"timeout {elapsed:.2f}s" task.record_attempt(task.attempts, task.score, task.last_error) break # 5. 重试前等待,避免给外部系统造成压力 if task.status != "success": time.sleep(backoff_base ** task.attempts) if task.status != "success": task.status = "dead" results.append(task) return results注意几个设计细节。我之所以不用一个for attempt in range(max_attempts)而用while,是因为任务有可能跳过某次尝试直接成功,也可能在执行中遇到永久性错误被标记提前退出。while给了这个流程更大的控制弹性。同时,我在每次尝试结束后都检查耗时,一旦超时就break,防止执行器卡在不稳定的外部调用里耗尽整个队列的时间预算。
3.3 执行器和校验器:业务逻辑长什么样
接着看tasks.py。这里定义了两个函数,分别对应清洗动作和验收动作:
import re import engine DATE_RE = re.compile(r"(\d{4})/(\d{1,2})/(\d{1,2})") FULL_COMMA = "," def clean_document(task: LoopTask) -> None: path = task.payload["path"] if task.history: # 如果之前失败过,把最后一条失败原因附加到待清洗内容里 task.payload["feedback"] = task.history[-1]["reason"] with open(path, "r", encoding="utf-8") as f: content = f.read() content = DATE_RE.sub(lambda m: f"{m.group(1)}-{int(m.group(2)):02d}-{int(m.group(3)):02d}", content) content = content.replace(FULL_COMMA, ",") content = re.sub(r"\n{3,}", "\n\n", content) with open(path, "w", encoding="utf-8") as f: f.write(content) def validate_document(task: LoopTask) -> None: path = task.payload["path"] with open(path, "r", encoding="utf-8") as f: content = f.read() problems = [] if not DATE_RE.search(content) and "/" not in content: problems.append("没有发现旧日期格式") if ",") in content: problems.append("仍存在全角逗号") if "\n\n\n" in content: problems.append("仍存在连续空行") task.score = 1.0 if not problems else 0.0 task.last_error = "; ".join(problems) if problems else "ok"这个示例里的clean_document做得逻辑很简单,但它体现了一个重要原则:执行器在重试时,不是把同一段代码原封不动再跑一遍,而是读取task.history判断上次校验器的反馈,然后有针对性地下手。这才是闭环不是“死循环”的根本原因。
3.4 跑起来看效果
main.py只需要把任务列表组装好,再把执行器和校验器传给run_loop:
import os from engine import run_loop from tasks import clean_document, validate_document, LoopTask def build_tasks(input_dir: str) -> list: tasks = [] for name in os.listdir(input_dir): if name.endswith(".md"): tasks.append(LoopTask( task_id=name, payload={"path": os.path.join(input_dir, name)} )) return tasks if __name__ == "__main__": file_tasks = build_tasks("inputs") results = run_loop( file_tasks, executor=clean_document, validator=validate_document, max_attempts=3, backoff_base=1.2, per_task_timeout=5.0 ) for task in results: print(f"{task.task_id}: {task.status}, score={task.score:.2f}, attempts={task.attempts}") for record in task.history: print(f" - {record['attempt']}: {record['score']:.2f} {record['reason']}")你会看到输出结果里带着每一次重试的完整轨迹。比如某个文件第一次跑完得了 0 分,原因是“仍存在全角逗号”,第二次执行时,payload 里已经带上了这条反馈,清洗逻辑就能发现哪里漏了。这就避免了肉眼排查时最容易出的问题:明明循环重试了三次,却因为每次都犯同样的错,等于一次都没重试过。
4. 用回退与分级策略承接外界的不可靠性
闭环里最容易被低估的是“等待多久再重试”这个细节。很多人图省事,直接在 except 里写sleep(1)然后重试。这种固定等待有两个毛病:并发场景下容易形成同步波峰,所有失败任务同时醒过来,把本来就不稳定的服务压垮;对于一些恢复需要十几秒的故障,等 1 秒就重试几乎必然失败。
4.1 指数回退不是玄学
更稳妥的做法是采用指数回退,也就是每次重试的等待时间按指数增长。我用一张表展示三种策略之间的差异,假设基准时间为 1 秒,连续重试 5 次:
| 重试次数 | 固定等待 | 线性增长 | 指数回退(base 2) |
|---|---|---|---|
| 第 1 次 | 1s | 1s | 2s |
| 第 2 次 | 1s | 2s | 4s |
| 第 3 次 | 1s | 3s | 8s |
| 第 4 次 | 1s | 4s | 16s |
| 第 5 次 | 1s | 5s | 32s |
指数回退的核心思想是:如果第一次重试失败,说明问题可能不是一瞬即逝的,而是需要一点时间恢复的系统性故障。等待时间指数拉长,既给系统复原留出空间,也避免了我们自己成为压垮骆驼的最后一根稻草。
上面engine.py里的backoff_base ** task.attempts就是标准的指数回退。base 取 1.2 时,等待序列是 1.2、1.44、1.73,增长温和,适合文件清洗这类本地任务;如果调用的外部接口波动明显,可以把 base 提高到 2,等待序列变成 2、4、8,更保守。
4.2 临时故障和永久故障要分开处理
回退策略有效的前提是“这次失败值得重试”。判断标准是失败类型:临时故障,比如接口超时、连接被重置、目标文件被临时锁定,重试是有利的;永久故障,比如权限不足、鉴权失败、文件不存在、数据格式根本性损坏,重试多少次也无济于事。
如果对永久故障继续走指数回退,结果就是一套循环把资源全部吃光,然后每个任务都死在超时上。正确姿势是定义两类异常或两类校验原因:
class TransientError(Exception): """临时故障:可以重试""" class PermanentError(Exception): """永久故障:直接进入死信队列""" def classify_and_run(task): try: executor(task) except TransientError: task.status = "running" return # 继续重试 except PermanentError: task.status = "dead" task.last_error = "permanent failure, no retry" return在文件清洗项目里,如果某个文件连读取都失败且提示文件不存在,它后面再重试也只是不断报同一个错误。我会让它直接进入死信队列,把资源留给那些“还有机会”的任务。这就像医院分诊:先判断病人是急诊还是普通门诊,而不是让所有人都排在同一个窗口前排队。
5. 让循环产生反馈:把失败经验带回下一次尝试
如果循环只是“执行、失败、等待、再执行”,那充其量算一个有耐心的一次性脚本。真正让 Loop Engineering 有“智能感”的,是反馈机制:把上一次失败的原因变成下一次执行的输入。这部分也是当前很多大模型应用里最常被提起的“自我修正”,本质上就是同一个套路。
5.1 反馈从哪来、怎么注入
反馈有两个来源:一个是校验器的reason,一个是执行器自己抛出的异常信息。问题是这些东西最初是给人看的,要让它变成机器理解的反馈,需要做一次“结构化”,让 executor 下一次执行时可以直接读取。最简单的做法是在任务 payload 里加一个feedback字段,每次失败后写进去,执行器启动时先检查这个字段。
在文件清洗任务里,校验器查出“标题编号不连续”,这个原因被写进task.history[-1]["reason"],下一次执行时执行器读到它,就能针对编号重新扫描。但如果校验器只是返回“格式不对”,执行器拿到这种模糊信息等于没有反馈,所以校验器写 reason 时一定要尽量具体。这是我反复强调的一点:reason 的质量直接决定反馈回路的质量。
5.2 一段带反馈的大模型生成循环
现在的模型调用场景特别适合这一套。模型输出不稳定,一次生成可能漏掉要点,给一次反馈后通常能明显改善。下面这个示例展示的是通用骨架,不绑定任何具体模型服务,你可以直接把它套到自己的代码里:
def generate_with_loop( call_model, # 接受 prompt,返回文本 base_prompt: str, validator, # 接受文本,返回 (score, reason) max_rounds: int = 5, pass_score: float = 0.9, ): feedback = "" best_output = None best_score = -1.0 for round_no in range(1, max_rounds + 1): prompt = base_prompt if feedback: prompt += "\n\n[上一轮校验反馈]\n" + feedback output = call_model(prompt) score, reason = validator(output) if best_output is None or score > best_score: best_output, best_score = output, score print(f"round {round_no}: score={score:.2f}, reason={reason}") if score >= pass_score: return best_output, best_score, round_no feedback = reason return best_output, best_score, max_rounds这里面有一个容易忽略的点:即使最终没有通过分数阈值,函数也会返回历史最高分的输出,而不是返回最后一轮的结果。因为最后一轮可能因为反馈过头而跑偏,历史最高分往往更可用。这个“保留最优”策略,在跑模型生成任务时非常实用。
5.3 反馈也不是万能的
反馈机制有一个致命前提:重试必须有可能在“不同方向”上变好。如果任务本身已经是一个永久故障,或者模型的采样空间里根本没有发生过可改善的偏移,那么反馈只会让系统空转。所以我在实际项目里总会给带反馈的循环加一道前置检查:当某类错误连续出现两次且返回内容几乎一致时,直接终止重试,转入人工兜底。否则循环会进入一种“看起来很努力,实际上横向不动”的无效状态。
6. 在真实系统里防止循环失控的五道护栏
闭环系统最大的风险不在编写循环代码那一下,而在运行一段时间之后,某些条件悄悄改变导致循环失控。这里说的失控不仅是死循环,还包括资源耗尽、订单重复、消息风暴等连锁事故。根据我自己的经验,至少要有五道护栏。
6.1 最大尝试次数和全局 deadline
前面代码里已经体现了max_attempts和per_task_timeout,这是第一道和第二道护栏。单任务次数防止单点无限摩擦,单次超时防止外部调用卡死。但还差一道全局护栏:整批任务的运行期限。
举个例子,后台任务队列里积压了 1000 个文件要处理,每个文件失败后等待时间越来越长,到后面可能一个文件要等 30 秒才能重试。1000 个文件排下来,整批任务要跑好几个小时。如果这个队列预计夜间 2 点前跑完,结果跑到早上 7 点还没收工,运维同事就要骂人了。所以我在run_loop外层会设一个 deadline:
import time queue_deadline = time.time() + 3600 # 这批任务最多跑 1 小时 for task in tasks: if time.time() > queue_deadline: task.status = "dead" task.last_error = "queue deadline exceeded" results.append(task) break # 继续处理 task这个全局 deadline 的好处是:即使某几个任务的回退特别长,也不会让整个队列无限拖延。
6.2 幂等:重试最怕制造重复副作用
重试中最隐蔽的雷是“副作用重复”。同样是发送一条通知,第一次请求其实已经发出去了,但客户端因为超时以为失败,于是重试,结果是用户收到两条通知。文件清洗还好,写文件本身是幂等的;但一涉及发送、扣款、下单这类外部动作,幂等就是硬约束。
幂等设计的原则是“先登记,后执行”。每次任务开始前,先把task_id写入一个已处理表,并带上处理状态;执行器执行前先查表,如果存在且状态为“处理中”,表示上一轮可能已经成功但回调没回来;如果存在且状态为“成功”,直接跳过。用数据库唯一索引来保证即使两个循环实例同时跑,也不会对同一个任务处理两次。
6.3 隔离任务状态,绝不共享可变全局变量
第三道护栏是任务状态隔离。在闭环里,每个任务对象都应有自己的history、score、last_error,任何跨任务共享的可变状态都要极度谨慎。我曾经见过一个案例:开发者用一个全局列表保存所有任务的失败原因,结果 A 任务的重试把 B 任务的失败原因读进了自己的反馈里,整个循环的反馈机制彻底失真。这类 bug 极难排查,因为错误不是必现的,只在并发或批量场景下出现。
如果一定要共享某些东西,比如这次批次的统计指标,用线程安全的计数器或者直接写入外部存储,不要用普通的dict在前端拼来拼去。
6.4 可观测性和失败率熔断
最后一道护栏是给整个循环装上仪表盘。每个任务的重试次数、失败原因分布、平均耗时都要记录。更重要的是一条保险丝规则:当失败率连续多轮高于某个阈值时,自动拉停整个循环。
这个阈值要根据业务定。我常用的是:连续 10 个任务全部失败,或整体失败率超过 60%,就停止调度新任务,切换成告警状态。因为这个时候继续循环下去,只会对外部依赖形成更大的压力。这就好比家里水管漏水,你先要关掉总阀,而不是不停地拿抹布去堵。
7. 进阶组合:从单循环到多级闭环智能体架构
讲到这里,你应该已经能独立设计并实现一个单任务的闭环了。但 Loop Engineering 真正的威力体现在组合上。一个复杂的业务流,往往不是一个大循环搞定,而是由多个不同粒度的循环嵌套、串联、协同构成的。
7.1 把流水线本身变成循环
以文档发布流程为例。一个文档要过三道关:格式清洗、内容审核、合规检查。你可以把每一道关都设计成独立的闭环。格式清洗不合格,回到清洗循环;内容审核不合格,回到重写循环;合规检查不通过,可能需要降级处理。每道关都有自己独立的校验器和重试策略,而上一道关的输出就是下一道关的输入。
这套架构的优势是故障隔离。如果审核服务不稳定,只会影响审核循环的重试节奏,不会拖垮格式清洗循环。整体流水线又可以通过一个“监控循环”不断检查每个环节的成功率,当某个环节失败率异常升高时,向负责人发出提示,甚至自动调整该环节的模型或参数版本。
7.2 一个简单的“工人循环 + 监督循环”结构
我最近在一个内部工具里用了两层循环。第一层叫“工人循环”,负责执行具体的批量任务,跑的就是run_loop。第二层叫“监督循环”,它会定期读取任务池的统计信息,如果发现某个时间段内失败率上升,就调整工人的重试策略,比如把指数回退的基准从 1 拉到 3,或者把某个永久错误直接从重试列表里剔除。
这三个环节互相配合:工人循环保证局部任务能闭环,监督循环保证整体参数动态可调,人工兜底负责处理那些循环融合多次仍然无法解决的疑难杂症。人工的处理结果会写回规则库,成为下一次循环的检验参考。也就是说,系统的每一次大规模运行,都不只是处理了存量任务,还提升了下一次运行的质量。
7.3 别为了“智能”而过度设计
最后说一句收尾的话。闭环是一种很吸引人的工程结构,尤其在做大模型相关应用时,自我修正、多轮迭代这些概念听着很高级。但工程是价值的取舍,不是技术的堆叠。如果你要处理的任务本身很稳定,外部依赖也可靠,那直接用简单脚本就行,不需要强行引入循环,更不需要上多级大脑。
我自己的判断标准是:当系统中任何一个环节,其失败率在 1% 到 30% 之间波动,且失败后重试有概率改善,Loop Engineering 就会显著提升整体成功率。如果失败率低到 0.1%,直接异常处理就够了;如果失败率高于 50%,你应该先解决基础设施稳定性问题,而不是在循环层做文章。
做闭环工程,最终修的不是代码,而是对不确定性的态度。把每次失败都当作一次有效样本,记录下来、反馈回去、重新尝试,在规定的时间和次数内逼近成功——这就是它真正的价值所在。