1. 从“能聊天”到“能干活”:任务规划到底在解决什么问题很多人第一次接触 AI Agent,脑子里想的还是“更聪明的聊天机器人”。但真正做过 Agent 项目的人都知道,聊天只是最表层的能力,真正决定一个 Agent 能不能落地干活的,是它能不能把一句模糊的用户指令,拆成一条可执行、可追踪、可恢复的任务链。这就是任务规划(Task Planning)要解决的核心问题。我见过太多 Demo 级别的 Agent,用户说“帮我整理一下这个季度的销售数据并生成报告”,它张口就来一段看似合理的文字,实际上既没读文件,也没做统计,纯靠语言模型“编”。这种 Agent 在演示时很唬人,一旦放到真实业务里立刻露馅。原因很简单:它没有真正的任务规划能力,只是在做文本续写。任务规划的本质,是把“目标”翻译成“步骤序列”,并且每一步都要明确三件事:做什么、用什么工具做、做完之后怎么判断成功。这三件事缺一不可。只说要做什么,那是待办清单;只说用什么工具,那是工具说明书;只有把三者绑定在一起,才构成一个可执行的计划节点。举个具体的例子。假设用户输入是“分析我上传的 CSV 文件,找出异常订单,并给我一份汇总”。一个合格的规划器会把它拆成类似这样的结构:步骤一:读取上传目录,确认 CSV 文件存在且可解析,工具是文件读取器,成功条件是拿到表头和行数。步骤二:对订单数据做字段校验,识别缺失值和格式异常,工具是数据校验函数,成功条件是输出异常字段清单。步骤三:基于业务规则(比如金额为负、时间倒挂、重复订单号)筛选异常记录,工具是过滤逻辑,成功条件是得到异常行集合。步骤四:对异常记录做聚合统计,工具是分组统计,成功条件是生成汇总表。步骤五:把汇总结果格式化成报告文本,工具是模板渲染,成功条件是输出可读报告。你会发现,这个拆解过程里,每一步都有明确的输入和输出。这就是规划器和“随口一说”的本质区别。规划器输出的不是自然语言段落,而是一个结构化的任务图(Task Graph),节点是步骤,边是依赖关系。为什么强调依赖关系?因为真实任务里步骤不是线性的。步骤三依赖步骤二的校验结果,步骤五依赖步骤四的统计结果,但步骤一和步骤二之间可能是强顺序,而某些子任务之间可以并行。如果规划器只会输出一条直线,遇到需要并行或需要回退的场景就会卡死。这里有个很多人忽略的点:规划粒度。拆得太粗,比如“处理数据”一步到位,那执行器根本不知道从哪下手;拆得太细,比如把“打开文件”和“读取第一行”都当成独立步骤,那规划开销会爆炸,而且步骤之间的状态传递会变得极其繁琐。我的经验是,一个步骤应该对应“一次有意义的工具调用或一次明确的判断”,粒度控制在执行器一次能完成、且失败后能独立重试的范围内最合适。还有一个反直觉的结论:规划不是一次性的,而是滚动进行的。很多人以为 Agent 应该先规划好全部步骤再执行,实际上在长任务里,后面的步骤往往依赖前面步骤的真实结果,提前规划太细反而会错。更稳的做法是“粗规划 + 动态细化”:先出一个高层计划,执行到某一步时再根据当前状态细化下一步。这也是后面要讲的状态管理和长任务恢复的基础。2. 状态管理:Agent 的“记忆”为什么总是丢如果说任务规划是 Agent 的骨架,那状态管理就是它的神经系统。我踩过最大的坑,就是早期做 Agent 时把状态全塞进对话历史里,结果任务一长,模型就开始“失忆”,前面确认过的参数后面又搞错,已经完成的步骤又重新执行一遍。这不是模型笨,是状态设计有问题。先明确一个概念:Agent 的状态不等于对话历史。对话历史只是状态的一个来源,而且是最不可靠的那个。真正需要被持久化、被结构化管理的状态,至少包含以下几类:任务状态:当前处于哪个步骤、哪些步骤已完成、哪些在进行中、哪些失败了。数据状态:中间产出的数据放在哪里,是内存变量、临时文件还是数据库记录。环境状态:当前可用的工具、权限、外部资源是否就绪。决策状态:之前做过哪些关键判断,依据是什么,避免重复决策或前后矛盾。把这四类混在一起塞进 prompt,是灾难的开始。因为 prompt 有长度限制,而且模型对长上下文的注意力是衰减的,越靠前的内容越容易被忽略。正确的做法是把状态外置,用一个结构化的存储来管理,每次只把当前步骤真正需要的那部分状态注入到模型上下文里。我通常会用类似下面这样的结构来组织任务状态:{ "task_id": "task_20240101_001", "goal": "分析CSV并生成异常订单报告", "status": "in_progress", "current_step": 3, "steps": [ {"id": 1, "name": "读取文件", "status": "done", "output_ref": "ctx.file_meta"}, {"id": 2, "name": "字段校验", "status": "done", "output_ref": "ctx.validation"}, {"id": 3, "name": "筛选异常", "status": "running", "output_ref": null}, {"id": 4, "name": "聚合统计", "status": "pending", "output_ref": null}, {"id": 5, "name": "生成报告", "status": "pending", "output_ref": null} ], "artifacts": { "ctx.file_meta": {"path": "/tmp/upload.csv", "rows": 1024}, "ctx.validation": {"missing_fields": ["amount"], "bad_rows": 12} } }注意这里的output_ref,它不直接存数据,而是存一个引用。真正的数据放在 artifacts 里或者外部存储里。这样做的好处是状态本身很轻,可以频繁读写和序列化,而大数据不占用上下文。为什么状态要能序列化?因为长任务恢复全靠它。想象一个任务跑了二十分钟,中间调用了十几次工具,突然进程崩了或者用户关掉了页面。如果没有持久化状态,一切从头再来,前面的工具调用全部白费。而如果状态是结构化的、可序列化的,重启后只要加载状态,就能从current_step继续往下走。这里有个实操心得:状态写入要幂等,且要在步骤边界写,而不是步骤中间写。什么意思?就是每完成一个步骤,立刻把状态落盘;但一个步骤执行到一半时不要写。因为步骤中间的状态是不完整的,恢复时可能处于“做了一半”的尴尬境地,反而更难处理。把步骤设计成“要么没开始,要么已完成”,恢复逻辑会简单很多。还有一个容易忽略的点:状态的版本管理。当 Agent 的逻辑升级后,旧的状态结构可能不兼容。我建议在状态里加一个schema_version字段,加载时先做版本判断和迁移,避免升级后老任务全部报错。这个坑我在一次迭代中踩得很惨,几十个正在跑的长任务因为状态字段改名全部恢复失败。3. 长任务恢复:断点续跑背后的完整机制长任务恢复是 Agent 从“玩具”走向“工具”的分水岭。短任务失败了重跑一遍无所谓,但一个跑了半小时、调用了大量外部资源的任务,如果失败就得从头来,用户体验和成本都受不了。断点续跑不是简单地“记住进度”,它涉及一套完整的机制设计。先说恢复要解决的核心问题:如何判断一个任务该从哪里继续。这听起来简单,实际上要考虑三种失败场景。第一种是进程崩溃,状态还在,直接加载即可;第二种是工具调用超时或失败,需要判断这个步骤是重试还是跳过;第三种是外部状态已经改变,比如文件被删了、数据源更新了,这时候盲目续跑可能得到错误结果。针对这三种场景,我的处理策略是这样的。对于进程崩溃,靠持久化状态恢复,这是最干净的。对于工具失败,要在状态里记录每个步骤的重试次数和最后一次错误信息,恢复时先看这个步骤是否可重试、重试次数是否超限。对于外部状态变化,需要在恢复前做一次“前置校验”,确认当前步骤依赖的输入是否还有效。具体到实现,我一般会把恢复逻辑拆成几个明确的阶段:加载状态:从持久化存储读取任务状态,校验 schema 版本。一致性检查:确认已完成步骤的产出物是否还存在、是否被篡改。定位恢复点:找到第一个未完成或失败的步骤作为恢复起点。重建上下文:把恢复点之前所有步骤的产出引用重新组装成执行上下文。继续执行:从恢复点开始,按正常流程往下走。这里最关键的是第三步和第四步。定位恢复点不能只看current_step,因为可能中间有步骤被标记为失败但实际已经产生了部分结果。更稳的做法是遍历所有步骤,找到第一个状态不是done的步骤。而重建上下文时,要注意只加载恢复点真正依赖的那些产出,不要把全部历史都塞进去,否则上下文又会爆炸。提示:恢复点之前的步骤产出,如果体积很大,建议只加载摘要或引用,真正需要全量数据时再按需读取。这样能显著降低恢复时的内存和上下文压力。还有一个高级技巧:检查点(Checkpoint)与恢复点分离。检查点是定期保存的完整状态快照,恢复点是实际继续执行的步骤位置。两者不一定重合。比如每完成三个步骤存一次检查点,但恢复时可能从第五个步骤继续,这时需要从最近的检查点(第三个步骤)开始,重放第四、第五步的决策。这种设计在步骤执行成本低、但状态保存成本高的场景下特别有用。我实测下来,长任务恢复最容易出问题的地方不是恢复逻辑本身,而是副作用的重放。比如一个步骤是“发送通知邮件”,如果恢复时这个步骤被判定为未完成而重新执行,用户就会收到两封邮件。解决办法是给每个有副作用的步骤加一个幂等键(Idempotency Key),执行前先查这个键是否已经用过,用过就直接返回上次的结果。这个机制在支付、通知、写库等场景里是必须的。4. 规划、状态、恢复三者的咬合关系单独看任务规划、状态管理、长任务恢复,每个都能讲清楚。但真正决定 Agent 好不好用的,是这三者能不能咬合在一起。我见过不少项目,规划器写得很漂亮,状态管理也做了,但恢复时就是跑不起来,问题往往出在接口没对齐。先说规划和状态的接口。规划器输出的步骤,必须带上足够的状态描述,让状态管理器知道每一步要存什么。比如一个步骤声明了output_ref: ctx.validation,那状态管理器就要在执行完这个步骤后,把结果写到这个引用下。如果规划器只输出“校验数据”四个字,状态管理器根本不知道该存什么。所以规划器的输出格式必须是结构化的,每个步骤都要有明确的输入引用和输出引用。再说状态和恢复的接口。恢复逻辑依赖状态里的步骤状态字段,所以状态管理器在更新状态时,必须严格遵循约定的状态机。我建议用一套固定的状态枚举,比如pending、running、done、failed、skipped,不要自己造词。状态流转也要有约束,比如pending只能到running,running只能到done或failed,不能从done回到running。这些约束看起来死板,但能避免大量恢复时的边界问题。最后是规划和恢复的接口。恢复时,规划器可能需要重新规划后续步骤,因为前面的执行结果可能和预期不符。比如原计划第三步筛选异常订单,结果发现数据里根本没有订单号字段,这时候硬着头皮往下走只会得到垃圾结果。正确的做法是恢复时触发一次“重规划”,根据当前状态调整后续步骤。这就要求规划器是可重入的,能接受一个“当前状态”作为输入,输出调整后的计划。把这三者串起来,一个完整的长任务执行流程大致是这样的:接收目标,规划器生成初始计划,状态管理器初始化任务状态。执行器按计划逐步执行,每步开始前把状态置为running,完成后置为done并写入产出。每完成一个步骤,状态落盘,形成检查点。如果中途失败,恢复逻辑加载状态,定位恢复点,重建上下文。恢复时如果发现前置条件变化,触发重规划,更新后续步骤。从恢复点继续执行,直到任务完成或达到终止条件。这个流程里,任何一个环节的接口没对齐,都会导致恢复失败。我的经验是,在开发阶段就要把“模拟崩溃”当成常规测试用例,每写完一个功能就手动杀进程再恢复一次,看看能不能正确续跑。这个习惯能帮你提前发现大量接口问题。5. 面试高频追问与真实项目里的取舍聊到这里,基本的概念和机制都清楚了。但面试里,面试官往往不会只问你“什么是任务规划”,而是会追问一些更刁钻的问题,考察你有没有真正落地过。我整理了几个高频追问,以及我在真实项目里的取舍思路。第一个高频问题:规划器用大模型还是用规则引擎?这其实不是二选一。纯规则引擎在固定流程里很稳,但遇到开放目标就歇菜;纯大模型灵活,但输出不稳定,可能今天拆成五步明天拆成八步。我的做法是混合:用大模型做高层意图理解和粗规划,用规则引擎做步骤的标准化和校验。比如大模型输出“分析数据”,规则引擎把它映射成标准的“读取-校验-统计”三步模板。这样既有灵活性,又有稳定性。第二个问题:状态存哪里?内存、文件还是数据库?取决于任务时长和并发量。短任务、单机跑,内存加定期落盘就够了;长任务、需要跨进程恢复,必须用持久化存储。我一般用轻量数据库存状态元数据,用对象存储或文件系统存大体积产出。不要把所有东西都塞数据库,大字段会拖慢状态读写。第三个问题:恢复时怎么保证不重复执行副作用?前面提过幂等键,这里补充一点:幂等键的生成要包含任务 ID、步骤 ID 和关键参数,确保同一个逻辑操作在任何时候生成的键都一样。另外,幂等记录本身也要持久化,不能只放内存,否则进程一崩就失效了。第四个问题:长任务怎么设置超时和终止条件?没有终止条件的 Agent 是危险的,可能陷入死循环或者无限重试。我通常设三层保护:单步骤超时、单任务总超时、最大重试次数。任何一层触发就终止任务并记录原因。终止不是失败,而是可控的停止,恢复时可以人工介入决定是否继续。第五个问题:怎么评估规划质量?这个很多人答不上来。我的评估维度有三个:步骤覆盖率(计划是否覆盖了目标的所有必要环节)、步骤冗余度(有没有重复或无用的步骤)、恢复成功率(失败后能否正确续跑)。前两个可以离线用测试集评估,第三个只能靠线上真实故障来验证。所以我建议在项目里埋点,记录每次恢复的触发原因和结果,积累一段时间后就能看出规划器的薄弱环节。真实项目里的取舍往往比面试题更残酷。比如为了降低复杂度,我可能会放弃“完全动态重规划”,改成“有限重规划”——只在特定类型的失败(比如输入格式不符)时触发重规划,其他失败直接终止。这样牺牲了一部分灵活性,但换来了可预测性和可维护性。在资源有限的情况下,这种取舍是值得的。6. 一套可复现的最小实现思路讲了这么多原理和取舍,最后给一套可以上手的最小实现思路。不追求生产级完备,但能把规划、状态、恢复三个环节串起来,适合用来练手或者作为项目起点。整体架构分四个模块:规划器、执行器、状态存储、恢复器。规划器负责把目标拆成步骤列表;执行器负责按步骤调用工具;状态存储负责持久化;恢复器负责加载状态并续跑。规划器的最小实现,可以用一次大模型调用完成。给它一个系统提示,要求输出 JSON 格式的步骤列表,每个步骤包含id、name、tool、input_ref、output_ref。解析后用 schema 校验,不合格就重试。这里要注意,提示里必须明确要求输出纯 JSON,不要带解释文字,否则解析会失败。执行器的核心是一个循环:读取当前步骤,把该步骤需要的上下文组装好,调用对应工具,拿到结果后写入状态。工具可以用一个字典注册,键是工具名,值是函数。每个工具函数接收上下文,返回结果和成功标志。状态存储最小实现可以用一个 JSON 文件。每次状态变更后整体写回。虽然效率不高,但胜在简单可靠,适合验证逻辑。等逻辑跑通了再换成数据库。恢复器的逻辑就是前面说的五步:加载、校验、定位、重建、继续。定位恢复点时,遍历步骤找第一个非done的。重建上下文时,把恢复点之前所有done步骤的产出按output_ref组装成一个字典。下面是一个简化的执行循环伪代码,帮你理解整体节奏:def run_task(task_state, tools): while task_state.current_step < len(task_state.steps): step = task_state.steps[task_state.current_step] if step.status == "done": task_state.current_step += 1 continue step.status = "running" save_state(task_state) try: context = build_context(task_state, step) result, ok = tools[step.tool](context) if ok: task_state.artifacts[step.output_ref] = result step.status = "done" else: step.status = "failed" step.retry_count += 1 if step.retry_count > MAX_RETRY: break except Exception as e: step.status = "failed" step.last_error = str(e) save_state(task_state) if step.status == "done": task_state.current_step += 1 return task_state这段代码里,save_state在每个步骤边界调用,保证崩溃后能恢复。build_context只加载当前步骤需要的产出,避免上下文膨胀。重试逻辑放在步骤级别,超过上限就终止。实测下来,这套最小实现能覆盖大部分单机长任务场景。真正上生产还需要补的东西包括:并发控制、幂等键、状态版本迁移、监控埋点。但作为理解规划、状态、恢复三者关系的起点,它足够用了。我在实际项目里最大的体会是:不要一开始就追求完美的规划器。先把状态和恢复做扎实,让任务能稳定跑完、失败能续跑,再去优化规划的智能程度。因为一个规划一般但恢复可靠的 Agent,比一个规划惊艳但一崩就全丢的 Agent,有用得多。这个顺序如果搞反了,后面会花大量时间在补状态管理的债。
1. 从“能聊天”到“能干活”:任务规划到底在解决什么问题很多人第一次接触 AI Agent,脑子里想的还是“更聪明的聊天机器人”。但真正做过 Agent 项目的人都知道,聊天只是最表层的能力,真正决定一个 Agent 能不能落地干活的,是它能不能把一句模糊的用户指令,拆成一条可执行、可追踪、可恢复的任务链。这就是任务规划(Task Planning)要解决的核心问题。我见过太多 Demo 级别的 Agent,用户说“帮我整理一下这个季度的销售数据并生成报告”,它张口就来一段看似合理的文字,实际上既没读文件,也没做统计,纯靠语言模型“编”。这种 Agent 在演示时很唬人,一旦放到真实业务里立刻露馅。原因很简单:它没有真正的任务规划能力,只是在做文本续写。任务规划的本质,是把“目标”翻译成“步骤序列”,并且每一步都要明确三件事:做什么、用什么工具做、做完之后怎么判断成功。这三件事缺一不可。只说要做什么,那是待办清单;只说用什么工具,那是工具说明书;只有把三者绑定在一起,才构成一个可执行的计划节点。举个具体的例子。假设用户输入是“分析我上传的 CSV 文件,找出异常订单,并给我一份汇总”。一个合格的规划器会把它拆成类似这样的结构:步骤一:读取上传目录,确认 CSV 文件存在且可解析,工具是文件读取器,成功条件是拿到表头和行数。步骤二:对订单数据做字段校验,识别缺失值和格式异常,工具是数据校验函数,成功条件是输出异常字段清单。步骤三:基于业务规则(比如金额为负、时间倒挂、重复订单号)筛选异常记录,工具是过滤逻辑,成功条件是得到异常行集合。步骤四:对异常记录做聚合统计,工具是分组统计,成功条件是生成汇总表。步骤五:把汇总结果格式化成报告文本,工具是模板渲染,成功条件是输出可读报告。你会发现,这个拆解过程里,每一步都有明确的输入和输出。这就是规划器和“随口一说”的本质区别。规划器输出的不是自然语言段落,而是一个结构化的任务图(Task Graph),节点是步骤,边是依赖关系。为什么强调依赖关系?因为真实任务里步骤不是线性的。步骤三依赖步骤二的校验结果,步骤五依赖步骤四的统计结果,但步骤一和步骤二之间可能是强顺序,而某些子任务之间可以并行。如果规划器只会输出一条直线,遇到需要并行或需要回退的场景就会卡死。这里有个很多人忽略的点:规划粒度。拆得太粗,比如“处理数据”一步到位,那执行器根本不知道从哪下手;拆得太细,比如把“打开文件”和“读取第一行”都当成独立步骤,那规划开销会爆炸,而且步骤之间的状态传递会变得极其繁琐。我的经验是,一个步骤应该对应“一次有意义的工具调用或一次明确的判断”,粒度控制在执行器一次能完成、且失败后能独立重试的范围内最合适。还有一个反直觉的结论:规划不是一次性的,而是滚动进行的。很多人以为 Agent 应该先规划好全部步骤再执行,实际上在长任务里,后面的步骤往往依赖前面步骤的真实结果,提前规划太细反而会错。更稳的做法是“粗规划 + 动态细化”:先出一个高层计划,执行到某一步时再根据当前状态细化下一步。这也是后面要讲的状态管理和长任务恢复的基础。2. 状态管理:Agent 的“记忆”为什么总是丢如果说任务规划是 Agent 的骨架,那状态管理就是它的神经系统。我踩过最大的坑,就是早期做 Agent 时把状态全塞进对话历史里,结果任务一长,模型就开始“失忆”,前面确认过的参数后面又搞错,已经完成的步骤又重新执行一遍。这不是模型笨,是状态设计有问题。先明确一个概念:Agent 的状态不等于对话历史。对话历史只是状态的一个来源,而且是最不可靠的那个。真正需要被持久化、被结构化管理的状态,至少包含以下几类:任务状态:当前处于哪个步骤、哪些步骤已完成、哪些在进行中、哪些失败了。数据状态:中间产出的数据放在哪里,是内存变量、临时文件还是数据库记录。环境状态:当前可用的工具、权限、外部资源是否就绪。决策状态:之前做过哪些关键判断,依据是什么,避免重复决策或前后矛盾。把这四类混在一起塞进 prompt,是灾难的开始。因为 prompt 有长度限制,而且模型对长上下文的注意力是衰减的,越靠前的内容越容易被忽略。正确的做法是把状态外置,用一个结构化的存储来管理,每次只把当前步骤真正需要的那部分状态注入到模型上下文里。我通常会用类似下面这样的结构来组织任务状态:{ "task_id": "task_20240101_001", "goal": "分析CSV并生成异常订单报告", "status": "in_progress", "current_step": 3, "steps": [ {"id": 1, "name": "读取文件", "status": "done", "output_ref": "ctx.file_meta"}, {"id": 2, "name": "字段校验", "status": "done", "output_ref": "ctx.validation"}, {"id": 3, "name": "筛选异常", "status": "running", "output_ref": null}, {"id": 4, "name": "聚合统计", "status": "pending", "output_ref": null}, {"id": 5, "name": "生成报告", "status": "pending", "output_ref": null} ], "artifacts": { "ctx.file_meta": {"path": "/tmp/upload.csv", "rows": 1024}, "ctx.validation": {"missing_fields": ["amount"], "bad_rows": 12} } }注意这里的output_ref,它不直接存数据,而是存一个引用。真正的数据放在 artifacts 里或者外部存储里。这样做的好处是状态本身很轻,可以频繁读写和序列化,而大数据不占用上下文。为什么状态要能序列化?因为长任务恢复全靠它。想象一个任务跑了二十分钟,中间调用了十几次工具,突然进程崩了或者用户关掉了页面。如果没有持久化状态,一切从头再来,前面的工具调用全部白费。而如果状态是结构化的、可序列化的,重启后只要加载状态,就能从current_step继续往下走。这里有个实操心得:状态写入要幂等,且要在步骤边界写,而不是步骤中间写。什么意思?就是每完成一个步骤,立刻把状态落盘;但一个步骤执行到一半时不要写。因为步骤中间的状态是不完整的,恢复时可能处于“做了一半”的尴尬境地,反而更难处理。把步骤设计成“要么没开始,要么已完成”,恢复逻辑会简单很多。还有一个容易忽略的点:状态的版本管理。当 Agent 的逻辑升级后,旧的状态结构可能不兼容。我建议在状态里加一个schema_version字段,加载时先做版本判断和迁移,避免升级后老任务全部报错。这个坑我在一次迭代中踩得很惨,几十个正在跑的长任务因为状态字段改名全部恢复失败。3. 长任务恢复:断点续跑背后的完整机制长任务恢复是 Agent 从“玩具”走向“工具”的分水岭。短任务失败了重跑一遍无所谓,但一个跑了半小时、调用了大量外部资源的任务,如果失败就得从头来,用户体验和成本都受不了。断点续跑不是简单地“记住进度”,它涉及一套完整的机制设计。先说恢复要解决的核心问题:如何判断一个任务该从哪里继续。这听起来简单,实际上要考虑三种失败场景。第一种是进程崩溃,状态还在,直接加载即可;第二种是工具调用超时或失败,需要判断这个步骤是重试还是跳过;第三种是外部状态已经改变,比如文件被删了、数据源更新了,这时候盲目续跑可能得到错误结果。针对这三种场景,我的处理策略是这样的。对于进程崩溃,靠持久化状态恢复,这是最干净的。对于工具失败,要在状态里记录每个步骤的重试次数和最后一次错误信息,恢复时先看这个步骤是否可重试、重试次数是否超限。对于外部状态变化,需要在恢复前做一次“前置校验”,确认当前步骤依赖的输入是否还有效。具体到实现,我一般会把恢复逻辑拆成几个明确的阶段:加载状态:从持久化存储读取任务状态,校验 schema 版本。一致性检查:确认已完成步骤的产出物是否还存在、是否被篡改。定位恢复点:找到第一个未完成或失败的步骤作为恢复起点。重建上下文:把恢复点之前所有步骤的产出引用重新组装成执行上下文。继续执行:从恢复点开始,按正常流程往下走。这里最关键的是第三步和第四步。定位恢复点不能只看current_step,因为可能中间有步骤被标记为失败但实际已经产生了部分结果。更稳的做法是遍历所有步骤,找到第一个状态不是done的步骤。而重建上下文时,要注意只加载恢复点真正依赖的那些产出,不要把全部历史都塞进去,否则上下文又会爆炸。提示:恢复点之前的步骤产出,如果体积很大,建议只加载摘要或引用,真正需要全量数据时再按需读取。这样能显著降低恢复时的内存和上下文压力。还有一个高级技巧:检查点(Checkpoint)与恢复点分离。检查点是定期保存的完整状态快照,恢复点是实际继续执行的步骤位置。两者不一定重合。比如每完成三个步骤存一次检查点,但恢复时可能从第五个步骤继续,这时需要从最近的检查点(第三个步骤)开始,重放第四、第五步的决策。这种设计在步骤执行成本低、但状态保存成本高的场景下特别有用。我实测下来,长任务恢复最容易出问题的地方不是恢复逻辑本身,而是副作用的重放。比如一个步骤是“发送通知邮件”,如果恢复时这个步骤被判定为未完成而重新执行,用户就会收到两封邮件。解决办法是给每个有副作用的步骤加一个幂等键(Idempotency Key),执行前先查这个键是否已经用过,用过就直接返回上次的结果。这个机制在支付、通知、写库等场景里是必须的。4. 规划、状态、恢复三者的咬合关系单独看任务规划、状态管理、长任务恢复,每个都能讲清楚。但真正决定 Agent 好不好用的,是这三者能不能咬合在一起。我见过不少项目,规划器写得很漂亮,状态管理也做了,但恢复时就是跑不起来,问题往往出在接口没对齐。先说规划和状态的接口。规划器输出的步骤,必须带上足够的状态描述,让状态管理器知道每一步要存什么。比如一个步骤声明了output_ref: ctx.validation,那状态管理器就要在执行完这个步骤后,把结果写到这个引用下。如果规划器只输出“校验数据”四个字,状态管理器根本不知道该存什么。所以规划器的输出格式必须是结构化的,每个步骤都要有明确的输入引用和输出引用。再说状态和恢复的接口。恢复逻辑依赖状态里的步骤状态字段,所以状态管理器在更新状态时,必须严格遵循约定的状态机。我建议用一套固定的状态枚举,比如pending、running、done、failed、skipped,不要自己造词。状态流转也要有约束,比如pending只能到running,running只能到done或failed,不能从done回到running。这些约束看起来死板,但能避免大量恢复时的边界问题。最后是规划和恢复的接口。恢复时,规划器可能需要重新规划后续步骤,因为前面的执行结果可能和预期不符。比如原计划第三步筛选异常订单,结果发现数据里根本没有订单号字段,这时候硬着头皮往下走只会得到垃圾结果。正确的做法是恢复时触发一次“重规划”,根据当前状态调整后续步骤。这就要求规划器是可重入的,能接受一个“当前状态”作为输入,输出调整后的计划。把这三者串起来,一个完整的长任务执行流程大致是这样的:接收目标,规划器生成初始计划,状态管理器初始化任务状态。执行器按计划逐步执行,每步开始前把状态置为running,完成后置为done并写入产出。每完成一个步骤,状态落盘,形成检查点。如果中途失败,恢复逻辑加载状态,定位恢复点,重建上下文。恢复时如果发现前置条件变化,触发重规划,更新后续步骤。从恢复点继续执行,直到任务完成或达到终止条件。这个流程里,任何一个环节的接口没对齐,都会导致恢复失败。我的经验是,在开发阶段就要把“模拟崩溃”当成常规测试用例,每写完一个功能就手动杀进程再恢复一次,看看能不能正确续跑。这个习惯能帮你提前发现大量接口问题。5. 面试高频追问与真实项目里的取舍聊到这里,基本的概念和机制都清楚了。但面试里,面试官往往不会只问你“什么是任务规划”,而是会追问一些更刁钻的问题,考察你有没有真正落地过。我整理了几个高频追问,以及我在真实项目里的取舍思路。第一个高频问题:规划器用大模型还是用规则引擎?这其实不是二选一。纯规则引擎在固定流程里很稳,但遇到开放目标就歇菜;纯大模型灵活,但输出不稳定,可能今天拆成五步明天拆成八步。我的做法是混合:用大模型做高层意图理解和粗规划,用规则引擎做步骤的标准化和校验。比如大模型输出“分析数据”,规则引擎把它映射成标准的“读取-校验-统计”三步模板。这样既有灵活性,又有稳定性。第二个问题:状态存哪里?内存、文件还是数据库?取决于任务时长和并发量。短任务、单机跑,内存加定期落盘就够了;长任务、需要跨进程恢复,必须用持久化存储。我一般用轻量数据库存状态元数据,用对象存储或文件系统存大体积产出。不要把所有东西都塞数据库,大字段会拖慢状态读写。第三个问题:恢复时怎么保证不重复执行副作用?前面提过幂等键,这里补充一点:幂等键的生成要包含任务 ID、步骤 ID 和关键参数,确保同一个逻辑操作在任何时候生成的键都一样。另外,幂等记录本身也要持久化,不能只放内存,否则进程一崩就失效了。第四个问题:长任务怎么设置超时和终止条件?没有终止条件的 Agent 是危险的,可能陷入死循环或者无限重试。我通常设三层保护:单步骤超时、单任务总超时、最大重试次数。任何一层触发就终止任务并记录原因。终止不是失败,而是可控的停止,恢复时可以人工介入决定是否继续。第五个问题:怎么评估规划质量?这个很多人答不上来。我的评估维度有三个:步骤覆盖率(计划是否覆盖了目标的所有必要环节)、步骤冗余度(有没有重复或无用的步骤)、恢复成功率(失败后能否正确续跑)。前两个可以离线用测试集评估,第三个只能靠线上真实故障来验证。所以我建议在项目里埋点,记录每次恢复的触发原因和结果,积累一段时间后就能看出规划器的薄弱环节。真实项目里的取舍往往比面试题更残酷。比如为了降低复杂度,我可能会放弃“完全动态重规划”,改成“有限重规划”——只在特定类型的失败(比如输入格式不符)时触发重规划,其他失败直接终止。这样牺牲了一部分灵活性,但换来了可预测性和可维护性。在资源有限的情况下,这种取舍是值得的。6. 一套可复现的最小实现思路讲了这么多原理和取舍,最后给一套可以上手的最小实现思路。不追求生产级完备,但能把规划、状态、恢复三个环节串起来,适合用来练手或者作为项目起点。整体架构分四个模块:规划器、执行器、状态存储、恢复器。规划器负责把目标拆成步骤列表;执行器负责按步骤调用工具;状态存储负责持久化;恢复器负责加载状态并续跑。规划器的最小实现,可以用一次大模型调用完成。给它一个系统提示,要求输出 JSON 格式的步骤列表,每个步骤包含id、name、tool、input_ref、output_ref。解析后用 schema 校验,不合格就重试。这里要注意,提示里必须明确要求输出纯 JSON,不要带解释文字,否则解析会失败。执行器的核心是一个循环:读取当前步骤,把该步骤需要的上下文组装好,调用对应工具,拿到结果后写入状态。工具可以用一个字典注册,键是工具名,值是函数。每个工具函数接收上下文,返回结果和成功标志。状态存储最小实现可以用一个 JSON 文件。每次状态变更后整体写回。虽然效率不高,但胜在简单可靠,适合验证逻辑。等逻辑跑通了再换成数据库。恢复器的逻辑就是前面说的五步:加载、校验、定位、重建、继续。定位恢复点时,遍历步骤找第一个非done的。重建上下文时,把恢复点之前所有done步骤的产出按output_ref组装成一个字典。下面是一个简化的执行循环伪代码,帮你理解整体节奏:def run_task(task_state, tools): while task_state.current_step < len(task_state.steps): step = task_state.steps[task_state.current_step] if step.status == "done": task_state.current_step += 1 continue step.status = "running" save_state(task_state) try: context = build_context(task_state, step) result, ok = tools[step.tool](context) if ok: task_state.artifacts[step.output_ref] = result step.status = "done" else: step.status = "failed" step.retry_count += 1 if step.retry_count > MAX_RETRY: break except Exception as e: step.status = "failed" step.last_error = str(e) save_state(task_state) if step.status == "done": task_state.current_step += 1 return task_state这段代码里,save_state在每个步骤边界调用,保证崩溃后能恢复。build_context只加载当前步骤需要的产出,避免上下文膨胀。重试逻辑放在步骤级别,超过上限就终止。实测下来,这套最小实现能覆盖大部分单机长任务场景。真正上生产还需要补的东西包括:并发控制、幂等键、状态版本迁移、监控埋点。但作为理解规划、状态、恢复三者关系的起点,它足够用了。我在实际项目里最大的体会是:不要一开始就追求完美的规划器。先把状态和恢复做扎实,让任务能稳定跑完、失败能续跑,再去优化规划的智能程度。因为一个规划一般但恢复可靠的 Agent,比一个规划惊艳但一崩就全丢的 Agent,有用得多。这个顺序如果搞反了,后面会花大量时间在补状态管理的债。