
1. Agent 执行到底在哪一步容易断为什么不能直接重跑先聊一个让我印象深刻的场景。之前做一个自动整理行业研报的 Agent流程大概是搜索资料、下载数据、抽取关键指标、生成图表、写正文、最终排版。听起来不难但实际跑起来十次任务有七八次会在中途挂掉。有时候是模型调用超时有时候是某个工具返回了意料之外的格式有时候是上下文太长直接把后面步骤冲昏头。我最早的处理方式简单粗暴失败就重跑整个流程。结果就是前面已经跑完的搜索、下载、抽取全部作废token 烧了一堆时间翻了好几倍而且因为模型有随机性重跑出来的结果跟第一次还不一样排错的成本更高。后来我才意识到这类场景需要的是“断点续跑”而不是“从头再来”。所谓断点续跑就是当 Agent 执行到某一步失败时能精确地定位到失败位置跳过所有已经成功完成的步骤只从失败那一步重新开始。就好比爬山爬到半山腰摔了一跤正常人是站起来继续往上爬而不是退回山脚重新开始。要理解为什么这么重要先得看清楚 Agent 到底是怎么被“打断”的。我把实际踩到的场景归成了三类。1.1 三类典型失败现场第一类是外部依赖不稳定。Agent 的日常操作高度依赖外部接口比如搜索 API、网页抓取、数据库查询、代码执行环境。这一类服务即使再稳定也会有超时、限流、返回非预期格式的时候。而一旦某个中间步骤拿到的是坏数据后面的逻辑就全乱了。这种问题不是你代码写得不好而是外部世界本来就是不确定的。第二类是上下文超长导致的“失忆”。多步骤 Agent 会在对话历史里不断累积信息当累积到一定量级模型要么开始忽略早期关键信息要么直接把长上下文处理得稀里糊涂。尤其是那种一次任务里要读几十个网页、做十几轮分析的场景到后半程效果会明显下滑。此时任务可能并没有“抛异常”但产出的质量已经不符合要求了本质上也是一种失败。第三类是模型自身的不确定性。即使同样的输入、同样的 prompt两次执行的结果也不会完全一致。如果你的步骤之间存在强依赖比如第二步的表格结构是根据第一步的字段生成的那么第一步一旦换了个输出格式第二步很容易就出问题。1.2 重跑为何不可取很多人第一反应是失败了就重启 Agent多试几次总会成功。但只要你实际算一笔账就会发现这个方案在真实项目里根本站不住脚。首先是成本。每一步都在消耗 token重跑前面 N 步相当于把已经付过钱的劳动又买了一遍。遇到长任务重跑一次的成本甚至会超过任务本身的正常消耗数倍。其次是时间。Agent 任务里很多操作是串行的搜索、解析、调用模型每一步都有延迟。一个完整跑下来要十几分钟的任务重跑一次可能就是半个多小时。这在开发调试阶段还能忍一旦到了生产环境用户根本等不起。然后是确定性。重跑不是重放模型每次生成的结果都会有细微差异这会导致同一个任务跑两次得到两套不同的中间数据后续步骤也跟着变。排查问题的时候你很难判断到底哪个环节是稳定可靠的。更隐蔽的一个问题是副作用。很多 Agent 步骤不是纯计算的它会调外部接口、写文件、发消息。如果一个步骤实际上已经执行成功只是响应超时让你误以为失败那么“重跑整个流程”就会造成重复操作。轻则产生重复数据重则把不该发的内容发出去。所以真正的断点续跑不仅要“省”还要“稳”要保证每个动作要么执行一次要么能被安全地跳过。2. 断点续跑的整体设计把长跑拆成短跑想清楚问题的本质之后断点续跑的核心思路就浮出来了永远不要把一个 Agent 任务当成一个不可分割的整体而是从头到尾把它拆成一连串有明确边界、有可保存状态、可单独执行的小步骤。2.1 任务拆解与中间结果落盘拆任务的标准很简单每一步必须有明确的输入和输出并且这个输出能被序列化保存下来。你不能让步骤的中间结果只存在于模型对话的上下文里因为一旦任务中断模型的那个“记忆”就丢了。要让每一步的结果主动落盘写进数据库或者文件系统。举个例子。做研报任务的第一个子步骤是“搜索行业关键词提取排名前十条新闻链接”那这一步的输出应该是一个结构化的链接列表。第二个步骤是“逐个打开链接抽取正文并生成摘要”那它的输出是摘要列表。每个输出都保存成独立的记录。这样当第三个步骤失败时我只需要重跑第三个步骤前两步的成果全部还在。这里有个非常容易犯的错误只保存最终结果不保存中间结果。我也这么干过结果就是任务在最后一步挂了虽然日志里能看到前面所有的输出但流程管理器并没有把这些输出“存下来”恢复的时候根本拿不到。所以落盘这件事必须在设计阶段就做进每个步骤的代码里而不是事后补救。2.2 状态机设计与状态定义有了步骤拆解还需要一套机制来标记每个步骤跑到哪了。我最常用的是经典四态设计pending等待执行、running正在执行、completed已完成、failed失败。每个步骤启动时都是 pending进入执行后变 running成功就置为 completed异常则置为 failed。恢复流程拿到这些状态信息后就能画出一条非常清晰的“进度条”哪些步骤可以跳过哪个步骤需要重跑哪些后续步骤需要跟着失效并重新计算。这里要注意一个细节一旦中间某一步失败并需要重跑那么这条链路后面的所有状态都必须重置。假设第 3 步失败了那么第 4 步和第 5 步的状态必须从 completed 打回 pending因为它们的输入讲不准已经不可靠了。如果不做这一步就会出现“部分成功”的脏状态比全部失败更难排查。2.3 断点粒度怎么选断点不是越细越好。如果把断点设计到模型内部每一次生成上面那本身也是一笔巨大的开销。我的经验是断点粒度应该跟“可验证的最小业务产出”对齐。也就是说一个断点结束后应该有一份可以被检查、被复用的产物而不是一句模型的中间思考。比如“抽取正文并生成摘要”这个可以作为一个断点因为它的产物是一段可以被完整落盘的文本。“生成一个图表”可以作为一个断点因为产物是一个图片文件和对应的配置数据。但如果把“模型在思考过程中调用了一个工具”也当断点那就过度设计了因为工具调用过程本身是即时性的它的产物已经体现在下一步的输出里。粒度选的合适恢复逻辑就简单失败时找到最近的那个未完成步骤从它开始执行它之前的所有步骤产物都可以直接加载。粒度如果选得太粗每次恢复还是会重复跑很多无效劳动选得太细又会在保存和恢复机制上耗费过多精力。3. 核心机制拆解快照、记忆与幂等拆好任务、定了状态机相当于有了骨架。要让断点续跑真正可靠还需要把三个关键机制做实状态快照、记忆重建、幂等控制。3.1 状态快照到底存什么快照不只是存一个“步骤已完成”的标签而是要存这个步骤的全部产出以及后续步骤恢复所依赖的输入信息。一个合格的状态快照应该包含四部分步骤标识、输入数据的引用、输出产物、附带元信息比如执行时间、模型版本、token 消耗。这里有一个容易被忽略的点输入引用比输入副本更重要。大型文本比如网页正文不需要原样塞进数据库存一个文件路径或者对象存储的 key 就够了。而小体量的结构化数据比如列表、字段配置直接内联存更省事。我一般会把“产出物”和“引用”分开产出物存到对象存储或者本地文件目录数据库里只存引用。这样恢复时既不会读取巨量数据拖慢速度也不会丢失关键内容。快照的持久化也要想清楚。最简单是直接写 JSON 文件按任务 ID 存放任务多了之后建议放到数据库里按任务 ID 和步骤 ID 建索引。恢复时只需要一条查询语句就能把整个任务的执行状态和所有中间产物捞出来。3.2 恢复的上下文如何重建这是断点续跑里最微妙的地方。很多人恢复任务时为了让模型“重新理解”任务会把历史对话一股脑塞回上下文。但这样有两个问题一是 token 开销大二是早期信息已经叠了好几层总结反而干扰模型对当前目标的判断。我的做法是重建一种“摘要型上下文”。每个步骤完成时除了保存完整产物还要额外生成一段简短的结果摘要记录这一步做了什么、关键结论是什么。恢复时把前序步骤的摘要按顺序拼成一个“任务简报”再加上当前步骤的详细输入喂给模型。这样模型既有全局视野又能专注于当前要干的事不会被海量历史淹没。打个比方。完整产物就像所有工作底稿摘要就像项目周报。恢复任务时你不需要让新来的接手人把档案室里所有底稿都翻一遍给他看几份周报让他接着干没干完的活这才是合理的。3.3 幂等机制防重放前面说过失败并不一定代表动作没有发生。一个 HTTP 请求可能已经在服务端执行成功了只是响应没回来。所以断点续跑的重试必须处理重复执行的危险。幂等设计是这个问题的唯一解法。具体实现思路分两类。第一类是自然幂等的步骤比如“根据关键词生成一篇文章”两次生成结果虽然文本不同但都是可接受的产出这类步骤重放没有破坏性。第二类是有副作用的步骤比如发送邮件、写入数据库、扣减库存、发布内容这类操作必须带上全局唯一的请求 ID服务端靠这个 ID 去重。如果检测到同一个请求 ID 已经处理过直接返回上次结果不做重复操作。早年间我在这上面吃过亏。一个“自动发布日报”的 Agent 因为接口超时被判定失败我加了个断点续跑结果它重新执行了一遍“发布”动作同一天出现了两份日报。后来我强制要求所有涉及外部写入的步骤都生成并保存幂等键才把这类问题彻底压住。4. 实操一个可落地的断点恢复方案理论说了不少下面给出一套我实践下来比较顺手的落地方案。整体结构是任务状态存数据库中间产物落本地或对象存储执行器循环读取状态并调度步骤恢复时只触发 failed 或 pending 的步骤。4.1 数据结构与存储下面是我常用的最小数据结构用 Python 示意。from dataclasses import dataclass, field from typing import Any, Dict, Optional from enum import Enum import uuid class StepStatus(str, Enum): PENDING pending RUNNING running COMPLETED completed FAILED failed dataclass class TaskRecord: task_id: str field(default_factorylambda: str(uuid.uuid4())) status: str running # 当前指针记录下一步要执行哪个步骤 current_step: str # 全部步骤的产出物引用key 为 step_name artifacts: Dict[str, Dict[str, Any]] field(default_factorydict) # 每个步骤的完整目录 step_status: Dict[str, StepStatus] field(default_factorydict) dataclass class StepRecord: task_id: str step_name: str status: StepStatus StepStatus.PENDING input_ref: Optional[str] None # 输入物引用对象存储 key 或文件路径 output_ref: Optional[str] None # 产出物引用 summary: Optional[str] None # 结果摘要用于恢复上下文 idempotent_key: Optional[str] None # 外部副作用操作的幂等键 retry_count: int 0存储层我用的是关系数据库加本地目录。数据库负责步骤状态和引用的查询本地目录负责存大文本、图表等产物。每次步骤开始前先查当前状态是 completed 就直接跳过是 failed 且重试次数没超限就进入执行逻辑。4.2 恢复主流程伪代码执行器的核心逻辑非常简单核心就一个 while 循环。def execute_task(task_id: str): task load_task(task_id) for step_name, step_impl in pipeline.items(): record task.step_status.get(step_name) if record.status StepStatus.COMPLETED: # 已经完成直接跳过 continue if record.status StepStatus.FAILED: # 说明是恢复场景重试该步骤 if record.retry_count step_impl.max_retries: notify_manual_review(task_id, step_name) return record.retry_count 1 record.status StepStatus.RUNNING save_task(task) # 组装输入前序摘要 当前步骤的原始输入 context build_context( summaries_of_completed_steps(task), load_input(step_impl.input_loader, record) ) try: result step_impl.run(context) # 所有产物立即落盘记录引用和摘要 new_output_ref save_artifact(task_id, step_name, result.output) record.output_ref new_output_ref record.summary result.summary record.status StepStatus.COMPLETED save_task(task) # 如果当前步骤依赖后续产物的数据需要把后续步骤重置为 pending invalidate_downstream_steps(task, step_name) except Exception as e: record.status StepStatus.FAILED record.error str(e) save_task(task) # 返回并等待调度器再次执行 raise几个细节值得单独说说。invalidate_downstream_steps很多人容易漏。假设第 3 步失败重跑后产出内容和之前不一样了那么基于第 3 步结果算出来的第 4 步、第 5 步的产物虽然状态是 completed但数据已经“过期”了。我会在重跑成功的分支里把这个步骤之后所有步骤的状态统一重置为 pending并清除它们的输出引用和摘要。否则恢复完成后任务看起来都完成了实际上某些中间产物是前后矛盾的。还有一点执行器的循环和调度器本身也要具备“可重入”能力。也就是说调度器崩溃重启后能扫描到所有状态为 running 的任务再根据当前指针继续执行。不能出现“进程挂了任务就永远卡在 running”的状态。我的做法是加一个心跳字段任务每次更新状态时顺手更新时间戳恢复时凡是 running 但心跳时间超过阈值比如 5 分钟的任务视为可疑中断置为 failed 并触发恢复。4.3 编排工具的使用心得如果项目用的是现成的 Agent 编排框架很多能力其实已经内置了不需要重复造轮子。以 LangGraph 这一类图编排方案为例它的 checkpointer 机制就是为断点续跑设计的能把每个节点执行后的状态序列化保存下来。任务中断恢复时框架会从断点继续执行剩余节点这点比我手工写循环要省力得多。但用框架不等于万事大吉。我发现几个坑保存的 checkpoint 默认包含完整的对话状态一旦对话历史很长序列化体积会非常大存数据库时容易撑爆字段长度恢复时也会拖慢加载速度。解决办法是开启消息摘要配置让框架在 checkpoint 里只保存压缩后的历史消息或者自己手动精简历史再保存。另外框架内置的恢复通常能解决“上下文还在”的问题但解决不了“副作用重复”的问题。像发送请求、写数据库这类操作框架并不知道是否已经执行过依然需要自己在工具函数里做幂等控制。本质还是我前面说的那套思路只是落在工具层。5. 常见问题与排查技巧实录再聊一些我在实际运行中断点恢复系统后遇到的典型问题和排查思路这些细节不一定写在任何文档里但非常影响稳定性。5.1 高频翻车清单排第一的是“恢复后上下文错位”。这个问题最常见的表现是任务恢复后模型接手的上下文里同时存在旧产物的引用和新产物的路径但模型分不清哪些最新于是在回答里引用了过期数据。排查时我一般先看恢复时的上下文组装代码检查摘要列表里是否过滤掉失效步骤的摘要。很多时候问题就出在“全部 completed 的摘要都拼接进去了”而不是“只拼有效路径的摘要”。排第二的是“幂等键被覆盖”。有些同事会把幂等键写死在步骤定义里比如用步骤名当 key。这会导致同一个任务恢复重试时外部服务检测到相同的 key 直接返回旧结果但旧结果可能是不完整的失败状态。正确做法是幂等键必须和“任务 ID 步骤 ID 重试次数”绑定每次重试生成一个新 key而已经成功但因为网络原因没拿到结果的场景则由外部服务的“原生幂等”兜底。排第三的是“无限重试循环”。如果失败原因不是临时性的比如接口已经彻底变了、参数永远校验不通过那么重试多少次都是白费。所以每个步骤必须设置最大重试次数并且失败次数达到阈值时不要继续自动跑而是进入人工确认队列。生产环境里我一般把自动重试上限设为 2 到 3 次超过之后立即通知人工介入。排第四的是“大状态快照加载过慢”。有些步骤的产出物是几兆甚至几十兆的文本或图片恢复时如果全部读进内存再传给模型token 和延迟都扛不住。解决办法是分级加载恢复时只加载摘要和引用真正的产出物等模型实际要用到某个输入文件时再按需读取。5.2 参数与配置速查我把常用的配置项整理成一个表给直接抄作业用。配置项推荐值说明步骤最大重试次数2超过立即转人工勿硬刚长上下文摘要模式开启防止 checkpoint 体积膨胀心跳超时阈值5 分钟超过视为中断自动标记失败下游失效重置开启重跑成功后重置后续状态幂等键策略任务ID步骤ID重试次数避免重复副作用产物存储策略大文件引用 小数据内联平衡加载速度与存储复杂度模型温度0恢复重跑时保证稳定性优先这几个参数不是拍脑袋定的。以“温度设为 0”为例普通生成任务我会保留一点随机性让结果更有创造力但断点恢复场景里最重要的是稳定复现同一套逻辑、同一个结果。温度设为 0能从源头上减少因为模型随机性导致的二次失败。最后再分享一个我养成的习惯。每次给 Agent 加一个新的“有副作用”的工具我都会第一时间给它配上幂等设计和对应的测试用例。测试逻辑很简单同一个参数调用两次第二次必须直接返回第一次的结果而不是重复执行。这个小习惯帮我把断点续跑这整套机制的可靠度提升了一大截。如果你准备在项目里引入类似方案建议也从这个最小但最核心的部分开始做。