Skip to content

你从第 04 章进入,还缺 02/03 前置。

自测代码证据锁定

第 4 章:任务规划——把「准备怎么做」变成可验证状态

← 上一章:记忆与 Skill 进化 · 下一章:Subagent

开场:一个「我会分四步做」的客服,真的分四步做了吗?

前几章你让 D 类客服能动手、能记住、会调用 Skill。现在给他派一个多步骤任务:「整理三份资料,先比较差异,再写 Markdown,最后生成 HTML 并确认能打开。」

他会很自然地点头说:「好,我分四步:检索、比较、写文档、生成网页。」——这很正常,因为模型特别擅长把任务列成步骤。

可这里有个陷阱:他把步骤说出来,不等于系统知道自己正在第几步,更不等于这四步真的完成了。 下面这几个问题,光靠他嘴里那份「编号列表」一个都答不了:

  • 他现在做到第几步了?
  • 第 1 步失败后,第 2 步还能偷偷开始吗?
  • 「写好的网页文件」究竟在哪?
  • 他说「做完了」的时候,谁去检查第 4 步?

这一章把自然语言的计划升级成 Harness 里的状态:每一步有稳定的 ID、状态、依赖和完成证据;模型负责建议下一步,代码负责批准状态迁移;模型想结束时,Completion Guard 再核对真实证据。本章结束时,你能回答:「我打算怎么做」和「系统知道我现在做到哪一步」之间,到底差了什么?

用户给了四步任务,模型自然列出四步。为什么这还不够?

  • A:模型说得不够详细,应该列成八步
  • B:列出步骤不代表系统知道自己正在第几步,更不代表步骤真的完成
  • C:用户应该自己去做
  • D:四步太多了,应该合并

编号列表不是状态机

「请先规划再执行」是一句很好的系统提示,但它缺一样东西:可检查的强约束。 列表里的字只存在于模型的回复里,程序拿不到「第 2 步开始了吗」「第 1 步失败还允不允许第 2 步」这些事实。

把计划外置,是从「说过」跨到「保存过」的第一步:计划不再是模型嘴里的一段话,而是 Harness 里一份被保存、可查询、可更新的状态。教学仓库这一步把计划写成了 TODOS

但「外置」只是起点。一份能支撑生产的计划,每一条至少要回答四个问题:

  • 状态pending / in_progress / completed / failed / cancelled,当前是哪一种;
  • 依赖:哪些步骤必须先完成(depends_on),而不是靠数组顺序猜;
  • 证据:标成 completed 时,凭什么——测试、文件校验、检索结果还是人工批准;
  • 尝试attempt 是首次执行还是重试,重新规划时保留旧版本(plan_version)。

这就是第一课:编号列表只回答「我打算做几件事」,状态机回答「现在做到哪、能不能做、凭什么说做完」。

把计划变成 Harness 里的状态

一个能用的规划系统,至少由四块组成,每一块都有「不能交给谁」的边界:

组件职责不能交给谁
Planner提出步骤、顺序和重新规划建议不负责证明任务已完成
Plan Store保存版本、步骤、状态、依赖、证据引用不从自然语言猜状态
Transition Guard校验合法迁移、依赖、并发约束不能只靠 Prompt
Completion Guard结束前核对残单与环境证据不能相信模型一句「完成」

最小步骤模型长这样——它比「字符串数组」多出四个字段:

python
from dataclasses import dataclass, field

@dataclass
class PlanStep:
    id: str
    content: str
    status: str = "pending"
    depends_on: tuple[str, ...] = ()
    evidence: list[str] = field(default_factory=list)
    attempt: int = 0

状态迁移必须由代码把关,而不是靠模型自觉。教学仓库里「同一时间只能有一个 in_progress」这类约束,就是让代码拒绝非法回退和跳转:

python
VALID = {
    "pending": {"in_progress", "cancelled"},
    "in_progress": {"completed", "failed"},
    "failed": {"in_progress", "cancelled"},
    "completed": set(),
    "cancelled": set(),
}

def transition(step, target):
    if target not in VALID[step["status"]]:
        raise ValueError(f"illegal transition: {step['status']} -> {target}")
    step["status"] = target

为什么 depends_on 比「数组里排在前面」更可靠?因为数组顺序是巧合——一旦重新规划、并行执行或有人调整顺序,先后关系就错了。显式声明依赖,才是代码能核对的约束。

两个步骤的先后关系,为什么用 depends_on 表达比「数组里排在前面」更可靠?

  • A:数组顺序读起来更直观
  • B:数组顺序是模型生成的,天然不可信
  • C:依赖被显式声明,不依赖数组位置的巧合;重新规划或并发时不易错位
  • D:depends_on 能让代码更短

要能停下、能换路、能证明

规划解决的不是「模型不会想步骤」,而是复杂任务的三个工程问题。

进度可见。 用户、运行时和下一次模型调用,都能读到当前步骤。用事件而不是形容词:plan.step_started / plan.step_completed / plan.step_failed 这种过去式事件表示「事实已经发生」,可以重放、可以审计;而 planning 只是个转圈动画,不能当证据。

执行可约束。 前置步骤没完成,后续步骤不能偷偷开始。这一步由 Transition Guard 和 depends_on 共同保证。

结束可核验。 模型说「完成」的时候,Completion Guard 检查两件事:还有没有未完成项(残单),以及标成 completed 的步骤有没有证据(unproven)。只满足其中一件都不算数:

python
def completion_guard(steps):
    unfinished = [
        item for item in steps
        if item["status"] not in {"completed", "cancelled"}
    ]
    unproven = [
        item for item in steps
        if item["status"] == "completed" and not item.get("evidence")
    ]
    return {"ok": not unfinished and not unproven,
            "unfinished": unfinished,
            "unproven": unproven}

还有一个容易被忽略的点:失败时不要覆盖旧计划。 首选路径 API 调用失败,应该保留失败轨迹,再生成 Plan v2,并发出 plan.replanned(from=1, to=2, reason=...)。这样调试时才能回答「为什么换了路」,而不是只看到最终清单。

完整流程长这样——模型负责「建议下一步」,Harness 负责「这一步能不能执行、完成证据够不够」:

一个步骤被标成 completed,但没有任何 evidence。Completion Guard 应该怎么做?

  • A:放行,因为模型明确说完成了
  • B:返回 unproven,把它视作未完成,继续循环
  • C:自动给这个步骤编一条 evidence
  • D:把整个计划作废重来

Plan 与 Goal 的边界

最后分清两个词。它们常被混用,但寿命和结束条件完全不同:

概念回答的问题典型寿命结束条件
待办状态当前有哪些执行项、各自状态如何一个 Run 或一次 Plan条目全部终态
Plan为达成目标选择的步骤和依赖可被重新规划替换通过或被新版本取代
Task可被执行与验收的一件工作一次委派或工作单元输出契约满足
Goal最终成功是什么可跨 Turn、Run、Plan成功标准有证据

边界只有一句话:Goal 定义成功,Plan 描述尝试。 Plan 可以因新观察而重排;Goal 不应因为某次 Plan 失败就悄悄改变。首选方案挂了、换条路继续走,改变发生在 Plan 层,Goal 一个字都没动。

这条边界也提醒你:本章的 Completion Guard 只能证明「当前 Plan 没有残单」。它证明不了「长期目标真正达成」——那是第 9 章 GoalTracker 的活。Mini Emperor 里的 GoalTracker 已经在做一件事:连续几轮没有进展就暂停,防止 Agent 在一个无望的方向上无限打转。

首选方案 API 调用失败,Agent 改用本地资料继续推进。这次改变发生在哪一层?

  • A:Goal 层——目标变了
  • B:Plan 层——换了一条路径;Goal 不应因为 Plan 失败而悄悄改变
  • C:用户记忆层——用户偏好更新了
  • D:Skill 发布层——发布了新 Skill

动手:让两个失败发生,再用一个测试拦住

理论和手感之间隔着一道墙,亲手撞一次才会留下印象。下面两个实验都是故意做错的

失败一:验证「模型自述」当作验证「环境产物」。

把下面这段直接复制到课程仓库根目录运行:

bash
uv run --python 3.12 python - <<'PY'
plan = ["检索资料", "写 Markdown", "生成 HTML"]
answer = "都完成了"
print("模型回答:", answer)
print("计划文本:", plan)
print("HTML 是否存在:无法从这两项状态判断")
assert answer == "都完成了"
PY

程序会成功退出——但这个「测试通过」本身就是失败证据:它只验证了模型嘴里说的「都完成了」,没有验证任何环境产物。网页文件存不存在、对不对,从这两项状态里根本看不出来。

失败二:把步骤标绿,但没有任何证据。

python
steps = [
    {"id": "write-md", "status": "completed", "depends_on": [], "evidence": []},
    {"id": "render-html", "status": "completed", "depends_on": ["write-md"], "evidence": []},
]

def completion_guard(steps):
    unfinished = [s for s in steps if s["status"] not in {"completed", "cancelled"}]
    unproven = [s for s in steps if s["status"] == "completed" and not s.get("evidence")]
    return {"ok": not unfinished and not unproven,
            "unfinished": unfinished,
            "unproven": unproven}

print(completion_guard(steps))   # ok 是 False,unproven 有两项

界面上一片绿,但只要没有 evidence,Completion Guard 就把它当未完成。「状态绿了」不等于「产物出现了」。

跑完后用现成测试验证「无进展就暂停」这个停止机制真的有效——它是 Goal 层防止空转的闸:

bash
cd "$(git rev-parse --show-toplevel)"
uv run --python 3.12 --extra dev pytest \
  mini-emperor/backend/tests/test_evolution.py::test_goal_pauses_after_three_no_progress_cycles \
  -v

如果它不过,按这个顺序排查:

  • 计数没走 → record(progress=False) 有没有被调用;
  • 该停没停 → 暂停条件和 max_no_progress 阈值;
  • 暂停后还在跑 → 调用方有没有检查 status == PAUSED

门禁·代码:跑通上面这条测试,把输出保存为证据。

本章小结

这一章把一句话「我会分四步做」变成了一个状态机。四件事连起来看:

编号列表不是状态机。 列表只回答「打算做几件事」;状态机回答「做到哪、能不能做、凭什么说完」。从「说过」到「保存过」,是计划外置的第一步。

状态由代码把关,不由模型自觉。 Planner 提建议、Transition Guard 批迁移、Completion Guard 核证据——每一块的信任边界都要写清楚。模型适合提出哪类更新,代码必须把守哪类更新。

完成要能证明,失败要留痕迹。 completed 必须绑定 evidence;重新规划要保留旧版本和 plan.replanned 原因,而不是覆盖。

Goal 定义成功,Plan 描述尝试。 换路径发生在 Plan 层;Goal 不应因为某次 Plan 失败就悄悄改变。

一句话记住这一章:

列出四步不等于系统知道自己正在第几步,更不等于四步真的完成。把「打算怎么做」变成可验证状态——有 ID、有状态、有依赖、有证据、有闸门。

如果上面这些话你自己也能说得出来(不看资料),就说明这一章的知识已经连成一片。

复述与证据

合上资料,画出这一章的完整流程:Planner 提计划 → Transition Guard 校验 → 执行 → 收证据 → Completion Guard 核验 → 结束。标出每一步里「谁建议、谁批准、谁证明」。

再追问三个问题:

  1. depends_on 与「数组顺序」有什么不同?
  2. 为什么重新规划要保留旧版本,而不是直接覆盖?
  3. 待办项全绿,为什么仍不能证明 Goal 完成?

门禁·证据:提交一条运行轨迹,标出 plan_version、当前步骤、最近 evidence 和不能结束的原因。

24 小时后:不看资料,写一个 30 行以内的 transition()completion_guard(),覆盖非法跳转与无证据完成。

有兴趣可以继续看

以下资料按优先级排。至少完成一项 P0,并保存完成证据。

视频P015 分钟 · 视频 04 [00:01:42–00:04:37],再看 [00:07:19–00:08:10]
预计
15 分钟
位置
视频 04 [00:01:42–00:04:37],再看 [00:07:19–00:08:10]
阅读任务
画出状态模型、工具更新与 Completion Guard;标出哪些完成状态来自模型、哪些来自环境。
完成证据
画一张时间线,标出每次状态更新与文件验收。
教学仓库P035 分钟 · Step 08,重点 update_todos()、Tool Schema、主循环结束分支
预计
35 分钟
位置
Step 08,重点 update_todos()、Tool Schema、主循环结束分支
阅读任务
运行一次多步骤任务,打印 stop_reason 与当前清单文本。
完成证据
保存至少三次状态快照和一次残单回推。
书与实验P045 分钟 · ai-agent-book 第 2 章 Agent 状态栏 + chapter10/staged-system-prompt
预计
45 分钟
位置
ai-agent-book 第 2 章 Agent 状态栏 + chapter10/staged-system-prompt
阅读任务
运行 demo.py --list-stages,观察阶段状态机、工具门禁和回退。
完成证据
写出三条允许转换和一条回退,说明 review 怎么退回 implementation。
论文P150 分钟 · ReAct: Synergizing Reasoning and Acting in Language Models
预计
50 分钟
位置
ReAct: Synergizing Reasoning and Acting in Language Models
阅读任务
区分模型生成的 reasoning 与环境的 observation;哪一层适合保存为计划证据。
完成证据
给本章最小实现画一条 ReAct 轨迹,标出一次重新规划的触发 observation。

本文提到的代码与出处(有兴趣逐条核对时用):

← 上一章:记忆与 Skill 进化 · 下一章:Subagent

Markdown 是唯一内容源,HTML 由构建流程生成。