Skip to content

你从第 09 章进入,还缺 04/05/08 前置。

自测代码证据锁定

第 9 章:Goal 目标驱动——持续推进,但绝不盲目循环

← 上一章:Hooks 与评估 · 下一章:Mini Emperor 综合项目

开场:装修队说「做完了」,你信吗?

你请了一支装修队。开工前签了合同,上面写得很清楚:三个月交付;验收标准——水电走线合规、墙面平整度两米内误差不超过三毫米、甲醛检测合格。

装修队干了两个多月,突然来电话:「我们做完了,你来看看吧。」

你到了现场,客厅确实在动工,墙刷了一半,地上堆着电线。装修队拍着胸脯说:「你看我们天天在干活,肯定没问题。」但这时候你真正想问的是:「忙」和「按合同完成」,是一回事吗?

你拿着合同逐项对:水电打压测试的记录在哪?墙面用两米靠尺量了吗?甲醛检测报告呢?装修队说「我们正在弄」——但你很清楚,没有这些证据,这句话等于没说

反过来,装修队也会提意见:吊顶设计方案太贵,能不能换一种?可以,方案可以改。但合同里「甲醛检测合格」这条,不能因为他们改方案就悄悄删掉。合同是稳定的北极星,方案是可变的路线。

这一章就把「装修合同」翻译成 Agent 工程的 Goal。本章结束时,你能回答:为什么「持续干活」不等于「朝目标推进」,以及连续三次没进展时为什么必须暂停?

装修队「天天在干活」,为什么还不能说目标完成了?

  • A:因为他们没有按合同逐项验收——忙不等于完成,要有证据
  • B:因为他们干得太快
  • C:因为合同没写验收标准
  • D:因为进度条没有动

五个词,五个生命周期

聊「目标驱动」最容易栽的坑是术语混用。五个概念,各自的生命周期完全不同:

概念精确定义生命周期示例
Turn一次用户/模型输入到回复或工具循环结束秒到分钟「检查发布状态」
RunRunner 的一次执行实例,有唯一 ID 和事件序列可含多个 turnrun-202
Task可分派、可验证的工作单元完成或失败后关闭「运行保留集评估」
Plan当前任务顺序与依赖,是可修改假设遇到新证据即可调整「先评估,再批准,再发布」
Goal稳定的结果、范围、成功判据和边界直到完成、取消或终止「Skill 1.1.0 可安全发布」

一句话记住五个词:

Turn 是交互,Run 是实例,Task 是工作,Plan 是路线,Goal 是验收合同。

混用会带来一连串假判断:

错误说法实际问题
「这一轮就是目标」一个 turn 不足以承载长期验收
「程序启动了一次,所以任务完成」run 只是执行实例,不是成功证据
「计划写完了,目标就完成了」plan 是假设路径,不是结果
「等待审批也是失败」waiting 是可恢复状态,不应丢失上下文
「Agent 还在输出,所以有进展」活动量不等于新证据

第 4 章你已经见过 Plan 与 Goal 的边界,这里把它放进五个词的坐标系里看:Plan 可以重排,Goal 不能被 Agent 偷偷改。 运行时间再长、输出再多、调用工具再频繁,都不能证明质量——这一章就是给「完成」加上证据门槛。

run-202 是下面哪个概念?

  • A:一次用户与模型的交互(Turn)
  • B:Runner 的一次执行实例,有唯一 ID 和事件序列(Run)
  • C:一份验收合同(Goal)
  • D:一个可修改的路线(Plan)

Goal Contract:把愿望写成能验收的合同

「我想发布一个客服 Skill」是愿望,不是 Goal。工程化的 Goal 至少是一份带范围、成功判据和停止边界的合同:

python
goal = {
    "objective": "发布 customer-support 1.1.0",
    "scope": ["candidate 1.1.0", "Skill Hub", "remote MCP"],
    "success_checks": [
        "deterministic_tests_passed",
        "safety_regressions == 0",
        "approved_by is not None",
        "published_version == 1.1.0",
    ],
    "limits": {
        "max_no_progress_cycles": 3,
        "max_cost": 5.0,
        "deadline": "2026-08-03T18:00:00+08:00",
    },
}

四个字段各管一件事:objective 说清楚要达成什么;scope 圈定边界,防止目标悄悄膨胀;success_checks 是可逐项核对的验收清单;limits 给推进上锁——最多连续几次无进展、最多花多少钱、最晚什么时候。

Goal 的成功条件应尽量由测试、API 状态、校验和或人工批准记录来证明,不能只依赖模型自评。「我说做完了」是最常见的一种幻觉来源,上一章的 Verifier 就是为它准备的。

首选方案失败,Agent 换了一条路继续走。哪一层变了?

  • A:Goal 层——验收标准变了
  • B:Task 层——但 Goal 也会被偷偷改
  • C:Plan 层——路线可以改;Goal 不应因 Plan 失败而改变
  • D:Turn 层——但 Goal 同样会变

做、验、找缺口、重规划

有了合同,推进就不是「想到哪做到哪」,而是一条受控循环:

text
Do:执行当前 Plan 的下一项
Verify:收集独立证据
Gap:一次只选择一个最关键缺口
Replan:调整路线,但不偷偷改变 Goal

Verify 是这条循环的发动机。 每做一步,都要拿合同对一下:哪个成功检查还没满足?有没有证据证明它满足了?「做了」不自动等于「满足了」,中间必须过验证。

Gap 一次只挑一个最关键缺口。 一堆缺口同时摆在面前,模型很容易挑简单的那个先做——但这未必是通往完成的唯一瓶颈。优先级应该来自 Goal Contract 的 success_checks,而不是模型当下的舒适区。

Replan 改的是 Plan,不是 Goal。 这是本章最硬的一条边界:验收标准若要改变,应由人明确修改 Goal Contract,并保存修订记录。Agent 自己不能因为某条路走不通,就把 success_checks 悄悄删掉一条。

什么才算「真进展」

现在回答开场的问题:「忙」到底算不算进展? 不算。进展不是「又执行了一次」,而是至少产生一项真实变化:

  • 一个未满足的成功条件变为满足;
  • 一个未知事实被证据确定;
  • 阻塞原因被解除;
  • 失败空间缩小;
  • 产生了新的、可验证的产物。

反过来,相同命令、相同错误、相同假设连续出现,就是无进展周期。判断进展不能看活动量,要看结构化的证据差异——上一章 Verifier 产出的东西,正好用来做这件事。

暂停、等待、恢复、终止

长期任务一定会被打断:等人工审批、预算见底、用户改主意。每个状态都要有明确的恢复语义:

  • pause:系统主动停止推进,状态和恢复点仍保留;
  • waiting:pause 的特例,明确等待谁、等待什么外部事件;
  • resume:阻塞证据到达后,从检查点继续,而不是重建整个任务;
  • cancel:用户主动取消,不再自动恢复;
  • terminate:预算耗尽、致命错误或安全策略触发的强制结束。

恢复必须匹配条件。 一个因「等待人工批准」进入 waiting 的 Goal,只有当 approval_received 这类证据到达时才能恢复;一个无关事件(比如「构建完成」)不应该能把它唤醒。否则重启后无法说清「这个 Goal 为什么被唤醒了」。

暂停与终止的差别是「还能不能回来」。 暂停保留状态和恢复点,条件满足就能继续;终止是终态,不再自动恢复。Mini Emperor 当前的 GoalTracker 诚实地说,只实现了 running / paused / completed / cancelled,没有 waiting 和 terminated——这是下一步工程扩展,不能把设计稿说成现有能力。

完整状态流转长这样:

推进算法则是把上面的循环再收紧一步——三次无进展就停下来,而不是无限重试

text
读取 Goal Contract
→ 选择一个未满足的 success check
→ 生成或更新 Plan
→ 执行一个 Task
→ 保存 Run/Turn 事件
→ Verifier 产生证据
→ 有进展:清零停滞计数
→ 无进展:计数 +1
→ 连续三次:pause,不再盲目重试
→ 全部检查有证据:complete

下面哪一项才算「真进展」?

  • A:又调用了一次工具
  • B:输出了更多文字
  • C:程序运行时间更长
  • D:一个未满足的成功条件变为满足,或被证据确定的事实

观察当前状态:从事件流看出它在哪

只凭「在跑」看不出 Agent 卡在哪。每次状态变化至少记录一份可诊断的快照:

json
{
  "goal_id": "goal-17",
  "status": "waiting",
  "revision": 4,
  "active_task": "publish-version",
  "run_id": "run-202",
  "last_event_id": "38",
  "no_progress_cycles": 0,
  "waiting_for": "course-maintainer approval",
  "resume_condition": "approved_by != null",
  "satisfied_checks": ["tests", "safety"],
  "unsatisfied_checks": ["approval", "published"],
  "last_evidence": "evaluation-1.1.0.json"
}

五条观察规则能立刻暴露五种坏状态:

  1. status=runningactive_task 为空 → 调度器可能卡住;
  2. status=waiting 但没有 resume_condition → 无法可靠恢复;
  3. status=completedunsatisfied_checks 非空 → 完成判定有 bug;
  4. no_progress_cycles 在真实进展后没清零 → 会误暂停;
  5. resume 后必须产生新的 run,但继续引用同一个 goal 和检查点。

观察一个运行中的 Goal,你要能同时说出:它处于哪个状态、哪个检查还没满足、它等的到底是什么、恢复条件是什么、上次证据在哪里。

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

理论和手感之间隔着一道墙,亲手撞一次才会留下印象。下面两段代码都是故意写错的

失败一:把「活动量」当「进展」。

python
# 错误:活动量不等于新证据
success_checks = {"tests_passed": False, "approved": False}
events = ["retry", "retry", "retry", "retry"]
for i, event in enumerate(events):
    print(f"第 {i + 1} 轮:执行了 {event} → 有进展!")   # ← 错:什么都没变
print("结论:四轮都在忙,但没有任何一项成功检查被满足")

如果系统把这四轮记为 progress=True,说明你的进展判断依赖的是活动量而不是证据。真实的 GoalTracker 只认 record(progress=True)——真进展必须伴随结构化差异。

失败二:任何事件都能把一个 waiting 的 Goal 唤醒。

python
# 错误:恢复不核对条件
goal = {"status": "waiting", "resume_condition": "approval_received"}
def resume(goal, event):
    goal["status"] = "running"          # ← 错:没检查 event 与 resume_condition
    return "已恢复,继续执行"

resume(goal, {"build_finished": True})  # 一个无关事件也把 Goal 唤醒了
print("Goal 状态:", goal["status"])
print("但人工批准从未到达——恢复条件根本没满足")

恢复是门禁,不是开关。resume() 必须核对 resume_condition 与事件类型,否则重启后任何信号都能唤醒不该醒的 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_progress_resets_stall_counter -v

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

  • 计数没清零 → record(progress=True) 有没有把 no_progress_cycles 置 0;
  • 真进展后仍被暂停 → 清零逻辑是否在达到 max_no_progress 之前执行;
  • 测试根本没跑到 → 文件名与函数名是否一致、路径是否在 mini-emperor/backend/tests/ 下。

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

本章小结

这一章把「持续干活」升级成了「朝目标推进」。五件事连起来看:

五个词,五个生命周期。 Turn 是交互,Run 是实例,Task 是工作,Plan 是路线,Goal 是验收合同。混用任何一个,都会产生假判断。

Goal 是合同,不是口号。 objective、scope、success_checks、limits 四件套,让「想做什么」变成「怎么验收」。成功条件尽量用测试、API 状态、校验和或人工批准记录证明,不能只信模型自评。

推进要过循环。 Do → Verify → Gap → Replan,Verify 是发动机,Gap 一次只挑最关键缺口,Replan 改路线但绝不偷偷改 Goal。

进展看证据,不看活动量。 一个成功条件被满足、一个事实被证据确定、阻塞被解除、失败空间缩小、新产物出现——都算进展;相同命令、相同错误连续出现就是无进展。

三次无进展必须暂停。 相同输入、相同错误没有新信息,继续执行只会浪费预算。暂停不等于失败,恢复必须匹配条件;终止才是终态。

一句话记住这一章:

有合同(Goal),有路线(Plan),有证据(Verify),有刹车(三次无进展暂停)——缺一样,Agent 就分不清「在忙」和「在推进」。

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

复述与证据

合上资料,画出五个概念的层级:Turn → Run → Task → Plan → Goal,标出各自的生命周期和结束条件。再画出推进循环:Do → Verify → Gap → Replan,标出证据产生在哪一步、暂停插在哪一步。

再追问四个问题:

  1. 为什么 Plan 可以改,Goal 不能被 Agent 偷偷改?
  2. 证明一次循环产生进展,至少需要什么?
  3. pause、waiting、cancel、terminate 的恢复语义分别是什么?
  4. status=completedunsatisfied_checks 非空,说明什么坏了?

门禁·证据:提交一份 Goal 状态快照(含 statusactive_taskno_progress_cyclessatisfied_checksunsatisfied_checksresume_condition),并说明它为什么还不能标记为完成。

24 小时后:不看资料,写一个 GoalTracker 的最小扩展——加 Goal ID、success checks 与证据字段,并让 complete() 在没有通过证据时抛错。

有兴趣可以继续看

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

视频P010 分钟 · 视频 09 [00:00:00–00:02:19] 长时间运行不等于完成,再看 [00:04:50–00:07:35] 完成与等待人工
预计
10 分钟
位置
视频 09 [00:00:00–00:02:19] 长时间运行不等于完成,再看 [00:04:50–00:07:35] 完成与等待人工
阅读任务
暂停视频,分别写出 Goal、Plan、Verify、Waiting 的职责。
完成证据
提交一份含结果、范围、成功检查和停止边界的 Goal Contract。
教学仓库P025 分钟 · templates/agent/identity.md 的 Goal Tree 规则
预计
25 分钟
位置
templates/agent/identity.md 的 Goal Tree 规则
阅读任务
检查目标树如何拆分子目标,并为每个叶子补一个可执行检查。
完成证据
说明目标分解为什么仍不能替代验证。
书与实验P050 分钟 · chapter4/async-agent/runtime.py 与 demo.py
预计
50 分钟
位置
chapter4/async-agent/runtime.py 与 demo.py
阅读任务
依次运行 state、interrupt、parallel 三个子命令,映射到 run、pause、task。
完成证据
保存三个命令的输出,画出中断前后的状态变化。
工程P030 分钟 · mini-emperor/backend/src/mini_emperor/goals.py
预计
30 分钟
位置
mini-emperor/backend/src/mini_emperor/goals.py
阅读任务
跑通三次无进展暂停和进展清零测试,再列出现有状态机缺口。
完成证据
提交测试输出与一张扩展状态图。
论文P145 分钟 · ../source-notes/papers/classic-papers.md 的 Reflexion 与 Self-Refine
预计
45 分钟
位置
../source-notes/papers/classic-papers.md 的 Reflexion 与 Self-Refine
阅读任务
比较基于轨迹反思与迭代反馈,找出它们仍需外部成功判据的原因。
完成证据
写出一个「模型自评会产生假进展」的反例。

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

← 上一章:Hooks 与评估 · 下一章:Mini Emperor 综合项目

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