首页 / 资讯中心 / 文章详情

Agent 的中断与恢复:跑到一半能停下来吗

Agent 的中断与恢复:跑到一半能停下来吗 ★ FEATURED ARTICLE
个人主页 for_ever_love__ 欢迎各位大佬莅临其他栏目: 大模型开发从0到1 其他栏目: iOS项目总结大全 其他栏目: 我想学python了 其他栏目: iOS UI 文章目录Agent 的中断与恢复跑到一半能停下来吗一、为什么中断比想象的难1.1 一次中断到底要停掉多少东西1.2 中断后的三种可能状态1.3 恢复 ≠ 重跑二、中断的几种类型2.1 用户主动中断优雅中断2.2 超时中断系统判定2.3 异常中断被动2.4 部署中断进程被杀三、恢复的技术基础持久化3.1 状态外置是前提3.2 WAL副作用前的意图日志3.3 幂等设计四、中断的实现分层取消4.1 一个可取消的任务树4.2 优雅停止 vs 强制停止五、恢复的两种形态5.1 热恢复暂停后接着跑5.2 冷恢复换个进程接着跑六、什么时候值得做6.1 值得做的信号6.2 不值得的信号6.3 最小可用版七、常见误区小结Agent 的中断与恢复跑到一半能停下来吗一个 Agent 接到「跑全量数据迁移」的任务跑了 20 分钟你发现参数配错了。你点了停止——然后呢如果它不能保留进度你就得从头再来。如果它保留了进度但参数是错的恢复出来的结果就是错的。如果它「停了」但底层子进程还在跑你得到的是一个僵尸任务。长任务 Agent 的中断与恢复比听起来复杂得多。这一篇讲清楚中断有哪几种、恢复需要什么、以及怎么设计才能既安全又实用。一、为什么中断比想象的难1.1 一次中断到底要停掉多少东西一个「跑全量迁移」的任务同时可能跑着一个 Plan-and-Execute 的主循环3 个子任务线程并发跑每个子任务里的 LLM 调用流式响应中2 个 HTTP 请求正在等远端返回1 个本地执行器进程正在跑生成的数据处理代码一个状态写库的后台任务点了「停止」上面每一个都要停。只停主循环下面的还在跑——这就是僵尸任务。1.2 中断后的三种可能状态中断 ┌────┴────┐ ▼ ▼ 放弃 暂停 │ │ ▼ ▼ 丢弃所有 保留现场 进度 │ ├─► 改参数后继续热恢复 └─► 稍后继续冷恢复需持久化暂停才是难点因为它要求「现场」是可保存的。现场包括任务状态、已完成的结果、计划、中间产物、以及下一步该干什么。1.3 恢复 ≠ 重跑很多人把恢复做成「重新执行整个任务跳过已完成的步骤」。这有两个问题副作用会重复。已完成的步骤里如果有「发邮件」「写数据库」重跑就再发一次、再写一次。慢。如果 80% 的步骤已完成重跑意味着 80% 的时间白花。正确的恢复是「从断点继续」而断点的定义取决于副作用是否幂等。二、中断的几种类型按来源分处理策略完全不同。2.1 用户主动中断优雅中断用户点「停止」或「暂停」。这是最理想的情况——你有机会做清理。classInterruptibleExecutor:def__init__(self):self._cancelthreading.Event()defrequest_stop(self,graceful:boolTrue):self._cancel.set()self._gracefulgracefuldefrun_step(self,step):forsubinself._running_subs:sub.check_cancel(self._cancel)ifself._cancel.is_set():raiseGracefulStop(用户请求停止)returnself._execute(step)关键每一层都要能感知到取消信号。主循环取消、子线程取消、正在等的 HTTP 也要能取消。用threading.Event或asyncio.CancelledError。2.2 超时中断系统判定预算超了、步骤超时、总时长超。这类中断必须在进入前就设好上限dataclassclassLimits:max_steps:int50max_tokens:int400_000max_seconds:int1800max_subtasks:int10触发时不能直接抛异常完事——要留下可恢复的现场否则超时中断等于浪费全部投入。2.3 异常中断被动LLM 调用失败、工具报错、进程崩溃。这类最容易出现「半完成」状态步骤 1 成功了步骤 2 失败在中间。处理原则任何副作用操作前先记录意图见第三节的 WAL失败后靠日志判断「刚才那步到底做没做」。2.4 部署中断进程被杀Pod 重启、OOM、发版。这种最残酷——内存里的状态全丢。不持久化就什么都没了。三、恢复的技术基础持久化3.1 状态外置是前提内存里的 Agent 状态不能作为唯一真相来源。必须外置classTaskState(BaseModel):task_id:strstatus:Literal[running,paused,completed,failed,aborted]plan:list[Step]step_results:dict[int,StepResult]# 已完成的步骤产出context_snapshot:str# 压缩后的关键上下文created_at:datetime updated_at:datetime checkpoint_version:int存到 Postgres / Redis / 到一个文件都行关键是每一步完成后立刻写不要攒着。3.2 WAL副作用前的意图日志这是恢复正确性的核心。任何有副作用的动作执行前先写一条「我要做 X」defexecute_side_effect(step:Step,wal:WriteAheadLog):# 1. 先记意图entrywal.append({step_id:step.id,action:step.action,params:step.params,status:pending,ts:now(),})try:# 2. 执行resultperform(step.action,step.params)# 3. 标记完成wal.update(entry.id,statusdone,resultresult)returnresultexceptExceptionase:wal.update(entry.id,statusfailed,errorstr(e))raise恢复时defrecover(task_id:str):stateload_state(task_id)pendingwal.pending_entries(task_id)# 意图写了但没写 done 的forentryinpending:# 这个动作可能已经执行了但没来得及标 doneifis_idempotent(entry.action):replayedperform(entry.action,entry.params)# 幂等重放安全wal.update(entry.id,statusdone,resultreplayed)else:# 非幂等不确定是否执行过必须人工确认flag_for_human_review(entry)abort_recovery(task_id,reason非幂等操作状态未知)returnstate这条规则值得单独记住非幂等的副作用操作状态不明时不要盲猜挂起等人工确认。误判为「没执行」会漏掉操作误判为「已执行」会漏掉补偿两个都可能出生产事故。3.3 幂等设计让恢复变简单的根本办法是让所有操作幂等# ❌ 非幂等defsend_email(to,body):smtp.send(...)# ✅ 幂等带唯一键 去重defsend_email(to,body,idempotency_key):ifdedupe_store.seen(idempotency_key):return{status:already_sent}dedupe_store.mark(idempotency_key)smtp.send(...)# ❌ 非幂等definsert_log(record):db.execute(insert_record,record)# ✅ 幂等自然键 upsertdefinsert_log(record):db.execute( INSERT INTO logs (biz_key, payload) VALUES (:biz_key, :payload) ON CONFLICT (biz_key) DO NOTHING ,record)给每个副作用操作设计一个幂等键恢复时重放就是安全的。这比任何精巧的恢复逻辑都值钱。四、中断的实现分层取消4.1 一个可取消的任务树importasyncioclassCancellationToken:def__init__(self):self._eventasyncio.Event()defcancel(self):self._event.set()defis_cancelled(self)-bool:returnself._event.is_set()asyncdefthrow_if_cancelled(self):ifself._event.is_set():raiseasyncio.CancelledError()classAgentTask:def__init__(self,task_id,token:CancellationToken):self.task_idtask_id self.tokentoken self.subtasks[]asyncdefrun(self):self.token.throw_if_cancelled()forstepinself.plan:ifstep.depends_on:awaitself.gather(step)# 并发子任务awaitself.token.throw_if_cancelled()resultawaitself.execute_step(step)awaitself.checkpoint(result)asyncdefexecute_step(self,step):# LLM 调用也是可取消的returnawaitasyncio.wait_for(self.llm_call(step),timeoutstep.timeout,)三层取消都要覆盖步骤之间检查 token取消就不再进下一步步骤之内asyncio.wait_for超时流式响应要能中断子任务之间把同一个 token 传给所有asyncio.create_task4.2 优雅停止 vs 强制停止defstop(self,graceful:bool):self.token.cancel()ifgraceful:# 等当前步骤收尾最多 5 秒保存状态然后退出awaitasyncio.wait_for(self.finish_current_step(),timeout5)else:# 直接取消状态可能不完整self.token.cancel(forceTrue)self.save_checkpoint()UI 上应该提供两个按钮停止优雅和强制终止。默认优雅长按或二次确认强制。一个常见错误点了停止主流程停了但后台还在跑的子 Agent 没收到取消。所以 token 必须一路传下去。五、恢复的两种形态5.1 热恢复暂停后接着跑任务暂停状态在库里参数可改。用户改完点「继续」asyncdefresume(task_id:str,new_params:dict|NoneNone):stateload_state(task_id)ifstate.status!paused:raiseValueError(只能恢复暂停中的任务)# 1. 处理暂停期间产生的副作用补偿或跳过awaitreplay_pending_wal(task_id)# 2. 如果参数变了重新规划剩余部分ifnew_params:state.planreplan(state,new_params)# 3. 从断点继续tokenCancellationToken()state.statusrunningsave_state(state)awaitAgentTask(task_id,token).run_from(state)新参数怎么作用到已完成的步骤是个设计难点。三种情况参数只影响未完成步骤 → 直接改剩余 plan参数影响已完成步骤 → 需要重算并标记旧结果失效要能识别哪些下游结果依赖它参数影响不可逆的副作用 →不能恢复只能放弃重跑第三种要在 UI 上明确告知用户别让他以为「改个参数接着跑」是万能的。5.2 冷恢复换个进程接着跑进程被杀后新进程接手defcold_start():# 扫描所有 running 但心跳超时的任务staledb.query( SELECT * FROM task_state WHERE status running AND updated_at :threshold ,{threshold:now()-timedelta(minutes5)})fortaskinstale:# 先尝试恢复恢复不了标记 failedtry:resume(task.id)exceptNonIdempotentState:mark_failed(task.id,非幂等状态需人工介入)心跳是冷恢复的关键没有心跳你无法区分「任务真的停了」和「任务还在跑只是慢」。心跳间隔 30~60 秒超时 2~3 个周期判定为僵死。六、什么时候值得做和别的高级特性一样中断恢复有明确的适用边界。6.1 值得做的信号任务运行时间 2 分钟用户等不了中途想停/改有不可逆副作用发消息、写库、花钱部署会重启Pod 滚动更新、OOM任务是长流水线数据迁移、批量生成用户需要在不同设备/时间点接手6.2 不值得的信号单轮对话、秒级返回 → 中断就是取消恢复等于重问纯读操作搜索、查询→ 没有副作用重跑零成本MVP 阶段 → 先把主流程跑通持久化后加6.3 最小可用版如果现在就要做优先级排序状态外置 每步 checkp否则一切免谈幂等键让恢复安全优雅停止用户主动中断超时限制兜底热恢复暂停改参继续冷恢复 心跳进程级容错前两项不做后面全是空中楼阁。七、常见误区误区一把「停止」做成「kill 进程」就完事。后台子任务、未完成的副作用、未保存的状态全都成了隐患。误区二恢复 重跑。副作用会重复。要么幂等重放要么改从断点续。误区三副作用状态不明时自动重放。非幂等操作必须挂起等人工盲猜会出事故。误区四没有心跳。分不清「停了」和「慢了」。误区五参数改了就直接恢复。要判断它影响的是剩余步骤还是已完成步骤。影响已完成且不可逆的只能放弃。误区六暂停不可取消的子任务。token 必须传进每一层包括并发子任务和正在进行的流式调用。误区七只在内存保存状态。进程一死全丢。外置存储是前提。小结中断要停掉的是一整棵树主循环、子任务线程、进行中的 LLM 调用、HTTP 请求、本地进程。只停主循环 僵尸任务。恢复的技术前提是状态外置每步立刻 checkpoint恢复的安全前提是WAL 幂等副作用前先记意图执行后标完成恢复时幂等重放、非幂等挂起等人工。中断分四类用户主动优雅停、超时预设上限、异常半完成靠 WAL 兜、部署进程死靠心跳 冷恢复。热恢复要处理参数变更的影响范围——只影响剩余步骤才好办影响不可逆副作用的只能放弃。落地优先级状态外置 → 幂等键 → 优雅停止 → 超时限制 → 热恢复 → 冷恢复。前两项不做后面都白搭。适用边界任务 2 分钟、有不可逆副作用、部署会重启、长流水线。单轮对话和纯读操作不值得做。长任务 Agent 和演示 Demo 的区别往往就在「能不能停下来、能不能接回去」。这两件事做扎实了Agent 才敢被用在真正有价值的任务上。
阅读完成 · 觉得有帮助?
咨询建站