docs: make workflow and goal lessons harness-first

This commit is contained in:
Haoran
2026-07-30 20:05:33 +08:00
parent cb8fae1bdd
commit 4bc33ec858
12 changed files with 115 additions and 207 deletions

View File

@@ -8,8 +8,6 @@ s01 → ... → s20 → s21 → `s22`
>
> **Harness 层**: 目标闭环 — 在轮次收尾处,加一道程序控制的完成闸门。
> **来源边界:** 本章产品细节来自对 Claude Code 2.1.177 的 clean-room 行为重建。后续版本可能更改名称与限制;`code.py` 是离线教学模型,不是产品源码复制。
---
从 s01 到 s21一轮对话怎么结束模型不再发 `tool_use`,循环就直接 `return` 了。一次性任务这么干没问题,做完就停。
@@ -20,7 +18,7 @@ s01 → ... → s20 → s21 → `s22`
## /goal每轮收尾加一道闸门
输入 `/goal <条件>` 就设了一个会话级的停止条件。程序把它存成当前活跃目标,每轮结束后,用一个独立的轻量小模型当判断器,看对话记录里的可信证据够不够满足条件。不够,闸门就把这次结束拦住,塞一条"继续干"的提示进下一轮;够了,就清除目标,标记完成。
输入 `/goal <条件>` 就设了一个会话级的停止条件。程序把它存成当前活跃目标,每轮结束后,判断器检查对话记录里的可信证据够不够满足条件。不够,闸门就把这次结束拦住,塞一条"继续干"的提示进下一轮;够了,就清除目标,标记完成。
![Goal Loop 总览](images/goal-loop-overview.svg)
@@ -40,8 +38,6 @@ if not has_tool_use(response):
这道闸门是程序自己控制的。不是模型自己约束自己,模型甚至不知道有这么一道闸门,它只是收到了下一轮的输入,接着干就是了。
> 真实 Claude Code`/goal` 是会话级的 Stop hook受工作区信任和 hook 限制控制;代码里有 `active_goal`、`goal_status`、`goal_met`、`tengu_goal_achieved` 这些标记。
## 设目标:证据从命令之后开始算
`set_goal` 会存一个活跃目标:目标文本、最大轮数预算、计数器,还有 `start_index`——也就是证据窗口的起点。它取当前对话记录的长度,所以 `/goal` 这行命令本身在窗口外面。这是第一道防线:命令自己不能证明自己完成了。
@@ -55,8 +51,6 @@ def set_goal(self, objective, max_turns=20):
}
```
> 真实 Claude Code`GoalRuntime.setGoal()` 存活跃目标、起始位置、计数器和预算;提交后再 `resetEvidenceStart()` 把窗口对齐到命令之后。
## 判断器:只信实打实的证据
这是整个机制最核心的地方。判断器不看整段对话,只看证据窗口里来自可信来源的消息。三层过滤,把"嘴上说完成了但不算数"的内容全挡在外面:
@@ -79,9 +73,7 @@ def evidence_text(self):
效果很明显:同样一句 `tests passed`,你打字说的不算,后台任务通知带回来的才算。模型糊弄不过去,它没法靠自己说一句"我做完了"就把目标判成完成。这是全课程反复出现的那条信任边界的最后一次登场s16 说协议靠字段不靠理解s19 说注解是申报、申报可以撒谎s22 说完成证据只看来源不看内容。
教学版里 `goal_satisfied()` 确定的关键词匹配;真实版会把证据窗口交给一个轻量小模型来判断
> 真实 Claude Code判断器是和干活的模型分开的轻量小模型标记是 `evaluatorModel`、`default small fast model`),判断对话里的证据,不是随便什么文本都信。
最小版的 `goal_satisfied()` 使用确定的关键词匹配,让演示保持离线和可复现。生产级 harness 可以把这条策略替换成独立的轻量判断模型,但仍然保留相同的可信证据边界
## 闸门三态:完成/继续/超预算
@@ -106,8 +98,6 @@ def evaluate_after_turn(self):
那条"继续干"的提示里特意写了"别把这条提醒当成完成证据",连提醒本身都被排除在证据之外。三层防误判就齐了:命令文本不算、提醒文本不算、普通聊天文本不算。预算则是 s11 教过的老规矩:任何自动重试的机制都得有上限,不然一个永远判不满足的目标就是个烧钱的永动机。
> 真实 Claude Code`evaluateAfterTurn` 会发 `goal_evaluated` 事件,按结果完成/塞继续提示/拦截;默认预算是 20 轮。
## 继续提示和外部异步消息分开走
继续提示进的是同一个 `CommandQueue`,但它和外部异步事件(任务完成通知、监控行)不是同一种消费方式。`dequeue` 带个开关:消费外部收件箱的时候,默认跳过目标的继续提示。
@@ -121,9 +111,7 @@ def dequeue(self, include_goal_continuations=True):
return None
```
为什么要分开?真实模型测试的时候出过一个 bug模型把继续提示当成外部通知一起消费了,结果后台证据还没到,就提前把目标判成完成了。分开之后,目标的推进是显式的一步,不会被异步事件带着走。
> 真实 Claude Code`drainCommandQueue` 默认 `includeGoalContinuations=false`,把目标继续提示和外部异步收件箱的消费分开。
为什么要分开?如果同一个消费者把继续提示外部通知一起取走,后台结果还没到,提醒文本就可能被误当成新证据。分开之后,目标的推进是显式的一步,不会被异步事件带着走。
## 跑起来看看