长时任务与自适应优化
正在读取学习记录…
长时任务的困难常常是“下次从哪里接着做”。把四个订单核对完,可能要分两次运行;程序重启后不能只依赖内存中的位置。检查点就是写在磁盘上的进度:已处理哪些订单,下一项是什么。像整理一叠发票时把已核对的放进独立文件夹,再写下下一张编号,接手的人才能继续。
本例第一次处理订单 A、B,写入检查点 next=2;第二次读回文件,从 C 开始,最终得到 A、B、C、D。自适应优化是根据反馈调整工作方式,比如频繁失败时减小每批数量,成功稳定时再增加;下面先观察检查点恢复,再用明确的失败比例、平均耗时和剩余时间预算计算下一轮批次。
从输入到可核对结果
订单清单是程序里给定的四个ID。每个ID成功后先更新 done 与 next,写入 .pending 文件,再替换检查点。pending 是待提交内容;同目录替换让读者看到完整的新状态,而不必读取写到一半的 JSON。
读取检查点后先要求 next 的类型是整数且不是布尔值,范围在0与清单长度之间,再核对 done 是否恰好等于清单已完成前缀。若换了一套订单清单,应重新建立任务身份,而不能照旧位置继续。fail_on 测试在订单B前抛错,磁盘仍保留已完成的订单A;恢复时从B开始。Anthropic 长任务工程文章说明了分阶段推进、持久进度与每次会话恢复状态的作用。
| 运行时刻 | 磁盘done | 磁盘next | 下次工作 |
|---|---|---|---|
| 开始前 | 空 | 0 | 订单A |
| 第一批后 | A、B | 2 | 订单C |
| 第二批后 | A、B、C、D | 4 | 无剩余 |
| B前失败 | A | 1 | 重试B |
保存并运行这两个文件
在 PowerShell 创建练习目录:
New-Item -ItemType Directory -Force l9-long-tasks
cd l9-long-tasksmacOS 终端:
mkdir -p l9-long-tasks
cd l9-long-tasks把下面第一段完整代码保存为 checkpoint_lab.py,第二段保存为 test_checkpoint_lab.py,两份文件放在这个目录。
import json
from pathlib import Path
import tempfile
BATCH = 2
ITEMS = ["订单A", "订单B", "订单C", "订单D"]
def advance(path, items, batch, fail_on=None):
if batch < 1:
raise ValueError("每批至少处理一项")
# 检查点含已完成ID与下一位置;每项成功后才推进位置。
state = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {"next": 0, "done": []}
next_index = state.get("next")
# bool在Python中也是int的子类,显式type检查可拒绝True这样的损坏位置。
if type(next_index) is not int or not 0 <= next_index <= len(items):
raise ValueError("检查点位置必须为合法整数")
if state.get("done") != items[:next_index]:
raise ValueError("任务清单与检查点不一致")
stop = min(state["next"] + batch, len(items))
for index in range(state["next"], stop):
if items[index] == fail_on:
raise RuntimeError("当前项失败,检查点保留")
state["done"].append(items[index])
state["next"] = index + 1
pending = path.with_suffix(".pending")
pending.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
pending.replace(path)
return state
def adapt_batch(previous, failures, attempts, average_ms, remaining_ms):
if previous < 1 or attempts < 1 or not 0 <= failures <= attempts:
raise ValueError("批次与反馈数量无效")
if average_ms <= 0 or remaining_ms < 0:
raise ValueError("平均耗时需为正,剩余预算不能为负")
# 失败比例达到25%先减半;低于25%则翻倍;再用剩余时间预算限制批次。
affordable = int(remaining_ms // average_ms)
candidate = max(1, previous // 2) if failures / attempts >= 0.25 else previous * 2
return min(candidate, affordable)
if __name__ == "__main__":
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "checkpoint.json"
first = advance(path, ITEMS, BATCH)
print("首次完成:" + ",".join(first["done"]))
resumed = advance(path, ITEMS, BATCH)
print("恢复完成:" + ",".join(resumed["done"]))
print("下次位置:" + str(resumed["next"]))
first_batch = adapt_batch(4, 1, 4, 200, 1200)
second_batch = adapt_batch(first_batch, 0, 2, 200, 600)
print("反馈调度1:失败1/4,均耗200ms,预算1200ms → 批量" + str(first_batch))
print("反馈调度2:失败0/2,均耗200ms,预算600ms → 批量" + str(second_batch))import unittest
from pathlib import Path
import tempfile
import checkpoint_lab as lab
class MechanismTests(unittest.TestCase):
# 正常、边界和失败输入都由这里提供,直接运行即可。
def test_normal_resume(self):
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "state.json"
lab.advance(path, lab.ITEMS, 2)
self.assertEqual(lab.advance(path, lab.ITEMS, 2)["done"], lab.ITEMS)
self.assertEqual(lab.adapt_batch(4, 1, 4, 200, 1200), 2)
self.assertEqual(lab.adapt_batch(2, 0, 2, 200, 600), 3)
def test_boundary_empty(self):
with tempfile.TemporaryDirectory() as folder:
self.assertEqual(lab.advance(Path(folder) / "s.json", [], 1)["next"], 0)
self.assertEqual(lab.adapt_batch(1, 0, 1, 200, 199), 0)
def test_failure_keeps_success(self):
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "s.json"
with self.assertRaises(RuntimeError):
lab.advance(path, lab.ITEMS, 4, "订单B")
self.assertEqual(lab.advance(path, lab.ITEMS, 1)["done"], ["订单A", "订单B"])
with self.assertRaises(ValueError):
lab.advance(path, lab.ITEMS, 0)
# 损坏的磁盘材料:负数、布尔、非整数、越界都在切片前拒绝。
for position in (-1, True, 1.5, 5):
path.write_text(lab.json.dumps({"next": position, "done": []}), encoding="utf-8")
with self.assertRaises(ValueError):
lab.advance(path, lab.ITEMS, 1)
if __name__ == "__main__":
unittest.main()打开 PowerShell,逐行输入下列命令。cd 是进入文件夹,dir 是列出文件;确认列表里有刚保存的两个 .py 文件,再运行 Python。
# 在刚才创建的练习目录检查文件,再运行程序。
ls
python -X utf8 checkpoint_lab.py
python -X utf8 -m unittest -v test_checkpoint_lab.py程序输出:
首次完成:订单A,订单B
恢复完成:订单A,订单B,订单C,订单D
下次位置:4
反馈调度1:失败1/4,均耗200ms,预算1200ms → 批量2
反馈调度2:失败0/2,均耗200ms,预算600ms → 批量3测试末尾应显示 Ran 3 tests 与 OK。若找不到文件,检查当前目录与扩展名;若导入失败,确认测试和主程序在同一文件夹。测试描述的三组材料就在上方代码,不需要另外准备输入。
改一处,再看结果
程序故意在同一个临时目录内调用两次 advance:第一次写盘,第二次重新读盘,因此输出展示真实恢复行为。运行结束后临时目录自动清理,重新执行会从新任务开始。
把 BATCH = 2 改成 BATCH = 1,保存并重跑。两次调用只完成 A、B,下次位置为 2。恢复成 2 后重跑,又会完成四项。批次越小,单次会话需要的时间越短,但会增加会话交接与保存次数;具体批次应来自每项耗时与剩余预算。
完成业务动作和保存检查点之间仍可能发生崩溃。如果真实任务是发送邮件或提交订单,要用业务幂等ID查询已执行结果,再决定是否重试;这比单纯“记住next”多一层约束。多人同时写同一个检查点还需要锁或版本冲突检测。
自适应规则把反馈变成具体数量:上一轮失败比例达到25%时把原批次减半,最低为1;低于25%时翻倍。然后计算预算可容纳数量 = 剩余毫秒 ÷ 平均每项毫秒,向下取整数;下一批取策略数量与预算数量的较小值。预算连一项都放不下时返回0,调度器应保存状态并结束会话。
两轮反馈材料已经给定:第一轮原批次4、失败1/4、均耗200ms、预算1200ms,失败比例25%触发减半为2,预算允许6项,最终取2。第二轮原批次2、失败0/2、均耗200ms、预算600ms,策略翻倍为4,但预算只允许3项,最终取3。adapt_batch 实际计算这两次结果;这些数字是规则练习输入,接入业务时用上一轮执行器测得的反馈替换。
改动后仍在刚才的 PowerShell 目录运行:
python -X utf8 checkpoint_lab.py确认上述变化后,把参数恢复成原值,保存并再次执行这条命令。
把机制接回 Agent
长任务循环每次启动先读任务清单与检查点,选择一项未完成工作,验证结果后保存。批次变化属于调度策略,任务完成条件保持稳定。实际业务可将唯一任务ID、清单版本与每项结果一并保存;恢复时先查询已经发生的业务结果,避免重复副作用。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“长时任务与自适应优化”,哪种做法更稳妥?
对应知识点:L9-long-tasks
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L9-long-tasks
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L9-long-tasks
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L9-long-tasks
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。