别再让 Agent 靠感觉改计划了:我把 Replan 做成了一个可计数的系统事件

最近在做 SkillLite,一个用 Rust 写的自进化 Agent 引擎。做到规划系统这块时,我意识到一件事:

大多数 Agent 系统里,replan 不是一个"事件",它只是模型在下一轮换了个说法。

这对 demo 来说完全够用。但对一个想做自进化的系统来说,这是个根本性的问题------你没法统计、没法复盘、没法让进化引擎从它身上学到任何东西。

所以我把 replan 做成了显式工具调用。我把中间遇到的问题和坑,选型思考,以及和 Claude CodeOpenClawManus 相比,这种方案解决了什么、牺牲了什么写成一个文章总结和大家交流,另外一方面也是做一个记录方便后期复盘。


选型分析:各家的 Replan 到底长什么样

如果只聚焦在 replan 机制本身,我会这样分类:

系统 Replan 触发方式 Replan 的表示形式 任务和工具显式绑定? 核心优势 主要局限
Cursor(Plan Mode) 执行前生成 Markdown 计划,用户确认才执行;执行中 replan 需用户重新输入 执行前的人工确认计划,执行后无自动 replan 无 per-task 工具绑定 人机协作、计划可编辑、适合 IDE 内编码改动 replan 不自主触发、不计数、执行中完全无规划结构
SkillLite 模型显式调用 update_task_plan,失败 / 深度用尽 / 结果无效时系统提示考虑 replan 独立工具调用事件,可计数、可记录 是,每个任务可带 tool_hint 可观测、可度量、适合 evolution 信号沉淀 需要额外清洗逻辑,看起来没有"自然重规划"丝滑
Claude Code 模型调用 TodoWrite 更新 todo,系统通过提醒促使更新 Todo 列表更新,也是显式结构 弱绑定,todo 只描述"做什么" 任务进度可见、人机协作感强、适合长任务推进 对"任务类型 → 工具模式"的沉淀弱,replan 更像进度维护
OpenClaw 根据观察结果自然再决策,必要时借助 Plan Skill / Task Router 重新分解 更偏隐式再决策,无统一 replan 事件 不做 per-task 工具绑定,由执行层和 skill 体系决定 灵活、规划深度可调(L0-L4)、适合复杂协同 replan 很难被定义为离散事件,不利于统计和 evolution
Manus planner 在外部工作区(如 task_plan.md)持续修订路线图(基于公开资料) 工作区中的计划重写,无统一公开 replan API 更像 agent 分工和子任务路由 强 context engineering、适配多 agent、擅长复杂开放任务 对单 Agent 的离散 replan 统计不友好

把五种方案压缩成一句话各自的核心:

  • Cursor 的 replan → 人工重新发消息或编辑计划
  • Claude Code 的 replan → 更新任务板
  • OpenClaw 的 replan → 下一轮重新判断怎么做
  • Manus 的 replan → planner 持续改写外部路线图
  • SkillLite 的 replan → 一次可计数、可审计、可进化的计划替换事件

SkillLite 当前选择的不是最"智能感"的方案,而是对自进化系统的可记录,可追溯的设计。

一、隐式 Replan 的根本问题:你没法学

很多 Agent 系统里 replan 的实际过程是这样的:

  1. 这轮执行失败了
  2. 模型下一轮换了个思路
  3. 你感觉它"好像重新规划了一下"
  4. 但系统里什么记录都没有

这在 demo 里看起来很自然,甚至更聪明。但只要你想让系统真的从执行历史里学习,三个问题会立刻变得很严重。

问题一:你没法定义 replan 是否发生过

失败后重试一次算不算 replan?换了工具算不算?整套计划重写算不算?如果系统里没有明确事件,这些边界全是模糊的。

问题二:你没法统计

  • 这类任务平均需要 replan 几次?
  • 首次成功率和多次 replan 后成功率的差异是多少?
  • 哪些工具序列最容易导致 replan?

没有离散信号,这些问题都只能靠感觉回答。

问题三:你没法复盘,也就没法进化

进化系统最需要的是可比较的历史轨迹。今天第 5 轮改主意,明天第 3 轮就换了------这种行为根本不能作为学习的稳定输入。

所以 SkillLite 的核心设计原则只有一条:

replan 必须是系统层面的正式动作,不能只是模型的思维变化。

二、SkillLite 的实现:显式调用 update_task_plan

SkillLite 的 planning 结构很轻:对话开始前生成一份任务列表,每个任务四个字段:

bash 复制代码
{ id, description, tool_hint, completed }

tool_hint 是和 Claude Code Todo 最大的差异------它不只告诉系统"要做什么",还保留了"原本打算怎么做"的信息。这对 evolution 信号至关重要,后面展开。

执行中,如果发现当前计划失效,模型不是随便换个说法,而是调用:

**update_task_plan**

提交一份新任务数组,替换待执行部分。从这一刻起,replan 变成一个系统里真实存在的事件:replan_count 加一,历史已完成任务保留,新计划经过清洗后落地。

三、代码拆解:三段关键实现

3.1 handle_update_task_plan:replan 是状态修改,不是文字风格变化

代码在 crates/skilllite-agent/src/agent_loop/helpers.rs

rust 复制代码
pub(super) fn handle_update_task_plan(
    arguments: &str,
    planner: &mut TaskPlanner,
    skills: &[LoadedSkill],
    event_sink: &mut dyn EventSink,
) -> ToolResult {
    // 1. 解析 LLM 提交的新任务列表
    // 2. 校验 tasks 不能为空

    // 新计划先过清洗 + 增强,和初始 planning 走同一套逻辑
    planner.sanitize_and_enhance_tasks(&mut new_tasks, skills);

    // 保留已完成任务,只替换待执行部分
    let completed_tasks: Vec<Task> = planner
        .task_list
        .iter()
        .filter(|t| t.completed)
        .cloned()
        .collect();

    let mut merged = completed_tasks;
    merged.extend(new_tasks.clone());
    planner.task_list = merged;

    // 通知事件系统,这次 replan 变成可记录的离散事件
    event_sink.on_task_plan(&planner.task_list);

    ToolResult {
        content: format!("Task plan updated ({} tasks).", new_tasks.len()),
        is_error: false,
        ..Default::default()
    }
}

三个设计决策,我觉得值得注意:

  1. replan 是状态修改planner.task_list 真实改变,不是回复风格变化。
  2. 已完成任务被保留,新计划只替换还没做的部分,历史不会清零。
  3. 新计划不直接执行 ,必须先过 sanitize_and_enhance_tasks 这层防御。

3.2 sanitize_and_enhance_tasks:模型可以提建议,系统负责接住

代码在 crates/skilllite-agent/src/task_planner.rs

rust 复制代码
fn sanitize_task_hints(tasks: &mut [Task], skills: &[LoadedSkill]) {
    for task in tasks.iter_mut() {
        if let Some(ref hint) = task.tool_hint {
            if !Self::is_hint_available(hint, skills) {
                // 模型幻觉出来的 hint,直接剥掉,不让它进入执行链路
                tracing::info!("Stripped unavailable tool_hint '{}' from task {}", hint, task.id);
                task.tool_hint = None;
            }
        }
    }
}

pub fn sanitize_and_enhance_tasks(&self, tasks: &mut Vec<Task>, skills: &[LoadedSkill]) {
    Self::sanitize_task_hints(tasks, skills);
    self.auto_enhance_tasks(tasks);  // 检测缺失步骤并自动补齐
}

这层的存在原因很现实:模型在 replan 时和初始 planning 一样会幻觉。它会写出不存在的 tool_hint,漏掉关键步骤,或者把没完成的任务标成 completed

如果不加清洗,replan 看起来像纠错,实际上只是重新生成了一份新的错误计划。

核心原则:replan 和初始 planning 必须走同一套清洗和增强逻辑。 这两件事在 SkillLite 里不允许有双重标准。

3.3 软上限:Agent 可以反思,但不能无限犹豫

显式 replan 带来一个副作用:模型有时会陷入"不停改计划但不执行"的循环。

SkillLitecrates/skilllite-agent/src/agent_loop/execution.rs 里做了软限制:

rust 复制代码
const MAX_REPLANS_PER_SESSION: usize = 3;

if is_replan {
    state.replan_count += 1;
    let mut r = handle_update_task_plan(arguments, planner, skills, event_sink);
    if !r.is_error && state.replan_count >= MAX_REPLANS_PER_SESSION {
        r.content.push_str(
            "\n\n⚠️ You have replanned 3 time(s). \
             STOP replanning and EXECUTE the current plan step by step."
        );
    }
    r
}

同时,单任务工具调用过深时,系统也给出两条明确出路,而不是只鼓励死磕:

rust 复制代码
pub fn build_depth_limit_message(&self, max_calls: usize) -> String {
    format!(
        "You have used {} tool calls for the current task. \
         Call `complete_task(task_id={})` to record completion, \
         or call `update_task_plan` if the approach is clearly wrong.",
        max_calls, self.current_task().map(|t| t.id).unwrap_or(0)
    )
}

设计逻辑:不硬拦,保留模型自救空间;不放任,防止系统陷入假忙状态。


四、为什么不选其他几条路

为什么不像 Cursor 那样做 Plan Mode

Cursor 的 Plan Mode 是我认为目前编辑器 Agent 里规划做得比较有想法的一套。它的流程是:Shift+Tab 进入规划模式 → 研究代码库 → 提问澄清 → 生成带文件路径的可编辑 Markdown 计划 → 用户确认 → 执行。

这套流程对开发者来说体验很好:计划可见可改,执行有 diff 视图,高风险改动先过人工眼。

但它有一个根本特征:执行阶段就是纯粹的 Agent 工具循环,没有任何 replan 结构。

一旦用户点确认开始执行,Cursor 就进入 Agent Mode------纯 ReAct 循环,每轮最多 25 次工具调用,没有任务列表,没有 tool_hint,没有 mid-execution 的自动 replan 机制。如果中途发现方向不对,唯一的办法是用户重新输入一条消息。

这对编码体验足够了,但对 SkillLite 要做的事不够用:

  • 系统无法在执行中自主识别"计划失效"并触发 replan
  • 没有 replan_count,没有任何 replan 事件记录
  • 没有工具模式信号,evolution 引擎拿不到学习材料
  • 无人值守跑批时,卡住就只能超时,没有 Agent 自救路径

Cursor Plan Mode 解决的是"人怎么更好地把关 Agent 的计划",SkillLite 解决的是"Agent 怎么在执行中自主纠偏并沉淀经验"。 这是两个不同层面的问题。

为什么不直接照搬 Claude Code 的 Todo

Claude Code Todo 的优点很明确:显式、可见、进度实时同步。

但它的 todo 项只有 content/status/activeForm,没有 per-task 工具绑定。

这意味着你知道哪些任务完成了,但系统学不到:

  • 这类任务原本打算用什么工具做
  • 为什么这个工具路径失败了
  • 哪类 task/hint 组合更容易导致 replan

SkillLite 的 evolution 引擎需要这一层信号来学习"任务类型 → 工具选择"的映射。没有 tool_hint,这条学习路径就断了。

Claude Code 的 Todo 更像执行进度结构SkillLite 的 plan/replan 更像可进化的执行信号载体

为什么不选 OpenClaw 的隐式再决策

OpenClaw 的规划体系很有意思:按任务复杂度动态决定规划深度(L0 无计划 → L4 需人工确认),执行中根据观察结果自然再决策,Task Router 还支持并行波次和依赖图。

但"灵活"正是它对 SkillLite 的最大障碍。

如果 replan 是"模型下一轮自然改主意",你就没法稳定定义"这次是否发生了 replan"。一旦这个定义模糊,后面这些事都会变很难:

  • 统计首次成功率 vs. 多次 replan 后成功率
  • 比较不同任务类型的平均 replan 次数
  • 从失败案例里抽取可复用的规则

SkillLite 的 evolution 引擎强依赖这些可计数信号。replan 不是离散事件,整条进化链路的数据基础就不稳了。

OpenClaw 很适合"更强、更灵活的协同智能体",但不适合"单细胞 + evolution 信号提纯"这个目标。

为什么 Manus 的路线也不是当前参考系

从公开资料看,Manus 是强 planner + 强 context engineering + 多 agent 协作的体系。任务路线写进 task_plan.md,结合 notes.mdcontext.md 等外部工作区持续推进,planner 随时改写路线。

这套方案对复杂开放任务非常合适,但有一个前提:你需要的是一个能调度多 agent 的总控系统。

SkillLite 当前想做的刚好相反:一个可复制、可进化、可度量的单 Agent 最小闭环

Manus 对我的启发更多是间接的------外部工作区有价值、context 工程化很重要、planner 和 executor 分层有意义。但在"replan 的离散可计数性"这件事上,它没有提供直接的参考模板。


五、Planning 不只是执行辅助,它是进化系统的接口

做完这轮设计之后,我对 planning 的理解变了。

以前会把它当成"让 Agent 少走弯路"的辅助功能。现在更认同另一种理解:

Planning / Replanning 是执行系统和进化系统之间的接口。

Agent 真正变强,靠的不是抽象意义上的"更聪明",而是:

  • 更会把任务拆对
  • 更会给当前任务配合适的工具
  • 更会在错误路径上及时止损
  • 更会把这次经验迁移到下一次

这些能力必须依赖结构化信号才能沉淀下来。而结构化信号的前提,是 replan 要是一个明确发生过的事件。

从这个角度看,planning 的意义已经不只是"让执行更顺",而是"让系统知道自己是怎么变好的"。

最后

如果你现在也在做 Agent,有一个问题可能需要思考一下:

你系统里的 replan,到底是一个正式动作,还是模型在上下文里的情绪变化?

前者看起来笨一点,但能积累经验。后者看起来更自然,但很难沉淀成系统能力。

至少对 SkillLite 来说,我越来越确定:

让 Agent 显式地改计划,比让它偷偷改主意,更适合做一个真正能进化的工程系统。

欢迎在评论区聊聊你们在做 Agent planning 时踩过的坑,或者对这几种方案有什么不同的判断。

欢迎star:github.com/EXboys/skil...

相关推荐
颜酱2 小时前
理解并查集Union-Find:从原理到练习
javascript·后端·算法
隔壁小邓2 小时前
分布式事务
java·后端
我叫黑大帅2 小时前
如何让两个Go程序远程调用?
后端·面试·go
qqacj2 小时前
如何使用Spring Boot框架整合Redis:超详细案例教程
spring boot·redis·后端
无心水2 小时前
【常见错误】1、Java并发工具类四大坑:从ThreadLocal到ConcurrentHashMap,你踩过几个?
java·开发语言·后端·架构·threadlocal·concurrent·java并发四大坑
惊讶的猫3 小时前
Springboot 组件注册 条件注解
java·spring boot·后端
爆炒西瓜@3 小时前
springboot内存定位,提取数据库账号密码
数据库·spring boot·后端
野犬寒鸦3 小时前
面试常问:什么是TCP连接:虚拟对话通道的奥秘
服务器·网络·后端·tcp/ip·面试·tcpdump
new code Boy3 小时前
NestJS、Nuxt.js 和 Next.js
前端·后端