/goal 命令:给 Agent 设定目标¶
对应原站:/goal 命令:给 Agent 设定目标(会员内容;本章为独立原创重写)
一句话结论:
/goal本质上是一个只在当前会话生效的 prompt 型 Stop hook。每轮结束时,它把「验收条件 + 到目前为止的对话」交给一个不带工具的小模型去判卷。判「没达成」,理由就变成下一轮的指令;判「达成」或「不可能达成」,目标就自动清除。干活的模型和验收的模型是分开的,这一点是整个设计的关键。
脚本判不了的「完成」¶
上一章写过一个 Stop hook:transcript 里没出现 passed 就不许停。这类检查很可靠,但能检查的东西很有限。
你可能会问:「测试覆盖了边界情况没有」「迁移后的每个调用点都改了没有」「文档补齐了没有」,这些判断怎么写成 shell 脚本?
写不出来。这类判断需要「读懂」,而读懂恰好是 LLM 擅长的事。
那让干活的 Claude 自己判断「我做完了没」不就行了?问题就出在这里。Claude 本来就是因为「觉得自己做完了」才停下来的。让同一个上下文里的同一个模型再问自己一遍,大概率还是同样的答案。
/goal 的做法是请一个外人来验收。
一个循环、两个角色¶
官方文档(https://code.claude.com/docs/en/goal)原话说 /goal 是「a wrapper around a session-scoped prompt-based Stop hook」。把它拆开画出来:
图里有两个角色:
- Worker:主会话里的 Claude,带着全部工具和完整上下文干活。
- Judge:文档里叫「small fast model」,就是 Haiku 那一档(可以用
ANTHROPIC_DEFAULT_HAIKU_MODEL换掉,但这个变量是全局的,所有后台小任务都会跟着换)。Judge 不调用任何工具,只能读对话里已经出现过的内容。
Judge 每次只给三种结论之一,并附一句理由:not yet met、met、impossible。这和 prompt 型 hook 的返回格式 {"ok": false, "reason": "...", "impossible": true} 是一一对应的(见官方 hooks-guide 的「Prompt-based hooks」一节)。
打个比方:Worker 是装修队,Judge 是甲方。但这个甲方不进屋,只看装修队发到群里的照片和视频。
这个比方点出了两件事:
- 甲方没法自己拿尺子去量,所以验收条件必须写成「照片能证明的」形式。
- 装修队发什么,甲方就只能看到什么。
这个比方在一个地方不成立:真实的 Judge 每轮都是从零开始判,没有「上次已经验过厨房」的记忆。每次它都会重新读一遍完整的条件和对话。
回到主线:Judge 也是一个按概率预测下一个 token 的模型,它不比 Worker 聪明。它的优势在于上下文干净:没有「我刚花了 20 轮写这段代码」的沉没成本,只看条件和证据。换了一个上下文,就换来了一次独立的判断。代价是每轮多一次小模型调用,文档说这部分花费「通常可以忽略」。
几个容易想错的行为细节¶
下面这些都出自官方 goal 文档,有几条和直觉不一样:
- 设目标会立即开跑。
/goal <条件>本身就把条件当作第一轮指令,不需要再发一条 prompt。一个会话同时只能有一个目标,新设的会覆盖旧的。 - 条件最长 4,000 字符。 想限制时长,就直接写进条件里,比如「或者跑满 20 轮就停」。这个子句也由 Judge 读对话来判断,Claude 每轮会汇报自己的进度。
- 后台任务会推迟评审。 本轮结束时,如果还有子 agent 或后台 shell 在跑,这一轮就不评审。后台任务卡住 30 分钟后会触发一次 check-in,之后的间隔依次翻倍(上限是首次间隔的 4 倍),可以用
CLAUDE_CODE_GOAL_CHECKIN_MINUTES调整。 - 光说不做会被叫停。 Claude 如果连续几轮只「回应」Judge、却不调用任何工具,循环就会停下来并打印警告,目标保留,等你下一次输入后再继续。底层机制就是上一章讲的 Stop hook block cap。
- 错误分两类处理。 鉴权失败、额度耗尽、自动压缩也救不回来的上下文溢出、模型不可用,这四类错误会直接清除目标。其它错误(过载、断线、限流)只会让目标重试或暂停。
- 它属于 hooks 体系。 所以要受工作区信任规则的约束;
disableAllHooks: true时/goal不可用,命令会告诉你原因。 /goal不改权限模式。 想让它无人值守地跑,要配合 auto mode。文档的说法是:auto mode 去掉了「每次调用工具都要确认」,/goal去掉了「每一轮都要你发话」。
至于 Judge 拿到的 prompt 具体长什么样,「对话」是全文还是摘要,文档都没有写。下面 demo 里 renderForJudge()(Python 版 render_for_judge())的格式是我的推测。依据是:文档说发送的是「the condition and the conversation so far」,评审不调工具,只依据 Claude 已经输出到对话里的内容。
先跑起来:离线复刻这个循环¶
code/agent-node/goal_demo/goal_loop.ts(Python 版 goal_loop.py)用一百来行代码复刻了上面那张状态图。Worker 和 Judge 都是可以替换的函数(Node 版允许它们返回 Promise,所以 runGoal 是 async 的):
- 离线模式:
scriptedWorker(scripted_worker)按剧本返回每一轮的结果,keywordJudge(keyword_judge)只在对话里找0 failed和lint clean两个证据。 - 真实模式:
makeLlmJudge(new Anthropic())(Python 版make_llm_judge(anthropic.Anthropic()))发一次无工具的claude-haiku-5-5调用,用output_config.format约束模型输出{status, reason}这个 JSON。本仓库只用 fake client 测过,没有发真实请求。
剧本是这样的:第 1 轮测试还有失败;第 2 轮起了后台 e2e;第 3 轮修好测试;第 4 轮跑 lint。离线 fake 模型,真实输出:
跑了 4 轮,只评审了 3 次,因为第 2 轮有后台任务没结束。第 3 轮的 reason 只剩「lint clean」,Worker 下一轮就照着这个去跑 lint。整个循环的核心就这么几行:
说穿了,就是在 agent 循环外面再套了一层 while,退出条件交给另一个模型来判断。reason 被追加到 messages 的末尾,和 Stop hook 返回 decision: block + reason 是同一种做法。
真实 Judge 的调用长这样(makeLlmJudge / make_llm_judge 里的代码,没有对真实 API 执行过):
测试:node --test 'goal_demo/*.test.ts',结果 ℹ pass 4(Python 版 uv run pytest -q goal_demo,结果 4 passed)。其中两条覆盖了「连续无工具调用 → stalled」和「impossible → 清除目标」。还有一条断言请求里没有 tools 字段,评审不许动手,这一点写成了测试。
/goal、Stop hook、/loop 怎么选¶
| 下一轮什么时候开始 | 什么时候停 | 作用范围 | |
|---|---|---|---|
/goal |
上一轮结束(或 check-in / 自动重试到期) | Judge 判 met / impossible,或你执行 /goal clear |
只在当前会话 |
| command 型 Stop hook | 上一轮结束 | 你的脚本决定 | settings 覆盖到的所有会话 |
| prompt 型 Stop hook | 上一轮结束 | 你写的 prompt + 模型决定 | 同上 |
/loop |
固定时间间隔 | 你叫停,或 Claude 自己判断做完了 | 当前会话 |
我的观点:能写成脚本的验收就别交给 Judge。 测试通过、lint 干净、文件数达标,这些用 command 型 Stop hook 判断,确定性高,也不花 token。
/goal 真正有价值的地方,是那种「需要读懂才能判断,但又说得清」的条件,比如「设计文档第 3 节列的验收标准逐条满足」。最理想的写法是两者叠加:条件里写明「npm test exit 0」,让 Claude 必须把测试输出贴进对话,Judge 再在这些证据之上判断那些「软」条件。
对照你的环境¶
- 你在
~/.claude/CLAUDE.md里写过「完成前跑命令贴真实输出」这条铁律。它和/goal的设计正好互补:Judge 只能读对话,贴出来的真实输出就是 Judge 唯一的证据。没贴输出的「已完成」,在 Judge 看来跟没做一样。 - 你的
Stop事件上目前只挂了cc-status,用来刷新 iTerm 状态。/goal的 Judge 是会话级的另一个 Stop hook。两者同时存在时,按 hooks 文档「命中的 hook 并行执行」的规则,状态刷新和评审互不影响。不过这一点我没有在真实会话里观察过(推测)。 webapp的提测流程里有一步是「受影响的 suite + lint」,这一步很适合写成 goal 条件:/goal apps/web 受影响的 vitest 全绿且 eslint 无 error,不修改任何 *.test.ts 以外的测试配置,或 15 轮后停。
常见坑¶
- 条件写得太虚。 比如「把代码改好」,Judge 没有可以对照的证据,结果要么永远 not met,要么被一句「我改好了」糊弄过去。条件要包含一个可测量的终态,再加上「怎么证明」。
- 以为 Judge 会自己去跑测试。 它不会。它不调用工具,只看对话里已经有的东西。
- 不写约束子句。 「测试全过」最省事的达成方法之一,就是把失败的测试删掉。条件里要写上「不许修改哪些东西」。
- 长时间的后台任务把评审一直往后拖。 子 agent 没跑完,这一轮就不评审,这时候别以为
/goal卡死了。可以执行/goal(不带参数)查看已评审的轮数和最近一次的理由。 - 用
-p跑时看起来像卡住了。 默认的文本输出要等循环结束才打印结果。想看到过程,加上--output-format stream-json --verbose。
练习¶
- 改
keywordJudge(keyword_judge):对话里同时出现0 failed和「删除了测试」时,返回not_met,理由是「不允许删除测试」。补一条测试(node:test或 pytest)。 - 不用
/goal,在测试仓库的.claude/settings.local.json里写一个type: "prompt"的 Stop hook,复现同样的「测试全过才许停」。它和/goal在作用范围、能不能用/goal clear撤销这两点上有什么区别? - (开放题)Judge 和 Worker 用同一族模型,会不会有「同一个模型族的盲区」?如果让 Codex 来当 Judge(异源评审,就像你的
codex-review),要改goal_loop.ts(goal_loop.py)的哪个接口?这样做的收益和成本各是什么?
延伸阅读¶
- 官方:Keep Claude working toward a goal:https://code.claude.com/docs/en/goal
- 官方:Prompt-based hooks:https://code.claude.com/docs/en/hooks-guide
- 本库:Hooks、Sub Agent(背景子 agent 为什么会推迟评审)、Harness Engineering