← 全部极客知识库

工程化 / 大模型应用工程化

14. 设计执行循环:让智能体根据反馈继续,而不是无限重试

原文备份 · 第 16 / 21 篇 · 查看极客时间原知识库 ↗

本篇目录 · 10 节

来源:大模型应用工程化

上一篇,我们给导出任务搭好了最小 Harness:有输入,有工具,有验证,也有保存下来的状态。但它做完一次就停。如果第一份候选的引用不在原文里,而你希望助手依据错误信息再提交一份,接下来应该怎么办?

最容易想到的办法是加一句:“没通过就继续,直到通过为止。”这句话漏掉了不少问题:它是在学习反馈,还是重复相同错误?还有多少预算?是否为了通过测试改坏了预期?如果下一步想把资料发布到外部网站,原来的授权是否包含这件事?

一个有用的执行 Loop,不只是允许继续,还要明确继续的条件和停止的依据。 本篇会在第13篇 Harness 外面增加一个小循环。每次尝试仍运行真正的导出与验证,但候选由作者预先准备,让你能稳定复现成功、停滞、预算耗尽和权限边界。

配套:工作区说明 · 完整 ZIP。主练习完全离线,不需要模型账户。它演示循环控制,不是 Kimi 自动修复的实录,也不是模型能力测评。

一、先分清:你究竟在循环什么

Loop 就是循环。在 AI 项目里,这个词至少可能出现在三个不同层次:

层次 一次循环做什么 笔记助手里的例子
请求重试 再尝试完成同一个请求 一次通信暂时失败后,按既定重试策略处理
应用工具循环 根据工具结果决定下一次动作 查笔记,读检索结果,再决定回答或调用另一个工具
开发迭代 修改方案或实现,再验证 Kimi 改搜索逻辑,运行测试,根据失败证据调整

这三者不能只靠同一个“最多三次”含糊带过。开发迭代做了两轮,不代表模型只请求了两次;一轮可能包含多次模型交互和多个工具。底层客户端如果还会重试,实际请求数又可能增加。

循环也不自动等于 Agent。预先写死的分支和动态由模型选择动作,是不同的控制方式。Anthropic 在工程文章中区分了固定工作流与模型动态决定流程的 Agent,并强调根据环境反馈行动、设置最大迭代次数等停止条件。参考:Building effective agents。

本篇把“选择下一候选”做成作者脚本,帮助我们专心检查外围规则。在真实 Agent 中,可以由模型提出候选或修改建议,但工具是否允许执行、预算是否还够、验证是否通过,都不应只凭模型一句自我评价决定。

二、这次在原项目上加什么

解压后进入 lesson-14 根目录,也就是包含 loop_lab.py 的目录。本篇使用 Python 3.11 或更高版本,仅需标准库和配套文件。

lesson-14/
├── loop_lab.py            # 新增:决定继续还是停止
├── verify_loop.py        # 新增:独立核对循环结果
├── harness.py            # 原单次任务入口
├── verify_harness.py
├── export_review.py
├── verify_export.py
├── docs/loop/TASK.md      # 新增:本轮范围与预算
└── project/              # 原笔记助手

原应用38个文件、导出器、验证器及 Harness 保持不变。应用运行时仍使用 DeepSeek;Kimi 仍是开发助手。我们没有给网页加循环按钮,也没有把真实用户数据送进新流程。

先读当前任务。它对上一轮范围作了一个明确扩展:单次 Harness 遇错仍然停止;外层 Loop 在核对到特定教学失败后,可以在预算内提交另一个固定候选。每次使用新目录,不覆盖上一份证据。

这很像日常开发:一次测试失败了,这次测试当然结束;开发任务是否继续修改,要由外层流程决定。不能因为“还允许迭代”,就把上一轮的失败记录改成通过。

先检查基础环境。

macOS / Linux:

python3 harness.py --check

Windows PowerShell:

py -3 harness.py --check

应看到 ready=true、missing=[]、watched_files=14。这是内层 Harness 的检查,只证明其所需文件存在和 Python 版本符合要求。新循环还会把自己的脚本和任务文档纳入上下文指纹,总共记录16个文件;它们都包含在完整配套中。

三、先跑通一轮“根据反馈改变候选”

macOS / Linux:

python3 loop_lab.py --run repair-01 --scenario repair
python3 verify_loop.py loop-runs/repair-01

Windows PowerShell:

py -3 loop_lab.py --run repair-01 --scenario repair
py -3 verify_loop.py loop-runs/repair-01

第一条预期输出:

{"status": "needs_review", "reason": "mechanical_pass", "semantic_review": "pending", "attempts": 2, "actual_api_calls": 0}

第二条预期输出:

{"result": "pass", "status": "needs_review", "attempts": 2, "semantic_review": "pending"}

打开下面三个目录,你会看到外层与内层记录如何对应:

loop-runs/repair-01/loop.json      # 循环摘要:为什么继续、为什么停止
runs/repair-01-01/                # 第一次:bad-quote,失败
runs/repair-01-02/                # 第二次:valid,通过机械检查

第一次候选使用假引用样例。导出器实际执行校验,发现引用不在原文中,返回 ValidationError,并留下具体错误:“第 1 张卡:引文不在对应笔记原文中。”

外层先调用独立验证,确认这次确实是预期的拒绝,再把错误类型和消息作为反馈交给候选选择函数。repair 情景收到该反馈后,提交事先准备好的 valid 样例。第二次导出和验证通过,整个循环立即停止,保留 needs_review/pending。

这里没有模型现场写出修正内容。 两份样例都早已存在;真实执行的是校验、导出、记录及分支判断。因此结果只能证明“这个循环按设定处理了反馈”,不能证明 Kimi 能一次修好某种错误。

打开 runs/repair-01-02/artifact/review.md 阅读产物,再看 loop.json 中的 attempts:每次应该有候选、子运行路径、状态和必要反馈。只保存 loop.json 不够,摘要所指向的子运行也必须保留。

四、一次合理的继续,应该经过哪些判断

本篇的控制顺序如下:

flowchart TD
    A[检查下一轮是否还有预算] --> B[根据已有反馈提出候选]
    B --> C{动作在允许范围内吗}
    C -->|否| H[交给人判断]
    C -->|是| D[用新的运行名执行Harness]
    D --> E[独立核对产物与证据]
    E -->|证据异常| H
    E -->|机械通过| R[停止并等待内容审核]
    E -->|已确认的教学失败| F{同候选同错误再次出现吗}
    F -->|是| S[停止:没有进展]
    F -->|否| G[保存反馈]
    G --> A

先看预算,再提候选,可以避免额度已经耗尽却还继续策划和执行。先检查动作范围,再运行工具,可以避免“等做完了才判断它不该做”。每次执行后核对真实文件,则避免模型或脚本只凭一句“我完成了”结束任务。

在loop_lab.py 中,候选不包含任意命令。只有固定动作 export 和三个已知样例可以进入内层 Harness。遇到未知异常、记录不一致或无法核对的状态,会停在 needs_human,不会把所有错误都当作可以继续的普通反馈。

这是一个刻意狭窄的循环。如果将来接入代码修改,允许改哪些文件、运行哪些测试、哪些失败可以自动处理,需要在任务里重新定义。不能把“本轮可更换固定样例”扩张成“Agent 可自行重写整个项目”。

五、连续做了两次,不代表取得了进展

现在运行一个始终提交同一份坏候选的情景。

macOS / Linux:

python3 loop_lab.py --run stuck-01 --scenario stuck
python3 verify_loop.py loop-runs/stuck-01

Windows PowerShell 将上面每行开头的 python3 换成 py -3,其他参数保持相同。后续命令也按这个规则使用。

Loop 预期输出:

{"status": "stalled", "reason": "repeated_failure", "semantic_review": "pending", "attempts": 2, "actual_api_calls": 0}

默认最多允许三次尝试,但本次只执行两次。第二次又是同一个样例、同一种错误、同一条错误消息,程序判定没有进展,提前停止。

本课把这三个字段组合成重复标记。因为候选只来自几个固定文件,这个方法足够说明问题。迁移到真实开发时,“同样的错误消息”可能来自不同代码版本;反过来,错误文字不同也可能是同一个根因。可以结合输入版本、代码差异、失败测试和工具参数判断,不能迷信错误文本去重。

一个有用的开发反馈应该足够具体,例如:“内部空格被删除,S10 的预期是空结果,实际命中 A01。”这会指向可检查的行为。只有“还是不对,再优化一下”,很容易让 Agent 再尝试一次几乎相同的修改。

也不要把减少失败数当作唯一进展:为了让五项测试变成零失败而删掉两项测试,并不等于修好了程序。验收标准、测试预期和输入版本需要保持可追溯。

六、预算要在程序里生效,并说清计量单位

先把本来需要两次的 repair 情景限制成一次:

python3 loop_lab.py --run budget-01 --scenario repair --max-attempts 1
python3 verify_loop.py loop-runs/budget-01

预期是 budget_exhausted,原因为 attempt_budget,attempts=1。只有 runs/budget-01-01,不能出现第二次运行。即使作者知道下一个候选可以通过,也不能因此擅自突破上限。

这里的“一次”指一次 Harness 尝试,包含第一次,不是失败后额外重试一次。一次尝试内部最多有导出和验证两步。如果导出已经失败,验证步骤就不会执行,所以尝试次数也不等于成功工具执行数。

再把时间预算设为零:

python3 loop_lab.py --run time-01 --scenario repair --max-seconds 0
python3 verify_loop.py loop-runs/time-01

预期 budget_exhausted/time_budget、attempts=0,只有循环记录,没有任何子运行。这是为了稳定演示“开始前预算已不足”的边界,不能拿它评价程序速度。

本篇默认是最多三次尝试、30秒轮次边界预算。时间通过 time.monotonic() 的两次读数差值计算,不依赖可能被校时改变的日历时间。参考:Python 单调时钟。

不过,它只在下一次尝试开始前检查。如果某个已经开始的工具迟迟不返回,外层无法靠这条检查打断它。即使完成时已超过30秒,这个示例也可能把本次结果保存为待审。因此应叫轮次边界检查,不能宣称“整个任务一定在30秒内结束”。要控制真实外部请求,还需要请求超时、执行器的终止机制及终止后的状态核对。

预算 应回答的问题 本篇是否实现
尝试次数 最多允许启动多少次任务尝试 是,含首次,允许1—5次
轮次边界时间 还有没有时间开始下一轮 是,0—300秒
单次动作时限 已启动的请求或命令多久必须结束 未实现通用强制超时
Token 与金额 每次及累计请求最多消耗多少 未接入真实模型,不假装已有费用封顶

真实调用时,还要考虑底层请求重试、最大输出以及累计消耗;未知 usage 不能当成零费用。Kimi 开发额度和笔记助手的 DeepSeek 额度仍须分开管理。提示词里写“别超过五块钱”,不会自动给调用程序增加硬限制。

七、有些停止,不需要再努力一次

还剩两个很值得运行的情景:

python3 loop_lab.py --run semantic-01 --scenario semantic
python3 verify_loop.py loop-runs/semantic-01
python3 loop_lab.py --run permission-01 --scenario permission
python3 verify_loop.py loop-runs/permission-01

semantic 会在第一次机械通过后停止,状态为 needs_review,而第一张卡片仍把 RAG 的步骤顺序说反了。打开它的 artifact/review.md 对照原文。循环不会为了增加“看起来更聪明”的迭代次数,凭空假定自己有可靠的语义判断;也不会把 pending 改成已通过。

permission 情景则提出一个作者构造的 publish 动作。当前范围只允许本地导出,所以结果应为 needs_human/permission_boundary,尝试次数为零。loop.json 的 blocked_proposal 记录这条提议。程序没有发布器,也没有尝试联系任何网站。

这两个例子回答了不同问题:前者缺少内容审核结论,后者超出了允许动作。它们都不是“多试几次应该就能解决”的普通错误。

人工入口也不能只说“请处理”。可以报告:当前停在哪里、已做哪些动作、哪些文件可以核对、需要用户提供什么。比如:“本轮尚未导出就提议发布,超出只允许本地导出的范围;先澄清任务目标,不能直接执行发布。”这让人能够做一个具体决定。

八、独立验证要核对“没有继续做什么”

六种情景的预期可以集中核对:

情景 停止状态 尝试次数 应重点检查
repair needs_review 2 保留第一次失败及第二次产物
stuck stalled 2 不应有第三次子运行
repair --max-attempts 1 budget_exhausted 1 不能擅自创建第二次
repair --max-seconds 0 budget_exhausted 0 没有子运行
semantic needs_review 1 错误内容仍须人工审阅
permission needs_human 0 越权提议被记录,没有执行

Loop 到达待审状态退出0,其他受控停止通常退出1;目录重名退出2,捕获中断退出130。独立验证器确认停止符合教学预期则退出0。因此,stuck 的验证通过,表示循环正确地停止了,不表示资料导出成功。

verify_loop.py 不导入候选决策函数。它根据固定情景的独立预期,核对次数、顺序、反馈、状态、上下文和真正的子运行,再调用上一篇的验证器检查产物。它还会检查是否有摘要没有记录的同名前缀子运行,防止把多执行的一次从摘要里抹掉。

作者做了这样的反例检查:删除第二次的真实产物、把状态改为 complete、删除摘要中的尝试、虚报调用次数,以及额外创建第三次运行。这些都被拒绝。没有只用执行脚本自己的“通过”消息证明自己正确。

这个验证器针对的是表中固定情景。如果非零时间预算在中途真的耗尽,或者发生中断、未知异常,它会要求人工核查,不能直接套用正常次数预期。验证器的能力边界也需要写清楚。

九、把这套判断带回 Kimi 开发任务

完成离线练习后,你可以选择在本篇目录打开 Kimi 讨论方案:

kimi --plan

2026年10月4日核对了官方命令文档与本机 Kimi Code 2.0.2 的帮助。这里的 --max-attempts、--max-seconds 是课程脚本参数,不要把它们传给 kimi,也不要沿用未经核实的旧 CLI 循环参数。参考:Kimi CLI 命令。

可以给它这样的任务:

读 AGENTS.md、docs/loop/TASK.md、loop_lab.py 和 verify_loop.py。先解释继续与停止条件,给出验证计划。经我确认后,用新名称运行一次 repair 情景,最多两次 Harness 尝试;核对两次子运行与最终产物。不要更改业务代码、样例或验证标准,不调用笔记助手的真实 API,不发布、不连接 MCP。重复失败、预算不足或未知异常就停止,报告证据路径及待处理问题。待审不等于内容正确。

进入执行阶段仍按工具当前界面审核动作。这个提示词明确了任务,但不能硬性限制 Kimi 会话的全部模型请求数;脚本能强制的是其内部的尝试次数。真实 Kimi 交互可能消耗开发额度,作者没有执行这段真实会话。

以后让 Kimi 修复实际代码,可以保留同样的骨架:每轮先说清依据什么失败证据、准备改什么,改完运行指定检查,保留 diff;连续相同失败则停止,验收通过则交付,不无限“再优化一轮”。至于修改文件如何保存成可接手的检查点,下一篇再展开。

十、最可能卡住的地方

现象 常见原因 处理
找不到新脚本 还在lesson-13或内层project 进入lesson-14根目录
重名退出2 已经执行过相同Loop名 留下旧记录,使用新名称
needs_human/unexpected_error 子运行重名、文件变化或证据不一致 看error_type及对应子目录,不自动清空重跑
子运行存在但摘要为running 发生中断或保存失败 核查实际文件,不能认定动作没做
时间预算没立即打断工具 当前只是轮次边界检查 不把它当强制超时器
验证旧记录提示指纹不同 当前文件已经变化 找对应版本,不重写历史指纹

作者在阻断网络的环境中验证了六种情景,以及非法预算、重名保护、中途预算耗尽、注入中断、丢失产物和伪造记录等边界。中途耗时使用可控时钟模拟,中断使用测试注入;没有把它们说成真实服务故障。原应用和 Harness 保持字节不变,没有真实 Kimi / DeepSeek 调用,也没有 Windows 实机或浏览器视觉验证。

最后留三个自检问题:

  1. 最多三次尝试是否意味着模型最多请求三次?不意味着;必须明确计数层级,本篇根本没有调用模型。
  2. 第二次还是同一输入、同一错误,是否应该用完第三次额度?不必,识别到停滞可以提前停止。
  3. 第一轮已经写了文件,却在更新摘要前中断,能否直接重新执行?不能;要先核对已经发生的动作和实际产物。

这时你的项目已经能完成有边界的离线循环,并保留每次尝试的证据。下一篇长任务与断点续做,就从最后这个问题继续:会话或程序中断之后,怎样据实判断接下来该做什么。